pvlib 0.10.4__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 (47) hide show
  1. pvlib/__init__.py +1 -0
  2. pvlib/albedo.py +168 -0
  3. pvlib/bifacial/utils.py +2 -1
  4. pvlib/data/ASTMG173.csv +2004 -0
  5. pvlib/iam.py +28 -28
  6. pvlib/iotools/__init__.py +0 -1
  7. pvlib/iotools/midc.py +15 -10
  8. pvlib/iotools/psm3.py +10 -25
  9. pvlib/iotools/srml.py +1 -61
  10. pvlib/iotools/surfrad.py +1 -1
  11. pvlib/irradiance.py +133 -95
  12. pvlib/location.py +16 -6
  13. pvlib/modelchain.py +2 -165
  14. pvlib/pvarray.py +7 -5
  15. pvlib/pvsystem.py +75 -106
  16. pvlib/scaling.py +4 -2
  17. pvlib/shading.py +350 -0
  18. pvlib/singlediode.py +37 -9
  19. pvlib/snow.py +3 -1
  20. pvlib/spectrum/__init__.py +5 -0
  21. pvlib/spectrum/mismatch.py +573 -43
  22. pvlib/spectrum/spectrl2.py +8 -8
  23. pvlib/tests/bifacial/test_utils.py +6 -5
  24. pvlib/tests/iotools/test_psm3.py +0 -18
  25. pvlib/tests/iotools/test_srml.py +1 -43
  26. pvlib/tests/test_albedo.py +84 -0
  27. pvlib/tests/test_inverter.py +2 -2
  28. pvlib/tests/test_irradiance.py +35 -2
  29. pvlib/tests/test_location.py +26 -18
  30. pvlib/tests/test_modelchain.py +0 -57
  31. pvlib/tests/test_pvsystem.py +73 -128
  32. pvlib/tests/test_shading.py +167 -1
  33. pvlib/tests/test_singlediode.py +68 -29
  34. pvlib/tests/test_spectrum.py +283 -22
  35. pvlib/tests/test_temperature.py +7 -7
  36. pvlib/tests/test_tools.py +24 -0
  37. pvlib/tests/test_transformer.py +60 -0
  38. pvlib/tools.py +27 -0
  39. pvlib/transformer.py +117 -0
  40. pvlib/version.py +1 -5
  41. {pvlib-0.10.4.dist-info → pvlib-0.11.0.dist-info}/METADATA +3 -4
  42. {pvlib-0.10.4.dist-info → pvlib-0.11.0.dist-info}/RECORD +46 -42
  43. {pvlib-0.10.4.dist-info → pvlib-0.11.0.dist-info}/WHEEL +1 -1
  44. pvlib/data/astm_g173_am15g.csv +0 -2003
  45. {pvlib-0.10.4.dist-info → pvlib-0.11.0.dist-info}/AUTHORS.md +0 -0
  46. {pvlib-0.10.4.dist-info → pvlib-0.11.0.dist-info}/LICENSE +0 -0
  47. {pvlib-0.10.4.dist-info → pvlib-0.11.0.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -0,0 +1,60 @@
1
+ import pandas as pd
2
+
3
+ from numpy.testing import assert_allclose
4
+
5
+ from pvlib import transformer
6
+
7
+
8
+ def test_simple_efficiency():
9
+
10
+ # define test inputs
11
+ input_power = pd.Series([
12
+ -800.0,
13
+ 436016.609823837,
14
+ 1511820.16603752,
15
+ 1580687.44677249,
16
+ 1616441.79660171
17
+ ])
18
+ no_load_loss = 0.002
19
+ load_loss = 0.007
20
+ transformer_rating = 2750000
21
+
22
+ # define expected test results
23
+ expected_output_power = pd.Series([
24
+ -6300.10103234071,
25
+ 430045.854892526,
26
+ 1500588.39919874,
27
+ 1568921.77089526,
28
+ 1604389.62839879
29
+ ])
30
+
31
+ # run test function with test inputs
32
+ calculated_output_power = transformer.simple_efficiency(
33
+ input_power=input_power,
34
+ no_load_loss=no_load_loss,
35
+ load_loss=load_loss,
36
+ transformer_rating=transformer_rating
37
+ )
38
+
39
+ # determine if expected results are obtained
40
+ assert_allclose(calculated_output_power, expected_output_power)
41
+
42
+
43
+ def test_simple_efficiency_known_values():
44
+ no_load_loss = 0.005
45
+ load_loss = 0.01
46
+ rating = 1000
47
+ args = (no_load_loss, load_loss, rating)
48
+
49
+ # verify correct behavior at no-load condition
50
+ assert_allclose(
51
+ transformer.simple_efficiency(no_load_loss*rating, *args),
52
+ 0.0
53
+ )
54
+
55
+ # verify correct behavior at rated condition
56
+ assert_allclose(
57
+ transformer.simple_efficiency(rating*(1 + no_load_loss + load_loss),
58
+ *args),
59
+ rating,
60
+ )
pvlib/tools.py CHANGED
@@ -507,3 +507,30 @@ def get_pandas_index(*args):
507
507
  (a.index for a in args if isinstance(a, (pd.DataFrame, pd.Series))),
508
508
  None
509
509
  )
510
+
511
+
512
+ def normalize_max2one(a):
513
+ r"""
514
+ Normalize an array so that the largest absolute value is ±1.
515
+
516
+ Handles both numpy arrays and pandas objects.
517
+ On 2D arrays, normalization is row-wise.
518
+ On pandas DataFrame, normalization is column-wise.
519
+
520
+ If all values of row are 0, the array is set to NaNs.
521
+
522
+ Parameters
523
+ ----------
524
+ a : array-like
525
+ The array to normalize.
526
+
527
+ Returns
528
+ -------
529
+ array-like
530
+ The normalized array.
531
+ """
532
+ try: # expect numpy array
533
+ res = a / np.max(np.absolute(a), axis=-1, keepdims=True)
534
+ except ValueError: # fails for pandas objects
535
+ res = a.div(a.abs().max(axis=0, skipna=True))
536
+ return res
pvlib/transformer.py ADDED
@@ -0,0 +1,117 @@
1
+ """
2
+ This module contains functions for transformer modeling.
3
+
4
+ Transformer models calculate AC power output and losses at a given input power.
5
+ """
6
+
7
+
8
+ def simple_efficiency(
9
+ input_power, no_load_loss, load_loss, transformer_rating
10
+ ):
11
+ r'''
12
+ Calculate the power at the output terminal of the transformer
13
+ after taking into account efficiency using a simple calculation.
14
+
15
+ The equation used in this function can be derived from [1]_.
16
+
17
+ For a zero input power, the output power will be negative.
18
+ This means the transformer will consume energy from the grid at night if
19
+ it stays connected (due to the parallel impedance in the equivalent
20
+ circuit).
21
+ If the input power is negative, the output power will be even more
22
+ negative; so the model can be used bidirectionally when drawing
23
+ energy from the grid.
24
+
25
+ Parameters
26
+ ----------
27
+ input_power : numeric
28
+ The real AC power input to the transformer. [W]
29
+
30
+ no_load_loss : numeric
31
+ The constant losses experienced by a transformer, even
32
+ when the transformer is not under load. Fraction of transformer rating,
33
+ value from 0 to 1. [unitless]
34
+
35
+ load_loss: numeric
36
+ The load dependent losses experienced by the transformer.
37
+ Fraction of transformer rating, value from 0 to 1. [unitless]
38
+
39
+ transformer_rating: numeric
40
+ The nominal output power of the transformer. [VA]
41
+
42
+ Returns
43
+ -------
44
+ output_power : numeric
45
+ Real AC power output. [W]
46
+
47
+ Notes
48
+ -------
49
+ First, assume that the load loss :math:`L_{load}` (as a fraction of rated power
50
+ :math:`P_{nom}`) is proportional to the square of output power:
51
+
52
+ .. math::
53
+
54
+ L_{load}(P_{out}) &= L_{load}(P_{rated}) \times (P_{out} / P_{nom})^2
55
+
56
+ &= L_{full, load} \times (P_{out} / P_{nom})^2
57
+
58
+ Total loss is the constant no-load loss plus the variable load loss:
59
+
60
+ .. math::
61
+
62
+ L_{total}(P_{out}) &= L_{no, load} + L_{load}(P_{out})
63
+
64
+ &= L_{no, load} + L_{full, load} \times (P_{out} / P_{nom})^2
65
+
66
+
67
+ By conservation of energy, total loss is the difference between input and
68
+ output power:
69
+
70
+ .. math::
71
+
72
+ \frac{P_{in}}{P_{nom}} &= \frac{P_{out}}{P_{nom}} + L_{total}(P_{out})
73
+
74
+ &= \frac{P_{out}}{P_{nom}} + L_{no, load} + L_{full, load} \times (P_{out} / P_{nom})^2
75
+
76
+ Now use the quadratic formula to solve for :math:`P_{out}` as a function of
77
+ :math:`P_{in}`.
78
+
79
+ .. math::
80
+
81
+ \frac{P_{out}}{P_{nom}} &= \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
82
+
83
+ a &= L_{full, load}
84
+
85
+ b &= 1
86
+
87
+ c &= L_{no, load} - P_{in} / P_{nom}
88
+
89
+ Therefore:
90
+
91
+ .. math::
92
+
93
+ P_{out} = P_{nom} \frac{-1 \pm \sqrt{1 - 4 L_{full, load}
94
+
95
+ \times (L_{no, load} - P_{in}/P_{nom})}}{2 L_{full, load}}
96
+
97
+ The positive root should be chosen, so that the output power is
98
+ positive.
99
+
100
+
101
+ References
102
+ ----------
103
+ .. [1] Central Station Engineers of the Westinghouse Electric Corporation,
104
+ "Electrical Transmission and Distribution Reference Book" 4th Edition.
105
+ pg. 101.
106
+ ''' # noqa: E501
107
+
108
+ input_power_normalized = input_power / transformer_rating
109
+
110
+ a = load_loss
111
+ b = 1
112
+ c = no_load_loss - input_power_normalized
113
+
114
+ output_power_normalized = (-b + (b**2 - 4*a*c)**0.5) / (2 * a)
115
+
116
+ output_power = output_power_normalized * transformer_rating
117
+ return output_power
pvlib/version.py CHANGED
@@ -1,8 +1,4 @@
1
- try:
2
- from importlib.metadata import PackageNotFoundError, version
3
- except ImportError:
4
- # for python < 3.8
5
- from importlib_metadata import PackageNotFoundError, version
1
+ from importlib.metadata import PackageNotFoundError, version
6
2
 
7
3
  try:
8
4
  __version__ = version(__package__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pvlib
3
- Version: 0.10.4
3
+ Version: 0.11.0
4
4
  Summary: A set of functions and classes for simulating the performance of photovoltaic energy systems.
5
5
  Home-page: https://github.com/pvlib/pvlib-python
6
6
  Author-email: pvlib python Developers <pvlib-admin@googlegroups.com>
@@ -15,7 +15,7 @@ Classifier: Intended Audience :: Science/Research
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Topic :: Scientific/Engineering
18
- Requires-Python: >=3.7
18
+ Requires-Python: >=3.8
19
19
  Description-Content-Type: text/x-rst
20
20
  License-File: LICENSE
21
21
  License-File: AUTHORS.md
@@ -23,9 +23,8 @@ Requires-Dist: numpy >=1.17.3
23
23
  Requires-Dist: pandas >=1.3.0
24
24
  Requires-Dist: pytz
25
25
  Requires-Dist: requests
26
- Requires-Dist: scipy >=1.5.0
26
+ Requires-Dist: scipy >=1.6.0
27
27
  Requires-Dist: h5py
28
- Requires-Dist: importlib-metadata ; python_version < "3.8"
29
28
  Provides-Extra: all
30
29
  Requires-Dist: pvlib[doc,optional,test] ; extra == 'all'
31
30
  Provides-Extra: doc