pvlib 0.10.5__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. pvlib/__init__.py +1 -0
  2. pvlib/albedo.py +168 -0
  3. pvlib/data/ASTMG173.csv +2004 -0
  4. pvlib/iam.py +28 -28
  5. pvlib/iotools/__init__.py +0 -1
  6. pvlib/iotools/midc.py +15 -10
  7. pvlib/iotools/psm3.py +10 -25
  8. pvlib/iotools/srml.py +0 -61
  9. pvlib/irradiance.py +133 -95
  10. pvlib/location.py +13 -5
  11. pvlib/modelchain.py +2 -165
  12. pvlib/pvsystem.py +23 -63
  13. pvlib/shading.py +350 -0
  14. pvlib/spectrum/__init__.py +5 -0
  15. pvlib/spectrum/mismatch.py +572 -43
  16. pvlib/spectrum/spectrl2.py +8 -8
  17. pvlib/tests/iotools/test_psm3.py +0 -18
  18. pvlib/tests/iotools/test_srml.py +1 -43
  19. pvlib/tests/test_albedo.py +84 -0
  20. pvlib/tests/test_inverter.py +2 -2
  21. pvlib/tests/test_irradiance.py +35 -2
  22. pvlib/tests/test_location.py +26 -18
  23. pvlib/tests/test_modelchain.py +0 -57
  24. pvlib/tests/test_pvsystem.py +11 -39
  25. pvlib/tests/test_shading.py +167 -1
  26. pvlib/tests/test_singlediode.py +0 -19
  27. pvlib/tests/test_spectrum.py +283 -22
  28. pvlib/tests/test_temperature.py +7 -7
  29. pvlib/tests/test_tools.py +24 -0
  30. pvlib/tests/test_transformer.py +60 -0
  31. pvlib/tools.py +27 -0
  32. pvlib/transformer.py +117 -0
  33. {pvlib-0.10.5.dist-info → pvlib-0.11.0.dist-info}/METADATA +1 -1
  34. {pvlib-0.10.5.dist-info → pvlib-0.11.0.dist-info}/RECORD +38 -34
  35. {pvlib-0.10.5.dist-info → pvlib-0.11.0.dist-info}/WHEEL +1 -1
  36. pvlib/data/astm_g173_am15g.csv +0 -2003
  37. {pvlib-0.10.5.dist-info → pvlib-0.11.0.dist-info}/AUTHORS.md +0 -0
  38. {pvlib-0.10.5.dist-info → pvlib-0.11.0.dist-info}/LICENSE +0 -0
  39. {pvlib-0.10.5.dist-info → pvlib-0.11.0.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,12 @@ import numpy as np
2
2
  import pandas as pd
3
3
 
4
4
  from pandas.testing import assert_series_equal
5
- from numpy.testing import assert_allclose
5
+ from numpy.testing import assert_allclose, assert_approx_equal
6
6
  import pytest
7
7
  from datetime import timezone, timedelta
8
8
 
9
9
  from pvlib import shading
10
+ from pvlib.tools import atand
10
11
 
11
12
 
12
13
  @pytest.fixture
@@ -223,3 +224,168 @@ def test_projected_solar_zenith_angle_datatypes(
223
224
  )
224
225
  psz = psz_func(sun_apparent_zenith, axis_azimuth, axis_tilt, axis_azimuth)
225
226
  assert isinstance(psz, cast_type)
227
+
228
+
229
+ @pytest.fixture
230
+ def sf1d_premises_and_expected():
231
+ """Data comprised of solar position, rows parameters and terrain slope
232
+ with respective shade fractions (sf). Returns a 2-tuple with the premises
233
+ to be used directly in shaded_fraction1d(...) in the first element and
234
+ the expected shaded fractions in the second element.
235
+ See [1] in shaded_fraction1d()
236
+ Test data sourced from http://doi.org/10.5281/zenodo.10513987
237
+ """
238
+ test_data = pd.DataFrame(
239
+ columns=["x_L", "z_L", "theta_L", "x_R", "z_R", "theta_R", "z_0", "l",
240
+ "theta_s", "f_s"],
241
+ data=(
242
+ (1, 0.2, 50, 0, 0, 25, 0, 0.5, 80, 1),
243
+ (1, 0.1, 50, 0, 0, 25, 0.05, 0.5, 80, 0.937191),
244
+ (1, 0, 50, 0, 0.1, 25, 0, 0.5, 80, 0.30605),
245
+ (1, 0, 50, 0, 0.2, 25, 0, 0.5, 80, 0),
246
+ (1, 0.2, -25, 0, 0, -50, 0, 0.5, -80, 0),
247
+ (1, 0.1, -25, 0, 0, -50, 0, 0.5, -80, 0.30605),
248
+ (1, 0, -25, 0, 0.1, -50, 0.1, 0.5, -80, 0.881549),
249
+ (1, 0, -25, 0, 0.2, -50, 0, 0.5, -80, 1),
250
+ (1, 0.2, 5, 0, 0, 25, 0.05, 0.5, 80, 0.832499),
251
+ (1, 0.2, -25, 0, 0, 25, 0.05, 0.5, 80, 0.832499),
252
+ (1, 0.2, 5, 0, 0, -45, 0.05, 0.5, 80, 0.832499),
253
+ (1, 0.2, -25, 0, 0, -45, 0.05, 0.5, 80, 0.832499),
254
+ (1, 0, -25, 0, 0.2, 25, 0.05, 0.5, -80, 0.832499),
255
+ (1, 0, -25, 0, 0.2, -5, 0.05, 0.5, -80, 0.832499),
256
+ (1, 0, 45, 0, 0.2, 25, 0.05, 0.5, -80, 0.832499),
257
+ (1, 0, 45, 0, 0.2, -5, 0.05, 0.5, -80, 0.832499),
258
+ ),
259
+ ) # fmt: skip
260
+
261
+ test_data["cross_axis_slope"] = atand(
262
+ (test_data["z_R"] - test_data["z_L"])
263
+ / (test_data["x_L"] - test_data["x_R"])
264
+ )
265
+ test_data["pitch"] = test_data["x_L"] - test_data["x_R"]
266
+ # switch Left/Right rows if needed to make the right one the shaded
267
+ where_switch = test_data["theta_s"] >= 0
268
+ test_data["theta_L"], test_data["theta_R"] = np.where(
269
+ where_switch,
270
+ (test_data["theta_L"], test_data["theta_R"]),
271
+ (test_data["theta_R"], test_data["theta_L"]),
272
+ )
273
+ test_data.rename(
274
+ columns={
275
+ "theta_L": "shading_row_rotation",
276
+ "theta_R": "shaded_row_rotation",
277
+ "z_0": "surface_to_axis_offset",
278
+ "l": "collector_width",
279
+ "theta_s": "solar_zenith", # for the projected solar zenith angle
280
+ "f_s": "shaded_fraction",
281
+ },
282
+ inplace=True,
283
+ )
284
+ test_data.drop(columns=["x_L", "z_L", "x_R", "z_R"], inplace=True)
285
+ # for the projected solar zenith angle
286
+ # this returns the same psz angle as test_data["solar_zenith"]
287
+ test_data["solar_azimuth"], test_data["axis_azimuth"] = 180, 90
288
+
289
+ # return 1st: premises dataframe first and 2nd: shaded fraction series
290
+ return (
291
+ test_data.drop(columns=["shaded_fraction"]),
292
+ test_data["shaded_fraction"],
293
+ )
294
+
295
+
296
+ def test_shaded_fraction1d(sf1d_premises_and_expected):
297
+ """Tests shaded_fraction1d"""
298
+ # unwrap sf_premises_and_expected values premises and expected results
299
+ premises, expected_sf_array = sf1d_premises_and_expected
300
+ # test scalar input
301
+ expected_result = expected_sf_array.iloc[0]
302
+ sf = shading.shaded_fraction1d(**premises.iloc[0])
303
+ assert_approx_equal(sf, expected_result)
304
+ assert isinstance(sf, float)
305
+
306
+ # test Series inputs
307
+ sf_vec = shading.shaded_fraction1d(**premises)
308
+ assert_allclose(sf_vec, expected_sf_array, atol=1e-6)
309
+ assert isinstance(sf_vec, pd.Series)
310
+
311
+
312
+ def test_shaded_fraction1d_unprovided_shading_row_rotation():
313
+ """Tests shaded_fraction1d without providing shading_row_rotation"""
314
+ test_data = pd.DataFrame(
315
+ columns=[
316
+ "shaded_row_rotation", "surface_to_axis_offset", "collector_width",
317
+ "solar_zenith", "cross_axis_slope", "pitch", "solar_azimuth",
318
+ "axis_azimuth", "expected_sf",
319
+ ],
320
+ data=[
321
+ (30, 0, 5.7735, 60, 0, 5, 90, 180, 0),
322
+ (30, 0, 5.7735, 79, 0, 5, 90, 180, 0.5),
323
+ (30, 0, 5.7735, 90, 0, 5, 90, 180, 1),
324
+ ],
325
+ ) # fmt: skip
326
+ expected_sf = test_data["expected_sf"]
327
+ premises = test_data.drop(columns=["expected_sf"])
328
+ sf = shading.shaded_fraction1d(**premises)
329
+ assert_allclose(sf, expected_sf, atol=1e-2)
330
+
331
+
332
+ @pytest.fixture
333
+ def direct_martinez_Table2():
334
+ """
335
+ Original data used in [1] (see pvlib.shading.direct_martinez) to validate
336
+ the model. Some of the data is provided in Table 2.
337
+ Returns tuple with (input: pandas.DataFrame, output: pandas.Series)
338
+ Output is power loss: 1 - (P_shaded / P_unshaded)
339
+ """
340
+ test_data = pd.DataFrame(
341
+ columns=[
342
+ "F_GS-H",
343
+ "F_GS-V",
344
+ "shaded_blocks",
345
+ "poa_direct",
346
+ "poa_diffuse",
347
+ "power_loss_model",
348
+ ],
349
+ data=[
350
+ # F-H, F-V, Nsb, direct, diffuse, power_loss
351
+ # original data sourced from researchers
352
+ [1.00, 0.09, 16, 846.59, 59.42, 0.8844],
353
+ [1.00, 0.18, 16, 841.85, 59.69, 0.8888],
354
+ [1.00, 0.36, 16, 843.38, 59.22, 0.8994],
355
+ [0.04, 0.64, 1, 851.90, 59.40, 0.0783],
356
+ [0.17, 0.45, 3, 862.86, 58.40, 0.2237],
357
+ [0.29, 0.27, 5, 864.14, 58.11, 0.3282],
358
+ [0.50, 0.09, 8, 863.23, 58.31, 0.4634],
359
+ [0.13, 1.00, 2, 870.14, 58.02, 0.2137],
360
+ [0.25, 1.00, 4, 876.57, 57.98, 0.4000],
361
+ [0.38, 1.00, 6, 866.86, 58.89, 0.5577],
362
+ [0.50, 1.00, 8, 874.58, 58.44, 0.6892],
363
+ [0.58, 0.82, 10, 876.80, 58.16, 0.7359],
364
+ [0.75, 0.73, 12, 866.89, 58.73, 0.8113],
365
+ [0.92, 0.64, 15, 861.48, 59.66, 0.8894],
366
+ # custom edge cases
367
+ [0.00, 0.00, 0, 800.00, 50.00, 0.0000],
368
+ [1.00, 1.00, 16, 900.00, 00.00, 1.0000],
369
+ [0.00, 1.00, 16, 000.00, 00.00, np.nan],
370
+ [1.00, 0.00, 0, 000.00, 00.00, np.nan],
371
+ [1.00, 0.00, 0, -50.00, 50.00, np.nan], # zero poa_global
372
+ [1.00, 0.00, 0, 50.00, -50.00, np.nan], # zero poa_global
373
+ ]
374
+ ) # fmt: skip
375
+ test_data["total_blocks"] = 16 # total blocks is 16 for all cases
376
+ test_data["shaded_fraction"] = test_data["F_GS-H"] * test_data["F_GS-V"]
377
+ test_data["poa_global"] = (
378
+ test_data["poa_direct"] + test_data["poa_diffuse"]
379
+ )
380
+ test_data = test_data.drop(columns=["F_GS-H", "F_GS-V", "poa_diffuse"])
381
+ return (
382
+ test_data.drop(columns="power_loss_model"),
383
+ test_data["power_loss_model"],
384
+ )
385
+
386
+
387
+ def test_direct_martinez(direct_martinez_Table2):
388
+ """Tests pvlib.shading.direct_martinez"""
389
+ test_data, power_losses_expected = direct_martinez_Table2
390
+ power_losses = shading.direct_martinez(**test_data)
391
+ assert_allclose(power_losses, power_losses_expected, atol=5e-3)
@@ -8,7 +8,6 @@ import scipy
8
8
  from pvlib import pvsystem
9
9
  from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN,
10
10
  bishop88, bishop88_i_from_v, bishop88_v_from_i)
11
- from pvlib._deprecation import pvlibDeprecationWarning
12
11
  import pytest
13
12
  from numpy.testing import assert_array_equal
14
13
  from .conftest import DATA_DIR
@@ -188,24 +187,6 @@ def test_singlediode_lambert_negative_voc(mocker):
188
187
  assert_array_equal(outs["v_oc"], [0, 0])
189
188
 
190
189
 
191
- @pytest.mark.parametrize('method', ['lambertw'])
192
- def test_ivcurve_pnts_precision(method, precise_iv_curves):
193
- """
194
- Tests the accuracy of the IV curve points calcuated by singlediode. Only
195
- methods of singlediode that linearly spaced points are tested.
196
- """
197
- x, pc = precise_iv_curves
198
- pc_i, pc_v = np.stack(pc['Currents']), np.stack(pc['Voltages'])
199
- ivcurve_pnts = len(pc['Currents'][0])
200
-
201
- with pytest.warns(pvlibDeprecationWarning, match='ivcurve_pnts'):
202
- outs = pvsystem.singlediode(method=method, ivcurve_pnts=ivcurve_pnts,
203
- **x)
204
-
205
- assert np.allclose(pc_i, outs['i'], atol=1e-10, rtol=0)
206
- assert np.allclose(pc_v, outs['v'], atol=1e-10, rtol=0)
207
-
208
-
209
190
  @pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton'])
210
191
  def test_v_from_i_i_from_v_precision(method, precise_iv_curves):
211
192
  """
@@ -3,11 +3,13 @@ from numpy.testing import assert_allclose, assert_approx_equal, assert_equal
3
3
  import pandas as pd
4
4
  import numpy as np
5
5
  from pvlib import spectrum
6
+ from pvlib._deprecation import pvlibDeprecationWarning
6
7
 
7
- from .conftest import DATA_DIR, assert_series_equal
8
+ from .conftest import DATA_DIR, assert_series_equal, fail_on_pvlib_version
8
9
 
9
10
  SPECTRL2_TEST_DATA = DATA_DIR / 'spectrl2_example_spectra.csv'
10
11
 
12
+
11
13
  @pytest.fixture
12
14
  def spectrl2_data():
13
15
  # reference spectra generated with solar_utils==0.3
@@ -123,24 +125,75 @@ def test_get_example_spectral_response():
123
125
  assert_allclose(sr, expected, rtol=1e-5)
124
126
 
125
127
 
128
+ @fail_on_pvlib_version('0.12')
126
129
  def test_get_am15g():
127
130
  # test that the reference spectrum is read and interpolated correctly
128
- e = spectrum.get_am15g()
131
+ with pytest.warns(pvlibDeprecationWarning,
132
+ match="get_reference_spectra instead"):
133
+ e = spectrum.get_am15g()
129
134
  assert_equal(len(e), 2002)
130
135
  assert_equal(np.sum(e.index), 2761442)
131
136
  assert_approx_equal(np.sum(e), 1002.88, significant=6)
132
137
 
133
- wavelength = [270, 850, 950, 1200, 4001]
134
- expected = [0.0, 0.893720, 0.147260, 0.448250, 0.0]
138
+ wavelength = [270, 850, 950, 1200, 1201.25, 4001]
139
+ expected = [0.0, 0.893720, 0.147260, 0.448250, 0.4371025, 0.0]
135
140
 
136
- e = spectrum.get_am15g(wavelength)
141
+ with pytest.warns(pvlibDeprecationWarning,
142
+ match="get_reference_spectra instead"):
143
+ e = spectrum.get_am15g(wavelength)
137
144
  assert_equal(len(e), len(wavelength))
138
145
  assert_allclose(e, expected, rtol=1e-6)
139
146
 
140
147
 
148
+ @pytest.mark.parametrize(
149
+ "reference_identifier,expected_sums",
150
+ [
151
+ (
152
+ "ASTM G173-03", # reference_identifier
153
+ { # expected_sums
154
+ "extraterrestrial": 1356.15,
155
+ "global": 1002.88,
156
+ "direct": 887.65,
157
+ },
158
+ ),
159
+ ],
160
+ )
161
+ def test_get_reference_spectra(reference_identifier, expected_sums):
162
+ # test reading of a standard spectrum
163
+ standard = spectrum.get_reference_spectra(standard=reference_identifier)
164
+ assert set(standard.columns) == expected_sums.keys()
165
+ assert standard.index.name == "wavelength"
166
+ assert standard.index.is_monotonic_increasing is True
167
+ expected_sums = pd.Series(expected_sums) # convert prior to comparison
168
+ assert_series_equal(np.sum(standard, axis=0), expected_sums, atol=1e-2)
169
+
170
+
171
+ def test_get_reference_spectra_custom_wavelengths():
172
+ # test that the spectrum is interpolated correctly when custom wavelengths
173
+ # are specified
174
+ # only checked for ASTM G173-03 reference spectrum
175
+ wavelength = [270, 850, 951.634, 1200, 4001]
176
+ expected_sums = pd.Series(
177
+ {"extraterrestrial": 2.23266, "global": 1.68952, "direct": 1.58480}
178
+ ) # for given ``wavelength``
179
+ standard = spectrum.get_reference_spectra(
180
+ wavelength, standard="ASTM G173-03"
181
+ )
182
+ assert_equal(len(standard), len(wavelength))
183
+ # check no NaN values were returned
184
+ assert not standard.isna().any().any() # double any to return one value
185
+ assert_series_equal(np.sum(standard, axis=0), expected_sums, atol=1e-4)
186
+
187
+
188
+ def test_get_reference_spectra_invalid_reference():
189
+ # test that an invalid reference identifier raises a ValueError
190
+ with pytest.raises(ValueError, match="Invalid standard identifier"):
191
+ spectrum.get_reference_spectra(standard="invalid")
192
+
193
+
141
194
  def test_calc_spectral_mismatch_field(spectrl2_data):
142
195
  # test that the mismatch is calculated correctly with
143
- # - default and custom reference sepctrum
196
+ # - default and custom reference spectrum
144
197
  # - single or multiple sun spectra
145
198
 
146
199
  # sample data
@@ -148,7 +201,7 @@ def test_calc_spectral_mismatch_field(spectrl2_data):
148
201
  e_sun = e_sun.set_index('wavelength')
149
202
  e_sun = e_sun.transpose()
150
203
 
151
- e_ref = spectrum.get_am15g()
204
+ e_ref = spectrum.get_reference_spectra(standard='ASTM G173-03')["global"]
152
205
  sr = spectrum.get_example_spectral_response()
153
206
 
154
207
  # test with single sun spectrum, same as ref spectrum
@@ -175,25 +228,25 @@ def test_calc_spectral_mismatch_field(spectrl2_data):
175
228
 
176
229
  @pytest.mark.parametrize("module_type,expect", [
177
230
  ('cdte', np.array(
178
- [[ 0.99051020, 0.97640320, 0.93975028],
179
- [ 1.02928735, 1.01881074, 0.98578821],
180
- [ 1.04750335, 1.03814456, 1.00623986]])),
231
+ [[0.99051020, 0.97640320, 0.93975028],
232
+ [1.02928735, 1.01881074, 0.98578821],
233
+ [1.04750335, 1.03814456, 1.00623986]])),
181
234
  ('monosi', np.array(
182
- [[ 0.97769770, 1.02043409, 1.03574032],
183
- [ 0.98630905, 1.03055092, 1.04736262],
184
- [ 0.98828494, 1.03299036, 1.05026561]])),
235
+ [[0.97769770, 1.02043409, 1.03574032],
236
+ [0.98630905, 1.03055092, 1.04736262],
237
+ [0.98828494, 1.03299036, 1.05026561]])),
185
238
  ('polysi', np.array(
186
- [[ 0.97704080, 1.01705849, 1.02613202],
187
- [ 0.98992828, 1.03173953, 1.04260662],
188
- [ 0.99352435, 1.03588785, 1.04730718]])),
239
+ [[0.97704080, 1.01705849, 1.02613202],
240
+ [0.98992828, 1.03173953, 1.04260662],
241
+ [0.99352435, 1.03588785, 1.04730718]])),
189
242
  ('cigs', np.array(
190
- [[ 0.97459190, 1.02821696, 1.05067895],
191
- [ 0.97529378, 1.02967497, 1.05289307],
192
- [ 0.97269159, 1.02730558, 1.05075651]])),
243
+ [[0.97459190, 1.02821696, 1.05067895],
244
+ [0.97529378, 1.02967497, 1.05289307],
245
+ [0.97269159, 1.02730558, 1.05075651]])),
193
246
  ('asi', np.array(
194
- [[ 1.05552750, 0.87707583, 0.72243772],
195
- [ 1.11225204, 0.93665901, 0.78487953],
196
- [ 1.14555295, 0.97084011, 0.81994083]]))
247
+ [[1.05552750, 0.87707583, 0.72243772],
248
+ [1.11225204, 0.93665901, 0.78487953],
249
+ [1.14555295, 0.97084011, 0.81994083]]))
197
250
  ])
198
251
  def test_spectral_factor_firstsolar(module_type, expect):
199
252
  ams = np.array([1, 3, 5])
@@ -315,3 +368,211 @@ def test_spectral_factor_caballero_supplied_ambiguous():
315
368
  with pytest.raises(ValueError):
316
369
  spectrum.spectral_factor_caballero(1, 1, 1, module_type=None,
317
370
  coefficients=None)
371
+
372
+
373
+ @pytest.mark.parametrize("module_type,expected", [
374
+ ('asi', np.array([1.15534029, 1.1123772, 1.08286684, 1.01915462])),
375
+ ('fs-2', np.array([1.0694323, 1.04948777, 1.03556288, 0.9881471])),
376
+ ('fs-4', np.array([1.05234725, 1.037771, 1.0275516, 0.98820533])),
377
+ ('multisi', np.array([1.03310403, 1.02391703, 1.01744833, 0.97947605])),
378
+ ('monosi', np.array([1.03225083, 1.02335353, 1.01708734, 0.97950110])),
379
+ ('cigs', np.array([1.01475834, 1.01143927, 1.00909094, 0.97852966])),
380
+ ])
381
+ def test_spectral_factor_pvspec(module_type, expected):
382
+ ams = np.array([1.0, 1.5, 2.0, 1.5])
383
+ kcs = np.array([0.4, 0.6, 0.8, 1.4])
384
+ out = spectrum.spectral_factor_pvspec(ams, kcs,
385
+ module_type=module_type)
386
+ assert np.allclose(expected, out, atol=1e-8)
387
+
388
+
389
+ @pytest.mark.parametrize("module_type,expected", [
390
+ ('asi', pd.Series([1.15534029, 1.1123772, 1.08286684, 1.01915462])),
391
+ ('fs-2', pd.Series([1.0694323, 1.04948777, 1.03556288, 0.9881471])),
392
+ ('fs-4', pd.Series([1.05234725, 1.037771, 1.0275516, 0.98820533])),
393
+ ('multisi', pd.Series([1.03310403, 1.02391703, 1.01744833, 0.97947605])),
394
+ ('monosi', pd.Series([1.03225083, 1.02335353, 1.01708734, 0.97950110])),
395
+ ('cigs', pd.Series([1.01475834, 1.01143927, 1.00909094, 0.97852966])),
396
+ ])
397
+ def test_spectral_factor_pvspec_series(module_type, expected):
398
+ ams = pd.Series([1.0, 1.5, 2.0, 1.5])
399
+ kcs = pd.Series([0.4, 0.6, 0.8, 1.4])
400
+ out = spectrum.spectral_factor_pvspec(ams, kcs,
401
+ module_type=module_type)
402
+ assert isinstance(out, pd.Series)
403
+ assert np.allclose(expected, out, atol=1e-8)
404
+
405
+
406
+ def test_spectral_factor_pvspec_supplied():
407
+ # use the multisi coeffs
408
+ coeffs = (0.9847, -0.05237, 0.03034)
409
+ out = spectrum.spectral_factor_pvspec(1.5, 0.8, coefficients=coeffs)
410
+ expected = 1.00860641
411
+ assert_allclose(out, expected, atol=1e-8)
412
+
413
+
414
+ def test_spectral_factor_pvspec_supplied_redundant():
415
+ # Error when specifying both module_type and coefficients
416
+ coeffs = (0.9847, -0.05237, 0.03034)
417
+ with pytest.raises(ValueError, match='supply only one of'):
418
+ spectrum.spectral_factor_pvspec(1.5, 0.8, module_type='multisi',
419
+ coefficients=coeffs)
420
+
421
+
422
+ def test_spectral_factor_pvspec_supplied_ambiguous():
423
+ # Error when specifying neither module_type nor coefficients
424
+ with pytest.raises(ValueError, match='No valid input provided'):
425
+ spectrum.spectral_factor_pvspec(1.5, 0.8, module_type=None,
426
+ coefficients=None)
427
+
428
+
429
+ @pytest.mark.parametrize("module_type,expected", [
430
+ ('multisi', np.array([1.06129, 1.03098, 1.01155, 0.99849])),
431
+ ('cdte', np.array([1.09657, 1.05594, 1.02763, 0.97740])),
432
+ ])
433
+ def test_spectral_factor_jrc(module_type, expected):
434
+ ams = np.array([1.0, 1.5, 2.0, 1.5])
435
+ kcs = np.array([0.4, 0.6, 0.8, 1.4])
436
+ out = spectrum.spectral_factor_jrc(ams, kcs,
437
+ module_type=module_type)
438
+ assert np.allclose(expected, out, atol=1e-4)
439
+
440
+
441
+ @pytest.mark.parametrize("module_type,expected", [
442
+ ('multisi', np.array([1.06129, 1.03098, 1.01155, 0.99849])),
443
+ ('cdte', np.array([1.09657, 1.05594, 1.02763, 0.97740])),
444
+ ])
445
+ def test_spectral_factor_jrc_series(module_type, expected):
446
+ ams = pd.Series([1.0, 1.5, 2.0, 1.5])
447
+ kcs = pd.Series([0.4, 0.6, 0.8, 1.4])
448
+ out = spectrum.spectral_factor_jrc(ams, kcs,
449
+ module_type=module_type)
450
+ assert isinstance(out, pd.Series)
451
+ assert np.allclose(expected, out, atol=1e-4)
452
+
453
+
454
+ def test_spectral_factor_jrc_supplied():
455
+ # use the multisi coeffs
456
+ coeffs = (0.494, 0.146, 0.00103)
457
+ out = spectrum.spectral_factor_jrc(1.0, 0.8, coefficients=coeffs)
458
+ expected = 1.01052106
459
+ assert_allclose(out, expected, atol=1e-4)
460
+
461
+
462
+ def test_spectral_factor_jrc_supplied_redundant():
463
+ # Error when specifying both module_type and coefficients
464
+ coeffs = (0.494, 0.146, 0.00103)
465
+ with pytest.raises(ValueError, match='supply only one of'):
466
+ spectrum.spectral_factor_jrc(1.0, 0.8, module_type='multisi',
467
+ coefficients=coeffs)
468
+
469
+
470
+ def test_spectral_factor_jrc_supplied_ambiguous():
471
+ # Error when specifying neither module_type nor coefficients
472
+ with pytest.raises(ValueError, match='No valid input provided'):
473
+ spectrum.spectral_factor_jrc(1.0, 0.8, module_type=None,
474
+ coefficients=None)
475
+
476
+
477
+ @pytest.fixture
478
+ def sr_and_eqe_fixture():
479
+ # Just some arbitrary data for testing the conversion functions
480
+ df = pd.DataFrame(
481
+ columns=("wavelength", "quantum_efficiency", "spectral_response"),
482
+ data=[
483
+ # nm, [0,1], A/W
484
+ [300, 0.85, 0.205671370402405],
485
+ [350, 0.86, 0.242772872514211],
486
+ [400, 0.87, 0.280680929019753],
487
+ [450, 0.88, 0.319395539919029],
488
+ [500, 0.89, 0.358916705212040],
489
+ [550, 0.90, 0.399244424898786],
490
+ [600, 0.91, 0.440378698979267],
491
+ [650, 0.92, 0.482319527453483],
492
+ [700, 0.93, 0.525066910321434],
493
+ [750, 0.94, 0.568620847583119],
494
+ [800, 0.95, 0.612981339238540],
495
+ [850, 0.90, 0.617014111207215],
496
+ [900, 0.80, 0.580719163489143],
497
+ [950, 0.70, 0.536358671833723],
498
+ [1000, 0.6, 0.483932636240953],
499
+ [1050, 0.4, 0.338752845368667],
500
+ ],
501
+ )
502
+ df.set_index("wavelength", inplace=True)
503
+ return df
504
+
505
+
506
+ def test_sr_to_qe(sr_and_eqe_fixture):
507
+ # vector type
508
+ qe = spectrum.sr_to_qe(
509
+ sr_and_eqe_fixture["spectral_response"].values,
510
+ sr_and_eqe_fixture.index.values, # wavelength, nm
511
+ )
512
+ assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"])
513
+ # pandas series type
514
+ # note: output Series' name should match the input
515
+ qe = spectrum.sr_to_qe(
516
+ sr_and_eqe_fixture["spectral_response"]
517
+ )
518
+ pd.testing.assert_series_equal(
519
+ qe, sr_and_eqe_fixture["quantum_efficiency"],
520
+ check_names=False
521
+ )
522
+ assert qe.name == "spectral_response"
523
+ # series normalization
524
+ qe = spectrum.sr_to_qe(
525
+ sr_and_eqe_fixture["spectral_response"] * 10, normalize=True
526
+ )
527
+ pd.testing.assert_series_equal(
528
+ qe,
529
+ sr_and_eqe_fixture["quantum_efficiency"]
530
+ / max(sr_and_eqe_fixture["quantum_efficiency"]),
531
+ check_names=False,
532
+ )
533
+ # error on lack of wavelength parameter if no pandas object is provided
534
+ with pytest.raises(TypeError, match="must have an '.index' attribute"):
535
+ _ = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"].values)
536
+
537
+
538
+ def test_qe_to_sr(sr_and_eqe_fixture):
539
+ # vector type
540
+ sr = spectrum.qe_to_sr(
541
+ sr_and_eqe_fixture["quantum_efficiency"].values,
542
+ sr_and_eqe_fixture.index.values, # wavelength, nm
543
+ )
544
+ assert_allclose(sr, sr_and_eqe_fixture["spectral_response"])
545
+ # pandas series type
546
+ # note: output Series' name should match the input
547
+ sr = spectrum.qe_to_sr(
548
+ sr_and_eqe_fixture["quantum_efficiency"]
549
+ )
550
+ pd.testing.assert_series_equal(
551
+ sr, sr_and_eqe_fixture["spectral_response"],
552
+ check_names=False
553
+ )
554
+ assert sr.name == "quantum_efficiency"
555
+ # series normalization
556
+ sr = spectrum.qe_to_sr(
557
+ sr_and_eqe_fixture["quantum_efficiency"] * 10, normalize=True
558
+ )
559
+ pd.testing.assert_series_equal(
560
+ sr,
561
+ sr_and_eqe_fixture["spectral_response"]
562
+ / max(sr_and_eqe_fixture["spectral_response"]),
563
+ check_names=False,
564
+ )
565
+ # error on lack of wavelength parameter if no pandas object is provided
566
+ with pytest.raises(TypeError, match="must have an '.index' attribute"):
567
+ _ = spectrum.qe_to_sr(
568
+ sr_and_eqe_fixture["quantum_efficiency"].values
569
+ )
570
+
571
+
572
+ def test_qe_and_sr_reciprocal_conversion(sr_and_eqe_fixture):
573
+ # test that the conversion functions are reciprocal
574
+ qe = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"])
575
+ sr = spectrum.qe_to_sr(qe)
576
+ assert_allclose(sr, sr_and_eqe_fixture["spectral_response"])
577
+ qe = spectrum.sr_to_qe(sr)
578
+ assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"])
@@ -20,13 +20,13 @@ def sapm_default():
20
20
  def test_sapm_cell(sapm_default):
21
21
  default = temperature.sapm_cell(900, 20, 5, sapm_default['a'],
22
22
  sapm_default['b'], sapm_default['deltaT'])
23
- assert_allclose(default, 43.509, 3)
23
+ assert_allclose(default, 43.509, 1e-3)
24
24
 
25
25
 
26
26
  def test_sapm_module(sapm_default):
27
27
  default = temperature.sapm_module(900, 20, 5, sapm_default['a'],
28
28
  sapm_default['b'])
29
- assert_allclose(default, 40.809, 3)
29
+ assert_allclose(default, 40.809, 1e-3)
30
30
 
31
31
 
32
32
  def test_sapm_cell_from_module(sapm_default):
@@ -47,8 +47,8 @@ def test_sapm_ndarray(sapm_default):
47
47
  sapm_default['b'])
48
48
  expected_cell = np.array([0., 23.06066166, 5.])
49
49
  expected_module = np.array([0., 21.56066166, 5.])
50
- assert_allclose(expected_cell, cell_temps, 3)
51
- assert_allclose(expected_module, module_temps, 3)
50
+ assert_allclose(expected_cell, cell_temps, 1e-3)
51
+ assert_allclose(expected_module, module_temps, 1e-3)
52
52
 
53
53
 
54
54
  def test_sapm_series(sapm_default):
@@ -84,8 +84,8 @@ def test_pvsyst_cell_ndarray():
84
84
  irrads = np.array([0, 500, 0])
85
85
  winds = np.array([10, 5, 0])
86
86
  result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds)
87
- expected = np.array([0.0, 23.96551, 5.0])
88
- assert_allclose(expected, result, 3)
87
+ expected = np.array([0.0, 23.965517, 5.0])
88
+ assert_allclose(expected, result)
89
89
 
90
90
 
91
91
  def test_pvsyst_cell_series():
@@ -95,7 +95,7 @@ def test_pvsyst_cell_series():
95
95
  winds = pd.Series([10, 5, 0], index=times)
96
96
 
97
97
  result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds)
98
- expected = pd.Series([0.0, 23.96551, 5.0], index=times)
98
+ expected = pd.Series([0.0, 23.965517, 5.0], index=times)
99
99
  assert_series_equal(expected, result)
100
100
 
101
101
 
pvlib/tests/test_tools.py CHANGED
@@ -3,6 +3,7 @@ import pytest
3
3
  from pvlib import tools
4
4
  import numpy as np
5
5
  import pandas as pd
6
+ from numpy.testing import assert_allclose
6
7
 
7
8
 
8
9
  @pytest.mark.parametrize('keys, input_dict, expected', [
@@ -120,3 +121,26 @@ def test_get_pandas_index(args, args_idx):
120
121
  assert index is None
121
122
  else:
122
123
  pd.testing.assert_index_equal(args[args_idx].index, index)
124
+
125
+
126
+ @pytest.mark.parametrize('data_in,expected', [
127
+ (np.array([1, 2, 3, 4, 5]),
128
+ np.array([0.2, 0.4, 0.6, 0.8, 1])),
129
+ (np.array([[0, 1, 2], [0, 3, 6]]),
130
+ np.array([[0, 0.5, 1], [0, 0.5, 1]])),
131
+ (pd.Series([1, 2, 3, 4, 5]),
132
+ pd.Series([0.2, 0.4, 0.6, 0.8, 1])),
133
+ (pd.DataFrame({"a": [0, 1, 2], "b": [0, 2, 8]}),
134
+ pd.DataFrame({"a": [0, 0.5, 1], "b": [0, 0.25, 1]})),
135
+ # test with NaN and all zeroes
136
+ (pd.DataFrame({"a": [0, np.nan, 1], "b": [0, 0, 0]}),
137
+ pd.DataFrame({"a": [0, np.nan, 1], "b": [np.nan]*3})),
138
+ # test with negative values
139
+ (np.array([1, 2, -3, 4, -5]),
140
+ np.array([0.2, 0.4, -0.6, 0.8, -1])),
141
+ (pd.Series([-2, np.nan, 1]),
142
+ pd.Series([-1, np.nan, 0.5])),
143
+ ])
144
+ def test_normalize_max2one(data_in, expected):
145
+ result = tools.normalize_max2one(data_in)
146
+ assert_allclose(result, expected)