Skip to content

Commit

Permalink
Merge branch 'openclimatefix:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
aryanbhosale authored Nov 12, 2024
2 parents 2f973d1 + 97714e5 commit 0de6465
Show file tree
Hide file tree
Showing 15 changed files with 1,695 additions and 84 deletions.
27 changes: 27 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,33 @@
"contributions": [
"code"
]
},
{
"login": "sumana-2705",
"name": "Sumana Sree Angajala",
"avatar_url": "https://avatars.githubusercontent.com/u/110307215?v=4",
"profile": "https://github.com/sumana-2705",
"contributions": [
"code"
]
},
{
"login": "EmilyIsCoding",
"name": "Emily",
"avatar_url": "https://avatars.githubusercontent.com/u/98443131?v=4",
"profile": "https://github.com/EmilyIsCoding",
"contributions": [
"test"
]
},
{
"login": "sergejlazuk",
"name": "sergejla",
"avatar_url": "https://avatars.githubusercontent.com/u/33606741?v=4",
"profile": "https://github.com/sergejlazuk",
"contributions": [
"question"
]
}
],
"contributorsPerLine": 7,
Expand Down
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 1.0.75
current_version = 1.0.81
message = Bump version: {current_version} → {new_version} [skip ci]

[bumpversion:file:pyproject.toml]
Expand Down
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ SOLIS_CLOUD_API_PORT = '13333'
# User needs to add their GivEnergy API details
GIVENERGY_API_KEY = 'user_givenergy_api_key'

# To connect to a Victron system use the environment variables below to set the username and password
#VICTRON_USER=username
#VICTRON_PASS=password

# User needs to add their GivEnergy API details
SOLARMAN_API_URL = 'https://home.solarmanpv.com/maintain-s/history/power'
SOLARMAN_TOKEN = 'user_solarman_token'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
call-run-python-tests:
uses: openclimatefix/.github/.github/workflows/python-test.yml@main
uses: openclimatefix/.github/.github/workflows/python-test.yml@issue/pip-all
with:
# pytest-cov looks at this folder
pytest_cov_dir: "quartz_solar_forecast"
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# Quartz Solar Forecast

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

![image](https://github.com/user-attachments/assets/6563849b-b355-4842-93e4-61c15a9a9ebf)
[![tags badge](https://img.shields.io/github/v/tag/openclimatefix/open-source-quartz-solar-forecast?include_prereleases&sort=semver&color=FFAC5F)](https://github.com/openclimatefix/open-source-quartz-solar-forecast/tags)
[![pypi badge](https://img.shields.io/pypi/v/quartz-solar-forecast?&color=07BCDF)](https://pypi.org/project/quartz-solar-forecast/)
[![ease of contribution: easy](https://img.shields.io/badge/ease%20of%20contribution:%20easy-32bd50)](https://github.com/openclimatefix/ocf-meta-repo?tab=readme-ov-file#overview-of-ocfs-nowcasting-repositories)
[![Python package tests](https://github.com/openclimatefix/open-source-quartz-solar-forecast/actions/workflows/pytest.yaml/badge.svg)](https://github.com/openclimatefix/open-source-quartz-solar-forecast/actions/workflows/pytest.yaml)


The aim of the project is to build an open source PV forecast that is free and easy to use.
The forecast provides the expected generation in `kw` for 0 to 48 hours for a single PV site.

Expand Down Expand Up @@ -35,16 +42,10 @@ A colab notebook providing some examples can be found [here](https://colab.resea

To generate solar forecasts and save them into a CSV file, follow these steps:

1. Navigate to the scripts directory

```bash
cd scripts
```

2. Run the forecast_csv.py script with desired inputs
- Run the forecast_csv.py script with desired inputs

```bash
python forecast_csv.py
python scripts/forecast_csv.py
```

Replace the --init_time_freq, --start_datetime, --end_datetime, and --site_name with your desired forecast initialization frequency (in hours), start datetime, end datetime, and the name of the forecast or site, respectively.
Expand Down Expand Up @@ -258,6 +259,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aayushyatiwari"><img src="https://avatars.githubusercontent.com/u/169576527?v=4?s=100" width="100px;" alt="aayush"/><br /><sub><b>aayush</b></sub></a><br /><a href="https://github.com/openclimatefix/open-source-quartz-solar-forecast/commits?author=aayushyatiwari" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Brohan7524"><img src="https://avatars.githubusercontent.com/u/85754035?v=4?s=100" width="100px;" alt="Rohan Singh"/><br /><sub><b>Rohan Singh</b></sub></a><br /><a href="https://github.com/openclimatefix/open-source-quartz-solar-forecast/commits?author=Brohan7524" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sumana-2705"><img src="https://avatars.githubusercontent.com/u/110307215?v=4?s=100" width="100px;" alt="Sumana Sree Angajala"/><br /><sub><b>Sumana Sree Angajala</b></sub></a><br /><a href="https://github.com/openclimatefix/open-source-quartz-solar-forecast/commits?author=sumana-2705" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EmilyIsCoding"><img src="https://avatars.githubusercontent.com/u/98443131?v=4?s=100" width="100px;" alt="Emily"/><br /><sub><b>Emily</b></sub></a><br /><a href="https://github.com/openclimatefix/open-source-quartz-solar-forecast/commits?author=EmilyIsCoding" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sergejlazuk"><img src="https://avatars.githubusercontent.com/u/33606741?v=4?s=100" width="100px;" alt="sergejla"/><br /><sub><b>sergejla</b></sub></a><br /><a href="#question-sergejlazuk" title="Answering Questions">💬</a></td>
</tr>
</tbody>
</table>
Expand Down
2 changes: 1 addition & 1 deletion examples/inverter_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def main(save_outputs: bool = False):
ts = pd.to_datetime(timestamp_str)

# make input data with live enphase, solis, givenergy, or solarman data
site_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25, inverter_type="enphase") # inverter_type="enphase", "solis", "givenergy", or "solarman"
site_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25, inverter_type="enphase") # inverter_type="enphase", "solis", "givenergy", "solarman" or "victron"

# make input data with nan data
site_no_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25)
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "quartz_solar_forecast"
version = "1.0.75"
version = "1.0.81"
description = "Open Source Solar Forecasting for a Site"
authors = [
{ name = "Peter Dudfield", email = "[email protected]" }
Expand Down Expand Up @@ -43,6 +43,9 @@ package-data = { "quartz_solar_forecast" = ["*"] }

[project.optional-dependencies]
dev = []
# additional vendor-specific dependencies for connecting to inverter APIs
inverters = ["ocf_vrmapi"] # victron
all = ["ocf_vrmapi"]

[tool.mypy]

Expand Down
16 changes: 16 additions & 0 deletions quartz_solar_forecast/forecast.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from datetime import datetime, timedelta
import logging

import pandas as pd

from quartz_solar_forecast.data import get_nwp, make_pv_data
from quartz_solar_forecast.forecasts import forecast_v1_tilt_orientation, TryolabsSolarPowerPredictor
from quartz_solar_forecast.pydantic_models import PVSite

log = logging.getLogger(__name__)

def predict_ocf(
site: PVSite, model=None, ts: datetime | str = None, nwp_source: str = "icon"
Expand All @@ -25,13 +27,27 @@ def predict_ocf(
if isinstance(ts, str):
ts = datetime.fromisoformat(ts)

if site.capacity_kwp > 4:
log.warning("Your site capacity is greater than 4kWp, "
"however the model is trained on sites with capacity <= 4kWp."
"We therefore will run the model with a capacity of 4 kWp, "
"and we'll scale the results afterwards.")
capacity_kwp_original = site.capacity_kwp
site.capacity_kwp = 4
else:
capacity_kwp_original = site.capacity_kwp

# make pv and nwp data from nwp_source
nwp_xr = get_nwp(site=site, ts=ts, nwp_source=nwp_source)
pv_xr = make_pv_data(site=site, ts=ts)

# load and run models
pred_df = forecast_v1_tilt_orientation(nwp_source, nwp_xr, pv_xr, ts, model=model)

# scale the results if the capacity is different
if capacity_kwp_original != site.capacity_kwp:
pred_df["power_kw"] = pred_df["power_kw"] * capacity_kwp_original / site.capacity_kwp

return pred_df


Expand Down
48 changes: 48 additions & 0 deletions quartz_solar_forecast/inverters/victron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Callable

import pandas as pd
from datetime import datetime, timedelta
from quartz_solar_forecast.inverters.inverter import AbstractInverter
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
try:
from ocf_vrmapi.vrm import VRM_API
except:
print("Tried to import ocf_vrmapi but couldnt, Victron Inverter functions won't work")

class VictronSettings(BaseSettings):
model_config = SettingsConfigDict(env_file='.env', extra='ignore')

username: str = Field(alias="VICTRON_USER")
password: str = Field(alias="VICTRON_PASS")


class VictronInverter(AbstractInverter):

def __init__(self, get_sites: Callable, get_kwh_stats: Callable):
self.__get_sites = get_sites
self.__get_kwh_stats = get_kwh_stats

@classmethod
def from_settings(cls, settings: VictronSettings):
api = VRM_API(username=settings.username, password=settings.password)
get_sites = lambda: api.get_user_sites(api.user_id)
end = datetime.now()
start = end - timedelta(weeks=1)
get_kwh_stats = lambda site_id: api.get_kwh_stats(site_id, start=start, end=end)
return cls(get_sites, get_kwh_stats)

def get_data(self, ts: pd.Timestamp) -> pd.DataFrame:
sites = self.__get_sites()
# get first site (bit of a guess)
first_site_id = sites["records"][0]["idSite"]

stats = self.__get_kwh_stats(first_site_id)

kwh = stats["records"]["kwh"]

df = pd.DataFrame(kwh)

df[0] = pd.to_datetime(df[0], unit='ms')
df.columns = ["timestamp", "power_kw"]
return df
3 changes: 3 additions & 0 deletions quartz_solar_forecast/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from quartz_solar_forecast.inverters.mock import MockInverter
from quartz_solar_forecast.inverters.solarman import SolarmanSettings, SolarmanInverter
from quartz_solar_forecast.inverters.solis import SolisSettings, SolisInverter
from quartz_solar_forecast.inverters.victron import VictronSettings, VictronInverter


class PVSite(BaseModel):
Expand Down Expand Up @@ -41,6 +42,8 @@ def get_inverter(self):
return GivEnergyInverter(GivEnergySettings())
elif self.inverter_type == 'solarman':
return SolarmanInverter(SolarmanSettings())
elif self.inverter_type == 'victron':
return VictronInverter.from_settings(VictronSettings())
else:
return MockInverter()

Expand Down
102 changes: 32 additions & 70 deletions quartz_solar_forecast/utils/forecast_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,56 @@
from datetime import datetime, timedelta
from quartz_solar_forecast.forecast import run_forecast
from quartz_solar_forecast.pydantic_models import PVSite
import unittest
from unittest.mock import patch


def generate_all_forecasts(
init_time_freq: int,
start: datetime,
end: datetime,
latitude: float,
longitude: float,
capacity_kwp: float) -> pd.DataFrame:
init_time_freq: int,
start: datetime,
end: datetime,
latitude: float,
longitude: float,
capacity_kwp: float,
) -> pd.DataFrame:

all_forecasts = pd.DataFrame()

init_time = start
while init_time <= end:
print(f"Running forecast for initialization time: {init_time}")
predictions_df = forecast_for_site(latitude, longitude, capacity_kwp, init_time=init_time)
predictions_df['forecast_init_time'] = init_time
predictions_df = forecast_for_site(
latitude, longitude, capacity_kwp, init_time=init_time
)
predictions_df["forecast_init_time"] = init_time
all_forecasts = pd.concat([all_forecasts, predictions_df])
init_time += timedelta(hours=init_time_freq)

return all_forecasts


def forecast_for_site(latitude: float,
longitude: float,
capacity_kwp: float,
model: str = "gb",
init_time: datetime = None) -> pd.DataFrame:
def forecast_for_site(
latitude: float,
longitude: float,
capacity_kwp: float,
model: str = "gb",
init_time: datetime = None,
) -> pd.DataFrame:

site = PVSite(latitude=latitude, longitude=longitude, capacity_kwp=capacity_kwp)
predictions_df = run_forecast(site=site, model=model, ts=init_time)
predictions_df.reset_index(inplace=True)
predictions_df.rename(columns={'index': 'datetime'}, inplace=True)
predictions_df.rename(columns={"index": "datetime"}, inplace=True)
return predictions_df


def write_out_forecasts(init_time_freq, start_datetime, end_datetime, site_name, latitude, longitude, capacity_kwp):
def write_out_forecasts(
init_time_freq,
start_datetime,
end_datetime,
site_name,
latitude,
longitude,
capacity_kwp,
):
"""
Generates forecasts at specified intervals and saves them into a CSV file.
Expand All @@ -58,64 +69,15 @@ def write_out_forecasts(init_time_freq, start_datetime, end_datetime, site_name,
start_date = start.date()
end = datetime.strptime(end_datetime, "%Y-%m-%d %H:%M:%S")
end_date = end.date()
all_forecasts = generate_all_forecasts(init_time_freq, start, end, latitude, longitude, capacity_kwp)
all_forecasts = generate_all_forecasts(
init_time_freq, start, end, latitude, longitude, capacity_kwp
)

output_dir = os.path.join(os.getcwd(), 'csv_forecasts')
output_dir = os.path.join(os.getcwd(), "csv_forecasts")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_file_name = f"forecast_{site_name}_{start_date}_{end_date}.csv"
output_file_path = os.path.join(output_dir, output_file_name)
all_forecasts.to_csv(output_file_path, index=False)
print(f"Forecasts saved to {output_file_path}")

if __name__ == "__main__":
# please change the site name, start_datetime and end_datetime, latitude, longitude and capacity_kwp as per your requirement
write_out_forecasts(
init_time_freq=6,
start_datetime="2024-03-10 00:00:00",
end_datetime="2024-03-11 00:00:00",
site_name="Test",
latitude=51.75,
longitude=-1.25,
capacity_kwp=1.25
)

class TestGenerateForecast(unittest.TestCase):
def setUp(self):
self.site_name = "TestCase"
self.latitude = 51.75
self.longitude = -1.25
self.capacity_kwp = 1.25
self.start_datetime = "2024-03-10 00:00:00"
self.end_datetime = "2024-03-11 00:00:00"
self.init_time_freq = 6
self.output_dir = os.path.join(os.getcwd(), 'csv_forecasts')
self.output_file_name = f"forecast_{self.site_name}_{self.start_datetime[:10]}_{self.end_datetime[:10]}.csv"
self.output_file_path = os.path.join(self.output_dir, self.output_file_name)

@patch('forecast_csv.run_forecast')
def test_generate_forecast(self, mock_run_forecast):
mock_df = pd.DataFrame({
'datetime': [datetime(2024, 3, 10, 0, 0) + timedelta(hours=6 * i) for i in range(4)],
'power_kw': [0.1, 0.5, 0.8, 0.6],
'forecast_init_time': [datetime(2024, 3, 10, 0, 0)] * 4
})
mock_run_forecast.return_value = mock_df

if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)

write_out_forecasts(self.init_time_freq,
self.start_datetime,
self.end_datetime,
self.site_name,
self.latitude,
self.longitude,
self.capacity_kwp
)

self.assertTrue(os.path.exists(self.output_file_path))
os.remove(self.output_file_path)

if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 0de6465

Please sign in to comment.