# coding=utf-8
"""Photovoltaic properties that can be applied to Shades."""
from __future__ import division
from honeybee._lockable import lockable
from honeybee.typing import valid_ep_string, valid_string, float_in_range
from ..properties.extension import PVPropertiesProperties
from ..reader import parse_idf_string
from ..writer import generate_idf_string
[docs]
@lockable
class PVProperties(object):
"""Simple Photovoltaic properties that run using PVWatts to estimate electricity.
Args:
identifier: Text string for PV properties identifier. Must be < 100 characters
and not contain any EnergyPlus special characters. This will be used to
identify the object across a model and in the exported IDF.
rated_efficiency: A number between 0 and 1 for the rated nameplate efficiency
of the photovoltaic solar cells under standard test conditions (STC).
Standard test conditions are 1,000 Watts per square meter solar
irradiance, 25 degrees C cell temperature, and ASTM G173-03 standard
spectrum. Nameplate efficiencies reported by manufacturers are typically
under STC. Standard poly- or mono-crystalline silicon modules tend to have
rated efficiencies in the range of 14-17%. Premium high efficiency
mono-crystalline silicon modules with anti-reflective coatings can have
efficiencies in the range of 18-20%. Thin film photovoltaic modules
typically have efficiencies of 11% or less. (Default: 0.15 for standard
silicon solar cells).
active_area_fraction: The fraction of the parent Shade geometry that is
covered in active solar cells. This fraction includes the difference
between the PV panel (aka. PV module) area and the active cells within
the panel as well as any losses for how the (typically rectangular) panels
can be arranged on the Shade geometry. When the parent Shade geometry
represents just the solar panels, this fraction is typically around 0.9
given that the metal framing elements of the panel reduce the overall
active area. (Default: 0.9, assuming parent Shade geometry represents
only the PV panel geometry).
module_type: Text to indicate the type of solar module. This is used to
determine the temperature coefficients used in the simulation of the
photovoltaic modules. Choose from the three options below. If None,
the module_type will be inferred from the rated_efficiency of these
PVProperties using the rated efficiencies listed below. (Default: None).
* Standard - 12% <= rated_efficiency < 18%
* Premium - rated_efficiency >= 18%
* ThinFilm - rated_efficiency < 12%
mounting_type: Text to indicate the type of mounting and/or tracking used
for the photovoltaic array. Note that the OneAxis options have an axis
of rotation that is determined by the azimuth of the parent Shade
geometry. Also note that, in the case of one or two axis tracking,
shadows on the (static) parent Shade geometry still reduce the
electrical output, enabling the simulation to account for large
context geometry casting shadows on the array. However, the effects
of smaller detailed shading may be improperly accounted for and self
shading of the dynamic panel geometry is only accounted for via the
tracking_ground_coverage_ratio property on this object. Choose from
the following. (Default: FixedOpenRack).
* FixedOpenRack - ground or roof mounting where the air flows freely
* FixedRoofMounted - mounting flush with the roof with limited air flow
* OneAxis - a fixed tilt and azimuth, which define an axis of rotation
* OneAxisBacktracking - same as OneAxis but with controls to reduce self-shade
* TwoAxis - a dynamic tilt and azimuth that track the sun
system_loss_fraction: A number between 0 and 1 for the fraction of the
electricity output lost due to factors other than EPW climate conditions,
panel efficiency/type, active area, mounting, and inverter conversion from
DC to AC. Factors that should be accounted for in this input include
soiling, snow, wiring losses, electrical connection losses, manufacturer
defects/tolerances/mismatch in cell characteristics, losses from power
grid availability, and losses due to age or light-induced degradation.
Losses from these factors tend to be between 10-20% but can vary widely
depending on the installation, maintenance and the grid to which the
panels are connected. The loss_fraction_from_components staticmethod
on this class can be used to estimate this value from the various
factors that it is intended to account for. (Default: 0.14).
tracking_ground_coverage_ratio: A number between 0 and 1 that only applies to
arrays with one-axis tracking mounting_type. The ground coverage ratio (GCR)
is the ratio of module surface area to the area of the ground beneath
the array, which is used to account for self shading of single-axis panels
as they move to track the sun. A GCR of 0.5 means that, when the modules
are horizontal, half of the surface below the array is occupied by
the array. An array with wider spacing between rows of modules has a
lower GCR than one with narrower spacing. A GCR of 1 would be for an
array with no space between modules, and a GCR of 0 for infinite spacing
between rows. Typical values range from 0.3 to 0.6. (Default: 0.4).
"""
__slots__ = (
'_identifier', '_display_name', '_rated_efficiency', '_active_area_fraction',
'_module_type', '_mounting_type', '_system_loss_fraction',
'_tracking_ground_coverage_ratio', '_locked', '_properties')
MODULE_TYPES = ('Standard', 'Premium', 'ThinFilm')
MOUNTING_TYPES = ('FixedOpenRack', 'FixedRoofMounted', 'OneAxis',
'OneAxisBacktracking', 'TwoAxis')
def __init__(self, identifier, rated_efficiency=0.15, active_area_fraction=0.9,
module_type=None, mounting_type='FixedOpenRack',
system_loss_fraction=0.14, tracking_ground_coverage_ratio=0.4):
"""Initialize PVProperties."""
# set the identifier and display name
self.identifier = identifier
self._display_name = None
# set the main features of the PV
self.rated_efficiency = rated_efficiency
self.active_area_fraction = active_area_fraction
self.module_type = module_type
self.mounting_type = mounting_type
self.system_loss_fraction = system_loss_fraction
self.tracking_ground_coverage_ratio = tracking_ground_coverage_ratio
self._properties = PVPropertiesProperties(self)
@property
def identifier(self):
"""Get or set the text string for PV properties system identifier."""
return self._identifier
@identifier.setter
def identifier(self, identifier):
self._identifier = valid_ep_string(
identifier, 'photovoltaic properties identifier')
@property
def display_name(self):
"""Get or set a string for the object name without any character restrictions.
If not set, this will be equal to the identifier.
"""
if self._display_name is None:
return self._identifier
return self._display_name
@display_name.setter
def display_name(self, value):
if value is not None:
try:
value = str(value)
except UnicodeEncodeError: # Python 2 machine lacking the character set
pass # keep it as unicode
self._display_name = value
@property
def rated_efficiency(self):
"""Get or set a number for the rated nameplate efficiency of the solar cells.
Standard poly- or mono-crystalline silicon modules tend to have rated
efficiencies in the range of 14-17%.
Premium high efficiency mono-crystalline silicon modules with anti-reflective
coatings can have efficiencies in the range of 18-20%.
Thin film photovoltaic modules typically have efficiencies of 11% or less.
"""
return self._rated_efficiency
@rated_efficiency.setter
def rated_efficiency(self, value):
self._rated_efficiency = float_in_range(
value, 0.0, 1.0, 'photovoltaic rated efficiency')
@property
def active_area_fraction(self):
"""Get or set a number for fraction of the parent covered in active solar cells.
This fraction includes the difference between the PV panel (aka. PV module) area
and the active cells within the panel as well as any losses for how
the (typically rectangular) panels can be arranged on the Shade geometry.
"""
return self._active_area_fraction
@active_area_fraction.setter
def active_area_fraction(self, value):
self._active_area_fraction = float_in_range(
value, 0.0, 1.0, 'photovoltaic active area fraction')
@property
def module_type(self):
"""Get or set text to indicate the type of photovoltaic module.
Choose from the following options:
* Standard
* Premium
* ThinFilm
"""
if self._module_type is None:
if self.rated_efficiency < 0.12:
return 'ThinFilm'
elif self.rated_efficiency < 0.18:
return 'Standard'
return 'Premium'
return self._module_type
@module_type.setter
def module_type(self, value):
if value is not None:
clean_input = valid_string(value).lower()
for key in self.MODULE_TYPES:
if key.lower() == clean_input:
value = key
break
else:
raise ValueError(
'PVProperties.module_type {} is not recognized.\nChoose from the '
'following:\n{}'.format(value, self.MODULE_TYPES))
self._module_type = value
@property
def mounting_type(self):
"""Get or set text to indicate the way the photovoltaic arrays are mounted.
Choose from the following options:
* FixedOpenRack
* FixedRoofMounted
* OneAxis
* OneAxisBacktracking
* TwoAxis
"""
return self._mounting_type
@mounting_type.setter
def mounting_type(self, value):
clean_input = valid_string(value).lower()
for key in self.MOUNTING_TYPES:
if key.lower() == clean_input:
value = key
break
else:
raise ValueError(
'PVProperties.mounting_type {} is not recognized.\nChoose from the '
'following:\n{}'.format(value, self.MOUNTING_TYPES))
self._mounting_type = value
@property
def system_loss_fraction(self):
"""Get or set a number for the fraction of the output lost due to other factors.
Factors that should be accounted for in this input include soiling, snow,
wiring losses, electrical connection losses, manufacturer defects/tolerances/
mismatch in cell characteristics, losses from power grid availability,
and losses due to age or light-induced degradation.
"""
return self._system_loss_fraction
@system_loss_fraction.setter
def system_loss_fraction(self, value):
self._system_loss_fraction = float_in_range(
value, 0.0, 1.0, 'photovoltaic system loss fraction')
@property
def tracking_ground_coverage_ratio(self):
"""Get or set a number between 0 and 1 for the ground coverage ratio.
This value only applies to systems using single-axis tracking and is
used to account for self shading of single-axis panels as they move
to track the sun.
"""
return self._tracking_ground_coverage_ratio
@tracking_ground_coverage_ratio.setter
def tracking_ground_coverage_ratio(self, value):
self._tracking_ground_coverage_ratio = float_in_range(
value, 0.0, 1.0, 'photovoltaic tracking ground coverage ratio')
@property
def properties(self):
"""Get the properties of the PVProperties."""
return self._properties
[docs]
@classmethod
def from_idf(cls, idf_string, shade, active_area_fraction=0.9):
"""Create a PVProperties object from a Generator:PVWatts IDF text string.
Note that the Generator:PVWatts idf_string must use the 'surface' array
geometry type in order to be successfully imported.
Args:
idf_string: A text string fully describing an EnergyPlus Generator:PVWatts
definition.
shade: The Honeybee Shade object to which the PV properties are assigned.
Note that the geometry of this Shade must be in meters for the
object to be loaded correctly.
active_area_fraction: The original active area fraction used to construct
the PVProperties object. This is needed to correctly derive
the PV efficiency from the system capacity.
Returns:
A PVProperties object from the IDF string.
"""
# check the inputs
ep_strs = parse_idf_string(idf_string, 'Generator:PVWatts,')
# check the inputs
assert len(ep_strs) >= 9, 'PVProperties must use Surface array geometry ' \
'type to be loaded from IDF to honeybee.'
assert ep_strs[9].upper() == shade.identifier.upper(), 'Generator:PVWatts ' \
'surface "{}" is different from the assigned Shade "{}".'.format(
ep_strs[9].upper(), shade.identifier.upper())
# extract the properties from the string
watts_per_area = float(ep_strs[2]) / shade.area
eff = round((watts_per_area / active_area_fraction) / 1000, 3)
loss = 0.14
gcr = 0.4
try:
loss = ep_strs[5] if ep_strs[5] != '' else 0.14
gcr = ep_strs[10] if ep_strs[10] != '' else 0.4
except IndexError:
pass # shorter definition lacking ground coverage ratio
# return the PV properties object
obj_id = ep_strs[0].split('..')[0]
return cls(obj_id, eff, active_area_fraction, ep_strs[3], ep_strs[4], loss, gcr)
[docs]
@classmethod
def from_dict(cls, data):
"""Create a PVProperties object from a dictionary.
Args:
data: A PVProperties dictionary in following the format below.
.. code-block:: python
{
"type": "PVProperties",
"identifier": "Ablytek 270 W Monocrystalline", # identifier for the PV
"display_name": "Ablytek Module", # name for the PV
"rated_efficiency": 0.18, # Nameplate rated efficiency
"active_area_fraction": 0.92, # Active area fraction
"module_type": "Standard", # Type of solar module
"mounting_type": "FixedOpenRack", # Type of mounting and tracking
"system_loss_fraction": 0.16 # Fraction lost to outside factors
}
"""
assert data['type'] == 'PVProperties', \
'Expected PVProperties dictionary. Got {}.'.format(data['type'])
# extract the key features and properties while setting defaults
eff = data['rated_efficiency'] if 'rated_efficiency' in data and \
data['rated_efficiency'] is not None else 0.15
act = data['active_area_fraction'] if 'active_area_fraction' in data and \
data['active_area_fraction'] is not None else 0.9
mod_type = data['module_type'] if 'module_type' in data and \
data['module_type'] is not None else 'Standard'
mount = data['mounting_type'] if 'mounting_type' in data and \
data['mounting_type'] is not None else 'FixedOpenRack'
loss = data['system_loss_fraction'] if 'system_loss_fraction' in data and \
data['system_loss_fraction'] is not None else 0.14
gcr = data['tracking_ground_coverage_ratio'] \
if 'tracking_ground_coverage_ratio' in data and \
data['tracking_ground_coverage_ratio'] is not None else 0.4
new_obj = cls(data['identifier'], eff, act, mod_type, mount, loss, gcr)
if 'display_name' in data and data['display_name'] is not None:
new_obj.display_name = data['display_name']
if 'properties' in data and data['properties'] is not None:
new_obj.properties._load_extension_attr_from_dict(data['properties'])
return new_obj
[docs]
def to_idf(self, shade):
"""IDF string representation of PVProperties object.
Args:
shade: A Honeybee Shade for which the specific IDF string will
be generated. Note that the geometry of this shade must be in
meters in order for the returned IDF string to have correct values.
.. code-block:: shell
Generator:PVWatts,
Photovoltaic Array 1..Shade_d5d1b861, !- PV generator name
, !- PVWatts version
4050, !- DC system capacity {W}
Standard, !- module type
FixedOpenRack, !- array type
0.14, !- system losses
Surface, !- array geometry type
, !- tilt angle {deg}
, !- azimuth angle {deg}
Shade_d5d1b861, !- surface name
0.4; !- ground coverage ratio
"""
# compte the system capacity and other factors from the shade
idf_id = '{}..{}'.format(self.identifier, shade.identifier)
rated_watts = self.rated_efficiency * 1000 # 1000W/m2 of solar irradiance
sys_cap = int(shade.area * self.active_area_fraction * rated_watts)
# write the IDF string
values = (idf_id, '', sys_cap, self.module_type, self.mounting_type,
self.system_loss_fraction, 'Surface', '', '', shade.identifier,
self.tracking_ground_coverage_ratio)
comments = (
'PV generator name', 'PVWatts version', 'DC system capacity {W}',
'module type', 'array type', 'system losses', 'array geometry type',
'tilt angle {deg}', 'azimuth angle {deg}', 'surface name',
'ground coverage ratio')
return generate_idf_string('Generator:PVWatts', values, comments)
[docs]
def to_dict(self):
"""PVProperties dictionary representation.
Args:
abridged: Boolean to note whether the full dictionary describing the
object should be returned (False) or just an abridged version (True),
which only specifies the identifiers of schedules. (Default: False).
"""
base = {'type': 'PVProperties'}
base['identifier'] = self.identifier
base['rated_efficiency'] = self.rated_efficiency
base['active_area_fraction'] = self.active_area_fraction
base['module_type'] = self.module_type
base['mounting_type'] = self.mounting_type
base['system_loss_fraction'] = self.system_loss_fraction
if self.mounting_type in ('OneAxis', 'OneAxisBacktracking') or \
self.tracking_ground_coverage_ratio != 0.4:
base['tracking_ground_coverage_ratio'] = self.tracking_ground_coverage_ratio
if self._display_name is not None:
base['display_name'] = self.display_name
prop_dict = self.properties.to_dict()
if prop_dict is not None:
base['properties'] = prop_dict
return base
[docs]
def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
[docs]
def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
[docs]
@staticmethod
def loss_fraction_from_components(
age=0.045, light_induced_degradation=0.015, soiling=0.02, snow=0.0,
manufacturer_nameplate_tolerance=0.01, cell_characteristic_mismatch=0.02,
wiring=0.02, electrical_connection=0.005, grid_availability=0.015):
"""Compute an estimate for system_loss_fraction from individual factors.
This method is intended to help account for all of the factors outside of
those modeled by EnergyPlus (and PVWatts), which can influence the annual
energy harvested by photovoltaic arrays. It also gives a rough understanding
of where the default value of 0.14 could originate from given the default
values used for the various factors on this method.
Note that this loss term does not include the effects of the climate's
temperature/radiation, site shading, panel efficiency/type, active area,
mounting, or inverter conversion from DC to AC since all of these effects
are modeled explicitly by EnergyPlus.
Args:
age: A number between 0 and 1 for the fraction of output lost due to the
aging/weathering of the photovoltaic panels over time. This term is
intended to account for gradual degradation through exposure to
the elements including corrosion, thermal expansion/contraction,
erosion. Typical conservative estimates assume a degradation
of 0.01 (or 1%) per year such that, on the 20th year, the panels will
be performing at 19% less than their original output. (Default: 0.045
for the average aging expected in the first 10 years of operation).
light_induced_degradation: A number between 0 and 1 for the fraction
of output lost due to light-induced degradation of the photovoltaic
cells, which is common during the first few months of operation and
results in the cells having a different efficiency than the
nameplate rating. (Default: 0.02).
soiling: A number between 0 and 1 for the fraction of output lost due to
dust, dirt, leaves, wildlife droppings, and other foreign matter
on the surface of the PV module that prevent solar radiation from
reaching the cells. Soiling is highly dependent on climate,
installation conditions, and the frequency with which the panels
are cleaned. The greatest soiling losses typically occur in
high-traffic, high-pollution areas with infrequent rain and
infrequent cleaning. (Default: 0.02).
snow: A number between 0 and 1 for the fraction of output lost due to
snow covering the panels. This is common in cases where panels have
a low slope and are not immediately cleared of snow. (Default: 0.0
assuming installation in a climate without snow).
manufacturer_nameplate_tolerance: A number between 0 and 1 for the
fraction of of output lost due to tolerance that the manufacturer
follows in the performance of its product from the nameplate
rating. (Default: 0.01).
cell_characteristic_mismatch: A number between 0 and 1 for the fraction of
output lost due to manufacturing imperfections between modules in
the array, which cause the modules to have slightly different
current-voltage characteristics and result in a reduction of
performance from the nameplate rating. (Default: 0.02).
wiring: A number between 0 and 1 for the fraction of output lost due to
resistive losses in the wires connecting the various parts of the
photovoltaic system. Setups that require longer wires and bigger
distances between equipment will have higher losses for this
term. (Default: 0.02).
electrical_connection: A number between 0 and 1 for the fraction of output
lost due to resistive losses in the electrical connectors across the
photovoltaic system. (Default: 0.005).
grid_availability: A number between 0 and 1 for the fraction of output
lost due to maintenance shutdowns, grid outages, inability for the
grid to accept input, and other operational factors. (Default: 0.015).
"""
all_factors = (
age, light_induced_degradation, soiling, snow,
manufacturer_nameplate_tolerance, cell_characteristic_mismatch,
wiring, electrical_connection, grid_availability
)
downrate_factor = 1
for factor in all_factors:
downrate_factor = downrate_factor * (1 - factor)
return 1 - downrate_factor
def __key(self):
"""A tuple based on the object properties, useful for hashing."""
return (self.identifier, self.rated_efficiency, self.active_area_fraction,
self.module_type, self.mounting_type, self.system_loss_fraction,
self.tracking_ground_coverage_ratio)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, PVProperties) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
def __copy__(self):
new_obj = PVProperties(
self.identifier, self._rated_efficiency, self._active_area_fraction,
self._module_type, self._mounting_type, self._system_loss_fraction,
self._tracking_ground_coverage_ratio)
new_obj._display_name = self._display_name
new_obj._properties._duplicate_extension_attr(self._properties)
return new_obj
def __repr__(self):
return 'PVProperties: {} [efficiency {}%]'.format(
self.display_name, round(self.rated_efficiency, 1))