From 29ecd9b2d79c23edf35724e2ec9e13170fe72613 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Wed, 13 Dec 2023 11:32:04 +1100 Subject: [PATCH 1/2] feedback --- 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..67b46b20fe 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 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 d55a0c3545b6e3f61477f419b9a9302875aad894 Mon Sep 17 00:00:00 2001 From: lorenzori Date: Wed, 13 Dec 2023 12:06:22 +1100 Subject: [PATCH 2/2] feedback --- 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 67b46b20fe..7248d5f2b0 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -47,7 +47,7 @@ def get_solcast_tmy( """Get irradiance and weather for a Typical Meteorological Year (TMY) at a requested location. - Data derived from a multi-year time series selected to present the + 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.