diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 845ad393fa..5e6a283c36 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -328,6 +328,7 @@ Pvsyst model temperature.pvsyst_cell pvsystem.calcparams_pvsyst pvsystem.singlediode + pvsystem.dc_ohms_from_percent PVWatts model ^^^^^^^^^^^^^ @@ -598,6 +599,7 @@ ModelChain properties that are aliases for your specific modeling functions. modelchain.ModelChain.aoi_model modelchain.ModelChain.spectral_model modelchain.ModelChain.temperature_model + modelchain.ModelChain.dc_ohmic_model modelchain.ModelChain.losses_model modelchain.ModelChain.effective_irradiance_model @@ -628,6 +630,8 @@ ModelChain model definitions. modelchain.ModelChain.pvsyst_temp modelchain.ModelChain.faiman_temp modelchain.ModelChain.fuentes_temp + modelchain.ModelChain.dc_ohmic_model + modelchain.ModelChain.no_dc_ohmic_loss modelchain.ModelChain.pvwatts_losses modelchain.ModelChain.no_extra_losses diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e5becc4a1a..b46ec1ac70 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -327,6 +327,11 @@ class ModelChain: The ModelChain instance will be passed as the first argument to a user-defined function. + dc_ohmic_model: None, str or function, default None + Valid strings are 'dc_ohms_from_percent', 'no_loss'. The ModelChain + instance will be passed as the first argument to a user-defined + function. + losses_model: str or function, default 'no_loss' Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance will be passed as the first argument to a user-defined function. @@ -343,6 +348,7 @@ def __init__(self, system, location, airmass_model='kastenyoung1989', dc_model=None, ac_model=None, aoi_model=None, spectral_model=None, temperature_model=None, + dc_ohmic_model=None, losses_model='no_loss', name=None, **kwargs): self.name = name @@ -361,7 +367,10 @@ def __init__(self, system, location, self.spectral_model = spectral_model self.temperature_model = temperature_model + # losses + self.dc_ohmic_model = dc_ohmic_model self.losses_model = losses_model + self.orientation_strategy = orientation_strategy self.weather = None @@ -924,6 +933,40 @@ def fuentes_temp(self): self.weather['wind_speed']) return self + @property + def dc_ohmic_model(self): + return self._dc_ohmic_model + + @dc_ohmic_model.setter + def dc_ohmic_model(self, model): + if model is None: + self._dc_ohmic_model = self.no_dc_ohmic_loss + elif isinstance(model, str): + model = model.lower() + if model == 'dc_ohms_from_percent': + self._dc_ohmic_model = self.dc_ohms_from_percent + elif model == 'no_loss': + self._dc_ohmic_model = self.no_dc_ohmic_loss + else: + raise ValueError(model + ' is not a valid losses model') + else: + self._dc_ohmic_model = partial(model, self) + + def dc_ohms_from_percent(self): + """ + Calculate time series of ohmic losses and apply those to the mpp power + output of the `dc_model` based on the pvsyst equivalent resistance + method. Uses a `dc_ohmic_percent` parameter in the `losses_parameters` + of the PVsystem. + """ + Rw = self.system.dc_ohms_from_percent() + self.dc_ohmic_losses = pvsystem.dc_ohmic_losses(Rw, self.dc['i_mp']) + self.dc['p_mp'] = self.dc['p_mp'] - self.dc_ohmic_losses + return self + + def no_dc_ohmic_loss(self): + return self + @property def losses_model(self): return self._losses_model @@ -1374,6 +1417,7 @@ def _run_from_effective_irrad(self, data=None): """ self._prepare_temperature(data) self.dc_model() + self.dc_ohmic_model() self.losses_model() self.ac_model() diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8a20e5fdef..e3ca545da5 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -838,6 +838,27 @@ def pvwatts_ac(self, pdc): return inverter.pvwatts(pdc, self.inverter_parameters['pdc0'], **kwargs) + def dc_ohms_from_percent(self): + """ + Calculates the equivalent resistance of the wires using + :py:func:`pvlib.pvsystem.dc_ohms_from_percent`, + `self.losses_parameters["dc_ohmic_percent"]`, + `self.module_parameters["V_mp_ref"]`, + `self.module_parameters["I_mp_ref"]`, + `self.modules_per_string`, and `self.strings_per_inverter`. + + See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for details. + """ + kwargs = _build_kwargs(['dc_ohmic_percent'], self.losses_parameters) + + kwargs.update(_build_kwargs(['V_mp_ref', 'I_mp_ref'], + self.module_parameters)) + + kwargs.update({'modules_per_string': self.modules_per_string, + 'strings_per_inverter': self.strings_per_inverter}) + + return dc_ohms_from_percent(**kwargs) + @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', name='PVSystem.localize', removal='0.9') def localize(self, location=None, latitude=None, longitude=None, @@ -2334,6 +2355,72 @@ def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2, return losses +def dc_ohms_from_percent(V_mp_ref, I_mp_ref, dc_ohmic_percent, + modules_per_string=1, + strings_per_inverter=1): + """ + Calculates the equivalent resistance of the wires from a percent + ohmic loss at STC. + + Equivalent resistance is calculated with the function: + + .. math:: + + Rw = (L_{stc} / 100) * (Varray / Iarray) + + :math:`L_{stc}` is the input dc loss as a percent, e.g. 1.5% loss is + input as 1.5 + + Parameters + ---------- + V_mp_ref: numeric + I_mp_ref: numeric + dc_ohmic_percent: numeric, default 0 + modules_per_string: numeric, default 1 + strings_per_inverter: numeric, default 1 + + Returns + ---------- + Rw: numeric + Equivalent resistance in ohms + + References + ---------- + -- [1] PVsyst 7 Help. "Array ohmic wiring loss". + https://www.pvsyst.com/help/ohmic_loss.htm + """ + vmp = modules_per_string * V_mp_ref + + imp = strings_per_inverter * I_mp_ref + + Rw = (dc_ohmic_percent / 100) * (vmp / imp) + + return Rw + + +def dc_ohmic_losses(ohms, current): + """ + Returns ohmic losses in in units of power from the equivalent + resistance of of the wires and the operating current. + + Parameters + ---------- + ohms: numeric, float + current: numeric, float or array-like + + Returns + ---------- + numeric + Single or array-like value of the losses in units of power + + References + ---------- + -- [1] PVsyst 7 Help. "Array ohmic wiring loss". + https://www.pvsyst.com/help/ohmic_loss.htm + """ + return ohms * current * current + + def combine_loss_factors(index, *losses, fill_method='ffill'): r""" Combines Series loss fractions while setting a common index. diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d02611bbf3..c8eda9f3e7 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -704,6 +704,69 @@ def constant_losses(mc): mc.dc *= mc.losses +def dc_constant_losses(mc): + mc.dc['p_mp'] *= 0.9 + + +def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, + location, + weather, + mocker): + + m = mocker.spy(pvsystem, 'dc_ohms_from_percent') + + cec_dc_snl_ac_system.losses_parameters = dict(dc_ohmic_percent=3) + mc = ModelChain(cec_dc_snl_ac_system, location, + aoi_model='no_loss', + spectral_model='no_loss', + dc_ohmic_model='dc_ohms_from_percent') + mc.run_model(weather) + + assert m.call_count == 1 + + assert isinstance(mc.dc_ohmic_losses, pd.Series) + + +def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, + location, + weather, + mocker): + + m = mocker.spy(modelchain.ModelChain, 'no_dc_ohmic_loss') + mc = ModelChain(cec_dc_snl_ac_system, location, + aoi_model='no_loss', + spectral_model='no_loss', + dc_ohmic_model='no_loss') + mc.run_model(weather) + + assert m.call_count == 1 + assert hasattr(mc, 'dc_ohmic_losses') is False + + +def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, + weather, mocker): + m = mocker.spy(sys.modules[__name__], 'dc_constant_losses') + mc = ModelChain(cec_dc_snl_ac_system, location, + aoi_model='no_loss', + spectral_model='no_loss', + dc_ohmic_model=dc_constant_losses) + mc.run_model(weather) + + assert m.call_count == 1 + assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) + assert not mc.ac.empty + + +def test_dc_ohmic_not_a_model(cec_dc_snl_ac_system, location, + weather, mocker): + exc_text = 'not_a_dc_model is not a valid losses model' + with pytest.raises(ValueError, match=exc_text): + ModelChain(cec_dc_snl_ac_system, location, + aoi_model='no_loss', + spectral_model='no_loss', + dc_ohmic_model='not_a_dc_model') + + def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, mocker): age = 1 diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 5675f8c3f7..7917c56c0d 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1220,6 +1220,38 @@ def test_LocalizedPVSystem___repr__(): assert localized_system.__repr__() == expected +def test_dc_ohms_from_percent(): + expected = .1425 + out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1) + assert_allclose(out, expected) + + +def test_PVSystem_dc_ohms_from_percent(mocker): + mocker.spy(pvsystem, 'dc_ohms_from_percent') + + expected = .1425 + system = pvsystem.PVSystem(losses_parameters={'dc_ohmic_percent': 3}, + module_parameters={'I_mp_ref': 8, + 'V_mp_ref': 38}) + out = system.dc_ohms_from_percent() + + pvsystem.dc_ohms_from_percent.assert_called_once_with( + dc_ohmic_percent=3, + V_mp_ref=38, + I_mp_ref=8, + modules_per_string=1, + strings_per_inverter=1 + ) + + assert_allclose(out, expected) + + +def test_dc_ohmic_losses(): + expected = 9.12 + out = pvsystem.dc_ohmic_losses(.1425, 8) + assert_allclose(out, expected) + + def test_pvwatts_dc_scalars(): expected = 88.65 out = pvsystem.pvwatts_dc(900, 30, 100, -0.003)