diff --git a/docs/examples/spectrum/plot_martin_ruiz_mismatch.py b/docs/examples/spectrum/plot_martin_ruiz_mismatch.py new file mode 100644 index 0000000000..6da7a4bf7d --- /dev/null +++ b/docs/examples/spectrum/plot_martin_ruiz_mismatch.py @@ -0,0 +1,219 @@ +""" +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 +# ------------------------------------------------------- +# +# Mismatch modifiers are applied to the irradiance components, so first +# step is to get them. We define an hypothetical POA surface and use TMY to +# compute sky diffuse, ground reflected and direct irradiance. + +import os + +import matplotlib.pyplot as plt +import pvlib +from scipy import stats +import pandas as pd + +surface_tilt = 40 +surface_azimuth = 180 # Pointing South + +# Get TMY data & create location +datapath = os.path.join(pvlib.__path__[0], 'data', + 'tmy_45.000_8.000_2005_2016.csv') +pvgis_data, _, metadata, _ = pvlib.iotools.read_pvgis_tmy(datapath, + map_variables=True) +site = pvlib.location.Location(metadata['latitude'], metadata['longitude'], + altitude=metadata['elevation']) + +# 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-09-03':'2022-09-06'] + +# Then calculate all we need to get the irradiance components +solar_pos = site.get_solarposition(weather_data.index) + +extra_rad = pvlib.irradiance.get_extra_radiation(weather_data.index) + +poa_sky_diffuse = pvlib.irradiance.haydavies(surface_tilt, surface_azimuth, + weather_data['dhi'], + weather_data['dni'], + extra_rad, + solar_pos['apparent_zenith'], + solar_pos['azimuth']) + +poa_ground_diffuse = pvlib.irradiance.get_ground_diffuse(surface_tilt, + weather_data['ghi']) + +aoi = pvlib.irradiance.aoi(surface_tilt, surface_azimuth, + solar_pos['apparent_zenith'], solar_pos['azimuth']) + +# Get dataframe with all components and global (includes 'poa_direct') +poa_irrad = pvlib.irradiance.poa_components(aoi, weather_data['dni'], + poa_sky_diffuse, + poa_ground_diffuse) + +# %% +# Here come the modifiers. Let's calculate them 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') +airmass_absolute = airmass['airmass_absolute'] # We only use absolute airmass +clearness_index = pvlib.irradiance.clearness_index(weather_data['ghi'], + solar_pos['zenith'], + extra_rad) + +# Get the spectral mismatch modifiers +spectral_modifiers = pvlib.spectrum.martin_ruiz(clearness_index, + 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 this does not modify ``poa_global`` nor +# ``poa_diffuse``, so we should update the dataframe afterwards. + +poa_irrad_modified = poa_irrad * spectral_modifiers +# Above line is equivalent to: +# poa_irrad_modified = pd.DataFrame() +# for component in ('poa_direct', 'poa_sky_diffuse', 'poa_ground_diffuse'): +# poa_irrad_modified[component] = (poa_irrad[component] +# * spectral_modifiers[component]) + +# 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']) +plt.figure() +datetimes = poa_irrad.index # common to poa_irrad_* +plt.plot(datetimes, poa_irrad['poa_global'].to_numpy()) +plt.plot(datetimes, poa_irrad_modified['poa_global'].to_numpy()) +plt.plot(datetimes, poa_irrad_global_diff.to_numpy()) +plt.legend(['Incident', 'Modified', 'Difference']) +plt.ylabel('POA Global irradiance [W/m²]') +plt.grid() +plt.show() + +# %% +# Comparison with other models +# ---------------------------- +# During the addition of this model, a question arose about its trustworthiness +# so, in order to check the integrity of the implementation, we will +# compare it against :py:func:`pvlib.pvsystem.sapm_spectral_loss` and +# :py:func:`pvlib.atmosphere.first_solar`. +# Former model needs the parameters that characterise a module, but which one? +# We will take the mean of Sandia parameters `'A0', 'A1', 'A2', 'A3', 'A4'` for +# the same material type. +# On the other hand, :py:func:`~pvlib.atmosphere.first_solar` needs the +# precipitable water. We assume the standard spectrum, `1.42 cm`. + +# Retrieve modules and select the subset we want to work with the SAPM model +module_type = 'mc-Si' # Equivalent to monosi +sandia_modules = pvlib.pvsystem.retrieve_sam(name='SandiaMod') +modules_subset = \ + sandia_modules.loc[:, sandia_modules.loc['Material'] == module_type] + +# Define typical module and get the means of the A0 to A4 parameters +modules_aggregated = pd.DataFrame(index=('mean', 'std')) +for param in ('A0', 'A1', 'A2', 'A3', 'A4'): + result, _, _ = stats.mvsdist(modules_subset.loc[param]) + modules_aggregated[param] = result.mean(), result.std() + +# Check if 'mean' is a representative value with help of 'std' just in case +print(modules_aggregated) + +# Then apply the SAPM model and calculate introduced difference +modifier_sapm_f1 = \ + pvlib.pvsystem.sapm_spectral_loss(airmass_absolute, + modules_aggregated.loc['mean']) +poa_irrad_sapm_modified = poa_irrad['poa_global'] * modifier_sapm_f1 +poa_irrad_sapm_difference = (poa_irrad['poa_global'] + - poa_irrad_sapm_modified) + +# atmosphere.first_solar model +first_solar_pw = 1.42 # Default for AM1.5 spectrum +modifier_first_solar = \ + pvlib.atmosphere.first_solar_spectral_correction(first_solar_pw, + airmass_absolute, + module_type='monosi') +poa_irrad_first_solar_mod = poa_irrad['poa_global'] * modifier_first_solar +poa_irrad_first_solar_diff = (poa_irrad['poa_global'] + - poa_irrad_first_solar_mod) + +# %% +# Plot global irradiance difference over time +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +datetimes = poa_irrad_global_diff.index # common to poa_irrad_*_diff* +plt.figure() +plt.plot(datetimes, poa_irrad_global_diff.to_numpy(), + label='spectrum.martin_ruiz') +plt.plot(datetimes, poa_irrad_sapm_difference.to_numpy(), + label='atmosphere.first_solar') +plt.plot(datetimes, poa_irrad_first_solar_diff.to_numpy(), + label='pvsystem.sapm_spectral_loss') +plt.legend() +plt.title('Introduced difference comparison of different models') +plt.ylabel('POA Global Irradiance Difference [W/m²]') +plt.grid() +plt.show() + +# %% +# Plot modifier vs absolute airmass +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +ama = airmass_absolute.to_numpy() +# spectrum.martin_ruiz has 3 modifiers, so we only calculate one as +# M = S_eff / S_incident that takes into account the global effect +martin_ruiz_agg_modifier = (poa_irrad_modified['poa_global'] + / poa_irrad['poa_global']) +plt.figure() +plt.scatter(ama, martin_ruiz_agg_modifier.to_numpy(), + label='spectrum.martin_ruiz') +plt.scatter(ama, modifier_sapm_f1.to_numpy(), + label='pvsystem.sapm_spectral_loss') +plt.scatter(ama, modifier_first_solar.to_numpy(), + label='atmosphere.first_solar') +plt.legend() +plt.title('Introduced difference comparison of different models') +plt.xlabel('Absolute airmass') +plt.ylabel(r'Modifier $M = \frac{S_{effective}}{S_{incident}}$') +plt.grid() +plt.show() diff --git a/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst b/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst index b6fe7f4684..8be15f1a30 100644 --- a/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst +++ b/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst @@ -10,3 +10,4 @@ Spectrum spectrum.get_example_spectral_response spectrum.get_am15g spectrum.calc_spectral_mismatch_field + spectrum.martin_ruiz diff --git a/docs/sphinx/source/whatsnew/v0.9.5.rst b/docs/sphinx/source/whatsnew/v0.9.5.rst index c11d72f0de..0da9ce6ab8 100644 --- a/docs/sphinx/source/whatsnew/v0.9.5.rst +++ b/docs/sphinx/source/whatsnew/v0.9.5.rst @@ -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 ~~~~~~~~~ @@ -64,3 +67,4 @@ Contributors * Mark Mikofski (:ghuser:`mikofski`) * Anton Driesse (:ghuser:`adriesse`) * Michael Deceglie (:ghuser:`mdeceglie`) +* Echedey Luis (:ghuser:`echedey-ls`) diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index ff1ce8d4d5..4233c68590 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -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] - diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 70c3be389f..6d10d87e9e 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -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 diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 48371ca961..fdb9c1d3bc 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -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 @@ -2674,10 +2674,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, 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)` + 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) diff --git a/pvlib/spectrum/__init__.py b/pvlib/spectrum/__init__.py index b3d838acfe..a73ca1e0cf 100644 --- a/pvlib/spectrum/__init__.py +++ b/pvlib/spectrum/__init__.py @@ -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) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 5db4649ddd..e1d127c129 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -235,3 +235,161 @@ 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]_. + The same airmass model *must* be used to calculate the airmass input + values to this function 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. ``kasten1966`` airmass algorithm must be used + 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 + upper case. If not specified, ``model_parameters`` has to 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. + ValueError + If neither ``module_type`` nor ``model_parameters`` are given. + ValueError + 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 :math:`c`, :math:`a` and :math:`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 + """ + # 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 + 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 ValueError('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 ValueError('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 diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 80b53d2af3..ad298e6c48 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -8,6 +8,7 @@ SPECTRL2_TEST_DATA = DATA_DIR / 'spectrl2_example_spectra.csv' + @pytest.fixture def spectrl2_data(): # reference spectra generated with solar_utils==0.3 @@ -140,7 +141,7 @@ def test_get_am15g(): def test_calc_spectral_mismatch_field(spectrl2_data): # test that the mismatch is calculated correctly with - # - default and custom reference sepctrum + # - default and custom reference spectrum # - single or multiple sun spectra # sample data @@ -171,3 +172,172 @@ def test_calc_spectral_mismatch_field(spectrl2_data): mm = spectrum.calc_spectral_mismatch_field(sr, e_sun=e_sun) assert mm.index is e_sun.index assert_allclose(mm, expected, rtol=1e-6) + + +@pytest.fixture +def martin_ruiz_mismatch_data(): + # Data to run tests of spectrum.martin_ruiz + kwargs = { + 'clearness_index': [0.56, 0.612, 0.664, 0.716, 0.768, 0.82], + 'airmass_absolute': [2, 1.8, 1.6, 1.4, 1.2, 1], + 'monosi_expected': { + 'dir': [1.09149, 1.07275, 1.05432, 1.03622, 1.01842, 1.00093], + 'sky': [0.88636, 0.85009, 0.81530, 0.78194, 0.74994, 0.71925], + 'gnd': [1.02011, 1.00465, 0.98943, 0.97444, 0.95967, 0.94513]}, + 'polysi_expected': { + 'dir': [1.09166, 1.07280, 1.05427, 1.03606, 1.01816, 1.00058], + 'sky': [0.89443, 0.85553, 0.81832, 0.78273, 0.74868, 0.71612], + 'gnd': [1.02638, 1.00888, 0.99168, 0.97476, 0.95814, 0.94180]}, + 'asi_expected': { + 'dir': [1.07066, 1.05643, 1.04238, 1.02852, 1.01485, 1.00136], + 'sky': [0.94889, 0.91699, 0.88616, 0.85637, 0.82758, 0.79976], + 'gnd': [1.03801, 1.02259, 1.00740, 0.99243, 0.97769, 0.96316]}, + 'monosi_model_params_dict': { + 'poa_direct': {'c': 1.029, 'a': -3.13e-1, 'b': 5.24e-3}, + 'poa_sky_diffuse': {'c': 0.764, 'a': -8.82e-1, 'b': -2.04e-2}, + 'poa_ground_diffuse': {'c': 0.970, 'a': -2.44e-1, 'b': 1.29e-2}}, + 'monosi_custom_params_df': pd.DataFrame({ + 'poa_direct': [1.029, -0.313, 0.00524], + 'poa_sky_diffuse': [0.764, -0.882, -0.0204]}, + index=('c', 'a', 'b')) + } + return kwargs + + +def test_martin_ruiz_mm_scalar(martin_ruiz_mismatch_data): + # test scalar input ; only module_type given + clearness_index = martin_ruiz_mismatch_data['clearness_index'][0] + airmass_absolute = martin_ruiz_mismatch_data['airmass_absolute'][0] + result = spectrum.martin_ruiz(clearness_index, + airmass_absolute, + module_type='asi') + + assert_approx_equal(result['poa_direct'], + martin_ruiz_mismatch_data['asi_expected']['dir'][0], + significant=5) + assert_approx_equal(result['poa_sky_diffuse'], + martin_ruiz_mismatch_data['asi_expected']['sky'][0], + significant=5) + assert_approx_equal(result['poa_ground_diffuse'], + martin_ruiz_mismatch_data['asi_expected']['gnd'][0], + significant=5) + + +def test_martin_ruiz_mm_series(martin_ruiz_mismatch_data): + # test with Series input ; only module_type given + clearness_index = pd.Series(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = pd.Series(martin_ruiz_mismatch_data['airmass_absolute']) + expected = pd.DataFrame(data={ + 'dir': pd.Series(martin_ruiz_mismatch_data['polysi_expected']['dir']), + 'sky': pd.Series(martin_ruiz_mismatch_data['polysi_expected']['sky']), + 'gnd': pd.Series(martin_ruiz_mismatch_data['polysi_expected']['gnd'])}) + + result = spectrum.martin_ruiz(clearness_index, airmass_absolute, + module_type='polysi') + assert_allclose(result['poa_direct'], expected['dir'], atol=1e-5) + assert_allclose(result['poa_sky_diffuse'], expected['sky'], atol=1e-5) + assert_allclose(result['poa_ground_diffuse'], expected['gnd'], atol=1e-5) + + +def test_martin_ruiz_mm_nans(martin_ruiz_mismatch_data): + # test NaN in, NaN out ; only module_type given + clearness_index = pd.Series(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = pd.Series(martin_ruiz_mismatch_data['airmass_absolute']) + airmass_absolute[:5] = np.nan + + result = spectrum.martin_ruiz(clearness_index, airmass_absolute, + module_type='monosi') + assert np.isnan(result['poa_direct'][:5]).all() + assert not np.isnan(result['poa_direct'][5:]).any() + assert np.isnan(result['poa_sky_diffuse'][:5]).all() + assert not np.isnan(result['poa_sky_diffuse'][5:]).any() + assert np.isnan(result['poa_ground_diffuse'][:5]).all() + assert not np.isnan(result['poa_ground_diffuse'][5:]).any() + + +def test_martin_ruiz_mm_model_dict(martin_ruiz_mismatch_data): + # test results when giving 'model_parameters' as dict + # test custom quantity of components and its names can be given + clearness_index = pd.Series(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = pd.Series(martin_ruiz_mismatch_data['airmass_absolute']) + expected = pd.DataFrame(data={ + 'dir': pd.Series(martin_ruiz_mismatch_data['monosi_expected']['dir']), + 'sky': pd.Series(martin_ruiz_mismatch_data['monosi_expected']['sky']), + 'gnd': pd.Series(martin_ruiz_mismatch_data['monosi_expected']['gnd'])}) + model_parameters = martin_ruiz_mismatch_data['monosi_model_params_dict'] + + result = spectrum.martin_ruiz( + clearness_index, + airmass_absolute, + model_parameters=model_parameters) + assert_allclose(result['poa_direct'], expected['dir'], atol=1e-5) + assert_allclose(result['poa_sky_diffuse'], expected['sky'], atol=1e-5) + assert_allclose(result['poa_ground_diffuse'], expected['gnd'], atol=1e-5) + + +def test_martin_ruiz_mm_model_df(martin_ruiz_mismatch_data): + # test results when giving 'model_parameters' as DataFrame + # test custom quantity of components and its names can be given + clearness_index = np.array(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = np.array(martin_ruiz_mismatch_data['airmass_absolute']) + model_parameters = martin_ruiz_mismatch_data['monosi_custom_params_df'] + expected = pd.DataFrame(data={ + 'dir': np.array(martin_ruiz_mismatch_data['monosi_expected']['dir']), + 'sky': np.array(martin_ruiz_mismatch_data['monosi_expected']['sky'])}) + + result = spectrum.martin_ruiz( + clearness_index, + airmass_absolute, + model_parameters=model_parameters) + assert_allclose(result['poa_direct'], expected['dir'], atol=1e-5) + assert_allclose(result['poa_sky_diffuse'], expected['sky'], atol=1e-5) + assert result['poa_ground_diffuse'].isna().all() + + +def test_martin_ruiz_mm_error_notimplemented(martin_ruiz_mismatch_data): + # test exception is raised when module_type does not exist in algorithm + clearness_index = np.array(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = np.array(martin_ruiz_mismatch_data['airmass_absolute']) + + with pytest.raises(NotImplementedError, + match='Cell type parameters not defined in algorithm.'): + _ = spectrum.martin_ruiz(clearness_index, airmass_absolute, + module_type='') + + +def test_martin_ruiz_mm_error_model_keys(martin_ruiz_mismatch_data): + # test exception is raised when in params keys + clearness_index = np.array(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = np.array(martin_ruiz_mismatch_data['airmass_absolute']) + model_parameters = { + 'component_example': {'z': 0.970, 'x': -2.44e-1, 'y': 1.29e-2}} + with pytest.raises(ValueError, + match="You must specify model parameters with keys " + "'a','b','c' for each irradiation component."): + _ = spectrum.martin_ruiz(clearness_index, airmass_absolute, + model_parameters=model_parameters) + + +def test_martin_ruiz_mm_error_missing_params(martin_ruiz_mismatch_data): + # test exception is raised when missing module_type and model_parameters + clearness_index = np.array(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = np.array(martin_ruiz_mismatch_data['airmass_absolute']) + + with pytest.raises(ValueError, + match='You must pass at least "module_type" ' + 'or "model_parameters" as arguments.'): + _ = spectrum.martin_ruiz(clearness_index, airmass_absolute) + + +def test_martin_ruiz_mm_error_too_many_arguments(martin_ruiz_mismatch_data): + # test warning is raised with both 'module_type' and 'model_parameters' + clearness_index = pd.Series(martin_ruiz_mismatch_data['clearness_index']) + airmass_absolute = pd.Series(martin_ruiz_mismatch_data['airmass_absolute']) + model_parameters = martin_ruiz_mismatch_data['monosi_model_params_dict'] + + with pytest.raises(ValueError, + match='Cannot resolve input: must supply only one of ' + '"module_type" or "model_parameters"'): + _ = spectrum.martin_ruiz(clearness_index, airmass_absolute, + module_type='asi', + model_parameters=model_parameters)