Skip to content

Commit c1856ac

Browse files
kandersolarcwhanse
andauthored
Vectorize pvlib.bifacial.utils._vf_ground_sky_2d across surface_tilt (#1682)
* vectorize _vf_ground_sky_2d across surface_tilt also a few other micro-optimizations * whatsnew * remove straggler instrumentation * docstring improvement * stickler * update failing test * remove "per-point" language * sprinkle in some `del`s * contorted version using numpy's `out` parameter * even more out * add vectorize kwarg to infinite_sheds * benchmark both vectorize=True and vectorize=False * improve comments * test coverage * stickler * Apply suggestions from code review Co-authored-by: Cliff Hansen <[email protected]> * stickler --------- Co-authored-by: Cliff Hansen <[email protected]>
1 parent ef8ad2f commit c1856ac

File tree

6 files changed

+117
-68
lines changed

6 files changed

+117
-68
lines changed

benchmarks/benchmarks/infinite_sheds.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010

1111
class InfiniteSheds:
1212

13-
def setup(self):
13+
# benchmark variant parameters (run both vectorize=True and False)
14+
params = [True, False]
15+
param_names = ['vectorize']
16+
17+
def setup(self, vectorize):
1418
self.times = pd.date_range(start='20180601', freq='1min',
1519
periods=1440)
1620
self.location = location.Location(40, -80)
@@ -38,7 +42,7 @@ def setup(self):
3842
gcr=self.gcr
3943
)
4044

41-
def time_get_irradiance_poa_fixed(self):
45+
def time_get_irradiance_poa_fixed(self, vectorize):
4246
infinite_sheds.get_irradiance_poa(
4347
surface_tilt=self.surface_tilt,
4448
surface_azimuth=self.surface_azimuth,
@@ -51,10 +55,11 @@ def time_get_irradiance_poa_fixed(self):
5155
dhi=self.clearsky_irradiance['dhi'],
5256
dni=self.clearsky_irradiance['dni'],
5357
albedo=self.albedo,
54-
npoints=self.npoints
58+
npoints=self.npoints,
59+
vectorize=vectorize,
5560
)
5661

57-
def time_get_irradiance_poa_tracking(self):
62+
def time_get_irradiance_poa_tracking(self, vectorize):
5863
infinite_sheds.get_irradiance_poa(
5964
surface_tilt=self.tracking['surface_tilt'],
6065
surface_azimuth=self.tracking['surface_azimuth'],
@@ -67,10 +72,11 @@ def time_get_irradiance_poa_tracking(self):
6772
dhi=self.clearsky_irradiance['dhi'],
6873
dni=self.clearsky_irradiance['dni'],
6974
albedo=self.albedo,
70-
npoints=self.npoints
75+
npoints=self.npoints,
76+
vectorize=vectorize,
7177
)
7278

73-
def time_get_irradiance_fixed(self):
79+
def time_get_irradiance_fixed(self, vectorize):
7480
infinite_sheds.get_irradiance(
7581
surface_tilt=self.surface_tilt,
7682
surface_azimuth=self.surface_azimuth,
@@ -83,10 +89,11 @@ def time_get_irradiance_fixed(self):
8389
dhi=self.clearsky_irradiance['dhi'],
8490
dni=self.clearsky_irradiance['dni'],
8591
albedo=self.albedo,
86-
npoints=self.npoints
92+
npoints=self.npoints,
93+
vectorize=vectorize,
8794
)
8895

89-
def time_get_irradiance_tracking(self):
96+
def time_get_irradiance_tracking(self, vectorize):
9097
infinite_sheds.get_irradiance(
9198
surface_tilt=self.tracking['surface_tilt'],
9299
surface_azimuth=self.tracking['surface_azimuth'],
@@ -99,5 +106,6 @@ def time_get_irradiance_tracking(self):
99106
dhi=self.clearsky_irradiance['dhi'],
100107
dni=self.clearsky_irradiance['dni'],
101108
albedo=self.albedo,
102-
npoints=self.npoints
109+
npoints=self.npoints,
110+
vectorize=vectorize,
103111
)

docs/sphinx/source/whatsnew/v0.9.5.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ Enhancements
3131
:py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa`
3232
to enable use of the hay-davies sky diffuse irradiance model
3333
instead of the default isotropic model. (:pull:`1668`)
34+
* Added an optional ``vectorize`` parameter to
35+
:py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and
36+
:py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` which,
37+
when set to ``True``, enables faster calculation but with increased
38+
memory usage. (:issue:`1680`, :pull:`1682`)
3439
* Update the ADR inverter model parameter database by appending the ADR equivalents of all
3540
inverters in the current (2019) Sandia inverter model parameter database. (:pull:`1695`)
3641
* Use `Horner's Method <https://en.wikipedia.org/wiki/Horner%27s_method>`_
@@ -87,5 +92,6 @@ Contributors
8792
* Michael Deceglie (:ghuser:`mdeceglie`)
8893
* Saurabh Aneja (:ghuser:`spaneja`)
8994
* John Moseley (:ghuser:`johnMoseleyArray`)
95+
* Areeba Turabi (:ghuser:`aturabi`)
9096
* Mark Campanelli (:ghuser:`markcampanelli`)
9197
* Taos Transue (:ghuser:`reepoi`)

pvlib/bifacial/infinite_sheds.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
from pvlib.irradiance import beam_component, aoi, haydavies
1111

1212
def _vf_ground_sky_integ(surface_tilt, surface_azimuth, gcr, height,
13-
pitch, max_rows=10, npoints=100):
13+
pitch, max_rows=10, npoints=100, vectorize=False):
1414
"""
15-
Integrated and per-point view factors from the ground to the sky at points
16-
between interior rows of the array.
15+
Integrated view factor to the sky from the ground underneath
16+
interior rows of the array.
1717
1818
Parameters
1919
----------
@@ -35,20 +35,16 @@ def _vf_ground_sky_integ(surface_tilt, surface_azimuth, gcr, height,
3535
Maximum number of rows to consider in front and behind the current row.
3636
npoints : int, default 100
3737
Number of points used to discretize distance along the ground.
38+
vectorize : bool, default False
39+
If True, vectorize the view factor calculation across ``surface_tilt``.
40+
This increases speed with the cost of increased memory usage.
3841
3942
Returns
4043
-------
41-
fgnd_sky : float
44+
fgnd_sky : numeric
4245
Integration of view factor over the length between adjacent, interior
43-
rows. [unitless]
44-
fz : ndarray
45-
Fraction of distance from the previous row to the next row. [unitless]
46-
fz_sky : ndarray
47-
View factors at discrete points between adjacent, interior rows.
48-
[unitless]
49-
46+
rows. Shape matches that of ``surface_tilt``. [unitless]
5047
"""
51-
# TODO: vectorize over surface_tilt
5248
# Abuse utils._vf_ground_sky_2d by supplying surface_tilt in place
5349
# of a signed rotation. This is OK because
5450
# 1) z span the full distance between 2 rows, and
@@ -57,12 +53,16 @@ def _vf_ground_sky_integ(surface_tilt, surface_azimuth, gcr, height,
5753
# The VFs to the sky will thus be symmetric around z=0.5
5854
z = np.linspace(0, 1, npoints)
5955
rotation = np.atleast_1d(surface_tilt)
60-
fz_sky = np.zeros((len(rotation), npoints))
61-
for k, r in enumerate(rotation):
62-
vf, _ = utils._vf_ground_sky_2d(z, r, gcr, pitch, height, max_rows)
63-
fz_sky[k, :] = vf
56+
if vectorize:
57+
fz_sky = utils._vf_ground_sky_2d(z, rotation, gcr, pitch, height,
58+
max_rows)
59+
else:
60+
fz_sky = np.zeros((npoints, len(rotation)))
61+
for k, r in enumerate(rotation):
62+
vf = utils._vf_ground_sky_2d(z, r, gcr, pitch, height, max_rows)
63+
fz_sky[:, k] = vf[:, 0] # remove spurious rotation dimension
6464
# calculate the integrated view factor for all of the ground between rows
65-
return np.trapz(fz_sky, z, axis=1)
65+
return np.trapz(fz_sky, z, axis=0)
6666

6767

6868
def _poa_ground_shadows(poa_ground, f_gnd_beam, df, vf_gnd_sky):
@@ -401,7 +401,7 @@ def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,
401401
def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
402402
solar_azimuth, gcr, height, pitch, ghi, dhi, dni,
403403
albedo, model='isotropic', dni_extra=None, iam=1.0,
404-
npoints=100):
404+
npoints=100, vectorize=False):
405405
r"""
406406
Calculate plane-of-array (POA) irradiance on one side of a row of modules.
407407
@@ -469,7 +469,12 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
469469
on the surface that is not reflected away. [unitless]
470470
471471
npoints : int, default 100
472-
Number of points used to discretize distance along the ground.
472+
Number of discretization points for calculating integrated view
473+
factors.
474+
475+
vectorize : bool, default False
476+
If True, vectorize the view factor calculation across ``surface_tilt``.
477+
This increases speed with the cost of increased memory usage.
473478
474479
Returns
475480
-------
@@ -537,7 +542,8 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
537542
# method differs from [1], Eq. 7 and Eq. 8; height is defined at row
538543
# center rather than at row lower edge as in [1].
539544
vf_gnd_sky = _vf_ground_sky_integ(
540-
surface_tilt, surface_azimuth, gcr, height, pitch, max_rows, npoints)
545+
surface_tilt, surface_azimuth, gcr, height, pitch, max_rows, npoints,
546+
vectorize)
541547
# fraction of row slant height that is shaded from direct irradiance
542548
f_x = _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,
543549
surface_azimuth, gcr)
@@ -610,7 +616,7 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
610616
gcr, height, pitch, ghi, dhi, dni,
611617
albedo, model='isotropic', dni_extra=None, iam_front=1.0,
612618
iam_back=1.0, bifaciality=0.8, shade_factor=-0.02,
613-
transmission_factor=0, npoints=100):
619+
transmission_factor=0, npoints=100, vectorize=False):
614620
"""
615621
Get front and rear irradiance using the infinite sheds model.
616622
@@ -701,7 +707,12 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
701707
etc. A negative value is a reduction in back irradiance. [unitless]
702708
703709
npoints : int, default 100
704-
Number of points used to discretize distance along the ground.
710+
Number of discretization points for calculating integrated view
711+
factors.
712+
713+
vectorize : bool, default False
714+
If True, vectorize the view factor calculation across ``surface_tilt``.
715+
This increases speed with the cost of increased memory usage.
705716
706717
Returns
707718
-------
@@ -756,14 +767,14 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
756767
solar_zenith=solar_zenith, solar_azimuth=solar_azimuth,
757768
gcr=gcr, height=height, pitch=pitch, ghi=ghi, dhi=dhi, dni=dni,
758769
albedo=albedo, model=model, dni_extra=dni_extra, iam=iam_front,
759-
npoints=npoints)
770+
npoints=npoints, vectorize=vectorize)
760771
# back side POA irradiance
761772
irrad_back = get_irradiance_poa(
762773
surface_tilt=backside_tilt, surface_azimuth=backside_sysaz,
763774
solar_zenith=solar_zenith, solar_azimuth=solar_azimuth,
764775
gcr=gcr, height=height, pitch=pitch, ghi=ghi, dhi=dhi, dni=dni,
765776
albedo=albedo, model=model, dni_extra=dni_extra, iam=iam_back,
766-
npoints=npoints)
777+
npoints=npoints, vectorize=vectorize)
767778

768779
colmap_front = {
769780
'poa_global': 'poa_front',

pvlib/bifacial/utils.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import numpy as np
66
from pvlib.tools import sind, cosd, tand
77

8-
98
def _solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth):
109
"""
1110
Tangent of the angle between the zenith vector and the sun vector
@@ -104,7 +103,7 @@ def _vf_ground_sky_2d(x, rotation, gcr, pitch, height, max_rows=10):
104103
Position on the ground between two rows, as a fraction of the pitch.
105104
x = 0 corresponds to the point on the ground directly below the
106105
center point of a row. Positive x is towards the right. [unitless]
107-
rotation : float
106+
rotation : numeric
108107
Rotation angle of the row's right edge relative to row center.
109108
[degree]
110109
gcr : float
@@ -120,30 +119,53 @@ def _vf_ground_sky_2d(x, rotation, gcr, pitch, height, max_rows=10):
120119
121120
Returns
122121
-------
123-
vf : numeric
124-
Fraction of sky dome visible from each point on the ground. [unitless]
125-
wedge_angles : array
126-
Angles defining each wedge of sky that is blocked by a row. Shape is
127-
(2, len(x), 2*max_rows+1). ``wedge_angles[0,:,:]`` is the
128-
starting angle of each wedge, ``wedge_angles[1,:,:]`` is the end angle.
129-
[degree]
122+
vf : array
123+
Fraction of sky dome visible from each point on the ground.
124+
Shape is (len(x), len(rotation)). [unitless]
130125
"""
131-
x = np.atleast_1d(x) # handle float
126+
# This function creates large float64 arrays of size
127+
# (2*len(x)*len(rotation)*len(max_rows)) or ~100 MB for
128+
# typical time series inputs. This function makes heavy
129+
# use of numpy's out parameter to avoid allocating new
130+
# memory. Unfortunately that comes at the cost of some
131+
# readability: because arrays get reused to avoid new allocations,
132+
# variable names don't always match what they hold.
133+
134+
# handle floats:
135+
x = np.atleast_1d(x)[:, np.newaxis, np.newaxis]
136+
rotation = np.atleast_1d(rotation)[np.newaxis, :, np.newaxis]
132137
all_k = np.arange(-max_rows, max_rows + 1)
133138
width = gcr * pitch / 2.
139+
distance_to_row_centers = (all_k - x) * pitch
140+
dy = width * sind(rotation)
141+
dx = width * cosd(rotation)
142+
143+
phi = np.empty((2, x.shape[0], rotation.shape[1], len(all_k)))
144+
134145
# angles from x to right edge of each row
135-
a1 = height + width * sind(rotation)
136-
b1 = (all_k - x[:, np.newaxis]) * pitch + width * cosd(rotation)
137-
phi_1 = np.degrees(np.arctan2(a1, b1))
146+
a1 = height + dy
147+
# temporarily store one leg of the triangle in phi:
148+
np.add(distance_to_row_centers, dx, out=phi[0])
149+
np.arctan2(a1, phi[0], out=phi[0])
150+
138151
# angles from x to left edge of each row
139-
a2 = height - width * sind(rotation)
140-
b2 = (all_k - x[:, np.newaxis]) * pitch - width * cosd(rotation)
141-
phi_2 = np.degrees(np.arctan2(a2, b2))
142-
phi = np.stack([phi_1, phi_2])
143-
swap = phi[0, :, :] > phi[1, :, :]
144-
# swap where phi_1 > phi_2 so that phi_1[0,:,:] is the lesser angle
145-
phi = np.where(swap, phi[::-1], phi)
146-
# right edge of next row - left edge of previous row
147-
wedge_vfs = 0.5 * (cosd(phi[1, :, 1:]) - cosd(phi[0, :, :-1]))
148-
vf = np.sum(np.where(wedge_vfs > 0, wedge_vfs, 0.), axis=1)
149-
return vf, phi
152+
a2 = height - dy
153+
np.subtract(distance_to_row_centers, dx, out=phi[1])
154+
np.arctan2(a2, phi[1], out=phi[1])
155+
156+
# swap angles so that phi[0,:,:,:] is the lesser angle
157+
phi.sort(axis=0)
158+
159+
# now re-use phi's memory again, this time storing cos(phi).
160+
next_edge = phi[1, :, :, 1:]
161+
np.cos(next_edge, out=next_edge)
162+
prev_edge = phi[0, :, :, :-1]
163+
np.cos(prev_edge, out=prev_edge)
164+
# right edge of next row - left edge of previous row, again
165+
# reusing memory so that the difference is stored in next_edge.
166+
# Note that the 0.5 view factor coefficient is applied after summing
167+
# as a minor speed optimization.
168+
np.subtract(next_edge, prev_edge, out=next_edge)
169+
np.clip(next_edge, a_min=0., a_max=None, out=next_edge)
170+
vf = np.sum(next_edge, axis=-1) / 2
171+
return vf

pvlib/tests/bifacial/test_infinite_sheds.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,16 @@ def test_system():
4242
return syst, pts, vfs_ground_sky
4343

4444

45-
def test__vf_ground_sky_integ(test_system):
45+
@pytest.mark.parametrize("vectorize", [True, False])
46+
def test__vf_ground_sky_integ(test_system, vectorize):
4647
ts, pts, vfs_gnd_sky = test_system
4748
# pass rotation here since max_rows=1 for the hand-solved case in
4849
# the fixture test_system, which means the ground-to-sky view factor
4950
# isn't summed over enough rows for symmetry to hold.
5051
vf_integ = infinite_sheds._vf_ground_sky_integ(
5152
ts['rotation'], ts['surface_azimuth'],
5253
ts['gcr'], ts['height'], ts['pitch'],
53-
max_rows=1, npoints=3)
54+
max_rows=1, npoints=3, vectorize=vectorize)
5455
expected_vf_integ = np.trapz(vfs_gnd_sky, pts)
5556
assert np.isclose(vf_integ, expected_vf_integ, rtol=0.1)
5657

@@ -262,7 +263,8 @@ def test__backside_tilt():
262263
assert np.allclose(back_az, np.array([0., 330., 90., 180.]))
263264

264265

265-
def test_get_irradiance():
266+
@pytest.mark.parametrize("vectorize", [True, False])
267+
def test_get_irradiance(vectorize):
266268
# singleton inputs
267269
solar_zenith = 0.
268270
solar_azimuth = 180.
@@ -282,7 +284,7 @@ def test_get_irradiance():
282284
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
283285
gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back,
284286
bifaciality=0.8, shade_factor=-0.02, transmission_factor=0,
285-
npoints=npoints)
287+
npoints=npoints, vectorize=vectorize)
286288
expected_front_diffuse = np.array([300.])
287289
expected_front_direct = np.array([700.])
288290
expected_front_global = expected_front_diffuse + expected_front_direct
@@ -300,11 +302,11 @@ def test_get_irradiance():
300302
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
301303
gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back,
302304
bifaciality=0.8, shade_factor=-0.02, transmission_factor=0,
303-
npoints=npoints)
305+
npoints=npoints, vectorize=vectorize)
304306
result_front = infinite_sheds.get_irradiance_poa(
305307
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
306308
gcr, height, pitch, ghi, dhi, dni,
307-
albedo, iam=iam_front)
309+
albedo, iam=iam_front, vectorize=vectorize)
308310
assert isinstance(result, pd.DataFrame)
309311
expected_poa_global = pd.Series(
310312
[1000., 500., result_front['poa_global'][2] * (1 + 0.8 * 0.98),

pvlib/tests/bifacial/test_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_system_fixed_tilt():
3535
c22 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3)**2) # right edge row 0
3636
c23 = (0 - sqr3) / np.sqrt(1.25**2 + (0 - sqr3)**2) # right edge row 1
3737
vf_2 = 0.5 * (c23 - c22 + c21 - c20) # vf at point 1
38-
vfs_ground_sky = np.array([vf_0, vf_1, vf_2])
38+
vfs_ground_sky = np.array([[vf_0], [vf_1], [vf_2]])
3939
return syst, pts, vfs_ground_sky
4040

4141

@@ -79,10 +79,10 @@ def test__unshaded_ground_fraction(
7979
def test__vf_ground_sky_2d(test_system_fixed_tilt):
8080
# vector input
8181
ts, pts, vfs_gnd_sky = test_system_fixed_tilt
82-
vfs, _ = utils._vf_ground_sky_2d(pts, ts['rotation'], ts['gcr'],
83-
ts['pitch'], ts['height'], max_rows=1)
82+
vfs = utils._vf_ground_sky_2d(pts, ts['rotation'], ts['gcr'],
83+
ts['pitch'], ts['height'], max_rows=1)
8484
assert np.allclose(vfs, vfs_gnd_sky, rtol=0.1) # middle point vf is off
8585
# test with singleton x
86-
vf, _ = utils._vf_ground_sky_2d(pts[0], ts['rotation'], ts['gcr'],
87-
ts['pitch'], ts['height'], max_rows=1)
86+
vf = utils._vf_ground_sky_2d(pts[0], ts['rotation'], ts['gcr'],
87+
ts['pitch'], ts['height'], max_rows=1)
8888
assert np.isclose(vf, vfs_gnd_sky[0])

0 commit comments

Comments
 (0)