pvlib 0.13.1a1__py3-none-any.whl → 0.15.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/singlediode.py CHANGED
@@ -3,6 +3,7 @@ Low-level functions for solving the single diode equation.
3
3
  """
4
4
 
5
5
  import numpy as np
6
+ import pandas as pd
6
7
  from pvlib.tools import _golden_sect_DataFrame
7
8
 
8
9
  from scipy.optimize import brentq, newton
@@ -109,13 +110,13 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
109
110
  (a-Si) modules that is the product of the PV module number of series
110
111
  cells :math:`N_{s}` and the builtin voltage :math:`V_{bi}` of the
111
112
  intrinsic layer. [V].
112
- breakdown_factor : float, default 0
113
+ breakdown_factor : numeric, default 0
113
114
  fraction of ohmic current involved in avalanche breakdown :math:`a`.
114
115
  Default of 0 excludes the reverse bias term from the model. [unitless]
115
- breakdown_voltage : float, default -5.5
116
+ breakdown_voltage : numeric, default -5.5
116
117
  reverse breakdown voltage of the photovoltaic junction :math:`V_{br}`
117
118
  [V]
118
- breakdown_exp : float, default 3.28
119
+ breakdown_exp : numeric, default 3.28
119
120
  avalanche breakdown exponent :math:`m` [unitless]
120
121
  gradients : bool
121
122
  False returns only I, V, and P. True also returns gradients
@@ -141,18 +142,20 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
141
142
 
142
143
  References
143
144
  ----------
144
- .. [1] "Computer simulation of the effects of electrical mismatches in
145
- photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
146
- :doi:`10.1016/0379-6787(88)90059-2`
147
-
148
- .. [2] "Improved equivalent circuit and Analytical Model for Amorphous
149
- Silicon Solar Cells and Modules." J. Mertens, et al., IEEE Transactions
150
- on Electron Devices, Vol 45, No 2, Feb 1998.
145
+ .. [1] J.W. Bishop, "Computer simulation of the effects of electrical
146
+ mismatches in photovoltaic cell interconnection circuits" Solar Cells,
147
+ vol. 25 no. 1, pp. 73-89, Oct. 1988.
148
+ :doi:`doi.org/10.1016/0379-6787(88)90059-2`
149
+
150
+ .. [2] J. Merten, J. M. Asensi, C. Voz, A. V. Shah, R. Platz and J. Andreu,
151
+ "Improved equivalent circuit and Analytical Model for Amorphous
152
+ Silicon Solar Cells and Modules." , IEEE Transactions
153
+ on Electron Devices, vol. 45, no. 2, pp. 423-429, Feb 1998.
151
154
  :doi:`10.1109/16.658676`
152
155
 
153
- .. [3] "Performance assessment of a simulation model for PV modules of any
154
- available technology", André Mermoud and Thibault Lejeune, 25th EUPVSEC,
155
- 2010
156
+ .. [3] A. Mermoud and T. Lejeune, "Performance assessment of a simulation
157
+ model for PV modules of any available technology", In Proc. of the 25th
158
+ European PVSEC, Valencia, ES, 2010.
156
159
  :doi:`10.4229/25thEUPVSEC2010-4BV.1.114`
157
160
  """
158
161
  # calculate recombination loss current where d2mutau > 0
@@ -162,12 +165,11 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
162
165
  # calculate temporary values to simplify calculations
163
166
  v_star = diode_voltage / nNsVth # non-dimensional diode voltage
164
167
  g_sh = 1.0 / resistance_shunt # conductance
165
- if breakdown_factor > 0: # reverse bias is considered
166
- brk_term = 1 - diode_voltage / breakdown_voltage
167
- brk_pwr = np.power(brk_term, -breakdown_exp)
168
- i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr
169
- else:
170
- i_breakdown = 0.
168
+
169
+ brk_term = 1 - diode_voltage / breakdown_voltage
170
+ brk_pwr = np.power(brk_term, -breakdown_exp)
171
+ i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr
172
+
171
173
  i = (photocurrent - saturation_current * np.expm1(v_star) # noqa: W503
172
174
  - diode_voltage * g_sh - i_recomb - i_breakdown) # noqa: W503
173
175
  v = diode_voltage - i * resistance_series
@@ -177,18 +179,14 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
177
179
  grad_i_recomb = np.where(is_recomb, i_recomb / v_recomb, 0)
178
180
  grad_2i_recomb = np.where(is_recomb, 2 * grad_i_recomb / v_recomb, 0)
179
181
  g_diode = saturation_current * np.exp(v_star) / nNsVth # conductance
180
- if breakdown_factor > 0: # reverse bias is considered
181
- brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1)
182
- brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2)
183
- brk_fctr = breakdown_factor * g_sh
184
- grad_i_brk = brk_fctr * (brk_pwr + diode_voltage *
185
- -breakdown_exp * brk_pwr_1)
186
- grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503
187
- * (2 * brk_pwr_1 + diode_voltage # noqa: W503
188
- * (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503
189
- else:
190
- grad_i_brk = 0.
191
- grad2i_brk = 0.
182
+ brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1)
183
+ brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2)
184
+ brk_fctr = breakdown_factor * g_sh
185
+ grad_i_brk = brk_fctr * (brk_pwr + diode_voltage *
186
+ -breakdown_exp * brk_pwr_1)
187
+ grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503
188
+ * (2 * brk_pwr_1 + diode_voltage # noqa: W503
189
+ * (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503
192
190
  grad_i = -g_diode - g_sh - grad_i_recomb - grad_i_brk # di/dvd
193
191
  grad_v = 1.0 - grad_i * resistance_series # dv/dvd
194
192
  # dp/dv = d(iv)/dv = v * di/dv + i
@@ -247,12 +245,19 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
247
245
  breakdown_exp : float, default 3.28
248
246
  avalanche breakdown exponent :math:`m` [unitless]
249
247
  method : str, default 'newton'
250
- Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
251
- if ``breakdown_factor`` is not 0.
248
+ Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
249
+ ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0.
250
+
251
+ .. note::
252
+ ``'chandrupatla'`` requires scipy 1.15 or greater.
253
+
252
254
  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.
255
+ Keyword arguments passed to the root finder. For options, see:
256
+
257
+ * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq`
258
+ * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton`
259
+ * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root`
260
+
256
261
  ``'full_output': True`` is allowed, and ``optimizer_output`` would be
257
262
  returned. See examples section.
258
263
 
@@ -291,7 +296,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
291
296
  .. [1] "Computer simulation of the effects of electrical mismatches in
292
297
  photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
293
298
  :doi:`10.1016/0379-6787(88)90059-2`
294
- """
299
+ """ # noqa: E501
295
300
  # collect args
296
301
  args = (photocurrent, saturation_current,
297
302
  resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
@@ -333,6 +338,30 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
333
338
  vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=x0,
334
339
  fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4],
335
340
  args=args, **method_kwargs)
341
+ elif method == 'chandrupatla':
342
+ try:
343
+ from scipy.optimize.elementwise import find_root
344
+ except ModuleNotFoundError as e:
345
+ # TODO remove this when our minimum scipy version is >=1.15
346
+ msg = (
347
+ "method='chandrupatla' requires scipy v1.15 or greater "
348
+ "(available for Python 3.10+). "
349
+ "Select another method, or update your version of scipy."
350
+ )
351
+ raise ImportError(msg) from e
352
+
353
+ voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
354
+ shape = _shape_of_max_size(voltage, voc_est)
355
+ vlo = np.zeros(shape)
356
+ vhi = np.full(shape, voc_est)
357
+ bounds = (vlo, vhi)
358
+ kwargs_trimmed = method_kwargs.copy()
359
+ kwargs_trimmed.pop("full_output", None) # not valid for find_root
360
+
361
+ result = find_root(fv, bounds, args=(voltage, *args), **kwargs_trimmed)
362
+ vd = result.x
363
+ if method_kwargs.get('full_output'):
364
+ vd = (vd, result) # mimic the other methods
336
365
  else:
337
366
  raise NotImplementedError("Method '%s' isn't implemented" % method)
338
367
 
@@ -388,12 +417,19 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
388
417
  breakdown_exp : float, default 3.28
389
418
  avalanche breakdown exponent :math:`m` [unitless]
390
419
  method : str, default 'newton'
391
- Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
392
- if ``breakdown_factor`` is not 0.
420
+ Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
421
+ ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0.
422
+
423
+ .. note::
424
+ ``'chandrupatla'`` requires scipy 1.15 or greater.
425
+
393
426
  method_kwargs : dict, optional
394
- Keyword arguments passed to root finder method. See
395
- :py:func:`scipy:scipy.optimize.brentq` and
396
- :py:func:`scipy:scipy.optimize.newton` parameters.
427
+ Keyword arguments passed to the root finder. For options, see:
428
+
429
+ * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq`
430
+ * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton`
431
+ * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root`
432
+
397
433
  ``'full_output': True`` is allowed, and ``optimizer_output`` would be
398
434
  returned. See examples section.
399
435
 
@@ -432,7 +468,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
432
468
  .. [1] "Computer simulation of the effects of electrical mismatches in
433
469
  photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
434
470
  :doi:`10.1016/0379-6787(88)90059-2`
435
- """
471
+ """ # noqa: E501
436
472
  # collect args
437
473
  args = (photocurrent, saturation_current,
438
474
  resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
@@ -474,6 +510,29 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
474
510
  vd = newton(func=lambda x, *a: fi(x, current, *a), x0=x0,
475
511
  fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3],
476
512
  args=args, **method_kwargs)
513
+ elif method == 'chandrupatla':
514
+ try:
515
+ from scipy.optimize.elementwise import find_root
516
+ except ModuleNotFoundError as e:
517
+ # TODO remove this when our minimum scipy version is >=1.15
518
+ msg = (
519
+ "method='chandrupatla' requires scipy v1.15 or greater "
520
+ "(available for Python 3.10+). "
521
+ "Select another method, or update your version of scipy."
522
+ )
523
+ raise ImportError(msg) from e
524
+
525
+ shape = _shape_of_max_size(current, voc_est)
526
+ vlo = np.zeros(shape)
527
+ vhi = np.full(shape, voc_est)
528
+ bounds = (vlo, vhi)
529
+ kwargs_trimmed = method_kwargs.copy()
530
+ kwargs_trimmed.pop("full_output", None) # not valid for find_root
531
+
532
+ result = find_root(fi, bounds, args=(current, *args), **kwargs_trimmed)
533
+ vd = result.x
534
+ if method_kwargs.get('full_output'):
535
+ vd = (vd, result) # mimic the other methods
477
536
  else:
478
537
  raise NotImplementedError("Method '%s' isn't implemented" % method)
479
538
 
@@ -526,12 +585,19 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
526
585
  breakdown_exp : numeric, default 3.28
527
586
  avalanche breakdown exponent :math:`m` [unitless]
528
587
  method : str, default 'newton'
529
- Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
530
- if ``breakdown_factor`` is not 0.
588
+ Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
589
+ ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0.
590
+
591
+ .. note::
592
+ ``'chandrupatla'`` requires scipy 1.15 or greater.
593
+
531
594
  method_kwargs : dict, optional
532
- Keyword arguments passed to root finder method. See
533
- :py:func:`scipy:scipy.optimize.brentq` and
534
- :py:func:`scipy:scipy.optimize.newton` parameters.
595
+ Keyword arguments passed to the root finder. For options, see:
596
+
597
+ * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq`
598
+ * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton`
599
+ * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root`
600
+
535
601
  ``'full_output': True`` is allowed, and ``optimizer_output`` would be
536
602
  returned. See examples section.
537
603
 
@@ -571,7 +637,7 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
571
637
  .. [1] "Computer simulation of the effects of electrical mismatches in
572
638
  photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
573
639
  :doi:`10.1016/0379-6787(88)90059-2`
574
- """
640
+ """ # noqa: E501
575
641
  # collect args
576
642
  args = (photocurrent, saturation_current,
577
643
  resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
@@ -611,6 +677,31 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
611
677
  vd = newton(func=fmpp, x0=x0,
612
678
  fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7],
613
679
  args=args, **method_kwargs)
680
+ elif method == 'chandrupatla':
681
+ try:
682
+ from scipy.optimize.elementwise import find_root
683
+ except ModuleNotFoundError as e:
684
+ # TODO remove this when our minimum scipy version is >=1.15
685
+ msg = (
686
+ "method='chandrupatla' requires scipy v1.15 or greater "
687
+ "(available for Python 3.10+). "
688
+ "Select another method, or update your version of scipy."
689
+ )
690
+ raise ImportError(msg) from e
691
+
692
+ vlo = np.zeros_like(photocurrent)
693
+ vhi = np.full_like(photocurrent, voc_est)
694
+ kwargs_trimmed = method_kwargs.copy()
695
+ kwargs_trimmed.pop("full_output", None) # not valid for find_root
696
+
697
+ result = find_root(fmpp,
698
+ (vlo, vhi),
699
+ args=args,
700
+ **kwargs_trimmed)
701
+ vd = result.x
702
+ if method_kwargs.get('full_output'):
703
+ vd = (vd, result) # mimic the other methods
704
+
614
705
  else:
615
706
  raise NotImplementedError("Method '%s' isn't implemented" % method)
616
707
 
@@ -825,10 +916,25 @@ def _lambertw(photocurrent, saturation_current, resistance_series,
825
916
  v_oc = 0.
826
917
 
827
918
  # Find the voltage, v_mp, where the power is maximized.
828
- # Start the golden section search at v_oc * 1.14
829
- p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn)
919
+ # use scipy.elementwise if available
920
+ # remove try/except when scipy>=1.15, and golden mean is retired
921
+ try:
922
+ from scipy.optimize.elementwise import find_minimum
923
+ # left negative to insure strict inequality
924
+ init = (-1., 0.8*v_oc, v_oc)
925
+ res = find_minimum(_vmp_opt, init,
926
+ args=(params['photocurrent'],
927
+ params['saturation_current'],
928
+ params['resistance_series'],
929
+ params['resistance_shunt'],
930
+ params['nNsVth'],))
931
+ v_mp = res.x
932
+ p_mp = -1.*res.f_x
933
+ except ModuleNotFoundError:
934
+ # switch to old golden section method
935
+ p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14,
936
+ _pwr_optfcn)
830
937
 
831
- # Find Imp using Lambert W
832
938
  i_mp = _lambertw_i_from_v(v_mp, **params)
833
939
 
834
940
  # Find Ix and Ixx using Lambert W
@@ -850,6 +956,15 @@ def _lambertw(photocurrent, saturation_current, resistance_series,
850
956
  return out
851
957
 
852
958
 
959
+ def _vmp_opt(v, iph, io, rs, rsh, nNsVth):
960
+ '''
961
+ Function to find negative of power from ``i_from_v``.
962
+ '''
963
+ current = _lambertw_i_from_v(v, iph, io, rs, rsh, nNsVth)
964
+
965
+ return -v * current
966
+
967
+
853
968
  def _pwr_optfcn(df, loc):
854
969
  '''
855
970
  Function to find power from ``i_from_v``.
@@ -861,3 +976,85 @@ def _pwr_optfcn(df, loc):
861
976
  df['resistance_shunt'], df['nNsVth'])
862
977
 
863
978
  return current * df[loc]
979
+
980
+
981
+ def batzelis(photocurrent, saturation_current, resistance_series,
982
+ resistance_shunt, nNsVth):
983
+ """
984
+ Estimate maximum power, open-circuit, and short-circuit points from
985
+ single-diode equation parameters using Batzelis's method.
986
+
987
+ This method is described in Section II.B of [1]_.
988
+
989
+ Parameters
990
+ ----------
991
+ photocurrent : numeric
992
+ Light-generated current. [A]
993
+ saturation_current : numeric
994
+ Diode saturation current. [A]
995
+ resistance_series : numeric
996
+ Series resistance. [Ohm]
997
+ resistance_shunt : numeric
998
+ Shunt resistance. [Ohm]
999
+ nNsVth : numeric
1000
+ The product of the usual diode ideality factor (n, unitless),
1001
+ number of cells in series (Ns), and cell thermal voltage at
1002
+ specified effective irradiance and cell temperature. [V]
1003
+
1004
+ Returns
1005
+ -------
1006
+ dict
1007
+ The returned dict-like object contains the keys/columns:
1008
+
1009
+ * ``p_mp`` - power at maximum power point. [W]
1010
+ * ``i_mp`` - current at maximum power point. [A]
1011
+ * ``v_mp`` - voltage at maximum power point. [V]
1012
+ * ``i_sc`` - short circuit current. [A]
1013
+ * ``v_oc`` - open circuit voltage. [V]
1014
+
1015
+ References
1016
+ ----------
1017
+ .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well
1018
+ Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7,
1019
+ no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431`
1020
+ """
1021
+ # convenience variables
1022
+ Iph = photocurrent
1023
+ Is = saturation_current
1024
+ Rsh = resistance_shunt
1025
+ Rs = resistance_series
1026
+ a = nNsVth
1027
+
1028
+ # Eqs 3-4
1029
+ isc = Iph / (Rs / Rsh + 1) # manipulated to handle Rsh=np.inf correctly
1030
+ with np.errstate(divide='ignore'): # zero Iph
1031
+ voc = a * np.log(Iph / Is)
1032
+
1033
+ # Eqs 5-8
1034
+ w = np.real(lambertw(np.e * Iph / Is))
1035
+ # vmp = (1 + Rs/Rsh) * a * (w - 1) - Rs * Iph * (1 - 1/w) # not needed
1036
+ with np.errstate(divide='ignore', invalid='ignore'): # zero Iph -> zero w
1037
+ imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh
1038
+
1039
+ vmp = a * (w - 1) - Rs * imp
1040
+
1041
+ vmp = np.where(Iph > 0, vmp, 0)
1042
+ voc = np.where(Iph > 0, voc, 0)
1043
+ imp = np.where(Iph > 0, imp, 0)
1044
+ isc = np.where(Iph > 0, isc, 0)
1045
+
1046
+ out = {
1047
+ 'p_mp': imp * vmp,
1048
+ 'i_mp': imp,
1049
+ 'v_mp': vmp,
1050
+ 'i_sc': isc,
1051
+ 'v_oc': voc,
1052
+ }
1053
+
1054
+ # if pandas in, ensure pandas out
1055
+ pandas_inputs = [
1056
+ x for x in [Iph, Is, Rsh, Rs, a] if isinstance(x, pd.Series)]
1057
+ if pandas_inputs:
1058
+ out = pd.DataFrame(out, index=pandas_inputs[0].index)
1059
+
1060
+ return out
pvlib/solarposition.py CHANGED
@@ -314,7 +314,7 @@ def spa_python(time, latitude, longitude,
314
314
  using time.year and time.month from pandas.DatetimeIndex.
315
315
  For most simulations the default delta_t is sufficient.
316
316
  The USNO has historical and forecasted delta_t [3]_.
317
- atmos_refrac : float, optional
317
+ atmos_refract : float, optional
318
318
  The approximate atmospheric refraction (in degrees)
319
319
  at sunrise and sunset.
320
320
  how : str, optional, default 'numpy'
pvlib/spa.py CHANGED
@@ -1057,7 +1057,7 @@ def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t,
1057
1057
  degrees C; used for atmospheric correction
1058
1058
  delta_t : float or array
1059
1059
  Difference between terrestrial time and UT1.
1060
- atmos_refrac : float
1060
+ atmos_refract : float
1061
1061
  The approximate atmospheric refraction (in degrees)
1062
1062
  at sunrise and sunset.
1063
1063
  numthreads: int, optional, default 8
@@ -3,9 +3,10 @@ from pvlib.spectrum.mismatch import ( # noqa: F401
3
3
  calc_spectral_mismatch_field,
4
4
  spectral_factor_caballero,
5
5
  spectral_factor_firstsolar,
6
- spectral_factor_sapm,
7
- spectral_factor_pvspec,
8
6
  spectral_factor_jrc,
7
+ spectral_factor_polo,
8
+ spectral_factor_pvspec,
9
+ spectral_factor_sapm,
9
10
  )
10
11
  from pvlib.spectrum.irradiance import ( # noqa: F401
11
12
  get_reference_spectra,
@@ -698,3 +698,104 @@ def spectral_factor_jrc(airmass, clearsky_index, module_type=None,
698
698
  + coeff[2] * (airmass - 1.5)
699
699
  )
700
700
  return mismatch
701
+
702
+
703
+ def spectral_factor_polo(precipitable_water, airmass_absolute, aod500, aoi,
704
+ pressure, module_type=None, coefficients=None,
705
+ albedo=0.2):
706
+ """
707
+ Estimate the spectral mismatch for BIPV application in vertical facades.
708
+
709
+ The model's authors note that this model could also be applied to
710
+ vertical bifacial ground-mount systems [1]_, although it has not been
711
+ validated in that context.
712
+
713
+ Parameters
714
+ ----------
715
+ precipitable_water : numeric
716
+ Atmospheric precipitable water. [cm]
717
+ airmass_absolute : numeric
718
+ Absolute (pressure-adjusted) airmass. See :term:`airmass_absolute`.
719
+ [unitless]
720
+ aod500 : numeric
721
+ Atmospheric aerosol optical depth at 500 nm. [unitless]
722
+ aoi : numeric
723
+ Angle of incidence on the vertical surface. See :term:`aoi`.
724
+ [degrees]
725
+ pressure : numeric
726
+ Atmospheric pressure. See :term:`pressure`. [Pa]
727
+ module_type : str, optional
728
+ One of the following PV technology strings from [1]_:
729
+
730
+ * ``'cdte'`` - anonymous CdTe module.
731
+ * ``'monosi'`` - anonymous monocrystalline silicon module.
732
+ * ``'cigs'`` - anonymous copper indium gallium selenide module.
733
+ * ``'asi'`` - anonymous amorphous silicon module.
734
+ coefficients : array-like, optional
735
+ User-defined coefficients, if not using one of the coefficient
736
+ sets via the ``module_type`` parameter. Must have nine elements.
737
+ The first six elements correspond to the [p1, p2, p3, p4, b, c]
738
+ parameters of the SMM model. The last three elements corresponds
739
+ to the [c1, c2, c3] parameters of the albedo correction factor.
740
+ albedo : numeric, default 0.2
741
+ Ground albedo. See :term:`albedo`. [unitless]
742
+
743
+ Returns
744
+ -------
745
+ modifier: numeric
746
+ spectral mismatch factor (unitless) which is multiplied
747
+ with broadband irradiance reaching a module's cells to estimate
748
+ effective irradiance, i.e., the irradiance that is converted to
749
+ electrical current.
750
+
751
+ Notes
752
+ -----
753
+ The Polo model was developed using only SMM values computed for scenarios
754
+ when the sun is visible from the module's surface (i.e., for ``aoi<90``),
755
+ and no provision was made in [1]_ for the case of ``aoi>90``. This would
756
+ create issues in the air mass calculation internal to the model.
757
+ Following discussion with the model's author, the pvlib implementation
758
+ handles ``aoi>90`` by truncating the input ``aoi`` to a maximum of
759
+ 90 degrees.
760
+
761
+ References
762
+ ----------
763
+ .. [1] J. Polo and C. Sanz-Saiz, 'Development of spectral mismatch models
764
+ for BIPV applications in building façades', Renewable Energy, vol. 245,
765
+ p. 122820, Jun. 2025, :doi:`10.1016/j.renene.2025.122820`
766
+ """
767
+ if module_type is None and coefficients is None:
768
+ raise ValueError('Must provide either `module_type` or `coefficients`')
769
+ if module_type is not None and coefficients is not None:
770
+ raise ValueError('Only one of `module_type` and `coefficients` should '
771
+ 'be provided')
772
+ # prevent nan for aoi greater than 90; see docstring Notes
773
+ aoi = np.clip(aoi, a_min=None, a_max=90)
774
+ f_aoi_rel = pvlib.atmosphere.get_relative_airmass(aoi,
775
+ model='kastenyoung1989')
776
+ f_aoi = pvlib.atmosphere.get_absolute_airmass(f_aoi_rel, pressure)
777
+ Ram = f_aoi / airmass_absolute
778
+ _coefficients = {
779
+ 'cdte': (-0.0009, 46.80, 49.20, -0.87, 0.00041, 0.053),
780
+ 'monosi': (0.0027, 10.34, 9.48, 0.31, 0.00077, 0.006),
781
+ 'cigs': (0.0017, 2.33, 1.30, 0.11, 0.00098, -0.018),
782
+ 'asi': (0.0024, 7.32, 7.09, -0.72, -0.0013, 0.089),
783
+ }
784
+ c = {
785
+ 'asi': (0.0056, -0.020, 1.014),
786
+ 'cigs': (-0.0009, -0.0003, 1),
787
+ 'cdte': (0.0021, -0.01, 1.01),
788
+ 'monosi': (0, -0.003, 1.0),
789
+ }
790
+ if module_type is not None:
791
+ coeff = _coefficients[module_type]
792
+ c_albedo = c[module_type]
793
+ else:
794
+ coeff = coefficients[:6]
795
+ c_albedo = coefficients[6:]
796
+ smm = coeff[0] * Ram + coeff[1] / (coeff[2] + Ram**coeff[3]) \
797
+ + coeff[4] / aod500 + coeff[5]*np.sqrt(precipitable_water)
798
+ # Ground albedo correction
799
+ g = c_albedo[0] * (albedo/0.2)**2 \
800
+ + c_albedo[1] * (albedo/0.2) + c_albedo[2]
801
+ return g*smm
pvlib/temperature.py CHANGED
@@ -456,14 +456,14 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84):
456
456
  speed at module height used to determine NOCT. [m/s]
457
457
 
458
458
  u0 : numeric, default 25.0
459
- Combined heat loss factor coefficient. The default value is one
460
- determined by Faiman for 7 silicon modules
459
+ Combined heat loss factor coefficient. The default value is for module
460
+ temperature determined by Faiman for 7 silicon modules
461
461
  in the Negev desert on an open rack at 30.9° tilt.
462
462
  :math:`\left[\frac{\text{W}/{\text{m}^2}}{\text{C}}\right]`
463
463
 
464
464
  u1 : numeric, default 6.84
465
- Combined heat loss factor influenced by wind. The default value is one
466
- determined by Faiman for 7 silicon modules
465
+ Combined heat loss factor influenced by wind. The default value is
466
+ for module temperature determined by Faiman for 7 silicon modules
467
467
  in the Negev desert on an open rack at 30.9° tilt.
468
468
  :math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`
469
469
 
@@ -539,14 +539,14 @@ def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None,
539
539
  surface. [W/m^2]
540
540
 
541
541
  u0 : numeric, default 25.0
542
- Combined heat loss factor coefficient. The default value is one
543
- determined by Faiman for 7 silicon modules
542
+ Combined heat loss factor coefficient. The default value is for module
543
+ temperature determined by Faiman for 7 silicon modules
544
544
  in the Negev desert on an open rack at 30.9° tilt.
545
545
  :math:`\left[\frac{\text{W}/{\text{m}^2}}{\text{C}}\right]`
546
546
 
547
547
  u1 : numeric, default 6.84
548
- Combined heat loss factor influenced by wind. The default value is one
549
- determined by Faiman for 7 silicon modules
548
+ Combined heat loss factor influenced by wind. The default value is for
549
+ module temperature determined by Faiman for 7 silicon modules
550
550
  in the Negev desert on an open rack at 30.9° tilt.
551
551
  :math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`
552
552
 
@@ -713,8 +713,7 @@ def ross(poa_global, temp_air, noct=None, k=None):
713
713
  return temp_air + k * poa_global
714
714
 
715
715
 
716
- def _fuentes_hconv(tave, windmod, tinoct, temp_delta, xlen, tilt,
717
- check_reynold):
716
+ def _fuentes_hconv(tave, windmod, temp_delta, xlen, tilt, check_reynold):
718
717
  # Calculate the convective coefficient as in Fuentes 1987 -- a mixture of
719
718
  # free, laminar, and turbulent convection.
720
719
  densair = 0.003484 * 101325.0 / tave # density
@@ -836,7 +835,7 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5,
836
835
  # convective coefficient of top surface of module at NOCT
837
836
  windmod = 1.0
838
837
  tave = (tinoct + 293.15) / 2
839
- hconv = _fuentes_hconv(tave, windmod, tinoct, tinoct - 293.15, xlen,
838
+ hconv = _fuentes_hconv(tave, windmod, tinoct - 293.15, xlen,
840
839
  surface_tilt, False)
841
840
 
842
841
  # determine the ground temperature ratio and the ratio of the total
@@ -896,7 +895,7 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5,
896
895
  for j in range(10):
897
896
  # overall convective coefficient
898
897
  tave = (tmod + tamb) / 2
899
- hconv = convrat * _fuentes_hconv(tave, windmod, tinoct,
898
+ hconv = convrat * _fuentes_hconv(tave, windmod,
900
899
  abs(tmod-tamb), xlen,
901
900
  surface_tilt, True)
902
901
  # sky radiation coefficient (Equation 3)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pvlib
3
- Version: 0.13.1a1
3
+ Version: 0.15.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>
@@ -29,7 +29,7 @@ Requires-Dist: cython; extra == "optional"
29
29
  Requires-Dist: ephem; extra == "optional"
30
30
  Requires-Dist: nrel-pysam; extra == "optional"
31
31
  Requires-Dist: numba>=0.17.0; extra == "optional"
32
- Requires-Dist: solarfactors; extra == "optional"
32
+ Requires-Dist: solarfactors>=1.6.1; extra == "optional"
33
33
  Requires-Dist: statsmodels; extra == "optional"
34
34
  Provides-Extra: doc
35
35
  Requires-Dist: ipython; extra == "doc"
@@ -42,7 +42,7 @@ Requires-Dist: docutils==0.21; extra == "doc"
42
42
  Requires-Dist: pillow; extra == "doc"
43
43
  Requires-Dist: sphinx-toggleprompt==0.5.2; extra == "doc"
44
44
  Requires-Dist: sphinx-favicon; extra == "doc"
45
- Requires-Dist: solarfactors; extra == "doc"
45
+ Requires-Dist: solarfactors>=1.6.1; extra == "doc"
46
46
  Requires-Dist: sphinx-hoverxref~=1.4.2; extra == "doc"
47
47
  Provides-Extra: test
48
48
  Requires-Dist: pytest; extra == "test"