Skip to content

Change setter for losses_model to allow it to set multiple loss funct… #1084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ Pvsyst model
temperature.pvsyst_cell
pvsystem.calcparams_pvsyst
pvsystem.singlediode
pvsystem.dc_ohms_from_percent

PVWatts model
^^^^^^^^^^^^^
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
87 changes: 87 additions & 0 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
63 changes: 63 additions & 0 deletions pvlib/tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions pvlib/tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down