"""
The ``pvsystem`` module contains functions for modeling the output and
performance of PV modules and inverters.
"""
from collections import OrderedDict
import functools
import io
import itertools
from pathlib import Path
import inspect
from urllib.request import urlopen
import numpy as np
from scipy import constants
import pandas as pd
from dataclasses import dataclass
from abc import ABC, abstractmethod
from typing import Optional, Union
from pvlib._deprecation import deprecated, warn_deprecated
import pvlib # used to avoid albedo name collision in the Array class
from pvlib import (atmosphere, iam, inverter, irradiance,
singlediode as _singlediode, spectrum, temperature)
from pvlib.tools import _build_kwargs, _build_args
import pvlib.tools as tools
# a dict of required parameter names for each DC power model
_DC_MODEL_PARAMS = {
'sapm': {
'A0', 'A1', 'A2', 'A3', 'A4', 'B0', 'B1', 'B2', 'B3',
'B4', 'B5', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6',
'C7', 'Isco', 'Impo', 'Voco', 'Vmpo', 'Aisc', 'Aimp', 'Bvoco',
'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series',
'IXO', 'IXXO', 'FD'},
'desoto': {
'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',
'R_sh_ref', 'R_s'},
'cec': {
'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',
'R_sh_ref', 'R_s', 'Adjust'},
'pvsyst': {
'gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref',
'R_sh_ref', 'R_sh_0', 'R_s', 'alpha_sc', 'EgRef',
'cells_in_series'},
'singlediode': {
'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',
'R_sh_ref', 'R_s'},
'pvwatts': {'pdc0', 'gamma_pdc'}
}
def _unwrap_single_value(func):
"""Decorator for functions that return iterables.
If the length of the iterable returned by `func` is 1, then
the single member of the iterable is returned. If the length is
greater than 1, then entire iterable is returned.
Adds 'unwrap' as a keyword argument that can be set to False
to force the return value to be a tuple, regardless of its length.
"""
@functools.wraps(func)
def f(*args, **kwargs):
unwrap = kwargs.pop('unwrap', True)
x = func(*args, **kwargs)
if unwrap and len(x) == 1:
return x[0]
return x
return f
# 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.
[docs]
class PVSystem:
"""
The PVSystem class defines a standard set of PV system attributes
and modeling functions. This class describes the collection and
interactions of PV system components rather than an installed system
on the ground. It is typically used in combination with
:py:class:`~pvlib.location.Location` and
:py:class:`~pvlib.modelchain.ModelChain`
objects.
The class supports basic system topologies consisting of:
* `N` total modules arranged in series
(`modules_per_string=N`, `strings_per_inverter=1`).
* `M` total modules arranged in parallel
(`modules_per_string=1`, `strings_per_inverter=M`).
* `NxM` total modules arranged in `M` strings of `N` modules each
(`modules_per_string=N`, `strings_per_inverter=M`).
The class is complementary to the module-level functions.
The attributes should generally be things that don't change about
the system, such the type of module and the inverter. The instance
methods accept arguments for things that do change, such as
irradiance and temperature.
Parameters
----------
arrays : Array or iterable of Array, optional
An Array or list of arrays that are part of the system. If not
specified a single array is created from the other parameters (e.g.
`surface_tilt`, `surface_azimuth`). If specified as a list, the list
must contain at least one Array;
if length of arrays is 0 a ValueError is raised. If `arrays` is
specified the following PVSystem parameters are ignored:
- `surface_tilt`
- `surface_azimuth`
- `albedo`
- `surface_type`
- `module`
- `module_type`
- `module_parameters`
- `temperature_model_parameters`
- `modules_per_string`
- `strings_per_inverter`
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.
albedo : float, optional
Ground surface albedo. If not supplied, then ``surface_type`` is used
to look up a value in :py:const:`pvlib.albedo.SURFACE_ALBEDOS`.
If ``surface_type`` is also not supplied then a ground surface albedo
of 0.25 is used.
surface_type : string, optional
The ground surface type. See :py:const:`pvlib.albedo.SURFACE_ALBEDOS`
for valid values.
module : string, optional
The model name of the modules.
May be used to look up the module_parameters dictionary
via some other method.
module_type : string, default 'glass_polymer'
Describes the module's construction. Valid strings are 'glass_polymer'
and 'glass_glass'. Used for cell and module temperature calculations.
module_parameters : dict or Series, optional
Module parameters as defined by the SAPM, CEC, or other.
temperature_model_parameters : dict or Series, optional
Temperature model parameters as required by one of the models in
pvlib.temperature (excluding poa_global, temp_air and wind_speed).
modules_per_string: int or float, default 1
See system topology discussion above.
strings_per_inverter: int or float, default 1
See system topology discussion above.
inverter : string, optional
The model name of the inverters.
May be used to look up the inverter_parameters dictionary
via some other method.
inverter_parameters : dict or Series, optional
Inverter parameters as defined by the SAPM, CEC, or other.
racking_model : string, default 'open_rack'
Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
Used to identify a parameter set for the SAPM cell temperature model.
losses_parameters : dict or Series, optional
Losses parameters as defined by PVWatts or other.
name : string, optional
**kwargs
Arbitrary keyword arguments.
Included for compatibility, but not used.
Raises
------
ValueError
If `arrays` is not None and has length 0.
See also
--------
pvlib.location.Location
"""
[docs]
def __init__(self,
arrays=None,
surface_tilt=0, surface_azimuth=180,
albedo=None, surface_type=None,
module=None, module_type=None,
module_parameters=None,
temperature_model_parameters=None,
modules_per_string=1, strings_per_inverter=1,
inverter=None, inverter_parameters=None,
racking_model=None, losses_parameters=None, name=None):
if arrays is None:
if losses_parameters is None:
array_losses_parameters = {}
else:
array_losses_parameters = _build_kwargs(['dc_ohmic_percent'],
losses_parameters)
self.arrays = (Array(
FixedMount(surface_tilt, surface_azimuth, racking_model),
albedo,
surface_type,
module,
module_type,
module_parameters,
temperature_model_parameters,
modules_per_string,
strings_per_inverter,
array_losses_parameters,
),)
elif isinstance(arrays, Array):
self.arrays = (arrays,)
elif len(arrays) == 0:
raise ValueError("PVSystem must have at least one Array. "
"If you want to create a PVSystem instance "
"with a single Array pass `arrays=None` and pass "
"values directly to PVSystem attributes, e.g., "
"`surface_tilt=30`")
else:
self.arrays = tuple(arrays)
self.inverter = inverter
if inverter_parameters is None:
self.inverter_parameters = {}
else:
self.inverter_parameters = inverter_parameters
if losses_parameters is None:
self.losses_parameters = {}
else:
self.losses_parameters = losses_parameters
self.name = name
def __repr__(self):
repr = f'PVSystem:\n name: {self.name}\n '
for array in self.arrays:
repr += '\n '.join(array.__repr__().split('\n'))
repr += '\n '
repr += f'inverter: {self.inverter}'
return repr
def _validate_per_array(self, values, system_wide=False):
"""Check that `values` is a tuple of the same length as
`self.arrays`.
If `values` is not a tuple it is packed in to a length-1 tuple before
the check. If the lengths are not the same a ValueError is raised,
otherwise the tuple `values` is returned.
When `system_wide` is True and `values` is not a tuple, `values`
is replicated to a tuple of the same length as `self.arrays` and that
tuple is returned.
"""
if system_wide and not isinstance(values, tuple):
return (values,) * self.num_arrays
if not isinstance(values, tuple):
values = (values,)
if len(values) != len(self.arrays):
raise ValueError("Length mismatch for per-array parameter")
return values
@_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
list of strings for cell type.
Returns
-------
cell_type: str
"""
return tuple(array._infer_cell_type() for array in self.arrays)
[docs]
@_unwrap_single_value
def get_aoi(self, solar_zenith, solar_azimuth):
"""Get the angle of incidence on the Array(s) in the system.
Parameters
----------
solar_zenith : float or Series.
Solar zenith angle.
solar_azimuth : float or Series.
Solar azimuth angle.
Returns
-------
aoi : Series or tuple of Series
The angle of incidence
"""
return tuple(array.get_aoi(solar_zenith, solar_azimuth)
for array in self.arrays)
[docs]
@_unwrap_single_value
def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
dni_extra=None, airmass=None, albedo=None,
model='haydavies', **kwargs):
"""
Uses :py:func:`pvlib.irradiance.get_total_irradiance` to
calculate the plane of array irradiance components on the tilted
surfaces defined by each array's ``surface_tilt`` and
``surface_azimuth``.
Parameters
----------
solar_zenith : float or Series
Solar zenith angle.
solar_azimuth : float or Series
Solar azimuth angle.
dni : float, Series, or tuple of float or Series
Direct Normal Irradiance. [W/m2]
ghi : float, Series, or tuple of float or Series
Global horizontal irradiance. [W/m2]
dhi : float, Series, or tuple of float or Series
Diffuse horizontal irradiance. [W/m2]
dni_extra : float, Series or tuple of float or Series, optional
Extraterrestrial direct normal irradiance. [W/m2]
airmass : float or Series, optional
Airmass. [unitless]
albedo : float or Series, optional
Ground surface albedo. [unitless]
model : String, default 'haydavies'
Irradiance model.
kwargs
Extra parameters passed to
:py:func:`pvlib.irradiance.get_total_irradiance`.
Notes
-----
Each of ``dni``, ``ghi``, and ``dni`` may be passed as a float, Series,
or tuple of float or Series. If passed as a float or Series, these
values are used for all Arrays. If passed as a tuple, the tuple length
must be the same as the number of Arrays. The first tuple element is
used for the first Array, the second tuple element for the second
Array, and so forth.
Some sky irradiance models require ``dni_extra``. For these models,
if ``dni_extra`` is not provided and ``solar_zenith`` has a
``DatetimeIndex``, then ``dni_extra`` is calculated.
Otherwise, ``dni_extra=1367`` is assumed.
Returns
-------
poa_irradiance : DataFrame or tuple of DataFrame
Column names are: ``'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'``.
See also
--------
pvlib.irradiance.get_total_irradiance
"""
dni = self._validate_per_array(dni, system_wide=True)
ghi = self._validate_per_array(ghi, system_wide=True)
dhi = self._validate_per_array(dhi, system_wide=True)
albedo = self._validate_per_array(albedo, system_wide=True)
return tuple(
array.get_irradiance(solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra=dni_extra, airmass=airmass,
albedo=albedo, model=model, **kwargs)
for array, dni, ghi, dhi, albedo in zip(
self.arrays, dni, ghi, dhi, albedo
)
)
[docs]
@_unwrap_single_value
def get_iam(self, aoi, iam_model='physical'):
"""
Determine the incidence angle modifier using the method specified by
``iam_model``.
Parameters for the selected IAM model are expected to be in
``PVSystem.module_parameters``. Default parameters are available for
the 'physical', 'ashrae' and 'martin_ruiz' models.
Parameters
----------
aoi : numeric or tuple of numeric
The angle of incidence in degrees.
aoi_model : string, default 'physical'
The IAM model to be used. Valid strings are 'physical', 'ashrae',
'martin_ruiz', 'sapm' and 'interp'.
Returns
-------
iam : numeric or tuple of numeric
The AOI modifier.
Raises
------
ValueError
if `iam_model` is not a valid model name.
"""
aoi = self._validate_per_array(aoi)
return tuple(array.get_iam(aoi, iam_model)
for array, aoi in zip(self.arrays, aoi))
[docs]
@_unwrap_single_value
def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
effective_irradiance=None):
"""
Determine cell temperature using the method specified by ``model``.
Parameters
----------
poa_global : numeric or tuple of numeric
Total incident irradiance in W/m^2.
temp_air : numeric or tuple of numeric
Ambient dry bulb temperature in degrees C.
wind_speed : numeric or tuple of numeric
Wind speed in m/s.
model : str
Supported models include ``'sapm'``, ``'pvsyst'``,
``'faiman'``, ``'fuentes'``, and ``'noct_sam'``
effective_irradiance : numeric or tuple of numeric, optional
The irradiance that is converted to photocurrent in W/m^2.
Only used for some models.
Returns
-------
numeric or tuple of numeric
Values in degrees C.
See Also
--------
Array.get_cell_temperature
Notes
-----
The `temp_air` and `wind_speed` parameters may be passed as tuples
to provide different values for each Array in the system. If passed as
a tuple the length must be the same as the number of Arrays. If not
passed as a tuple then the same value is used for each Array.
"""
poa_global = self._validate_per_array(poa_global)
temp_air = self._validate_per_array(temp_air, system_wide=True)
wind_speed = self._validate_per_array(wind_speed, system_wide=True)
# Not used for all models, but Array.get_cell_temperature handles it
effective_irradiance = self._validate_per_array(effective_irradiance,
system_wide=True)
return tuple(
array.get_cell_temperature(poa_global, temp_air, wind_speed,
model, effective_irradiance)
for array, poa_global, temp_air, wind_speed, effective_irradiance
in zip(
self.arrays, poa_global, temp_air, wind_speed,
effective_irradiance
)
)
[docs]
@_unwrap_single_value
def calcparams_desoto(self, effective_irradiance, temp_cell):
"""
Use the :py:func:`calcparams_desoto` function, the input
parameters and ``self.module_parameters`` to calculate the
module currents and resistances.
Parameters
----------
effective_irradiance : numeric or tuple of numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : float or Series or tuple of float or Series
The average cell temperature of cells within a module in C.
Returns
-------
See pvsystem.calcparams_desoto for details
"""
effective_irradiance = self._validate_per_array(effective_irradiance)
temp_cell = self._validate_per_array(temp_cell)
build_kwargs = functools.partial(
_build_kwargs,
['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref',
'R_s', 'alpha_sc', 'EgRef', 'dEgdT',
'irrad_ref', 'temp_ref']
)
return tuple(
calcparams_desoto(
effective_irradiance, temp_cell,
**build_kwargs(array.module_parameters)
)
for array, effective_irradiance, temp_cell
in zip(self.arrays, effective_irradiance, temp_cell)
)
[docs]
@_unwrap_single_value
def calcparams_cec(self, effective_irradiance, temp_cell):
"""
Use the :py:func:`calcparams_cec` function, the input
parameters and ``self.module_parameters`` to calculate the
module currents and resistances.
Parameters
----------
effective_irradiance : numeric or tuple of numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : float or Series or tuple of float or Series
The average cell temperature of cells within a module in C.
Returns
-------
See pvsystem.calcparams_cec for details
"""
effective_irradiance = self._validate_per_array(effective_irradiance)
temp_cell = self._validate_per_array(temp_cell)
build_kwargs = functools.partial(
_build_kwargs,
['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref',
'R_s', 'alpha_sc', 'Adjust', 'EgRef', 'dEgdT',
'irrad_ref', 'temp_ref']
)
return tuple(
calcparams_cec(
effective_irradiance, temp_cell,
**build_kwargs(array.module_parameters)
)
for array, effective_irradiance, temp_cell
in zip(self.arrays, effective_irradiance, temp_cell)
)
[docs]
@_unwrap_single_value
def calcparams_pvsyst(self, effective_irradiance, temp_cell):
"""
Use the :py:func:`calcparams_pvsyst` function, the input
parameters and ``self.module_parameters`` to calculate the
module currents and resistances.
Parameters
----------
effective_irradiance : numeric or tuple of numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : float or Series or tuple of float or Series
The average cell temperature of cells within a module in C.
Returns
-------
See pvsystem.calcparams_pvsyst for details
"""
effective_irradiance = self._validate_per_array(effective_irradiance)
temp_cell = self._validate_per_array(temp_cell)
build_kwargs = functools.partial(
_build_kwargs,
['gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref',
'R_sh_ref', 'R_sh_0', 'R_sh_exp',
'R_s', 'alpha_sc', 'EgRef',
'irrad_ref', 'temp_ref',
'cells_in_series']
)
return tuple(
calcparams_pvsyst(
effective_irradiance, temp_cell,
**build_kwargs(array.module_parameters)
)
for array, effective_irradiance, temp_cell
in zip(self.arrays, effective_irradiance, temp_cell)
)
[docs]
@_unwrap_single_value
def sapm(self, effective_irradiance, temp_cell):
"""
Use the :py:func:`sapm` function, the input parameters,
and ``self.module_parameters`` to calculate
Voc, Isc, Ix, Ixx, Vmp, and Imp.
Parameters
----------
effective_irradiance : numeric or tuple of numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : float or Series or tuple of float or Series
The average cell temperature of cells within a module in C.
Returns
-------
See pvsystem.sapm for details
"""
effective_irradiance = self._validate_per_array(effective_irradiance)
temp_cell = self._validate_per_array(temp_cell)
return tuple(
sapm(effective_irradiance, temp_cell, array.module_parameters)
for array, effective_irradiance, temp_cell
in zip(self.arrays, effective_irradiance, temp_cell)
)
[docs]
@_unwrap_single_value
def sapm_spectral_loss(self, airmass_absolute):
"""
Use the :py:func:`pvlib.spectrum.spectral_factor_sapm` function,
the input parameters, and ``self.module_parameters`` to calculate F1.
Parameters
----------
airmass_absolute : numeric
Absolute airmass.
Returns
-------
F1 : numeric or tuple of numeric
The SAPM spectral loss coefficient.
"""
return tuple(
spectrum.spectral_factor_sapm(airmass_absolute,
array.module_parameters)
for array in self.arrays
)
[docs]
@_unwrap_single_value
def sapm_effective_irradiance(self, poa_direct, poa_diffuse,
airmass_absolute, aoi,
reference_irradiance=1000):
"""
Use the :py:func:`sapm_effective_irradiance` function, the input
parameters, and ``self.module_parameters`` to calculate
effective irradiance.
Parameters
----------
poa_direct : numeric or tuple of numeric
The direct irradiance incident upon the module. [W/m2]
poa_diffuse : numeric or tuple of numeric
The diffuse irradiance incident on module. [W/m2]
airmass_absolute : numeric
Absolute airmass. [unitless]
aoi : numeric or tuple of numeric
Angle of incidence. [degrees]
Returns
-------
effective_irradiance : numeric or tuple of numeric
The SAPM effective irradiance. [W/m2]
"""
poa_direct = self._validate_per_array(poa_direct)
poa_diffuse = self._validate_per_array(poa_diffuse)
aoi = self._validate_per_array(aoi)
return tuple(
sapm_effective_irradiance(
poa_direct, poa_diffuse, airmass_absolute, aoi,
array.module_parameters)
for array, poa_direct, poa_diffuse, aoi
in zip(self.arrays, poa_direct, poa_diffuse, aoi)
)
[docs]
@_unwrap_single_value
def first_solar_spectral_loss(self, pw, airmass_absolute):
"""
Use :py:func:`pvlib.spectrum.spectral_factor_firstsolar` to
calculate the spectral loss modifier. The model coefficients are
specific to the module's cell type, and are determined by searching
for one of the following keys in self.module_parameters (in order):
- 'first_solar_spectral_coefficients' (user-supplied coefficients)
- 'Technology' - a string describing the cell type, can be read from
the CEC module parameter database
- 'Material' - a string describing the cell type, can be read from
the Sandia module database.
Parameters
----------
pw : array-like
atmospheric precipitable water (cm).
airmass_absolute : array-like
absolute (pressure corrected) airmass.
Returns
-------
modifier: array-like or tuple of array-like
spectral mismatch factor (unitless) which can be multiplied
with broadband irradiance reaching a module's cells to estimate
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, pw):
if 'first_solar_spectral_coefficients' in \
array.module_parameters.keys():
coefficients = \
array.module_parameters[
'first_solar_spectral_coefficients'
]
module_type = None
else:
module_type = array._infer_cell_type()
coefficients = None
return spectrum.spectral_factor_firstsolar(
pw, airmass_absolute, module_type, coefficients
)
return tuple(
itertools.starmap(_spectral_correction, zip(self.arrays, pw))
)
[docs]
def singlediode(self, photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth):
"""Wrapper around the :py:func:`pvlib.pvsystem.singlediode` function.
See :py:func:`pvsystem.singlediode` for details
"""
return singlediode(photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth)
[docs]
def i_from_v(self, voltage, photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth):
"""Wrapper around the :py:func:`pvlib.pvsystem.i_from_v` function.
See :py:func:`pvlib.pvsystem.i_from_v` for details.
.. versionchanged:: 0.10.0
The function's arguments have been reordered.
"""
return i_from_v(voltage, photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth)
[docs]
def get_ac(self, model, p_dc, v_dc=None):
r"""Calculates AC power from p_dc using the inverter model indicated
by model and self.inverter_parameters.
Parameters
----------
model : str
Must be one of 'sandia', 'adr', or 'pvwatts'.
p_dc : numeric, or tuple, list or array of numeric
DC power on each MPPT input of the inverter. Use tuple, list or
array for inverters with multiple MPPT inputs. If type is array,
p_dc must be 2d with axis 0 being the MPPT inputs. [W]
v_dc : numeric, or tuple, list or array of numeric
DC voltage on each MPPT input of the inverter. Required when
model='sandia' or model='adr'. Use tuple, list or
array for inverters with multiple MPPT inputs. If type is array,
v_dc must be 2d with axis 0 being the MPPT inputs. [V]
Returns
-------
power_ac : numeric
AC power output for the inverter. [W]
Raises
------
ValueError
If model is not one of 'sandia', 'adr' or 'pvwatts'.
ValueError
If model='adr' and the PVSystem has more than one array.
See also
--------
pvlib.inverter.sandia
pvlib.inverter.sandia_multi
pvlib.inverter.adr
pvlib.inverter.pvwatts
pvlib.inverter.pvwatts_multi
"""
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:
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:
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')
# 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.',
' model must be one of "sandia", "adr" or "pvwatts"')
[docs]
@_unwrap_single_value
def scale_voltage_current_power(self, data):
"""
Scales the voltage, current, and power of the `data` DataFrame
by `self.modules_per_string` and `self.strings_per_inverter`.
Parameters
----------
data: DataFrame or tuple of DataFrame
May contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx',
'i_sc', 'p_mp'`.
Returns
-------
scaled_data: DataFrame or tuple of DataFrame
A scaled copy of the input data.
"""
data = self._validate_per_array(data)
return tuple(
scale_voltage_current_power(data,
voltage=array.modules_per_string,
current=array.strings)
for array, data in zip(self.arrays, data)
)
[docs]
@_unwrap_single_value
def pvwatts_dc(self, g_poa_effective, temp_cell):
"""
Calculates DC power according to the PVWatts model using
:py:func:`pvlib.pvsystem.pvwatts_dc`, `self.module_parameters['pdc0']`,
and `self.module_parameters['gamma_pdc']`.
See :py:func:`pvlib.pvsystem.pvwatts_dc` for details.
"""
g_poa_effective = self._validate_per_array(g_poa_effective)
temp_cell = self._validate_per_array(temp_cell)
return tuple(
pvwatts_dc(g_poa_effective, temp_cell,
array.module_parameters['pdc0'],
array.module_parameters['gamma_pdc'],
**_build_kwargs(['temp_ref'], array.module_parameters))
for array, g_poa_effective, temp_cell
in zip(self.arrays, g_poa_effective, temp_cell)
)
[docs]
def pvwatts_losses(self):
"""
Calculates DC power losses according the PVwatts model using
:py:func:`pvlib.pvsystem.pvwatts_losses` and
``self.losses_parameters``.
See :py:func:`pvlib.pvsystem.pvwatts_losses` for details.
"""
kwargs = _build_kwargs(['soiling', 'shading', 'snow', 'mismatch',
'wiring', 'connections', 'lid',
'nameplate_rating', 'age', 'availability'],
self.losses_parameters)
return pvwatts_losses(**kwargs)
[docs]
@_unwrap_single_value
def dc_ohms_from_percent(self):
"""
Calculates the equivalent resistance of the wires for each array using
:py:func:`pvlib.pvsystem.dc_ohms_from_percent`
See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for details.
"""
return tuple(array.dc_ohms_from_percent() for array in self.arrays)
@property
def num_arrays(self):
"""The number of Arrays in the system."""
return len(self.arrays)
[docs]
class Array:
"""
An Array is a set of modules at the same orientation.
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
----------
mount: FixedMount, SingleAxisTrackerMount, or other
Mounting for the array, either on fixed-tilt racking or horizontal
single axis tracker. Mounting is used to determine module orientation.
If not provided, a FixedMount with zero tilt is used.
albedo : float, optional
Ground surface albedo. If not supplied, then ``surface_type`` is used
to look up a value in :py:const:`pvlib.albedo.SURFACE_ALBEDOS`.
If ``surface_type`` is also not supplied then a ground surface albedo
of 0.25 is used.
surface_type : string, optional
The ground surface type. See :py:const:`pvlib.albedo.SURFACE_ALBEDOS`
for valid values.
module : string, optional
The model name of the modules.
May be used to look up the module_parameters dictionary
via some other method.
module_type : string, optional
Describes the module's construction. Valid strings are 'glass_polymer'
and 'glass_glass'. Used for cell and module temperature calculations.
module_parameters : dict or Series, optional
Parameters for the module model, e.g., SAPM, CEC, or other.
temperature_model_parameters : dict or Series, optional
Parameters for the module temperature model, e.g., SAPM, Pvsyst, or
other.
modules_per_string: int, default 1
Number of modules per string in the array.
strings: int, default 1
Number of parallel strings in the array.
array_losses_parameters : dict or Series, optional
Supported keys are 'dc_ohmic_percent'.
name : str, optional
Name of Array instance.
"""
[docs]
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,
array_losses_parameters=None,
name=None):
self.mount = mount
self.surface_type = surface_type
if albedo is None:
self.albedo = pvlib.albedo.SURFACE_ALBEDOS.get(surface_type, 0.25)
else:
self.albedo = albedo
self.module = module
if module_parameters is None:
self.module_parameters = {}
else:
self.module_parameters = module_parameters
self.module_type = module_type
self.strings = strings
self.modules_per_string = modules_per_string
if temperature_model_parameters is None:
self.temperature_model_parameters = \
self._infer_temperature_model_params()
else:
self.temperature_model_parameters = temperature_model_parameters
if array_losses_parameters is None:
self.array_losses_parameters = {}
else:
self.array_losses_parameters = array_losses_parameters
self.name = name
def __repr__(self):
attrs = ['name', 'mount', 'module',
'albedo', 'module_type',
'temperature_model_parameters',
'strings', 'modules_per_string']
return 'Array:\n ' + '\n '.join(
f'{attr}: {getattr(self, attr)}' for attr in attrs
)
def _infer_temperature_model_params(self):
# try to infer temperature model parameters from racking_model
# and module_type
param_set = f'{self.mount.racking_model}_{self.module_type}'
if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']:
return temperature._temperature_model_params('sapm', param_set)
elif 'freestanding' in param_set:
return temperature._temperature_model_params('pvsyst',
'freestanding')
elif 'insulated' in param_set: # after SAPM to avoid confusing keys
return temperature._temperature_model_params('pvsyst',
'insulated')
else:
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
list of strings for cell type.
Returns
-------
cell_type: str
"""
_cell_type_dict = {'Multi-c-Si': 'multisi',
'Mono-c-Si': 'monosi',
'Thin Film': 'cigs',
'a-Si/nc': 'asi',
'CIS': 'cigs',
'CIGS': 'cigs',
'1-a-Si': 'asi',
'CdTe': 'cdte',
'a-Si': 'asi',
'2-a-Si': None,
'3-a-Si': None,
'HIT-Si': 'monosi',
'mc-Si': 'multisi',
'c-Si': 'multisi',
'Si-Film': 'asi',
'EFG mc-Si': 'multisi',
'GaAs': None,
'a-Si / mono-Si': 'monosi'}
if 'Technology' in self.module_parameters.keys():
# CEC module parameter set
cell_type = _cell_type_dict[self.module_parameters['Technology']]
elif 'Material' in self.module_parameters.keys():
# Sandia module parameter set
cell_type = _cell_type_dict[self.module_parameters['Material']]
else:
cell_type = None
return cell_type
[docs]
def get_aoi(self, solar_zenith, solar_azimuth):
"""
Get the angle of incidence on the array.
Parameters
----------
solar_zenith : float or Series
Solar zenith angle.
solar_azimuth : float or Series
Solar azimuth angle
Returns
-------
aoi : Series
Then angle of incidence.
"""
orientation = self.mount.get_orientation(solar_zenith, solar_azimuth)
return irradiance.aoi(orientation['surface_tilt'],
orientation['surface_azimuth'],
solar_zenith, solar_azimuth)
[docs]
def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
dni_extra=None, airmass=None, albedo=None,
model='haydavies', **kwargs):
"""
Get plane of array irradiance components.
Uses :py:func:`pvlib.irradiance.get_total_irradiance` to
calculate the plane of array irradiance components for a surface
defined by ``self.surface_tilt`` and ``self.surface_azimuth``.
Parameters
----------
solar_zenith : float or Series.
Solar zenith angle.
solar_azimuth : float or Series.
Solar azimuth angle.
dni : float or Series
Direct normal irradiance. [W/m2]
ghi : float or Series. [W/m2]
Global horizontal irradiance
dhi : float or Series
Diffuse horizontal irradiance. [W/m2]
dni_extra : float or Series, optional
Extraterrestrial direct normal irradiance. [W/m2]
airmass : float or Series, optional
Airmass. [unitless]
albedo : float or Series, optional
Ground surface albedo. [unitless]
model : String, default 'haydavies'
Irradiance model.
kwargs
Extra parameters passed to
:py:func:`pvlib.irradiance.get_total_irradiance`.
Returns
-------
poa_irradiance : DataFrame
Column names are: ``'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'``.
Notes
-----
Some sky irradiance models require ``dni_extra``. For these models,
if ``dni_extra`` is not provided and ``solar_zenith`` has a
``DatetimeIndex``, then ``dni_extra`` is calculated.
Otherwise, ``dni_extra=1367`` is assumed.
See also
--------
:py:func:`pvlib.irradiance.get_total_irradiance`
"""
if albedo is None:
albedo = self.albedo
# dni_extra is not needed for all models, but this is easier
if dni_extra is None:
if (hasattr(solar_zenith, 'index') and
isinstance(solar_zenith.index, pd.DatetimeIndex)):
# calculate extraterrestrial irradiance
dni_extra = irradiance.get_extra_radiation(
solar_zenith.index)
else:
# use the solar constant
dni_extra = 1367.0
if airmass is None:
airmass = atmosphere.get_relative_airmass(solar_zenith)
orientation = self.mount.get_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,
airmass=airmass,
albedo=albedo,
model=model,
**kwargs)
[docs]
def get_iam(self, aoi, iam_model='physical'):
"""
Determine the incidence angle modifier using the method specified by
``iam_model``.
Parameters for the selected IAM model are expected to be in
``Array.module_parameters``. Default parameters are available for
the 'physical', 'ashrae' and 'martin_ruiz' models.
Parameters
----------
aoi : numeric
The angle of incidence in degrees.
aoi_model : string, default 'physical'
The IAM model to be used. Valid strings are 'physical', 'ashrae',
'martin_ruiz', 'sapm' and 'interp'.
Returns
-------
iam : numeric
The AOI modifier.
Raises
------
ValueError
if `iam_model` is not a valid model name.
"""
model = iam_model.lower()
if model in ['ashrae', 'physical', 'martin_ruiz', 'interp']:
func = getattr(iam, model) # get function at pvlib.iam
# get all parameters from function signature to retrieve them from
# module_parameters if present
params = set(inspect.signature(func).parameters.keys())
params.discard('aoi') # exclude aoi so it can't be repeated
kwargs = _build_kwargs(params, self.module_parameters)
return func(aoi, **kwargs)
elif model == 'sapm':
return iam.sapm(aoi, self.module_parameters)
else:
raise ValueError(model + ' is not a valid IAM model')
[docs]
def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
effective_irradiance=None):
"""
Determine cell temperature using the method specified by ``model``.
Parameters
----------
poa_global : numeric
Total incident irradiance [W/m^2]
temp_air : numeric
Ambient dry bulb temperature [C]
wind_speed : numeric
Wind speed [m/s]
model : str
Supported models include ``'sapm'``, ``'pvsyst'``,
``'faiman'``, ``'fuentes'``, and ``'noct_sam'``
effective_irradiance : numeric, optional
The irradiance that is converted to photocurrent in W/m^2.
Only used for some models.
Returns
-------
numeric
Values in degrees C.
See Also
--------
pvlib.temperature.sapm_cell, pvlib.temperature.pvsyst_cell,
pvlib.temperature.faiman, pvlib.temperature.fuentes,
pvlib.temperature.noct_sam
Notes
-----
Some temperature models have requirements for the input types;
see the documentation of the underlying model function for details.
"""
# convenience wrapper to avoid passing args 2 and 3 every call
_build_tcell_args = functools.partial(
_build_args, input_dict=self.temperature_model_parameters,
dict_name='temperature_model_parameters')
if model == 'sapm':
func = temperature.sapm_cell
required = _build_tcell_args(['a', 'b', 'deltaT'])
optional = _build_kwargs(['irrad_ref'],
self.temperature_model_parameters)
elif model == 'pvsyst':
func = temperature.pvsyst_cell
required = tuple()
optional = {
**_build_kwargs(['module_efficiency', 'alpha_absorption'],
self.module_parameters),
**_build_kwargs(['u_c', 'u_v'],
self.temperature_model_parameters)
}
elif model == 'faiman':
func = temperature.faiman
required = tuple()
optional = _build_kwargs(['u0', 'u1'],
self.temperature_model_parameters)
elif model == 'fuentes':
func = temperature.fuentes
required = _build_tcell_args(['noct_installed'])
optional = _build_kwargs([
'wind_height', 'emissivity', 'absorption',
'surface_tilt', 'module_width', 'module_length'],
self.temperature_model_parameters)
if self.mount.module_height is not None:
optional['module_height'] = self.mount.module_height
elif model == 'noct_sam':
func = functools.partial(temperature.noct_sam,
effective_irradiance=effective_irradiance)
required = _build_tcell_args(['noct', 'module_efficiency'])
optional = _build_kwargs(['transmittance_absorptance',
'array_height', 'mount_standoff'],
self.temperature_model_parameters)
else:
raise ValueError(f'{model} is not a valid cell temperature model')
temperature_cell = func(poa_global, temp_air, wind_speed,
*required, **optional)
return temperature_cell
[docs]
def dc_ohms_from_percent(self):
"""
Calculates the equivalent resistance of the wires using
:py:func:`pvlib.pvsystem.dc_ohms_from_percent`
Makes use of array module parameters according to the
following DC models:
CEC:
* `self.module_parameters["V_mp_ref"]`
* `self.module_parameters["I_mp_ref"]`
SAPM:
* `self.module_parameters["Vmpo"]`
* `self.module_parameters["Impo"]`
PVsyst-like or other:
* `self.module_parameters["Vmpp"]`
* `self.module_parameters["Impp"]`
Other array parameters that are used are:
`self.losses_parameters["dc_ohmic_percent"]`,
`self.modules_per_string`, and
`self.strings`.
See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for more details.
"""
# get relevent Vmp and Imp parameters from CEC parameters
if all(elem in self.module_parameters
for elem in ['V_mp_ref', 'I_mp_ref']):
vmp_ref = self.module_parameters['V_mp_ref']
imp_ref = self.module_parameters['I_mp_ref']
# get relevant Vmp and Imp parameters from SAPM parameters
elif all(elem in self.module_parameters for elem in ['Vmpo', 'Impo']):
vmp_ref = self.module_parameters['Vmpo']
imp_ref = self.module_parameters['Impo']
# get relevant Vmp and Imp parameters if they are PVsyst-like
elif all(elem in self.module_parameters for elem in ['Vmpp', 'Impp']):
vmp_ref = self.module_parameters['Vmpp']
imp_ref = self.module_parameters['Impp']
# raise error if relevant Vmp and Imp parameters are not found
else:
raise ValueError('Parameters for Vmp and Imp could not be found '
'in the array module parameters. Module '
'parameters must include one set of '
'{"V_mp_ref", "I_mp_Ref"}, '
'{"Vmpo", "Impo"}, or '
'{"Vmpp", "Impp"}.'
)
return dc_ohms_from_percent(
vmp_ref,
imp_ref,
self.array_losses_parameters['dc_ohmic_percent'],
self.modules_per_string,
self.strings)
@dataclass
class AbstractMount(ABC):
"""
A base class for Mount classes to extend. It is not intended to be
instantiated directly.
"""
@abstractmethod
def get_orientation(self, solar_zenith, solar_azimuth):
"""
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'`
(typically a dict or pandas.DataFrame)
"""
[docs]
@dataclass
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]
racking_model : str, optional
Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
Used to identify a parameter set for the SAPM cell temperature model.
module_height : float, optional
The height above ground of the center of the module [m]. Used for
the Fuentes cell temperature model.
"""
surface_tilt: float = 0.0
surface_azimuth: float = 180.0
racking_model: Optional[str] = None
module_height: Optional[float] = None
[docs]
def get_orientation(self, solar_zenith, solar_azimuth):
# note -- docstring is automatically inherited from AbstractMount
return {
'surface_tilt': self.surface_tilt,
'surface_azimuth': self.surface_azimuth,
}
[docs]
@dataclass
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 or tuple, default 90
A value denoting the maximum rotation angle, in decimal degrees,
of the one-axis tracker from its horizontal position (horizontal
if axis_tilt = 0). If a float is provided, it represents the maximum
rotation angle, and the minimum rotation angle is assumed to be the
opposite of the maximum angle. If a tuple of (min_angle, max_angle) is
provided, it represents both the minimum and maximum rotation angles.
A rotation to 'max_angle' is a counter-clockwise rotation about the
y-axis of the tracker coordinate system. For example, for a tracker
with 'axis_azimuth' oriented to the south, a rotation to 'max_angle'
is towards the west, and a rotation toward 'min_angle' is in the
opposite direction, toward the east. Hence a max_angle of 180 degrees
(equivalent to max_angle = (-180, 180)) allows the tracker to achieve
its full rotation capability.
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]
racking_model : str, optional
Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
Used to identify a parameter set for the SAPM cell temperature model.
module_height : float, optional
The height above ground of the center of the module [m]. Used for
the Fuentes cell temperature model.
"""
axis_tilt: float = 0.0
axis_azimuth: float = 0.0
max_angle: Union[float, tuple] = 90.0
backtrack: bool = True
gcr: float = 2.0/7.0
cross_axis_tilt: float = 0.0
racking_model: Optional[str] = None
module_height: Optional[float] = None
[docs]
def get_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
[docs]
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,
irrad_ref=1000, temp_ref=25):
'''
Calculates five parameter values for the single diode equation at
effective irradiance and cell temperature using the De Soto et al.
model. The five values returned by ``calcparams_desoto`` can be used by
singlediode to calculate an IV curve.
The model is described in [1]_.
Parameters
----------
effective_irradiance : numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : numeric
The average cell temperature of cells within a module in C.
alpha_sc : float
The short-circuit current temperature coefficient of the
module in units of A/C.
a_ref : float
The product of the usual diode ideality factor (n, unitless),
number of cells in series (Ns), and cell thermal voltage at reference
conditions, in units of V.
I_L_ref : float
The light-generated current (or photocurrent) at reference conditions,
in amperes.
I_o_ref : float
The dark or diode reverse saturation current at reference conditions,
in amperes.
R_sh_ref : float
The shunt resistance at reference conditions, in ohms.
R_s : float
The series resistance at reference conditions, in ohms.
EgRef : float
The energy bandgap at reference temperature in units of eV.
1.121 eV for crystalline silicon. EgRef must be >0. For parameters
from the SAM CEC module database, EgRef=1.121 is implicit for all
cell types in the parameter estimation algorithm used by NREL.
dEgdT : float
The temperature dependence of the energy bandgap at reference
conditions in units of 1/K. May be either a scalar value
(e.g. -0.0002677 as in [1]_) or a DataFrame (this may be useful if
dEgdT is a modeled as a function of temperature). For parameters from
the SAM CEC module database, dEgdT=-0.0002677 is implicit for all cell
types in the parameter estimation algorithm used by NREL.
irrad_ref : float, default 1000
Reference irradiance in W/m^2.
temp_ref : float, default 25
Reference cell temperature in C.
Returns
-------
Tuple of the following results:
photocurrent : numeric
Light-generated current in amperes
saturation_current : numeric
Diode saturation current in amperes
resistance_series : numeric
Series resistance in ohms
resistance_shunt : numeric
Shunt resistance in ohms
nNsVth : numeric
The product of the usual diode ideality factor (n, unitless),
number of cells in series (Ns), and cell thermal voltage at
specified effective irradiance and cell temperature.
References
----------
.. [1] W. De Soto et al., "Improvement and validation of a model for
photovoltaic array performance", Solar Energy, vol 80, pp. 78-88,
2006.
.. [2] System Advisor Model web page. https://sam.nrel.gov.
.. [3] A. Dobos, "An Improved Coefficient Calculator for the California
Energy Commission 6 Parameter Photovoltaic Module Model", Journal of
Solar Energy Engineering, vol 134, 2012.
.. [4] O. Madelung, "Semiconductors: Data Handbook, 3rd ed." ISBN
3-540-40488-0
See Also
--------
singlediode
retrieve_sam
Notes
-----
If the reference parameters in the ModuleParameters struct are read
from a database or library of parameters (e.g. System Advisor
Model), it is important to use the same EgRef and dEgdT values that
were used to generate the reference parameters, regardless of the
actual bandgap characteristics of the semiconductor. For example, in
the case of the System Advisor Model library, created as described
in [3], EgRef and dEgdT for all modules were 1.121 and -0.0002677,
respectively.
This table of reference bandgap energies (EgRef), bandgap energy
temperature dependence (dEgdT), and "typical" airmass response (M)
is provided purely as reference to those who may generate their own
reference module parameters (a_ref, IL_ref, I0_ref, etc.) based upon
the various PV semiconductors. Again, we stress the importance of
using identical EgRef and dEgdT when generation reference parameters
and modifying the reference parameters (for irradiance, temperature,
and airmass) per DeSoto's equations.
Crystalline Silicon (Si):
* EgRef = 1.121
* dEgdT = -0.0002677
>>> M = np.polyval([-1.26E-4, 2.816E-3, -0.024459, 0.086257, 0.9181],
... AMa) # doctest: +SKIP
Source: [1]
Cadmium Telluride (CdTe):
* EgRef = 1.475
* dEgdT = -0.0003
>>> M = np.polyval([-2.46E-5, 9.607E-4, -0.0134, 0.0716, 0.9196],
... AMa) # doctest: +SKIP
Source: [4]
Copper Indium diSelenide (CIS):
* EgRef = 1.010
* dEgdT = -0.00011
>>> M = np.polyval([-3.74E-5, 0.00125, -0.01462, 0.0718, 0.9210],
... AMa) # doctest: +SKIP
Source: [4]
Copper Indium Gallium diSelenide (CIGS):
* EgRef = 1.15
* dEgdT = ????
>>> M = np.polyval([-9.07E-5, 0.0022, -0.0202, 0.0652, 0.9417],
... AMa) # doctest: +SKIP
Source: Wikipedia
Gallium Arsenide (GaAs):
* EgRef = 1.424
* dEgdT = -0.000433
* M = unknown
Source: [4]
'''
# Boltzmann constant in eV/K, 8.617332478e-05
k = constants.value('Boltzmann constant in eV/K')
# reference temperature
Tref_K = temp_ref + 273.15
Tcell_K = temp_cell + 273.15
E_g = EgRef * (1 + dEgdT*(Tcell_K - Tref_K))
nNsVth = a_ref * (Tcell_K / Tref_K)
# In the equation for IL, the single factor effective_irradiance is
# used, in place of the product S*M in [1]. effective_irradiance is
# equivalent to the product of S (irradiance reaching a module's cells) *
# M (spectral adjustment factor) as described in [1].
IL = effective_irradiance / irrad_ref * \
(I_L_ref + alpha_sc * (Tcell_K - Tref_K))
I0 = (I_o_ref * ((Tcell_K / Tref_K) ** 3) *
(np.exp(EgRef / (k*(Tref_K)) - (E_g / (k*(Tcell_K))))))
# Note that the equation for Rsh differs from [1]. In [1] Rsh is given as
# Rsh = Rsh_ref * (S_ref / S) where S is broadband irradiance reaching
# the module's cells. If desired this model behavior can be duplicated
# by applying reflection and soiling losses to broadband plane of array
# irradiance and not applying a spectral loss modifier, i.e.,
# spectral_modifier = 1.0.
# use errstate to silence divide by warning
with np.errstate(divide='ignore'):
Rsh = R_sh_ref * (irrad_ref / effective_irradiance)
Rs = R_s
numeric_args = (effective_irradiance, temp_cell)
out = (IL, I0, Rs, Rsh, nNsVth)
if all(map(np.isscalar, numeric_args)):
return out
index = tools.get_pandas_index(*numeric_args)
if index is None:
return np.broadcast_arrays(*out)
return tuple(pd.Series(a, index=index).rename(None) for a in out)
[docs]
def calcparams_cec(effective_irradiance, temp_cell,
alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
Adjust, EgRef=1.121, dEgdT=-0.0002677,
irrad_ref=1000, temp_ref=25):
'''
Calculates five parameter values for the single diode equation at
effective irradiance and cell temperature using the CEC
model. The CEC model [1]_ differs from the De soto et al.
model [3]_ by the parameter Adjust. The five values returned by
calcparams_cec can be used by singlediode to calculate an IV curve.
Parameters
----------
effective_irradiance : numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : numeric
The average cell temperature of cells within a module in C.
alpha_sc : float
The short-circuit current temperature coefficient of the
module in units of A/C.
a_ref : float
The product of the usual diode ideality factor (n, unitless),
number of cells in series (Ns), and cell thermal voltage at reference
conditions, in units of V.
I_L_ref : float
The light-generated current (or photocurrent) at reference conditions,
in amperes.
I_o_ref : float
The dark or diode reverse saturation current at reference conditions,
in amperes.
R_sh_ref : float
The shunt resistance at reference conditions, in ohms.
R_s : float
The series resistance at reference conditions, in ohms.
Adjust : float
The adjustment to the temperature coefficient for short circuit
current, in percent
EgRef : float
The energy bandgap at reference temperature in units of eV.
1.121 eV for crystalline silicon. EgRef must be >0. For parameters
from the SAM CEC module database, EgRef=1.121 is implicit for all
cell types in the parameter estimation algorithm used by NREL.
dEgdT : float
The temperature dependence of the energy bandgap at reference
conditions in units of 1/K. May be either a scalar value
(e.g. -0.0002677 as in [3]) or a DataFrame (this may be useful if
dEgdT is a modeled as a function of temperature). For parameters from
the SAM CEC module database, dEgdT=-0.0002677 is implicit for all cell
types in the parameter estimation algorithm used by NREL.
irrad_ref : float, default 1000
Reference irradiance in W/m^2.
temp_ref : float, default 25
Reference cell temperature in C.
Returns
-------
Tuple of the following results:
photocurrent : numeric
Light-generated current in amperes
saturation_current : numeric
Diode saturation curent in amperes
resistance_series : numeric
Series resistance in ohms
resistance_shunt : numeric
Shunt resistance in ohms
nNsVth : numeric
The product of the usual diode ideality factor (n, unitless),
number of cells in series (Ns), and cell thermal voltage at
specified effective irradiance and cell temperature.
References
----------
.. [1] A. Dobos, "An Improved Coefficient Calculator for the California
Energy Commission 6 Parameter Photovoltaic Module Model", Journal of
Solar Energy Engineering, vol 134, 2012.
.. [2] System Advisor Model web page. https://sam.nrel.gov.
.. [3] W. De Soto et al., "Improvement and validation of a model for
photovoltaic array performance", Solar Energy, vol 80, pp. 78-88,
2006.
See Also
--------
calcparams_desoto
singlediode
retrieve_sam
'''
# pass adjusted temperature coefficient to desoto
return calcparams_desoto(effective_irradiance, temp_cell,
alpha_sc*(1.0 - Adjust/100),
a_ref, I_L_ref, I_o_ref,
R_sh_ref, R_s,
EgRef=EgRef, dEgdT=dEgdT,
irrad_ref=irrad_ref, temp_ref=temp_ref)
[docs]
def calcparams_pvsyst(effective_irradiance, temp_cell,
alpha_sc, gamma_ref, mu_gamma,
I_L_ref, I_o_ref,
R_sh_ref, R_sh_0, R_s,
cells_in_series,
R_sh_exp=5.5,
EgRef=1.121,
irrad_ref=1000, temp_ref=25):
'''
Calculates five parameter values for the single diode equation at
effective irradiance and cell temperature using the PVsyst v6
model. The PVsyst v6 model is described in [1]_, [2]_, [3]_.
The five values returned by calcparams_pvsyst can be used by singlediode
to calculate an IV curve.
Parameters
----------
effective_irradiance : numeric
The irradiance (W/m2) that is converted to photocurrent.
temp_cell : numeric
The average cell temperature of cells within a module in C.
alpha_sc : float
The short-circuit current temperature coefficient of the
module in units of A/C.
gamma_ref : float
The diode ideality factor
mu_gamma : float
The temperature coefficient for the diode ideality factor, 1/K
I_L_ref : float
The light-generated current (or photocurrent) at reference conditions,
in amperes.
I_o_ref : float
The dark or diode reverse saturation current at reference conditions,
in amperes.
R_sh_ref : float
The shunt resistance at reference conditions, in ohms.
R_sh_0 : float
The shunt resistance at zero irradiance conditions, in ohms.
R_s : float
The series resistance at reference conditions, in ohms.
cells_in_series : integer
The number of cells connected in series.
R_sh_exp : float
The exponent in the equation for shunt resistance, unitless. Defaults
to 5.5.
EgRef : float
The energy bandgap at reference temperature in units of eV.
1.121 eV for crystalline silicon. EgRef must be >0.
irrad_ref : float, default 1000
Reference irradiance in W/m^2.
temp_ref : float, default 25
Reference cell temperature in C.
Returns
-------
Tuple of the following results:
photocurrent : numeric
Light-generated current in amperes
saturation_current : numeric
Diode saturation current in amperes
resistance_series : numeric
Series resistance in ohms
resistance_shunt : numeric
Shunt resistance in ohms
nNsVth : numeric
The product of the usual diode ideality factor (n, unitless),
number of cells in series (Ns), and cell thermal voltage at
specified effective irradiance and cell temperature.
References
----------
.. [1] K. Sauer, T. Roessler, C. W. Hansen, Modeling the Irradiance and
Temperature Dependence of Photovoltaic Modules in PVsyst,
IEEE Journal of Photovoltaics v5(1), January 2015.
.. [2] A. Mermoud, PV modules modelling, Presentation at the 2nd PV
Performance Modeling Workshop, Santa Clara, CA, May 2013
.. [3] A. Mermoud, T. Lejeune, Performance Assessment of a Simulation Model
for PV modules of any available technology, 25th European Photovoltaic
Solar Energy Conference, Valencia, Spain, Sept. 2010
See Also
--------
calcparams_desoto
singlediode
'''
# Boltzmann constant in J/K
k = constants.k
# elementary charge in coulomb
q = constants.e
# reference temperature
Tref_K = temp_ref + 273.15
Tcell_K = temp_cell + 273.15
gamma = gamma_ref + mu_gamma * (Tcell_K - Tref_K)
nNsVth = gamma * k / q * cells_in_series * Tcell_K
IL = effective_irradiance / irrad_ref * \
(I_L_ref + alpha_sc * (Tcell_K - Tref_K))
I0 = I_o_ref * ((Tcell_K / Tref_K) ** 3) * \
(np.exp((q * EgRef) / (k * gamma) * (1 / Tref_K - 1 / Tcell_K)))
Rsh_tmp = \
(R_sh_ref - R_sh_0 * np.exp(-R_sh_exp)) / (1.0 - np.exp(-R_sh_exp))
Rsh_base = np.maximum(0.0, Rsh_tmp)
Rsh = Rsh_base + (R_sh_0 - Rsh_base) * \
np.exp(-R_sh_exp * effective_irradiance / irrad_ref)
Rs = R_s
numeric_args = (effective_irradiance, temp_cell)
out = (IL, I0, Rs, Rsh, nNsVth)
if all(map(np.isscalar, numeric_args)):
return out
index = tools.get_pandas_index(*numeric_args)
if index is None:
return np.broadcast_arrays(*out)
return tuple(pd.Series(a, index=index).rename(None) for a in out)
[docs]
def retrieve_sam(name=None, path=None):
"""
Retrieve latest module and inverter info from a file bundled with pvlib,
a path or an URL (like SAM's website).
This function will retrieve either:
* CEC module database
* Sandia Module database
* CEC Inverter database
* Anton Driesse Inverter database
and return it as a pandas DataFrame.
.. note::
Only provide one of ``name`` or ``path``.
Parameters
----------
name : string, optional
Use one of the following strings to retrieve a database bundled with
pvlib:
* 'CECMod' - returns the CEC module database
* 'CECInverter' - returns the CEC Inverter database
* 'SandiaInverter' - returns the CEC Inverter database
(CEC is only current inverter db available; tag kept for
backwards compatibility)
* 'SandiaMod' - returns the Sandia Module database
* 'ADRInverter' - returns the ADR Inverter database
path : string, optional
Path to a CSV file or a URL.
Returns
-------
samfile : DataFrame
A DataFrame containing all the elements of the desired database.
Each column represents a module or inverter, and a specific
dataset can be retrieved by the command
Raises
------
ValueError
If no ``name`` or ``path`` is provided.
ValueError
If both ``name`` and ``path`` are provided.
KeyError
If the provided ``name`` is not a valid database name.
Notes
-----
Files available at
https://github.com/NREL/SAM/tree/develop/deploy/libraries
Documentation for module and inverter data sets:
https://sam.nrel.gov/photovoltaic/pv-sub-page-2.html
Examples
--------
>>> from pvlib import pvsystem
>>> invdb = pvsystem.retrieve_sam('CECInverter')
>>> inverter = invdb.AE_Solar_Energy__AE6_0__277V_
>>> inverter
Vac 277
Pso 36.197575
Paco 6000.0
Pdco 6158.746094
Vdco 360.0
C0 -0.000002
C1 -0.000026
C2 -0.001253
C3 0.00021
Pnt 1.8
Vdcmax 450.0
Idcmax 17.107628
Mppt_low 100.0
Mppt_high 450.0
CEC_Date NaN
CEC_Type Utility Interactive
Name: AE_Solar_Energy__AE6_0__277V_, dtype: object
"""
# error: path was previously silently ignored if name was given GH#2018
if name is not None and path is not None:
raise ValueError("Please provide either 'name' or 'path', not both.")
elif name is None and path is None:
raise ValueError("Please provide either 'name' or 'path'.")
elif name is not None:
internal_dbs = {
"cecmod": "sam-library-cec-modules-2019-03-05.csv",
"sandiamod": "sam-library-sandia-modules-2015-6-30.csv",
"adrinverter": "adr-library-cec-inverters-2019-03-05.csv",
# Both 'cecinverter' and 'sandiainverter', point to same database
# to provide for old code, while aligning with current expectations
"cecinverter": "sam-library-cec-inverters-2019-03-05.csv",
"sandiainverter": "sam-library-cec-inverters-2019-03-05.csv",
}
try:
csvdata_path = Path(__file__).parent.joinpath(
"data", internal_dbs[name.lower()]
)
except KeyError:
raise KeyError(
f"Invalid name {name}. "
+ f"Provide one of {list(internal_dbs.keys())}."
) from None
else: # path is not None
if path.lower().startswith("http"): # URL check is not case-sensitive
response = urlopen(path) # URL is case-sensitive
csvdata_path = io.StringIO(response.read().decode(errors="ignore"))
else:
csvdata_path = path
return _parse_raw_sam_df(csvdata_path)
def _normalize_sam_product_names(names):
'''
Replace special characters within the product names to make them more
suitable for use as Dataframe column names.
'''
# Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
import warnings
BAD_CHARS = ' -.()[]:+/",'
GOOD_CHARS = '____________'
mapping = str.maketrans(BAD_CHARS, GOOD_CHARS)
names = pd.Series(data=names)
norm_names = names.str.translate(mapping)
n_duplicates = names.duplicated().sum()
if n_duplicates > 0:
warnings.warn('Original names contain %d duplicate(s).' % n_duplicates)
n_duplicates = norm_names.duplicated().sum()
if n_duplicates > 0:
warnings.warn(
'Normalized names contain %d duplicate(s).' % n_duplicates)
return norm_names.values
def _parse_raw_sam_df(csvdata):
df = pd.read_csv(csvdata, index_col=0, skiprows=[1, 2])
df.columns = df.columns.str.replace(' ', '_')
df.index = _normalize_sam_product_names(df.index)
df = df.transpose()
if 'ADRCoefficients' in df.index:
ad_ce = 'ADRCoefficients'
# for each inverter, parses a string of coefficients like
# ' 1.33, 2.11, 3.12' into a list containing floats:
# [1.33, 2.11, 3.12]
df.loc[ad_ce] = df.loc[ad_ce].map(lambda x: list(
map(float, x.strip(' []').split())))
return df
[docs]
def sapm(effective_irradiance, temp_cell, module):
'''
The Sandia PV Array Performance Model (SAPM) generates 5 points on a
PV module's I-V curve (Voc, Isc, Ix, Ixx, Vmp/Imp) according to
SAND2004-3535. Assumes a reference cell temperature of 25 C.
Parameters
----------
effective_irradiance : numeric
Irradiance reaching the module's cells, after reflections and
adjustment for spectrum. [W/m2]
temp_cell : numeric
Cell temperature [C].
module : dict-like
A dict or Series defining the SAPM parameters. See the notes section
for more details.
Returns
-------
A DataFrame with the columns:
* i_sc : Short-circuit current (A)
* i_mp : Current at the maximum-power point (A)
* v_oc : Open-circuit voltage (V)
* v_mp : Voltage at maximum-power point (V)
* p_mp : Power at maximum-power point (W)
* i_x : Current at module V = 0.5Voc, defines 4th point on I-V
curve for modeling curve shape
* i_xx : Current at module V = 0.5(Voc+Vmp), defines 5th point on
I-V curve for modeling curve shape
Notes
-----
The SAPM parameters which are required in ``module`` are
listed in the following table.
The Sandia module database contains parameter values for a limited set
of modules. The CEC module database does not contain these parameters.
Both databases can be accessed using :py:func:`retrieve_sam`.
================ ========================================================
Key Description
================ ========================================================
A0-A4 The airmass coefficients used in calculating
effective irradiance
B0-B5 The angle of incidence coefficients used in calculating
effective irradiance
C0-C7 The empirically determined coefficients relating
Imp, Vmp, Ix, and Ixx to effective irradiance
Isco Short circuit current at reference condition (amps)
Impo Maximum power current at reference condition (amps)
Voco Open circuit voltage at reference condition (amps)
Vmpo Maximum power voltage at reference condition (amps)
Aisc Short circuit current temperature coefficient at
reference condition (1/C)
Aimp Maximum power current temperature coefficient at
reference condition (1/C)
Bvoco Open circuit voltage temperature coefficient at
reference condition (V/C)
Mbvoc Coefficient providing the irradiance dependence for the
BetaVoc temperature coefficient at reference irradiance
(V/C)
Bvmpo Maximum power voltage temperature coefficient at
reference condition
Mbvmp Coefficient providing the irradiance dependence for the
BetaVmp temperature coefficient at reference irradiance
(V/C)
N Empirically determined "diode factor" (dimensionless)
Cells_in_Series Number of cells in series in a module's cell string(s)
IXO Ix at reference conditions
IXXO Ixx at reference conditions
FD Fraction of diffuse irradiance used by module
================ ========================================================
References
----------
.. [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
NM.
See Also
--------
retrieve_sam
pvlib.temperature.sapm_cell
pvlib.temperature.sapm_module
'''
# TODO: someday, change temp_ref and irrad_ref to reference_temperature and
# reference_irradiance and expose
temp_ref = 25
irrad_ref = 1000
q = constants.e # Elementary charge in units of coulombs
kb = constants.k # Boltzmann's constant in units of J/K
# avoid problem with integer input
Ee = np.array(effective_irradiance, dtype='float64') / irrad_ref
# set up masking for 0, positive, and nan inputs
Ee_gt_0 = np.full_like(Ee, False, dtype='bool')
Ee_eq_0 = np.full_like(Ee, False, dtype='bool')
notnan = ~np.isnan(Ee)
np.greater(Ee, 0, where=notnan, out=Ee_gt_0)
np.equal(Ee, 0, where=notnan, out=Ee_eq_0)
Bvmpo = module['Bvmpo'] + module['Mbvmp']*(1 - Ee)
Bvoco = module['Bvoco'] + module['Mbvoc']*(1 - Ee)
delta = module['N'] * kb * (temp_cell + 273.15) / q
# avoid repeated computation
logEe = np.full_like(Ee, np.nan)
np.log(Ee, where=Ee_gt_0, out=logEe)
logEe = np.where(Ee_eq_0, -np.inf, logEe)
# avoid repeated __getitem__
cells_in_series = module['Cells_in_Series']
out = OrderedDict()
out['i_sc'] = (
module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - temp_ref)))
out['i_mp'] = (
module['Impo'] * (module['C0']*Ee + module['C1']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - temp_ref)))
out['v_oc'] = np.maximum(0, (
module['Voco'] + cells_in_series * delta * logEe +
Bvoco*(temp_cell - temp_ref)))
out['v_mp'] = np.maximum(0, (
module['Vmpo'] +
module['C2'] * cells_in_series * delta * logEe +
module['C3'] * cells_in_series * ((delta * logEe) ** 2) +
Bvmpo*(temp_cell - temp_ref)))
out['p_mp'] = out['i_mp'] * out['v_mp']
out['i_x'] = (
module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
(1 + module['Aisc']*(temp_cell - temp_ref)))
out['i_xx'] = (
module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - temp_ref)))
if isinstance(out['i_sc'], pd.Series):
out = pd.DataFrame(out)
return out
sapm_spectral_loss = deprecated(
since='0.10.0',
alternative='pvlib.spectrum.spectral_factor_sapm'
)(spectrum.spectral_factor_sapm)
[docs]
def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
module):
r"""
Calculates the SAPM effective irradiance using the SAPM spectral
loss and SAPM angle of incidence loss functions.
Parameters
----------
poa_direct : numeric
The direct irradiance incident upon the module. [W/m2]
poa_diffuse : numeric
The diffuse irradiance incident on module. [W/m2]
airmass_absolute : numeric
Absolute airmass. [unitless]
aoi : numeric
Angle of incidence. [degrees]
module : dict-like
A dict, Series, or DataFrame defining the SAPM performance
parameters. See the :py:func:`sapm` notes section for more
details.
Returns
-------
effective_irradiance : numeric
Effective irradiance accounting for reflections and spectral content.
[W/m2]
Notes
-----
The SAPM model for effective irradiance [1]_ translates broadband direct
and diffuse irradiance on the plane of array to the irradiance absorbed by
a module's cells.
The model is
.. math::
Ee = f_1(AM_a) (E_b f_2(AOI) + f_d E_d)
where :math:`Ee` is effective irradiance (W/m2), :math:`f_1` is a fourth
degree polynomial in air mass :math:`AM_a`, :math:`E_b` is beam (direct)
irradiance on the plane of array, :math:`E_d` is diffuse irradiance on the
plane of array, :math:`f_2` is a fifth degree polynomial in the angle of
incidence :math:`AOI`, and :math:`f_d` is the fraction of diffuse
irradiance on the plane of array that is not reflected away.
References
----------
.. [1] D. King et al, "Sandia Photovoltaic Array Performance Model",
SAND2004-3535, Sandia National Laboratories, Albuquerque, NM
See also
--------
pvlib.iam.sapm
pvlib.spectrum.spectral_factor_sapm
pvlib.pvsystem.sapm
"""
F1 = spectrum.spectral_factor_sapm(airmass_absolute, module)
F2 = iam.sapm(aoi, module)
Ee = F1 * (poa_direct * F2 + module['FD'] * poa_diffuse)
return Ee
[docs]
def singlediode(photocurrent, saturation_current, resistance_series,
resistance_shunt, nNsVth, method='lambertw'):
r"""
Solve the single diode equation to obtain a photovoltaic IV curve.
Solves the single diode equation [1]_
.. math::
I = I_L -
I_0 \left[
\exp \left(\frac{V+I R_s}{n N_s V_{th}} \right)-1
\right] -
\frac{V + I R_s}{R_{sh}}
for :math:`I` and :math:`V` when given :math:`I_L, I_0, R_s, R_{sh},` and
:math:`n N_s V_{th}` which are described later. The five points on the I-V
curve specified in [3]_ are returned. If :math:`I_L, I_0, R_s, R_{sh},` and
:math:`n N_s V_{th}` are all scalars, a single curve is returned. If any
are array-like (of the same length), multiple IV curves are calculated.
The input parameters can be calculated from meteorological data using a
function for a single diode model, e.g.,
:py:func:`~pvlib.pvsystem.calcparams_desoto`.
Parameters
----------
photocurrent : numeric
Light-generated current :math:`I_L` (photocurrent)
``0 <= photocurrent``. [A]
saturation_current : numeric
Diode saturation :math:`I_0` current under desired IV curve
conditions. ``0 < saturation_current``. [A]
resistance_series : numeric
Series resistance :math:`R_s` under desired IV curve conditions.
``0 <= resistance_series < numpy.inf``. [ohm]
resistance_shunt : numeric
Shunt resistance :math:`R_{sh}` under desired IV curve conditions.
``0 < resistance_shunt <= numpy.inf``. [ohm]
nNsVth : numeric
The product of three components: 1) the usual diode ideality factor
:math:`n`, 2) the number of cells in series :math:`N_s`, and 3)
the cell thermal voltage
:math:`V_{th}`. The thermal voltage of the cell (in volts) may be
calculated as :math:`k_B T_c / q`, where :math:`k_B` is
Boltzmann's constant (J/K), :math:`T_c` is the temperature of the p-n
junction in Kelvin, and :math:`q` is the charge of an electron
(coulombs). ``0 < nNsVth``. [V]
method : str, default 'lambertw'
Determines the method used to calculate points on the IV curve. The
options are ``'lambertw'``, ``'newton'``, or ``'brentq'``.
Returns
-------
dict or pandas.DataFrame
The returned dict-like object always contains the keys/columns:
* i_sc - short circuit current in amperes.
* v_oc - open circuit voltage in volts.
* i_mp - current at maximum power point in amperes.
* v_mp - voltage at maximum power point in volts.
* p_mp - power at maximum power point in watts.
* i_x - current, in amperes, at ``v = 0.5*v_oc``.
* i_xx - current, in amperes, at ``v = 0.5*(v_oc+v_mp)``.
A dict is returned when the input parameters are scalars.
See also
--------
calcparams_desoto
calcparams_cec
calcparams_pvsyst
sapm
pvlib.singlediode.bishop88
Notes
-----
If the method is ``'lambertw'`` then the solution employed to solve the
implicit diode equation utilizes the Lambert W function to obtain an
explicit function of :math:`V=f(I)` and :math:`I=f(V)` as shown in [2]_.
If the method is ``'newton'`` then the root-finding Newton-Raphson method
is used. It should be safe for well behaved IV-curves, but the ``'brentq'``
method is recommended for reliability.
If the method is ``'brentq'`` then Brent's bisection search method is used
that guarantees convergence by bounding the voltage between zero and
open-circuit.
References
----------
.. [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
0 86758 909 4
.. [2] A. Jain, A. Kapoor, "Exact analytical solutions of the
parameters of real solar cells using Lambert W-function", Solar
Energy Materials and Solar Cells, 81 (2004) 269-277.
.. [3] D. King et al, "Sandia Photovoltaic Array Performance Model",
SAND2004-3535, Sandia National Laboratories, Albuquerque, NM
.. [4] "Computer simulation of the effects of electrical mismatches in
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
https://doi.org/10.1016/0379-6787(88)90059-2
"""
args = (photocurrent, saturation_current, resistance_series,
resistance_shunt, nNsVth) # collect args
# Calculate points on the IV curve using the LambertW solution to the
# single diode equation
if method.lower() == 'lambertw':
out = _singlediode._lambertw(*args)
points = out[:7]
else:
# Calculate points on the IV curve using either 'newton' or 'brentq'
# methods. Voltages are determined by first solving the single diode
# equation for the diode voltage V_d then backing out voltage
v_oc = _singlediode.bishop88_v_from_i(
0.0, *args, method=method.lower()
)
i_mp, v_mp, p_mp = _singlediode.bishop88_mpp(
*args, method=method.lower()
)
i_sc = _singlediode.bishop88_i_from_v(
0.0, *args, method=method.lower()
)
i_x = _singlediode.bishop88_i_from_v(
v_oc / 2.0, *args, method=method.lower()
)
i_xx = _singlediode.bishop88_i_from_v(
(v_oc + v_mp) / 2.0, *args, method=method.lower()
)
points = i_sc, v_oc, i_mp, v_mp, p_mp, i_x, i_xx
columns = ('i_sc', 'v_oc', 'i_mp', 'v_mp', 'p_mp', 'i_x', 'i_xx')
if all(map(np.isscalar, args)):
out = {c: p for c, p in zip(columns, points)}
return out
points = np.atleast_1d(*points) # convert scalars to 1d-arrays
points = np.vstack(points).T # collect rows into DataFrame columns
# save the first available pd.Series index, otherwise set to None
index = next((a.index for a in args if isinstance(a, pd.Series)), None)
out = pd.DataFrame(points, columns=columns, index=index)
return out
[docs]
def max_power_point(photocurrent, saturation_current, resistance_series,
resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.inf,
method='brentq'):
"""
Given the single diode equation coefficients, calculates the maximum power
point (MPP).
Parameters
----------
photocurrent : numeric
photo-generated current [A]
saturation_current : numeric
diode reverse saturation current [A]
resistance_series : numeric
series resitance [ohms]
resistance_shunt : numeric
shunt resitance [ohms]
nNsVth : numeric
product of thermal voltage ``Vth`` [V], diode ideality factor ``n``,
and number of serices cells ``Ns``
d2mutau : numeric, default 0
PVsyst parameter for cadmium-telluride (CdTe) and amorphous-silicon
(a-Si) modules that accounts for recombination current in the
intrinsic layer. The value is the ratio of intrinsic layer thickness
squared :math:`d^2` to the diffusion length of charge carriers
:math:`\\mu \\tau`. [V]
NsVbi : numeric, default np.inf
PVsyst parameter for cadmium-telluride (CdTe) and amorphous-silicon
(a-Si) modules that is the product of the PV module number of series
cells ``Ns`` and the builtin voltage ``Vbi`` of the intrinsic layer.
[V].
method : str
either ``'newton'`` or ``'brentq'``
Returns
-------
OrderedDict or pandas.DataFrame
``(i_mp, v_mp, p_mp)``
Notes
-----
Use this function when you only want to find the maximum power point. Use
:func:`singlediode` when you need to find additional points on the IV
curve. This function uses Brent's method by default because it is
guaranteed to converge.
"""
i_mp, v_mp, p_mp = _singlediode.bishop88_mpp(
photocurrent, saturation_current, resistance_series,
resistance_shunt, nNsVth, d2mutau, NsVbi, method=method.lower()
)
if isinstance(photocurrent, pd.Series):
ivp = {'i_mp': i_mp, 'v_mp': v_mp, 'p_mp': p_mp}
out = pd.DataFrame(ivp, index=photocurrent.index)
else:
out = OrderedDict()
out['i_mp'] = i_mp
out['v_mp'] = v_mp
out['p_mp'] = p_mp
return out
[docs]
def v_from_i(current, photocurrent, saturation_current, resistance_series,
resistance_shunt, nNsVth, method='lambertw'):
'''
Device voltage at the given device current for the single diode model.
Uses the single diode model (SDM) as described in, e.g.,
Jain and Kapoor 2004 [1]_.
The solution is per Eq 3 of [1]_ except when resistance_shunt=numpy.inf,
in which case the explict solution for voltage is used.
Ideal device parameters are specified by resistance_shunt=np.inf and
resistance_series=0.
Inputs to this function can include scalars and pandas.Series, but it is
the caller's responsibility to ensure that the arguments are all float64
and within the proper ranges.
.. versionchanged:: 0.10.0
The function's arguments have been reordered.
Parameters
----------
current : numeric
The current in amperes under desired IV curve conditions.
photocurrent : numeric
Light-generated current (photocurrent) in amperes under desired
IV curve conditions. Often abbreviated ``I_L``.
0 <= photocurrent
saturation_current : numeric
Diode saturation current in amperes under desired IV curve
conditions. Often abbreviated ``I_0``.
0 < saturation_current
resistance_series : numeric
Series resistance in ohms under desired IV curve conditions.
Often abbreviated ``Rs``.
0 <= resistance_series < numpy.inf
resistance_shunt : numeric
Shunt resistance in ohms under desired IV curve conditions.
Often abbreviated ``Rsh``.
0 < resistance_shunt <= numpy.inf
nNsVth : numeric
The product of three components. 1) The usual diode ideal factor
(n), 2) the number of cells in series (Ns), and 3) the cell
thermal voltage under the desired IV curve conditions (Vth). The
thermal voltage of the cell (in volts) may be calculated as
``k*temp_cell/q``, where k is Boltzmann's constant (J/K),
temp_cell is the temperature of the p-n junction in Kelvin, and
q is the charge of an electron (coulombs).
0 < nNsVth
method : str
Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*:
``'brentq'`` is limited to 1st quadrant only.
Returns
-------
current : np.ndarray or scalar
References
----------
.. [1] A. Jain, A. Kapoor, "Exact analytical solutions of the
parameters of real solar cells using Lambert W-function", Solar
Energy Materials and Solar Cells, 81 (2004) 269-277.
'''
args = (current, photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth)
if method.lower() == 'lambertw':
return _singlediode._lambertw_v_from_i(*args)
else:
# Calculate points on the IV curve using either 'newton' or 'brentq'
# methods. Voltages are determined by first solving the single diode
# equation for the diode voltage V_d then backing out voltage
V = _singlediode.bishop88_v_from_i(*args, method=method.lower())
if all(map(np.isscalar, args)):
return V
shape = _singlediode._shape_of_max_size(*args)
return np.broadcast_to(V, shape)
[docs]
def i_from_v(voltage, photocurrent, saturation_current, resistance_series,
resistance_shunt, nNsVth, method='lambertw'):
'''
Device current at the given device voltage for the single diode model.
Uses the single diode model (SDM) as described in, e.g.,
Jain and Kapoor 2004 [1]_.
The solution is per Eq 2 of [1] except when resistance_series=0,
in which case the explict solution for current is used.
Ideal device parameters are specified by resistance_shunt=np.inf and
resistance_series=0.
Inputs to this function can include scalars and pandas.Series, but it is
the caller's responsibility to ensure that the arguments are all float64
and within the proper ranges.
.. versionchanged:: 0.10.0
The function's arguments have been reordered.
Parameters
----------
voltage : numeric
The voltage in Volts under desired IV curve conditions.
photocurrent : numeric
Light-generated current (photocurrent) in amperes under desired
IV curve conditions. Often abbreviated ``I_L``.
0 <= photocurrent
saturation_current : numeric
Diode saturation current in amperes under desired IV curve
conditions. Often abbreviated ``I_0``.
0 < saturation_current
resistance_series : numeric
Series resistance in ohms under desired IV curve conditions.
Often abbreviated ``Rs``.
0 <= resistance_series < numpy.inf
resistance_shunt : numeric
Shunt resistance in ohms under desired IV curve conditions.
Often abbreviated ``Rsh``.
0 < resistance_shunt <= numpy.inf
nNsVth : numeric
The product of three components. 1) The usual diode ideal factor
(n), 2) the number of cells in series (Ns), and 3) the cell
thermal voltage under the desired IV curve conditions (Vth). The
thermal voltage of the cell (in volts) may be calculated as
``k*temp_cell/q``, where k is Boltzmann's constant (J/K),
temp_cell is the temperature of the p-n junction in Kelvin, and
q is the charge of an electron (coulombs).
0 < nNsVth
method : str
Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*:
``'brentq'`` is limited to 1st quadrant only.
Returns
-------
current : np.ndarray or scalar
References
----------
.. [1] A. Jain, A. Kapoor, "Exact analytical solutions of the
parameters of real solar cells using Lambert W-function", Solar
Energy Materials and Solar Cells, 81 (2004) 269-277.
'''
args = (voltage, photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth)
if method.lower() == 'lambertw':
return _singlediode._lambertw_i_from_v(*args)
else:
# Calculate points on the IV curve using either 'newton' or 'brentq'
# methods. Voltages are determined by first solving the single diode
# equation for the diode voltage V_d then backing out voltage
current = _singlediode.bishop88_i_from_v(*args, method=method.lower())
if all(map(np.isscalar, args)):
return current
shape = _singlediode._shape_of_max_size(*args)
return np.broadcast_to(current, shape)
[docs]
def scale_voltage_current_power(data, voltage=1, current=1):
"""
Scales the voltage, current, and power in data by the voltage
and current factors.
Parameters
----------
data: DataFrame
May contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx',
'i_sc', 'p_mp'`.
voltage: numeric, default 1
The amount by which to multiply the voltages.
current: numeric, default 1
The amount by which to multiply the currents.
Returns
-------
scaled_data: DataFrame
A scaled copy of the input data.
`'p_mp'` is scaled by `voltage * current`.
"""
# as written, only works with a DataFrame
# could make it work with a dict, but it would be more verbose
voltage_keys = ['v_mp', 'v_oc']
current_keys = ['i_mp', 'i_x', 'i_xx', 'i_sc']
power_keys = ['p_mp']
voltage_df = data.filter(voltage_keys, axis=1) * voltage
current_df = data.filter(current_keys, axis=1) * current
power_df = data.filter(power_keys, axis=1) * voltage * current
df = pd.concat([voltage_df, current_df, power_df], axis=1)
df_sorted = df[data.columns] # retain original column order
return df_sorted
[docs]
def pvwatts_dc(g_poa_effective, temp_cell, pdc0, gamma_pdc, temp_ref=25.):
r"""
Implements NREL's PVWatts DC power model. The PVWatts DC model [1]_ is:
.. math::
P_{dc} = \frac{G_{poa eff}}{1000} P_{dc0} ( 1 + \gamma_{pdc} (T_{cell} - T_{ref}))
Note that ``pdc0`` is also used as a symbol in
:py:func:`pvlib.inverter.pvwatts`. ``pdc0`` in this function refers to the DC
power of the modules at reference conditions. ``pdc0`` in
:py:func:`pvlib.inverter.pvwatts` refers to the DC power input limit of
the inverter.
Parameters
----------
g_poa_effective: numeric
Irradiance transmitted to the PV cells. To be
fully consistent with PVWatts, the user must have already
applied angle of incidence losses, but not soiling, spectral,
etc. [W/m^2]
temp_cell: numeric
Cell temperature [C].
pdc0: numeric
Power of the modules at 1000 W/m^2 and cell reference temperature. [W]
gamma_pdc: numeric
The temperature coefficient of power. Typically -0.002 to
-0.005 per degree C. [1/C]
temp_ref: numeric, default 25.0
Cell reference temperature. PVWatts defines it to be 25 C and
is included here for flexibility. [C]
Returns
-------
pdc: numeric
DC power. [W]
References
----------
.. [1] A. P. Dobos, "PVWatts Version 5 Manual"
http://pvwatts.nrel.gov/downloads/pvwattsv5.pdf
(2014).
""" # noqa: E501
pdc = (g_poa_effective * 0.001 * pdc0 *
(1 + gamma_pdc * (temp_cell - temp_ref)))
return pdc
[docs]
def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2,
connections=0.5, lid=1.5, nameplate_rating=1, age=0,
availability=3):
r"""
Implements NREL's PVWatts system loss model.
The PVWatts loss model [1]_ is:
.. math::
L_{total}(\%) = 100 [ 1 - \Pi_i ( 1 - \frac{L_i}{100} ) ]
All parameters must be in units of %. Parameters may be
array-like, though all array sizes must match.
Parameters
----------
soiling: numeric, default 2
shading: numeric, default 3
snow: numeric, default 0
mismatch: numeric, default 2
wiring: numeric, default 2
connections: numeric, default 0.5
lid: numeric, default 1.5
Light induced degradation
nameplate_rating: numeric, default 1
age: numeric, default 0
availability: numeric, default 3
Returns
-------
losses: numeric
System losses in units of %.
References
----------
.. [1] A. P. Dobos, "PVWatts Version 5 Manual"
http://pvwatts.nrel.gov/downloads/pvwattsv5.pdf
(2014).
"""
params = [soiling, shading, snow, mismatch, wiring, connections, lid,
nameplate_rating, age, availability]
# manually looping over params allows for numpy/pandas to handle any
# array-like broadcasting that might be necessary.
perf = 1
for param in params:
perf *= 1 - param/100
losses = (1 - perf) * 100.
return losses
[docs]
def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent,
modules_per_string=1,
strings=1):
r"""
Calculate the equivalent resistance of the conductors from the percent
ohmic loss of an array at reference conditions.
Parameters
----------
vmp_ref: numeric
Maximum power voltage of one module at reference conditions. [V]
imp_ref: numeric
Maximum power current of one module at reference conditions. [A]
dc_ohmic_percent: numeric
Array DC power loss as a percent of DC power loss at reference
conditions. In percent, e.g. 1.5% loss is input as 1.5.
modules_per_string: int, default 1
Number of series-connected modules per string in the array.
strings: int, default 1
Number of parallel strings in the array.
Returns
----------
Rw: numeric
Equivalent resistance. [ohm]
See Also
--------
pvlib.pvsystem.dc_ohmic_losses
Notes
-----
Equivalent resistance is calculated as:
.. math::
R_w = \left(\frac{L_{stc}}{100}\right) \times \left(\frac{
V_{array}}{I_{array}}\right)
:math:`R_w` is the equivalent resistance in ohms.
:math:`V_{array}` is the array voltage, equal to ``vmp_ref`` times
``modules_per_string``.
:math:`I_{array}` is the array current, equal to ``imp_ref`` times
``strings``.
:math:`L_{stc}` is the input DC loss percent at reference conditions.
"""
vmp = modules_per_string * vmp_ref
imp = strings * imp_ref
Rw = (dc_ohmic_percent / 100) * (vmp / imp)
return Rw
[docs]
def dc_ohmic_losses(resistance, current):
r"""
Returns ohmic losses in units of power from the equivalent
resistance of the wires and the operating current.
Parameters
----------
resistance: numeric
Equivalent resistance of wires. [ohm]
current: numeric, float or array-like
Operating current. [A]
Returns
----------
loss: numeric
Power loss. [W]
See Also
--------
pvlib.pvsystem.dc_ohms_from_percent
Notes
-----
Ohmic (also termed joule or heat) loss is the power lost due to current
flowing through a conductor. Ohmic loss, :math:`L`, is computed as
.. math::
L = I^2 \times R
where :math:`I` is the current (A) and :math:`R` is the resistance of the
conductor (ohms).
"""
return resistance * current * current
[docs]
def combine_loss_factors(index, *losses, fill_method='ffill'):
r"""
Combines Series loss fractions while setting a common index.
The separate losses are compounded using the following equation:
.. math::
L_{total} = 1 - [ 1 - \Pi_i ( 1 - L_i ) ]
:math:`L_{total}` is the total loss returned
:math:`L_i` is each individual loss factor input
Note the losses must each be a series with a DatetimeIndex.
All losses will be resampled to match the index parameter using
the fill method specified (defaults to "fill forward").
Parameters
----------
index : DatetimeIndex
The index of the returned loss factors
*losses : Series
One or more Series of fractions to be compounded
fill_method : {'ffill', 'bfill', 'nearest'}, default 'ffill'
Method to use for filling holes in reindexed DataFrame
Returns
-------
Series
Fractions resulting from the combination of each loss factor
"""
combined_factor = 1
for loss in losses:
loss = loss.reindex(index, method=fill_method)
combined_factor *= (1 - loss)
return 1 - combined_factor