pvlib 0.9.4a1__py3-none-any.whl → 0.10.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 (86) hide show
  1. pvlib/__init__.py +3 -2
  2. pvlib/atmosphere.py +23 -173
  3. pvlib/bifacial/infinite_sheds.py +88 -277
  4. pvlib/bifacial/utils.py +270 -28
  5. pvlib/data/adr-library-cec-inverters-2019-03-05.csv +5009 -0
  6. pvlib/data/precise_iv_curves1.json +10251 -0
  7. pvlib/data/precise_iv_curves2.json +10251 -0
  8. pvlib/data/precise_iv_curves_parameter_sets1.csv +33 -0
  9. pvlib/data/precise_iv_curves_parameter_sets2.csv +33 -0
  10. pvlib/data/test_psm3_2017.csv +17521 -17521
  11. pvlib/data/test_psm3_2019_5min.csv +288 -288
  12. pvlib/data/test_read_psm3.csv +17522 -17522
  13. pvlib/data/test_read_pvgis_horizon.csv +49 -0
  14. pvlib/data/variables_style_rules.csv +3 -0
  15. pvlib/iam.py +207 -51
  16. pvlib/inverter.py +6 -1
  17. pvlib/iotools/__init__.py +7 -2
  18. pvlib/iotools/acis.py +516 -0
  19. pvlib/iotools/midc.py +4 -4
  20. pvlib/iotools/psm3.py +59 -42
  21. pvlib/iotools/pvgis.py +84 -28
  22. pvlib/iotools/sodapro.py +8 -6
  23. pvlib/iotools/srml.py +121 -18
  24. pvlib/iotools/surfrad.py +2 -2
  25. pvlib/iotools/tmy.py +146 -102
  26. pvlib/irradiance.py +270 -15
  27. pvlib/ivtools/sde.py +14 -20
  28. pvlib/ivtools/sdm.py +31 -20
  29. pvlib/ivtools/utils.py +127 -6
  30. pvlib/location.py +3 -2
  31. pvlib/modelchain.py +67 -70
  32. pvlib/pvarray.py +225 -0
  33. pvlib/pvsystem.py +169 -539
  34. pvlib/shading.py +43 -2
  35. pvlib/singlediode.py +216 -66
  36. pvlib/snow.py +36 -15
  37. pvlib/soiling.py +3 -3
  38. pvlib/spa.py +327 -368
  39. pvlib/spectrum/__init__.py +8 -2
  40. pvlib/spectrum/mismatch.py +335 -0
  41. pvlib/temperature.py +124 -13
  42. pvlib/tests/bifacial/test_infinite_sheds.py +44 -106
  43. pvlib/tests/bifacial/test_utils.py +102 -5
  44. pvlib/tests/conftest.py +0 -31
  45. pvlib/tests/iotools/test_acis.py +213 -0
  46. pvlib/tests/iotools/test_midc.py +6 -6
  47. pvlib/tests/iotools/test_psm3.py +7 -5
  48. pvlib/tests/iotools/test_pvgis.py +21 -14
  49. pvlib/tests/iotools/test_sodapro.py +1 -1
  50. pvlib/tests/iotools/test_srml.py +71 -6
  51. pvlib/tests/iotools/test_tmy.py +43 -8
  52. pvlib/tests/ivtools/test_sde.py +19 -17
  53. pvlib/tests/ivtools/test_sdm.py +9 -4
  54. pvlib/tests/ivtools/test_utils.py +96 -1
  55. pvlib/tests/test_atmosphere.py +8 -64
  56. pvlib/tests/test_clearsky.py +0 -1
  57. pvlib/tests/test_iam.py +74 -1
  58. pvlib/tests/test_irradiance.py +56 -2
  59. pvlib/tests/test_location.py +1 -1
  60. pvlib/tests/test_modelchain.py +33 -76
  61. pvlib/tests/test_pvarray.py +46 -0
  62. pvlib/tests/test_pvsystem.py +366 -201
  63. pvlib/tests/test_shading.py +35 -0
  64. pvlib/tests/test_singlediode.py +306 -29
  65. pvlib/tests/test_snow.py +84 -1
  66. pvlib/tests/test_soiling.py +8 -7
  67. pvlib/tests/test_solarposition.py +7 -7
  68. pvlib/tests/test_spa.py +6 -7
  69. pvlib/tests/test_spectrum.py +145 -1
  70. pvlib/tests/test_temperature.py +29 -11
  71. pvlib/tests/test_tools.py +41 -0
  72. pvlib/tests/test_tracking.py +0 -149
  73. pvlib/tools.py +49 -25
  74. pvlib/tracking.py +1 -269
  75. pvlib-0.10.0.dist-info/AUTHORS.md +35 -0
  76. {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/LICENSE +5 -2
  77. {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/METADATA +3 -13
  78. {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/RECORD +80 -75
  79. {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/WHEEL +1 -1
  80. pvlib/data/adr-library-2013-10-01.csv +0 -1762
  81. pvlib/forecast.py +0 -1211
  82. pvlib/iotools/ecmwf_macc.py +0 -312
  83. pvlib/tests/iotools/test_ecmwf_macc.py +0 -162
  84. pvlib/tests/test_forecast.py +0 -228
  85. pvlib-0.9.4a1.dist-info/AUTHORS.md +0 -32
  86. {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/top_level.txt +0 -0
pvlib/shading.py CHANGED
@@ -8,6 +8,47 @@ import pandas as pd
8
8
  from pvlib.tools import sind, cosd
9
9
 
10
10
 
11
+ def ground_angle(surface_tilt, gcr, slant_height):
12
+ """
13
+ Angle from horizontal of the line from a point on the row slant length
14
+ to the bottom of the facing row.
15
+
16
+ The angles are clockwise from horizontal, rather than the usual
17
+ counterclockwise direction.
18
+
19
+ Parameters
20
+ ----------
21
+ surface_tilt : numeric
22
+ Surface tilt angle in degrees from horizontal, e.g., surface facing up
23
+ = 0, surface facing horizon = 90. [degree]
24
+ gcr : float
25
+ ground coverage ratio, ratio of row slant length to row spacing.
26
+ [unitless]
27
+ slant_height : numeric
28
+ The distance up the module's slant height to evaluate the ground
29
+ angle, as a fraction [0-1] of the module slant height [unitless].
30
+
31
+ Returns
32
+ -------
33
+ psi : numeric
34
+ Angle [degree].
35
+ """
36
+ # : \\ \
37
+ # : \\ \
38
+ # : \\ \
39
+ # : \\ \ facing row
40
+ # : \\.___________\
41
+ # : \ ^*-. psi \
42
+ # : \ x *-. \
43
+ # : \ v *-.\
44
+ # : \<-----P---->\
45
+
46
+ x1 = gcr * slant_height * sind(surface_tilt)
47
+ x2 = gcr * slant_height * cosd(surface_tilt) + 1
48
+ psi = np.arctan2(x1, x2) # do this before rad2deg because it handles 0 / 0
49
+ return np.rad2deg(psi)
50
+
51
+
11
52
  def masking_angle(surface_tilt, gcr, slant_height):
12
53
  """
13
54
  The elevation angle below which diffuse irradiance is blocked.
@@ -52,8 +93,8 @@ def masking_angle(surface_tilt, gcr, slant_height):
52
93
  # The original equation (8 in [1]) requires pitch and collector width,
53
94
  # but it's easy to non-dimensionalize it to make it a function of GCR
54
95
  # by factoring out B from the argument to arctan.
55
- numerator = (1 - slant_height) * sind(surface_tilt)
56
- denominator = 1/gcr - (1 - slant_height) * cosd(surface_tilt)
96
+ numerator = gcr * (1 - slant_height) * sind(surface_tilt)
97
+ denominator = 1 - gcr * (1 - slant_height) * cosd(surface_tilt)
57
98
  phi = np.arctan(numerator / denominator)
58
99
  return np.degrees(phi)
59
100
 
pvlib/singlediode.py CHANGED
@@ -2,15 +2,17 @@
2
2
  Low-level functions for solving the single diode equation.
3
3
  """
4
4
 
5
- from functools import partial
6
5
  import numpy as np
7
6
  from pvlib.tools import _golden_sect_DataFrame
8
7
 
9
8
  from scipy.optimize import brentq, newton
10
9
  from scipy.special import lambertw
11
10
 
12
- # set keyword arguments for all uses of newton in this module
13
- newton = partial(newton, tol=1e-6, maxiter=100, fprime2=None)
11
+ # newton method default parameters for this module
12
+ NEWTON_DEFAULT_PARAMS = {
13
+ 'tol': 1e-6,
14
+ 'maxiter': 100
15
+ }
14
16
 
15
17
  # intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al.
16
18
  VOLTAGE_BUILTIN = 0.9 # [V]
@@ -206,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
206
208
  resistance_series, resistance_shunt, nNsVth,
207
209
  d2mutau=0, NsVbi=np.Inf, breakdown_factor=0.,
208
210
  breakdown_voltage=-5.5, breakdown_exp=3.28,
209
- method='newton'):
211
+ method='newton', method_kwargs=None):
210
212
  """
211
213
  Find current given any voltage.
212
214
 
@@ -247,22 +249,59 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
247
249
  method : str, default 'newton'
248
250
  Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
249
251
  if ``breakdown_factor`` is not 0.
252
+ method_kwargs : dict, optional
253
+ Keyword arguments passed to root finder method. See
254
+ :py:func:`scipy:scipy.optimize.brentq` and
255
+ :py:func:`scipy:scipy.optimize.newton` parameters.
256
+ ``'full_output': True`` is allowed, and ``optimizer_output`` would be
257
+ returned. See examples section.
250
258
 
251
259
  Returns
252
260
  -------
253
261
  current : numeric
254
262
  current (I) at the specified voltage (V). [A]
263
+ optimizer_output : tuple, optional, if specified in ``method_kwargs``
264
+ see root finder documentation for selected method.
265
+ Found root is diode voltage in [1]_.
266
+
267
+ Examples
268
+ --------
269
+ Using the following arguments that may come from any
270
+ `calcparams_.*` function in :py:mod:`pvlib.pvsystem`:
271
+
272
+ >>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4.,
273
+ ... 'resistance_series': 4., 'resistance_shunt': 5000.0}
274
+
275
+ Use default values:
276
+
277
+ >>> i = bishop88_i_from_v(0.0, **args)
278
+
279
+ Specify tolerances and maximum number of iterations:
280
+
281
+ >>> i = bishop88_i_from_v(0.0, **args, method='newton',
282
+ ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20})
283
+
284
+ Retrieve full output from the root finder:
285
+
286
+ >>> i, method_output = bishop88_i_from_v(0.0, **args, method='newton',
287
+ ... method_kwargs={'full_output': True})
255
288
  """
256
289
  # collect args
257
290
  args = (photocurrent, saturation_current, resistance_series,
258
291
  resistance_shunt, nNsVth, d2mutau, NsVbi,
259
292
  breakdown_factor, breakdown_voltage, breakdown_exp)
293
+ method = method.lower()
294
+
295
+ # method_kwargs create dict if not provided
296
+ # this pattern avoids bugs with Mutable Default Parameters
297
+ if not method_kwargs:
298
+ method_kwargs = {}
260
299
 
261
300
  def fv(x, v, *a):
262
301
  # calculate voltage residual given diode voltage "x"
263
302
  return bishop88(x, *a)[1] - v
264
303
 
265
- if method.lower() == 'brentq':
304
+ if method == 'brentq':
266
305
  # first bound the search using voc
267
306
  voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
268
307
 
@@ -274,27 +313,37 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
274
313
  return brentq(fv, 0.0, voc,
275
314
  args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
276
315
  breakdown_factor, breakdown_voltage,
277
- breakdown_exp))
316
+ breakdown_exp),
317
+ **method_kwargs)
278
318
 
279
319
  vd_from_brent_vectorized = np.vectorize(vd_from_brent)
280
320
  vd = vd_from_brent_vectorized(voc_est, voltage, *args)
281
- elif method.lower() == 'newton':
321
+ elif method == 'newton':
282
322
  # make sure all args are numpy arrays if max size > 1
283
323
  # if voltage is an array, then make a copy to use for initial guess, v0
284
- args, v0 = _prepare_newton_inputs((voltage,), args, voltage)
324
+ args, v0, method_kwargs = \
325
+ _prepare_newton_inputs((voltage,), args, voltage, method_kwargs)
285
326
  vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0,
286
327
  fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4],
287
- args=args)
328
+ args=args,
329
+ **method_kwargs)
288
330
  else:
289
331
  raise NotImplementedError("Method '%s' isn't implemented" % method)
290
- return bishop88(vd, *args)[0]
332
+
333
+ # When 'full_output' parameter is specified, returned 'vd' is a tuple with
334
+ # many elements, where the root is the first one. So we use it to output
335
+ # the bishop88 result and return tuple(scalar, tuple with method results)
336
+ if method_kwargs.get('full_output') is True:
337
+ return (bishop88(vd[0], *args)[0], vd)
338
+ else:
339
+ return bishop88(vd, *args)[0]
291
340
 
292
341
 
293
342
  def bishop88_v_from_i(current, photocurrent, saturation_current,
294
343
  resistance_series, resistance_shunt, nNsVth,
295
344
  d2mutau=0, NsVbi=np.Inf, breakdown_factor=0.,
296
345
  breakdown_voltage=-5.5, breakdown_exp=3.28,
297
- method='newton'):
346
+ method='newton', method_kwargs=None):
298
347
  """
299
348
  Find voltage given any current.
300
349
 
@@ -335,16 +384,54 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
335
384
  method : str, default 'newton'
336
385
  Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
337
386
  if ``breakdown_factor`` is not 0.
387
+ method_kwargs : dict, optional
388
+ Keyword arguments passed to root finder method. See
389
+ :py:func:`scipy:scipy.optimize.brentq` and
390
+ :py:func:`scipy:scipy.optimize.newton` parameters.
391
+ ``'full_output': True`` is allowed, and ``optimizer_output`` would be
392
+ returned. See examples section.
338
393
 
339
394
  Returns
340
395
  -------
341
396
  voltage : numeric
342
397
  voltage (V) at the specified current (I) in volts [V]
398
+ optimizer_output : tuple, optional, if specified in ``method_kwargs``
399
+ see root finder documentation for selected method.
400
+ Found root is diode voltage in [1]_.
401
+
402
+ Examples
403
+ --------
404
+ Using the following arguments that may come from any
405
+ `calcparams_.*` function in :py:mod:`pvlib.pvsystem`:
406
+
407
+ >>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4.,
408
+ ... 'resistance_series': 4., 'resistance_shunt': 5000.0}
409
+
410
+ Use default values:
411
+
412
+ >>> v = bishop88_v_from_i(0.0, **args)
413
+
414
+ Specify tolerances and maximum number of iterations:
415
+
416
+ >>> v = bishop88_v_from_i(0.0, **args, method='newton',
417
+ ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20})
418
+
419
+ Retrieve full output from the root finder:
420
+
421
+ >>> v, method_output = bishop88_v_from_i(0.0, **args, method='newton',
422
+ ... method_kwargs={'full_output': True})
343
423
  """
344
424
  # collect args
345
425
  args = (photocurrent, saturation_current, resistance_series,
346
426
  resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor,
347
427
  breakdown_voltage, breakdown_exp)
428
+ method = method.lower()
429
+
430
+ # method_kwargs create dict if not provided
431
+ # this pattern avoids bugs with Mutable Default Parameters
432
+ if not method_kwargs:
433
+ method_kwargs = {}
434
+
348
435
  # first bound the search using voc
349
436
  voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
350
437
 
@@ -352,7 +439,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
352
439
  # calculate current residual given diode voltage "x"
353
440
  return bishop88(x, *a)[0] - i
354
441
 
355
- if method.lower() == 'brentq':
442
+ if method == 'brentq':
356
443
  # brentq only works with scalar inputs, so we need a set up function
357
444
  # and np.vectorize to repeatedly call the optimizer with the right
358
445
  # arguments for possible array input
@@ -361,26 +448,36 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
361
448
  return brentq(fi, 0.0, voc,
362
449
  args=(i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
363
450
  breakdown_factor, breakdown_voltage,
364
- breakdown_exp))
451
+ breakdown_exp),
452
+ **method_kwargs)
365
453
 
366
454
  vd_from_brent_vectorized = np.vectorize(vd_from_brent)
367
455
  vd = vd_from_brent_vectorized(voc_est, current, *args)
368
- elif method.lower() == 'newton':
456
+ elif method == 'newton':
369
457
  # make sure all args are numpy arrays if max size > 1
370
458
  # if voc_est is an array, then make a copy to use for initial guess, v0
371
- args, v0 = _prepare_newton_inputs((current,), args, voc_est)
459
+ args, v0, method_kwargs = \
460
+ _prepare_newton_inputs((current,), args, voc_est, method_kwargs)
372
461
  vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0,
373
462
  fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3],
374
- args=args)
463
+ args=args,
464
+ **method_kwargs)
375
465
  else:
376
466
  raise NotImplementedError("Method '%s' isn't implemented" % method)
377
- return bishop88(vd, *args)[1]
467
+
468
+ # When 'full_output' parameter is specified, returned 'vd' is a tuple with
469
+ # many elements, where the root is the first one. So we use it to output
470
+ # the bishop88 result and return tuple(scalar, tuple with method results)
471
+ if method_kwargs.get('full_output') is True:
472
+ return (bishop88(vd[0], *args)[1], vd)
473
+ else:
474
+ return bishop88(vd, *args)[1]
378
475
 
379
476
 
380
477
  def bishop88_mpp(photocurrent, saturation_current, resistance_series,
381
478
  resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf,
382
479
  breakdown_factor=0., breakdown_voltage=-5.5,
383
- breakdown_exp=3.28, method='newton'):
480
+ breakdown_exp=3.28, method='newton', method_kwargs=None):
384
481
  """
385
482
  Find max power point.
386
483
 
@@ -419,43 +516,91 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
419
516
  method : str, default 'newton'
420
517
  Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
421
518
  if ``breakdown_factor`` is not 0.
519
+ method_kwargs : dict, optional
520
+ Keyword arguments passed to root finder method. See
521
+ :py:func:`scipy:scipy.optimize.brentq` and
522
+ :py:func:`scipy:scipy.optimize.newton` parameters.
523
+ ``'full_output': True`` is allowed, and ``optimizer_output`` would be
524
+ returned. See examples section.
422
525
 
423
526
  Returns
424
527
  -------
425
- OrderedDict or pandas.DataFrame
528
+ tuple
426
529
  max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and
427
530
  max power ``p_mp`` [W]
531
+ optimizer_output : tuple, optional, if specified in ``method_kwargs``
532
+ see root finder documentation for selected method.
533
+ Found root is diode voltage in [1]_.
534
+
535
+ Examples
536
+ --------
537
+ Using the following arguments that may come from any
538
+ `calcparams_.*` function in :py:mod:`pvlib.pvsystem`:
539
+
540
+ >>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4.,
541
+ ... 'resistance_series': 4., 'resistance_shunt': 5000.0}
542
+
543
+ Use default values:
544
+
545
+ >>> i_mp, v_mp, p_mp = bishop88_mpp(**args)
546
+
547
+ Specify tolerances and maximum number of iterations:
548
+
549
+ >>> i_mp, v_mp, p_mp = bishop88_mpp(**args, method='newton',
550
+ ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20})
551
+
552
+ Retrieve full output from the root finder:
553
+
554
+ >>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(**args,
555
+ ... method='newton', method_kwargs={'full_output': True})
428
556
  """
429
557
  # collect args
430
558
  args = (photocurrent, saturation_current, resistance_series,
431
559
  resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor,
432
560
  breakdown_voltage, breakdown_exp)
561
+ method = method.lower()
562
+
563
+ # method_kwargs create dict if not provided
564
+ # this pattern avoids bugs with Mutable Default Parameters
565
+ if not method_kwargs:
566
+ method_kwargs = {}
567
+
433
568
  # first bound the search using voc
434
569
  voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
435
570
 
436
571
  def fmpp(x, *a):
437
572
  return bishop88(x, *a, gradients=True)[6]
438
573
 
439
- if method.lower() == 'brentq':
574
+ if method == 'brentq':
440
575
  # break out arguments for numpy.vectorize to handle broadcasting
441
576
  vec_fun = np.vectorize(
442
577
  lambda voc, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vbr_a, vbr,
443
578
  vbr_exp: brentq(fmpp, 0.0, voc,
444
579
  args=(iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
445
- vbr_a, vbr, vbr_exp))
580
+ vbr_a, vbr, vbr_exp),
581
+ **method_kwargs)
446
582
  )
447
583
  vd = vec_fun(voc_est, *args)
448
- elif method.lower() == 'newton':
584
+ elif method == 'newton':
449
585
  # make sure all args are numpy arrays if max size > 1
450
586
  # if voc_est is an array, then make a copy to use for initial guess, v0
451
- args, v0 = _prepare_newton_inputs((), args, voc_est)
587
+ args, v0, method_kwargs = \
588
+ _prepare_newton_inputs((), args, voc_est, method_kwargs)
452
589
  vd = newton(
453
590
  func=fmpp, x0=v0,
454
- fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args
455
- )
591
+ fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args,
592
+ **method_kwargs)
456
593
  else:
457
594
  raise NotImplementedError("Method '%s' isn't implemented" % method)
458
- return bishop88(vd, *args)
595
+
596
+ # When 'full_output' parameter is specified, returned 'vd' is a tuple with
597
+ # many elements, where the root is the first one. So we use it to output
598
+ # the bishop88 result and return
599
+ # tuple(tuple with bishop88 solution, tuple with method results)
600
+ if method_kwargs.get('full_output') is True:
601
+ return (bishop88(vd[0], *args), vd)
602
+ else:
603
+ return bishop88(vd, *args)
459
604
 
460
605
 
461
606
  def _get_size_and_shape(args):
@@ -482,7 +627,7 @@ def _get_size_and_shape(args):
482
627
  return size, shape
483
628
 
484
629
 
485
- def _prepare_newton_inputs(i_or_v_tup, args, v0):
630
+ def _prepare_newton_inputs(i_or_v_tup, args, v0, method_kwargs):
486
631
  # broadcast arguments for newton method
487
632
  # the first argument should be a tuple, eg: (i,), (v,) or ()
488
633
  size, shape = _get_size_and_shape(i_or_v_tup + args)
@@ -492,15 +637,20 @@ def _prepare_newton_inputs(i_or_v_tup, args, v0):
492
637
  # copy v0 to a new array and broadcast it to the shape of max size
493
638
  if shape is not None:
494
639
  v0 = np.broadcast_to(v0, shape).copy()
495
- return args, v0
640
+
641
+ # set abs tolerance and maxiter from method_kwargs if not provided
642
+ # apply defaults, but giving priority to user-specified values
643
+ method_kwargs = {**NEWTON_DEFAULT_PARAMS, **method_kwargs}
644
+
645
+ return args, v0, method_kwargs
496
646
 
497
647
 
498
- def _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, current,
499
- saturation_current, photocurrent):
648
+ def _lambertw_v_from_i(current, photocurrent, saturation_current,
649
+ resistance_series, resistance_shunt, nNsVth):
500
650
  # Record if inputs were all scalar
501
651
  output_is_scalar = all(map(np.isscalar,
502
- [resistance_shunt, resistance_series, nNsVth,
503
- current, saturation_current, photocurrent]))
652
+ (current, photocurrent, saturation_current,
653
+ resistance_series, resistance_shunt, nNsVth)))
504
654
 
505
655
  # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which
506
656
  # is generally more numerically stable
@@ -509,9 +659,9 @@ def _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, current,
509
659
  # Ensure that we are working with read-only views of numpy arrays
510
660
  # Turns Series into arrays so that we don't have to worry about
511
661
  # multidimensional broadcasting failing
512
- Gsh, Rs, a, I, I0, IL = \
513
- np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth,
514
- current, saturation_current, photocurrent)
662
+ I, IL, I0, Rs, Gsh, a = \
663
+ np.broadcast_arrays(current, photocurrent, saturation_current,
664
+ resistance_series, conductance_shunt, nNsVth)
515
665
 
516
666
  # Intitalize output V (I might not be float64)
517
667
  V = np.full_like(I, np.nan, dtype=np.float64)
@@ -572,12 +722,12 @@ def _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, current,
572
722
  return V
573
723
 
574
724
 
575
- def _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
576
- saturation_current, photocurrent):
725
+ def _lambertw_i_from_v(voltage, photocurrent, saturation_current,
726
+ resistance_series, resistance_shunt, nNsVth):
577
727
  # Record if inputs were all scalar
578
728
  output_is_scalar = all(map(np.isscalar,
579
- [resistance_shunt, resistance_series, nNsVth,
580
- voltage, saturation_current, photocurrent]))
729
+ (voltage, photocurrent, saturation_current,
730
+ resistance_series, resistance_shunt, nNsVth)))
581
731
 
582
732
  # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which
583
733
  # is generally more numerically stable
@@ -586,9 +736,9 @@ def _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
586
736
  # Ensure that we are working with read-only views of numpy arrays
587
737
  # Turns Series into arrays so that we don't have to worry about
588
738
  # multidimensional broadcasting failing
589
- Gsh, Rs, a, V, I0, IL = \
590
- np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth,
591
- voltage, saturation_current, photocurrent)
739
+ V, IL, I0, Rs, Gsh, a = \
740
+ np.broadcast_arrays(voltage, photocurrent, saturation_current,
741
+ resistance_series, conductance_shunt, nNsVth)
592
742
 
593
743
  # Intitalize output I (V might not be float64)
594
744
  I = np.full_like(V, np.nan, dtype=np.float64) # noqa: E741, N806
@@ -632,36 +782,36 @@ def _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
632
782
 
633
783
  def _lambertw(photocurrent, saturation_current, resistance_series,
634
784
  resistance_shunt, nNsVth, ivcurve_pnts=None):
785
+ # collect args
786
+ params = {'photocurrent': photocurrent,
787
+ 'saturation_current': saturation_current,
788
+ 'resistance_series': resistance_series,
789
+ 'resistance_shunt': resistance_shunt, 'nNsVth': nNsVth}
790
+
635
791
  # Compute short circuit current
636
- i_sc = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, 0.,
637
- saturation_current, photocurrent)
792
+ i_sc = _lambertw_i_from_v(0., **params)
638
793
 
639
794
  # Compute open circuit voltage
640
- v_oc = _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, 0.,
641
- saturation_current, photocurrent)
795
+ v_oc = _lambertw_v_from_i(0., **params)
642
796
 
643
- params = {'r_sh': resistance_shunt,
644
- 'r_s': resistance_series,
645
- 'nNsVth': nNsVth,
646
- 'i_0': saturation_current,
647
- 'i_l': photocurrent}
797
+ # Set small elements <0 in v_oc to 0
798
+ if isinstance(v_oc, np.ndarray):
799
+ v_oc[(v_oc < 0) & (v_oc > -1e-12)] = 0.
800
+ elif isinstance(v_oc, (float, int)):
801
+ if v_oc < 0 and v_oc > -1e-12:
802
+ v_oc = 0.
648
803
 
649
804
  # Find the voltage, v_mp, where the power is maximized.
650
805
  # Start the golden section search at v_oc * 1.14
651
- p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14,
652
- _pwr_optfcn)
806
+ p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn)
653
807
 
654
808
  # Find Imp using Lambert W
655
- i_mp = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth,
656
- v_mp, saturation_current, photocurrent)
809
+ i_mp = _lambertw_i_from_v(v_mp, **params)
657
810
 
658
811
  # Find Ix and Ixx using Lambert W
659
- i_x = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth,
660
- 0.5 * v_oc, saturation_current, photocurrent)
812
+ i_x = _lambertw_i_from_v(0.5 * v_oc, **params)
661
813
 
662
- i_xx = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth,
663
- 0.5 * (v_oc + v_mp), saturation_current,
664
- photocurrent)
814
+ i_xx = _lambertw_i_from_v(0.5 * (v_oc + v_mp), **params)
665
815
 
666
816
  out = (i_sc, v_oc, i_mp, v_mp, p_mp, i_x, i_xx)
667
817
 
@@ -670,9 +820,7 @@ def _lambertw(photocurrent, saturation_current, resistance_series,
670
820
  ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] *
671
821
  np.linspace(0, 1, ivcurve_pnts))
672
822
 
673
- ivcurve_i = _lambertw_i_from_v(resistance_shunt, resistance_series,
674
- nNsVth, ivcurve_v.T, saturation_current,
675
- photocurrent).T
823
+ ivcurve_i = _lambertw_i_from_v(ivcurve_v.T, **params).T
676
824
 
677
825
  out += (ivcurve_i, ivcurve_v)
678
826
 
@@ -684,7 +832,9 @@ def _pwr_optfcn(df, loc):
684
832
  Function to find power from ``i_from_v``.
685
833
  '''
686
834
 
687
- I = _lambertw_i_from_v(df['r_sh'], df['r_s'], # noqa: E741, N806
688
- df['nNsVth'], df[loc], df['i_0'], df['i_l'])
835
+ current = _lambertw_i_from_v(df[loc], df['photocurrent'],
836
+ df['saturation_current'],
837
+ df['resistance_series'],
838
+ df['resistance_shunt'], df['nNsVth'])
689
839
 
690
- return I * df[loc]
840
+ return current * df[loc]
pvlib/snow.py CHANGED
@@ -219,7 +219,7 @@ def _townsend_effective_snow(snow_total, snow_events):
219
219
 
220
220
  def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
221
221
  temp_air, poa_global, slant_height, lower_edge_height,
222
- angle_of_repose=40):
222
+ string_factor=1.0, angle_of_repose=40):
223
223
  '''
224
224
  Calculates monthly snow loss based on the Townsend monthly snow loss
225
225
  model [1]_.
@@ -230,7 +230,8 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
230
230
  Snow received each month. Referred to as S in [1]_. [cm]
231
231
 
232
232
  snow_events : array-like
233
- Number of snowfall events each month. Referred to as N in [1]_. [-]
233
+ Number of snowfall events each month. May be int or float type for
234
+ the average events in a typical month. Referred to as N in [1]_.
234
235
 
235
236
  surface_tilt : float
236
237
  Tilt angle of the array. [deg]
@@ -250,6 +251,11 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
250
251
  lower_edge_height : float
251
252
  Distance from array lower edge to the ground. [m]
252
253
 
254
+ string_factor : float, default 1.0
255
+ Multiplier applied to monthly loss fraction. Use 1.0 if the DC array
256
+ has only one string of modules in the slant direction, use 0.75
257
+ otherwise. [-]
258
+
253
259
  angle_of_repose : float, default 40
254
260
  Piled snow angle, assumed to stabilize at 40°, the midpoint of
255
261
  25°-55° avalanching slope angles. [deg]
@@ -263,7 +269,12 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
263
269
  -----
264
270
  This model has not been validated for tracking arrays; however, for
265
271
  tracking arrays [1]_ suggests using the maximum rotation angle in place
266
- of ``surface_tilt``.
272
+ of ``surface_tilt``. The author of [1]_ recommends using one-half the
273
+ table width for ``slant_height``, i.e., the distance from the tracker
274
+ axis to the module edge.
275
+
276
+ The parameter `string_factor` is an enhancement added to the model after
277
+ publication of [1]_ per private communication with the model's author.
267
278
 
268
279
  References
269
280
  ----------
@@ -273,13 +284,22 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
273
284
  :doi:`10.1109/PVSC.2011.6186627`
274
285
  '''
275
286
 
287
+ # unit conversions from cm and m to in, from C to K, and from % to fraction
288
+ # doing this early to facilitate comparison of this code with [1]
289
+ snow_total_inches = snow_total / 2.54 # to inches
290
+ relative_humidity_fraction = relative_humidity / 100.
291
+ poa_global_kWh = poa_global / 1000.
292
+ slant_height_inches = slant_height * 39.37
293
+ lower_edge_height_inches = lower_edge_height * 39.37
294
+ temp_air_kelvin = temp_air + 273.15
295
+
276
296
  C1 = 5.7e04
277
297
  C2 = 0.51
278
298
 
279
- snow_total_prev = np.roll(snow_total, 1)
299
+ snow_total_prev = np.roll(snow_total_inches, 1)
280
300
  snow_events_prev = np.roll(snow_events, 1)
281
301
 
282
- effective_snow = _townsend_effective_snow(snow_total, snow_events)
302
+ effective_snow = _townsend_effective_snow(snow_total_inches, snow_events)
283
303
  effective_snow_prev = _townsend_effective_snow(
284
304
  snow_total_prev,
285
305
  snow_events_prev
@@ -288,37 +308,38 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
288
308
  1 / 3 * effective_snow_prev
289
309
  + 2 / 3 * effective_snow
290
310
  )
291
- effective_snow_weighted_m = effective_snow_weighted / 100
292
311
 
293
- lower_edge_height_clipped = np.maximum(lower_edge_height, 0.01)
312
+ # the lower limit of 0.1 in^2 is per private communication with the model's
313
+ # author. CWH 1/30/2023
314
+ lower_edge_distance = np.clip(
315
+ lower_edge_height_inches**2 - effective_snow_weighted**2, a_min=0.1,
316
+ a_max=None)
294
317
  gamma = (
295
- slant_height
296
- * effective_snow_weighted_m
318
+ slant_height_inches
319
+ * effective_snow_weighted
297
320
  * cosd(surface_tilt)
298
- / (lower_edge_height_clipped**2 - effective_snow_weighted_m**2)
321
+ / lower_edge_distance
299
322
  * 2
300
323
  * tand(angle_of_repose)
301
324
  )
302
325
 
303
326
  ground_interference_term = 1 - C2 * np.exp(-gamma)
304
- relative_humidity_fraction = relative_humidity / 100
305
- temp_air_kelvin = temp_air + 273.15
306
- effective_snow_weighted_in = effective_snow_weighted / 2.54
307
- poa_global_kWh = poa_global / 1000
308
327
 
309
328
  # Calculate Eqn. 3 in the reference.
310
329
  # Although the reference says Eqn. 3 calculates percentage loss, the y-axis
311
330
  # of Figure 7 indicates Eqn. 3 calculates fractional loss. Since the slope
312
331
  # of the line in Figure 7 is the same as C1 in Eqn. 3, it is assumed that
313
332
  # Eqn. 3 calculates fractional loss.
333
+
314
334
  loss_fraction = (
315
335
  C1
316
- * effective_snow_weighted_in
336
+ * effective_snow_weighted
317
337
  * cosd(surface_tilt)**2
318
338
  * ground_interference_term
319
339
  * relative_humidity_fraction
320
340
  / temp_air_kelvin**2
321
341
  / poa_global_kWh**0.67
342
+ * string_factor
322
343
  )
323
344
 
324
345
  return np.clip(loss_fraction, 0, 1)