Skip to content

[BUG]: don't infer spectral model in ModelChain #2253

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
1 change: 0 additions & 1 deletion docs/sphinx/source/reference/modelchain.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ on the information in the associated :py:class:`~pvsystem.PVSystem` object.
modelchain.ModelChain.infer_dc_model
modelchain.ModelChain.infer_ac_model
modelchain.ModelChain.infer_aoi_model
modelchain.ModelChain.infer_spectral_model
modelchain.ModelChain.infer_temperature_model
modelchain.ModelChain.infer_losses_model

Expand Down
14 changes: 9 additions & 5 deletions docs/sphinx/source/whatsnew/v0.11.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
v0.11.3 (Anticipated March, 2025)
---------------------------------

Breaking Changes
~~~~~~~~~~~~~~~~
* The pvlib.location.Location.pytz attribute is now read only. The
pytz attribute is now set internally to be consistent with the
pvlib.location.Location.tz attribute. (:issue:`2340`, :pull:`2341`)
* Users must now provide ModelChain.spectral_model, or the 'no_loss' spectral
model is assumed. pvlib.modelchain.ModelChain no longer attempts to infer
the spectral model from PVSystem attributes. (:issue:`2017`, :pull:`2253`)

Bug fixes
~~~~~~~~~
* Fix a bug in :py:func:`pvlib.bifacial.get_irradiance_poa` which may have yielded non-zero
Expand Down Expand Up @@ -62,11 +71,6 @@ Maintenance
* asv 0.4.2 upgraded to asv 0.6.4 to fix CI failure due to pinned older conda.
(:pull:`2352`)

Breaking Changes
~~~~~~~~~~~~~~~~
* The pvlib.location.Location.pytz attribute is now read only. The
pytz attribute is now set internally to be consistent with the
pvlib.location.Location.tz attribute. (:issue:`2340`, :pull:`2341`)

Contributors
~~~~~~~~~~~~
Expand Down
53 changes: 18 additions & 35 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

# Optional keys to communicate temperature data. If provided,
# 'cell_temperature' overrides ModelChain.temperature_model and sets
# ModelChain.cell_temperature to the data. If 'module_temperature' is provdied,
# ModelChain.cell_temperature to the data. If 'module_temperature' is provided,
# overrides ModelChain.temperature_model with
# pvlib.temperature.sapm_celL_from_module
TEMPERATURE_KEYS = ('module_temperature', 'cell_temperature')
Expand Down Expand Up @@ -253,7 +253,7 @@
def _head(obj):
try:
return obj[:3]
except:
except Exception:

Check warning on line 256 in pvlib/modelchain.py

View check run for this annotation

Codecov / codecov/patch

pvlib/modelchain.py#L256

Added line #L256 was not covered by tests
return obj

if type(self.dc) is tuple:
Expand All @@ -269,7 +269,7 @@
'\n')
lines = []
for attr in mc_attrs:
if not (attr.startswith('_') or attr=='times'):
if not (attr.startswith('_') or attr == 'times'):
lines.append(f' {attr}: ' + _mcr_repr(getattr(self, attr)))
desc4 = '\n'.join(lines)
return (desc1 + desc2 + desc3 + desc4)
Expand Down Expand Up @@ -330,12 +330,15 @@
'interp' and 'no_loss'. The ModelChain instance will be passed as the
first argument to a user-defined function.

spectral_model : str, or function, optional
If not specified, the model will be inferred from the parameters that
are common to all of system.arrays[i].module_parameters.
Valid strings are 'sapm', 'first_solar', 'no_loss'.
spectral_model : str or function, optional
Valid strings are:

- ``'sapm'``
- ``'first_solar'``
- ``'no_loss'``

The ModelChain instance will be passed as the first argument to
a user-defined function.
a user-defined function. If not specified, ``'no_loss'`` is assumed.

temperature_model : str or function, optional
Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'.
Expand Down Expand Up @@ -386,7 +389,6 @@

self.results = ModelChainResult()


@classmethod
def with_pvwatts(cls, system, location,
clearsky_model='ineichen',
Expand Down Expand Up @@ -855,9 +857,7 @@

@spectral_model.setter
def spectral_model(self, model):
if model is None:
self._spectral_model = self.infer_spectral_model()
elif isinstance(model, str):
if isinstance(model, str):
model = model.lower()
if model == 'first_solar':
self._spectral_model = self.first_solar_spectral_loss
Expand All @@ -867,30 +867,12 @@
self._spectral_model = self.no_spectral_loss
else:
raise ValueError(model + ' is not a valid spectral loss model')
else:
elif model is None:
# not setting a model is equivalent to setting no_loss
self._spectral_model = self.no_spectral_loss
else: # assume model is callable with 1st argument = the MC instance
self._spectral_model = partial(model, self)

def infer_spectral_model(self):
"""Infer spectral model from system attributes."""
module_parameters = tuple(
array.module_parameters for array in self.system.arrays)
params = _common_keys(module_parameters)
if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params:
return self.sapm_spectral_loss
elif ((('Technology' in params or
'Material' in params) and
(self.system._infer_cell_type() is not None)) or
'first_solar_spectral_coefficients' in params):
return self.first_solar_spectral_loss
else:
raise ValueError('could not infer spectral model from '
'system.arrays[i].module_parameters. Check that '
'the module_parameters for all Arrays in '
'system.arrays contain valid '
'first_solar_spectral_coefficients, a valid '
'Material or Technology value, or set '
'spectral_model="no_loss".')

def first_solar_spectral_loss(self):
self.results.spectral_modifier = self.system.first_solar_spectral_loss(
_tuple_from_dfs(self.results.weather, 'precipitable_water'),
Expand Down Expand Up @@ -1570,7 +1552,7 @@
----------
data : DataFrame
May contain columns ``'cell_temperature'`` or
``'module_temperaure'``.
``'module_temperature'``.

Returns
-------
Expand Down Expand Up @@ -1679,6 +1661,7 @@
self.prepare_inputs(weather)
self.aoi_model()
self.spectral_model()

self.effective_irradiance_model()

self._run_from_effective_irrad(weather)
Expand Down
42 changes: 19 additions & 23 deletions tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ def test_with_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather):


def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location):
mc = ModelChain(sapm_dc_snl_ac_system, location)
mc = ModelChain(sapm_dc_snl_ac_system, location, spectral_model='sapm')
times = pd.date_range('20160101 1200-0700', periods=2, freq='6h')
irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
index=times)
Expand Down Expand Up @@ -629,9 +629,13 @@ def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays,


def test_run_model_perez(sapm_dc_snl_ac_system, location):
mc = ModelChain(sapm_dc_snl_ac_system, location,
transposition_model='perez')
times = pd.date_range('20160101 1200-0700', periods=2, freq='6h')
mc = ModelChain(
sapm_dc_snl_ac_system,
location,
transposition_model="perez",
spectral_model="sapm",
)
times = pd.date_range("20160101 1200-0700", periods=2, freq="6h")
irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
index=times)
ac = mc.run_model(irradiance).results.ac
Expand All @@ -642,10 +646,14 @@ def test_run_model_perez(sapm_dc_snl_ac_system, location):


def test_run_model_gueymard_perez(sapm_dc_snl_ac_system, location):
mc = ModelChain(sapm_dc_snl_ac_system, location,
airmass_model='gueymard1993',
transposition_model='perez')
times = pd.date_range('20160101 1200-0700', periods=2, freq='6h')
mc = ModelChain(
sapm_dc_snl_ac_system,
location,
airmass_model="gueymard1993",
transposition_model="perez",
spectral_model="sapm",
)
times = pd.date_range("20160101 1200-0700", periods=2, freq="6h")
irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
index=times)
ac = mc.run_model(irradiance).results.ac
Expand Down Expand Up @@ -1272,18 +1280,6 @@ def test_singlediode_dc_arrays(location, dc_model,
assert isinstance(dc, (pd.Series, pd.DataFrame))


@pytest.mark.parametrize('dc_model', ['sapm', 'cec', 'cec_native'])
def test_infer_spectral_model(location, sapm_dc_snl_ac_system,
cec_dc_snl_ac_system,
cec_dc_native_snl_ac_system, dc_model):
dc_systems = {'sapm': sapm_dc_snl_ac_system,
'cec': cec_dc_snl_ac_system,
'cec_native': cec_dc_native_snl_ac_system}
system = dc_systems[dc_model]
mc = ModelChain(system, location, aoi_model='physical')
assert isinstance(mc, ModelChain)


@pytest.mark.parametrize('temp_model', [
'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp',
'noct_sam_temp'])
Expand Down Expand Up @@ -2002,9 +1998,9 @@ def test__irrad_for_celltemp():


def test_ModelChain___repr__(sapm_dc_snl_ac_system, location):

mc = ModelChain(sapm_dc_snl_ac_system, location,
name='my mc')
mc = ModelChain(
sapm_dc_snl_ac_system, location, name="my mc", spectral_model="sapm"
)

expected = '\n'.join([
'ModelChain: ',
Expand Down