-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from all commits
5c79590
b6fa6f6
8b6769c
18228de
ab5ecff
f63a24b
0db5afe
a8a911e
3c7b85d
8749450
4500bc7
547f6c1
68857ab
26be1fe
f96ac03
ba92da6
19728cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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. | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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) | ||
|
@@ -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): | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm OK with either I think as loss models are added to pvlib and we work those models into ModelChain, we'll end up refactoring the parameter containers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think my concern was making it distinguishable from the That makes me realize that I added |
||
Supported keys are 'dc_ohmic_percent'. | ||
|
||
""" | ||
|
||
def __init__(self, | ||
|
@@ -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 | ||
|
||
|
@@ -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): | ||
|
@@ -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, | ||
|
@@ -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. | ||
|
Uh oh!
There was an error while loading. Please reload this page.