From 5c7959050296a87b4c5d2d5743ae6b1aff4566fe Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Mon, 25 Jan 2021 13:10:52 -0700 Subject: [PATCH 01/16] Support functions for ohmic losses --- pvlib/pvsystem.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index afc1c195cf..15594ecf70 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2763,6 +2763,71 @@ 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. From b6fa6f62ab0cadc8287bcb0eb9b57d5796668061 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Mon, 15 Feb 2021 10:04:23 -0700 Subject: [PATCH 02/16] PVSystem dc ohmic losses and tests --- pvlib/pvsystem.py | 52 ++++++++++++++++++++++++++++++++++-- pvlib/tests/test_pvsystem.py | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 15594ecf70..952b58b287 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -191,6 +191,11 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None): if arrays is None: + if losses_parameters is None: + array_losses_parameters = {} + else: + array_losses_parameters = _build_kwargs(['dc_ohmic_percent'], + losses_parameters) self.arrays = (Array( surface_tilt, surface_azimuth, @@ -202,7 +207,8 @@ def __init__(self, temperature_model_parameters, modules_per_string, strings_per_inverter, - racking_model + racking_model, + array_losses_parameters, ),) else: self.arrays = tuple(arrays) @@ -970,6 +976,17 @@ def pvwatts_multi(self, p_dc): return inverter.pvwatts_multi(p_dc, self.inverter_parameters['pdc0'], **kwargs) + @_unwrap_single_value + def dc_ohms_from_percent(self): + """ + Calculates the equivalent resistance of the wires for each array using + :py:func:`pvlib.pvsystem.dc_ohms_from_percent` + + See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for details. + """ + + return tuple(array.dc_ohms_from_percent() for array in self.arrays) + @property @_unwrap_single_value def module_parameters(self): @@ -1102,6 +1119,9 @@ class Array: Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. Used to identify a parameter set for the SAPM cell temperature model. + array_losses_parameters: None, dict or Series, default None. + Supported keys are dc_ohmic_percent. + """ def __init__(self, @@ -1111,7 +1131,8 @@ def __init__(self, module_parameters=None, temperature_model_parameters=None, modules_per_string=1, strings=1, - racking_model=None, name=None): + racking_model=None, array_losses_parameters=None, + name=None): self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth @@ -1139,6 +1160,11 @@ def __init__(self, else: self.temperature_model_parameters = temperature_model_parameters + if array_losses_parameters is None: + self.array_losses_parameters = {} + else: + self.array_losses_parameters = array_losses_parameters + self.name = name def __repr__(self): @@ -1325,6 +1351,28 @@ def get_iam(self, aoi, iam_model='physical'): else: raise ValueError(model + ' is not a valid IAM model') + 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.array_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}) + + return dc_ohms_from_percent(**kwargs) + def calcparams_desoto(effective_irradiance, temp_cell, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 311820828b..a58d42408c 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1955,3 +1955,55 @@ def test_combine_loss_factors(): def test_no_extra_kwargs(): with pytest.raises(TypeError, match="arbitrary_kwarg"): pvsystem.PVSystem(arbitrary_kwarg='value') + + +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_Array_dc_ohms_from_percent(mocker): + mocker.spy(pvsystem, 'dc_ohms_from_percent') + + expected = .1425 + array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, + module_parameters={'I_mp_ref': 8, + 'V_mp_ref': 38}) + out = array.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) From 8b6769ca0af83654e2a0d086c788b16c2a8746b9 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Mon, 15 Feb 2021 10:32:22 -0700 Subject: [PATCH 03/16] ModelChain dc ohimc losses as tests --- pvlib/modelchain.py | 43 ++++++++++++++++++++++ pvlib/tests/test_modelchain.py | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c9c578bf96..6a56b3a532 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -355,6 +355,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. @@ -377,6 +382,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): self.name = name @@ -395,7 +401,9 @@ def __init__(self, system, location, self.spectral_model = spectral_model self.temperature_model = temperature_model + self.dc_ohmic_model = dc_ohmic_model self.losses_model = losses_model + self.orientation_strategy = orientation_strategy self.weather = None @@ -1058,6 +1066,40 @@ def faiman_temp(self): def fuentes_temp(self): return self._set_celltemp(self.system.fuentes_celltemp) + @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 @@ -1728,6 +1770,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/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 9085b02b13..1a22ea7664 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1378,6 +1378,73 @@ def constant_losses(mc): mc.results.dc *= mc.losses +def dc_constant_losses(mc): + mc.results.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') + + system = cec_dc_snl_ac_system + + for array in system.arrays: + array.array_losses_parameters = dict(dc_ohmic_percent=3) + + mc = ModelChain(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.results.ac, (pd.Series, pd.DataFrame)) + assert not mc.results.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 From ab5ecffa47833763d62209bf59a7468c3d6b8025 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Mon, 15 Feb 2021 13:31:40 -0700 Subject: [PATCH 04/16] updates to api.rst for dc_ohmic_losses --- docs/sphinx/source/api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 8805d199a4..c36ab0a7bf 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -331,6 +331,7 @@ Pvsyst model temperature.pvsyst_cell pvsystem.calcparams_pvsyst pvsystem.singlediode + pvsystem.dc_ohms_from_percent PVWatts model ^^^^^^^^^^^^^ @@ -389,6 +390,8 @@ Loss models :toctree: generated/ pvsystem.combine_loss_factors + pvsystem.dc_ohms_from_percent + modelchain.ModelChain.dc_ohmic_model Snow ---- @@ -615,6 +618,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 @@ -645,6 +649,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 From f63a24b6a1925b8e7170eaeab78de559155e5c2e Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Wed, 24 Feb 2021 10:26:57 -0700 Subject: [PATCH 05/16] Docstring formatting updates --- pvlib/pvsystem.py | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d721321254..cd9a109cb0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1167,7 +1167,7 @@ class Array: Used to identify a parameter set for the SAPM cell temperature model. array_losses_parameters: None, dict or Series, default None. - Supported keys are dc_ohmic_percent. + Supported keys are 'dc_ohmic_percent'. """ @@ -2870,26 +2870,33 @@ def dc_ohms_from_percent(V_mp_ref, I_mp_ref, dc_ohmic_percent, .. 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 + :math:`Rw` is the equivalent resistance in ohms + :math:`Varray` is the Vmp of the modules times modules per string + :math:`Iarray` is the Imp of the modules times strings per array + :math:`L_{stc}` is the input dc loss percent Parameters ---------- V_mp_ref: numeric + Voltage at maximum power in reference conditions [V] I_mp_ref: numeric + Current at maximum power in reference conditions [V] dc_ohmic_percent: numeric, default 0 - modules_per_string: numeric, default 1 - strings_per_inverter: numeric, default 1 + input dc loss as a percent, e.g. 1.5% loss is input as 1.5 + modules_per_string: int, default 1 + Number of modules per string in the array. + strings: int, default 1 + Number of parallel strings in the array. Returns ---------- Rw: numeric - Equivalent resistance in ohms + Equivalent resistance [ohm] References ---------- - -- [1] PVsyst 7 Help. "Array ohmic wiring loss". - https://www.pvsyst.com/help/ohmic_loss.htm + .. [1] PVsyst 7 Help. "Array ohmic wiring loss". + https://www.pvsyst.com/help/ohmic_loss.htm """ vmp = modules_per_string * V_mp_ref @@ -2900,27 +2907,29 @@ def dc_ohms_from_percent(V_mp_ref, I_mp_ref, dc_ohmic_percent, return Rw -def dc_ohmic_losses(ohms, current): +def dc_ohmic_losses(resistance, current): """ - Returns ohmic losses in in units of power from the equivalent - resistance of of the wires and the operating current. + Returns ohmic losses in units of power from the equivalent + resistance of the wires and the operating current. Parameters ---------- - ohms: numeric, float + resistance: numeric + Equivalent resistance of wires [ohm] current: numeric, float or array-like + Operating current [A] Returns ---------- - numeric - Single or array-like value of the losses in units of power + loss: numeric + Power Loss [W] References ---------- - -- [1] PVsyst 7 Help. "Array ohmic wiring loss". - https://www.pvsyst.com/help/ohmic_loss.htm + .. [1] PVsyst 7 Help. "Array ohmic wiring loss". + https://www.pvsyst.com/help/ohmic_loss.htm """ - return ohms * current * current + return resistance * current * current def combine_loss_factors(index, *losses, fill_method='ffill'): From 0db5afe0725326f8e8124b2c021609198e1608b4 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Wed, 24 Feb 2021 11:42:34 -0700 Subject: [PATCH 06/16] make dc_ohms_from_percent work with other dc models --- pvlib/pvsystem.py | 68 ++++++++++++++++++++++++++++-------- pvlib/tests/test_pvsystem.py | 12 +++---- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index cd9a109cb0..3e82314dca 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1401,22 +1401,62 @@ def get_iam(self, aoi, iam_model='physical'): def dc_ohms_from_percent(self): """ Calculates the equivalent resistance of the wires using - :py:func:`pvlib.pvsystem.dc_ohms_from_percent`, + :py:func:`pvlib.pvsystem.dc_ohms_from_percent` + + Makes use of array module parameters according to the + following DC models: + CEC: + `self.module_parameters["V_mp_ref"]`, + `self.module_parameters["I_mp_ref"]`, + SAPM: + `self.module_parameters["Vmpo"]`, + `self.module_parameters["Impo"]`, + PVsyst-like or other: + `self.module_parameters["Vmpp"]`, + `self.module_parameters["Impp"]`, + + Other array parameters that are used are: `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. + `self.modules_per_string`, and + `self.strings`. + + See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for more details. """ kwargs = _build_kwargs(['dc_ohmic_percent'], self.array_losses_parameters) - kwargs.update(_build_kwargs(['V_mp_ref', 'I_mp_ref'], - self.module_parameters)) + # add relevent Vmp and Imp parameters from CEC parameters + if all([elem in self.module_parameters + for elem in ['V_mp_ref', 'I_mp_ref']]): + kwargs.update({'vmp_ref': self.module_parameters['V_mp_ref'], + 'imp_ref': self.module_parameters['I_mp_ref']}) + + + # add relevant Vmp and Imp parameters from SAPM parameters + elif all([elem in self.module_parameters + for elem in ['Vmpo', 'Impo']]): + kwargs.update({'vmp_ref': self.module_parameters['Vmpo'], + 'imp_ref': self.module_parameters['Impo']}) + + # add relevant Vmp and Imp parameters if they are PVsyst-like + elif all([elem in self.module_parameters + for elem in ['Vmpp', 'Impp']]): + kwargs.update({'vmp_ref': self.module_parameters['Vmpp'], + 'imp_ref': self.module_parameters['Impp']}) + + # raise error if relevant Vmp and Imp parameters are not found + else: + raise ValueError('Parameters for Vmp and Imp could not be found ', + 'in the array module parameters. Module ', + 'parameters must include one set of ', + '{"V_mp_ref", "I_mp_Ref"}, ' + '{"Vmpo", "Impo"}, or ' + '{"Vmpp", "Impp"}, ' + ) kwargs.update({'modules_per_string': self.modules_per_string, - 'strings_per_inverter': self.strings}) + 'strings': self.strings}) return dc_ohms_from_percent(**kwargs) @@ -2858,9 +2898,9 @@ 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, +def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent, modules_per_string=1, - strings_per_inverter=1): + strings=1): """ Calculates the equivalent resistance of the wires from a percent ohmic loss at STC. @@ -2877,9 +2917,9 @@ def dc_ohms_from_percent(V_mp_ref, I_mp_ref, dc_ohmic_percent, Parameters ---------- - V_mp_ref: numeric + vmp_ref: numeric Voltage at maximum power in reference conditions [V] - I_mp_ref: numeric + imp_ref: numeric Current at maximum power in reference conditions [V] dc_ohmic_percent: numeric, default 0 input dc loss as a percent, e.g. 1.5% loss is input as 1.5 @@ -2898,9 +2938,9 @@ def dc_ohms_from_percent(V_mp_ref, I_mp_ref, dc_ohmic_percent, .. [1] PVsyst 7 Help. "Array ohmic wiring loss". https://www.pvsyst.com/help/ohmic_loss.htm """ - vmp = modules_per_string * V_mp_ref + vmp = modules_per_string * vmp_ref - imp = strings_per_inverter * I_mp_ref + imp = strings * imp_ref Rw = (dc_ohmic_percent / 100) * (vmp / imp) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index ea232dbfdd..23e0e4573c 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2063,10 +2063,10 @@ def test_PVSystem_dc_ohms_from_percent(mocker): pvsystem.dc_ohms_from_percent.assert_called_once_with( dc_ohmic_percent=3, - V_mp_ref=38, - I_mp_ref=8, + vmp_ref=38, + imp_ref=8, modules_per_string=1, - strings_per_inverter=1 + strings=1 ) assert_allclose(out, expected) @@ -2089,10 +2089,10 @@ def test_Array_dc_ohms_from_percent(mocker): pvsystem.dc_ohms_from_percent.assert_called_once_with( dc_ohmic_percent=3, - V_mp_ref=38, - I_mp_ref=8, + vmp_ref=38, + imp_ref=8, modules_per_string=1, - strings_per_inverter=1 + strings=1 ) assert_allclose(out, expected) From a8a911efabf2f336820d4db6892c8a8e648fb2de Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Wed, 24 Feb 2021 12:31:48 -0700 Subject: [PATCH 07/16] format fixes --- pvlib/pvsystem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 3e82314dca..cb3d81915f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1432,7 +1432,6 @@ def dc_ohms_from_percent(self): kwargs.update({'vmp_ref': self.module_parameters['V_mp_ref'], 'imp_ref': self.module_parameters['I_mp_ref']}) - # add relevant Vmp and Imp parameters from SAPM parameters elif all([elem in self.module_parameters for elem in ['Vmpo', 'Impo']]): @@ -2936,7 +2935,7 @@ def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent, References ---------- .. [1] PVsyst 7 Help. "Array ohmic wiring loss". - https://www.pvsyst.com/help/ohmic_loss.htm + https://www.pvsyst.com/help/ohmic_loss.htm """ vmp = modules_per_string * vmp_ref @@ -2967,7 +2966,7 @@ def dc_ohmic_losses(resistance, current): References ---------- .. [1] PVsyst 7 Help. "Array ohmic wiring loss". - https://www.pvsyst.com/help/ohmic_loss.htm + https://www.pvsyst.com/help/ohmic_loss.htm """ return resistance * current * current From 3c7b85d5f0852eda4aa8070277d9a8aec4179828 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Wed, 24 Feb 2021 14:12:35 -0700 Subject: [PATCH 08/16] support for multiple arrays --- pvlib/modelchain.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index aca42fe791..5bb28ccfe6 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1103,8 +1103,19 @@ def dc_ohms_from_percent(self): 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 + if isinstance(self.results.dc, tuple): + self.dc_ohmic_losses = tuple( + pvsystem.dc_ohmic_losses(Rw, df['i_mp']) + for Rw, df in zip(Rw, self.results.dc) + ) + for df, loss in zip(self.results.dc, self.dc_ohmic_losses): + df['p_mp'] = df['p_mp'] - loss + else: + self.dc_ohmic_losses = pvsystem.dc_ohmic_losses( + Rw, self.results.dc['i_mp'] + ) + self.results.dc['p_mp'] = (self.results.dc['p_mp'] + - self.dc_ohmic_losses) return self def no_dc_ohmic_loss(self): From 8749450a3941ea6a1007341e86d001a8f3ccddae Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Wed, 24 Feb 2021 15:23:01 -0700 Subject: [PATCH 09/16] added tests for new module parameters --- pvlib/pvsystem.py | 8 ++++---- pvlib/tests/test_pvsystem.py | 38 +++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index cb3d81915f..066e439fe0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1446,12 +1446,12 @@ def dc_ohms_from_percent(self): # raise error if relevant Vmp and Imp parameters are not found else: - raise ValueError('Parameters for Vmp and Imp could not be found ', - 'in the array module parameters. Module ', - 'parameters must include one set of ', + raise ValueError('Parameters for Vmp and Imp could not be found ' + 'in the array module parameters. Module ' + 'parameters must include one set of ' '{"V_mp_ref", "I_mp_Ref"}, ' '{"Vmpo", "Impo"}, or ' - '{"Vmpp", "Impp"}, ' + '{"Vmpp", "Impp"}.' ) kwargs.update({'modules_per_string': self.modules_per_string, diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 23e0e4573c..21d0af5564 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2082,17 +2082,53 @@ def test_Array_dc_ohms_from_percent(mocker): mocker.spy(pvsystem, 'dc_ohms_from_percent') expected = .1425 + array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, module_parameters={'I_mp_ref': 8, 'V_mp_ref': 38}) out = array.dc_ohms_from_percent() + pvsystem.dc_ohms_from_percent.assert_called_with( + dc_ohmic_percent=3, + vmp_ref=38, + imp_ref=8, + modules_per_string=1, + strings=1 + ) + assert_allclose(out, expected) - pvsystem.dc_ohms_from_percent.assert_called_once_with( + array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, + module_parameters={'Impo': 8, + 'Vmpo': 38}) + out = array.dc_ohms_from_percent() + pvsystem.dc_ohms_from_percent.assert_called_with( dc_ohmic_percent=3, vmp_ref=38, imp_ref=8, modules_per_string=1, strings=1 ) + assert_allclose(out, expected) + array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, + module_parameters={'Impp': 8, + 'Vmpp': 38}) + out = array.dc_ohms_from_percent() + + pvsystem.dc_ohms_from_percent.assert_called_with( + dc_ohmic_percent=3, + vmp_ref=38, + imp_ref=8, + modules_per_string=1, + strings=1 + ) assert_allclose(out, expected) + + with pytest.raises(ValueError, + match=('Parameters for Vmp and Imp could not be found ' + 'in the array module parameters. Module ' + 'parameters must include one set of ' + '{"V_mp_ref", "I_mp_Ref"}, ' + '{"Vmpo", "Impo"}, or ' + '{"Vmpp", "Impp"}.')): + array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}) + out = array.dc_ohms_from_percent() From 4500bc71a1349f29384cc8f3f54f59256555c4a7 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Wed, 24 Feb 2021 15:42:16 -0700 Subject: [PATCH 10/16] modelchain tests for multiple array support --- pvlib/tests/test_modelchain.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 1a62a74bb5..b95e282b3a 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1496,6 +1496,7 @@ def dc_constant_losses(mc): def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, + cec_dc_snl_ac_arrays, location, weather, mocker): @@ -1517,6 +1518,22 @@ def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, assert isinstance(mc.dc_ohmic_losses, pd.Series) + system = cec_dc_snl_ac_arrays + + for array in system.arrays: + array.array_losses_parameters = dict(dc_ohmic_percent=3) + + mc = ModelChain(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 == 3 + assert len(mc.dc_ohmic_losses) == len(mc.system.arrays) + + assert isinstance(mc.dc_ohmic_losses, tuple) + def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, location, From 547f6c192d85386bf0b982d2744a0a306d63f47d Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Fri, 5 Mar 2021 12:15:16 -0700 Subject: [PATCH 11/16] fix api, document render, and use explict variables --- docs/sphinx/source/api.rst | 2 +- pvlib/pvsystem.py | 49 +++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index c36ab0a7bf..43f598c7c7 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -332,6 +332,7 @@ Pvsyst model pvsystem.calcparams_pvsyst pvsystem.singlediode pvsystem.dc_ohms_from_percent + pvsystem.dc_ohmic_losses PVWatts model ^^^^^^^^^^^^^ @@ -391,7 +392,6 @@ Loss models pvsystem.combine_loss_factors pvsystem.dc_ohms_from_percent - modelchain.ModelChain.dc_ohmic_model Snow ---- diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 066e439fe0..de37aa877e 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1405,15 +1405,21 @@ def dc_ohms_from_percent(self): Makes use of array module parameters according to the following DC models: + CEC: - `self.module_parameters["V_mp_ref"]`, - `self.module_parameters["I_mp_ref"]`, + + * `self.module_parameters["V_mp_ref"]`, + * `self.module_parameters["I_mp_ref"]` + SAPM: - `self.module_parameters["Vmpo"]`, - `self.module_parameters["Impo"]`, + + * `self.module_parameters["Vmpo"]`, + * `self.module_parameters["Impo"]` + PVsyst-like or other: - `self.module_parameters["Vmpp"]`, - `self.module_parameters["Impp"]`, + + * `self.module_parameters["Vmpp"]`, + * `self.module_parameters["Impp"]` Other array parameters that are used are: `self.losses_parameters["dc_ohmic_percent"]`, @@ -1423,26 +1429,23 @@ def dc_ohms_from_percent(self): See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for more details. """ - kwargs = _build_kwargs(['dc_ohmic_percent'], - self.array_losses_parameters) - - # add relevent Vmp and Imp parameters from CEC parameters + # get relevent Vmp and Imp parameters from CEC parameters if all([elem in self.module_parameters for elem in ['V_mp_ref', 'I_mp_ref']]): - kwargs.update({'vmp_ref': self.module_parameters['V_mp_ref'], - 'imp_ref': self.module_parameters['I_mp_ref']}) + vmp_ref = self.module_parameters['V_mp_ref'] + imp_ref = self.module_parameters['I_mp_ref'] - # add relevant Vmp and Imp parameters from SAPM parameters + # get relevant Vmp and Imp parameters from SAPM parameters elif all([elem in self.module_parameters for elem in ['Vmpo', 'Impo']]): - kwargs.update({'vmp_ref': self.module_parameters['Vmpo'], - 'imp_ref': self.module_parameters['Impo']}) + vmp_ref = self.module_parameters['Vmpo'] + imp_ref = self.module_parameters['Impo'] - # add relevant Vmp and Imp parameters if they are PVsyst-like + # get relevant Vmp and Imp parameters if they are PVsyst-like elif all([elem in self.module_parameters for elem in ['Vmpp', 'Impp']]): - kwargs.update({'vmp_ref': self.module_parameters['Vmpp'], - 'imp_ref': self.module_parameters['Impp']}) + vmp_ref = self.module_parameters['Vmpp'] + imp_ref = self.module_parameters['Impp'] # raise error if relevant Vmp and Imp parameters are not found else: @@ -1454,10 +1457,12 @@ def dc_ohms_from_percent(self): '{"Vmpp", "Impp"}.' ) - kwargs.update({'modules_per_string': self.modules_per_string, - 'strings': self.strings}) - - return dc_ohms_from_percent(**kwargs) + return dc_ohms_from_percent( + vmp_ref, + imp_ref, + self.array_losses_parameters['dc_ohmic_percent'], + self.modules_per_string, + self.strings) def calcparams_desoto(effective_irradiance, temp_cell, From 68857abf63b97e7580cbe0797cfe8f4c05e27c12 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Fri, 5 Mar 2021 13:06:43 -0700 Subject: [PATCH 12/16] resolve conflict and small format fix --- pvlib/modelchain.py | 2 -- pvlib/pvsystem.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 5bb28ccfe6..294f280d61 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -429,8 +429,6 @@ def __init__(self, system, location, self.dc_ohmic_model = dc_ohmic_model self.losses_model = losses_model - self.orientation_strategy = orientation_strategy - self.weather = None self.times = None diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index de37aa877e..612dc69e11 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1408,17 +1408,17 @@ def dc_ohms_from_percent(self): CEC: - * `self.module_parameters["V_mp_ref"]`, + * `self.module_parameters["V_mp_ref"]` * `self.module_parameters["I_mp_ref"]` SAPM: - * `self.module_parameters["Vmpo"]`, + * `self.module_parameters["Vmpo"]` * `self.module_parameters["Impo"]` PVsyst-like or other: - * `self.module_parameters["Vmpp"]`, + * `self.module_parameters["Vmpp"]` * `self.module_parameters["Impp"]` Other array parameters that are used are: From 26be1fe96b6e11ff9aeb9973a3e6b1db943f237d Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Fri, 5 Mar 2021 14:14:49 -0700 Subject: [PATCH 13/16] dc_ohmic_losses as results attribute --- pvlib/modelchain.py | 20 +++++++++++--------- pvlib/tests/test_modelchain.py | 8 ++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 294f280d61..21c88f440e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -273,7 +273,8 @@ class ModelChainResult: _singleton_tuples: bool = field(default=False) _per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier', 'spectral_modifier', 'cell_temperature', - 'effective_irradiance', 'dc', 'diode_params'} + 'effective_irradiance', 'dc', 'diode_params', + 'dc_ohmic_losses'} # system-level information solar_position: Optional[pd.DataFrame] = field(default=None) @@ -293,6 +294,7 @@ class ModelChainResult: dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \ field(default=None) diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) + dc_ohmic_losses: Optional[PerArray[pd.Series]] = field(default=None) def _result_type(self, value): """Coerce `value` to the correct type according to @@ -407,7 +409,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, + dc_ohmic_model='no_loss', losses_model='no_loss', name=None): self.name = name @@ -1080,9 +1082,9 @@ def dc_ohmic_model(self): @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): + # if model is None: + # self._dc_ohmic_model = self.no_dc_ohmic_loss + if isinstance(model, str): model = model.lower() if model == 'dc_ohms_from_percent': self._dc_ohmic_model = self.dc_ohms_from_percent @@ -1102,18 +1104,18 @@ def dc_ohms_from_percent(self): """ Rw = self.system.dc_ohms_from_percent() if isinstance(self.results.dc, tuple): - self.dc_ohmic_losses = tuple( + self.results.dc_ohmic_losses = tuple( pvsystem.dc_ohmic_losses(Rw, df['i_mp']) for Rw, df in zip(Rw, self.results.dc) ) - for df, loss in zip(self.results.dc, self.dc_ohmic_losses): + for df, loss in zip(self.results.dc, self.results.dc_ohmic_losses): df['p_mp'] = df['p_mp'] - loss else: - self.dc_ohmic_losses = pvsystem.dc_ohmic_losses( + self.results.dc_ohmic_losses = pvsystem.dc_ohmic_losses( Rw, self.results.dc['i_mp'] ) self.results.dc['p_mp'] = (self.results.dc['p_mp'] - - self.dc_ohmic_losses) + - self.results.dc_ohmic_losses) return self def no_dc_ohmic_loss(self): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index b95e282b3a..81e3f53628 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1516,7 +1516,7 @@ def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, assert m.call_count == 1 - assert isinstance(mc.dc_ohmic_losses, pd.Series) + assert isinstance(mc.results.dc_ohmic_losses, pd.Series) system = cec_dc_snl_ac_arrays @@ -1530,9 +1530,9 @@ def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, mc.run_model(weather) assert m.call_count == 3 - assert len(mc.dc_ohmic_losses) == len(mc.system.arrays) + assert len(mc.results.dc_ohmic_losses) == len(mc.system.arrays) - assert isinstance(mc.dc_ohmic_losses, tuple) + assert isinstance(mc.results.dc_ohmic_losses, tuple) def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, @@ -1548,7 +1548,7 @@ def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, mc.run_model(weather) assert m.call_count == 1 - assert hasattr(mc, 'dc_ohmic_losses') is False + assert mc.results.dc_ohmic_losses is None def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, From f96ac03e6f99f122d028828250cbc1a6fbc65713 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Fri, 5 Mar 2021 14:42:10 -0700 Subject: [PATCH 14/16] fix tests and None model --- pvlib/modelchain.py | 6 +++--- pvlib/tests/test_modelchain.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 21c88f440e..9d177bf1ac 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1082,9 +1082,9 @@ def dc_ohmic_model(self): @dc_ohmic_model.setter def dc_ohmic_model(self, model): - # if model is None: - # self._dc_ohmic_model = self.no_dc_ohmic_loss - if isinstance(model, str): + 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 diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 81e3f53628..c49c632ebb 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1547,6 +1547,7 @@ def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, dc_ohmic_model='no_loss') mc.run_model(weather) + assert mc.dc_ohmic_model == mc.no_dc_ohmic_loss assert m.call_count == 1 assert mc.results.dc_ohmic_losses is None @@ -1564,6 +1565,17 @@ def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) assert not mc.results.ac.empty + mc = ModelChain(cec_dc_snl_ac_system, location, + aoi_model='no_loss', + spectral_model='no_loss', + dc_ohmic_model=None) + mc.run_model(weather) + + assert m.call_count == 1 + assert mc.dc_ohmic_model == mc.no_dc_ohmic_loss + assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) + assert not mc.results.ac.empty + def test_dc_ohmic_not_a_model(cec_dc_snl_ac_system, location, weather, mocker): From ba92da61df138bf5f4842dcf51648e429cb928c9 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Mon, 15 Mar 2021 14:34:15 -0700 Subject: [PATCH 15/16] remove none as model option --- pvlib/modelchain.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9d177bf1ac..95411b3359 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -382,7 +382,7 @@ 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 + dc_ohmic_model: str or function, default 'no_loss' Valid strings are 'dc_ohms_from_percent', 'no_loss'. The ModelChain instance will be passed as the first argument to a user-defined function. @@ -1082,9 +1082,7 @@ def dc_ohmic_model(self): @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): + if isinstance(model, str): model = model.lower() if model == 'dc_ohms_from_percent': self._dc_ohmic_model = self.dc_ohms_from_percent From 19728cd2be7ca1e488fdb92d52812e3971da9ac1 Mon Sep 17 00:00:00 2001 From: Nate Croft Date: Mon, 15 Mar 2021 14:41:10 -0700 Subject: [PATCH 16/16] remove none from tests --- pvlib/tests/test_modelchain.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index c49c632ebb..28a3adef45 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1565,17 +1565,6 @@ def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) assert not mc.results.ac.empty - mc = ModelChain(cec_dc_snl_ac_system, location, - aoi_model='no_loss', - spectral_model='no_loss', - dc_ohmic_model=None) - mc.run_model(weather) - - assert m.call_count == 1 - assert mc.dc_ohmic_model == mc.no_dc_ohmic_loss - assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) - assert not mc.results.ac.empty - def test_dc_ohmic_not_a_model(cec_dc_snl_ac_system, location, weather, mocker):