Skip to content

Accept albedo in weather input to ModelChain.run_model method #1469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
7 changes: 5 additions & 2 deletions docs/sphinx/source/whatsnew/v0.9.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ Deprecations

Enhancements
~~~~~~~~~~~~
* albedo can now be provided as a column in the `weather` DataFrame input to
:py:method:`pvlib.modelchain.ModelChain.run_model`. (:issue:`1387`, :pull:`1469`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the PVSystem and Array changes should be noted too


Bug fixes
~~~~~~~~~
* :py:func:`pvlib.irradiance.get_total_irradiance` and
:py:func:`pvlib.solarposition.spa_python` now raise an error instead
of silently ignoring unknown parameters (:pull:`1437`)
of silently ignoring unknown parameters. (:pull:`1437`)
* Fix a bug in :py:func:`pvlib.solarposition.sun_rise_set_transit_ephem`
where passing localized timezones with large UTC offsets could return
rise/set/transit times for the wrong day in recent versions of ``ephem``
rise/set/transit times for the wrong day in recent versions of ``ephem``.
(:issue:`1449`, :pull:`1448`)


Expand All @@ -41,3 +43,4 @@ Contributors
* Naman Priyadarshi (:ghuser:`Naman-Priyadarshi`)
* Chencheng Luo (:ghuser:`roger-lcc`)
* Prajwal Borkar (:ghuser:`PrajwalBorkar`)
* Cliff Hansen (:ghuser:`cwhanse`)
4 changes: 2 additions & 2 deletions pvlib/clearsky.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,8 +960,8 @@ def bird(zenith, airmass_relative, aod380, aod500, precipitable_water,
Extraterrestrial radiation [W/m^2], defaults to 1364[W/m^2]
asymmetry : numeric
Asymmetry factor, defaults to 0.85
albedo : numeric
Albedo, defaults to 0.2
albedo : numeric, default 0.2
Ground surface albedo. [unitless]

Returns
-------
Expand Down
4 changes: 2 additions & 2 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
airmass : None or numeric, default None
Relative airmass (not adjusted for pressure). [unitless]
albedo : numeric, default 0.25
Surface albedo. [unitless]
Ground surface albedo. [unitless]
surface_type : None or str, default None
Surface type. See :py:func:`~pvlib.irradiance.get_ground_diffuse` for
the list of accepted values.
Expand Down Expand Up @@ -1872,7 +1872,7 @@ def gti_dirint(poa_global, aoi, solar_zenith, solar_azimuth, times,
applied.

albedo : numeric, default 0.25
Surface albedo
Ground surface albedo. [unitless]

model : String, default 'perez'
Irradiance model. See :py:func:`get_sky_diffuse` for allowed values.
Expand Down
56 changes: 48 additions & 8 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1471,11 +1471,14 @@ def prepare_inputs(self, weather):

Parameters
----------
weather : DataFrame, or tuple or list of DataFrame
weather : DataFrame, or tuple or list of DataFrames
Required column names include ``'dni'``, ``'ghi'``, ``'dhi'``.
Optional column names are ``'wind_speed'``, ``'temp_air'``; if not
Optional column names are ``'wind_speed'``, ``'temp_air'``,
``'albedo'``.

If optional columns ``'wind_speed'``, ``'temp_air'`` are not
provided, air temperature of 20 C and wind speed
of 0 m/s will be added to the DataFrame.
of 0 m/s will be added to the `weather` DataFrame.

If `weather` is a tuple or list, it must be of the same length and
order as the Arrays of the ModelChain's PVSystem.
Expand All @@ -1490,6 +1493,9 @@ def prepare_inputs(self, weather):
ValueError
If `weather` is a tuple or list with a different length than the
number of Arrays in the system.
ValueError
If ``'albedo'`` is a column in `weather` and is also an attribute
of the ModelChain's PVSystem.Arrays.

Notes
-----
Expand All @@ -1500,6 +1506,24 @@ def prepare_inputs(self, weather):
--------
ModelChain.complete_irradiance
"""
# transfer albedo from weather to mc.system.arrays if needed
if isinstance(weather, pd.DataFrame): # single weather, many arrays
if 'albedo' in weather.columns:
for array in self.system.arrays:
if hasattr(array, 'albedo'):
raise ValueError('albedo found in both weather and on'
' PVsystem.Array Provide albedo on'
' one or on neither, but not both.')
array.albedo = weather['albedo']
else: # multiple weather and arrays
for w, array in zip(weather, self.system.arrays):
if 'albedo' in w.columns:
if hasattr(array, 'albedo'):
raise ValueError('albedo found in both weather and on'
' PVsystem.Array Provide albedo on'
' one or on neither, but not both.')
array.albedo = w['albedo']

weather = _to_tuple(weather)
self._check_multiple_input(weather, strict=False)
self._verify_df(weather, required=['ghi', 'dni', 'dhi'])
Expand Down Expand Up @@ -1724,16 +1748,32 @@ def run_model(self, weather):
Parameters
----------
weather : DataFrame, or tuple or list of DataFrame
Irradiance column names must include ``'dni'``, ``'ghi'``, and
``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'``
Column names must include:

- ``'dni'``
- ``'ghi'``
- ``'dhi'``

Optional columns are:

- ``'temp_air'``
- ``'cell_temperature'``
- ``'module_temperature'``
- ``'wind_speed'``
- ``'albedo'``

If optional columns ``'temp_air'`` and ``'wind_speed'``
are not provided, air temperature of 20 C and wind speed of 0 m/s
are added to the DataFrame. If optional column
``'cell_temperature'`` is provided, these values are used instead
of `temperature_model`. If optional column `module_temperature`
of `temperature_model`. If optional column ``'module_temperature'``
is provided, `temperature_model` must be ``'sapm'``.

If list or tuple, must be of the same length and order as the
Arrays of the ModelChain's PVSystem.
If optional column ``'albedo'`` is provided, ``'albedo'`` may not
be present on the ModelChain's PVSystem.Arrays.

If weather is a list or tuple, it must be of the same length and
order as the Arrays of the ModelChain's PVSystem.

Returns
-------
Expand Down
27 changes: 15 additions & 12 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class PVSystem:
a single array is created from the other parameters (e.g.
`surface_tilt`, `surface_azimuth`). Must contain at least one Array,
if length of arrays is 0 a ValueError is raised. If `arrays` is
specified the following parameters are ignored:
specified the following PVSystem parameters are ignored:

- `surface_tilt`
- `surface_azimuth`
Expand All @@ -157,13 +157,16 @@ class PVSystem:
North=0, East=90, South=180, West=270.

albedo : None or float, default None
The ground albedo. If ``None``, will attempt to use
``surface_type`` and ``irradiance.SURFACE_ALBEDOS``
to lookup albedo.
Ground surface albedo. If ``None``, then ``surface_type`` is used
to look up a value in ``irradiance.SURFACE_ALBEDOS``.
If ``surface_type`` is also None then a ground surface albedo
of 0.25 is used. For time-dependent albedos, add ``'albedo'`` to
the input ``'weather'`` DataFrame for
:py:class:`pvlib.modelchain.ModelChain` methods.

surface_type : None or string, default None
The ground surface type. See ``irradiance.SURFACE_ALBEDOS``
for valid values.
The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for
valid values.

module : None or string, default None
The model name of the modules.
Expand Down Expand Up @@ -1258,14 +1261,14 @@ class Array:
If not provided, a FixedMount with zero tilt is used.

albedo : None or float, default None
The ground albedo. If ``None``, will attempt to use
``surface_type`` to look up an albedo value in
``irradiance.SURFACE_ALBEDOS``. If a surface albedo
cannot be found then 0.25 is used.
Ground surface albedo. If ``None``, then ``surface_type`` is used
to look up a value in ``irradiance.SURFACE_ALBEDOS``.
If ``surface_type`` is also None then a ground surface albedo
of 0.25 is used.

surface_type : None or string, default None
The ground surface type. See ``irradiance.SURFACE_ALBEDOS``
for valid values.
The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for valid
values.

module : None or string, default None
The model name of the modules.
Expand Down
24 changes: 24 additions & 0 deletions pvlib/tests/test_clearsky.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,30 @@ def test_bird():
assert np.allclose(
testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:48], rtol=1e-3
)
# repeat test with albedo as a Series
alb_series = pd.Series(0.2, index=times)
irrads = clearsky.bird(
zenith, airmass, aod_380nm, aod_500nm, h2o_cm, o3_cm, press_mB * 100.,
etr, b_a, alb_series
)
Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names)
direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.)
assert np.allclose(
testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:48], rtol=1e-3
)
direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.)
assert np.allclose(
testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:48], rtol=1e-3
)
global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.)
assert np.allclose(
testdata['Global Hz'].where(dusk, 0.), global_horz[1:48], rtol=1e-3
)
diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.)
assert np.allclose(
testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:48], rtol=1e-3
)

# test keyword parameters
irrads2 = clearsky.bird(
zenith, airmass, aod_380nm, aod_500nm, h2o_cm, dni_extra=etr
Expand Down
45 changes: 41 additions & 4 deletions pvlib/tests/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,29 +120,38 @@ def test_get_extra_radiation_invalid():
irradiance.get_extra_radiation(300, method='invalid')


def test_grounddiffuse_simple_float():
def test_get_ground_diffuse_simple_float():
result = irradiance.get_ground_diffuse(40, 900)
assert_allclose(result, 26.32000014911496)


def test_grounddiffuse_simple_series(irrad_data):
def test_get_ground_diffuse_simple_series(irrad_data):
ground_irrad = irradiance.get_ground_diffuse(40, irrad_data['ghi'])
assert ground_irrad.name == 'diffuse_ground'


def test_grounddiffuse_albedo_0(irrad_data):
def test_get_ground_diffuse_albedo_0(irrad_data):
ground_irrad = irradiance.get_ground_diffuse(
40, irrad_data['ghi'], albedo=0)
assert 0 == ground_irrad.all()


def test_get_ground_diffuse_albedo_series(times):
albedo = pd.Series(0.2, index=times)
ground_irrad = irradiance.get_ground_diffuse(
45, pd.Series(1000, index=times), albedo)
expected = albedo * 0.5 * (1 - np.sqrt(2) / 2.) * 1000
expected.name = 'diffuse_ground'
assert_series_equal(ground_irrad, expected)


def test_grounddiffuse_albedo_invalid_surface(irrad_data):
with pytest.raises(KeyError):
irradiance.get_ground_diffuse(
40, irrad_data['ghi'], surface_type='invalid')


def test_grounddiffuse_albedo_surface(irrad_data):
def test_get_ground_diffuse_albedo_surface(irrad_data):
result = irradiance.get_ground_diffuse(40, irrad_data['ghi'],
surface_type='sand')
assert_allclose(result, [0, 3.731058, 48.778813, 12.035025], atol=1e-4)
Expand Down Expand Up @@ -387,6 +396,26 @@ def test_get_total_irradiance(irrad_data, ephem_data, dni_et,
'poa_ground_diffuse']


def test_get_total_irradiance_albedo(
irrad_data, ephem_data, dni_et, relative_airmass):
models = ['isotropic', 'klucher',
'haydavies', 'reindl', 'king', 'perez']
albedo = pd.Series(0.2, index=ephem_data.index)
for model in models:
total = irradiance.get_total_irradiance(
32, 180,
ephem_data['apparent_zenith'], ephem_data['azimuth'],
dni=irrad_data['dni'], ghi=irrad_data['ghi'],
dhi=irrad_data['dhi'],
dni_extra=dni_et, airmass=relative_airmass,
model=model,
albedo=albedo)

assert total.columns.tolist() == ['poa_global', 'poa_direct',
'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse']


@pytest.mark.parametrize('model', ['isotropic', 'klucher',
'haydavies', 'reindl', 'king', 'perez'])
def test_get_total_irradiance_scalars(model):
Expand Down Expand Up @@ -698,6 +727,14 @@ def test_gti_dirint():

assert_frame_equal(output, expected)

# test with albedo as a Series
albedo = pd.Series(0.05, index=times)
output = irradiance.gti_dirint(
poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth,
albedo=albedo)

assert_frame_equal(output, expected)

# test temp_dew input
temp_dew = np.array([70, 80, 20])
output = irradiance.gti_dirint(
Expand Down
39 changes: 39 additions & 0 deletions pvlib/tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,45 @@ def test_prepare_inputs_multi_weather(
assert len(mc.results.total_irrad) == num_arrays


@pytest.mark.parametrize("input_type", [tuple, list])
def test_prepare_inputs_transfer_albedo(
sapm_dc_snl_ac_system_Array, location, input_type):
times = pd.date_range(start='20160101 1200-0700',
end='20160101 1800-0700', freq='6H')
mc = ModelChain(sapm_dc_snl_ac_system_Array, location)
# albedo on pvsystem but not in weather
weather = pd.DataFrame({'ghi': 1, 'dhi': 1, 'dni': 1},
index=times)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this comment doesn't match the test code

# weather as a single DataFrame
mc.prepare_inputs(weather)
num_arrays = sapm_dc_snl_ac_system_Array.num_arrays
assert len(mc.results.total_irrad) == num_arrays
# repeat with tuple of weather
mc.prepare_inputs(input_type((weather, weather)))
num_arrays = sapm_dc_snl_ac_system_Array.num_arrays
assert len(mc.results.total_irrad) == num_arrays
# albedo on both weather and system
weather['albedo'] = 0.5
with pytest.raises(ValueError, match='albedo found in both weather'):
mc.prepare_inputs(weather)
with pytest.raises(ValueError, match='albedo found in both weather'):
mc.prepare_inputs(input_type((weather, weather)))
# albedo on weather but not system
pvsystem = sapm_dc_snl_ac_system_Array
for a in pvsystem.arrays:
del a.albedo
mc = ModelChain(pvsystem, location)
mc = mc.prepare_inputs(weather)
assert (mc.system.arrays[0].albedo.values == 0.5).all()
# again with weather as a tuple
for a in pvsystem.arrays:
del a.albedo
mc = ModelChain(pvsystem, location)
mc = mc.prepare_inputs(input_type((weather, weather)))
for a in mc.system.arrays:
assert (a.albedo.values == 0.5).all()


def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location):
mc = ModelChain(sapm_dc_snl_ac_system, location)
weather = pd.DataFrame()
Expand Down