Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/sphinx/source/whatsnew/v0.10.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 42 additions & 37 deletions pvlib/iotools/solcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
----------
Expand All @@ -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"
Expand All @@ -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,
Expand All @@ -93,8 +95,9 @@ def get_solcast_tmy(

References
----------
.. [1] `Get an API Key <https://toolkit.solcast.com.au/register>`_
.. [1] `Solcast TMY Docs <https://solcast.com/tmy>`_
.. [2] `Solcast API Docs <https://docs.solcast.com.au/>`_
.. [3] `Get an API Key <https://toolkit.solcast.com.au/register>`_
"""

params = dict(
Expand All @@ -111,7 +114,7 @@ def get_solcast_tmy(
map_variables=map_variables
)

return data, {}
return data, {"latitude": latitude, "longitude": longitude}


def get_solcast_historic(
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -210,7 +213,7 @@ def get_solcast_historic(
map_variables=map_variables
)

return data, {}
return data, {"latitude": latitude, "longitude": longitude}


def get_solcast_forecast(
Expand All @@ -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"
Expand All @@ -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'],
Expand All @@ -279,7 +283,7 @@ def get_solcast_forecast(
map_variables=map_variables
)

return data, {}
return data, {"latitude": latitude, "longitude": longitude}


def get_solcast_live(
Expand All @@ -300,27 +304,28 @@ 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"
>>> )

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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
-------
Expand All @@ -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:
Expand Down
101 changes: 97 additions & 4 deletions pvlib/tests/iotools/test_solcast.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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]])
)
Expand Down Expand Up @@ -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]])
)
Expand Down Expand Up @@ -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]]
)
)
)