diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 0eed2c2a2d..625f3f119f 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -11,6 +11,8 @@ API Changes Enhancements ~~~~~~~~~~~~ * Add sea surface albedo in irradiance.py (:issue:`458`) +* Implement first_solar_spectral_loss in modelchain.py (:issue:'359') + Bug fixes ~~~~~~~~~ @@ -29,3 +31,5 @@ Contributors ~~~~~~~~~~~~ * Will Holmgren * Yu Cao +* Cliff Hansen + diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 4878cc796d..ee57135ff9 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -466,8 +466,11 @@ def first_solar_spectral_correction(pw, airmass_absolute, module_type=None, coefficients = _coefficients[module_type.lower()] elif module_type is None and coefficients is not None: pass + elif module_type is None and coefficients is None: + raise TypeError('No valid input provided, both module_type and ' + + 'coefficients are None') else: - raise TypeError('ambiguous input, must supply only 1 of ' + + raise TypeError('Cannot resolve input, must supply only one of ' + 'module_type and coefficients') # Evaluate Spectral Shift diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8115939f1f..29f4cfa674 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -273,8 +273,8 @@ class ModelChain(object): spectral_model: None, str, or function, default None If None, the model will be inferred from the contents of system.module_parameters. Valid strings are 'sapm', - 'first_solar' (not implemented), 'no_loss'. The ModelChain - instance will be passed as the first argument to a user-defined + 'first_solar', 'no_loss'. The ModelChain instance will be passed + as the first argument to a user-defined function. temp_model: str or function, default 'sapm' @@ -518,7 +518,7 @@ def sapm_aoi_loss(self): return self def no_aoi_loss(self): - self.aoi_modifier = 1 + self.aoi_modifier = 1.0 return self @property @@ -532,7 +532,7 @@ def spectral_model(self, model): elif isinstance(model, str): model = model.lower() if model == 'first_solar': - raise NotImplementedError + self._spectral_model = self.first_solar_spectral_loss elif model == 'sapm': self._spectral_model = self.sapm_spectral_loss elif model == 'no_loss': @@ -546,12 +546,23 @@ def infer_spectral_model(self): params = set(self.system.module_parameters.keys()) if set(['A4', 'A3', 'A2', 'A1', 'A0']) <= params: return self.sapm_spectral_loss + elif ((('Technology' in params or + 'Material' in params) and + (pvsystem._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.module_parameters') + 'system.module_parameters. Check that the ' + 'parameters contain valid ' + 'first_solar_spectral_coefficients or a valid ' + 'Material or Technology value') def first_solar_spectral_loss(self): - raise NotImplementedError + self.spectral_modifier = self.system.first_solar_spectral_loss( + self.weather['precipitable_water'], + self.airmass['airmass_absolute']) + return self def sapm_spectral_loss(self): self.spectral_modifier = self.system.sapm_spectral_loss( diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 465b117c66..bcf1474448 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -421,6 +421,94 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, poa_direct, poa_diffuse, airmass_absolute, aoi, self.module_parameters, reference_irradiance=reference_irradiance) + def first_solar_spectral_loss(self, pw, airmass_absolute): + + """ + Use the :py:func:`first_solar_spectral_correction` function to + calculate the spectral loss modifier. The model coefficients are + specific to the module's cell type, and are determined by searching + for one of the following keys in self.module_parameters (in order): + 'first_solar_spectral_coefficients' (user-supplied coefficients) + 'Technology' - a string describing the cell type, can be read from + the CEC module parameter database + 'Material' - a string describing the cell type, can be read from + the Sandia module database. + + Parameters + ---------- + pw : array-like + atmospheric precipitable water (cm). + + airmass_absolute : array-like + absolute (pressure corrected) airmass. + + Returns + ------- + modifier: array-like + spectral mismatch factor (unitless) which can be multiplied + with broadband irradiance reaching a module's cells to estimate + effective irradiance, i.e., the irradiance that is converted to + electrical current. + """ + + if 'first_solar_spectral_coefficients' in \ + self.module_parameters.keys(): + coefficients = \ + self.module_parameters['first_solar_spectral_coefficients'] + module_type = None + else: + module_type = self._infer_cell_type() + coefficients = None + + return atmosphere.first_solar_spectral_correction(pw, + airmass_absolute, + module_type, + coefficients) + + def _infer_cell_type(self): + + """ + Examines module_parameters and maps the Technology key for the CEC + database and the Material key for the Sandia database to a common + list of strings for cell type. + + Returns + ------- + cell_type: str + + """ + + _cell_type_dict = {'Multi-c-Si': 'multisi', + 'Mono-c-Si': 'monosi', + 'Thin Film': 'cigs', + 'a-Si/nc': 'asi', + 'CIS': 'cigs', + 'CIGS': 'cigs', + '1-a-Si': 'asi', + 'CdTe': 'cdte', + 'a-Si': 'asi', + '2-a-Si': None, + '3-a-Si': None, + 'HIT-Si': 'monosi', + 'mc-Si': 'multisi', + 'c-Si': 'multisi', + 'Si-Film': 'asi', + 'CdTe': 'cdte', + 'EFG mc-Si': 'multisi', + 'GaAs': None, + 'a-Si / mono-Si': 'monosi'} + + if 'Technology' in self.module_parameters.keys(): + # CEC module parameter set + cell_type = _cell_type_dict[self.module_parameters['Technology']] + elif 'Material' in self.module_parameters.keys(): + # Sandia module parameter set + cell_type = _cell_type_dict[self.module_parameters['Material']] + else: + cell_type = None + + return cell_type + def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts=None): diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 9c13e4b2f8..600b803abd 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -262,28 +262,28 @@ def test_aoi_models(system, location, aoi_model, expected): def constant_spectral_loss(mc): mc.spectral_modifier = 0.9 + @requires_scipy -@pytest.mark.parametrize('spectral_model, expected', [ - ('sapm', [182.338436597, -2.00000000e-02]), - pytest.mark.xfail(raises=NotImplementedError) - (('first_solar', [179.371460714, -2.00000000e-02])), - ('no_loss', [181.604438144, -2.00000000e-02]), - (constant_spectral_loss, [163.061569511, -2e-2]) +@pytest.mark.parametrize('spectral_model', [ + 'sapm', 'first_solar', 'no_loss', constant_spectral_loss ]) -def test_spectral_models(system, location, spectral_model, expected): +def test_spectral_models(system, location, spectral_model): + times = pd.date_range('20160101 1200-0700', periods=3, freq='6H') + weather = pd.DataFrame(data=[0.3, 0.5, 1.0], + index=times, + columns=['precipitable_water']) mc = ModelChain(system, location, dc_model='sapm', aoi_model='no_loss', spectral_model=spectral_model) - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac - - expected = pd.Series(np.array(expected), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + spectral_modifier = mc.run_model(times=times, + weather=weather).spectral_modifier + assert isinstance(spectral_modifier, (pd.Series, float, int)) def constant_losses(mc): mc.losses = 0.9 mc.ac *= mc.losses + @requires_scipy @pytest.mark.parametrize('losses_model, expected', [ ('pvwatts', [163.280464174, 0]), diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 42c696d392..7ea9fde7fe 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -266,6 +266,16 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params): out = system.sapm_spectral_loss(airmass) +@pytest.mark.parametrize("expected", [1.03173953]) +def test_PVSystem_first_solar_spectral_loss(sapm_module_params, expected): + system = pvsystem.PVSystem(module_parameters=sapm_module_params) + pw = 3 + airmass_absolute = 3 + out = system.first_solar_spectral_loss(pw, airmass_absolute) + + assert_allclose(out, expected, atol=1e-4) + + @pytest.mark.parametrize('aoi,expected', [ (45, 0.9975036250000002), (np.array([[-30, 30, 100, np.nan]]),