Skip to content

Add N. Martin & J. M. Ruiz spectral response mismatch modifiers model #1658

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a7d0070
First prototype
echedey-ls Nov 10, 2022
60a63fd
Allow for custom irradiations in 'model_parameters'
echedey-ls Nov 13, 2022
93f1038
Atomize tests with a fixture
echedey-ls Nov 24, 2022
f606cd8
Comply with 'optional' instead of 'default None'
echedey-ls Nov 25, 2022
f79628a
Update docstring
echedey-ls Nov 26, 2022
a7b47b4
Update docstring (2)
echedey-ls Nov 26, 2022
b9ec506
Update comments
echedey-ls Nov 28, 2022
3a8630e
Change return & model_parameters behaviour
echedey-ls Dec 2, 2022
025c76e
Minor typos in docstrings
echedey-ls Feb 3, 2023
f0ea498
Squashed commit of the following:
echedey-ls Feb 3, 2023
8d4442e
Typo
echedey-ls Feb 3, 2023
399312b
Allow easier multiplication of modifiers and irradiances
echedey-ls Feb 3, 2023
f24aa92
Show few days on example
echedey-ls Feb 6, 2023
c26b865
Cant live without line length limit
echedey-ls Feb 6, 2023
900ed98
Rename notebook and specify spectral mismatch modifiers
echedey-ls Feb 7, 2023
eb1b245
Delete plot_martin_ruiz_spectral_modifier_figs4to6.py
echedey-ls Feb 7, 2023
149c557
Little changes to docs and var names
echedey-ls Feb 7, 2023
2bf9d5b
Clean up & minor upgrades to notebook
echedey-ls Feb 8, 2023
be9554f
Add output of notebook
echedey-ls Feb 8, 2023
10ff9c2
Small error in docsting
echedey-ls Feb 8, 2023
6a2dbf0
docstring: Apply suggestions from code review
echedey-ls Feb 9, 2023
3d5cceb
Exceptions: Delete exclamation marks as per code review
echedey-ls Feb 9, 2023
b16eaa6
docstring: update per code review (II)
echedey-ls Feb 9, 2023
f5352ac
code review
echedey-ls Feb 15, 2023
d66f16e
code review
echedey-ls Feb 15, 2023
db48a76
Add to reference and add whatsnew entry
echedey-ls Feb 15, 2023
e7011ef
docstring: forgot to update raises
echedey-ls Feb 16, 2023
30c66b9
Add sphinx example and delete notebook
echedey-ls Feb 19, 2023
c04b70d
example: code review
echedey-ls Feb 20, 2023
183320f
example: code review (II)
echedey-ls Feb 20, 2023
8730fcd
docstring: update per PR #1628
echedey-ls Feb 20, 2023
7ced74b
sapm_effective_irradiance: docstring rst typo
echedey-ls Feb 22, 2023
6385897
docstring: add math directives to parameters
echedey-ls Feb 22, 2023
108def0
example: add comparison with other models
echedey-ls Feb 24, 2023
d75eea0
code review: func model, tests & example
echedey-ls Feb 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions docs/examples/spectrum/plot_martin_ruiz_mismatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
N. Martin & J. M. Ruiz Spectral Mismatch Modifier
=================================================

How to use this correction factor to adjust the POA global irradiance.
"""

# %%
# Effectiveness of a material to convert incident sunlight to current depends
# on the incident light wavelength. During the day, the spectral distribution
# of the incident irradiance varies from the standard testing spectra,
# introducing a small difference between the expected and the real output.
# In [1]_, N. Martín and J. M. Ruiz propose 3 mismatch factors, one for each
# irradiance component. These mismatch modifiers are calculated with the help
# of the airmass, the clearness index and three experimental fitting
# parameters. In the same paper, these parameters have been obtained for m-Si,
# p-Si and a-Si modules.
# With :py:func:`pvlib.spectrum.martin_ruiz` we are able to make use of these
# already computed values or provide ours.
#
# References
# ----------
# .. [1] Martín, N. and Ruiz, J.M. (1999), A new method for the spectral
# characterisation of PV modules. Prog. Photovolt: Res. Appl., 7: 299-310.
# :doi:`10.1002/(SICI)1099-159X(199907/08)7:4<299::AID-PIP260>3.0.CO;2-0`
#
# Calculating the incident and modified global irradiance
# -------------------------------------------------------
#
# This mismatch modifier is applied to the irradiance components, so first
# step is to get them. We define an hypothetical POA surface and use a TMY to
# compute them from a TMY.

import matplotlib.pyplot as plt
from pvlib import spectrum, irradiance, iotools, location

surface_tilt = 40
surface_azimuth = 180 # Pointing South
# We will need some location to start with & the TMY
site = location.Location(40.4534, -3.7270, altitude=664,
name='IES-UPM, Madrid')

pvgis_data, _, _, _ = iotools.get_pvgis_tmy(site.latitude, site.longitude,
map_variables=True,
startyear=2005, endyear=2015)
# Coerce a year: above function returns typical months of different years
pvgis_data.index = [ts.replace(year=2022) for ts in pvgis_data.index]
# Select days to show
weather_data = pvgis_data['2022-10-03':'2022-10-07']

# Then calculate all we need to get the irradiance components
solar_pos = site.get_solarposition(weather_data.index)

extra_rad = irradiance.get_extra_radiation(weather_data.index)

poa_sky_diffuse = irradiance.haydavies(surface_tilt, surface_azimuth,
weather_data['dhi'],
weather_data['dni'],
extra_rad,
solar_pos['apparent_zenith'],
solar_pos['azimuth'])

poa_ground_diffuse = irradiance.get_ground_diffuse(surface_tilt,
weather_data['ghi'])

aoi = irradiance.aoi(surface_tilt, surface_azimuth,
solar_pos['apparent_zenith'], solar_pos['azimuth'])

# %%
# Let's consider this the irradiance components without spectral modifiers.
# We can calculate the mismatch before and then create a "poa_irrad" var for
# modified components directly, but we want to show the output difference.
# Also, note that :py:func:`pvlib.spectrum.martin_ruiz` result is designed to
# make it easy to multiply each modifier and the irradiance component with a
# single line of code, if you get this dataframe before.

poa_irrad = irradiance.poa_components(aoi, weather_data['dni'],
poa_sky_diffuse, poa_ground_diffuse)

# %%
# Here comes the modifier. Let's calculate it with the airmass and clearness
# index.
# First, let's find the airmass and the clearness index.
# Little caution: default values for this model were fitted obtaining the
# airmass through the `'kasten1966'` method, which is not used by default.

airmass = site.get_airmass(solar_position=solar_pos, model='kasten1966')
clearness_index = irradiance.clearness_index(weather_data['ghi'],
solar_pos['zenith'], extra_rad)

# Get the spectral mismatch modifiers
spectral_modifiers = spectrum.martin_ruiz(clearness_index,
airmass['airmass_absolute'],
module_type='monosi')

# %%
# And then we can find the 3 modified components of the POA irradiance
# by means of a simple multiplication.
# Note, however, that neither this does modify ``poa_global`` nor
# ``poa_diffuse``, so we should update the dataframe afterwards.

poa_irrad_modified = poa_irrad * spectral_modifiers

# We want global modified irradiance
poa_irrad_modified['poa_global'] = (poa_irrad_modified['poa_direct']
+ poa_irrad_modified['poa_sky_diffuse']
+ poa_irrad_modified['poa_ground_diffuse'])
# Don't forget to update `'poa_diffuse'` if you want to use it
# poa_irrad_modified['poa_diffuse'] = \
# (poa_irrad_modified['poa_sky_diffuse']
# + poa_irrad_modified['poa_ground_diffuse'])

# %%
# Finally, let's plot the incident vs modified global irradiance, and their
# difference.

poa_irrad_global_diff = (poa_irrad['poa_global']
- poa_irrad_modified['poa_global'])
poa_irrad['poa_global'].plot()
poa_irrad_modified['poa_global'].plot()
poa_irrad_global_diff.plot()
plt.legend(['Incident', 'Modified', 'Difference'])
plt.ylabel('POA Global irradiance [W/m²]')
plt.show()
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Spectrum
spectrum.get_example_spectral_response
spectrum.get_am15g
spectrum.calc_spectral_mismatch_field
spectrum.martin_ruiz
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Enhancements
:py:func:`pvlib.snow.loss_townsend` (:issue:`1636`, :pull:`1653`)
* Added optional ``n_ar`` parameter to :py:func:`pvlib.iam.physical` to
support an anti-reflective coating. (:issue:`1501`, :pull:`1616`)
* Added :py:func:`pvlib.spectrum.martin_ruiz`, a spectral
mismatch correction factor for POA components, dependant in module type,
airmass and clearness index (:pull:`1658`)

Bug fixes
~~~~~~~~~
Expand Down Expand Up @@ -64,3 +67,4 @@ Contributors
* Mark Mikofski (:ghuser:`mikofski`)
* Anton Driesse (:ghuser:`adriesse`)
* Michael Deceglie (:ghuser:`mdeceglie`)
* Echedey Luis (:ghuser:`echedey-ls`)
2 changes: 1 addition & 1 deletion pvlib/atmosphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def get_relative_airmass(zenith, model='kastenyoung1989'):
Available models include the following:

* 'simple' - secant(apparent zenith angle) -
Note that this gives -Inf at zenith=90
Note that this gives +Inf at zenith=90
* 'kasten1966' - See reference [1] -
requires apparent sun zenith
* 'youngirvine1967' - See reference [2] -
Expand Down
2 changes: 1 addition & 1 deletion pvlib/ivtools/sdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def fit_cec_sam(celltype, v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc,

Notes
-----
The CEC model and estimation method are described in [1]_.
The CEC model and estimation method are described in [1]_.
Inputs ``v_mp``, ``i_mp``, ``v_oc`` and ``i_sc`` are assumed to be from a
single IV curve at constant irradiance and cell temperature. Irradiance is
not explicitly used by the fitting procedure. The irradiance level at which
Expand Down
4 changes: 2 additions & 2 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -2497,8 +2497,8 @@ def sapm(effective_irradiance, temp_cell, module):
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)
Voco Open circuit voltage at reference condition (volts)
Vmpo Maximum power voltage at reference condition (volts)
Aisc Short circuit current temperature coefficient at
reference condition (1/C)
Aimp Maximum power current temperature coefficient at
Expand Down
3 changes: 2 additions & 1 deletion pvlib/spectrum/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pvlib.spectrum.spectrl2 import spectrl2 # noqa: F401
from pvlib.spectrum.mismatch import (get_example_spectral_response, get_am15g,
calc_spectral_mismatch_field)
calc_spectral_mismatch_field,
martin_ruiz)
157 changes: 157 additions & 0 deletions pvlib/spectrum/mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,160 @@ def integrate(e):
smm = pd.Series(smm, index=e_sun.index)

return smm


def martin_ruiz(clearness_index, airmass_absolute, module_type=None,
model_parameters=None):
r"""
Calculate spectral mismatch modifiers for POA direct, sky diffuse and
ground diffuse irradiances using the clearness index and the absolute
airmass.

.. warning::
Included model parameters for ``monosi``, ``polysi`` and ``asi`` were
estimated using the airmass model ``kasten1966`` [1]_. It is heavily
recommended to use the same model in order to not introduce errors.
See :py:func:`~pvlib.atmosphere.get_relative_airmass`.

Parameters
----------
clearness_index : numeric
Clearness index of the sky.

airmass_absolute : numeric
Absolute airmass. Give attention to algorithm used (``kasten1966`` is
recommended for default parameters of ``monosi``, ``polysi`` and
``asi``, see [1]_).
Copy link
Member

@adriesse adriesse Feb 18, 2023

Choose a reason for hiding this comment

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

If the model requires a specific air mass calculation, then perhaps it is better to have zenith angle as input.

This comment applies to a number of other existing pvlib functions as well, actually.


module_type : string, optional
Specifies material of the cell in order to infer model parameters.
Allowed types are ``monosi``, ``polysi`` and ``asi``, either lower or
upper case. If not specified, ``model_parameters`` must be provided.

model_parameters : dict-like, optional
Provide either a dict or a ``pd.DataFrame`` as follows:

.. code-block:: python

# Using a dict
# Return keys are the same as specifying 'module_type'
model_parameters = {
'poa_direct': {'c': c1, 'a': a1, 'b': b1},
'poa_sky_diffuse': {'c': c2, 'a': a2, 'b': b2},
'poa_ground_diffuse': {'c': c3, 'a': a3, 'b': b3}
}
# Using a pd.DataFrame
model_parameters = pd.DataFrame({
'poa_direct': [c1, a1, b1],
'poa_sky_diffuse': [c2, a2, b2],
'poa_ground_diffuse': [c3, a3, b3]},
index=('c', 'a', 'b'))

``c``, ``a`` and ``b`` must be scalar.

Unspecified parameters for an irradiance component (`'poa_direct'`,
`'poa_sky_diffuse'`, or `'poa_ground_diffuse'`) will cause ``np.nan``
to be returned in the corresponding result.

Returns
-------
Modifiers : pd.DataFrame (iterable input) or dict (scalar input) of numeric
Mismatch modifiers for direct, sky diffuse and ground diffuse
irradiances, with indexes `'poa_direct'`, `'poa_sky_diffuse'`,
`'poa_ground_diffuse'`.
Each mismatch modifier should be multiplied by its corresponding
POA component.

Raises
------
ValueError
If ``model_parameters`` is not suitable. See examples given above.
TypeError
If neither ``module_type`` nor ``model_parameters`` are given.
TypeError
If both ``module_type`` and ``model_parameters`` are provided.
NotImplementedError
If ``module_type`` is not found in internal table of parameters.

Notes
-----
The mismatch modifier is defined as

.. math:: M = c \cdot \exp( a \cdot (K_t - 0.74) + b \cdot (AM - 1.5) )

where ``c``, ``a`` and ``b`` are the model parameters, different for each
irradiance component.

References
----------
.. [1] Martín, N. and Ruiz, J.M. (1999), A new method for the spectral
characterisation of PV modules. Prog. Photovolt: Res. Appl., 7: 299-310.
:doi:`10.1002/(SICI)1099-159X(199907/08)7:4<299::AID-PIP260>3.0.CO;2-0`

See Also
--------
pvlib.irradiance.clearness_index
pvlib.atmosphere.get_relative_airmass
pvlib.atmosphere.get_absolute_airmass
pvlib.atmosphere.first_solar_spectral_correction
"""
# Note tests for this function are prefixed with test_martin_ruiz_mm_*

IRRAD_COMPONENTS = ('poa_direct', 'poa_sky_diffuse', 'poa_ground_diffuse')
# Fitting parameters directly from [1]_
MARTIN_RUIZ_PARAMS = pd.DataFrame(
index=('monosi', 'polysi', 'asi'),
columns=pd.MultiIndex.from_product([IRRAD_COMPONENTS,
('c', 'a', 'b')]),
data=[ # Direct(c,a,b) | Sky diffuse(c,a,b) | Ground diffuse(c,a,b)
[1.029, -.313, 524e-5, .764, -.882, -.0204, .970, -.244, .0129],
[1.029, -.311, 626e-5, .764, -.929, -.0192, .970, -.270, .0158],
[1.024, -.222, 920e-5, .840, -.728, -.0183, .989, -.219, .0179],
])

# Argument validation and choose components and model parameters
Copy link
Member

Choose a reason for hiding this comment

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

Most functions in pvlib have less friendly input checking, which may be good or bad. You might just see what happens in each case without these checks and see whether the message python produces upon failure is comprehensible. In that case you may not need some of these custom messages.

Other than that I would prefer two nested if/else structures over the four combined conditions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have tried what you say, but I don't get nice results - when both are provided, one of the inputs is ignored without telling so; and when neither of them are, _params is not defined is raised, which could be a good error message.
In my humble opinion, being verbatim in the interface is not a problem, and is better than seeing a variable you did not write is not being defined. It will save time for the user, essentially.

Copy link
Member

Choose a reason for hiding this comment

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

numpy and scipy use the None, None default pattern, where if both are None the function fails, so I think we're among good company.

if module_type is not None and model_parameters is None:
# Infer parameters from cell material
module_type_lower = module_type.lower()
if module_type_lower in MARTIN_RUIZ_PARAMS.index:
_params = MARTIN_RUIZ_PARAMS.loc[module_type_lower]
else:
raise NotImplementedError('Cell type parameters not defined in '
'algorithm. Allowed types are '
f'{tuple(MARTIN_RUIZ_PARAMS.index)}')
elif model_parameters is not None and module_type is None:
# Use user-defined model parameters
# Validate 'model_parameters' sub-dicts keys
if any([{'a', 'b', 'c'} != set(model_parameters[component].keys())
for component in model_parameters.keys()]):
raise ValueError("You must specify model parameters with keys "
"'a','b','c' for each irradiation component.")
_params = model_parameters
elif module_type is None and model_parameters is None:
raise TypeError('You must pass at least "module_type" '
'or "model_parameters" as arguments.')
elif model_parameters is not None and module_type is not None:
raise TypeError('Cannot resolve input: must supply only one of '
'"module_type" or "model_parameters"')

if np.isscalar(clearness_index) and np.isscalar(airmass_absolute):
modifiers = dict(zip(IRRAD_COMPONENTS, (np.nan,)*3))
else:
modifiers = pd.DataFrame(columns=IRRAD_COMPONENTS)

# Compute difference here to avoid recalculating inside loop
kt_delta = clearness_index - 0.74
am_delta = airmass_absolute - 1.5

# Calculate mismatch modifier for each irradiation
for irrad_type in IRRAD_COMPONENTS:
# Skip irradiations not specified in 'model_params'
if irrad_type not in _params.keys():
continue
# Else, calculate the mismatch modifier
_coeffs = _params[irrad_type]
modifier = _coeffs['c'] * np.exp(_coeffs['a'] * kt_delta
+ _coeffs['b'] * am_delta)
modifiers[irrad_type] = modifier

return modifiers
Loading