Skip to content

Commit 2a1ed55

Browse files
authored
DC Ohmic Losses for pvsystem and modelchain (#1168)
* Support functions for ohmic losses * PVSystem dc ohmic losses and tests * ModelChain dc ohimc losses as tests * updates to api.rst for dc_ohmic_losses * Docstring formatting updates * make dc_ohms_from_percent work with other dc models * format fixes * support for multiple arrays * added tests for new module parameters * modelchain tests for multiple array support * fix api, document render, and use explict variables * resolve conflict and small format fix * dc_ohmic_losses as results attribute * fix tests and None model * remove none as model option * remove none from tests
1 parent 652a3ba commit 2a1ed55

File tree

5 files changed

+401
-3
lines changed

5 files changed

+401
-3
lines changed

docs/sphinx/source/api.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ Pvsyst model
334334
temperature.pvsyst_cell
335335
pvsystem.calcparams_pvsyst
336336
pvsystem.singlediode
337+
pvsystem.dc_ohms_from_percent
338+
pvsystem.dc_ohmic_losses
337339

338340
PVWatts model
339341
^^^^^^^^^^^^^
@@ -392,6 +394,7 @@ Loss models
392394
:toctree: generated/
393395

394396
pvsystem.combine_loss_factors
397+
pvsystem.dc_ohms_from_percent
395398

396399
Snow
397400
----
@@ -617,6 +620,7 @@ ModelChain properties that are aliases for your specific modeling functions.
617620
modelchain.ModelChain.aoi_model
618621
modelchain.ModelChain.spectral_model
619622
modelchain.ModelChain.temperature_model
623+
modelchain.ModelChain.dc_ohmic_model
620624
modelchain.ModelChain.losses_model
621625
modelchain.ModelChain.effective_irradiance_model
622626

@@ -647,6 +651,8 @@ ModelChain model definitions.
647651
modelchain.ModelChain.pvsyst_temp
648652
modelchain.ModelChain.faiman_temp
649653
modelchain.ModelChain.fuentes_temp
654+
modelchain.ModelChain.dc_ohmic_model
655+
modelchain.ModelChain.no_dc_ohmic_loss
650656
modelchain.ModelChain.pvwatts_losses
651657
modelchain.ModelChain.no_extra_losses
652658

pvlib/modelchain.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ class ModelChainResult:
255255
_singleton_tuples: bool = field(default=False)
256256
_per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier',
257257
'spectral_modifier', 'cell_temperature',
258-
'effective_irradiance', 'dc', 'diode_params'}
258+
'effective_irradiance', 'dc', 'diode_params',
259+
'dc_ohmic_losses'}
259260

260261
# system-level information
261262
solar_position: Optional[pd.DataFrame] = field(default=None)
@@ -275,6 +276,7 @@ class ModelChainResult:
275276
dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \
276277
field(default=None)
277278
diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None)
279+
dc_ohmic_losses: Optional[PerArray[pd.Series]] = field(default=None)
278280

279281
def _result_type(self, value):
280282
"""Coerce `value` to the correct type according to
@@ -356,6 +358,11 @@ class ModelChain:
356358
The ModelChain instance will be passed as the first argument to a
357359
user-defined function.
358360
361+
dc_ohmic_model: str or function, default 'no_loss'
362+
Valid strings are 'dc_ohms_from_percent', 'no_loss'. The ModelChain
363+
instance will be passed as the first argument to a user-defined
364+
function.
365+
359366
losses_model: str or function, default 'no_loss'
360367
Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance
361368
will be passed as the first argument to a user-defined function.
@@ -377,6 +384,7 @@ def __init__(self, system, location,
377384
airmass_model='kastenyoung1989',
378385
dc_model=None, ac_model=None, aoi_model=None,
379386
spectral_model=None, temperature_model=None,
387+
dc_ohmic_model='no_loss',
380388
losses_model='no_loss', name=None):
381389

382390
self.name = name
@@ -395,6 +403,7 @@ def __init__(self, system, location,
395403
self.spectral_model = spectral_model
396404
self.temperature_model = temperature_model
397405

406+
self.dc_ohmic_model = dc_ohmic_model
398407
self.losses_model = losses_model
399408

400409
self.weather = None
@@ -1020,6 +1029,49 @@ def fuentes_temp(self):
10201029
def noct_sam_temp(self):
10211030
return self._set_celltemp(self.system.noct_sam_celltemp)
10221031

1032+
@property
1033+
def dc_ohmic_model(self):
1034+
return self._dc_ohmic_model
1035+
1036+
@dc_ohmic_model.setter
1037+
def dc_ohmic_model(self, model):
1038+
if isinstance(model, str):
1039+
model = model.lower()
1040+
if model == 'dc_ohms_from_percent':
1041+
self._dc_ohmic_model = self.dc_ohms_from_percent
1042+
elif model == 'no_loss':
1043+
self._dc_ohmic_model = self.no_dc_ohmic_loss
1044+
else:
1045+
raise ValueError(model + ' is not a valid losses model')
1046+
else:
1047+
self._dc_ohmic_model = partial(model, self)
1048+
1049+
def dc_ohms_from_percent(self):
1050+
"""
1051+
Calculate time series of ohmic losses and apply those to the mpp power
1052+
output of the `dc_model` based on the pvsyst equivalent resistance
1053+
method. Uses a `dc_ohmic_percent` parameter in the `losses_parameters`
1054+
of the PVsystem.
1055+
"""
1056+
Rw = self.system.dc_ohms_from_percent()
1057+
if isinstance(self.results.dc, tuple):
1058+
self.results.dc_ohmic_losses = tuple(
1059+
pvsystem.dc_ohmic_losses(Rw, df['i_mp'])
1060+
for Rw, df in zip(Rw, self.results.dc)
1061+
)
1062+
for df, loss in zip(self.results.dc, self.results.dc_ohmic_losses):
1063+
df['p_mp'] = df['p_mp'] - loss
1064+
else:
1065+
self.results.dc_ohmic_losses = pvsystem.dc_ohmic_losses(
1066+
Rw, self.results.dc['i_mp']
1067+
)
1068+
self.results.dc['p_mp'] = (self.results.dc['p_mp']
1069+
- self.results.dc_ohmic_losses)
1070+
return self
1071+
1072+
def no_dc_ohmic_loss(self):
1073+
return self
1074+
10231075
@property
10241076
def losses_model(self):
10251077
return self._losses_model
@@ -1693,6 +1745,7 @@ def _run_from_effective_irrad(self, data=None):
16931745
"""
16941746
self._prepare_temperature(data)
16951747
self.dc_model()
1748+
self.dc_ohmic_model()
16961749
self.losses_model()
16971750
self.ac_model()
16981751

pvlib/pvsystem.py

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ def __init__(self,
191191
racking_model=None, losses_parameters=None, name=None):
192192

193193
if arrays is None:
194+
if losses_parameters is None:
195+
array_losses_parameters = {}
196+
else:
197+
array_losses_parameters = _build_kwargs(['dc_ohmic_percent'],
198+
losses_parameters)
194199
self.arrays = (Array(
195200
surface_tilt,
196201
surface_azimuth,
@@ -202,7 +207,8 @@ def __init__(self,
202207
temperature_model_parameters,
203208
modules_per_string,
204209
strings_per_inverter,
205-
racking_model
210+
racking_model,
211+
array_losses_parameters,
206212
),)
207213
else:
208214
self.arrays = tuple(arrays)
@@ -1092,6 +1098,17 @@ def pvwatts_ac(self, pdc):
10921098
return inverter.pvwatts(pdc, self.inverter_parameters['pdc0'],
10931099
**kwargs)
10941100

1101+
@_unwrap_single_value
1102+
def dc_ohms_from_percent(self):
1103+
"""
1104+
Calculates the equivalent resistance of the wires for each array using
1105+
:py:func:`pvlib.pvsystem.dc_ohms_from_percent`
1106+
1107+
See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for details.
1108+
"""
1109+
1110+
return tuple(array.dc_ohms_from_percent() for array in self.arrays)
1111+
10951112
@property
10961113
@_unwrap_single_value
10971114
def module_parameters(self):
@@ -1224,6 +1241,9 @@ class Array:
12241241
Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
12251242
Used to identify a parameter set for the SAPM cell temperature model.
12261243
1244+
array_losses_parameters: None, dict or Series, default None.
1245+
Supported keys are 'dc_ohmic_percent'.
1246+
12271247
"""
12281248

12291249
def __init__(self,
@@ -1233,7 +1253,8 @@ def __init__(self,
12331253
module_parameters=None,
12341254
temperature_model_parameters=None,
12351255
modules_per_string=1, strings=1,
1236-
racking_model=None, name=None):
1256+
racking_model=None, array_losses_parameters=None,
1257+
name=None):
12371258
self.surface_tilt = surface_tilt
12381259
self.surface_azimuth = surface_azimuth
12391260

@@ -1261,6 +1282,11 @@ def __init__(self,
12611282
else:
12621283
self.temperature_model_parameters = temperature_model_parameters
12631284

1285+
if array_losses_parameters is None:
1286+
self.array_losses_parameters = {}
1287+
else:
1288+
self.array_losses_parameters = array_losses_parameters
1289+
12641290
self.name = name
12651291

12661292
def __repr__(self):
@@ -1447,6 +1473,72 @@ def get_iam(self, aoi, iam_model='physical'):
14471473
else:
14481474
raise ValueError(model + ' is not a valid IAM model')
14491475

1476+
def dc_ohms_from_percent(self):
1477+
"""
1478+
Calculates the equivalent resistance of the wires using
1479+
:py:func:`pvlib.pvsystem.dc_ohms_from_percent`
1480+
1481+
Makes use of array module parameters according to the
1482+
following DC models:
1483+
1484+
CEC:
1485+
1486+
* `self.module_parameters["V_mp_ref"]`
1487+
* `self.module_parameters["I_mp_ref"]`
1488+
1489+
SAPM:
1490+
1491+
* `self.module_parameters["Vmpo"]`
1492+
* `self.module_parameters["Impo"]`
1493+
1494+
PVsyst-like or other:
1495+
1496+
* `self.module_parameters["Vmpp"]`
1497+
* `self.module_parameters["Impp"]`
1498+
1499+
Other array parameters that are used are:
1500+
`self.losses_parameters["dc_ohmic_percent"]`,
1501+
`self.modules_per_string`, and
1502+
`self.strings`.
1503+
1504+
See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for more details.
1505+
"""
1506+
1507+
# get relevent Vmp and Imp parameters from CEC parameters
1508+
if all([elem in self.module_parameters
1509+
for elem in ['V_mp_ref', 'I_mp_ref']]):
1510+
vmp_ref = self.module_parameters['V_mp_ref']
1511+
imp_ref = self.module_parameters['I_mp_ref']
1512+
1513+
# get relevant Vmp and Imp parameters from SAPM parameters
1514+
elif all([elem in self.module_parameters
1515+
for elem in ['Vmpo', 'Impo']]):
1516+
vmp_ref = self.module_parameters['Vmpo']
1517+
imp_ref = self.module_parameters['Impo']
1518+
1519+
# get relevant Vmp and Imp parameters if they are PVsyst-like
1520+
elif all([elem in self.module_parameters
1521+
for elem in ['Vmpp', 'Impp']]):
1522+
vmp_ref = self.module_parameters['Vmpp']
1523+
imp_ref = self.module_parameters['Impp']
1524+
1525+
# raise error if relevant Vmp and Imp parameters are not found
1526+
else:
1527+
raise ValueError('Parameters for Vmp and Imp could not be found '
1528+
'in the array module parameters. Module '
1529+
'parameters must include one set of '
1530+
'{"V_mp_ref", "I_mp_Ref"}, '
1531+
'{"Vmpo", "Impo"}, or '
1532+
'{"Vmpp", "Impp"}.'
1533+
)
1534+
1535+
return dc_ohms_from_percent(
1536+
vmp_ref,
1537+
imp_ref,
1538+
self.array_losses_parameters['dc_ohmic_percent'],
1539+
self.modules_per_string,
1540+
self.strings)
1541+
14501542

14511543
def calcparams_desoto(effective_irradiance, temp_cell,
14521544
alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
@@ -2885,6 +2977,80 @@ def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2,
28852977
return losses
28862978

28872979

2980+
def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent,
2981+
modules_per_string=1,
2982+
strings=1):
2983+
"""
2984+
Calculates the equivalent resistance of the wires from a percent
2985+
ohmic loss at STC.
2986+
2987+
Equivalent resistance is calculated with the function:
2988+
2989+
.. math::
2990+
Rw = (L_{stc} / 100) * (Varray / Iarray)
2991+
2992+
:math:`Rw` is the equivalent resistance in ohms
2993+
:math:`Varray` is the Vmp of the modules times modules per string
2994+
:math:`Iarray` is the Imp of the modules times strings per array
2995+
:math:`L_{stc}` is the input dc loss percent
2996+
2997+
Parameters
2998+
----------
2999+
vmp_ref: numeric
3000+
Voltage at maximum power in reference conditions [V]
3001+
imp_ref: numeric
3002+
Current at maximum power in reference conditions [V]
3003+
dc_ohmic_percent: numeric, default 0
3004+
input dc loss as a percent, e.g. 1.5% loss is input as 1.5
3005+
modules_per_string: int, default 1
3006+
Number of modules per string in the array.
3007+
strings: int, default 1
3008+
Number of parallel strings in the array.
3009+
3010+
Returns
3011+
----------
3012+
Rw: numeric
3013+
Equivalent resistance [ohm]
3014+
3015+
References
3016+
----------
3017+
.. [1] PVsyst 7 Help. "Array ohmic wiring loss".
3018+
https://www.pvsyst.com/help/ohmic_loss.htm
3019+
"""
3020+
vmp = modules_per_string * vmp_ref
3021+
3022+
imp = strings * imp_ref
3023+
3024+
Rw = (dc_ohmic_percent / 100) * (vmp / imp)
3025+
3026+
return Rw
3027+
3028+
3029+
def dc_ohmic_losses(resistance, current):
3030+
"""
3031+
Returns ohmic losses in units of power from the equivalent
3032+
resistance of the wires and the operating current.
3033+
3034+
Parameters
3035+
----------
3036+
resistance: numeric
3037+
Equivalent resistance of wires [ohm]
3038+
current: numeric, float or array-like
3039+
Operating current [A]
3040+
3041+
Returns
3042+
----------
3043+
loss: numeric
3044+
Power Loss [W]
3045+
3046+
References
3047+
----------
3048+
.. [1] PVsyst 7 Help. "Array ohmic wiring loss".
3049+
https://www.pvsyst.com/help/ohmic_loss.htm
3050+
"""
3051+
return resistance * current * current
3052+
3053+
28883054
def combine_loss_factors(index, *losses, fill_method='ffill'):
28893055
r"""
28903056
Combines Series loss fractions while setting a common index.

0 commit comments

Comments
 (0)