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.
- pvlib/__init__.py +3 -2
- pvlib/atmosphere.py +23 -173
- pvlib/bifacial/infinite_sheds.py +88 -277
- pvlib/bifacial/utils.py +270 -28
- pvlib/data/adr-library-cec-inverters-2019-03-05.csv +5009 -0
- pvlib/data/precise_iv_curves1.json +10251 -0
- pvlib/data/precise_iv_curves2.json +10251 -0
- pvlib/data/precise_iv_curves_parameter_sets1.csv +33 -0
- pvlib/data/precise_iv_curves_parameter_sets2.csv +33 -0
- pvlib/data/test_psm3_2017.csv +17521 -17521
- pvlib/data/test_psm3_2019_5min.csv +288 -288
- pvlib/data/test_read_psm3.csv +17522 -17522
- pvlib/data/test_read_pvgis_horizon.csv +49 -0
- pvlib/data/variables_style_rules.csv +3 -0
- pvlib/iam.py +207 -51
- pvlib/inverter.py +6 -1
- pvlib/iotools/__init__.py +7 -2
- pvlib/iotools/acis.py +516 -0
- pvlib/iotools/midc.py +4 -4
- pvlib/iotools/psm3.py +59 -42
- pvlib/iotools/pvgis.py +84 -28
- pvlib/iotools/sodapro.py +8 -6
- pvlib/iotools/srml.py +121 -18
- pvlib/iotools/surfrad.py +2 -2
- pvlib/iotools/tmy.py +146 -102
- pvlib/irradiance.py +270 -15
- pvlib/ivtools/sde.py +14 -20
- pvlib/ivtools/sdm.py +31 -20
- pvlib/ivtools/utils.py +127 -6
- pvlib/location.py +3 -2
- pvlib/modelchain.py +67 -70
- pvlib/pvarray.py +225 -0
- pvlib/pvsystem.py +169 -539
- pvlib/shading.py +43 -2
- pvlib/singlediode.py +216 -66
- pvlib/snow.py +36 -15
- pvlib/soiling.py +3 -3
- pvlib/spa.py +327 -368
- pvlib/spectrum/__init__.py +8 -2
- pvlib/spectrum/mismatch.py +335 -0
- pvlib/temperature.py +124 -13
- pvlib/tests/bifacial/test_infinite_sheds.py +44 -106
- pvlib/tests/bifacial/test_utils.py +102 -5
- pvlib/tests/conftest.py +0 -31
- pvlib/tests/iotools/test_acis.py +213 -0
- pvlib/tests/iotools/test_midc.py +6 -6
- pvlib/tests/iotools/test_psm3.py +7 -5
- pvlib/tests/iotools/test_pvgis.py +21 -14
- pvlib/tests/iotools/test_sodapro.py +1 -1
- pvlib/tests/iotools/test_srml.py +71 -6
- pvlib/tests/iotools/test_tmy.py +43 -8
- pvlib/tests/ivtools/test_sde.py +19 -17
- pvlib/tests/ivtools/test_sdm.py +9 -4
- pvlib/tests/ivtools/test_utils.py +96 -1
- pvlib/tests/test_atmosphere.py +8 -64
- pvlib/tests/test_clearsky.py +0 -1
- pvlib/tests/test_iam.py +74 -1
- pvlib/tests/test_irradiance.py +56 -2
- pvlib/tests/test_location.py +1 -1
- pvlib/tests/test_modelchain.py +33 -76
- pvlib/tests/test_pvarray.py +46 -0
- pvlib/tests/test_pvsystem.py +366 -201
- pvlib/tests/test_shading.py +35 -0
- pvlib/tests/test_singlediode.py +306 -29
- pvlib/tests/test_snow.py +84 -1
- pvlib/tests/test_soiling.py +8 -7
- pvlib/tests/test_solarposition.py +7 -7
- pvlib/tests/test_spa.py +6 -7
- pvlib/tests/test_spectrum.py +145 -1
- pvlib/tests/test_temperature.py +29 -11
- pvlib/tests/test_tools.py +41 -0
- pvlib/tests/test_tracking.py +0 -149
- pvlib/tools.py +49 -25
- pvlib/tracking.py +1 -269
- pvlib-0.10.0.dist-info/AUTHORS.md +35 -0
- {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/LICENSE +5 -2
- {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/METADATA +3 -13
- {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/RECORD +80 -75
- {pvlib-0.9.4a1.dist-info → pvlib-0.10.0.dist-info}/WHEEL +1 -1
- pvlib/data/adr-library-2013-10-01.csv +0 -1762
- pvlib/forecast.py +0 -1211
- pvlib/iotools/ecmwf_macc.py +0 -312
- pvlib/tests/iotools/test_ecmwf_macc.py +0 -162
- pvlib/tests/test_forecast.py +0 -228
- pvlib-0.9.4a1.dist-info/AUTHORS.md +0 -32
- {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
|
|
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
|
-
#
|
|
13
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
513
|
-
np.broadcast_arrays(
|
|
514
|
-
|
|
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(
|
|
576
|
-
|
|
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
|
-
|
|
580
|
-
|
|
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
|
-
|
|
590
|
-
np.broadcast_arrays(
|
|
591
|
-
|
|
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(
|
|
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(
|
|
641
|
-
saturation_current, photocurrent)
|
|
795
|
+
v_oc = _lambertw_v_from_i(0., **params)
|
|
642
796
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
688
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
*
|
|
318
|
+
slant_height_inches
|
|
319
|
+
* effective_snow_weighted
|
|
297
320
|
* cosd(surface_tilt)
|
|
298
|
-
/
|
|
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
|
-
*
|
|
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)
|