diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index f7ad2b7d7c..2244409dde 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -26,6 +26,11 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* The return values of :py:func:`pvlib.pvsystem.calcparams_desoto`, + :py:func:`pvlib.pvsystem.calcparams_cec`, and + :py:func:`pvlib.pvsystem.calcparams_pvsyst` are all numeric types and have + the same Python type as the `effective_irradiance` and `temp_cell` parameters. (:issue:`1626`, :pull:`1700`) + * Added `map_variables` parameter to :py:func:`pvlib.iotools.read_srml` and :py:func:`pvlib.iotools.read_srml_month_from_solardat` (:pull:`1773`) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index bdab5d604d..9f05e4c97a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -21,6 +21,7 @@ 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 @@ -1913,7 +1914,7 @@ def calcparams_desoto(effective_irradiance, temp_cell, saturation_current : numeric Diode saturation curent in amperes - resistance_series : float + resistance_series : numeric Series resistance in ohms resistance_shunt : numeric @@ -2036,9 +2037,21 @@ def calcparams_desoto(effective_irradiance, temp_cell, # use errstate to silence divide by warning with np.errstate(divide='ignore'): Rsh = R_sh_ref * (irrad_ref / effective_irradiance) + Rs = R_s - return IL, I0, Rs, Rsh, nNsVth + 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) def calcparams_cec(effective_irradiance, temp_cell, @@ -2117,7 +2130,7 @@ def calcparams_cec(effective_irradiance, temp_cell, saturation_current : numeric Diode saturation curent in amperes - resistance_series : float + resistance_series : numeric Series resistance in ohms resistance_shunt : numeric @@ -2234,7 +2247,7 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, saturation_current : numeric Diode saturation current in amperes - resistance_series : float + resistance_series : numeric Series resistance in ohms resistance_shunt : numeric @@ -2293,7 +2306,18 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, Rs = R_s - return IL, I0, Rs, Rsh, nNsVth + 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) def retrieve_sam(name=None, path=None): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index d5dcfc1420..bbc9739f64 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1,4 +1,5 @@ from collections import OrderedDict +import itertools import numpy as np from numpy import nan, array @@ -738,28 +739,220 @@ def test_Array__infer_cell_type(): assert array._infer_cell_type() is None +def _calcparams_correct_Python_type_numeric_type_cases(): + """ + An auxilary function used in the unit tests named + ``test_calcparams_*_returns_correct_Python_type``. + + Returns + ------- + Returns a list of tuples of functions intended for transforming a + Python scalar into a numeric type: scalar, np.ndarray, or pandas.Series + """ + return list(itertools.product(*(2 * [[ + # scalars (e.g. Python floats) + lambda x: x, + # np.ndarrays (0d and 1d-arrays) + np.array, + lambda x: np.array([x]), + # pd.Series (1d-arrays) + pd.Series + ]]))) + + +def _calcparams_correct_Python_type_check(out_value, numeric_args): + """ + An auxilary function used in the unit tests named + ``test_calcparams_*_returns_correct_Python_type``. + + Parameters + ---------- + out_value: numeric + A value returned by a pvsystem.calcparams_ function. + + numeric_args: numeric + An iterable of the numeric-type arguments to the pvsystem.calcparams_ + functions: ``effective_irradiance`` and ``temp_cell``. + + Returns + ------- + bool indicating whether ``out_value`` has the correct Python type + based on the Python types of ``effective_irradiance`` and + ``temp_cell``. + """ + if any(isinstance(a, pd.Series) for a in numeric_args): + return isinstance(out_value, pd.Series) + elif any(isinstance(a, np.ndarray) for a in numeric_args): + return isinstance(out_value, np.ndarray) # 0d or 1d-arrays + return np.isscalar(out_value) + + +@pytest.mark.parametrize('numeric_type_funcs', + _calcparams_correct_Python_type_numeric_type_cases()) +def test_calcparams_desoto_returns_correct_Python_type(numeric_type_funcs, + cec_module_params): + numeric_args = dict( + effective_irradiance=numeric_type_funcs[0](800.0), + temp_cell=numeric_type_funcs[1](25), + ) + out = pvsystem.calcparams_desoto( + **numeric_args, + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + EgRef=1.121, + dEgdT=-0.0002677 + ) + + assert all(_calcparams_correct_Python_type_check(a, numeric_args.values()) + for a in out) + + +@pytest.mark.parametrize('numeric_type_funcs', + _calcparams_correct_Python_type_numeric_type_cases()) +def test_calcparams_cec_returns_correct_Python_type(numeric_type_funcs, + cec_module_params): + numeric_args = dict( + effective_irradiance=numeric_type_funcs[0](800.0), + temp_cell=numeric_type_funcs[1](25), + ) + out = pvsystem.calcparams_cec( + **numeric_args, + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + Adjust=cec_module_params['Adjust'], + EgRef=1.121, + dEgdT=-0.0002677 + ) + + assert all(_calcparams_correct_Python_type_check(a, numeric_args.values()) + for a in out) + + +@pytest.mark.parametrize('numeric_type_funcs', + _calcparams_correct_Python_type_numeric_type_cases()) +def test_calcparams_pvsyst_returns_correct_Python_type(numeric_type_funcs, + pvsyst_module_params): + numeric_args = dict( + effective_irradiance=numeric_type_funcs[0](800.0), + temp_cell=numeric_type_funcs[1](25), + ) + out = pvsystem.calcparams_pvsyst( + **numeric_args, + alpha_sc=pvsyst_module_params['alpha_sc'], + gamma_ref=pvsyst_module_params['gamma_ref'], + mu_gamma=pvsyst_module_params['mu_gamma'], + I_L_ref=pvsyst_module_params['I_L_ref'], + I_o_ref=pvsyst_module_params['I_o_ref'], + R_sh_ref=pvsyst_module_params['R_sh_ref'], + R_sh_0=pvsyst_module_params['R_sh_0'], + R_s=pvsyst_module_params['R_s'], + cells_in_series=pvsyst_module_params['cells_in_series'], + EgRef=pvsyst_module_params['EgRef'] + ) + + assert all(_calcparams_correct_Python_type_check(a, numeric_args.values()) + for a in out) + + +def test_calcparams_desoto_all_scalars(cec_module_params): + IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( + effective_irradiance=800.0, + temp_cell=25, + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + EgRef=1.121, + dEgdT=-0.0002677 + ) + + assert np.isclose(IL, 6.036, atol=1e-4, rtol=0) + assert np.isclose(I0, 1.94e-9, atol=1e-4, rtol=0) + assert np.isclose(Rs, 0.094, atol=1e-4, rtol=0) + assert np.isclose(Rsh, 19.65, atol=1e-4, rtol=0) + assert np.isclose(nNsVth, 0.473, atol=1e-4, rtol=0) + + +def test_calcparams_cec_all_scalars(cec_module_params): + IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_cec( + effective_irradiance=800.0, + temp_cell=25, + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + Adjust=cec_module_params['Adjust'], + EgRef=1.121, + dEgdT=-0.0002677 + ) + + assert np.isclose(IL, 6.036, atol=1e-4, rtol=0) + assert np.isclose(I0, 1.94e-9, atol=1e-4, rtol=0) + assert np.isclose(Rs, 0.094, atol=1e-4, rtol=0) + assert np.isclose(Rsh, 19.65, atol=1e-4, rtol=0) + assert np.isclose(nNsVth, 0.473, atol=1e-4, rtol=0) + + +def test_calcparams_pvsyst_all_scalars(pvsyst_module_params): + IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_pvsyst( + effective_irradiance=800.0, + temp_cell=50, + alpha_sc=pvsyst_module_params['alpha_sc'], + gamma_ref=pvsyst_module_params['gamma_ref'], + mu_gamma=pvsyst_module_params['mu_gamma'], + I_L_ref=pvsyst_module_params['I_L_ref'], + I_o_ref=pvsyst_module_params['I_o_ref'], + R_sh_ref=pvsyst_module_params['R_sh_ref'], + R_sh_0=pvsyst_module_params['R_sh_0'], + R_s=pvsyst_module_params['R_s'], + cells_in_series=pvsyst_module_params['cells_in_series'], + EgRef=pvsyst_module_params['EgRef']) + + assert np.isclose(IL, 4.8200, atol=1e-4, rtol=0) + assert np.isclose(I0, 1.47e-7, atol=1e-4, rtol=0) + assert np.isclose(Rs, 0.500, atol=1e-4, rtol=0) + assert np.isclose(Rsh, 305.757, atol=1e-4, rtol=0) + assert np.isclose(nNsVth, 1.7961, atol=1e-4, rtol=0) + + def test_calcparams_desoto(cec_module_params): times = pd.date_range(start='2015-01-01', periods=3, freq='12H') - effective_irradiance = pd.Series([0.0, 800.0, 800.0], index=times) - temp_cell = pd.Series([25, 25, 50], index=times) + df = pd.DataFrame({ + 'effective_irradiance': [0.0, 800.0, 800.0], + 'temp_cell': [25, 25, 50] + }, index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - effective_irradiance, - temp_cell, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - EgRef=1.121, - dEgdT=-0.0002677) + df['effective_irradiance'], + df['temp_cell'], + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + EgRef=1.121, + dEgdT=-0.0002677 + ) assert_series_equal(IL, pd.Series([0.0, 6.036, 6.096], index=times), check_less_precise=3) assert_series_equal(I0, pd.Series([0.0, 1.94e-9, 7.419e-8], index=times), check_less_precise=3) - assert_allclose(Rs, 0.094) + assert_series_equal(Rs, pd.Series([0.094, 0.094, 0.094], index=times), + check_less_precise=3) assert_series_equal(Rsh, pd.Series([np.inf, 19.65, 19.65], index=times), check_less_precise=3) assert_series_equal(nNsVth, pd.Series([0.473, 0.473, 0.5127], index=times), @@ -768,27 +961,31 @@ def test_calcparams_desoto(cec_module_params): def test_calcparams_cec(cec_module_params): times = pd.date_range(start='2015-01-01', periods=3, freq='12H') - effective_irradiance = pd.Series([0.0, 800.0, 800.0], index=times) - temp_cell = pd.Series([25, 25, 50], index=times) + df = pd.DataFrame({ + 'effective_irradiance': [0.0, 800.0, 800.0], + 'temp_cell': [25, 25, 50] + }, index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_cec( - effective_irradiance, - temp_cell, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - Adjust=cec_module_params['Adjust'], - EgRef=1.121, - dEgdT=-0.0002677) + df['effective_irradiance'], + df['temp_cell'], + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + Adjust=cec_module_params['Adjust'], + EgRef=1.121, + dEgdT=-0.0002677 + ) assert_series_equal(IL, pd.Series([0.0, 6.036, 6.0896], index=times), check_less_precise=3) assert_series_equal(I0, pd.Series([0.0, 1.94e-9, 7.419e-8], index=times), check_less_precise=3) - assert_allclose(Rs, 0.094) + assert_series_equal(Rs, pd.Series([0.094, 0.094, 0.094], index=times), + check_less_precise=3) assert_series_equal(Rsh, pd.Series([np.inf, 19.65, 19.65], index=times), check_less_precise=3) assert_series_equal(nNsVth, pd.Series([0.473, 0.473, 0.5127], index=times), @@ -834,12 +1031,14 @@ def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): def test_calcparams_pvsyst(pvsyst_module_params): times = pd.date_range(start='2015-01-01', periods=2, freq='12H') - effective_irradiance = pd.Series([0.0, 800.0], index=times) - temp_cell = pd.Series([25, 50], index=times) + df = pd.DataFrame({ + 'effective_irradiance': [0.0, 800.0], + 'temp_cell': [25, 50] + }, index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_pvsyst( - effective_irradiance, - temp_cell, + df['effective_irradiance'], + df['temp_cell'], alpha_sc=pvsyst_module_params['alpha_sc'], gamma_ref=pvsyst_module_params['gamma_ref'], mu_gamma=pvsyst_module_params['mu_gamma'], @@ -855,7 +1054,8 @@ def test_calcparams_pvsyst(pvsyst_module_params): IL.round(decimals=3), pd.Series([0.0, 4.8200], index=times)) assert_series_equal( I0.round(decimals=3), pd.Series([0.0, 1.47e-7], index=times)) - assert_allclose(Rs, 0.500) + assert_series_equal( + Rs.round(decimals=3), pd.Series([0.500, 0.500], index=times)) assert_series_equal( Rsh.round(decimals=3), pd.Series([1000.0, 305.757], index=times)) assert_series_equal( @@ -873,21 +1073,23 @@ def test_PVSystem_calcparams_desoto(cec_module_params, mocker): IL, I0, Rs, Rsh, nNsVth = system.calcparams_desoto(effective_irradiance, temp_cell) pvsystem.calcparams_desoto.assert_called_once_with( - effective_irradiance, - temp_cell, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - EgRef=module_parameters['EgRef'], - dEgdT=module_parameters['dEgdT']) + effective_irradiance, + temp_cell, + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + EgRef=module_parameters['EgRef'], + dEgdT=module_parameters['dEgdT'] + ) + assert_allclose(IL, np.array([0.0, 6.036]), atol=1) - assert_allclose(I0, 2.0e-9, atol=1.0e-9) - assert_allclose(Rs, 0.1, atol=0.1) + assert_allclose(I0, np.array([2.0e-9, 2.0e-9]), atol=1.0e-9) + assert_allclose(Rs, np.array([0.1, 0.1]), atol=0.1) assert_allclose(Rsh, np.array([np.inf, 20]), atol=1) - assert_allclose(nNsVth, 0.5, atol=0.1) + assert_allclose(nNsVth, np.array([0.5, 0.5]), atol=0.1) def test_PVSystem_calcparams_pvsyst(pvsyst_module_params, mocker): @@ -899,23 +1101,24 @@ def test_PVSystem_calcparams_pvsyst(pvsyst_module_params, mocker): IL, I0, Rs, Rsh, nNsVth = system.calcparams_pvsyst(effective_irradiance, temp_cell) pvsystem.calcparams_pvsyst.assert_called_once_with( - effective_irradiance, - temp_cell, - alpha_sc=pvsyst_module_params['alpha_sc'], - gamma_ref=pvsyst_module_params['gamma_ref'], - mu_gamma=pvsyst_module_params['mu_gamma'], - I_L_ref=pvsyst_module_params['I_L_ref'], - I_o_ref=pvsyst_module_params['I_o_ref'], - R_sh_ref=pvsyst_module_params['R_sh_ref'], - R_sh_0=pvsyst_module_params['R_sh_0'], - R_s=pvsyst_module_params['R_s'], - cells_in_series=pvsyst_module_params['cells_in_series'], - EgRef=pvsyst_module_params['EgRef'], - R_sh_exp=pvsyst_module_params['R_sh_exp']) + effective_irradiance, + temp_cell, + alpha_sc=pvsyst_module_params['alpha_sc'], + gamma_ref=pvsyst_module_params['gamma_ref'], + mu_gamma=pvsyst_module_params['mu_gamma'], + I_L_ref=pvsyst_module_params['I_L_ref'], + I_o_ref=pvsyst_module_params['I_o_ref'], + R_sh_ref=pvsyst_module_params['R_sh_ref'], + R_sh_0=pvsyst_module_params['R_sh_0'], + R_s=pvsyst_module_params['R_s'], + cells_in_series=pvsyst_module_params['cells_in_series'], + EgRef=pvsyst_module_params['EgRef'], + R_sh_exp=pvsyst_module_params['R_sh_exp'] + ) assert_allclose(IL, np.array([0.0, 4.8200]), atol=1) assert_allclose(I0, np.array([0.0, 1.47e-7]), atol=1.0e-5) - assert_allclose(Rs, 0.5, atol=0.1) + assert_allclose(Rs, np.array([0.5, 0.5]), atol=0.1) assert_allclose(Rsh, np.array([1000, 305.757]), atol=50) assert_allclose(nNsVth, np.array([1.6186, 1.7961]), atol=0.1) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 4d5312088b..583141a726 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -2,6 +2,7 @@ from pvlib import tools import numpy as np +import pandas as pd @pytest.mark.parametrize('keys, input_dict, expected', [ @@ -95,3 +96,27 @@ def test_degrees_to_index_1(): 'latitude' or 'longitude' is passed.""" with pytest.raises(IndexError): # invalid value for coordinate argument tools._degrees_to_index(degrees=22.0, coordinate='width') + + +@pytest.mark.parametrize('args, args_idx', [ + # no pandas.Series or pandas.DataFrame args + ((1,), None), + (([1],), None), + ((np.array(1),), None), + ((np.array([1]),), None), + # has pandas.Series or pandas.DataFrame args + ((pd.DataFrame([1], index=[1]),), 0), + ((pd.Series([1], index=[1]),), 0), + ((1, pd.Series([1], index=[1]),), 1), + ((1, pd.DataFrame([1], index=[1]),), 1), + # first pandas.Series or pandas.DataFrame is used + ((1, pd.Series([1], index=[1]), pd.DataFrame([2], index=[2]),), 1), + ((1, pd.DataFrame([1], index=[1]), pd.Series([2], index=[2]),), 1), +]) +def test_get_pandas_index(args, args_idx): + index = tools.get_pandas_index(*args) + + if args_idx is None: + assert index is None + else: + pd.testing.assert_index_equal(args[args_idx].index, index) diff --git a/pvlib/tools.py b/pvlib/tools.py index f6974cf3d3..ffc2b122c1 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -469,3 +469,25 @@ def _first_order_centered_difference(f, x0, dx=DX, args=()): # removal in scipy 1.12.0 df = f(x0+dx, *args) - f(x0-dx, *args) return df / 2 / dx + + +def get_pandas_index(*args): + """ + Get the index of the first pandas DataFrame or Series in a list of + arguments. + + Parameters + ---------- + args: positional arguments + The numeric values to scan for a pandas index. + + Returns + ------- + A pandas index or None + None is returned if there are no pandas DataFrames or Series in the + args list. + """ + return next( + (a.index for a in args if isinstance(a, (pd.DataFrame, pd.Series))), + None + )