From f36d83291cde9caf51dd4aa9140e95a77793f83c Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sat, 18 Mar 2023 09:44:42 -0600 Subject: [PATCH 01/19] two decorators for renaming pd.Series in a function --- pvlib/pvsystem.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 48371ca961..095f2fa143 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1839,6 +1839,69 @@ def get_orientation(self, solar_zenith, solar_azimuth): return tracking_data +def format_return_values(return_types): + def format_val(r, rtype): + if rtype[0] == 'dict-like': + if isinstance(r, (dict, OrderedDict)): + pass + elif isinstance(r, pd.Series): + r.name = rtype[1] + elif rtype[0] == 'numeric': + if isinstance(r, (int, float, np.ndarray)): + pass + elif isinstance(r, pd.Series): + r.name = rtype[1] + elif rtype[0] == 'array-like': + if isinstance(r, np.ndarray): + pass + elif isinstance(r, pd.Series): + r.name = rtype[1] + return r + + def decorator(func): + @functools.wraps(func) + def f(*args, **kwargs): + x = func(*args, **kwargs) + if isinstance(x, tuple): + return tuple(format_val(r, rt) for r, rt in zip(x, return_types)) + return format_val(x, return_types[0]) + return f + return decorator + + +def format_args(func): + """ + Decorator for functions that take dict-like, numeric, or array-like + arguments. + + If a pd.Series is passed to the function, pd.Series.name is cleared + using pd.Series.rename. This does not rename the original pd.Series + passed, and does not copy its data. + + This still allows the function to give a name to it later. + """ + @functools.wraps(func) + def f(*args, **kwargs): + formatted_args = [] + for a in args: + if isinstance(a, pd.Series): + a = a.rename(None) + formatted_args.append(a) + for k, v in kwargs.items(): + if isinstance(v, pd.Series): + kwargs[k] = v.rename(None) + return func(*formatted_args, **kwargs) + return f + + +# @format_return_values([ + # ('numeric', 'photocurrent'), + # ('numeric', 'saturation_current'), + # ('numeric', 'resistance_series'), + # ('numeric', 'resistance_shunt'), + # ('numeric', 'nNsVth') +# ]) +@format_args 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, From 8d184fea619872ec1757fc658ab978c0b28fe987 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sat, 18 Mar 2023 10:34:12 -0600 Subject: [PATCH 02/19] calcparams_* Rs return value to match temp_cell type --- pvlib/pvsystem.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 095f2fa143..fe3356d124 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1894,6 +1894,14 @@ def f(*args, **kwargs): return f +def match_type_scalar_to_numeric(a, ref): + if isinstance(ref, (np.ndarray, pd.Series)): + a = np.full(ref.shape[0], a, dtype=ref.dtype) + if isinstance(ref, pd.Series): + a = pd.Series(data=a, index=ref.index) + return a + + # @format_return_values([ # ('numeric', 'photocurrent'), # ('numeric', 'saturation_current'), @@ -1973,7 +1981,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 @@ -2096,7 +2104,8 @@ 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 + + Rs = match_type_scalar_to_numeric(R_s, temp_cell) return IL, I0, Rs, Rsh, nNsVth @@ -2177,7 +2186,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 @@ -2294,7 +2303,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 @@ -2351,7 +2360,7 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, Rsh = Rsh_base + (R_sh_0 - Rsh_base) * \ np.exp(-R_sh_exp * effective_irradiance / irrad_ref) - Rs = R_s + Rs = match_type_scalar_to_numeric(R_s, temp_cell) return IL, I0, Rs, Rsh, nNsVth From 2842a775d9e714ce2e965aa9edfd6ab151480bc6 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sat, 18 Mar 2023 11:19:20 -0600 Subject: [PATCH 03/19] adding tests for pd.Series naming and resistance_series type conversion --- pvlib/pvsystem.py | 4 ++- pvlib/tests/test_pvsystem.py | 63 +++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fe3356d124..32b74b6d4b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1896,7 +1896,7 @@ def f(*args, **kwargs): def match_type_scalar_to_numeric(a, ref): if isinstance(ref, (np.ndarray, pd.Series)): - a = np.full(ref.shape[0], a, dtype=ref.dtype) + a = np.full(ref.shape[0], a, dtype=type(a)) if isinstance(ref, pd.Series): a = pd.Series(data=a, index=ref.index) return a @@ -2110,6 +2110,7 @@ def calcparams_desoto(effective_irradiance, temp_cell, return IL, I0, Rs, Rsh, nNsVth +@format_args 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, @@ -2226,6 +2227,7 @@ def calcparams_cec(effective_irradiance, temp_cell, irrad_ref=irrad_ref, temp_ref=temp_ref) +@format_args def calcparams_pvsyst(effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 7fa013d0dc..609c68c787 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -746,12 +746,14 @@ def test_Array__infer_cell_type(): 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, + 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'], @@ -765,21 +767,32 @@ def test_calcparams_desoto(cec_module_params): 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), check_less_precise=3) + pdSeries_names = set(df.columns) + + assert IL.name not in pdSeries_names + assert I0.name not in pdSeries_names + assert Rs.name not in pdSeries_names + assert Rsh.name not in pdSeries_names + assert nNsVth.name not in pdSeries_names + 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, + 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'], @@ -794,12 +807,21 @@ def test_calcparams_cec(cec_module_params): 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), check_less_precise=3) + pdSeries_names = set(df.columns) + + assert IL.name not in pdSeries_names + assert I0.name not in pdSeries_names + assert Rs.name not in pdSeries_names + assert Rsh.name not in pdSeries_names + assert nNsVth.name not in pdSeries_names + def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): """ @@ -840,12 +862,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'], @@ -861,12 +885,21 @@ 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( nNsVth.round(decimals=4), pd.Series([1.6186, 1.7961], index=times)) + pdSeries_names = set(df.columns) + + assert IL.name not in pdSeries_names + assert I0.name not in pdSeries_names + assert Rs.name not in pdSeries_names + assert Rsh.name not in pdSeries_names + assert nNsVth.name not in pdSeries_names + def test_PVSystem_calcparams_desoto(cec_module_params, mocker): mocker.spy(pvsystem, 'calcparams_desoto') From 95553fb145cdaac2829fe58bd472994c8dbb4d20 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sat, 18 Mar 2023 18:53:20 -0600 Subject: [PATCH 04/19] methods for converting between numeric-type values --- pvlib/pvsystem.py | 18 ++----- pvlib/tests/test_tools.py | 108 ++++++++++++++++++++++++++++++++++++++ pvlib/tools.py | 82 +++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 32b74b6d4b..78e4ee9dd2 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -21,6 +21,7 @@ from pvlib import (atmosphere, iam, inverter, irradiance, singlediode as _singlediode, 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 @@ -1894,14 +1895,6 @@ def f(*args, **kwargs): return f -def match_type_scalar_to_numeric(a, ref): - if isinstance(ref, (np.ndarray, pd.Series)): - a = np.full(ref.shape[0], a, dtype=type(a)) - if isinstance(ref, pd.Series): - a = pd.Series(data=a, index=ref.index) - return a - - # @format_return_values([ # ('numeric', 'photocurrent'), # ('numeric', 'saturation_current'), @@ -2105,12 +2098,11 @@ def calcparams_desoto(effective_irradiance, temp_cell, with np.errstate(divide='ignore'): Rsh = R_sh_ref * (irrad_ref / effective_irradiance) - Rs = match_type_scalar_to_numeric(R_s, temp_cell) + Rs = R_s - return IL, I0, Rs, Rsh, nNsVth + return tools.match_type_all_numeric(IL, I0, Rs, Rsh, nNsVth) -@format_args 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, @@ -2362,9 +2354,9 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, Rsh = Rsh_base + (R_sh_0 - Rsh_base) * \ np.exp(-R_sh_exp * effective_irradiance / irrad_ref) - Rs = match_type_scalar_to_numeric(R_s, temp_cell) + Rs = R_s - return IL, I0, Rs, Rsh, nNsVth + return tools.match_type_all_numeric(IL, I0, Rs, Rsh, nNsVth) def retrieve_sam(name=None, path=None): diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 4d5312088b..f1e9ae4d37 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -1,7 +1,9 @@ import pytest +from .conftest import assert_series_equal from pvlib import tools import numpy as np +import pandas as pd @pytest.mark.parametrize('keys, input_dict, expected', [ @@ -95,3 +97,109 @@ 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') + + +def get_match_type_array_like_test_cases(): + return [ + # identity + (np.array([1]), np.array([1]), lambda a, b: np.array_equal(a, b)), + (np.array([1]), np.array([1.]), lambda a, b: np.array_equal(a, b)), + (np.array([1.]), np.array([1]), lambda a, b: np.array_equal(a, b)), + (np.array([1.]), np.array([1.]), lambda a, b: np.array_equal(a, b)), + (pd.Series([1]), pd.Series([1]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1]), pd.Series([1.]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1.]), pd.Series([1]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1.]), pd.Series([1.]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + # np.ndarray to pd.Series + (np.array([1]), pd.Series([1]), lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1]), pd.Series([1.]), lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1.]), pd.Series([1]), lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1.]), pd.Series([1.]), lambda a, b: np.array_equal(a, b.to_numpy())), + # pd.Series to np.ndarray + (pd.Series([1]), np.array([1]), lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1]), np.array([1.]), lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1.]), np.array([1]), lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1.]), np.array([1.]), lambda a, b: np.array_equal(a.to_numpy(), b)), + # x shorter than type_of + (np.array([1]), np.array([1, 2]), lambda a, b: np.array_equal(a, b)), + (np.array([1]), pd.Series([1, 2]), lambda a, b: np.array_equal(a, b.to_numpy())), + (pd.Series([1]), np.array([1, 2]), lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1]), pd.Series([1, 2]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + # x longer than type_of + (np.array([1, 2]), np.array([1]), lambda a, b: np.array_equal(a, b)), + (np.array([1, 2]), pd.Series([1]), lambda a, b: np.array_equal(a, b.to_numpy())), + (pd.Series([1, 2]), np.array([1]), lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1, 2]), pd.Series([1]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())) + ] + + +@pytest.mark.parametrize('x, type_of, content_equal', get_match_type_array_like_test_cases()) +def test_match_type_array_like(x, type_of, content_equal): + x_matched = tools.match_type_array_like(x, type_of) + + assert type(x_matched) is type(type_of) + assert content_equal(x, x_matched) + + +@pytest.mark.parametrize('x, type_of, content_equal', [ + (1, 1, lambda a, b: a == b), + (1., 1., lambda a, b: a == b), + ('numpy', 'numpy', lambda a, b: a == b) +]) +def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): + x_matched = tools.match_type_numeric(x, type_of) + + assert type(x) is type(x_matched) + assert content_equal(x, x_matched) + + +@pytest.mark.parametrize('x, type_of, match_size, content_equal', [ + (1, np.array([1]), True, lambda a, b: a == b.item()), + (1, np.array([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), + (1, np.array([1, 2]), False, lambda a, b: a == b.item()), + (1, np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), + (1, pd.Series([1]), True, lambda a, b: a == b.item()), + (1, pd.Series([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), + (1, pd.Series([1., 2.]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), + (1, pd.Series([1, 2]), False, lambda a, b: a == b.item()), + (1, pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), + (1., np.array([1]), True, lambda a, b: a == b.item()), + (1., np.array([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), + (1., np.array([1, 2]), False, lambda a, b: a == b.item()), + (1., np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), + (1., pd.Series([1]), True, lambda a, b: a == b.item()), + (1., pd.Series([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), + (1., pd.Series([1., 2.]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), + (1., pd.Series([1, 2]), False, lambda a, b: a == b.item()), + (1., pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()) +]) +def test_match_type_numeric_scalar_to_array_like(x, type_of, match_size, content_equal): + x_matched = tools.match_type_numeric(x, type_of, match_size=match_size) + + assert type(x_matched) is type(type_of) + assert content_equal(x, x_matched) + + +@pytest.mark.parametrize('x, type_of, content_equal', [ + (np.array([1]), 1, lambda a, b: a.item() == b), + (np.array([1.]), 1, lambda a, b: a.item() == b), + (np.array([1]), 1., lambda a, b: a.item() == b), + (np.array([1.]), 1., lambda a, b: a.item() == b), + (pd.Series([1]), 1, lambda a, b: a.item() == b), + (pd.Series([1.]), 1, lambda a, b: a.item() == b), + (pd.Series([1]), 1., lambda a, b: a.item() == b), + (pd.Series([1.]), 1., lambda a, b: a.item() == b) +]) +def test_match_type_numeric_array_like_to_scalar(x, type_of, content_equal): + x_matched = tools.match_type_numeric(x, type_of) + + assert type(x.item()) is type(x_matched) + assert content_equal(x, x_matched) + + +@pytest.mark.parametrize('x, type_of, content_equal', get_match_type_array_like_test_cases()) +def test_match_type_numeric_array_like_to_array_like(x, type_of, content_equal): + x_matched = tools.match_type_numeric(x, type_of) + + assert type(x_matched) is type(type_of) + assert content_equal(x, x_matched) diff --git a/pvlib/tools.py b/pvlib/tools.py index f1a569bb1f..5852525a6c 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -469,3 +469,85 @@ 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 match_type_array_like(x, type_of): + """ + Convert an array-like value to the Python type of another. + + The dtype of the converted value is preserved. + + Parameters + ---------- + x: numeric + The value whose Python type will be converted. + + type_of: numeric + The value thie the Python type that ``x`` will be converted to. + + Returns + ------- + The vlaue of ``x`` now stroed in the Python type of ``type_of``. + """ + if type(x) is type(type_of): + return x + if isinstance(type_of, np.ndarray): + return x.to_numpy() + try: + return pd.Series(data=x, index=type_of.index) + except ValueError: # data and index length do not match + return pd.Series(data=x) + + +def match_type_numeric(x, type_of, match_size=True): + """ + Convert a numeric-type value to the Python type of another. + + The dtype of the converted value is preserved. + + Parameters + ---------- + x: numeric + The value whose Python type will be converted. + + type_of: numeric + The value with the Python type that ``x`` will be converted to. + + match_size: bool + If ``x`` is a scalar and ``type_of`` is an array-like, return an + array-like of ``type_of.size`` that contains ``x`` repeated. + + Returns + ------- + numeric + The value of ``x`` now stored in the Python type of ``type_of``. + """ + a_is_array_like = isinstance(x, (np.ndarray, pd.Series)) + type_of_is_array_like = isinstance(type_of, (np.ndarray, pd.Series)) + if a_is_array_like and type_of_is_array_like: + return match_type_array_like(x, type_of) + if a_is_array_like: + return _scalar_out(x) + if type_of_is_array_like: + if isinstance(type_of, np.ndarray): + size = type_of.size if match_size else 1 + return np.full(size, x, dtype=type(x)) + index = type_of.index if match_size else [type_of.index[0]] + return pd.Series(data=x, index=index) + return x # x and type_of are both scalars + + +def match_type_all_numeric(*args): + """ + Convert all numeric-type values to the Python type of the first numeric + value passed. + + The dtype of the converted values is preserved. + + Returns + ------- + tuple(numeric) + All of the passed values now stored in the Python type of the first + value passed. + """ + return args[0], *(match_type_numeric(x, args[0]) for x in args[1:]) From c1887431fd0cb81d9baf92250bf96d7d03efea39 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sun, 19 Mar 2023 10:41:40 -0600 Subject: [PATCH 05/19] adding more tests and clarifying documentation --- pvlib/tests/test_tools.py | 42 ++++++++++++++++++++++++---- pvlib/tools.py | 58 ++++++++++++++++++++++++--------------- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index f1e9ae4d37..d4589528b6 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -133,7 +133,9 @@ def get_match_type_array_like_test_cases(): ] -@pytest.mark.parametrize('x, type_of, content_equal', get_match_type_array_like_test_cases()) +@pytest.mark.parametrize('x, type_of, content_equal', [ + *get_match_type_array_like_test_cases() +]) def test_match_type_array_like(x, type_of, content_equal): x_matched = tools.match_type_array_like(x, type_of) @@ -143,8 +145,9 @@ def test_match_type_array_like(x, type_of, content_equal): @pytest.mark.parametrize('x, type_of, content_equal', [ (1, 1, lambda a, b: a == b), - (1., 1., lambda a, b: a == b), - ('numpy', 'numpy', lambda a, b: a == b) + (1, 1., lambda a, b: a == b), + (1., 1, lambda a, b: a == b), + (1., 1., lambda a, b: a == b) ]) def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): x_matched = tools.match_type_numeric(x, type_of) @@ -155,11 +158,11 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): @pytest.mark.parametrize('x, type_of, match_size, content_equal', [ (1, np.array([1]), True, lambda a, b: a == b.item()), - (1, np.array([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), + (1, np.array([1, 2]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), (1, np.array([1, 2]), False, lambda a, b: a == b.item()), (1, np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), (1, pd.Series([1]), True, lambda a, b: a == b.item()), - (1, pd.Series([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), + (1, pd.Series([1, 2]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), (1, pd.Series([1., 2.]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), (1, pd.Series([1, 2]), False, lambda a, b: a == b.item()), (1, pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), @@ -197,9 +200,36 @@ def test_match_type_numeric_array_like_to_scalar(x, type_of, content_equal): assert content_equal(x, x_matched) -@pytest.mark.parametrize('x, type_of, content_equal', get_match_type_array_like_test_cases()) +@pytest.mark.parametrize('x, type_of, content_equal', [ + *get_match_type_array_like_test_cases() +]) def test_match_type_numeric_array_like_to_array_like(x, type_of, content_equal): x_matched = tools.match_type_numeric(x, type_of) assert type(x_matched) is type(type_of) assert content_equal(x, x_matched) + + +@pytest.mark.parametrize('args, expected_type', [ + ((1, np.array([1]), pd.Series([1])), np.isscalar), + ((np.array([1]), 1, np.array([1]), pd.Series([1])), lambda a: isinstance(a, np.ndarray)), + ((pd.Series([1]), 1, np.array([1]), pd.Series([1])), lambda a: isinstance(a, pd.Series)), +]) +def test_match_type_all_numeric(args, expected_type): + assert all(map(expected_type, tools.match_type_all_numeric(*args))) + + +@pytest.mark.parametrize('args, match_size', [ + ((np.array([1, 2]), 1), True), + ((pd.Series([1, 2]), 1), True), + ((np.array([1, 2]), 1), False), + ((pd.Series([1, 2]), 1), False) +]) +def test_match_type_all_numeric_match_size(args, match_size): + first, second = tools.match_type_all_numeric(*args, match_size=match_size) + + assert type(first) is type(second) + if match_size: + assert first.size == second.size + else: + assert second.size == 1 diff --git a/pvlib/tools.py b/pvlib/tools.py index 5852525a6c..aa01e23b52 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -473,21 +473,20 @@ def _first_order_centered_difference(f, x0, dx=DX, args=()): def match_type_array_like(x, type_of): """ - Convert an array-like value to the Python type of another. - - The dtype of the converted value is preserved. + Convert an array-like value to the array-like Python type of another. The + dtype of the converted value is preserved. Parameters ---------- - x: numeric - The value whose Python type will be converted. + x: array-like + The value whose type will be converted. - type_of: numeric - The value thie the Python type that ``x`` will be converted to. + type_of: array-like + The value with the type to convert to. Returns ------- - The vlaue of ``x`` now stroed in the Python type of ``type_of``. + ``x`` converted to the array-like Python type of ``type_of``. """ if type(x) is type(type_of): return x @@ -495,32 +494,38 @@ def match_type_array_like(x, type_of): return x.to_numpy() try: return pd.Series(data=x, index=type_of.index) - except ValueError: # data and index length do not match + except ValueError: # data and index have different lengths return pd.Series(data=x) def match_type_numeric(x, type_of, match_size=True): """ - Convert a numeric-type value to the Python type of another. - - The dtype of the converted value is preserved. + Convert a numeric-type value to the numeric Python type of another. The + dtype of the converted value is preserved. Parameters ---------- x: numeric - The value whose Python type will be converted. + The value whose type will be converted. type_of: numeric - The value with the Python type that ``x`` will be converted to. + The value with tye type to convert to. - match_size: bool + match_size: bool, default True If ``x`` is a scalar and ``type_of`` is an array-like, return an array-like of ``type_of.size`` that contains ``x`` repeated. Returns ------- numeric - The value of ``x`` now stored in the Python type of ``type_of``. + ``x`` converted to the numeric Python type of ``type_of``. + + Notes + ----- + If ``x`` and ``type_of`` are both scalars, no type conversion is done + because that could change the dtype of ``x``. For example, if + ``type(x) is int`` and ``type(type_of)`` is float, the return type is + still ``int``. """ a_is_array_like = isinstance(x, (np.ndarray, pd.Series)) type_of_is_array_like = isinstance(type_of, (np.ndarray, pd.Series)) @@ -537,17 +542,26 @@ def match_type_numeric(x, type_of, match_size=True): return x # x and type_of are both scalars -def match_type_all_numeric(*args): +def match_type_all_numeric(*args, match_size=True): """ Convert all numeric-type values to the Python type of the first numeric - value passed. + value passed. The dtype of the converted values is preserved. See + :py:func:`pvlib.tools.match_type_numeric` for more details. + + Parameters + ---------- + args: tuple(numeric) + A tuple of numeric-type values. - The dtype of the converted values is preserved. + match_size: bool, default True + If ``x`` is a scalar and ``type_of`` is an array-like, return an + array-like of ``type_of.size`` that contains ``x`` repeated. Returns ------- tuple(numeric) - All of the passed values now stored in the Python type of the first - value passed. + All of the passed values converted to the numeric Python type of the + first value passed. """ - return args[0], *(match_type_numeric(x, args[0]) for x in args[1:]) + return args[0], *(match_type_numeric(x, args[0], match_size=match_size) + for x in args[1:]) From 993243ed7801e1badad0ba90193a476c6af56ba5 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sun, 19 Mar 2023 11:04:28 -0600 Subject: [PATCH 06/19] rename pdSeries clear name decorator and move it to pvlib.tools --- pvlib/pvsystem.py | 66 ++--------------------------------------------- pvlib/tools.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 64 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 78e4ee9dd2..ef324fca01 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1840,69 +1840,7 @@ def get_orientation(self, solar_zenith, solar_azimuth): return tracking_data -def format_return_values(return_types): - def format_val(r, rtype): - if rtype[0] == 'dict-like': - if isinstance(r, (dict, OrderedDict)): - pass - elif isinstance(r, pd.Series): - r.name = rtype[1] - elif rtype[0] == 'numeric': - if isinstance(r, (int, float, np.ndarray)): - pass - elif isinstance(r, pd.Series): - r.name = rtype[1] - elif rtype[0] == 'array-like': - if isinstance(r, np.ndarray): - pass - elif isinstance(r, pd.Series): - r.name = rtype[1] - return r - - def decorator(func): - @functools.wraps(func) - def f(*args, **kwargs): - x = func(*args, **kwargs) - if isinstance(x, tuple): - return tuple(format_val(r, rt) for r, rt in zip(x, return_types)) - return format_val(x, return_types[0]) - return f - return decorator - - -def format_args(func): - """ - Decorator for functions that take dict-like, numeric, or array-like - arguments. - - If a pd.Series is passed to the function, pd.Series.name is cleared - using pd.Series.rename. This does not rename the original pd.Series - passed, and does not copy its data. - - This still allows the function to give a name to it later. - """ - @functools.wraps(func) - def f(*args, **kwargs): - formatted_args = [] - for a in args: - if isinstance(a, pd.Series): - a = a.rename(None) - formatted_args.append(a) - for k, v in kwargs.items(): - if isinstance(v, pd.Series): - kwargs[k] = v.rename(None) - return func(*formatted_args, **kwargs) - return f - - -# @format_return_values([ - # ('numeric', 'photocurrent'), - # ('numeric', 'saturation_current'), - # ('numeric', 'resistance_series'), - # ('numeric', 'resistance_shunt'), - # ('numeric', 'nNsVth') -# ]) -@format_args +@tools.args_clear_pdSeries_name 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, @@ -2219,7 +2157,7 @@ def calcparams_cec(effective_irradiance, temp_cell, irrad_ref=irrad_ref, temp_ref=temp_ref) -@format_args +@tools.args_clear_pdSeries_name def calcparams_pvsyst(effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, diff --git a/pvlib/tools.py b/pvlib/tools.py index aa01e23b52..7260ac16ed 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -7,6 +7,7 @@ import pandas as pd import pytz import warnings +import functools def cosd(angle): @@ -565,3 +566,28 @@ def match_type_all_numeric(*args, match_size=True): """ return args[0], *(match_type_numeric(x, args[0], match_size=match_size) for x in args[1:]) + + +def args_clear_pdSeries_name(func): + """ + Decorator for functions that take dict-like, numeric, or array-like + arguments. + + If a pd.Series is passed to the function, pd.Series.name is cleared + using pd.Series.rename. This does not rename the original pd.Series + passed, and does not copy its data. + + This still allows the decorated function to name returned pd.Series. + """ + @functools.wraps(func) + def f(*args, **kwargs): + formatted_args = [] + for a in args: + if isinstance(a, pd.Series): + a = a.rename(None) + formatted_args.append(a) + for k, v in kwargs.items(): + if isinstance(v, pd.Series): + kwargs[k] = v.rename(None) + return func(*formatted_args, **kwargs) + return f From 44a2a6ae030bcdab299d6bc0ca22b8240474de15 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sun, 19 Mar 2023 11:48:41 -0600 Subject: [PATCH 07/19] adding support for scalar to np.ndarray of any shape --- pvlib/tests/test_tools.py | 13 ++++++++++++- pvlib/tools.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index d4589528b6..18fdede0ea 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -157,6 +157,7 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): @pytest.mark.parametrize('x, type_of, match_size, content_equal', [ + # scalar to array with shape (N,) (1, np.array([1]), True, lambda a, b: a == b.item()), (1, np.array([1, 2]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), (1, np.array([1, 2]), False, lambda a, b: a == b.item()), @@ -174,7 +175,17 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): (1., pd.Series([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), (1., pd.Series([1., 2.]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), (1., pd.Series([1, 2]), False, lambda a, b: a == b.item()), - (1., pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()) + (1., pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), + # scalar to np.ndarray with any shape. this does not work for pd.Series + # because they only have shape (N,) + (1, np.array([[1]]), True, lambda a, b: np.array_equal([[a]], b)), + (1, np.array([[1, 1]]), True, lambda a, b: np.array_equal([[a, a]], b)), + (1, np.array([[1], [1]]), True, lambda a, b: np.array_equal([[a], [a]], b)), + (1, np.array([[[1], [1]], [[1], [1]]]), True, lambda a, b: np.array_equal([[[a], [a]], [[a], [a]]], b)), + (1, np.array([[1]]), False, lambda a, b: a == b.item()), + (1, np.array([[1, 1]]), False, lambda a, b: a == b.item()), + (1, np.array([[1], [1]]), False, lambda a, b: a == b.item()), + (1, np.array([[[1], [1]], [[1], [1]]]), False, lambda a, b: a == b.item()) ]) def test_match_type_numeric_scalar_to_array_like(x, type_of, match_size, content_equal): x_matched = tools.match_type_numeric(x, type_of, match_size=match_size) diff --git a/pvlib/tools.py b/pvlib/tools.py index 7260ac16ed..1f645f840a 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -536,8 +536,8 @@ def match_type_numeric(x, type_of, match_size=True): return _scalar_out(x) if type_of_is_array_like: if isinstance(type_of, np.ndarray): - size = type_of.size if match_size else 1 - return np.full(size, x, dtype=type(x)) + shape = type_of.shape if match_size else 1 + return np.full(shape, x, dtype=type(x)) index = type_of.index if match_size else [type_of.index[0]] return pd.Series(data=x, index=index) return x # x and type_of are both scalars From fabb4eb8ea62bfbd4fa34f0978803a1992aa6e6e Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Sun, 19 Mar 2023 12:27:46 -0600 Subject: [PATCH 08/19] updating match_shape docstring --- pvlib/tests/test_tools.py | 14 +++++++------- pvlib/tools.py | 28 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 18fdede0ea..4250721b22 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -156,7 +156,7 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): assert content_equal(x, x_matched) -@pytest.mark.parametrize('x, type_of, match_size, content_equal', [ +@pytest.mark.parametrize('x, type_of, match_shape, content_equal', [ # scalar to array with shape (N,) (1, np.array([1]), True, lambda a, b: a == b.item()), (1, np.array([1, 2]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), @@ -187,8 +187,8 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): (1, np.array([[1], [1]]), False, lambda a, b: a == b.item()), (1, np.array([[[1], [1]], [[1], [1]]]), False, lambda a, b: a == b.item()) ]) -def test_match_type_numeric_scalar_to_array_like(x, type_of, match_size, content_equal): - x_matched = tools.match_type_numeric(x, type_of, match_size=match_size) +def test_match_type_numeric_scalar_to_array_like(x, type_of, match_shape, content_equal): + x_matched = tools.match_type_numeric(x, type_of, match_shape=match_shape) assert type(x_matched) is type(type_of) assert content_equal(x, x_matched) @@ -230,17 +230,17 @@ def test_match_type_all_numeric(args, expected_type): assert all(map(expected_type, tools.match_type_all_numeric(*args))) -@pytest.mark.parametrize('args, match_size', [ +@pytest.mark.parametrize('args, match_shape', [ ((np.array([1, 2]), 1), True), ((pd.Series([1, 2]), 1), True), ((np.array([1, 2]), 1), False), ((pd.Series([1, 2]), 1), False) ]) -def test_match_type_all_numeric_match_size(args, match_size): - first, second = tools.match_type_all_numeric(*args, match_size=match_size) +def test_match_type_all_numeric_match_size(args, match_shape): + first, second = tools.match_type_all_numeric(*args, match_shape=match_shape) assert type(first) is type(second) - if match_size: + if match_shape: assert first.size == second.size else: assert second.size == 1 diff --git a/pvlib/tools.py b/pvlib/tools.py index 1f645f840a..4dfe221d62 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -499,7 +499,7 @@ def match_type_array_like(x, type_of): return pd.Series(data=x) -def match_type_numeric(x, type_of, match_size=True): +def match_type_numeric(x, type_of, match_shape=True): """ Convert a numeric-type value to the numeric Python type of another. The dtype of the converted value is preserved. @@ -512,9 +512,12 @@ def match_type_numeric(x, type_of, match_size=True): type_of: numeric The value with tye type to convert to. - match_size: bool, default True - If ``x`` is a scalar and ``type_of`` is an array-like, return an - array-like of ``type_of.size`` that contains ``x`` repeated. + match_shape: bool, default True + Let ``x = args[i]`` for some ``i``. + If ``x`` is a scalar and ``args[0]`` is an array-like, convert ``x`` to + an array-like with shape ``args[0].shape`` if + ``type(args[0]) is np.ndarray`` or with shape ``(args[0].size,)`` if + ``type(args[0]) is pd.Series``. Returns ------- @@ -536,14 +539,14 @@ def match_type_numeric(x, type_of, match_size=True): return _scalar_out(x) if type_of_is_array_like: if isinstance(type_of, np.ndarray): - shape = type_of.shape if match_size else 1 + shape = type_of.shape if match_shape else 1 return np.full(shape, x, dtype=type(x)) - index = type_of.index if match_size else [type_of.index[0]] + index = type_of.index if match_shape else [type_of.index[0]] return pd.Series(data=x, index=index) return x # x and type_of are both scalars -def match_type_all_numeric(*args, match_size=True): +def match_type_all_numeric(*args, match_shape=True): """ Convert all numeric-type values to the Python type of the first numeric value passed. The dtype of the converted values is preserved. See @@ -554,9 +557,12 @@ def match_type_all_numeric(*args, match_size=True): args: tuple(numeric) A tuple of numeric-type values. - match_size: bool, default True - If ``x`` is a scalar and ``type_of`` is an array-like, return an - array-like of ``type_of.size`` that contains ``x`` repeated. + match_shape: bool, default True + Let ``x = args[i]`` for some ``i``. + If ``x`` is a scalar and ``args[0]`` is an array-like, convert ``x`` to + an array-like with shape ``args[0].shape`` if + ``type(args[0]) is np.ndarray`` or with shape ``(args[0].size,)`` if + ``type(args[0]) is pd.Series``. Returns ------- @@ -564,7 +570,7 @@ def match_type_all_numeric(*args, match_size=True): All of the passed values converted to the numeric Python type of the first value passed. """ - return args[0], *(match_type_numeric(x, args[0], match_size=match_size) + return args[0], *(match_type_numeric(x, args[0], match_shape=match_shape) for x in args[1:]) From 3cb684582040b14c4a8f3ddc8005c1279dc616ac Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 22 Mar 2023 08:09:57 -0600 Subject: [PATCH 09/19] fixing syntax error --- pvlib/tools.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 4dfe221d62..6a1c737ea1 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -570,8 +570,12 @@ def match_type_all_numeric(*args, match_shape=True): All of the passed values converted to the numeric Python type of the first value passed. """ - return args[0], *(match_type_numeric(x, args[0], match_shape=match_shape) - for x in args[1:]) + type_of = args[0] + converted_values = type_of, *( + match_type_numeric(x, type_of, match_shape=match_shape) + for x in args[1:] + ) + return converted_values def args_clear_pdSeries_name(func): From 96b558bd2e0734d2b695cad46dfa1a0bbee23773 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 22 Mar 2023 10:40:00 -0600 Subject: [PATCH 10/19] fix stickler issues --- pvlib/tests/test_pvsystem.py | 44 +++++++++-------- pvlib/tests/test_tools.py | 94 ++++++++++++++++++++++++------------ 2 files changed, 85 insertions(+), 53 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 609c68c787..00a833e60b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -752,16 +752,17 @@ def test_calcparams_desoto(cec_module_params): }, index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - 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) + 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) @@ -791,17 +792,18 @@ def test_calcparams_cec(cec_module_params): }, index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_cec( - 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) + 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) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 4250721b22..87253bc979 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -1,5 +1,4 @@ import pytest -from .conftest import assert_series_equal from pvlib import tools import numpy as np @@ -106,30 +105,50 @@ def get_match_type_array_like_test_cases(): (np.array([1]), np.array([1.]), lambda a, b: np.array_equal(a, b)), (np.array([1.]), np.array([1]), lambda a, b: np.array_equal(a, b)), (np.array([1.]), np.array([1.]), lambda a, b: np.array_equal(a, b)), - (pd.Series([1]), pd.Series([1]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - (pd.Series([1]), pd.Series([1.]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - (pd.Series([1.]), pd.Series([1]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - (pd.Series([1.]), pd.Series([1.]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1]), pd.Series([1]), + lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1]), pd.Series([1.]), + lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1.]), pd.Series([1]), + lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (pd.Series([1.]), pd.Series([1.]), + lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), # np.ndarray to pd.Series - (np.array([1]), pd.Series([1]), lambda a, b: np.array_equal(a, b.to_numpy())), - (np.array([1]), pd.Series([1.]), lambda a, b: np.array_equal(a, b.to_numpy())), - (np.array([1.]), pd.Series([1]), lambda a, b: np.array_equal(a, b.to_numpy())), - (np.array([1.]), pd.Series([1.]), lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1]), pd.Series([1]), + lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1]), pd.Series([1.]), + lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1.]), pd.Series([1]), + lambda a, b: np.array_equal(a, b.to_numpy())), + (np.array([1.]), pd.Series([1.]), + lambda a, b: np.array_equal(a, b.to_numpy())), # pd.Series to np.ndarray - (pd.Series([1]), np.array([1]), lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1]), np.array([1.]), lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1.]), np.array([1]), lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1.]), np.array([1.]), lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1]), np.array([1]), + lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1]), np.array([1.]), + lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1.]), np.array([1]), + lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1.]), np.array([1.]), + lambda a, b: np.array_equal(a.to_numpy(), b)), # x shorter than type_of - (np.array([1]), np.array([1, 2]), lambda a, b: np.array_equal(a, b)), - (np.array([1]), pd.Series([1, 2]), lambda a, b: np.array_equal(a, b.to_numpy())), - (pd.Series([1]), np.array([1, 2]), lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1]), pd.Series([1, 2]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), + (np.array([1]), np.array([1, 2]), + lambda a, b: np.array_equal(a, b)), + (np.array([1]), pd.Series([1, 2]), + lambda a, b: np.array_equal(a, b.to_numpy())), + (pd.Series([1]), np.array([1, 2]), + lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1]), pd.Series([1, 2]), + lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), # x longer than type_of - (np.array([1, 2]), np.array([1]), lambda a, b: np.array_equal(a, b)), - (np.array([1, 2]), pd.Series([1]), lambda a, b: np.array_equal(a, b.to_numpy())), - (pd.Series([1, 2]), np.array([1]), lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1, 2]), pd.Series([1]), lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())) + (np.array([1, 2]), np.array([1]), + lambda a, b: np.array_equal(a, b)), + (np.array([1, 2]), pd.Series([1]), + lambda a, b: np.array_equal(a, b.to_numpy())), + (pd.Series([1, 2]), np.array([1]), + lambda a, b: np.array_equal(a.to_numpy(), b)), + (pd.Series([1, 2]), pd.Series([1]), + lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())) ] @@ -159,12 +178,15 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): @pytest.mark.parametrize('x, type_of, match_shape, content_equal', [ # scalar to array with shape (N,) (1, np.array([1]), True, lambda a, b: a == b.item()), - (1, np.array([1, 2]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), + (1, np.array([1, 2]), True, + lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), (1, np.array([1, 2]), False, lambda a, b: a == b.item()), (1, np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), (1, pd.Series([1]), True, lambda a, b: a == b.item()), - (1, pd.Series([1, 2]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), - (1, pd.Series([1., 2.]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), + (1, pd.Series([1, 2]), True, + lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), + (1, pd.Series([1., 2.]), True, + lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), (1, pd.Series([1, 2]), False, lambda a, b: a == b.item()), (1, pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), (1., np.array([1]), True, lambda a, b: a == b.item()), @@ -173,21 +195,25 @@ def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): (1., np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), (1., pd.Series([1]), True, lambda a, b: a == b.item()), (1., pd.Series([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), - (1., pd.Series([1., 2.]), True, lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), + (1., pd.Series([1., 2.]), True, + lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), (1., pd.Series([1, 2]), False, lambda a, b: a == b.item()), (1., pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), # scalar to np.ndarray with any shape. this does not work for pd.Series # because they only have shape (N,) (1, np.array([[1]]), True, lambda a, b: np.array_equal([[a]], b)), (1, np.array([[1, 1]]), True, lambda a, b: np.array_equal([[a, a]], b)), - (1, np.array([[1], [1]]), True, lambda a, b: np.array_equal([[a], [a]], b)), - (1, np.array([[[1], [1]], [[1], [1]]]), True, lambda a, b: np.array_equal([[[a], [a]], [[a], [a]]], b)), + (1, np.array([[1], [1]]), True, + lambda a, b: np.array_equal([[a], [a]], b)), + (1, np.array([[[1], [1]], [[1], [1]]]), True, + lambda a, b: np.array_equal([[[a], [a]], [[a], [a]]], b)), (1, np.array([[1]]), False, lambda a, b: a == b.item()), (1, np.array([[1, 1]]), False, lambda a, b: a == b.item()), (1, np.array([[1], [1]]), False, lambda a, b: a == b.item()), (1, np.array([[[1], [1]], [[1], [1]]]), False, lambda a, b: a == b.item()) ]) -def test_match_type_numeric_scalar_to_array_like(x, type_of, match_shape, content_equal): +def test_match_type_numeric_scalar_to_array_like(x, type_of, match_shape, + content_equal): x_matched = tools.match_type_numeric(x, type_of, match_shape=match_shape) assert type(x_matched) is type(type_of) @@ -214,7 +240,8 @@ def test_match_type_numeric_array_like_to_scalar(x, type_of, content_equal): @pytest.mark.parametrize('x, type_of, content_equal', [ *get_match_type_array_like_test_cases() ]) -def test_match_type_numeric_array_like_to_array_like(x, type_of, content_equal): +def test_match_type_numeric_array_like_to_array_like(x, type_of, + content_equal): x_matched = tools.match_type_numeric(x, type_of) assert type(x_matched) is type(type_of) @@ -223,8 +250,10 @@ def test_match_type_numeric_array_like_to_array_like(x, type_of, content_equal): @pytest.mark.parametrize('args, expected_type', [ ((1, np.array([1]), pd.Series([1])), np.isscalar), - ((np.array([1]), 1, np.array([1]), pd.Series([1])), lambda a: isinstance(a, np.ndarray)), - ((pd.Series([1]), 1, np.array([1]), pd.Series([1])), lambda a: isinstance(a, pd.Series)), + ((np.array([1]), 1, np.array([1]), pd.Series([1])), + lambda a: isinstance(a, np.ndarray)), + ((pd.Series([1]), 1, np.array([1]), pd.Series([1])), + lambda a: isinstance(a, pd.Series)) ]) def test_match_type_all_numeric(args, expected_type): assert all(map(expected_type, tools.match_type_all_numeric(*args))) @@ -237,7 +266,8 @@ def test_match_type_all_numeric(args, expected_type): ((pd.Series([1, 2]), 1), False) ]) def test_match_type_all_numeric_match_size(args, match_shape): - first, second = tools.match_type_all_numeric(*args, match_shape=match_shape) + first, second = tools.match_type_all_numeric(*args, + match_shape=match_shape) assert type(first) is type(second) if match_shape: From 0547ed78fb660228cedb4022352fe090a0a336ea Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 24 May 2023 10:15:38 -0700 Subject: [PATCH 11/19] different approach --- pvlib/pvsystem.py | 26 ++++- pvlib/tests/test_pvsystem.py | 8 -- pvlib/tests/test_tools.py | 185 ++--------------------------------- pvlib/tools.py | 131 +++---------------------- 4 files changed, 43 insertions(+), 307 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 9b82e13344..36b5c87278 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1843,7 +1843,6 @@ def get_orientation(self, solar_zenith, solar_azimuth): return tracking_data -@tools.args_clear_pdSeries_name 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, @@ -2041,7 +2040,17 @@ def calcparams_desoto(effective_irradiance, temp_cell, Rs = R_s - return tools.match_type_all_numeric(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 not None: + return tuple(pd.Series(a, index=index).rename(None) for a in out) + + return np.broadcast_arrays(*out) def calcparams_cec(effective_irradiance, temp_cell, @@ -2160,7 +2169,6 @@ def calcparams_cec(effective_irradiance, temp_cell, irrad_ref=irrad_ref, temp_ref=temp_ref) -@tools.args_clear_pdSeries_name def calcparams_pvsyst(effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, @@ -2297,7 +2305,17 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, Rs = R_s - return tools.match_type_all_numeric(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 not None: + return tuple(pd.Series(a, index=index).rename(None) for a in out) + + return np.broadcast_arrays(*out) def retrieve_sam(name=None, path=None): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index bd8bfb5b61..b2af430b3f 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -775,14 +775,6 @@ def test_calcparams_desoto(cec_module_params): assert_series_equal(nNsVth, pd.Series([0.473, 0.473, 0.5127], index=times), check_less_precise=3) - pdSeries_names = set(df.columns) - - assert IL.name not in pdSeries_names - assert I0.name not in pdSeries_names - assert Rs.name not in pdSeries_names - assert Rsh.name not in pdSeries_names - assert nNsVth.name not in pdSeries_names - def test_calcparams_cec(cec_module_params): times = pd.date_range(start='2015-01-01', periods=3, freq='12H') diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 87253bc979..e85c8cef5d 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -98,179 +98,14 @@ def test_degrees_to_index_1(): tools._degrees_to_index(degrees=22.0, coordinate='width') -def get_match_type_array_like_test_cases(): - return [ - # identity - (np.array([1]), np.array([1]), lambda a, b: np.array_equal(a, b)), - (np.array([1]), np.array([1.]), lambda a, b: np.array_equal(a, b)), - (np.array([1.]), np.array([1]), lambda a, b: np.array_equal(a, b)), - (np.array([1.]), np.array([1.]), lambda a, b: np.array_equal(a, b)), - (pd.Series([1]), pd.Series([1]), - lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - (pd.Series([1]), pd.Series([1.]), - lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - (pd.Series([1.]), pd.Series([1]), - lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - (pd.Series([1.]), pd.Series([1.]), - lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - # np.ndarray to pd.Series - (np.array([1]), pd.Series([1]), - lambda a, b: np.array_equal(a, b.to_numpy())), - (np.array([1]), pd.Series([1.]), - lambda a, b: np.array_equal(a, b.to_numpy())), - (np.array([1.]), pd.Series([1]), - lambda a, b: np.array_equal(a, b.to_numpy())), - (np.array([1.]), pd.Series([1.]), - lambda a, b: np.array_equal(a, b.to_numpy())), - # pd.Series to np.ndarray - (pd.Series([1]), np.array([1]), - lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1]), np.array([1.]), - lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1.]), np.array([1]), - lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1.]), np.array([1.]), - lambda a, b: np.array_equal(a.to_numpy(), b)), - # x shorter than type_of - (np.array([1]), np.array([1, 2]), - lambda a, b: np.array_equal(a, b)), - (np.array([1]), pd.Series([1, 2]), - lambda a, b: np.array_equal(a, b.to_numpy())), - (pd.Series([1]), np.array([1, 2]), - lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1]), pd.Series([1, 2]), - lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())), - # x longer than type_of - (np.array([1, 2]), np.array([1]), - lambda a, b: np.array_equal(a, b)), - (np.array([1, 2]), pd.Series([1]), - lambda a, b: np.array_equal(a, b.to_numpy())), - (pd.Series([1, 2]), np.array([1]), - lambda a, b: np.array_equal(a.to_numpy(), b)), - (pd.Series([1, 2]), pd.Series([1]), - lambda a, b: np.array_equal(a.to_numpy(), b.to_numpy())) - ] - - -@pytest.mark.parametrize('x, type_of, content_equal', [ - *get_match_type_array_like_test_cases() -]) -def test_match_type_array_like(x, type_of, content_equal): - x_matched = tools.match_type_array_like(x, type_of) - - assert type(x_matched) is type(type_of) - assert content_equal(x, x_matched) - - -@pytest.mark.parametrize('x, type_of, content_equal', [ - (1, 1, lambda a, b: a == b), - (1, 1., lambda a, b: a == b), - (1., 1, lambda a, b: a == b), - (1., 1., lambda a, b: a == b) -]) -def test_match_type_numeric_scalar_to_scalar(x, type_of, content_equal): - x_matched = tools.match_type_numeric(x, type_of) - - assert type(x) is type(x_matched) - assert content_equal(x, x_matched) - - -@pytest.mark.parametrize('x, type_of, match_shape, content_equal', [ - # scalar to array with shape (N,) - (1, np.array([1]), True, lambda a, b: a == b.item()), - (1, np.array([1, 2]), True, - lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), - (1, np.array([1, 2]), False, lambda a, b: a == b.item()), - (1, np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), - (1, pd.Series([1]), True, lambda a, b: a == b.item()), - (1, pd.Series([1, 2]), True, - lambda a, b: np.array_equal(np.array([a, a], dtype=int), b)), - (1, pd.Series([1., 2.]), True, - lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), - (1, pd.Series([1, 2]), False, lambda a, b: a == b.item()), - (1, pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), - (1., np.array([1]), True, lambda a, b: a == b.item()), - (1., np.array([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), - (1., np.array([1, 2]), False, lambda a, b: a == b.item()), - (1., np.array([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), - (1., pd.Series([1]), True, lambda a, b: a == b.item()), - (1., pd.Series([1, 2]), True, lambda a, b: np.array_equal([a, a], b)), - (1., pd.Series([1., 2.]), True, - lambda a, b: np.array_equal(np.array([a, a], dtype=np.float64), b)), - (1., pd.Series([1, 2]), False, lambda a, b: a == b.item()), - (1., pd.Series([1., 2.]), False, lambda a, b: np.float64(a) == b.item()), - # scalar to np.ndarray with any shape. this does not work for pd.Series - # because they only have shape (N,) - (1, np.array([[1]]), True, lambda a, b: np.array_equal([[a]], b)), - (1, np.array([[1, 1]]), True, lambda a, b: np.array_equal([[a, a]], b)), - (1, np.array([[1], [1]]), True, - lambda a, b: np.array_equal([[a], [a]], b)), - (1, np.array([[[1], [1]], [[1], [1]]]), True, - lambda a, b: np.array_equal([[[a], [a]], [[a], [a]]], b)), - (1, np.array([[1]]), False, lambda a, b: a == b.item()), - (1, np.array([[1, 1]]), False, lambda a, b: a == b.item()), - (1, np.array([[1], [1]]), False, lambda a, b: a == b.item()), - (1, np.array([[[1], [1]], [[1], [1]]]), False, lambda a, b: a == b.item()) -]) -def test_match_type_numeric_scalar_to_array_like(x, type_of, match_shape, - content_equal): - x_matched = tools.match_type_numeric(x, type_of, match_shape=match_shape) - - assert type(x_matched) is type(type_of) - assert content_equal(x, x_matched) - - -@pytest.mark.parametrize('x, type_of, content_equal', [ - (np.array([1]), 1, lambda a, b: a.item() == b), - (np.array([1.]), 1, lambda a, b: a.item() == b), - (np.array([1]), 1., lambda a, b: a.item() == b), - (np.array([1.]), 1., lambda a, b: a.item() == b), - (pd.Series([1]), 1, lambda a, b: a.item() == b), - (pd.Series([1.]), 1, lambda a, b: a.item() == b), - (pd.Series([1]), 1., lambda a, b: a.item() == b), - (pd.Series([1.]), 1., lambda a, b: a.item() == b) +@pytest.mark.parametrize('args, item_idx', [ + ((pd.DataFrame([1], index=[1]),), 0), + ((pd.Series([1], index=[1]),), 0), + ((1, pd.Series([1], index=[1]),), 1), + ((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_match_type_numeric_array_like_to_scalar(x, type_of, content_equal): - x_matched = tools.match_type_numeric(x, type_of) - - assert type(x.item()) is type(x_matched) - assert content_equal(x, x_matched) - - -@pytest.mark.parametrize('x, type_of, content_equal', [ - *get_match_type_array_like_test_cases() -]) -def test_match_type_numeric_array_like_to_array_like(x, type_of, - content_equal): - x_matched = tools.match_type_numeric(x, type_of) - - assert type(x_matched) is type(type_of) - assert content_equal(x, x_matched) - - -@pytest.mark.parametrize('args, expected_type', [ - ((1, np.array([1]), pd.Series([1])), np.isscalar), - ((np.array([1]), 1, np.array([1]), pd.Series([1])), - lambda a: isinstance(a, np.ndarray)), - ((pd.Series([1]), 1, np.array([1]), pd.Series([1])), - lambda a: isinstance(a, pd.Series)) -]) -def test_match_type_all_numeric(args, expected_type): - assert all(map(expected_type, tools.match_type_all_numeric(*args))) - - -@pytest.mark.parametrize('args, match_shape', [ - ((np.array([1, 2]), 1), True), - ((pd.Series([1, 2]), 1), True), - ((np.array([1, 2]), 1), False), - ((pd.Series([1, 2]), 1), False) -]) -def test_match_type_all_numeric_match_size(args, match_shape): - first, second = tools.match_type_all_numeric(*args, - match_shape=match_shape) - - assert type(first) is type(second) - if match_shape: - assert first.size == second.size - else: - assert second.size == 1 +def test_get_pandas_index(args, item_idx): + pd.testing.assert_index_equal( + args[item_idx].index, tools.get_pandas_index(*args) + ) diff --git a/pvlib/tools.py b/pvlib/tools.py index dfc93d04b0..775d9ddb98 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -472,132 +472,23 @@ def _first_order_centered_difference(f, x0, dx=DX, args=()): return df / 2 / dx -def match_type_array_like(x, type_of): +def get_pandas_index(*args): """ - Convert an array-like value to the array-like Python type of another. The - dtype of the converted value is preserved. - - Parameters - ---------- - x: array-like - The value whose type will be converted. - - type_of: array-like - The value with the type to convert to. - - Returns - ------- - ``x`` converted to the array-like Python type of ``type_of``. - """ - if type(x) is type(type_of): - return x - if isinstance(type_of, np.ndarray): - return x.to_numpy() - try: - return pd.Series(data=x, index=type_of.index) - except ValueError: # data and index have different lengths - return pd.Series(data=x) - - -def match_type_numeric(x, type_of, match_shape=True): - """ - Convert a numeric-type value to the numeric Python type of another. The - dtype of the converted value is preserved. - - Parameters - ---------- - x: numeric - The value whose type will be converted. - - type_of: numeric - The value with tye type to convert to. - - match_shape: bool, default True - Let ``x = args[i]`` for some ``i``. - If ``x`` is a scalar and ``args[0]`` is an array-like, convert ``x`` to - an array-like with shape ``args[0].shape`` if - ``type(args[0]) is np.ndarray`` or with shape ``(args[0].size,)`` if - ``type(args[0]) is pd.Series``. - - Returns - ------- - numeric - ``x`` converted to the numeric Python type of ``type_of``. - - Notes - ----- - If ``x`` and ``type_of`` are both scalars, no type conversion is done - because that could change the dtype of ``x``. For example, if - ``type(x) is int`` and ``type(type_of)`` is float, the return type is - still ``int``. - """ - a_is_array_like = isinstance(x, (np.ndarray, pd.Series)) - type_of_is_array_like = isinstance(type_of, (np.ndarray, pd.Series)) - if a_is_array_like and type_of_is_array_like: - return match_type_array_like(x, type_of) - if a_is_array_like: - return _scalar_out(x) - if type_of_is_array_like: - if isinstance(type_of, np.ndarray): - shape = type_of.shape if match_shape else 1 - return np.full(shape, x, dtype=type(x)) - index = type_of.index if match_shape else [type_of.index[0]] - return pd.Series(data=x, index=index) - return x # x and type_of are both scalars - - -def match_type_all_numeric(*args, match_shape=True): - """ - Convert all numeric-type values to the Python type of the first numeric - value passed. The dtype of the converted values is preserved. See - :py:func:`pvlib.tools.match_type_numeric` for more details. + Get the index of the first pandas DataFrame or Series in a list of + arguments. Parameters ---------- - args: tuple(numeric) - A tuple of numeric-type values. - - match_shape: bool, default True - Let ``x = args[i]`` for some ``i``. - If ``x`` is a scalar and ``args[0]`` is an array-like, convert ``x`` to - an array-like with shape ``args[0].shape`` if - ``type(args[0]) is np.ndarray`` or with shape ``(args[0].size,)`` if - ``type(args[0]) is pd.Series``. + args: positional arguments + The numeric values to scan for a pandas index. Returns ------- - tuple(numeric) - All of the passed values converted to the numeric Python type of the - first value passed. + A pandas index or None + None is returned if there are no pandas DataFrames or Series is the + args list. """ - type_of = args[0] - converted_values = type_of, *( - match_type_numeric(x, type_of, match_shape=match_shape) - for x in args[1:] + return next( + (a.index for a in args if isinstance(a, (pd.DataFrame, pd.Series))), + None ) - return converted_values - - -def args_clear_pdSeries_name(func): - """ - Decorator for functions that take dict-like, numeric, or array-like - arguments. - - If a pd.Series is passed to the function, pd.Series.name is cleared - using pd.Series.rename. This does not rename the original pd.Series - passed, and does not copy its data. - - This still allows the decorated function to name returned pd.Series. - """ - @functools.wraps(func) - def f(*args, **kwargs): - formatted_args = [] - for a in args: - if isinstance(a, pd.Series): - a = a.rename(None) - formatted_args.append(a) - for k, v in kwargs.items(): - if isinstance(v, pd.Series): - kwargs[k] = v.rename(None) - return func(*formatted_args, **kwargs) - return f From dd2f64b5153f1b89e7ae88a37ff69eef703a8e22 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 24 May 2023 10:19:48 -0700 Subject: [PATCH 12/19] remove unnecessary asserts --- gist.py | 86 ++++++++++++++++++++++++++++++++++++ pvlib/tests/test_pvsystem.py | 16 ------- pvlib/tools.py | 1 - 3 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 gist.py diff --git a/gist.py b/gist.py new file mode 100644 index 0000000000..ab3df9f5fe --- /dev/null +++ b/gist.py @@ -0,0 +1,86 @@ +import numpy as np +import pandas as pd +import pvlib +import matplotlib.pyplot as plt + + +modules = pvlib.pvsystem.retrieve_sam('CECMod') +cec_model = modules['Canadian_Solar_Inc__CS6P_235P'] + +STC = pd.DataFrame( + {'effective_irradiance': + np.array([1000.]), + 'temp_cell': + np.array([25.]) + }) + +IEC61853 = pd.DataFrame( + {'effective_irradiance': + np.array([100., 100., 200., 200., 400., 400., 400., + 600., 600., 600., 600., 800., 800., 800., 800., + 1000., 1000., 1000., 1000., 1100., 1100., 1100.]), + 'temp_cell': + np.array([15., 25., 15., 25., 15., 25., 50., + 15., 25., 50., 75., 15., 25., 50., 75., + 15., 25., 50., 75., 25., 50., 75.]) + }) + + +# Constant irradiance and temperature +cec_stc_params_const = pvlib.pvsystem.calcparams_cec( + 1000., 25., + alpha_sc=cec_model['alpha_sc'], a_ref=cec_model['a_ref'], + I_L_ref=cec_model['I_L_ref'], I_o_ref=cec_model['I_o_ref'], + R_sh_ref=cec_model['R_sh_ref'], R_s=cec_model['R_s'], + Adjust=cec_model['Adjust']) + +# Single irradiance and temperature value but use Series +cec_stc_params_series = pvlib.pvsystem.calcparams_cec( + STC['effective_irradiance'], STC['temp_cell'], + alpha_sc=cec_model['alpha_sc'], a_ref=cec_model['a_ref'], + I_L_ref=cec_model['I_L_ref'], I_o_ref=cec_model['I_o_ref'], + R_sh_ref=cec_model['R_sh_ref'], R_s=cec_model['R_s'], + Adjust=cec_model['Adjust']) + +print('When Series are input, output from calcparams inherits names from input Series') +print(cec_stc_params_series) +print() + +cec_iec_params = pvlib.pvsystem.calcparams_cec( + IEC61853['effective_irradiance'], IEC61853['temp_cell'], + alpha_sc=cec_model['alpha_sc'], a_ref=cec_model['a_ref'], + I_L_ref=cec_model['I_L_ref'], I_o_ref=cec_model['I_o_ref'], + R_sh_ref=cec_model['R_sh_ref'], R_s=cec_model['R_s'], + Adjust=cec_model['Adjust']) + +iv_stc = pvlib.pvsystem.singlediode(*cec_stc_params_const) +print('Irradiance and temperature are floats') +print('Returned items have different types') +for k, v in iv_stc.items(): + print(k, type(v)) +print() + +iv_stc_series = pvlib.pvsystem.singlediode(*cec_stc_params_series, ivcurve_pnts=100) +print('Irradiance and temperature are Series of length 1') +print('All returned items are numpy arrays, but v and i are 1x100 so the v, i ' + 'plot is 100 Line2D objects') +for k, v in iv_stc_series.items(): + print(k, v.shape) +plt.figure() +plt.plot(iv_stc_series['v'], iv_stc_series['i'], '.') +print(' Have to transpose to get an IV curve plot') +plt.figure() +plt.plot(iv_stc_series['v'].T, iv_stc_series['i'].T, '.') +print() + +iv_iec_series = pvlib.pvsystem.singlediode(*cec_iec_params) +print('Irradiance and temperature are Series with length > 1') +print('Returned items are columns in a DataFrame') +print(iv_iec_series.head()) +print() +iv_iec_series_with_pnts = pvlib.pvsystem.singlediode(*cec_iec_params, + ivcurve_pnts=100) +print('Returns an OrderedDict') +print('IV curve data are by rows so must be transposted to plot') +for k, v in iv_iec_series_with_pnts.items(): + print(k, v.shape) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index b2af430b3f..ddbe57a6c3 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -808,14 +808,6 @@ def test_calcparams_cec(cec_module_params): assert_series_equal(nNsVth, pd.Series([0.473, 0.473, 0.5127], index=times), check_less_precise=3) - pdSeries_names = set(df.columns) - - assert IL.name not in pdSeries_names - assert I0.name not in pdSeries_names - assert Rs.name not in pdSeries_names - assert Rsh.name not in pdSeries_names - assert nNsVth.name not in pdSeries_names - def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): """ @@ -886,14 +878,6 @@ def test_calcparams_pvsyst(pvsyst_module_params): assert_series_equal( nNsVth.round(decimals=4), pd.Series([1.6186, 1.7961], index=times)) - pdSeries_names = set(df.columns) - - assert IL.name not in pdSeries_names - assert I0.name not in pdSeries_names - assert Rs.name not in pdSeries_names - assert Rsh.name not in pdSeries_names - assert nNsVth.name not in pdSeries_names - def test_PVSystem_calcparams_desoto(cec_module_params, mocker): mocker.spy(pvsystem, 'calcparams_desoto') diff --git a/pvlib/tools.py b/pvlib/tools.py index 775d9ddb98..1d63b2076a 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -7,7 +7,6 @@ import pandas as pd import pytz import warnings -import functools def cosd(angle): From 86125000c5c127a1c521d9726be3a1bf220d0fb7 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 24 May 2023 10:21:08 -0700 Subject: [PATCH 13/19] remove file --- gist.py | 86 --------------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 gist.py diff --git a/gist.py b/gist.py deleted file mode 100644 index ab3df9f5fe..0000000000 --- a/gist.py +++ /dev/null @@ -1,86 +0,0 @@ -import numpy as np -import pandas as pd -import pvlib -import matplotlib.pyplot as plt - - -modules = pvlib.pvsystem.retrieve_sam('CECMod') -cec_model = modules['Canadian_Solar_Inc__CS6P_235P'] - -STC = pd.DataFrame( - {'effective_irradiance': - np.array([1000.]), - 'temp_cell': - np.array([25.]) - }) - -IEC61853 = pd.DataFrame( - {'effective_irradiance': - np.array([100., 100., 200., 200., 400., 400., 400., - 600., 600., 600., 600., 800., 800., 800., 800., - 1000., 1000., 1000., 1000., 1100., 1100., 1100.]), - 'temp_cell': - np.array([15., 25., 15., 25., 15., 25., 50., - 15., 25., 50., 75., 15., 25., 50., 75., - 15., 25., 50., 75., 25., 50., 75.]) - }) - - -# Constant irradiance and temperature -cec_stc_params_const = pvlib.pvsystem.calcparams_cec( - 1000., 25., - alpha_sc=cec_model['alpha_sc'], a_ref=cec_model['a_ref'], - I_L_ref=cec_model['I_L_ref'], I_o_ref=cec_model['I_o_ref'], - R_sh_ref=cec_model['R_sh_ref'], R_s=cec_model['R_s'], - Adjust=cec_model['Adjust']) - -# Single irradiance and temperature value but use Series -cec_stc_params_series = pvlib.pvsystem.calcparams_cec( - STC['effective_irradiance'], STC['temp_cell'], - alpha_sc=cec_model['alpha_sc'], a_ref=cec_model['a_ref'], - I_L_ref=cec_model['I_L_ref'], I_o_ref=cec_model['I_o_ref'], - R_sh_ref=cec_model['R_sh_ref'], R_s=cec_model['R_s'], - Adjust=cec_model['Adjust']) - -print('When Series are input, output from calcparams inherits names from input Series') -print(cec_stc_params_series) -print() - -cec_iec_params = pvlib.pvsystem.calcparams_cec( - IEC61853['effective_irradiance'], IEC61853['temp_cell'], - alpha_sc=cec_model['alpha_sc'], a_ref=cec_model['a_ref'], - I_L_ref=cec_model['I_L_ref'], I_o_ref=cec_model['I_o_ref'], - R_sh_ref=cec_model['R_sh_ref'], R_s=cec_model['R_s'], - Adjust=cec_model['Adjust']) - -iv_stc = pvlib.pvsystem.singlediode(*cec_stc_params_const) -print('Irradiance and temperature are floats') -print('Returned items have different types') -for k, v in iv_stc.items(): - print(k, type(v)) -print() - -iv_stc_series = pvlib.pvsystem.singlediode(*cec_stc_params_series, ivcurve_pnts=100) -print('Irradiance and temperature are Series of length 1') -print('All returned items are numpy arrays, but v and i are 1x100 so the v, i ' - 'plot is 100 Line2D objects') -for k, v in iv_stc_series.items(): - print(k, v.shape) -plt.figure() -plt.plot(iv_stc_series['v'], iv_stc_series['i'], '.') -print(' Have to transpose to get an IV curve plot') -plt.figure() -plt.plot(iv_stc_series['v'].T, iv_stc_series['i'].T, '.') -print() - -iv_iec_series = pvlib.pvsystem.singlediode(*cec_iec_params) -print('Irradiance and temperature are Series with length > 1') -print('Returned items are columns in a DataFrame') -print(iv_iec_series.head()) -print() -iv_iec_series_with_pnts = pvlib.pvsystem.singlediode(*cec_iec_params, - ivcurve_pnts=100) -print('Returns an OrderedDict') -print('IV curve data are by rows so must be transposted to plot') -for k, v in iv_iec_series_with_pnts.items(): - print(k, v.shape) From 43945f3292456e6a3f90839940b3e05c98c976ad Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 24 May 2023 10:34:31 -0700 Subject: [PATCH 14/19] update whatsnew --- docs/sphinx/source/whatsnew/v0.10.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index a4406072f8..ddc2f5df17 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -21,6 +21,10 @@ 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. (:issue:`1626`, :pull:`1700`) Bug fixes From d86bc10daed1d713838841d4080f0236712de0e8 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Wed, 7 Jun 2023 09:05:20 -0700 Subject: [PATCH 15/19] add tests for all scalar inputs, and pass np.arrays to assert_allclose --- pvlib/tests/test_pvsystem.py | 100 +++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index ddbe57a6c3..548ba8fe54 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -744,6 +744,49 @@ def test_Array__infer_cell_type(): assert array._infer_cell_type() is None +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_desoto(cec_module_params): times = pd.date_range(start='2015-01-01', periods=3, freq='12H') df = pd.DataFrame({ @@ -890,21 +933,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): @@ -916,23 +961,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) From 5d4f6d497b23d391ce814e0007e306c5b20bd6af Mon Sep 17 00:00:00 2001 From: Taos Transue <41020789+reepoi@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:07:40 -0700 Subject: [PATCH 16/19] Update docs/sphinx/source/whatsnew/v0.10.0.rst Co-authored-by: Cliff Hansen --- docs/sphinx/source/whatsnew/v0.10.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index ddc2f5df17..3fd5d5ba85 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -24,7 +24,7 @@ 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. (:issue:`1626`, :pull:`1700`) + the same Python type as the `effective_irradiance` and `temp_cell` parameters. (:issue:`1626`, :pull:`1700`) Bug fixes From 05a7de6c402bbe5e8a34df7df61f10a80f3ac41a Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Thu, 8 Jun 2023 11:03:59 -0700 Subject: [PATCH 17/19] if condition swap, tests for tools.get_pandas_index, tests for returned Python type --- pvlib/pvsystem.py | 14 ++-- pvlib/tests/test_pvsystem.py | 123 +++++++++++++++++++++++++++++++++++ pvlib/tests/test_tools.py | 21 ++++-- 3 files changed, 147 insertions(+), 11 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 36b5c87278..0cd6db2f63 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2047,10 +2047,11 @@ def calcparams_desoto(effective_irradiance, temp_cell, return out index = tools.get_pandas_index(*numeric_args) - if index is not None: - return tuple(pd.Series(a, index=index).rename(None) for a in out) - return np.broadcast_arrays(*out) + 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, @@ -2312,10 +2313,11 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, return out index = tools.get_pandas_index(*numeric_args) - if index is not None: - return tuple(pd.Series(a, index=index).rename(None) for a in out) - return np.broadcast_arrays(*out) + 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 548ba8fe54..e48acef75d 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 @@ -744,6 +745,128 @@ 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_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_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, diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index e85c8cef5d..583141a726 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -98,14 +98,25 @@ def test_degrees_to_index_1(): tools._degrees_to_index(degrees=22.0, coordinate='width') -@pytest.mark.parametrize('args, item_idx', [ +@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, item_idx): - pd.testing.assert_index_equal( - args[item_idx].index, tools.get_pandas_index(*args) - ) +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) From 5e4c16c59ec74aa83218c3e676cf6895730acfe0 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Thu, 8 Jun 2023 12:16:42 -0700 Subject: [PATCH 18/19] fix typo in docstring --- pvlib/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 1d63b2076a..ffc2b122c1 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -484,7 +484,7 @@ def get_pandas_index(*args): Returns ------- A pandas index or None - None is returned if there are no pandas DataFrames or Series is the + None is returned if there are no pandas DataFrames or Series in the args list. """ return next( From 0ff0f45d2d902616affb9d34624b65ada04a9460 Mon Sep 17 00:00:00 2001 From: Taos Transue Date: Tue, 13 Jun 2023 09:08:59 -0700 Subject: [PATCH 19/19] correct calcparams_cec test, and add test for calcparams_pvsyst all scalars --- pvlib/tests/test_pvsystem.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index e48acef75d..7776fc5eb0 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -825,7 +825,7 @@ def test_calcparams_cec_returns_correct_Python_type(numeric_type_funcs, effective_irradiance=numeric_type_funcs[0](800.0), temp_cell=numeric_type_funcs[1](25), ) - out = pvsystem.calcparams_desoto( + out = pvsystem.calcparams_cec( **numeric_args, alpha_sc=cec_module_params['alpha_sc'], a_ref=cec_module_params['a_ref'], @@ -833,6 +833,7 @@ def test_calcparams_cec_returns_correct_Python_type(numeric_type_funcs, 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 ) @@ -910,6 +911,28 @@ def test_calcparams_cec_all_scalars(cec_module_params): 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') df = pd.DataFrame({