From 3c708029f652b6d4e3e34e4b79236f106418fd34 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:04:42 +0200 Subject: [PATCH 01/16] Fix #2017 --- pvlib/modelchain.py | 76 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8456aac114..e0361ae7b6 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -330,10 +330,15 @@ class ModelChain: '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 + 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'. + are common to all of system.arrays[i].module_parameters. Valid strings + are: + + - ``'sapm'`` + - ``'first_solar'`` + - ``'no_loss'`` + The ModelChain instance will be passed as the first argument to a user-defined function. @@ -855,9 +860,7 @@ def spectral_model(self): @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 @@ -867,29 +870,60 @@ def spectral_model(self, model): self._spectral_model = self.no_spectral_loss else: raise ValueError(model + ' is not a valid spectral loss model') + elif model is None: + # None is a valid value, model inference will be done later + self._spectral_model = None else: self._spectral_model = partial(model, self) - def infer_spectral_model(self): + def infer_spectral_model(self): # TODO: support other spectral models? """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): + elif "first_solar_spectral_coefficients" in params: + # user explicitly sets spectral coefficients return self.first_solar_spectral_loss + elif ( + # cell type is known or can be inferred + ("Technology" in params or "Material" in params) + and (self.system._infer_cell_type() is not None) + ): + # This suggests models that provide default parameters per cell + # type can be used. However, some of them depend on other weather + # parameters, so we need to check if they are available. + # At this point, weather may not be available, so let's take that + # into account. + if self.results is not None and self.results.weather is not None: + # weather is available + if "precipitable_water" in self.results.weather: + return self.first_solar_spectral_loss + else: + return self.no_spectral_loss + else: + # weather is not available, so it's unknown what model to use. + # Warn user about this and default to no_spectral_loss. + warnings.warn( + "Weather data is not available to infer spectral model. " + "Defaulting to 'no_loss'. Explicitly set the " + "spectral model with the 'spectral_model' kwarg to " + "suppress this warning." + ) + return self.no_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".') + # TODO: list valid models in message down below + 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 " + "valid spectral coefficients, a valid " + "Material or Technology value, or a valid model " + "string; explicitly set the model with the " + "'spectral_model' kwarg." + ) def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( @@ -1678,7 +1712,13 @@ def run_model(self, weather): weather = _to_tuple(weather) self.prepare_inputs(weather) self.aoi_model() + + # spectral model is optional, and weather may impact model inference + # check if spectral model inference is requested by means of "None" + if self._spectral_model is None: + self._spectral_model = self.infer_spectral_model() self.spectral_model() + self.effective_irradiance_model() self._run_from_effective_irrad(weather) From 6121759c6eab2b4ee89d00bb4f7132ae9cf19f9c Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:12:43 +0200 Subject: [PATCH 02/16] rm TODOs --- pvlib/modelchain.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e0361ae7b6..14d0adbed1 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -876,7 +876,7 @@ def spectral_model(self, model): else: self._spectral_model = partial(model, self) - def infer_spectral_model(self): # TODO: support other spectral models? + def infer_spectral_model(self): """Infer spectral model from system attributes.""" module_parameters = tuple( array.module_parameters for array in self.system.arrays) @@ -913,7 +913,6 @@ def infer_spectral_model(self): # TODO: support other spectral models? ) return self.no_spectral_loss else: - # TODO: list valid models in message down below raise ValueError( "could not infer spectral model from " "system.arrays[i].module_parameters. Check that " From 08bfe62e12d43bb74e312c1cab24d2d6cdaa0655 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:13:31 +0200 Subject: [PATCH 03/16] cwhanse review Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com> --- pvlib/modelchain.py | 71 ++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 14d0adbed1..4ceebff18a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -330,10 +330,8 @@ class ModelChain: '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: + spectral_model : str or function, default ``'no_loss'`` + Valid strings are: - ``'sapm'`` - ``'first_solar'`` @@ -342,6 +340,9 @@ class ModelChain: The ModelChain instance will be passed as the first argument to a user-defined function. + See :py:func:`~pvlib.modelchain.ModelChain.infer_spectral_model` to + infer the spectral model from system and weather information. + temperature_model : str or function, optional Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'. The ModelChain instance will be passed as the first argument to a @@ -870,14 +871,30 @@ def spectral_model(self, model): self._spectral_model = self.no_spectral_loss else: raise ValueError(model + ' is not a valid spectral loss model') - elif model is None: - # None is a valid value, model inference will be done later - self._spectral_model = None else: self._spectral_model = partial(model, self) - def infer_spectral_model(self): - """Infer spectral model from system attributes.""" + def infer_spectral_model(self, weather=None): + """ + Infer spectral model from system attributes, and optionally from + the input weather dataframe, and set it to the ``ModelChain`` instance. + + Parameters + ---------- + weather : pd.DataFrame or collection of str, optional + An object with columns of available input data to help infer + the spectral model. If ``None``, the spectral model will be + inferred from the system attributes only. + + Returns + ------- + Inferred spectral correction model : function + + Examples + -------- + >>> mc = ModelChain(system, location) + >>> mc.spectral_model = mc.infer_spectral_model(weather=weather) + """ module_parameters = tuple( array.module_parameters for array in self.system.arrays) params = _common_keys(module_parameters) @@ -892,37 +909,13 @@ def infer_spectral_model(self): and (self.system._infer_cell_type() is not None) ): # This suggests models that provide default parameters per cell - # type can be used. However, some of them depend on other weather + # type can be used. However, some models depend on other weather # parameters, so we need to check if they are available. - # At this point, weather may not be available, so let's take that - # into account. - if self.results is not None and self.results.weather is not None: - # weather is available - if "precipitable_water" in self.results.weather: + if weather is not None: # weather is available + if "precipitable_water" in weather: return self.first_solar_spectral_loss - else: - return self.no_spectral_loss - else: - # weather is not available, so it's unknown what model to use. - # Warn user about this and default to no_spectral_loss. - warnings.warn( - "Weather data is not available to infer spectral model. " - "Defaulting to 'no_loss'. Explicitly set the " - "spectral model with the 'spectral_model' kwarg to " - "suppress this warning." - ) - return self.no_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 " - "valid spectral coefficients, a valid " - "Material or Technology value, or a valid model " - "string; explicitly set the model with the " - "'spectral_model' kwarg." - ) + + return self.no_spectral_loss def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( @@ -1715,7 +1708,7 @@ def run_model(self, weather): # spectral model is optional, and weather may impact model inference # check if spectral model inference is requested by means of "None" if self._spectral_model is None: - self._spectral_model = self.infer_spectral_model() + self._spectral_model = self.infer_spectral_model(weather=weather) self.spectral_model() self.effective_irradiance_model() From 55faf8ac629eff8d799a55e9d3ff0f656ad4602d Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:14:09 +0200 Subject: [PATCH 04/16] typo --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 4ceebff18a..38eafd85e7 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1596,7 +1596,7 @@ def _prepare_temperature(self, data): ---------- data : DataFrame May contain columns ``'cell_temperature'`` or - ``'module_temperaure'``. + ``'module_temperature'``. Returns ------- From 831698a2e920ee0b8bffce68ac4cf79df2dd72a5 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:14:21 +0200 Subject: [PATCH 05/16] pep8 compliance --- pvlib/modelchain.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 38eafd85e7..f5a309478a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -253,7 +253,7 @@ def __repr__(self): def _head(obj): try: return obj[:3] - except: + except Exception: return obj if type(self.dc) is tuple: @@ -269,7 +269,7 @@ def _head(obj): '\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) @@ -392,7 +392,6 @@ def __init__(self, system, location, self.results = ModelChainResult() - @classmethod def with_pvwatts(cls, system, location, clearsky_model='ineichen', From 49acea8ab2e4df698ff2ed5118bcb4b0e3da4c2b Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:21:44 +0200 Subject: [PATCH 06/16] ups --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f5a309478a..13b10d4b67 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -367,7 +367,7 @@ def __init__(self, system, location, solar_position_method='nrel_numpy', airmass_model='kastenyoung1989', dc_model=None, ac_model=None, aoi_model=None, - spectral_model=None, temperature_model=None, + spectral_model='no_loss', temperature_model=None, dc_ohmic_model='no_loss', losses_model='no_loss', name=None): From f06bbf49ad1ee1109e400f0c9fad284d223c4ad9 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:26:03 +0200 Subject: [PATCH 07/16] Update modelchain.py --- pvlib/modelchain.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 13b10d4b67..f86162d941 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -330,7 +330,7 @@ class ModelChain: 'interp' and 'no_loss'. The ModelChain instance will be passed as the first argument to a user-defined function. - spectral_model : str or function, default ``'no_loss'`` + spectral_model : str or function, optional Valid strings are: - ``'sapm'`` @@ -340,6 +340,8 @@ class ModelChain: The ModelChain instance will be passed as the first argument to a user-defined function. + By default, it will be inferred from the system attributes only. + See :py:func:`~pvlib.modelchain.ModelChain.infer_spectral_model` to infer the spectral model from system and weather information. @@ -367,7 +369,7 @@ def __init__(self, system, location, solar_position_method='nrel_numpy', airmass_model='kastenyoung1989', dc_model=None, ac_model=None, aoi_model=None, - spectral_model='no_loss', temperature_model=None, + spectral_model=None, temperature_model=None, dc_ohmic_model='no_loss', losses_model='no_loss', name=None): @@ -870,6 +872,8 @@ def spectral_model(self, model): self._spectral_model = self.no_spectral_loss else: raise ValueError(model + ' is not a valid spectral loss model') + elif model is None: + self._spectral_model = self.infer_spectral_model(weather=None) else: self._spectral_model = partial(model, self) From 774edfee66304ca1cb26e69ef911a43c9da3d4d2 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:37:45 +0100 Subject: [PATCH 08/16] Infer a string, key for setter --- pvlib/modelchain.py | 20 +++++++++++--------- pvlib/tests/test_modelchain.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f86162d941..cb5e58dbed 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -873,8 +873,9 @@ def spectral_model(self, model): else: raise ValueError(model + ' is not a valid spectral loss model') elif model is None: - self._spectral_model = self.infer_spectral_model(weather=None) - else: + # uses recursive setter to infer model, which returns a string + self.spectral_model = self.infer_spectral_model(weather=None) + else: # assume model is a callable self._spectral_model = partial(model, self) def infer_spectral_model(self, weather=None): @@ -891,7 +892,7 @@ def infer_spectral_model(self, weather=None): Returns ------- - Inferred spectral correction model : function + Inferred spectral correction model : string key for model setter Examples -------- @@ -899,13 +900,14 @@ def infer_spectral_model(self, weather=None): >>> mc.spectral_model = mc.infer_spectral_model(weather=weather) """ module_parameters = tuple( - array.module_parameters for array in self.system.arrays) + 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 + if {"A4", "A3", "A2", "A1", "A0"} <= params: + return "sapm" elif "first_solar_spectral_coefficients" in params: # user explicitly sets spectral coefficients - return self.first_solar_spectral_loss + return "first_solar" elif ( # cell type is known or can be inferred ("Technology" in params or "Material" in params) @@ -916,9 +918,9 @@ def infer_spectral_model(self, weather=None): # parameters, so we need to check if they are available. if weather is not None: # weather is available if "precipitable_water" in weather: - return self.first_solar_spectral_loss + return "first_solar" - return self.no_spectral_loss + return "no_loss" def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index dcbd820f16..95c201214b 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1271,6 +1271,28 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system, assert isinstance(mc, ModelChain) +def test_infer_spectral_model_with_weather(location, sapm_dc_snl_ac_system, + cec_dc_snl_ac_system, weather): + # instantiate example ModelChain to get the default spectral model + # inferred without weather available by default + # - should resolve to sapm + mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical') + assert mc.spectral_model == mc.sapm_spectral_loss + # - should resolve to no loss + mc = ModelChain(cec_dc_snl_ac_system, location, aoi_model='physical') + assert mc.spectral_model == mc.no_spectral_loss + + # infer spectral model from weather + # - without precipitable water in it, should resolve to no loss + mc.spectral_model = mc.infer_spectral_model(weather=weather) + assert mc.spectral_model == mc.no_spectral_loss + # - with precipitable water in it, should resolve to first solar + weather['precipitable_water'] = 1.42 + mc.spectral_model = mc.infer_spectral_model(weather=weather) + assert mc.spectral_model == mc.first_solar_spectral_loss + assert isinstance(mc, ModelChain) + + @pytest.mark.parametrize('temp_model', [ 'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp', 'noct_sam_temp']) From 240f47968e276214b46b3d787a7f9a97473b08ae Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:37:56 +0100 Subject: [PATCH 09/16] pep8 linting --- pvlib/tests/test_modelchain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 95c201214b..d4b302a7b3 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -7,13 +7,10 @@ from pvlib.modelchain import ModelChain from pvlib.pvsystem import PVSystem from pvlib.location import Location -from pvlib._deprecation import pvlibDeprecationWarning from .conftest import assert_series_equal, assert_frame_equal import pytest -from .conftest import fail_on_pvlib_version - @pytest.fixture(scope='function') def sapm_dc_snl_ac_system(sapm_module_params, cec_inverter_parameters, From 2680b4e9bfc65748c87445a1ccbac24623074e5c Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:44:51 +0100 Subject: [PATCH 10/16] Remove delayed-inference code - codecov warning --- pvlib/modelchain.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index cb5e58dbed..7188f29521 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1709,11 +1709,6 @@ def run_model(self, weather): weather = _to_tuple(weather) self.prepare_inputs(weather) self.aoi_model() - - # spectral model is optional, and weather may impact model inference - # check if spectral model inference is requested by means of "None" - if self._spectral_model is None: - self._spectral_model = self.infer_spectral_model(weather=weather) self.spectral_model() self.effective_irradiance_model() From feb99649b46674991185cb520573d717f3be55bc Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:42:22 +0000 Subject: [PATCH 11/16] default to no_loss --- pvlib/modelchain.py | 14 +++++------- pvlib/tests/test_modelchain.py | 42 ++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7188f29521..05d40fcc54 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -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') @@ -330,7 +330,7 @@ class ModelChain: '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 + spectral_model : str or function, default ``'no_loss'`` Valid strings are: - ``'sapm'`` @@ -340,8 +340,6 @@ class ModelChain: The ModelChain instance will be passed as the first argument to a user-defined function. - By default, it will be inferred from the system attributes only. - See :py:func:`~pvlib.modelchain.ModelChain.infer_spectral_model` to infer the spectral model from system and weather information. @@ -369,7 +367,7 @@ def __init__(self, system, location, solar_position_method='nrel_numpy', airmass_model='kastenyoung1989', dc_model=None, ac_model=None, aoi_model=None, - spectral_model=None, temperature_model=None, + spectral_model='no_loss', temperature_model=None, dc_ohmic_model='no_loss', losses_model='no_loss', name=None): @@ -873,9 +871,9 @@ def spectral_model(self, model): else: raise ValueError(model + ' is not a valid spectral loss model') elif model is None: - # uses recursive setter to infer model, which returns a string - self.spectral_model = self.infer_spectral_model(weather=None) - else: # assume model is a callable + # 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, weather=None): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d4b302a7b3..1caf7720d2 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -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) @@ -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 @@ -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 @@ -1271,12 +1279,16 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system, def test_infer_spectral_model_with_weather(location, sapm_dc_snl_ac_system, cec_dc_snl_ac_system, weather): # instantiate example ModelChain to get the default spectral model - # inferred without weather available by default - # - should resolve to sapm + # default should resolve to no loss mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical') assert mc.spectral_model == mc.sapm_spectral_loss - # - should resolve to no loss - mc = ModelChain(cec_dc_snl_ac_system, location, aoi_model='physical') + # - next inference should resolve to no loss + mc = ModelChain( + cec_dc_snl_ac_system, + location, + aoi_model="physical", + spectral_model=None, + ) assert mc.spectral_model == mc.no_spectral_loss # infer spectral model from weather @@ -2008,9 +2020,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: ', From 09c5bdae93d3a287ff65545a154ef015d4198974 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:42:52 +0000 Subject: [PATCH 12/16] infer_spectral_model tweak -> return list if possible --- pvlib/modelchain.py | 65 +++++++++++++++++++++++----------- pvlib/tests/test_modelchain.py | 4 +++ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 05d40fcc54..d675ef5ac3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -341,7 +341,7 @@ class ModelChain: a user-defined function. See :py:func:`~pvlib.modelchain.ModelChain.infer_spectral_model` to - infer the spectral model from system and weather information. + get a list of available spectral models for the current system. temperature_model : str or function, optional Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'. @@ -878,8 +878,10 @@ def spectral_model(self, model): def infer_spectral_model(self, weather=None): """ - Infer spectral model from system attributes, and optionally from - the input weather dataframe, and set it to the ``ModelChain`` instance. + Return a spectral model key or a list of possible spectral models based + on the system attributes and weather data. If the weather data is not + provided, the spectral model will be inferred from the system + attributes only. Parameters ---------- @@ -890,35 +892,56 @@ def infer_spectral_model(self, weather=None): Returns ------- - Inferred spectral correction model : string key for model setter + Inferred spectral correction model : string key or list of string keys + If no spectral model is inferred, 'no_loss' is returned. + If only one spectral model is inferred, a string key is returned. + If multiple spectral models are inferred, a list of string keys is + returned. + + The following spectral models keys may be returned: + + - ``'sapm'`` for + :py:func:`~pvlib.spectrum.spectral_factor_sapm` + - ``'first_solar'`` for + :py:func:`~pvlib.spectrum.spectral_factor_first_solar`. + Requires ``'precipitable_water'`` in the weather dataframe. + + If no spectral model is inferred, 'no_loss' is returned. Examples -------- >>> mc = ModelChain(system, location) >>> mc.spectral_model = mc.infer_spectral_model(weather=weather) """ + possible_models_result = [] # list of possible spectral models module_parameters = tuple( array.module_parameters for array in self.system.arrays ) params = _common_keys(module_parameters) + + # infer all the possible spectral models and append them to the list if {"A4", "A3", "A2", "A1", "A0"} <= params: - return "sapm" - elif "first_solar_spectral_coefficients" in params: - # user explicitly sets spectral coefficients - return "first_solar" - elif ( - # cell type is known or can be inferred - ("Technology" in params or "Material" in params) - and (self.system._infer_cell_type() is not None) - ): - # This suggests models that provide default parameters per cell - # type can be used. However, some models depend on other weather - # parameters, so we need to check if they are available. - if weather is not None: # weather is available - if "precipitable_water" in weather: - return "first_solar" - - return "no_loss" + possible_models_result.append("sapm") + if weather is not None: # models that depend on weather data + if "precipitable_water" in weather: + if "first_solar_spectral_coefficients" in params: + # user explicitly sets first solar spectral coefficients + possible_models_result.append("first_solar") + # cell type is known or can be inferred + if ("Technology" in params or "Material" in params) and ( + self.system._infer_cell_type() is not None + ): + possible_models_result.append("first_solar") + + # result resolution based on the number of inferred spectral models + if (result_len := len(possible_models_result)) == 0: + # if no spectral model is inferred, return no_loss + return "no_loss" + elif result_len == 1: + # when only one spectral model is inferred, return it + return possible_models_result[0] + else: # multiple spectral models are inferred (avoiding duplicates) + return list(set(possible_models_result)) def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 1caf7720d2..384f5b8e1a 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1281,6 +1281,9 @@ def test_infer_spectral_model_with_weather(location, sapm_dc_snl_ac_system, # instantiate example ModelChain to get the default spectral model # default should resolve to no loss mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical') + assert mc.spectral_model == mc.no_spectral_loss + # - inference should resolve to sapm + mc.spectral_model = mc.infer_spectral_model(weather=weather) assert mc.spectral_model == mc.sapm_spectral_loss # - next inference should resolve to no loss mc = ModelChain( @@ -1289,6 +1292,7 @@ def test_infer_spectral_model_with_weather(location, sapm_dc_snl_ac_system, aoi_model="physical", spectral_model=None, ) + mc.spectral_model = mc.infer_spectral_model(weather=weather) assert mc.spectral_model == mc.no_spectral_loss # infer spectral model from weather From e67080049b9851237bbfb12b66339e257b017fab Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 13 Mar 2025 13:52:19 -0600 Subject: [PATCH 13/16] remove infer_spectral_model --- docs/sphinx/source/whatsnew/v0.11.3.rst | 14 +++-- pvlib/modelchain.py | 69 ------------------------- tests/test_modelchain.py | 42 --------------- 3 files changed, 9 insertions(+), 116 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index b431cda364..d3c8932728 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -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 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 @@ -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 ~~~~~~~~~~~~ diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index d675ef5ac3..36b10f980e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -340,9 +340,6 @@ class ModelChain: The ModelChain instance will be passed as the first argument to a user-defined function. - See :py:func:`~pvlib.modelchain.ModelChain.infer_spectral_model` to - get a list of available spectral models for the current system. - temperature_model : str or function, optional Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'. The ModelChain instance will be passed as the first argument to a @@ -876,72 +873,6 @@ def spectral_model(self, model): else: # assume model is callable with 1st argument = the MC instance self._spectral_model = partial(model, self) - def infer_spectral_model(self, weather=None): - """ - Return a spectral model key or a list of possible spectral models based - on the system attributes and weather data. If the weather data is not - provided, the spectral model will be inferred from the system - attributes only. - - Parameters - ---------- - weather : pd.DataFrame or collection of str, optional - An object with columns of available input data to help infer - the spectral model. If ``None``, the spectral model will be - inferred from the system attributes only. - - Returns - ------- - Inferred spectral correction model : string key or list of string keys - If no spectral model is inferred, 'no_loss' is returned. - If only one spectral model is inferred, a string key is returned. - If multiple spectral models are inferred, a list of string keys is - returned. - - The following spectral models keys may be returned: - - - ``'sapm'`` for - :py:func:`~pvlib.spectrum.spectral_factor_sapm` - - ``'first_solar'`` for - :py:func:`~pvlib.spectrum.spectral_factor_first_solar`. - Requires ``'precipitable_water'`` in the weather dataframe. - - If no spectral model is inferred, 'no_loss' is returned. - - Examples - -------- - >>> mc = ModelChain(system, location) - >>> mc.spectral_model = mc.infer_spectral_model(weather=weather) - """ - possible_models_result = [] # list of possible spectral models - module_parameters = tuple( - array.module_parameters for array in self.system.arrays - ) - params = _common_keys(module_parameters) - - # infer all the possible spectral models and append them to the list - if {"A4", "A3", "A2", "A1", "A0"} <= params: - possible_models_result.append("sapm") - if weather is not None: # models that depend on weather data - if "precipitable_water" in weather: - if "first_solar_spectral_coefficients" in params: - # user explicitly sets first solar spectral coefficients - possible_models_result.append("first_solar") - # cell type is known or can be inferred - if ("Technology" in params or "Material" in params) and ( - self.system._infer_cell_type() is not None - ): - possible_models_result.append("first_solar") - - # result resolution based on the number of inferred spectral models - if (result_len := len(possible_models_result)) == 0: - # if no spectral model is inferred, return no_loss - return "no_loss" - elif result_len == 1: - # when only one spectral model is inferred, return it - return possible_models_result[0] - else: # multiple spectral models are inferred (avoiding duplicates) - return list(set(possible_models_result)) def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( diff --git a/tests/test_modelchain.py b/tests/test_modelchain.py index 38fe78bf92..1bdcb87f90 100644 --- a/tests/test_modelchain.py +++ b/tests/test_modelchain.py @@ -1280,48 +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) - - -def test_infer_spectral_model_with_weather(location, sapm_dc_snl_ac_system, - cec_dc_snl_ac_system, weather): - # instantiate example ModelChain to get the default spectral model - # default should resolve to no loss - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical') - assert mc.spectral_model == mc.no_spectral_loss - # - inference should resolve to sapm - mc.spectral_model = mc.infer_spectral_model(weather=weather) - assert mc.spectral_model == mc.sapm_spectral_loss - # - next inference should resolve to no loss - mc = ModelChain( - cec_dc_snl_ac_system, - location, - aoi_model="physical", - spectral_model=None, - ) - mc.spectral_model = mc.infer_spectral_model(weather=weather) - assert mc.spectral_model == mc.no_spectral_loss - - # infer spectral model from weather - # - without precipitable water in it, should resolve to no loss - mc.spectral_model = mc.infer_spectral_model(weather=weather) - assert mc.spectral_model == mc.no_spectral_loss - # - with precipitable water in it, should resolve to first solar - weather['precipitable_water'] = 1.42 - mc.spectral_model = mc.infer_spectral_model(weather=weather) - assert mc.spectral_model == mc.first_solar_spectral_loss - assert isinstance(mc, ModelChain) - - @pytest.mark.parametrize('temp_model', [ 'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp', 'noct_sam_temp']) From f638d93b4556760a4ae6f18252dfe7ec5b98bf92 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 13 Mar 2025 13:55:05 -0600 Subject: [PATCH 14/16] lint --- pvlib/modelchain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 36b10f980e..f4c19507e3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -873,7 +873,6 @@ def spectral_model(self, model): else: # assume model is callable with 1st argument = the MC instance self._spectral_model = partial(model, self) - def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( _tuple_from_dfs(self.results.weather, 'precipitable_water'), From 0a62ed65ef094f31301f86902b98494aae28f900 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 14 Mar 2025 07:54:25 -0600 Subject: [PATCH 15/16] review --- docs/sphinx/source/whatsnew/v0.11.3.rst | 2 +- pvlib/modelchain.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index d3c8932728..1cc524ee07 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -11,7 +11,7 @@ Breaking Changes 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 from PVSystem attributes. (:issue:`2017`, :pull:`2253`) + the spectral model from PVSystem attributes. (:issue:`2017`, :pull:`2253`) Bug fixes ~~~~~~~~~ diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f4c19507e3..0f58da533a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -330,7 +330,7 @@ class ModelChain: 'interp' and 'no_loss'. The ModelChain instance will be passed as the first argument to a user-defined function. - spectral_model : str or function, default ``'no_loss'`` + spectral_model : str or function, optional Valid strings are: - ``'sapm'`` @@ -338,7 +338,7 @@ class ModelChain: - ``'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'. @@ -364,7 +364,7 @@ def __init__(self, system, location, solar_position_method='nrel_numpy', airmass_model='kastenyoung1989', dc_model=None, ac_model=None, aoi_model=None, - spectral_model='no_loss', temperature_model=None, + spectral_model=None, temperature_model=None, dc_ohmic_model='no_loss', losses_model='no_loss', name=None): From d201f4a0c924cc60e6d4467af5b055934d7a9a23 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 14 Mar 2025 08:41:11 -0600 Subject: [PATCH 16/16] remove reference to infer_spectral_model method --- docs/sphinx/source/reference/modelchain.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sphinx/source/reference/modelchain.rst b/docs/sphinx/source/reference/modelchain.rst index bcbb0006b9..89de825f0d 100644 --- a/docs/sphinx/source/reference/modelchain.rst +++ b/docs/sphinx/source/reference/modelchain.rst @@ -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