Skip to content

Commit 2d11d5f

Browse files
echedey-lsmikofski
andcommitted
Update implementation based on PSZ PR. See description.
Commit highlights ✨ : * I think I made all the code a bit more legible; sorry for the big changes @mikofski * Tests a bit more complete (not much, still consider the same test data) * Rename shaded fraction acronym from `fs` to `sf` * Asserts changed to `assert_allclose` for a more legible output in case of failure Co-Authored-By: Mark Mikofski <[email protected]>
1 parent 766a670 commit 2d11d5f

File tree

2 files changed

+78
-53
lines changed

2 files changed

+78
-53
lines changed

pvlib/shading.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -307,22 +307,25 @@ def projected_solar_zenith_angle(solar_zenith, solar_azimuth,
307307
return theta_T
308308

309309

310-
def tracker_shaded_fraction(tracker_theta, gcr, projected_solar_zenith,
311-
cross_axis_slope=0):
310+
def tracker_shaded_fraction(solar_zenith, solar_azimuth, tracker_tilt,
311+
tracker_azimuth, gcr, cross_axis_slope=0):
312312
r"""
313313
Shaded fraction for trackers with a common angle on an east-west slope.
314314
315315
Parameters
316316
----------
317-
tracker_theta : numeric
318-
The tracker rotation angle in degrees from horizontal.
319-
gcr : float
317+
solar_zenith : numeric
318+
Solar position zenith, in degrees.
319+
solar_azimuth : numeric
320+
Solar position azimuth, in degrees.
321+
tracker_tilt : numeric
322+
In degrees.
323+
tracker_azimuth : numeric
324+
In degrees. North=0º, South=180º, East=90º, West=270º.
325+
gcr : numeric
320326
The ground coverage ratio as a fraction equal to the collector width
321327
over the horizontal row-to-row pitch.
322-
projected_solar_zenith : numeric
323-
Zenith angle in degrees of the solar vector projected into the plane
324-
perpendicular to the tracker axes.
325-
cross_axis_slope : float, default 0
328+
cross_axis_slope : numeric, default 0
326329
Angle of the plane containing the tracker axes in degrees from
327330
horizontal.
328331
@@ -368,9 +371,19 @@ def tracker_shaded_fraction(tracker_theta, gcr, projected_solar_zenith,
368371
PVPMC, 2023
369372
"""
370373
theta_g_rad = np.radians(cross_axis_slope)
374+
# projected solar zenith:
375+
# consider the angle the sun direct beam has on the vertical plane which
376+
# contains the tracker normal vector, with respect to a horizontal line
377+
projected_solar_zenith = projected_solar_zenith_angle(
378+
0, # no rotation from the horizontal
379+
# the vector that defines the projection plane for prior conditions
380+
tracker_azimuth-90,
381+
solar_zenith,
382+
solar_azimuth
383+
)
371384
# angle opposite shadow cast on the ground, z
372385
angle_z = (
373-
np.pi / 2 - np.radians(tracker_theta)
386+
np.pi / 2 - np.radians(tracker_tilt)
374387
+ np.radians(projected_solar_zenith))
375388
# angle opposite the collector width, L
376389
angle_gcr = (
@@ -381,9 +394,9 @@ def tracker_shaded_fraction(tracker_theta, gcr, projected_solar_zenith,
381394
# there's only row-to-row shade loss if the shadow on the ground, z, is
382395
# longer than row-to-row pitch projected on the ground, P*cos(theta_g)
383396
zp_cos_g = zp*np.cos(theta_g_rad)
384-
# shaded fraction (fs)
385-
fs = np.where(zp_cos_g <= 1, 0, 1 - 1/zp_cos_g)
386-
return fs
397+
# shaded fraction (sf)
398+
sf = np.where(zp_cos_g <= 1, 0, 1 - 1/zp_cos_g)
399+
return sf
387400

388401

389402
def linear_shade_loss(shaded_fraction, diffuse_fraction):
@@ -414,8 +427,8 @@ def linear_shade_loss(shaded_fraction, diffuse_fraction):
414427
Example
415428
-------
416429
>>> from pvlib import shading
417-
>>> fs = shading.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
418-
>>> loss = shading.linear_shade_loss(fs, 0.2)
430+
>>> sf = shading.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
431+
>>> loss = shading.linear_shade_loss(sf, 0.2)
419432
>>> P_no_shade = 100 # [kWdc] DC output from modules
420433
>>> P_linear_shade = P_no_shade * (1-loss) # [kWdc] output after loss
421434
# 90.71067811865476 [kWdc]

pvlib/tests/test_shading.py

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -226,43 +226,55 @@ def test_projected_solar_zenith_angle_datatypes(
226226

227227

228228
@pytest.fixture
229-
def expected_fs():
230-
# trivial case, 80% gcr, no slope, trackers & psz at 45-deg
231-
z0 = np.sqrt(2*0.8*0.8)
232-
# another trivial case, 60% gcr, no slope, trackers & psz at 60-deg
233-
z1 = 2*0.6
234-
# 30-deg isosceles, 60% gcr, no slope, 30-deg trackers, psz at 60-deg
235-
z2 = 0.6*np.sqrt(3)
236-
z = np.array([z0, z1, z2])
237-
return 1 - 1/z
238-
239-
240-
def test_tracker_shade_fraction(expected_fs):
241-
"""closes gh1690"""
242-
fs = shading.tracker_shaded_fraction(45.0, 0.8, 45.0)
243-
assert np.isclose(fs, expected_fs[0])
244-
# same trivial case with 40%, shadow is only 0.565-m long < 1-m r2r P
245-
zero_fs = shading.tracker_shaded_fraction(45.0, 0.4, 45.0)
246-
assert np.isclose(zero_fs, 0)
247-
# test vectors
248-
tracker_theta = [45.0, 60.0, 30.0]
249-
gcr = [0.8, 0.6, 0.6]
250-
psz = [45.0, 60.0, 60.0]
251-
slope = [0]*3
252-
fs_vec = shading.tracker_shaded_fraction(
253-
tracker_theta, gcr, psz, slope)
254-
assert np.allclose(fs_vec, expected_fs)
255-
256-
257-
def test_linear_shade_loss(expected_fs):
258-
loss = shading.linear_shade_loss(expected_fs[0], 0.2)
259-
assert np.isclose(loss, 0.09289321881345258)
229+
def sf_premises_and_expected():
230+
"""Data comprised of solar position, tracker orientations, ground coverage
231+
ratios and terrain slopes with respective shade fractions (sf)"""
232+
# preserve tracker_shade_fraction's args order and append shadow depth, z
233+
premises_and_results = pd.DataFrame(
234+
columns=["solar_zenith", "solar_azimuth", "tracker_tilt",
235+
"tracker_azimuth", "gcr", "cross_axis_slope", "z"],
236+
data=(
237+
# trivial case, 80% gcr, no slope, trackers & psz at 45-deg
238+
(45, 90., 45, 90., 0.8, 0, np.sqrt(2*0.8*0.8)),
239+
# another trivial case, 60% gcr, no slope, trackers & psz at 60-deg
240+
(60, 120, 60, 120, 0.6, 0, 2*0.6),
241+
# 30-deg isosceles, 60% gcr, no slope, 30-deg trackers, psz 60-deg
242+
(60, 135, 30, 135, 0.6, 0, 0.6*np.sqrt(3)),
243+
# no shading, 40% gcr, shadow is only 0.565-m long < 1-m r2r P
244+
(45, 180, 45, 180, 0.4, 0, 1) # z := 1 means no shadow
245+
))
246+
# append shaded fraction
247+
premises_and_results["shaded_fraction"] = 1 - 1/premises_and_results["z"]
248+
return premises_and_results
249+
250+
251+
def test_tracker_shade_fraction(sf_premises_and_expected):
252+
"""Tests tracker_shade_fraction"""
253+
# unwrap sf_premises_and_expected values premises and expected results
254+
premises = sf_premises_and_expected.drop(columns=["z", "shaded_fraction"])
255+
expected_sf_array = sf_premises_and_expected["shaded_fraction"]
256+
# test scalar inputs from the row iterator
257+
# series label := corresponding index in expected_sf_array
258+
for index, premise in premises.iterrows():
259+
expected_result = expected_sf_array[index]
260+
sf = shading.tracker_shaded_fraction(**premise)
261+
assert_allclose(sf, expected_result)
262+
263+
# test vector inputs
264+
sf_vec = shading.tracker_shaded_fraction(**premises)
265+
assert_allclose(sf_vec, expected_sf_array)
266+
267+
268+
def test_linear_shade_loss(sf_premises_and_expected):
269+
expected_sf_array = sf_premises_and_expected["shaded_fraction"]
270+
loss = shading.linear_shade_loss(expected_sf_array[0], 0.2)
271+
assert_allclose(loss, 0.09289321881345258)
260272
# if no diffuse, shade fraction is the loss
261-
loss_no_df = shading.linear_shade_loss(expected_fs[0], 0)
262-
assert np.isclose(loss_no_df, expected_fs[0])
273+
loss_no_df = shading.linear_shade_loss(expected_sf_array[0], 0)
274+
assert_allclose(loss_no_df, expected_sf_array[0])
263275
# if all diffuse, no shade loss
264-
no_loss = shading.linear_shade_loss(expected_fs[0], 1.0)
265-
assert np.isclose(no_loss, 0)
266-
vec_loss = shading.linear_shade_loss(expected_fs, 0.2)
267-
expected_loss = np.array([0.09289322, 0.13333333, 0.03019964])
268-
assert np.allclose(vec_loss, expected_loss)
276+
no_loss = shading.linear_shade_loss(expected_sf_array[0], 1.0)
277+
assert_allclose(no_loss, 0)
278+
vec_loss = shading.linear_shade_loss(expected_sf_array, 0.2)
279+
expected_loss = np.array([0.09289322, 0.13333333, 0.03019964, 0.0])
280+
assert_allclose(vec_loss, expected_loss)

0 commit comments

Comments
 (0)