Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 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
----------
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]]
)
)
)