-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from 28 commits
a7d0070
60a63fd
93f1038
f606cd8
f79628a
a7b47b4
b9ec506
3a8630e
025c76e
f0ea498
8d4442e
399312b
f24aa92
c26b865
900ed98
eb1b245
149c557
2bf9d5b
be9554f
10ff9c2
6a2dbf0
3d5cceb
b16eaa6
f5352ac
d66f16e
db48a76
e7011ef
30c66b9
c04b70d
183320f
8730fcd
7ced74b
6385897
108def0
d75eea0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
echedey-ls marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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. | ||
echedey-ls marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # 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 | ||
echedey-ls marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # 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 | ||
echedey-ls marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # ``poa_diffuse``, so we should update the dataframe afterwards. | ||
|
|
||
| poa_irrad_modified = poa_irrad * spectral_modifiers | ||
echedey-ls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # 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 |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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`. | ||
echedey-ls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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]_). | ||
|
||
|
|
||
| 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 | ||
echedey-ls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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. | ||
|
|
||
echedey-ls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.') | ||
echedey-ls marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| elif model_parameters is not None and module_type is not None: | ||
| raise TypeError('Cannot resolve input: must supply only one of ' | ||
echedey-ls marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| '"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 | ||
Uh oh!
There was an error while loading. Please reload this page.