Skip to content

DC Ohmic Losses for pvsystem and modelchain #1168

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

Merged
merged 17 commits into from
Mar 16, 2021
Merged
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
6 changes: 6 additions & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ Pvsyst model
temperature.pvsyst_cell
pvsystem.calcparams_pvsyst
pvsystem.singlediode
pvsystem.dc_ohms_from_percent
pvsystem.dc_ohmic_losses

PVWatts model
^^^^^^^^^^^^^
Expand Down Expand Up @@ -389,6 +391,7 @@ Loss models
:toctree: generated/

pvsystem.combine_loss_factors
pvsystem.dc_ohms_from_percent

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

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

Expand Down
56 changes: 54 additions & 2 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -380,6 +382,11 @@ class ModelChain:
The ModelChain instance will be passed as the first argument to a
user-defined function.

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.

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 @@ -402,6 +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='no_loss',
losses_model='no_loss', name=None):

self.name = name
Expand All @@ -420,8 +428,8 @@ 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
self.times = None
Expand Down Expand Up @@ -1068,6 +1076,49 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea why codecov says this line isn't tested? It looks like it should be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was confusing for me as well. And now it is telling me that blank lines 1082 and 1095 are not covered, and that 1090 is not covered which it should be as there is a test specifically for dc_ohmic_model='no_loss'. I'd welcome an assist from someone more familiar with the tests on what might be leading to the failed coverage if anyone has an idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran a coverage report locally and cannot reproduce the codecov results. Adding exceptions to these lines causes errors, as you would expect.


@dc_ohmic_model.setter
def dc_ohmic_model(self, model):
if 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()
if isinstance(self.results.dc, 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.results.dc_ohmic_losses):
df['p_mp'] = df['p_mp'] - loss
else:
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.results.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 @@ -1737,6 +1788,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
170 changes: 168 additions & 2 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,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,
Expand All @@ -201,7 +206,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)
Expand Down Expand Up @@ -1017,6 +1023,17 @@ def pvwatts_ac(self, pdc):
return inverter.pvwatts(pdc, 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):
Expand Down Expand Up @@ -1149,6 +1166,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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, should have caught this earlier.... it seems repetitive to call this parameter array_losses_parameters since it's already on an Array. I suppose there is some risk of user confusion if this and the PVSystem losses_parameters are called the same thing, but it doesn't hurt to have extra keys in these dictionaries. It's unlikely that would change in the future.

Maybe we should live with the current API on master in an alpha release or two and then maybe take up renaming in a follow up issue. I could see something like #1176 providing more clarity on this.

Thoughts @ncroft-b4 @cwhanse ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with either array_losses_parameters or Array.losses_parameters (and PVSystem.losses_parameters) in this PR.

I think as loss models are added to pvlib and we work those models into ModelChain, we'll end up refactoring the parameter containers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think my concern was making it distinguishable from the PVSystem parameter. Also, to keep compatibility with initializing a PVSystem directly, there needs to be a difference between the losses_parameters that would be passed into the Array and the ones that would stay with the PVSystem.

That makes me realize that I added array_losses_parameters to the list of keys that get passed to Array from PVSystem if an Array is not specified, but I did not add array_losses_parameters explicitly to the documentation for PVSystem or to the init. That makes sense to be because array_losses_parameters is only an Array parameter, but it does make it different from the other Array parameters that are still part of PVSystem by legacy. I'm good with it as it is but if you want to keep it uniform with the legacy parameters we could change it.

Supported keys are 'dc_ohmic_percent'.

"""

def __init__(self,
Expand All @@ -1158,7 +1178,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

Expand Down Expand Up @@ -1186,6 +1207,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):
Expand Down Expand Up @@ -1372,6 +1398,72 @@ 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`

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.modules_per_string`, and
`self.strings`.

See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for more details.
"""

# 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']]):
vmp_ref = self.module_parameters['V_mp_ref']
imp_ref = self.module_parameters['I_mp_ref']

# get relevant Vmp and Imp parameters from SAPM parameters
elif all([elem in self.module_parameters
for elem in ['Vmpo', 'Impo']]):
vmp_ref = self.module_parameters['Vmpo']
imp_ref = self.module_parameters['Impo']

# get relevant Vmp and Imp parameters if they are PVsyst-like
elif all([elem in self.module_parameters
for elem in ['Vmpp', '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:
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"}.'
)

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,
alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
Expand Down Expand Up @@ -2810,6 +2902,80 @@ def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2,
return losses


def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent,
modules_per_string=1,
strings=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:`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
----------
vmp_ref: numeric
Voltage at maximum power in reference conditions [V]
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
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 [ohm]

References
----------
.. [1] PVsyst 7 Help. "Array ohmic wiring loss".
https://www.pvsyst.com/help/ohmic_loss.htm
"""
vmp = modules_per_string * vmp_ref

imp = strings * imp_ref

Rw = (dc_ohmic_percent / 100) * (vmp / imp)

return Rw


def dc_ohmic_losses(resistance, current):
"""
Returns ohmic losses in units of power from the equivalent
resistance of the wires and the operating current.

Parameters
----------
resistance: numeric
Equivalent resistance of wires [ohm]
current: numeric, float or array-like
Operating current [A]

Returns
----------
loss: numeric
Power Loss [W]

References
----------
.. [1] PVsyst 7 Help. "Array ohmic wiring loss".
https://www.pvsyst.com/help/ohmic_loss.htm
"""
return resistance * current * current


def combine_loss_factors(index, *losses, fill_method='ffill'):
r"""
Combines Series loss fractions while setting a common index.
Expand Down
Loading