-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Support list of weather input for ModelChain with a single-array PVSystem #1157
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
5283390
8c9e7da
5e36312
42f7ca0
7dd6134
4461f45
6ab9bab
88a5e15
562d729
081c258
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 |
---|---|---|
|
@@ -268,22 +268,47 @@ class ModelChainResult: | |
_T = TypeVar('T') | ||
PerArray = Union[_T, Tuple[_T, ...]] | ||
"""Type for fields that vary between arrays""" | ||
|
||
# these attributes are used in __setattr__ to determine the correct type. | ||
_singleton_tuples: bool = field(default=False) | ||
_per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier', | ||
'spectral_modifier', 'cell_temperature', | ||
'effective_irradiance', 'dc', 'diode_params'} | ||
|
||
# system-level information | ||
solar_position: Optional[pd.DataFrame] = field(default=None) | ||
airmass: Optional[pd.DataFrame] = field(default=None) | ||
ac: Optional[pd.Series] = field(default=None) | ||
# per DC array information | ||
tracking: Optional[pd.DataFrame] = field(default=None) | ||
wfvining marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# per DC array information | ||
total_irrad: Optional[PerArray[pd.DataFrame]] = field(default=None) | ||
aoi: Optional[PerArray[pd.Series]] = field(default=None) | ||
aoi_modifier: Optional[PerArray[pd.Series]] = field(default=None) | ||
spectral_modifier: Optional[PerArray[pd.Series]] = field(default=None) | ||
aoi_modifier: Optional[PerArray[Union[pd.Series, float]]] = \ | ||
field(default=None) | ||
spectral_modifier: Optional[PerArray[Union[pd.Series, float]]] = \ | ||
field(default=None) | ||
cell_temperature: Optional[PerArray[pd.Series]] = field(default=None) | ||
effective_irradiance: Optional[PerArray[pd.Series]] = field(default=None) | ||
dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \ | ||
field(default=None) | ||
diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) | ||
|
||
def _result_type(self, value): | ||
"""Coerce `value` to the correct type according to | ||
``self._singleton_tuples``.""" | ||
# Allow None to pass through without being wrapped in a tuple | ||
if (self._singleton_tuples | ||
and not isinstance(value, tuple) | ||
and value is not None): | ||
return (value,) | ||
return value | ||
|
||
def __setattr__(self, key, value): | ||
if key in ModelChainResult._per_array_fields: | ||
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. could we use 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 would be nice, but I can't seem to figure out how to compare the concrete types returned from 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. After further research, my idea seems like a misuse of the typing module at this point in time, so I'm good with your solution. 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 a little bummed... I was excited to actually put the types to use. |
||
value = self._result_type(value) | ||
super().__setattr__(key, value) | ||
|
||
|
||
class ModelChain: | ||
""" | ||
|
@@ -684,12 +709,9 @@ def infer_dc_model(self): | |
'set the model with the dc_model kwarg.') | ||
|
||
def sapm(self): | ||
self.results.dc = self.system.sapm(self.results.effective_irradiance, | ||
self.results.cell_temperature) | ||
|
||
self.results.dc = self.system.scale_voltage_current_power( | ||
self.results.dc) | ||
|
||
dc = self.system.sapm(self.results.effective_irradiance, | ||
self.results.cell_temperature) | ||
self.results.dc = self.system.scale_voltage_current_power(dc) | ||
return self | ||
|
||
def _singlediode(self, calcparams_model_function): | ||
|
@@ -745,18 +767,14 @@ def pvwatts_dc(self): | |
pvlib.pvsystem.PVSystem.pvwatts_dc | ||
pvlib.pvsystem.PVSystem.scale_voltage_current_power | ||
""" | ||
self.results.dc = self.system.pvwatts_dc( | ||
self.results.effective_irradiance, self.results.cell_temperature) | ||
if isinstance(self.results.dc, tuple): | ||
temp = tuple( | ||
pd.DataFrame(s, columns=['p_mp']) for s in self.results.dc) | ||
else: | ||
temp = pd.DataFrame(self.results.dc, columns=['p_mp']) | ||
scaled = self.system.scale_voltage_current_power(temp) | ||
if isinstance(scaled, tuple): | ||
self.results.dc = tuple(s['p_mp'] for s in scaled) | ||
else: | ||
self.results.dc = scaled['p_mp'] | ||
dc = self.system.pvwatts_dc( | ||
self.results.effective_irradiance, | ||
self.results.cell_temperature, | ||
unwrap=False | ||
) | ||
p_mp = tuple(pd.DataFrame(s, columns=['p_mp']) for s in dc) | ||
scaled = self.system.scale_voltage_current_power(p_mp) | ||
self.results.dc = _tuple_from_dfs(scaled, "p_mp") | ||
return self | ||
|
||
@property | ||
|
@@ -866,23 +884,29 @@ def infer_aoi_model(self): | |
|
||
def ashrae_aoi_loss(self): | ||
self.results.aoi_modifier = self.system.get_iam( | ||
self.results.aoi, iam_model='ashrae') | ||
self.results.aoi, | ||
iam_model='ashrae' | ||
) | ||
return self | ||
|
||
def physical_aoi_loss(self): | ||
self.results.aoi_modifier = self.system.get_iam(self.results.aoi, | ||
iam_model='physical') | ||
self.results.aoi_modifier = self.system.get_iam( | ||
self.results.aoi, | ||
iam_model='physical' | ||
) | ||
return self | ||
|
||
def sapm_aoi_loss(self): | ||
self.results.aoi_modifier = self.system.get_iam(self.results.aoi, | ||
iam_model='sapm') | ||
self.results.aoi_modifier = self.system.get_iam( | ||
self.results.aoi, | ||
iam_model='sapm' | ||
) | ||
return self | ||
|
||
def martin_ruiz_aoi_loss(self): | ||
self.results.aoi_modifier = self.system.get_iam( | ||
self.results.aoi, | ||
iam_model='martin_ruiz') | ||
self.results.aoi, iam_model='martin_ruiz' | ||
) | ||
return self | ||
|
||
def no_aoi_loss(self): | ||
|
@@ -934,13 +958,15 @@ def infer_spectral_model(self): | |
|
||
def first_solar_spectral_loss(self): | ||
self.results.spectral_modifier = self.system.first_solar_spectral_loss( | ||
self.weather['precipitable_water'], | ||
self.results.airmass['airmass_absolute']) | ||
_tuple_from_dfs(self.weather, 'precipitable_water'), | ||
self.results.airmass['airmass_absolute'] | ||
) | ||
return self | ||
|
||
def sapm_spectral_loss(self): | ||
self.results.spectral_modifier = self.system.sapm_spectral_loss( | ||
self.results.airmass['airmass_absolute']) | ||
self.results.airmass['airmass_absolute'] | ||
) | ||
return self | ||
|
||
def no_spectral_loss(self): | ||
|
@@ -1066,7 +1092,7 @@ def infer_losses_model(self): | |
|
||
def pvwatts_losses(self): | ||
self.losses = (100 - self.system.pvwatts_losses()) / 100. | ||
if self.system.num_arrays > 1: | ||
if isinstance(self.results.dc, tuple): | ||
for dc in self.results.dc: | ||
dc *= self.losses | ||
else: | ||
|
@@ -1271,6 +1297,17 @@ def _verify(data, index=None): | |
for (i, array_data) in enumerate(data): | ||
_verify(array_data, i) | ||
|
||
def _configure_results(self): | ||
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. an alternative to this method might be to instantiate
I guess maybe you'd still want to extract that logic into a method like this one. Then the difference comes down to setting the right value vs instantiating the object with the right value. 🤷 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. This would basically be a transformation from 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. Should 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 viewing ModelChainResult as a single use object so I don't think we need to watch for that kind of state change. |
||
"""Configure the type used for per-array fields in ModelChainResult. | ||
|
||
Must be called after ``self.weather`` has been assigned. If | ||
``self.weather`` is a tuple and the number of arrays in the system | ||
is 1, then per-array results are stored as length-1 tuples. | ||
""" | ||
self.results._singleton_tuples = ( | ||
self.system.num_arrays == 1 and isinstance(self.weather, tuple) | ||
) | ||
|
||
def _assign_weather(self, data): | ||
def _build_weather(data): | ||
key_list = [k for k in WEATHER_KEYS if k in data] | ||
|
@@ -1286,6 +1323,7 @@ def _build_weather(data): | |
self.weather = tuple( | ||
_build_weather(weather) for weather in data | ||
) | ||
self._configure_results() | ||
return self | ||
|
||
def _assign_total_irrad(self, data): | ||
|
@@ -1383,7 +1421,8 @@ def prepare_inputs(self, weather): | |
_tuple_from_dfs(self.weather, 'ghi'), | ||
_tuple_from_dfs(self.weather, 'dhi'), | ||
airmass=self.results.airmass['airmass_relative'], | ||
model=self.transposition_model) | ||
model=self.transposition_model | ||
) | ||
|
||
return self | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
from collections import OrderedDict | ||
import functools | ||
import io | ||
import itertools | ||
import os | ||
from urllib.request import urlopen | ||
import numpy as np | ||
|
@@ -811,8 +812,9 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): | |
effective irradiance, i.e., the irradiance that is converted to | ||
electrical current. | ||
""" | ||
pw = self._validate_per_array(pw, system_wide=True) | ||
|
||
def _spectral_correction(array): | ||
def _spectral_correction(array, pw): | ||
if 'first_solar_spectral_coefficients' in \ | ||
array.module_parameters.keys(): | ||
coefficients = \ | ||
|
@@ -828,7 +830,9 @@ def _spectral_correction(array): | |
pw, airmass_absolute, | ||
module_type, coefficients | ||
) | ||
return tuple(_spectral_correction(array) for array in self.arrays) | ||
return tuple( | ||
itertools.starmap(_spectral_correction, zip(self.arrays, pw)) | ||
) | ||
|
||
def singlediode(self, photocurrent, saturation_current, | ||
resistance_series, resistance_shunt, nNsVth, | ||
|
@@ -891,29 +895,31 @@ def get_ac(self, model, p_dc, v_dc=None): | |
model = model.lower() | ||
multiple_arrays = self.num_arrays > 1 | ||
if model == 'sandia': | ||
p_dc = self._validate_per_array(p_dc) | ||
v_dc = self._validate_per_array(v_dc) | ||
if multiple_arrays: | ||
p_dc = self._validate_per_array(p_dc) | ||
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. why move the 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. If The goal is to be able to do both of these and have them act the same: system = PVSystem(arrays=[Array()], ...)
system.get_ac(p_dc=50, model='pvwatts')
system.get_ac(p_dc=(50,), model='pvwatts') 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. As written, 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. The tuples are always subscripted before being passed to the inverter functions when if model == 'sandia':
p_dc = self._validate_per_array(p_dc)
v_dc = self._validate_per_array(v_dc)
if multiple_arrays:
return inverter.sandia_multi(
v_dc, p_dc, self.inverter_parameters)
return inverter.sandia(v_dc[0], p_dc[0], self.inverter_parameters) When 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 think we can now move the p_dc = self._validate_per_array(p_dc)
if v_dc is not None:
v_dc = self._validate_per_array(v_dc) |
||
v_dc = self._validate_per_array(v_dc) | ||
inv_fun = inverter.sandia_multi | ||
else: | ||
inv_fun = inverter.sandia | ||
return inv_fun(v_dc, p_dc, self.inverter_parameters) | ||
return inverter.sandia_multi( | ||
v_dc, p_dc, self.inverter_parameters) | ||
return inverter.sandia(v_dc[0], p_dc[0], self.inverter_parameters) | ||
elif model == 'pvwatts': | ||
kwargs = _build_kwargs(['eta_inv_nom', 'eta_inv_ref'], | ||
self.inverter_parameters) | ||
p_dc = self._validate_per_array(p_dc) | ||
if multiple_arrays: | ||
p_dc = self._validate_per_array(p_dc) | ||
inv_fun = inverter.pvwatts_multi | ||
else: | ||
inv_fun = inverter.pvwatts | ||
return inv_fun(p_dc, self.inverter_parameters['pdc0'], **kwargs) | ||
return inverter.pvwatts_multi( | ||
p_dc, self.inverter_parameters['pdc0'], **kwargs) | ||
return inverter.pvwatts( | ||
p_dc[0], self.inverter_parameters['pdc0'], **kwargs) | ||
elif model == 'adr': | ||
if multiple_arrays: | ||
raise ValueError( | ||
'The adr inverter function cannot be used for an inverter', | ||
' with multiple MPPT inputs') | ||
else: | ||
return inverter.adr(v_dc, p_dc, self.inverter_parameters) | ||
# While this is only used for single-array systems, calling | ||
# _validate_per_arry lets us pass in singleton tuples. | ||
p_dc = self._validate_per_array(p_dc) | ||
v_dc = self._validate_per_array(v_dc) | ||
return inverter.adr(v_dc[0], p_dc[0], self.inverter_parameters) | ||
else: | ||
raise ValueError( | ||
model + ' is not a valid AC power model.', | ||
|
Uh oh!
There was an error while loading. Please reload this page.