From beba7ba1bdde72c23e2b23bf04ef4d39a54ee3fa Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Fri, 22 Sep 2023 15:51:19 +1000 Subject: [PATCH 01/30] prototype (#1) * first iteration --- docs/sphinx/source/reference/iotools.rst | 4 + pvlib/iotools/__init__.py | 4 + pvlib/iotools/solcast.py | 359 +++++++++++++++++++++++ pvlib/tests/iotools/test_solcast.py | 96 ++++++ 4 files changed, 463 insertions(+) create mode 100644 pvlib/iotools/solcast.py create mode 100644 pvlib/tests/iotools/test_solcast.py diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index 39bd4f2ad3..9caf81619d 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -43,6 +43,10 @@ of sources and file formats relevant to solar energy modeling. iotools.get_acis_station_data iotools.get_acis_available_stations iotools.read_panond + iotools.get_solcast_tmy + iotools.get_solcast_historic + iotools.get_solcast_forecast + iotools.get_solcast_live A :py:class:`~pvlib.location.Location` object may be created from metadata diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 9935719b29..168a7f4224 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -27,3 +27,7 @@ from pvlib.iotools.acis import get_acis_mpe # noqa: F401 from pvlib.iotools.acis import get_acis_station_data # noqa: F401 from pvlib.iotools.acis import get_acis_available_stations # noqa: F401 +from pvlib.iotools.solcast import get_solcast_forecast # noqa: F401 +from pvlib.iotools.solcast import get_solcast_live # noqa: F401 +from pvlib.iotools.solcast import get_solcast_historic # noqa: F401 +from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401 diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py new file mode 100644 index 0000000000..f63a496dfb --- /dev/null +++ b/pvlib/iotools/solcast.py @@ -0,0 +1,359 @@ +""" Functions to access data from the Solcast API. +""" + +import requests +import pandas as pd +from dataclasses import dataclass + + +BASE_URL = "https://api.solcast.com.au/data" + +@dataclass +class ParameterMap: + solcast_name: str + pvlib_name: str + conversion: callable=lambda x: x + +# define the conventions between Solcast and PVLib nomenclature and units +VARIABLE_MAP = [ + ParameterMap("air_temp", "temp_air"), # air_temp -> temp_air (deg C) + ParameterMap("surface_pressure", "pressure", lambda x: x*100), # surface_pressure (hPa) -> pressure (Pa) + ParameterMap("dewpoint_temp", "temp_dew"), # dewpoint_temp -> temp_dew (deg C) + ParameterMap("gti", "poa_global"), # gti (W/m^2) -> poa_global (W/m^2) + ParameterMap("wind_speed_10m", "wind_speed"), # wind_speed_10m (m/s) -> wind_speed (m/s) + ParameterMap("wind_direction_10m", "wind_direction"), # wind_direction_10m (deg) -> wind_direction (deg) (Convention?) + ParameterMap( + "azimuth", "solar_azimuth", lambda x: abs(x) if x <= 0 else 360 - x + ), # azimuth -> solar_azimuth (degrees) (different convention) + ParameterMap("precipitable_water", "precipitable_water", lambda x: x*10), # precipitable_water (kg/m2) -> precipitable_water (cm) + ParameterMap("zenith", "solar_zenith") # zenith -> solar_zenith +] + + +def get_solcast_tmy( + latitude, longitude, api_key, map_variables=True, **kwargs +): + """Get the irradiance and weather for a Typical Meteorological Year (TMY) at a requested location. + + Derived from satellite (clouds and irradiance over non-polar continental areas) and + numerical weather models (other data). The TMY is calculated with data from 2007 to 2023. + + Parameters + ---------- + latitude : float + in decimal degrees, between -90 and 90, north is positive + longitude : float + in decimal degrees, between -180 and 180, east is positive + api_key : str + To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + map_variables: bool, default: True + When true, renames columns of the DataFrame to pvlib variable names + where applicable. See variable :const:`VARIABLE_MAP`. + kwargs: + Optional parameters passed to the API. See https://docs.solcast.com.au/ for full list of parameters. + + Returns + ------- + df : pandas.DataFrame + containing the values for the parameters requested + + Examples + -------- + get_solcast_tmy( + latitude=-33.856784, + longitude=151.215297, + api_key="your-key" + ) + + you can pass any of the parameters listed in the API docs, like time_zone: + + get_solcast_tmy( + latitude=-33.856784, + longitude=151.215297, + time_zone=10, + api_key="your-key" + ) + + """ + + params = dict( + latitude=latitude, + longitude=longitude, + format="json", + **kwargs + ) + + return _get_solcast( + endpoint="tmy/radiation_and_weather", + params=params, + api_key=api_key, + map_variables=map_variables + ) + + +def get_solcast_historic( + latitude, + longitude, + start, + api_key, + end=None, + duration=None, + map_variables=True, + **kwargs +): + """Get historical irradiance and weather estimated actuals + + for up to 31 days of data at a time for a requested location, + derived from satellite (clouds and irradiance + over non-polar continental areas) and numerical weather models (other data). + Data is available from 2007-01-01T00:00Z up to real time estimated actuals. + + Parameters + ---------- + latitude : float + in decimal degrees, between -90 and 90, north is positive + longitude : float + in decimal degrees, between -180 and 180, east is positive + start : datetime-like + First day of the requested period + end : optional, datetime-like + Last day of the requested period + duration : optional, default is None + Must include one of end_date and duration. ISO_8601 compliant duration for the historic data. + Must be within 31 days of the start_date. + map_variables: bool, default: True + When true, renames columns of the DataFrame to pvlib variable names + where applicable. See variable :const:`VARIABLE_MAP`. + api_key : str + To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + kwargs: + Optional parameters passed to the GET request + + See https://docs.solcast.com.au/ for full list of parameters. + + Returns + ------- + df : pandas.DataFrame + containing the values for the parameters requested + + Examples + -------- + get_solcast_historic( + latitude=-33.856784, + longitude=151.215297, + start='2007-01-01T00:00Z', + duration='P1D', + api_key="your-key" + ) + + you can pass any of the parameters listed in the API docs, for example using the end parameter instead + + get_solcast_historic( + latitude=-33.856784, + longitude=151.215297, + start='2007-01-01T00:00Z', + end='2007-01-02T00:00Z', + api_key="your-key" + ) + """ + + params = dict( + latitude=latitude, + longitude=longitude, + start=start, + end=end, + duration=duration, + api_key=api_key, + format="json", + **kwargs + ) + + return _get_solcast( + endpoint="historic/radiation_and_weather", + params=params, + api_key=api_key, + map_variables=map_variables + ) + +def get_solcast_forecast( + latitude, longitude, api_key, map_variables=True, **kwargs +): + """Get irradiance and weather forecasts from the present time up to 14 days ahead + + Parameters + ---------- + latitude : float + in decimal degrees, between -90 and 90, north is positive + longitude : float + in decimal degrees, between -180 and 180, east is positive + api_key : str + To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + map_variables: bool, default: True + When true, renames columns of the DataFrame to pvlib variable names + where applicable. See variable :const:`VARIABLE_MAP`. + kwargs: + Optional parameters passed to the GET request + + See https://docs.solcast.com.au/ for full list of parameters. + + Returns + ------- + df : pandas.DataFrame + containing the values for the parameters requested + + Examples + -------- + get_solcast_forecast( + latitude=-33.856784, + longitude=151.215297, + api_key="your-key" + ) + + you can pass any of the parameters listed in the API docs, like asking for specific variables + get_solcast_forecast( + latitude=-33.856784, + longitude=151.215297, + output_parameters='dni,clearsky_dni', + api_key="your-key" + ) + """ + + params = dict( + latitude=latitude, + longitude=longitude, + format="json", + **kwargs + ) + + return _get_solcast( + endpoint="forecast/radiation_and_weather", + params=params, + api_key=api_key, + map_variables=map_variables + ) + +def get_solcast_live( + latitude, longitude, api_key, map_variables=True, **kwargs +): + """Get irradiance and weather estimated actuals for near real-time and past 7 days + + Parameters + ---------- + latitude : float + in decimal degrees, between -90 and 90, north is positive + longitude : float + in decimal degrees, between -180 and 180, east is positive + api_key : str + To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + map_variables: bool, default: True + When true, renames columns of the DataFrame to pvlib variable names + where applicable. See variable :const:`VARIABLE_MAP`. + kwargs: + Optional parameters passed to the GET request + + See https://docs.solcast.com.au/ for full list of parameters. + + Returns + ------- + df : pandas.DataFrame + containing the values for the parameters requested + + Examples + -------- + get_solcast_live( + latitude=-33.856784, + longitude=151.215297, + api_key="your-key" + ) + + you can pass any of the parameters listed in the API docs, like + + get_solcast_live( + latitude=-33.856784, + longitude=151.215297, + terrain_shading=True, + api_key="your-key" + ) + + use map_variables=False to avoid converting the data to PVLib's conventions + + get_solcast_live( + latitude=-33.856784, + longitude=151.215297, + map_variables=False, + api_key="your-key" + ) + """ + + params = dict( + latitude=latitude, + longitude=longitude, + format="json", + **kwargs + ) + + return _get_solcast( + endpoint="live/radiation_and_weather", + params=params, + api_key=api_key, + map_variables=map_variables + ) + +def solcast2pvlib(df): + """Formats the data from Solcast to PVLib's conventions. + """ + # move from period_end to period_middle as per pvlib convention + df["period_mid"] = pd.to_datetime(df.period_end) - pd.Timedelta(df.period.values[0]) / 2 + df = df.set_index("period_mid").drop(columns=["period_end", "period"]) + + # rename and convert variables + for variable in VARIABLE_MAP: + if variable.solcast_name in df.columns: + df.rename(columns={variable.solcast_name: variable.pvlib_name}, inplace=True) + df[variable.pvlib_name] = df[variable.pvlib_name].apply(variable.conversion) + return df + +def _get_solcast( + endpoint, + params, + api_key, + map_variables +): + """retrieves weather, irradiance and power data from the Solcast API + + Parameters + ---------- + endpoint : str + one of Solcast API endpoint: + - live/radiation_and_weather + - forecast/radiation_and_weather + - historic/radiation_and_weather + - tmy/radiation_and_weather + params : dict + parameters to be passed to the API + api_key : str + To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + map_variables: bool, default: True + When true, renames columns of the DataFrame to pvlib variable names + where applicable. See variable :const:`VARIABLE_MAP`. + + Returns + ------- + A pandas.DataFrame with the data if the request is successful, an error message otherwise + """ + + response = requests.get( + url= '/'.join([BASE_URL, endpoint]), + params=params, + headers={"Authorization": f"Bearer {api_key}"} + ) + + if response.status_code == 200: + j = response.json() + df = pd.DataFrame.from_dict(j[list(j.keys())[0]]) + if map_variables: + return solcast2pvlib(df) + else: + return df + else: + raise Exception(response.json()) diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py new file mode 100644 index 0000000000..1b836bd7e3 --- /dev/null +++ b/pvlib/tests/iotools/test_solcast.py @@ -0,0 +1,96 @@ +import pandas as pd +from pvlib.iotools.solcast import get_solcast_live, get_solcast_tmy, solcast2pvlib +import pytest + + +@pytest.mark.parametrize("endpoint,function,params,json_response", [ + ( + "live/radiation_and_weather", + get_solcast_live, + dict( + api_key="1234", + latitude = -33.856784, + longitude = 151.215297, + output_parameters = 'dni,ghi' + ), + {'estimated_actuals': + [{'dni': 836, 'ghi': 561, 'period_end': '2023-09-18T05:00:00.0000000Z', 'period': 'PT30M'}, + {'dni': 866, 'ghi': 643, 'period_end': '2023-09-18T04:30:00.0000000Z', 'period': 'PT30M'}, + {'dni': 890, 'ghi': 713, 'period_end': '2023-09-18T04:00:00.0000000Z', 'period': 'PT30M'}, + {'dni': 909, 'ghi': 768, 'period_end': '2023-09-18T03:30:00.0000000Z', 'period': 'PT30M'}] + } + ), +]) +def test_get_solcast_live(requests_mock, endpoint, function, params, json_response): + + mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ + f"&latitude={params['latitude']}&longitude={params['longitude']}&" \ + f"output_parameters={params['output_parameters']}&format=json" + + requests_mock.get(mock_url, json=json_response) + + pd.testing.assert_frame_equal( + function(**params), + solcast2pvlib(pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]])) + ) + + +@pytest.mark.parametrize("endpoint,function,params,json_response", [ + ( + "tmy/radiation_and_weather", + get_solcast_tmy, + dict( + api_key="1234", + latitude = -33.856784, + longitude = 151.215297 + ), + {'estimated_actuals': [ + {'dni': 151, 'ghi': 609, 'period_end': '2059-01-01T01:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 404, 'period_end': '2059-01-01T02:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 304, 'period_end': '2059-01-01T03:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 174, 'period_end': '2059-01-01T04:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 111, 'period_end': '2059-01-01T05:00:00.0000000Z', 'period': 'PT60M'}] + } + ), +]) +def test_get_solcast_tmy(requests_mock, endpoint, function, params, json_response): + + mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ + f"&latitude={params['latitude']}&longitude={params['longitude']}&format=json" + + requests_mock.get(mock_url, json=json_response) + + pd.testing.assert_frame_equal( + function(**params), + solcast2pvlib(pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]])) + ) + + +@pytest.mark.parametrize("in_df,out_df", [ + ( + pd.DataFrame( + [[942, 843, 1017.4, 30, 7.8, 316, 1010, -2, 4.6, 16.4, + '2023-09-20T02:00:00.0000000Z', 'PT30M', 90], + [936, 832, 1017.9, 30, 7.9, 316, 996, -14, 4.5, 16.3, + '2023-09-20T01:30:00.0000000Z', 'PT30M', 0]], + columns=['dni', 'ghi', 'surface_pressure', 'air_temp', 'wind_speed_10m', + 'wind_direction_10m', 'gti', 'azimuth', 'dewpoint_temp', + 'precipitable_water', 'period_end', 'period', 'zenith'], + index=pd.RangeIndex(start=0, stop=2, step=1) + ), + pd.DataFrame( + [[9.4200e+02, 8.4300e+02, 1.0174e+05, 3.0000e+01, 7.8000e+00, + 3.1600e+02, 1.0100e+03, 2.0000e+00, 4.6000e+00, 1.6400e+02, 90], + [9.3600e+02, 8.3200e+02, 1.0179e+05, 3.0000e+01, 7.9000e+00, + 3.1600e+02, 9.9600e+02, 1.4000e+01, 4.5000e+00, 1.6300e+02, 0]], + columns=['dni', 'ghi', 'pressure', 'temp_air', 'wind_speed', 'wind_direction', + 'poa_global', 'solar_azimuth', 'temp_dew', 'precipitable_water', 'solar_zenith'], + index=pd.DatetimeIndex( + ['2023-09-20 01:45:00+00:00', '2023-09-20 01:15:00+00:00'], + dtype='datetime64[ns, UTC]', name='period_mid', freq=None) + ) + ) +]) +def test_solcast2pvlib(in_df, out_df): + df = solcast2pvlib(in_df) + pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) \ No newline at end of file From e731ee038e2f5164f2d085f9a82d50f7fc218b50 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Mon, 25 Sep 2023 14:07:59 +1000 Subject: [PATCH 02/30] dynamic period --- pvlib/iotools/solcast.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index f63a496dfb..6b69e18371 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -49,6 +49,7 @@ def get_solcast_tmy( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is made the index with the "period mid" convention from Solcast's "period end". kwargs: Optional parameters passed to the API. See https://docs.solcast.com.au/ for full list of parameters. @@ -124,6 +125,7 @@ def get_solcast_historic( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is made the index with the "period mid" convention from Solcast's "period end". api_key : str To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. kwargs: @@ -191,6 +193,7 @@ def get_solcast_forecast( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is made the index with the "period mid" convention from Solcast's "period end". kwargs: Optional parameters passed to the GET request @@ -213,7 +216,7 @@ def get_solcast_forecast( get_solcast_forecast( latitude=-33.856784, longitude=151.215297, - output_parameters='dni,clearsky_dni', + output_parameters=['dni', 'clearsky_dni', 'snow_soiling_rooftop'], api_key="your-key" ) """ @@ -248,6 +251,7 @@ def get_solcast_live( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is made the index with the "period mid" convention from Solcast's "period end". kwargs: Optional parameters passed to the GET request @@ -272,6 +276,7 @@ def get_solcast_live( latitude=-33.856784, longitude=151.215297, terrain_shading=True, + output_parameters=['ghi', 'clearsky_ghi', 'snow_soiling_rooftop'], api_key="your-key" ) @@ -303,7 +308,7 @@ def solcast2pvlib(df): """Formats the data from Solcast to PVLib's conventions. """ # move from period_end to period_middle as per pvlib convention - df["period_mid"] = pd.to_datetime(df.period_end) - pd.Timedelta(df.period.values[0]) / 2 + df["period_mid"] = pd.to_datetime(df.period_end) - pd.to_timedelta(df.period.values) / 2 df = df.set_index("period_mid").drop(columns=["period_end", "period"]) # rename and convert variables @@ -336,6 +341,7 @@ def _get_solcast( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is made the index with the "period mid" convention from Solcast's "period end". Returns ------- From 1f162cf678162fbb30c34bd614ca0c85aacf16a9 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Mon, 25 Sep 2023 14:11:43 +1000 Subject: [PATCH 03/30] docstring --- pvlib/iotools/solcast.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 6b69e18371..5623472e93 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -306,6 +306,15 @@ def get_solcast_live( def solcast2pvlib(df): """Formats the data from Solcast to PVLib's conventions. + + Parameters + ---------- + df : pandas.DataFrame + contains the data as returned from the Solcast API + + Returns + ------- + a pandas.DataFrame with the data cast to PVLib's conventions """ # move from period_end to period_middle as per pvlib convention df["period_mid"] = pd.to_datetime(df.period_end) - pd.to_timedelta(df.period.values) / 2 From 12c1fe388a6fa0ad050847e4b1a11b85c995965e Mon Sep 17 00:00:00 2001 From: lorenzori Date: Mon, 25 Sep 2023 17:07:52 +1000 Subject: [PATCH 04/30] feedback --- pvlib/iotools/solcast.py | 305 +++++++++++++++++----------- pvlib/tests/iotools/test_solcast.py | 88 +++++--- 2 files changed, 243 insertions(+), 150 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 5623472e93..f0b6deee47 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -8,35 +8,48 @@ BASE_URL = "https://api.solcast.com.au/data" + @dataclass class ParameterMap: solcast_name: str pvlib_name: str - conversion: callable=lambda x: x + conversion: callable = lambda x: x + # define the conventions between Solcast and PVLib nomenclature and units VARIABLE_MAP = [ - ParameterMap("air_temp", "temp_air"), # air_temp -> temp_air (deg C) - ParameterMap("surface_pressure", "pressure", lambda x: x*100), # surface_pressure (hPa) -> pressure (Pa) - ParameterMap("dewpoint_temp", "temp_dew"), # dewpoint_temp -> temp_dew (deg C) - ParameterMap("gti", "poa_global"), # gti (W/m^2) -> poa_global (W/m^2) - ParameterMap("wind_speed_10m", "wind_speed"), # wind_speed_10m (m/s) -> wind_speed (m/s) - ParameterMap("wind_direction_10m", "wind_direction"), # wind_direction_10m (deg) -> wind_direction (deg) (Convention?) + # air_temp -> temp_air (deg C) + ParameterMap("air_temp", "temp_air"), + # surface_pressure (hPa) -> pressure (Pa) + ParameterMap("surface_pressure", "pressure", lambda x: x*100), + # dewpoint_temp -> temp_dew (deg C) + ParameterMap("dewpoint_temp", "temp_dew"), + # gti (W/m^2) -> poa_global (W/m^2) + ParameterMap("gti", "poa_global"), + # wind_speed_10m (m/s) -> wind_speed (m/s) + ParameterMap("wind_speed_10m", "wind_speed"), + # wind_direction_10m (deg) -> wind_direction (deg) + ParameterMap("wind_direction_10m", "wind_direction"), + # azimuth -> solar_azimuth (degrees) (different convention) ParameterMap( "azimuth", "solar_azimuth", lambda x: abs(x) if x <= 0 else 360 - x - ), # azimuth -> solar_azimuth (degrees) (different convention) - ParameterMap("precipitable_water", "precipitable_water", lambda x: x*10), # precipitable_water (kg/m2) -> precipitable_water (cm) - ParameterMap("zenith", "solar_zenith") # zenith -> solar_zenith + ), + # precipitable_water (kg/m2) -> precipitable_water (cm) + ParameterMap("precipitable_water", "precipitable_water", lambda x: x*10), + # zenith -> solar_zenith + ParameterMap("zenith", "solar_zenith") ] def get_solcast_tmy( latitude, longitude, api_key, map_variables=True, **kwargs ): - """Get the irradiance and weather for a Typical Meteorological Year (TMY) at a requested location. + """Get the irradiance and weather for a + Typical Meteorological Year (TMY) at a requested location. - Derived from satellite (clouds and irradiance over non-polar continental areas) and - numerical weather models (other data). The TMY is calculated with data from 2007 to 2023. + Derived from satellite (clouds and irradiance over + non-polar continental areas) and numerical weather models (other data). + The TMY is calculated with data from 2007 to 2023. Parameters ---------- @@ -45,36 +58,43 @@ def get_solcast_tmy( longitude : float in decimal degrees, between -180 and 180, east is positive api_key : str - To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + To access Solcast data you will need an API key [1]_. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is made the index with the "period mid" convention from Solcast's "period end". kwargs: - Optional parameters passed to the API. See https://docs.solcast.com.au/ for full list of parameters. + Optional parameters passed to the API. + See [2]_ for full list of parameters. Returns ------- - df : pandas.DataFrame - containing the values for the parameters requested + data : pandas.DataFrame + containing the values for the parameters requested. + Time is made the index with the "period mid" convention. Examples -------- - get_solcast_tmy( - latitude=-33.856784, - longitude=151.215297, - api_key="your-key" - ) - - you can pass any of the parameters listed in the API docs, like time_zone: - - get_solcast_tmy( - latitude=-33.856784, - longitude=151.215297, - time_zone=10, - api_key="your-key" - ) - + >>> pvlib.iotools.solcast.get_solcast_tmy( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> api_key="your-key" + >>> ) + + you can pass any of the parameters listed in the API docs, + like ``time_zone``. Here we set the value of 10 for + "10 hours ahead of UTC": + + >>> pvlib.iotools.solcast.get_solcast_tmy( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> time_zone=10, + >>> api_key="your-key" + >>> ) + + References + ---------- + .. [1] `Get an API Key `_ + .. [2] `Solcast API Docs `_ """ params = dict( @@ -84,13 +104,15 @@ def get_solcast_tmy( **kwargs ) - return _get_solcast( + data = _get_solcast( endpoint="tmy/radiation_and_weather", params=params, api_key=api_key, map_variables=map_variables ) + return data, {} + def get_solcast_historic( latitude, @@ -106,7 +128,8 @@ def get_solcast_historic( for up to 31 days of data at a time for a requested location, derived from satellite (clouds and irradiance - over non-polar continental areas) and numerical weather models (other data). + over non-polar continental areas) and + numerical weather models (other data). Data is available from 2007-01-01T00:00Z up to real time estimated actuals. Parameters @@ -120,43 +143,51 @@ def get_solcast_historic( end : optional, datetime-like Last day of the requested period duration : optional, default is None - Must include one of end_date and duration. ISO_8601 compliant duration for the historic data. + Must include one of end_date and duration. + ISO_8601 compliant duration for the historic data, + like "P1D" for one day of data. Must be within 31 days of the start_date. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is made the index with the "period mid" convention from Solcast's "period end". api_key : str - To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + To access Solcast data you will need an API key [1]_. kwargs: Optional parameters passed to the GET request - See https://docs.solcast.com.au/ for full list of parameters. + See [2]_ for full list of parameters. Returns ------- - df : pandas.DataFrame - containing the values for the parameters requested + data : pandas.DataFrame + containing the values for the parameters requested. + Time is made the index with the "period mid" convention. Examples -------- - get_solcast_historic( - latitude=-33.856784, - longitude=151.215297, - start='2007-01-01T00:00Z', - duration='P1D', - api_key="your-key" - ) - - you can pass any of the parameters listed in the API docs, for example using the end parameter instead - - get_solcast_historic( - latitude=-33.856784, - longitude=151.215297, - start='2007-01-01T00:00Z', - end='2007-01-02T00:00Z', - api_key="your-key" - ) + >>> pvlib.iotools.solcast.get_solcast_historic( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> start='2007-01-01T00:00Z', + >>> duration='P1D', + >>> api_key="your-key" + >>> ) + + you can pass any of the parameters listed in the API docs, + for example using the ``end`` parameter instead + + >>> pvlib.iotools.solcast.get_solcast_historic( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> start='2007-01-01T00:00Z', + >>> end='2007-01-02T00:00Z', + >>> api_key="your-key" + >>> ) + + References + ---------- + .. [1] `Get an API Key `_ + .. [2] `Solcast API Docs `_ """ params = dict( @@ -170,17 +201,21 @@ def get_solcast_historic( **kwargs ) - return _get_solcast( + data = _get_solcast( endpoint="historic/radiation_and_weather", params=params, api_key=api_key, map_variables=map_variables ) + return data, {} + + def get_solcast_forecast( latitude, longitude, api_key, map_variables=True, **kwargs ): - """Get irradiance and weather forecasts from the present time up to 14 days ahead + """Get irradiance and weather forecasts from the present time + up to 14 days ahead Parameters ---------- @@ -189,36 +224,43 @@ def get_solcast_forecast( longitude : float in decimal degrees, between -180 and 180, east is positive api_key : str - To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + To access Solcast data you will need an API key [1]_. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is made the index with the "period mid" convention from Solcast's "period end". kwargs: Optional parameters passed to the GET request - See https://docs.solcast.com.au/ for full list of parameters. + See [2]_ for full list of parameters. Returns ------- - df : pandas.DataFrame - containing the values for the parameters requested + data : pandas.DataFrame + containing the values for the parameters requested. + Time is made the index with the "period mid" convention. Examples -------- - get_solcast_forecast( - latitude=-33.856784, - longitude=151.215297, - api_key="your-key" - ) - - you can pass any of the parameters listed in the API docs, like asking for specific variables - get_solcast_forecast( - latitude=-33.856784, - longitude=151.215297, - output_parameters=['dni', 'clearsky_dni', 'snow_soiling_rooftop'], - api_key="your-key" - ) + >>> pvlib.iotools.solcast.get_solcast_forecast( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> api_key="your-key" + >>> ) + + you can pass any of the parameters listed in the API docs, + like asking for specific variables: + + >>> pvlib.iotools.solcast.get_solcast_forecast( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> output_parameters=['dni', 'clearsky_dni', 'snow_soiling_rooftop'], + >>> api_key="your-key" + >>> ) + + References + ---------- + .. [1] `Get an API Key `_ + .. [2] `Solcast API Docs `_ """ params = dict( @@ -228,17 +270,21 @@ def get_solcast_forecast( **kwargs ) - return _get_solcast( + data = _get_solcast( endpoint="forecast/radiation_and_weather", params=params, api_key=api_key, map_variables=map_variables ) + return data, {} + + def get_solcast_live( latitude, longitude, api_key, map_variables=True, **kwargs ): - """Get irradiance and weather estimated actuals for near real-time and past 7 days + """Get irradiance and weather estimated actuals for near real-time + and past 7 days Parameters ---------- @@ -247,47 +293,53 @@ def get_solcast_live( longitude : float in decimal degrees, between -180 and 180, east is positive api_key : str - To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + To access Solcast data you will need an API key [1]_. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is made the index with the "period mid" convention from Solcast's "period end". kwargs: Optional parameters passed to the GET request - See https://docs.solcast.com.au/ for full list of parameters. + See [2]_ for full list of parameters. Returns ------- - df : pandas.DataFrame - containing the values for the parameters requested + data : pandas.DataFrame + containing the values for the parameters requested. + Time is made the index with the "period mid" convention. Examples -------- - get_solcast_live( - latitude=-33.856784, - longitude=151.215297, - api_key="your-key" - ) + >>> pvlib.iotools.solcast.get_solcast_live( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> api_key="your-key" + >>> ) you can pass any of the parameters listed in the API docs, like - get_solcast_live( - latitude=-33.856784, - longitude=151.215297, - terrain_shading=True, - output_parameters=['ghi', 'clearsky_ghi', 'snow_soiling_rooftop'], - api_key="your-key" - ) - - use map_variables=False to avoid converting the data to PVLib's conventions - - get_solcast_live( - latitude=-33.856784, - longitude=151.215297, - map_variables=False, - api_key="your-key" - ) + >>> pvlib.iotools.solcast.get_solcast_live( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> terrain_shading=True, + >>> output_parameters=['ghi', 'clearsky_ghi', 'snow_soiling_rooftop'], + >>> api_key="your-key" + >>> ) + + use ``map_variables=False`` to avoid converting the data + to PVLib's conventions. + + >>> pvlib.iotools.solcast.get_solcast_live( + >>> latitude=-33.856784, + >>> longitude=151.215297, + >>> map_variables=False, + >>> api_key="your-key" + >>> ) + + References + ---------- + .. [1] `Get an API Key `_ + .. [2] `Solcast API Docs `_ """ params = dict( @@ -297,19 +349,22 @@ def get_solcast_live( **kwargs ) - return _get_solcast( + data = _get_solcast( endpoint="live/radiation_and_weather", params=params, api_key=api_key, map_variables=map_variables ) -def solcast2pvlib(df): + return data, {} + + +def solcast2pvlib(data): """Formats the data from Solcast to PVLib's conventions. Parameters ---------- - df : pandas.DataFrame + data : pandas.DataFrame contains the data as returned from the Solcast API Returns @@ -317,15 +372,21 @@ def solcast2pvlib(df): a pandas.DataFrame with the data cast to PVLib's conventions """ # move from period_end to period_middle as per pvlib convention - df["period_mid"] = pd.to_datetime(df.period_end) - pd.to_timedelta(df.period.values) / 2 - df = df.set_index("period_mid").drop(columns=["period_end", "period"]) + data["period_mid"] = pd.to_datetime( + data.period_end) - pd.to_timedelta(data.period.values) / 2 + data = data.set_index("period_mid").drop(columns=["period_end", "period"]) # rename and convert variables for variable in VARIABLE_MAP: - if variable.solcast_name in df.columns: - df.rename(columns={variable.solcast_name: variable.pvlib_name}, inplace=True) - df[variable.pvlib_name] = df[variable.pvlib_name].apply(variable.conversion) - return df + if variable.solcast_name in data.columns: + data.rename( + columns={variable.solcast_name: variable.pvlib_name}, + inplace=True + ) + data[variable.pvlib_name] = data[ + variable.pvlib_name].apply(variable.conversion) + return data + def _get_solcast( endpoint, @@ -346,19 +407,25 @@ def _get_solcast( params : dict parameters to be passed to the API api_key : str - To access Solcast data you will need an API key: https://toolkit.solcast.com.au/register. + To access Solcast data you will need an API key [1]_. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is made the index with the "period mid" convention from Solcast's "period end". + Time is made the index with the "period mid" convention + from Solcast's "period end". Returns ------- - A pandas.DataFrame with the data if the request is successful, an error message otherwise + A pandas.DataFrame with the data if the request is successful, + an error message otherwise + + References + ---------- + .. [1] `register ` """ response = requests.get( - url= '/'.join([BASE_URL, endpoint]), + url='/'.join([BASE_URL, endpoint]), params=params, headers={"Authorization": f"Bearer {api_key}"} ) diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 1b836bd7e3..80bce9c8b7 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -1,5 +1,7 @@ import pandas as pd -from pvlib.iotools.solcast import get_solcast_live, get_solcast_tmy, solcast2pvlib +from pvlib.iotools.solcast import ( + get_solcast_live, get_solcast_tmy, solcast2pvlib +) import pytest @@ -9,29 +11,39 @@ get_solcast_live, dict( api_key="1234", - latitude = -33.856784, - longitude = 151.215297, - output_parameters = 'dni,ghi' + latitude=-33.856784, + longitude=151.215297, + output_parameters='dni,ghi' ), {'estimated_actuals': - [{'dni': 836, 'ghi': 561, 'period_end': '2023-09-18T05:00:00.0000000Z', 'period': 'PT30M'}, - {'dni': 866, 'ghi': 643, 'period_end': '2023-09-18T04:30:00.0000000Z', 'period': 'PT30M'}, - {'dni': 890, 'ghi': 713, 'period_end': '2023-09-18T04:00:00.0000000Z', 'period': 'PT30M'}, - {'dni': 909, 'ghi': 768, 'period_end': '2023-09-18T03:30:00.0000000Z', 'period': 'PT30M'}] - } + [{'dni': 836, 'ghi': 561, + 'period_end': '2023-09-18T05:00:00.0000000Z', 'period': 'PT30M'}, + {'dni': 866, 'ghi': 643, + 'period_end': '2023-09-18T04:30:00.0000000Z', 'period': 'PT30M'}, + {'dni': 890, 'ghi': 713, + 'period_end': '2023-09-18T04:00:00.0000000Z', 'period': 'PT30M'}, + {'dni': 909, 'ghi': 768, + 'period_end': '2023-09-18T03:30:00.0000000Z', 'period': 'PT30M'}] + } ), ]) -def test_get_solcast_live(requests_mock, endpoint, function, params, json_response): +def test_get_solcast_live( + requests_mock, endpoint, function, params, json_response +): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"&latitude={params['latitude']}&longitude={params['longitude']}&" \ + f"&latitude={params['latitude']}&" \ + f"longitude={params['longitude']}&" \ f"output_parameters={params['output_parameters']}&format=json" requests_mock.get(mock_url, json=json_response) pd.testing.assert_frame_equal( - function(**params), - solcast2pvlib(pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]])) + function(**params)[0], + solcast2pvlib( + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]]) + ) ) @@ -41,28 +53,39 @@ def test_get_solcast_live(requests_mock, endpoint, function, params, json_respon get_solcast_tmy, dict( api_key="1234", - latitude = -33.856784, - longitude = 151.215297 + latitude=-33.856784, + longitude=51.215297 ), {'estimated_actuals': [ - {'dni': 151, 'ghi': 609, 'period_end': '2059-01-01T01:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 404, 'period_end': '2059-01-01T02:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 304, 'period_end': '2059-01-01T03:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 174, 'period_end': '2059-01-01T04:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 111, 'period_end': '2059-01-01T05:00:00.0000000Z', 'period': 'PT60M'}] - } + {'dni': 151, 'ghi': 609, + 'period_end': '2059-01-01T01:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 404, + 'period_end': '2059-01-01T02:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 304, + 'period_end': '2059-01-01T03:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 174, + 'period_end': '2059-01-01T04:00:00.0000000Z', 'period': 'PT60M'}, + {'dni': 0, 'ghi': 111, + 'period_end': '2059-01-01T05:00:00.0000000Z', 'period': 'PT60M'}] + } ), ]) -def test_get_solcast_tmy(requests_mock, endpoint, function, params, json_response): +def test_get_solcast_tmy( + requests_mock, endpoint, function, params, json_response +): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"&latitude={params['latitude']}&longitude={params['longitude']}&format=json" + f"&latitude={params['latitude']}&" \ + f"longitude={params['longitude']}&format=json" requests_mock.get(mock_url, json=json_response) pd.testing.assert_frame_equal( - function(**params), - solcast2pvlib(pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]])) + function(**params)[0], + solcast2pvlib( + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]]) + ) ) @@ -73,9 +96,10 @@ def test_get_solcast_tmy(requests_mock, endpoint, function, params, json_respons '2023-09-20T02:00:00.0000000Z', 'PT30M', 90], [936, 832, 1017.9, 30, 7.9, 316, 996, -14, 4.5, 16.3, '2023-09-20T01:30:00.0000000Z', 'PT30M', 0]], - columns=['dni', 'ghi', 'surface_pressure', 'air_temp', 'wind_speed_10m', - 'wind_direction_10m', 'gti', 'azimuth', 'dewpoint_temp', - 'precipitable_water', 'period_end', 'period', 'zenith'], + columns=[ + 'dni', 'ghi', 'surface_pressure', 'air_temp', 'wind_speed_10m', + 'wind_direction_10m', 'gti', 'azimuth', 'dewpoint_temp', + 'precipitable_water', 'period_end', 'period', 'zenith'], index=pd.RangeIndex(start=0, stop=2, step=1) ), pd.DataFrame( @@ -83,8 +107,10 @@ def test_get_solcast_tmy(requests_mock, endpoint, function, params, json_respons 3.1600e+02, 1.0100e+03, 2.0000e+00, 4.6000e+00, 1.6400e+02, 90], [9.3600e+02, 8.3200e+02, 1.0179e+05, 3.0000e+01, 7.9000e+00, 3.1600e+02, 9.9600e+02, 1.4000e+01, 4.5000e+00, 1.6300e+02, 0]], - columns=['dni', 'ghi', 'pressure', 'temp_air', 'wind_speed', 'wind_direction', - 'poa_global', 'solar_azimuth', 'temp_dew', 'precipitable_water', 'solar_zenith'], + columns=[ + 'dni', 'ghi', 'pressure', 'temp_air', 'wind_speed', + 'wind_direction', 'poa_global', 'solar_azimuth', + 'temp_dew', 'precipitable_water', 'solar_zenith'], index=pd.DatetimeIndex( ['2023-09-20 01:45:00+00:00', '2023-09-20 01:15:00+00:00'], dtype='datetime64[ns, UTC]', name='period_mid', freq=None) @@ -93,4 +119,4 @@ def test_get_solcast_tmy(requests_mock, endpoint, function, params, json_respons ]) def test_solcast2pvlib(in_df, out_df): df = solcast2pvlib(in_df) - pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) \ No newline at end of file + pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) From 30278c9754dfa23bbc2173fdeb103ea20fe159da Mon Sep 17 00:00:00 2001 From: lorenzori Date: Tue, 26 Sep 2023 11:13:43 +1000 Subject: [PATCH 05/30] linting --- pvlib/iotools/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 168a7f4224..6a4991ee7b 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -27,7 +27,7 @@ from pvlib.iotools.acis import get_acis_mpe # noqa: F401 from pvlib.iotools.acis import get_acis_station_data # noqa: F401 from pvlib.iotools.acis import get_acis_available_stations # noqa: F401 -from pvlib.iotools.solcast import get_solcast_forecast # noqa: F401 -from pvlib.iotools.solcast import get_solcast_live # noqa: F401 -from pvlib.iotools.solcast import get_solcast_historic # noqa: F401 -from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401 +from pvlib.iotools.solcast import get_solcast_forecast # noqa: F401 +from pvlib.iotools.solcast import get_solcast_live # noqa: F401 +from pvlib.iotools.solcast import get_solcast_historic # noqa: F401 +from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401 From 9dfc3d07accd0a7d3828a17fd389edc318a75dd2 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:47:56 +1100 Subject: [PATCH 06/30] Update pvlib/iotools/solcast.py Co-authored-by: Cliff Hansen --- pvlib/iotools/solcast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index f0b6deee47..32006e0ba3 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -236,7 +236,8 @@ def get_solcast_forecast( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested. + Contains the values for the parameters requested. + Time is made the index with the "period mid" convention. Examples From 85f8f7369a88ab290efd38de26c55fdfd4fea97a Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:48:01 +1100 Subject: [PATCH 07/30] Update pvlib/iotools/solcast.py Co-authored-by: Cliff Hansen --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 32006e0ba3..3add1795fd 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -143,7 +143,7 @@ def get_solcast_historic( end : optional, datetime-like Last day of the requested period duration : optional, default is None - Must include one of end_date and duration. + Must include either ``end`` or ``duration``. ISO_8601 compliant duration for the historic data, like "P1D" for one day of data. Must be within 31 days of the start_date. From 90983fe212bc5218786ed595ccf96535dbd8db7d Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:48:07 +1100 Subject: [PATCH 08/30] Update pvlib/iotools/solcast.py Co-authored-by: Cliff Hansen --- pvlib/iotools/solcast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 3add1795fd..784b2342f0 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -141,7 +141,8 @@ def get_solcast_historic( start : datetime-like First day of the requested period end : optional, datetime-like - Last day of the requested period + Last day of the requested period. Must include one of ``end`` or ``duration``. + duration : optional, default is None Must include either ``end`` or ``duration``. ISO_8601 compliant duration for the historic data, From 499f51a38145417fd233300e4259b5ec7b72aa20 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Wed, 11 Oct 2023 13:52:18 +1100 Subject: [PATCH 09/30] midpoint docstring --- pvlib/iotools/solcast.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index f0b6deee47..e08dba5a15 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -69,8 +69,8 @@ def get_solcast_tmy( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested. - Time is made the index with the "period mid" convention. + containing the values for the parameters requested.The times + in the DataFrame index indicate the midpoint of each interval. Examples -------- @@ -160,8 +160,8 @@ def get_solcast_historic( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested. - Time is made the index with the "period mid" convention. + containing the values for the parameters requested.The times + in the DataFrame index indicate the midpoint of each interval. Examples -------- @@ -236,8 +236,8 @@ def get_solcast_forecast( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested. - Time is made the index with the "period mid" convention. + containing the values for the parameters requested.The times + in the DataFrame index indicate the midpoint of each interval. Examples -------- @@ -305,8 +305,8 @@ def get_solcast_live( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested. - Time is made the index with the "period mid" convention. + containing the values for the parameters requested.The times + in the DataFrame index indicate the midpoint of each interval. Examples -------- @@ -411,7 +411,7 @@ def _get_solcast( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is made the index with the "period mid" convention + Time is the index as midpoint of each interval. from Solcast's "period end". Returns From d79a76471d2d5d51eeefb521a39719db16e67d58 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Fri, 20 Oct 2023 08:34:03 +1100 Subject: [PATCH 10/30] flak8 formatting --- pvlib/iotools/solcast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 1400215b44..5f2ae3ed01 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -141,7 +141,8 @@ def get_solcast_historic( start : datetime-like First day of the requested period end : optional, datetime-like - Last day of the requested period. Must include one of ``end`` or ``duration``. + Last day of the requested period. + Must include one of ``end`` or ``duration``. duration : optional, default is None Must include either ``end`` or ``duration``. From 6792ecbf69268634116f0cca2343585e40f5448c Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:17:42 +1100 Subject: [PATCH 11/30] PR 1875 (#2) --- docs/sphinx/source/whatsnew/v0.10.3.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst index 007eb8d34b..4f15b5bf44 100644 --- a/docs/sphinx/source/whatsnew/v0.10.3.rst +++ b/docs/sphinx/source/whatsnew/v0.10.3.rst @@ -12,6 +12,9 @@ Enhancements * :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` now include shaded fraction in returned variables. (:pull:`1871`) +* Added :py:func:`pvlib.iotools.solcast.get_solcast_tmy`, :py:func:`pvlib.iotools.solcast.get_solcast_historic`, + :py:func:`pvlib.iotools.solcast.get_solcast_forecast` and :py:func:`pvlib.iotools.solcast.get_solcast_live` to + read data from the Solcast API. (:issue:`1313`, :pull:`1875`) Bug fixes ~~~~~~~~~ @@ -44,3 +47,4 @@ Contributors * Harry Jack (:ghuser:`harry-solcast`) * Adam R. Jensen (:ghuser:`AdamRJensen`) * Kevin Anderson (:ghuser:`kandersolar`) +* Lorenzo Riches (:ghuser:`lorenzo-solcast`) From c0fd3125f3cdc32280b70fc4fad0cc0917228787 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:07:40 +1100 Subject: [PATCH 12/30] kandersolar feedback (#3) * addressing feedback from Kandersolar --- docs/sphinx/source/whatsnew/v0.10.3.rst | 4 +- pvlib/iotools/solcast.py | 79 +++++++++--------- pvlib/tests/iotools/test_solcast.py | 101 +++++++++++++++++++++++- 3 files changed, 141 insertions(+), 43 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst index 4f15b5bf44..2ea3beed07 100644 --- a/docs/sphinx/source/whatsnew/v0.10.3.rst +++ b/docs/sphinx/source/whatsnew/v0.10.3.rst @@ -12,8 +12,8 @@ Enhancements * :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` now include shaded fraction in returned variables. (:pull:`1871`) -* Added :py:func:`pvlib.iotools.solcast.get_solcast_tmy`, :py:func:`pvlib.iotools.solcast.get_solcast_historic`, - :py:func:`pvlib.iotools.solcast.get_solcast_forecast` and :py:func:`pvlib.iotools.solcast.get_solcast_live` to +* Added :py:func:`~pvlib.iotools.get_solcast_tmy`, :py:func:`~pvlib.iotools.get_solcast_historic`, + :py:func:`~pvlib.iotools.get_solcast_forecast` and :py:func:`~pvlib.iotools.get_solcast_live` to read data from the Solcast API. (:issue:`1313`, :pull:`1875`) Bug fixes diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 5f2ae3ed01..7248d5f2b0 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -32,7 +32,7 @@ class ParameterMap: ParameterMap("wind_direction_10m", "wind_direction"), # azimuth -> solar_azimuth (degrees) (different convention) ParameterMap( - "azimuth", "solar_azimuth", lambda x: abs(x) if x <= 0 else 360 - x + "azimuth", "solar_azimuth", lambda x: -x % 360 ), # precipitable_water (kg/m2) -> precipitable_water (cm) ParameterMap("precipitable_water", "precipitable_water", lambda x: x*10), @@ -44,12 +44,12 @@ class ParameterMap: def get_solcast_tmy( latitude, longitude, api_key, map_variables=True, **kwargs ): - """Get the irradiance and weather for a + """Get irradiance and weather for a Typical Meteorological Year (TMY) at a requested location. - Derived from satellite (clouds and irradiance over - non-polar continental areas) and numerical weather models (other data). - The TMY is calculated with data from 2007 to 2023. + Data is derived from a multi-year time series selected to present the + unique weather phenomena with annual averages that are consistent with + long term averages. See [1]_ for details on the calculation. Parameters ---------- @@ -58,23 +58,25 @@ def get_solcast_tmy( longitude : float in decimal degrees, between -180 and 180, east is positive api_key : str - To access Solcast data you will need an API key [1]_. + To access Solcast data you will need an API key [2]_. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. kwargs: Optional parameters passed to the API. - See [2]_ for full list of parameters. + See [3]_ for full list of parameters. Returns ------- data : pandas.DataFrame containing the values for the parameters requested.The times in the DataFrame index indicate the midpoint of each interval. + metadata: dict + latitude and longitude of the request. Examples -------- - >>> pvlib.iotools.solcast.get_solcast_tmy( + >>> df, meta = pvlib.iotools.solcast.get_solcast_tmy( >>> latitude=-33.856784, >>> longitude=151.215297, >>> api_key="your-key" @@ -84,7 +86,7 @@ def get_solcast_tmy( like ``time_zone``. Here we set the value of 10 for "10 hours ahead of UTC": - >>> pvlib.iotools.solcast.get_solcast_tmy( + >>> df, meta = pvlib.iotools.solcast.get_solcast_tmy( >>> latitude=-33.856784, >>> longitude=151.215297, >>> time_zone=10, @@ -93,8 +95,9 @@ def get_solcast_tmy( References ---------- - .. [1] `Get an API Key `_ + .. [1] `Solcast TMY Docs `_ .. [2] `Solcast API Docs `_ + .. [3] `Get an API Key `_ """ params = dict( @@ -111,7 +114,7 @@ def get_solcast_tmy( map_variables=map_variables ) - return data, {} + return data, {"latitude": latitude, "longitude": longitude} def get_solcast_historic( @@ -143,31 +146,31 @@ def get_solcast_historic( end : optional, datetime-like Last day of the requested period. Must include one of ``end`` or ``duration``. - duration : optional, default is None Must include either ``end`` or ``duration``. ISO_8601 compliant duration for the historic data, like "P1D" for one day of data. - Must be within 31 days of the start_date. + Must be within 31 days of the ``start``. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. api_key : str To access Solcast data you will need an API key [1]_. kwargs: - Optional parameters passed to the GET request - - See [2]_ for full list of parameters. + Optional parameters passed to the API. + See [2]_ for full list of parameters. Returns ------- data : pandas.DataFrame containing the values for the parameters requested.The times in the DataFrame index indicate the midpoint of each interval. + metadata: dict + latitude and longitude of the request. Examples -------- - >>> pvlib.iotools.solcast.get_solcast_historic( + >>> df, meta = pvlib.iotools.solcast.get_solcast_historic( >>> latitude=-33.856784, >>> longitude=151.215297, >>> start='2007-01-01T00:00Z', @@ -178,7 +181,7 @@ def get_solcast_historic( you can pass any of the parameters listed in the API docs, for example using the ``end`` parameter instead - >>> pvlib.iotools.solcast.get_solcast_historic( + >>> df, meta = pvlib.iotools.solcast.get_solcast_historic( >>> latitude=-33.856784, >>> longitude=151.215297, >>> start='2007-01-01T00:00Z', @@ -210,7 +213,7 @@ def get_solcast_historic( map_variables=map_variables ) - return data, {} + return data, {"latitude": latitude, "longitude": longitude} def get_solcast_forecast( @@ -231,19 +234,20 @@ def get_solcast_forecast( When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. kwargs: - Optional parameters passed to the GET request - - See [2]_ for full list of parameters. + Optional parameters passed to the API. + See [2]_ for full list of parameters. Returns ------- data : pandas.DataFrame Contains the values for the parameters requested.The times in the DataFrame index indicate the midpoint of each interval. + metadata: dict + latitude and longitude of the request. Examples -------- - >>> pvlib.iotools.solcast.get_solcast_forecast( + >>> df, meta = pvlib.iotools.solcast.get_solcast_forecast( >>> latitude=-33.856784, >>> longitude=151.215297, >>> api_key="your-key" @@ -252,7 +256,7 @@ def get_solcast_forecast( you can pass any of the parameters listed in the API docs, like asking for specific variables: - >>> pvlib.iotools.solcast.get_solcast_forecast( + >>> df, meta = pvlib.iotools.solcast.get_solcast_forecast( >>> latitude=-33.856784, >>> longitude=151.215297, >>> output_parameters=['dni', 'clearsky_dni', 'snow_soiling_rooftop'], @@ -279,7 +283,7 @@ def get_solcast_forecast( map_variables=map_variables ) - return data, {} + return data, {"latitude": latitude, "longitude": longitude} def get_solcast_live( @@ -300,19 +304,20 @@ def get_solcast_live( When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. kwargs: - Optional parameters passed to the GET request - - See [2]_ for full list of parameters. + Optional parameters passed to the API. + See [2]_ for full list of parameters. Returns ------- data : pandas.DataFrame containing the values for the parameters requested.The times in the DataFrame index indicate the midpoint of each interval. + metadata: dict + latitude and longitude of the request. Examples -------- - >>> pvlib.iotools.solcast.get_solcast_live( + >>> df, meta = pvlib.iotools.solcast.get_solcast_live( >>> latitude=-33.856784, >>> longitude=151.215297, >>> api_key="your-key" @@ -320,7 +325,7 @@ def get_solcast_live( you can pass any of the parameters listed in the API docs, like - >>> pvlib.iotools.solcast.get_solcast_live( + >>> df, meta = pvlib.iotools.solcast.get_solcast_live( >>> latitude=-33.856784, >>> longitude=151.215297, >>> terrain_shading=True, @@ -331,7 +336,7 @@ def get_solcast_live( use ``map_variables=False`` to avoid converting the data to PVLib's conventions. - >>> pvlib.iotools.solcast.get_solcast_live( + >>> df, meta = pvlib.iotools.solcast.get_solcast_live( >>> latitude=-33.856784, >>> longitude=151.215297, >>> map_variables=False, @@ -358,10 +363,10 @@ def get_solcast_live( map_variables=map_variables ) - return data, {} + return data, {"latitude": latitude, "longitude": longitude} -def solcast2pvlib(data): +def _solcast2pvlib(data): """Formats the data from Solcast to PVLib's conventions. Parameters @@ -411,10 +416,10 @@ def _get_solcast( api_key : str To access Solcast data you will need an API key [1]_. map_variables: bool, default: True - When true, renames columns of the DataFrame to pvlib variable names + When true, renames columns of the DataFrame to PVLib's variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is the index as midpoint of each interval. - from Solcast's "period end". + Time is the index as midpoint of each interval + from Solcast's "period end" convention. Returns ------- @@ -436,7 +441,7 @@ def _get_solcast( j = response.json() df = pd.DataFrame.from_dict(j[list(j.keys())[0]]) if map_variables: - return solcast2pvlib(df) + return _solcast2pvlib(df) else: return df else: diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 80bce9c8b7..739ced205d 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -1,6 +1,7 @@ import pandas as pd from pvlib.iotools.solcast import ( - get_solcast_live, get_solcast_tmy, solcast2pvlib + get_solcast_live, get_solcast_tmy, _solcast2pvlib, get_solcast_historic, + get_solcast_forecast ) import pytest @@ -40,7 +41,7 @@ def test_get_solcast_live( pd.testing.assert_frame_equal( function(**params)[0], - solcast2pvlib( + _solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]]) ) @@ -82,7 +83,7 @@ def test_get_solcast_tmy( pd.testing.assert_frame_equal( function(**params)[0], - solcast2pvlib( + _solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]]) ) @@ -118,5 +119,97 @@ def test_get_solcast_tmy( ) ]) def test_solcast2pvlib(in_df, out_df): - df = solcast2pvlib(in_df) + df = _solcast2pvlib(in_df) pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) + + +@pytest.mark.parametrize("endpoint,function,params,json_response", [ + ( + "historic/radiation_and_weather", + get_solcast_historic, + dict( + api_key="1234", + latitude=-33.856784, + longitude=51.215297, + start="2023-01-01T08:00", + duration="P1D", + period="PT1H", + output_parameters='dni' + ), {'estimated_actuals': [ + {'dni': 822, 'period_end': '2023-01-01T09:00:00.0000000Z', + 'period': 'PT60M'}, + {'dni': 918, 'period_end': '2023-01-01T10:00:00.0000000Z', + 'period': 'PT60M'}, + {'dni': 772, 'period_end': '2023-01-01T11:00:00.0000000Z', + 'period': 'PT60M'}, + {'dni': 574, 'period_end': '2023-01-01T12:00:00.0000000Z', + 'period': 'PT60M'}, + {'dni': 494, 'period_end': '2023-01-01T13:00:00.0000000Z', + 'period': 'PT60M'} + ]} + ), +]) +def test_get_solcast_historic( + requests_mock, endpoint, function, params, json_response +): + mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ + f"&latitude={params['latitude']}&" \ + f"longitude={params['longitude']}&format=json" + + requests_mock.get(mock_url, json=json_response) + + pd.testing.assert_frame_equal( + function(**params)[0], + _solcast2pvlib( + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ) + ) + ) + + +@pytest.mark.parametrize("endpoint,function,params,json_response", [ + ( + "forecast/radiation_and_weather", + get_solcast_forecast, + dict( + api_key="1234", + latitude=-33.856784, + longitude=51.215297, + hours="5", + period="PT1H", + output_parameters='dni' + ), { + 'forecast': [ + {'dni': 0, 'period_end': '2023-12-13T01:00:00.0000000Z', + 'period': 'PT1H'}, + {'dni': 1, 'period_end': '2023-12-13T02:00:00.0000000Z', + 'period': 'PT1H'}, + {'dni': 2, 'period_end': '2023-12-13T03:00:00.0000000Z', + 'period': 'PT1H'}, + {'dni': 3, 'period_end': '2023-12-13T04:00:00.0000000Z', + 'period': 'PT1H'}, + {'dni': 4, 'period_end': '2023-12-13T05:00:00.0000000Z', + 'period': 'PT1H'}, + {'dni': 5, 'period_end': '2023-12-13T06:00:00.0000000Z', + 'period': 'PT1H'} + ]} + ), +]) +def test_get_solcast_forecast( + requests_mock, endpoint, function, params, json_response +): + mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ + f"&latitude={params['latitude']}&" \ + f"longitude={params['longitude']}&format=json" + + requests_mock.get(mock_url, json=json_response) + + pd.testing.assert_frame_equal( + function(**params)[0], + _solcast2pvlib( + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ) + ) + ) From be27d850d4441d1a853ed1d7fab79233b9b467b8 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:38:30 +1100 Subject: [PATCH 13/30] Review (#4) added hack for ISO periods in Pandas 0.25 and clearsky parameter maps --- pvlib/iotools/solcast.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 7248d5f2b0..a76f0b4d9f 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -37,7 +37,12 @@ class ParameterMap: # precipitable_water (kg/m2) -> precipitable_water (cm) ParameterMap("precipitable_water", "precipitable_water", lambda x: x*10), # zenith -> solar_zenith - ParameterMap("zenith", "solar_zenith") + ParameterMap("zenith", "solar_zenith"), + # clearsky + ParameterMap("clearsky_dhi", "dhi_clear"), + ParameterMap("clearsky_dni", "dni_clear"), + ParameterMap("clearsky_ghi", "ghi_clear"), + ParameterMap("clearsky_gti", "poa_global_clear") ] @@ -96,8 +101,8 @@ def get_solcast_tmy( References ---------- .. [1] `Solcast TMY Docs `_ - .. [2] `Solcast API Docs `_ - .. [3] `Get an API Key `_ + .. [2] `Get an API Key `_ + .. [3] `Solcast API Docs `_ """ params = dict( @@ -379,8 +384,14 @@ def _solcast2pvlib(data): a pandas.DataFrame with the data cast to PVLib's conventions """ # move from period_end to period_middle as per pvlib convention + # to support Pandas 0.25 for Python 3.7 distribution + # we cast PTXX to XX as ISO8601 durations without days aren't + # supported: + # https://github.com/pandas-dev/pandas/pull/37159 + periods = data.period.str.replace("PT", "").str.replace("M", "m") + data["period_mid"] = pd.to_datetime( - data.period_end) - pd.to_timedelta(data.period.values) / 2 + data.period_end) - pd.to_timedelta(periods) / 2 data = data.set_index("period_mid").drop(columns=["period_end", "period"]) # rename and convert variables From a33b4b26963165a2b9ff601a1bdd77a2f4ce7c3f Mon Sep 17 00:00:00 2001 From: lorenzori Date: Fri, 15 Dec 2023 10:15:07 +1100 Subject: [PATCH 14/30] comment on pandas version --- pvlib/iotools/solcast.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index a76f0b4d9f..7e137b201b 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -384,10 +384,10 @@ def _solcast2pvlib(data): a pandas.DataFrame with the data cast to PVLib's conventions """ # move from period_end to period_middle as per pvlib convention - # to support Pandas 0.25 for Python 3.7 distribution - # we cast PTXX to XX as ISO8601 durations without days aren't - # supported: - # https://github.com/pandas-dev/pandas/pull/37159 + # to support Pandas 0.25 we cast PTXX to XX as ISO8601 + # durations without days aren't supported: + # https://github.com/pandas-dev/pandas/pull/37159\ + # Can remove once minimum supported Pandas version is >=1.2 periods = data.period.str.replace("PT", "").str.replace("M", "m") data["period_mid"] = pd.to_datetime( From f2baf747c6835abb58fe532c5ccce490137238ff Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:02:11 +1100 Subject: [PATCH 15/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 7e137b201b..125a9df79a 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -245,7 +245,7 @@ def get_solcast_forecast( Returns ------- data : pandas.DataFrame - Contains the values for the parameters requested.The times + Contains the values for the parameters requested. The times in the DataFrame index indicate the midpoint of each interval. metadata: dict latitude and longitude of the request. From 25448cec51db8665755f9959623d0d3b77daf407 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:02:27 +1100 Subject: [PATCH 16/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 125a9df79a..58e34f12e0 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -372,7 +372,7 @@ def get_solcast_live( def _solcast2pvlib(data): - """Formats the data from Solcast to PVLib's conventions. + """Format the data from Solcast to pvlib's conventions. Parameters ---------- From a9dac8f227ac4b45b5d72d0805760de7e0e74118 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:03:10 +1100 Subject: [PATCH 17/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 58e34f12e0..714f97cb67 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -381,7 +381,7 @@ def _solcast2pvlib(data): Returns ------- - a pandas.DataFrame with the data cast to PVLib's conventions + a pandas.DataFrame with the data cast to pvlib's conventions """ # move from period_end to period_middle as per pvlib convention # to support Pandas 0.25 we cast PTXX to XX as ISO8601 From ec21becfe4c32b95b8a322ed9045b50e3546ecad Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:03:20 +1100 Subject: [PATCH 18/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 714f97cb67..940b3a0e39 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -427,7 +427,7 @@ def _get_solcast( api_key : str To access Solcast data you will need an API key [1]_. map_variables: bool, default: True - When true, renames columns of the DataFrame to PVLib's variable names + When true, renames columns of the DataFrame to pvlib's variable names where applicable. See variable :const:`VARIABLE_MAP`. Time is the index as midpoint of each interval from Solcast's "period end" convention. From abdee5fb0f69a936e98bd7ea75ef1e1f55f2222b Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:03:31 +1100 Subject: [PATCH 19/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 940b3a0e39..35012a5f30 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -153,7 +153,7 @@ def get_solcast_historic( Must include one of ``end`` or ``duration``. duration : optional, default is None Must include either ``end`` or ``duration``. - ISO_8601 compliant duration for the historic data, + ISO 8601 compliant duration for the historic data, like "P1D" for one day of data. Must be within 31 days of the ``start``. map_variables: bool, default: True From ba28943d57596018bd2f268bad52d507497d74ae Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:03:43 +1100 Subject: [PATCH 20/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 35012a5f30..28eed61382 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -155,7 +155,7 @@ def get_solcast_historic( Must include either ``end`` or ``duration``. ISO 8601 compliant duration for the historic data, like "P1D" for one day of data. - Must be within 31 days of the ``start``. + Must be within 31 days of ``start``. map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. From f2b732317a41c028f9a1995f7e2103fc5584a9e4 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:04:05 +1100 Subject: [PATCH 21/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 28eed61382..76419e3402 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -225,7 +225,7 @@ def get_solcast_forecast( latitude, longitude, api_key, map_variables=True, **kwargs ): """Get irradiance and weather forecasts from the present time - up to 14 days ahead + up to 14 days ahead. Parameters ---------- From c9788adce02d1884fc6f69cac6572ca9ef2d5f16 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:04:14 +1100 Subject: [PATCH 22/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 76419e3402..1fe3e9ea75 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -295,7 +295,7 @@ def get_solcast_live( latitude, longitude, api_key, map_variables=True, **kwargs ): """Get irradiance and weather estimated actuals for near real-time - and past 7 days + and past 7 days. Parameters ---------- From 52ab6d7a1f7c6f4953d8171ce5a52b1367b0d5ba Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:04:34 +1100 Subject: [PATCH 23/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 1fe3e9ea75..e22892d386 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -16,7 +16,7 @@ class ParameterMap: conversion: callable = lambda x: x -# define the conventions between Solcast and PVLib nomenclature and units +# define the conventions between Solcast and pvlib nomenclature and units VARIABLE_MAP = [ # air_temp -> temp_air (deg C) ParameterMap("air_temp", "temp_air"), From d45ddec1a7283916961fd3e3a28094f9714040e3 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Tue, 19 Dec 2023 13:58:09 +1100 Subject: [PATCH 24/30] Adams's feedback --- pvlib/iotools/solcast.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index e22892d386..b90e3e0b1c 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -67,6 +67,8 @@ def get_solcast_tmy( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is the index shifted to the midpoint of each interval + from Solcast's "period end" convention. kwargs: Optional parameters passed to the API. See [3]_ for full list of parameters. @@ -151,7 +153,7 @@ def get_solcast_historic( end : optional, datetime-like Last day of the requested period. Must include one of ``end`` or ``duration``. - duration : optional, default is None + duration : optional, ISO 8601 compliant duration Must include either ``end`` or ``duration``. ISO 8601 compliant duration for the historic data, like "P1D" for one day of data. @@ -159,6 +161,8 @@ def get_solcast_historic( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is the index shifted to the midpoint of each interval + from Solcast's "period end" convention. api_key : str To access Solcast data you will need an API key [1]_. kwargs: @@ -238,6 +242,8 @@ def get_solcast_forecast( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is the index shifted to the midpoint of each interval + from Solcast's "period end" convention. kwargs: Optional parameters passed to the API. See [2]_ for full list of parameters. @@ -259,12 +265,13 @@ def get_solcast_forecast( >>> ) you can pass any of the parameters listed in the API docs, - like asking for specific variables: + like asking for specific variables for a specific time horizon: >>> df, meta = pvlib.iotools.solcast.get_solcast_forecast( >>> latitude=-33.856784, >>> longitude=151.215297, >>> output_parameters=['dni', 'clearsky_dni', 'snow_soiling_rooftop'], + >>> hours=24, >>> api_key="your-key" >>> ) @@ -308,6 +315,8 @@ def get_solcast_live( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. + Time is the index shifted to the midpoint of each interval + from Solcast's "period end" convention. kwargs: Optional parameters passed to the API. See [2]_ for full list of parameters. @@ -429,7 +438,7 @@ def _get_solcast( map_variables: bool, default: True When true, renames columns of the DataFrame to pvlib's variable names where applicable. See variable :const:`VARIABLE_MAP`. - Time is the index as midpoint of each interval + Time is the index shifted to the midpoint of each interval from Solcast's "period end" convention. Returns From 9be7792cb3ec796389be173c600e8b1c78ebfe55 Mon Sep 17 00:00:00 2001 From: lorenzo-solcast <142359869+lorenzo-solcast@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:57:54 +1100 Subject: [PATCH 25/30] Update pvlib/iotools/solcast.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/solcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index b90e3e0b1c..a32d963e26 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -386,7 +386,7 @@ def _solcast2pvlib(data): Parameters ---------- data : pandas.DataFrame - contains the data as returned from the Solcast API + contains the data returned from the Solcast API Returns ------- From 6fc890bae3fff7883aa612392eb8d87c447fc984 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Tue, 19 Dec 2023 21:36:12 +1100 Subject: [PATCH 26/30] Adams's feedback --- pvlib/iotools/solcast.py | 26 +++++++++++++++++++++----- pvlib/tests/iotools/test_solcast.py | 23 ++++++++++------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index b90e3e0b1c..c1b67701ee 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -76,7 +76,7 @@ def get_solcast_tmy( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested.The times + containing the values for the parameters requested. The times in the DataFrame index indicate the midpoint of each interval. metadata: dict latitude and longitude of the request. @@ -105,6 +105,10 @@ def get_solcast_tmy( .. [1] `Solcast TMY Docs `_ .. [2] `Get an API Key `_ .. [3] `Solcast API Docs `_ + + See Also + -------- + pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn """ params = dict( @@ -134,7 +138,7 @@ def get_solcast_historic( map_variables=True, **kwargs ): - """Get historical irradiance and weather estimated actuals + """Get historical irradiance and weather estimates for up to 31 days of data at a time for a requested location, derived from satellite (clouds and irradiance @@ -172,7 +176,7 @@ def get_solcast_historic( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested.The times + containing the values for the parameters requested. The times in the DataFrame index indicate the midpoint of each interval. metadata: dict latitude and longitude of the request. @@ -202,6 +206,10 @@ def get_solcast_historic( ---------- .. [1] `Get an API Key `_ .. [2] `Solcast API Docs `_ + + See Also + -------- + pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn """ params = dict( @@ -279,6 +287,10 @@ def get_solcast_forecast( ---------- .. [1] `Get an API Key `_ .. [2] `Solcast API Docs `_ + + See Also + -------- + pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn """ params = dict( @@ -324,7 +336,7 @@ def get_solcast_live( Returns ------- data : pandas.DataFrame - containing the values for the parameters requested.The times + containing the values for the parameters requested. The times in the DataFrame index indicate the midpoint of each interval. metadata: dict latitude and longitude of the request. @@ -361,6 +373,10 @@ def get_solcast_live( ---------- .. [1] `Get an API Key `_ .. [2] `Solcast API Docs `_ + + See Also + -------- + pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn """ params = dict( @@ -421,7 +437,7 @@ def _get_solcast( api_key, map_variables ): - """retrieves weather, irradiance and power data from the Solcast API + """retrieves weather, irradiance and power data from the Solcast API. Parameters ---------- diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 739ced205d..8689894d62 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -1,15 +1,12 @@ import pandas as pd -from pvlib.iotools.solcast import ( - get_solcast_live, get_solcast_tmy, _solcast2pvlib, get_solcast_historic, - get_solcast_forecast -) +from pvlib.iotools import solcast import pytest @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "live/radiation_and_weather", - get_solcast_live, + solcast.get_solcast_live, dict( api_key="1234", latitude=-33.856784, @@ -41,7 +38,7 @@ def test_get_solcast_live( pd.testing.assert_frame_equal( function(**params)[0], - _solcast2pvlib( + solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]]) ) @@ -51,7 +48,7 @@ def test_get_solcast_live( @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "tmy/radiation_and_weather", - get_solcast_tmy, + solcast.get_solcast_tmy, dict( api_key="1234", latitude=-33.856784, @@ -83,7 +80,7 @@ def test_get_solcast_tmy( pd.testing.assert_frame_equal( function(**params)[0], - _solcast2pvlib( + solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]]) ) @@ -119,14 +116,14 @@ def test_get_solcast_tmy( ) ]) def test_solcast2pvlib(in_df, out_df): - df = _solcast2pvlib(in_df) + df = solcast._solcast2pvlib(in_df) pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "historic/radiation_and_weather", - get_solcast_historic, + solcast.get_solcast_historic, dict( api_key="1234", latitude=-33.856784, @@ -160,7 +157,7 @@ def test_get_solcast_historic( pd.testing.assert_frame_equal( function(**params)[0], - _solcast2pvlib( + solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]] ) @@ -171,7 +168,7 @@ def test_get_solcast_historic( @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "forecast/radiation_and_weather", - get_solcast_forecast, + solcast.get_solcast_forecast, dict( api_key="1234", latitude=-33.856784, @@ -207,7 +204,7 @@ def test_get_solcast_forecast( pd.testing.assert_frame_equal( function(**params)[0], - _solcast2pvlib( + solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]] ) From aa5005d529feadf3f1e40281098cc5e2d4cb282e Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Tue, 19 Dec 2023 23:07:04 +0100 Subject: [PATCH 27/30] Last minor changes --- pvlib/iotools/solcast.py | 16 ++++++++++------ pvlib/tests/iotools/test_solcast.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 76e658d4fd..4fcee40050 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -108,7 +108,8 @@ def get_solcast_tmy( See Also -------- - pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn + pvlib.iotools.get_solcast_historic, pvlib.iotools.get_solcast_forecast, + pvlib.iotools.get_solcast_live """ params = dict( @@ -138,7 +139,7 @@ def get_solcast_historic( map_variables=True, **kwargs ): - """Get historical irradiance and weather estimates + """Get historical irradiance and weather estimates. for up to 31 days of data at a time for a requested location, derived from satellite (clouds and irradiance @@ -209,7 +210,8 @@ def get_solcast_historic( See Also -------- - pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn + pvlib.iotools.get_solcast_tmy, pvlib.iotools.get_solcast_forecast, + pvlib.iotools.get_solcast_live """ params = dict( @@ -290,7 +292,8 @@ def get_solcast_forecast( See Also -------- - pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn + pvlib.iotools.get_solcast_tmy, pvlib.iotools.get_solcast_historic, + pvlib.iotools.get_solcast_live """ params = dict( @@ -376,7 +379,8 @@ def get_solcast_live( See Also -------- - pvlib.iotools.read_solaranywhere, pvlib.iotools.get_bsrn + pvlib.iotools.get_solcast_tmy, pvlib.iotools.get_solcast_historic, + pvlib.iotools.get_solcast_forecast """ params = dict( @@ -437,7 +441,7 @@ def _get_solcast( api_key, map_variables ): - """retrieves weather, irradiance and power data from the Solcast API. + """Retrieve weather, irradiance and power data from the Solcast API. Parameters ---------- diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 8689894d62..0a23008858 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -1,12 +1,12 @@ import pandas as pd -from pvlib.iotools import solcast +import pvlib import pytest @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "live/radiation_and_weather", - solcast.get_solcast_live, + pvlib.iotools.get_solcast_live, dict( api_key="1234", latitude=-33.856784, @@ -38,7 +38,7 @@ def test_get_solcast_live( pd.testing.assert_frame_equal( function(**params)[0], - solcast._solcast2pvlib( + pvlib.iotools.solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]]) ) @@ -48,7 +48,7 @@ def test_get_solcast_live( @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "tmy/radiation_and_weather", - solcast.get_solcast_tmy, + pvlib.iotools.get_solcast_tmy, dict( api_key="1234", latitude=-33.856784, @@ -80,7 +80,7 @@ def test_get_solcast_tmy( pd.testing.assert_frame_equal( function(**params)[0], - solcast._solcast2pvlib( + pvlib.iotools.solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]]) ) @@ -116,14 +116,14 @@ def test_get_solcast_tmy( ) ]) def test_solcast2pvlib(in_df, out_df): - df = solcast._solcast2pvlib(in_df) + df = pvlib.iotools.solcast._solcast2pvlib(in_df) pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "historic/radiation_and_weather", - solcast.get_solcast_historic, + pvlib.iotools.get_solcast_historic, dict( api_key="1234", latitude=-33.856784, @@ -157,7 +157,7 @@ def test_get_solcast_historic( pd.testing.assert_frame_equal( function(**params)[0], - solcast._solcast2pvlib( + pvlib.iotools.solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]] ) @@ -168,7 +168,7 @@ def test_get_solcast_historic( @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "forecast/radiation_and_weather", - solcast.get_solcast_forecast, + pvlib.iotools.get_solcast_forecast, dict( api_key="1234", latitude=-33.856784, @@ -204,7 +204,7 @@ def test_get_solcast_forecast( pd.testing.assert_frame_equal( function(**params)[0], - solcast._solcast2pvlib( + pvlib.iotools.solcast._solcast2pvlib( pd.DataFrame.from_dict( json_response[list(json_response.keys())[0]] ) From 593d8ac46b14203f229cc2324f39f59ca6007778 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Wed, 20 Dec 2023 00:06:14 +1100 Subject: [PATCH 28/30] added test for _get_solcast --- pvlib/tests/iotools/test_solcast.py | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 0a23008858..ca38ab31c6 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -3,6 +3,56 @@ import pytest +@pytest.mark.parametrize("endpoint,params,api_key,json_response", [ + ( + "live/radiation_and_weather", + dict( + latitude=-33.856784, + longitude=151.215297, + output_parameters='dni,ghi' + ), + "1234", + {'estimated_actuals': + [{'dni': 836, 'ghi': 561, + 'period_end': '2023-09-18T05:00:00.0000000Z', 'period': 'PT30M'}, + {'dni': 866, 'ghi': 643, + 'period_end': '2023-09-18T04:30:00.0000000Z', 'period': 'PT30M'}, + {'dni': 890, 'ghi': 713, + 'period_end': '2023-09-18T04:00:00.0000000Z', 'period': 'PT30M'}, + {'dni': 909, 'ghi': 768, + 'period_end': '2023-09-18T03:30:00.0000000Z', 'period': 'PT30M'}] + } + ), +]) +def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): + mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ + f"latitude={params['latitude']}&" \ + f"longitude={params['longitude']}&" \ + f"output_parameters={params['output_parameters']}" + + requests_mock.get(mock_url, json=json_response) + + # with variables remapping + pd.testing.assert_frame_equal( + pvlib.iotools.solcast._get_solcast( + endpoint, params, api_key, True + ), + pvlib.iotools.solcast._solcast2pvlib( + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]]) + ) + ) + + # no remapping of varaibles + pd.testing.assert_frame_equal( + pvlib.iotools.solcast._get_solcast( + endpoint, params, api_key, False + ), + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]]) + ) + + @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "live/radiation_and_weather", From 26942dc781cc1da2a118f29a6f5a9020f120c2e6 Mon Sep 17 00:00:00 2001 From: hugh-solcast <143680553+hugh-solcast@users.noreply.github.com> Date: Wed, 20 Dec 2023 11:29:57 +1100 Subject: [PATCH 29/30] feat: add additional test coverage (#5) Co-authored-by: Hugh Cutcher --- pvlib/tests/iotools/test_solcast.py | 123 +++++++++++++++++++--------- 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index ca38ab31c6..481f68c6be 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -1,8 +1,13 @@ +from unittest.mock import patch + +from unittest.mock import patch + import pandas as pd import pvlib import pytest +@pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,params,api_key,json_response", [ ( "live/radiation_and_weather", @@ -24,7 +29,7 @@ } ), ]) -def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): +def test__get_solcast(requests_mock, endpoint, params, api_key, json_response, map_variables): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ f"latitude={params['latitude']}&" \ f"longitude={params['longitude']}&" \ @@ -52,7 +57,7 @@ def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): json_response[list(json_response.keys())[0]]) ) - +@pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "live/radiation_and_weather", @@ -76,25 +81,32 @@ def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): ), ]) def test_get_solcast_live( - requests_mock, endpoint, function, params, json_response + requests_mock, endpoint, function, params, json_response, map_variables ): - - mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"&latitude={params['latitude']}&" \ - f"longitude={params['longitude']}&" \ - f"output_parameters={params['output_parameters']}&format=json" + mock_url = ( + f"https://api.solcast.com.au/data/{endpoint}?" + f"&latitude={params['latitude']}&" + f"longitude={params['longitude']}&" + f"output_parameters={params['output_parameters']}&format=json" + ) requests_mock.get(mock_url, json=json_response) - pd.testing.assert_frame_equal( - function(**params)[0], - pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( - json_response[list(json_response.keys())[0]]) + if map_variables: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pvlib.iotools.solcast._solcast2pvlib( + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + ), + ) + else: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), ) - ) +@pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "tmy/radiation_and_weather", @@ -119,7 +131,7 @@ def test_get_solcast_live( ), ]) def test_get_solcast_tmy( - requests_mock, endpoint, function, params, json_response + requests_mock, endpoint, function, params, json_response, map_variables ): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ @@ -128,13 +140,18 @@ def test_get_solcast_tmy( requests_mock.get(mock_url, json=json_response) - pd.testing.assert_frame_equal( - function(**params)[0], - pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( - json_response[list(json_response.keys())[0]]) + if map_variables: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pvlib.iotools.solcast._solcast2pvlib( + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + ), + ) + else: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), ) - ) @pytest.mark.parametrize("in_df,out_df", [ @@ -170,6 +187,7 @@ def test_solcast2pvlib(in_df, out_df): pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) +@pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "historic/radiation_and_weather", @@ -197,7 +215,7 @@ def test_solcast2pvlib(in_df, out_df): ), ]) def test_get_solcast_historic( - requests_mock, endpoint, function, params, json_response + requests_mock, endpoint, function, params, json_response, map_variables ): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ f"&latitude={params['latitude']}&" \ @@ -205,16 +223,21 @@ def test_get_solcast_historic( requests_mock.get(mock_url, json=json_response) - pd.testing.assert_frame_equal( - function(**params)[0], - pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( - json_response[list(json_response.keys())[0]] - ) + if map_variables: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pvlib.iotools.solcast._solcast2pvlib( + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + ), + ) + else: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), ) - ) +@pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( "forecast/radiation_and_weather", @@ -244,7 +267,7 @@ def test_get_solcast_historic( ), ]) def test_get_solcast_forecast( - requests_mock, endpoint, function, params, json_response + requests_mock, endpoint, function, params, json_response, map_variables ): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ f"&latitude={params['latitude']}&" \ @@ -252,11 +275,37 @@ def test_get_solcast_forecast( requests_mock.get(mock_url, json=json_response) - pd.testing.assert_frame_equal( - function(**params)[0], - pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( - json_response[list(json_response.keys())[0]] - ) + if map_variables: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pvlib.iotools.solcast._solcast2pvlib( + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + ), ) - ) + else: + pd.testing.assert_frame_equal( + function(**params, map_variables=map_variables)[0], + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), + ) + + +@pytest.mark.parametrize( + "function", + [ + pvlib.iotools.get_solcast_forecast, + pvlib.iotools.get_solcast_live, + pvlib.iotools.get_solcast_tmy, + pvlib.iotools.get_solcast_historic, + ], +) +@patch("requests.api.request") +def test_raises_exception(mock_response, function): + dummy_args = { + "latitude": 0, + "longitude": 0, + "api_key": "", + } + with patch.object(mock_response, "status_code", return_value=404): + with pytest.raises(Exception): + function(**dummy_args) + mock_response.json.assert_called_once() From 147a2b186f28eee1702c4471ce906d58bacdc133 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Wed, 20 Dec 2023 01:38:19 +1100 Subject: [PATCH 30/30] linting --- pvlib/tests/iotools/test_solcast.py | 41 +++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 481f68c6be..19b00b8611 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -1,13 +1,9 @@ from unittest.mock import patch - -from unittest.mock import patch - import pandas as pd import pvlib import pytest -@pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,params,api_key,json_response", [ ( "live/radiation_and_weather", @@ -29,7 +25,7 @@ } ), ]) -def test__get_solcast(requests_mock, endpoint, params, api_key, json_response, map_variables): +def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ f"latitude={params['latitude']}&" \ f"longitude={params['longitude']}&" \ @@ -48,7 +44,7 @@ def test__get_solcast(requests_mock, endpoint, params, api_key, json_response, m ) ) - # no remapping of varaibles + # no remapping of variables pd.testing.assert_frame_equal( pvlib.iotools.solcast._get_solcast( endpoint, params, api_key, False @@ -57,6 +53,7 @@ def test__get_solcast(requests_mock, endpoint, params, api_key, json_response, m json_response[list(json_response.keys())[0]]) ) + @pytest.mark.parametrize("map_variables", [True, False]) @pytest.mark.parametrize("endpoint,function,params,json_response", [ ( @@ -96,13 +93,17 @@ def test_get_solcast_live( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ) ), ) else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ), ) @@ -144,13 +145,17 @@ def test_get_solcast_tmy( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ) ), ) else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ), ) @@ -227,13 +232,17 @@ def test_get_solcast_historic( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ) ), ) else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ), ) @@ -279,13 +288,17 @@ def test_get_solcast_forecast( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]) + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ) ), ) else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), + pd.DataFrame.from_dict( + json_response[list(json_response.keys())[0]] + ), )