Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
16 changes: 16 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ Deprecations
* ``ModelChain.total_irrad``
* ``ModelChain.tracking``

* The following attributes of :py:class:`pvlib.pvsystem.PVSystem` and
:py:class:`pvlib.tracking.SingleAxisTracker` have been deprecated in
favor of the corresponding :py:class:`pvlib.pvsystem.Array` attributes:

* ``PVSystem.module_parameters``
* ``PVSystem.module``
* ``PVSystem.module_type``
* ``PVSystem.albedo``
* ``PVSystem.temperature_model_parameters``
* ``PVSystem.surface_tilt``
* ``PVSystem.surface_azimuth``
* ``PVSystem.racking_model``
* ``PVSystem.modules_per_string``
* ``PVSystem.strings_per_inverter``
Copy link
Member

Choose a reason for hiding this comment

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

I like the alphabetization of the list preceding this one and would prefer to see that here too.



Enhancements
~~~~~~~~~~~~
* Add :func:`~pvlib.iotools.read_bsrn` for reading BSRN solar radiation data
Expand Down
57 changes: 34 additions & 23 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,26 +328,26 @@ class ModelChain:

dc_model: None, str, or function, default None
If None, the model will be inferred from the contents of
system.module_parameters. Valid strings are 'sapm',
system.arrays[i].module_parameters. Valid strings are 'sapm',
'desoto', 'cec', 'pvsyst', 'pvwatts'. The ModelChain instance will
be passed as the first argument to a user-defined function.

ac_model: None, str, or function, default None
If None, the model will be inferred from the contents of
system.inverter_parameters and system.module_parameters. Valid
strings are 'sandia', 'adr', 'pvwatts'. The
system.inverter_parameters and system.arrays[i].module_parameters.
Copy link
Member

Choose a reason for hiding this comment

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

I don't see that infer_ac_model uses any information from system.arrays. The check on num_arrays is meant to provide a more meaningful error message, if a user chooses the adr inverter model that doesn't (presently) support multiple inputs.

Valid strings are 'sandia', 'adr', 'pvwatts'. The
ModelChain instance will be passed as the first argument to a
user-defined function.

aoi_model: None, str, or function, default None
If None, the model will be inferred from the contents of
system.module_parameters. Valid strings are 'physical',
system.arrays[i].module_parameters. Valid strings are 'physical',
'ashrae', 'sapm', 'martin_ruiz', 'no_loss'. The ModelChain instance
will be passed as the first argument to a user-defined function.

spectral_model: None, str, or function, default None
If None, the model will be inferred from the contents of
system.module_parameters. Valid strings are 'sapm',
system.arrays[i].module_parameters. Valid strings are 'sapm',
'first_solar', 'no_loss'. The ModelChain instance will be passed
as the first argument to a user-defined function.

Expand Down Expand Up @@ -605,9 +605,10 @@ def dc_model(self, model):
model = model.lower()
if model in _DC_MODEL_PARAMS.keys():
# validate module parameters
module_parameters = tuple(
array.module_parameters for array in self.system.arrays)
missing_params = (
_DC_MODEL_PARAMS[model] -
_common_keys(self.system.module_parameters))
_DC_MODEL_PARAMS[model] - _common_keys(module_parameters))
if missing_params: # some parameters are not in module.keys()
raise ValueError(model + ' selected for the DC model but '
'one or more Arrays are missing '
Expand All @@ -630,7 +631,8 @@ def dc_model(self, model):

def infer_dc_model(self):
"""Infer DC power model from Array module parameters."""
params = _common_keys(self.system.module_parameters)
params = _common_keys(
tuple(array.module_parameters for array in self.system.arrays))
if {'A0', 'A1', 'C7'} <= params:
return self.sapm, 'sapm'
elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s',
Expand All @@ -645,9 +647,10 @@ def infer_dc_model(self):
return self.pvwatts_dc, 'pvwatts'
else:
raise ValueError('could not infer DC model from '
'system.module_parameters. Check '
'system.module_parameters or explicitly '
'set the model with the dc_model kwarg.')
'system.arrays[i].module_parameters. Check '
'system.arrays[i].module_parameters or '
Copy link
Member

Choose a reason for hiding this comment

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

I don't know if this notation will make sense to some users. I'm fine with it, but here's an alternative:

Could not infer DC model from the module_parameters attributes of system.arrays. Check the module_parameters attributes or explicitly set the model with the dc_model keyword argument.

'explicitly set the model with the dc_model '
'kwarg.')

def sapm(self):
dc = self.system.sapm(self.results.effective_irradiance,
Expand Down Expand Up @@ -696,7 +699,7 @@ def pvwatts_dc(self):
"""Calculate DC power using the PVWatts model.

Results are stored in ModelChain.results.dc. DC power is computed
from PVSystem.module_parameters['pdc0'] and then scaled by
from PVSystem.arrays[i].module_parameters['pdc0'] and then scaled by
PVSystem.modules_per_string and PVSystem.strings_per_inverter.

Returns
Expand Down Expand Up @@ -805,7 +808,9 @@ def aoi_model(self, model):
self._aoi_model = partial(model, self)

def infer_aoi_model(self):
params = _common_keys(self.system.module_parameters)
module_parameters = tuple(
array.module_parameters for array in self.system.arrays)
params = _common_keys(module_parameters)
if {'K', 'L', 'n'} <= params:
return self.physical_aoi_loss
elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params:
Expand All @@ -816,8 +821,8 @@ def infer_aoi_model(self):
return self.martin_ruiz_aoi_loss
else:
raise ValueError('could not infer AOI model from '
'system.module_parameters. Check that the '
'module_parameters for all Arrays in '
'system.arrays[i].module_parameters. Check that '
'the module_parameters for all Arrays in '
'system.arrays contain parameters for '
'the physical, aoi, ashrae or martin_ruiz model; '
'explicitly set the model with the aoi_model '
Expand Down Expand Up @@ -880,7 +885,9 @@ def spectral_model(self, model):

def infer_spectral_model(self):
"""Infer spectral model from system attributes."""
params = _common_keys(self.system.module_parameters)
module_parameters = tuple(
array.module_parameters for array in self.system.arrays)
params = _common_keys(module_parameters)
if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params:
return self.sapm_spectral_loss
elif ((('Technology' in params or
Expand All @@ -890,8 +897,8 @@ def infer_spectral_model(self):
return self.first_solar_spectral_loss
else:
raise ValueError('could not infer spectral model from '
'system.module_parameters. Check that the '
'module_parameters for all Arrays in '
'system.arrays[i].module_parameters. Check that '
'the module_parameters for all Arrays in '
'system.arrays contain valid '
'first_solar_spectral_coefficients, a valid '
'Material or Technology value, or set '
Expand Down Expand Up @@ -940,20 +947,24 @@ def temperature_model(self, model):
# check system.temperature_model_parameters for consistency
name_from_params = self.infer_temperature_model().__name__
if self._temperature_model.__name__ != name_from_params:
common_params = _common_keys(tuple(
array.temperature_model_parameters
for array in self.system.arrays))
raise ValueError(
f'Temperature model {self._temperature_model.__name__} is '
f'inconsistent with PVSystem temperature model '
f'parameters. All Arrays in system.arrays must have '
f'consistent parameters. Common temperature model '
f'parameters: '
f'{_common_keys(self.system.temperature_model_parameters)}'
f'parameters: {common_params}'
)
else:
self._temperature_model = partial(model, self)

def infer_temperature_model(self):
"""Infer temperature model from system attributes."""
params = _common_keys(self.system.temperature_model_parameters)
temperature_model_parameters = tuple(
array.temperature_model_parameters for array in self.system.arrays)
params = _common_keys(temperature_model_parameters)
# remove or statement in v0.9
if {'a', 'b', 'deltaT'} <= params or (
not params and self.system.racking_model is None
Expand Down Expand Up @@ -1057,7 +1068,7 @@ def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod):
self.results.spectral_modifier, self.results.aoi_modifier))
else:
self.results.effective_irradiance = _eff_irrad(
self.system.module_parameters,
self.system.arrays[0].module_parameters,
self.results.total_irrad,
self.results.spectral_modifier,
self.results.aoi_modifier
Expand Down Expand Up @@ -1481,7 +1492,7 @@ def _prepare_temperature_single_array(self, data, poa):
self.results.cell_temperature = self._get_cell_temperature(
data,
poa,
self.system.temperature_model_parameters
self.system.arrays[0].temperature_model_parameters
)
if self.results.cell_temperature is None:
self.temperature_model()
Expand Down
44 changes: 44 additions & 0 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,36 @@ def f(*args, **kwargs):
return f


def _check_deprecated_passthrough(func):
"""
Decorator to warn or error when getting and setting the "pass-through"
PVSystem properties that have been moved to Array. Emits a warning for
PVSystems with only one Array and raises an error for PVSystems with
more than one Array.
"""

@functools.wraps(func)
def wrapper(self, *args, **kwargs):
pvsystem_attr = func.__name__
class_name = self.__class__.__name__ # PVSystem or SingleAxisTracker
overrides = { # some Array attrs aren't the same as PVSystem
'strings_per_inverter': 'strings',
}
array_attr = overrides.get(pvsystem_attr, pvsystem_attr)
alternative = f'{class_name}.arrays[i].{array_attr}'

if len(self.arrays) > 1:
raise AttributeError(
f'{class_name}.{pvsystem_attr} not supported for multi-array '
f'systems. Use {alternative} instead.')
Copy link
Member

Choose a reason for hiding this comment

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

Consistent with my comment in modelchain.py, maybe something like:

{class_name}.{pvsystem_attr} not supported for multi-array systems. Set {array_attr} for each Array in {class_name}.arrays instead.


wrapped = deprecated('0.9', alternative=alternative, removal='0.10',
name=f"{class_name}.{pvsystem_attr}")(func)
return wrapped(self, *args, **kwargs)

return wrapper


# not sure if this belongs in the pvsystem module.
# maybe something more like core.py? It may eventually grow to
# import a lot more functionality from other modules.
Expand Down Expand Up @@ -1019,72 +1049,86 @@ def pvwatts_ac(self, pdc):

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def module_parameters(self):
return tuple(array.module_parameters for array in self.arrays)

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def module(self):
return tuple(array.module for array in self.arrays)

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def module_type(self):
return tuple(array.module_type for array in self.arrays)

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def temperature_model_parameters(self):
return tuple(array.temperature_model_parameters
for array in self.arrays)

@temperature_model_parameters.setter
Copy link
Member

Choose a reason for hiding this comment

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

The setters were added in #1076, so the deprecation requirement is a little more subtle. I think we either need to implement deprecation setters for each attribute or implement a __setattr__ like this from modelchain:

def __setattr__(self, key, value):
if key in ModelChain._deprecated_attrs:
msg = f'ModelChain.{key} is deprecated from v0.9. Use' \
f' ModelChain.results.{key} instead'
warnings.warn(msg, pvlibDeprecationWarning)
setattr(self.results, key, value)
else:
super().__setattr__(key, value)

For that matter we could implement a __getattr__ instead of properties, like this one:

def __getattr__(self, key):
if key in ModelChain._deprecated_attrs:
msg = f'ModelChain.{key} is deprecated and will' \
f' be removed in v0.10. Use' \
f' ModelChain.results.{key} instead'
warnings.warn(msg, pvlibDeprecationWarning)
return getattr(self.results, key)
# __getattr__ is only called if __getattribute__ fails.
# In that case we should check if key is a deprecated attribute,
# and fail with an AttributeError if it is not.
raise AttributeError

I don't know that one approach is better than another.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not following the issue here, can you expand on what's different about the setters?

Copy link
Member

Choose a reason for hiding this comment

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

After trying to come up with an example, I'm realizing the problem is a little different than I first described.

There are currently setters for only 4 properties:

  • temperature_model_parameters
  • surface_tilt
  • surface_azimuth
  • racking_model

The remaining properties do not contain setters. At first I thought this meant that for all of the other properties, a user could set something on a PVSystem object that's different than what they'd get back from a getter that inspects the Arrays. But I'm now reminded that the user simply will not be able to set them.

In [5]: system = PVSystem([Array()])

In [6]: system.albedo = 0.3
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-d89b04b86f95> in <module>
----> 1 system.albedo = 0.3

AttributeError: can't set attribute

So the issue is that the user can set some but not all of the deprecated attributes. I'd prefer if they could set none or all.

I brought up the __getattr__ and __setattr__ methods only as an alternative to the properties and setters.

Copy link
Member Author

Choose a reason for hiding this comment

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

So the issue is that the user can set some but not all of the deprecated attributes. I'd prefer if they could set none or all.

I see, and agree it should be consistent. I think omitting the setters was a (probably unintentional?) breaking change, so I think being able to set all (with a deprecation warning) makes sense. I've added setters where they were missing.

@_check_deprecated_passthrough
def temperature_model_parameters(self, value):
for array in self.arrays:
array.temperature_model_parameters = value

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def surface_tilt(self):
return tuple(array.surface_tilt for array in self.arrays)

@surface_tilt.setter
@_check_deprecated_passthrough
def surface_tilt(self, value):
for array in self.arrays:
array.surface_tilt = value

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def surface_azimuth(self):
return tuple(array.surface_azimuth for array in self.arrays)

@surface_azimuth.setter
@_check_deprecated_passthrough
def surface_azimuth(self, value):
for array in self.arrays:
array.surface_azimuth = value

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def albedo(self):
return tuple(array.albedo for array in self.arrays)

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def racking_model(self):
return tuple(array.racking_model for array in self.arrays)

@racking_model.setter
@_check_deprecated_passthrough
def racking_model(self, value):
for array in self.arrays:
array.racking_model = value

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def modules_per_string(self):
return tuple(array.modules_per_string for array in self.arrays)

@property
@_unwrap_single_value
@_check_deprecated_passthrough
def strings_per_inverter(self):
return tuple(array.strings for array in self.arrays)

Expand Down
Loading