Skip to content
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

add Mount classes #1176

Merged
merged 54 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cb3c207
add Mount classes, incorporate into Array and PVSystem
kandersolar Feb 23, 2021
8a6d0e6
update pvsystem tests
kandersolar Feb 23, 2021
6d4240e
delete mounts module
kandersolar Feb 23, 2021
fc4064b
fix modelchain tests
kandersolar Feb 24, 2021
e8f1404
some modifications to SingleAxisTracker
kandersolar Feb 24, 2021
02a2b27
changes from review
kandersolar Feb 27, 2021
0be45ae
stickler
kandersolar Feb 27, 2021
9406ccd
Merge branch 'master' into mount_classes
kandersolar Mar 2, 2021
e1bdc67
use dataclasses for mounts
kandersolar Mar 7, 2021
61650e9
update tests
kandersolar Mar 7, 2021
fc47003
update docs
kandersolar Mar 7, 2021
887fd3a
whatsnew
kandersolar Mar 7, 2021
ecc4737
test mount classes
kandersolar Mar 7, 2021
cbb41e1
stickler
kandersolar Mar 7, 2021
9e663d2
more tests
kandersolar Mar 7, 2021
105edb7
another test
kandersolar Mar 7, 2021
6d32c41
fix typo
kandersolar Mar 7, 2021
4a6347e
clean up AbstractMount
kandersolar Mar 13, 2021
1e063ba
remove unnecessary use of dataclasses.field
kandersolar Mar 13, 2021
3dcf106
calculate -> get
kandersolar Mar 13, 2021
d4045a3
Merge remote-tracking branch 'upstream/master' into mount_classes
kandersolar Mar 21, 2021
b0b551f
Update pvlib/pvsystem.py
kandersolar Mar 21, 2021
515a359
stickler
kandersolar Mar 21, 2021
a874b11
test fixes
kandersolar Mar 21, 2021
17195ae
add optional surface_tilt parameter to PVSystem.fuentes_celltemp
kandersolar Mar 21, 2021
93716bb
move racking_model and module_height to the Mounts
kandersolar Mar 21, 2021
efe6b3b
fix some tests
kandersolar Mar 21, 2021
74a6be4
remove unnecessary fixture
kandersolar Mar 28, 2021
9da9eb4
Revert "remove unnecessary fixture"
kandersolar Mar 30, 2021
2320ed7
Merge remote-tracking branch 'upstream/master' into mount_classes
kandersolar May 14, 2021
6370143
update merged test
kandersolar May 14, 2021
6181e96
fix fuentes issue, sort of
kandersolar May 14, 2021
47b6884
pep8
kandersolar May 14, 2021
dbc1193
pep8
kandersolar May 14, 2021
e0eeef4
remove PVSystem.fuentes_celltemp surface_tilt parameter
kandersolar May 14, 2021
cf3fe20
placeholder fuentes surface_tilt logic
kandersolar May 14, 2021
7c18126
Merge branch 'master' into mount_classes
kandersolar May 21, 2021
86292c5
test updates
kandersolar May 21, 2021
cc5f45a
Merge remote-tracking branch 'upstream/master' into mount_classes
kandersolar Jun 17, 2021
739359e
remove unused imports
kandersolar Jun 17, 2021
59547f5
remove fuentes override complexity
kandersolar Jun 23, 2021
bed27c9
stickler
kandersolar Jun 23, 2021
1103f99
update RST pages
kandersolar Jun 23, 2021
e6504cd
Merge remote-tracking branch 'upstream/master' into mount_classes
kandersolar Jun 23, 2021
73aba9a
revert unnecessary docs change
kandersolar Jun 23, 2021
f3ff722
add link to pvsystem and modelchain pages in api listing
kandersolar Jun 29, 2021
9c0c3ff
other changes from review
kandersolar Jun 29, 2021
35da280
get module_height from mount instead of temperature_model_parameters
kandersolar Jun 29, 2021
1a43ea8
coverage for fuentes module_height parameter
kandersolar Jun 29, 2021
c1738e9
deprecate SingleAxisTracker
kandersolar Jul 21, 2021
781b133
suppress SAT deprecation warnings in tests
kandersolar Jul 21, 2021
22341ec
Apply suggestions from code review
kandersolar Jul 22, 2021
6df8408
Tracking -> Tracker
kandersolar Jul 22, 2021
506edbc
Merge branch 'mount_classes' of https://github.com/kanderso-nrel/pvli…
kandersolar Jul 22, 2021
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
4 changes: 3 additions & 1 deletion pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,9 @@ def orientation_strategy(self, strategy):
strategy = None

if strategy is not None:
self.system.surface_tilt, self.system.surface_azimuth = \
# TODO: this is probably not what we want to do here
kandersolar marked this conversation as resolved.
Show resolved Hide resolved
(self.system.arrays[0].mount.surface_tilt,
self.system.arrays[0].mount.surface_azimuth) = \
get_orientation(strategy, latitude=self.location.latitude)

self._orientation_strategy = strategy
Expand Down
224 changes: 192 additions & 32 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from urllib.request import urlopen
import numpy as np
import pandas as pd
from abc import ABC, abstractmethod

from pvlib._deprecation import deprecated
import warnings
from pvlib._deprecation import deprecated, pvlibDeprecationWarning

from pvlib import (atmosphere, iam, inverter, irradiance,
singlediode as _singlediode, temperature)
Expand Down Expand Up @@ -191,8 +193,7 @@ def __init__(self,

if arrays is None:
self.arrays = (Array(
surface_tilt,
surface_azimuth,
FixedMount(surface_tilt, surface_azimuth),
albedo,
surface_type,
module,
Expand Down Expand Up @@ -249,7 +250,6 @@ def _validate_per_array(self, values, system_wide=False):

@_unwrap_single_value
def _infer_cell_type(self):

"""
Examines module_parameters and maps the Technology key for the CEC
database and the Material key for the Sandia database to a common
Expand Down Expand Up @@ -765,7 +765,10 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed):
wind_speed = self._validate_per_array(wind_speed, system_wide=True)

def _build_kwargs_fuentes(array):
kwargs = {'surface_tilt': array.surface_tilt}
# TODO: I think there should be an interface function so that
# directly accessing surface_tilt isn't necessary. Doesn't this
# break for SAT?
kwargs = {'surface_tilt': array.mount.surface_tilt}
temp_model_kwargs = _build_kwargs([
'noct_installed', 'module_height', 'wind_height', 'emissivity',
'absorption', 'surface_tilt', 'module_width', 'module_length'],
Expand Down Expand Up @@ -1044,24 +1047,60 @@ def temperature_model_parameters(self, value):
array.temperature_model_parameters = value

@property
@_unwrap_single_value
def surface_tilt(self):
return tuple(array.surface_tilt for array in self.arrays)
if len(self.arrays) == 1:
msg = (
'PVSystem.surface_tilt attribute is deprecated. '
'Use PVSystem.arrays[0].mount.surface_tilt.'
)
warnings.warn(msg, pvlibDeprecationWarning)
else:
raise AttributeError(
'PVSystem.surface_tilt not supported for multi-array systems. '
'Use PVSystem.arrays[i].mount.surface_tilt.')
return self.arrays[0].mount.surface_tilt

@surface_tilt.setter
def surface_tilt(self, value):
for array in self.arrays:
array.surface_tilt = value
if len(self.arrays) == 1:
msg = (
'PVSystem.surface_tilt attribute is deprecated. '
'Use PVSystem.arrays[0].mount.surface_tilt.'
)
warnings.warn(msg, pvlibDeprecationWarning)
else:
raise AttributeError(
'PVSystem.surface_tilt not supported for multi-array systems. '
'Use PVSystem.arrays[i].mount.surface_tilt.')
self.arrays[0].mount.surface_tilt = value

@property
@_unwrap_single_value
def surface_azimuth(self):
kandersolar marked this conversation as resolved.
Show resolved Hide resolved
return tuple(array.surface_azimuth for array in self.arrays)
if len(self.arrays) == 1:
msg = (
'PVSystem.surface_azimuth attribute is deprecated. '
'Use PVSystem.arrays[0].mount.surface_azimuth.'
)
warnings.warn(msg, pvlibDeprecationWarning)
else:
raise AttributeError(
'PVSystem.surface_azimuth not supported for multi-array '
'systems. Use PVSystem.arrays[i].mount.surface_azimuth.')
return self.arrays[0].mount.surface_azimuth

@surface_azimuth.setter
def surface_azimuth(self, value):
for array in self.arrays:
array.surface_azimuth = value
if len(self.arrays) == 1:
msg = (
'PVSystem.surface_azimuth attribute is deprecated. '
'Use PVSystem.arrays[0].mount.surface_azimuth.'
)
warnings.warn(msg, pvlibDeprecationWarning)
else:
raise AttributeError(
'PVSystem.surface_azimuth not supported for multi-array '
'systems. Use PVSystem.arrays[i].mount.surface_azimuth.')
self.arrays[0].mount.surface_azimuth = value

@property
@_unwrap_single_value
Expand Down Expand Up @@ -1098,20 +1137,15 @@ class Array:
"""
An Array is a set of of modules at the same orientation.

Specifically, an array is defined by tilt, azimuth, the
Specifically, an array is defined by its mount, the
module parameters, the number of parallel strings of modules
and the number of modules on each string.

Parameters
----------
surface_tilt: float or array-like, default 0
Surface tilt angles in decimal degrees.
The tilt angle is defined as degrees from horizontal
(e.g. surface facing up = 0, surface facing horizon = 90)

surface_azimuth: float or array-like, default 180
Azimuth angle of the module surface.
North=0, East=90, South=180, West=270.
mount: FixedMount, SingleAxisTrackerMount, or other
Mounting strategy for the array, used to determine module orientation.
kandersolar marked this conversation as resolved.
Show resolved Hide resolved
If not provided, a FixedMount with zero tilt is used.

albedo : None or float, default None
The ground albedo. If ``None``, will attempt to use
Expand Down Expand Up @@ -1151,17 +1185,14 @@ class Array:

"""

def __init__(self,
surface_tilt=0, surface_azimuth=180,
def __init__(self, mount,
albedo=None, surface_type=None,
module=None, module_type=None,
module_parameters=None,
temperature_model_parameters=None,
modules_per_string=1, strings=1,
racking_model=None, name=None):
self.surface_tilt = surface_tilt
self.surface_azimuth = surface_azimuth

self.mount = mount
self.surface_type = surface_type
if albedo is None:
self.albedo = irradiance.SURFACE_ALBEDOS.get(surface_type, 0.25)
Expand Down Expand Up @@ -1189,10 +1220,11 @@ def __init__(self,
self.name = name

def __repr__(self):
attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module',
attrs = ['name', 'mount', 'module',
'albedo', 'racking_model', 'module_type',
'temperature_model_parameters',
'strings', 'modules_per_string']

return 'Array:\n ' + '\n '.join(
f'{attr}: {getattr(self, attr)}' for attr in attrs
)
Expand All @@ -1213,7 +1245,6 @@ def _infer_temperature_model_params(self):
return {}

def _infer_cell_type(self):

"""
Examines module_parameters and maps the Technology key for the CEC
database and the Material key for the Sandia database to a common
Expand Down Expand Up @@ -1271,7 +1302,10 @@ def get_aoi(self, solar_zenith, solar_azimuth):
aoi : Series
Then angle of incidence.
"""
return irradiance.aoi(self.surface_tilt, self.surface_azimuth,
orientation = self.mount.calculate_orientation(solar_zenith,
solar_azimuth)
return irradiance.aoi(orientation['surface_tilt'],
orientation['surface_azimuth'],
solar_zenith, solar_azimuth)

def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
Expand Down Expand Up @@ -1320,8 +1354,10 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
if airmass is None:
airmass = atmosphere.get_relative_airmass(solar_zenith)

return irradiance.get_total_irradiance(self.surface_tilt,
self.surface_azimuth,
orientation = self.mount.calculate_orientation(solar_zenith,
solar_azimuth)
return irradiance.get_total_irradiance(orientation['surface_tilt'],
orientation['surface_azimuth'],
solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra=dni_extra,
Expand Down Expand Up @@ -1373,6 +1409,130 @@ def get_iam(self, aoi, iam_model='physical'):
raise ValueError(model + ' is not a valid IAM model')


class AbstractMount(ABC):
def __repr__(self):
classname = self.__class__.__name__
return f'{classname}:\n ' + '\n '.join(
f'{attr}: {getattr(self, attr)}' for attr in self._repr_attrs
)

@abstractmethod
def calculate_orientation(self, solar_zenith, solar_azimuth):
Copy link
Member

@cwhanse cwhanse Mar 11, 2021

Choose a reason for hiding this comment

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

maybe get_module_orientation? Or get_module_position? get_orientation?

Copy link
Member

Choose a reason for hiding this comment

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

pvlib methods prefixed with 'get' typically have 'how' or 'method' kwargs. Python discourages the prefix in most cases.

Copy link
Member

@cwhanse cwhanse Mar 11, 2021

Choose a reason for hiding this comment

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

What is this method intended to return? tilt and azimuth I assume? [nvm I read the code again, short memory I guess]. calculate seems like a long way to say get to me.

Copy link
Member

Choose a reason for hiding this comment

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

After thinking a little more about this... I had an unfair allergic reaction to get in this case. It's more correct to say that getter methods are not pythonic. This isn't a getter method, so we're ok. We use get for a lot of things that do calculations, so there's still consistency. I'm now neutral on the prefix.

"""
Determine module orientation.

Parameters
----------
solar_zenith : numeric
Solar apparent zenith angle [degrees]
solar_azimuth : numeric
Solar azimuth angle [degrees]

Returns
-------
orientation : dict-like
A dict-like object with keys `'surface_tilt', 'surface_azimuth'`
kandersolar marked this conversation as resolved.
Show resolved Hide resolved
"""
pass
kandersolar marked this conversation as resolved.
Show resolved Hide resolved


class FixedMount(AbstractMount):
"""
Racking at fixed (static) orientation.

Parameters
----------
surface_tilt : float, default 0
Surface tilt angle. The tilt angle is defined as angle from horizontal
(e.g. surface facing up = 0, surface facing horizon = 90) [degrees]

surface_azimuth : float, default 180
Azimuth angle of the module surface. North=0, East=90, South=180,
West=270. [degrees]
"""
kandersolar marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, surface_tilt=0, surface_azimuth=180):
self.surface_tilt = surface_tilt
self.surface_azimuth = surface_azimuth
self._repr_attrs = ['surface_tilt', 'surface_azimuth']

def calculate_orientation(self, solar_zenith, solar_azimuth):
# note -- docstring is automatically inherited from AbstractMount
return {
'surface_tilt': self.surface_tilt,
'surface_azimuth': self.surface_azimuth,
}


class SingleAxisTrackerMount(AbstractMount):
"""
Single-axis tracker racking for dynamic solar tracking.

Parameters
----------
axis_tilt : float, default 0
The tilt of the axis of rotation (i.e, the y-axis defined by
axis_azimuth) with respect to horizontal. [degrees]

axis_azimuth : float, default 180
A value denoting the compass direction along which the axis of
rotation lies, measured east of north. [degrees]

max_angle : float, default 90
A value denoting the maximum rotation angle
of the one-axis tracker from its horizontal position (horizontal
if axis_tilt = 0). A max_angle of 90 degrees allows the tracker
to rotate to a vertical position to point the panel towards a
horizon. max_angle of 180 degrees allows for full rotation. [degrees]

backtrack : bool, default True
Controls whether the tracker has the capability to "backtrack"
to avoid row-to-row shading. False denotes no backtrack
capability. True denotes backtrack capability.

gcr : float, default 2.0/7.0
A value denoting the ground coverage ratio of a tracker system
which utilizes backtracking; i.e. the ratio between the PV array
surface area to total ground area. A tracker system with modules
2 meters wide, centered on the tracking axis, with 6 meters
between the tracking axes has a gcr of 2/6=0.333. If gcr is not
provided, a gcr of 2/7 is default. gcr must be <=1. [unitless]

cross_axis_tilt : float, default 0.0
The angle, relative to horizontal, of the line formed by the
intersection between the slope containing the tracker axes and a plane
perpendicular to the tracker axes. Cross-axis tilt should be specified
using a right-handed convention. For example, trackers with axis
azimuth of 180 degrees (heading south) will have a negative cross-axis
tilt if the tracker axes plane slopes down to the east and positive
cross-axis tilt if the tracker axes plane slopes up to the east. Use
:func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
`cross_axis_tilt`. [degrees]
"""

def __init__(self, axis_tilt=0, axis_azimuth=180, max_angle=90,
backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0):
self.axis_tilt = axis_tilt
self.axis_azimuth = axis_azimuth
self.max_angle = max_angle
self.backtrack = backtrack
self.gcr = gcr
self.cross_axis_tilt = cross_axis_tilt
self._repr_attrs = ['axis_tilt', 'axis_azimuth', 'max_angle',
'backtrack', 'gcr', 'cross_axis_tilt']

def calculate_orientation(self, solar_zenith, solar_azimuth):
# note -- docstring is automatically inherited from AbstractMount
from pvlib import tracking # avoid circular import issue
tracking_data = tracking.singleaxis(
solar_zenith, solar_azimuth,
self.axis_tilt, self.axis_azimuth,
self.max_angle, self.backtrack,
self.gcr, self.cross_axis_tilt
)
return tracking_data
Copy link
Member

Choose a reason for hiding this comment

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

still thinking about interface promises... get_orientation promises to return something dict-like that includes keys surface_tilt and surface_azimuth.



def calcparams_desoto(effective_irradiance, temp_cell,
alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
EgRef=1.121, dEgdT=-0.0002677,
Expand Down
Loading