Skip to content

Commit 4f4e1c3

Browse files
mikofskiechedey-ls
authored andcommitted
responding to comments
- move linear shade loss to shading module - don't use ternary, doesn't work on vectors, instead use np.where() - set cross axis default to zero - test vectors - update docs
1 parent 4d3946b commit 4f4e1c3

File tree

7 files changed

+133
-114
lines changed

7 files changed

+133
-114
lines changed

docs/sphinx/source/reference/effects_on_pv_system_output/shading.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ Shading
1111
shading.masking_angle_passias
1212
shading.sky_diffuse_passias
1313
shading.projected_solar_zenith_angle
14+
shading.tracker_shaded_fraction
15+
shading.linear_shade_loss
16+

docs/sphinx/source/reference/tracking.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,3 @@ Functions
1313
tracking.calc_axis_tilt
1414
tracking.calc_cross_axis_tilt
1515
tracking.calc_surface_orientation
16-
tracking.tracker_shaded_fraction
17-
tracking.linear_shade_loss

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Deprecations
1111

1212
Enhancements
1313
~~~~~~~~~~~~
14-
* added functions `pvlib.tracking.tracker_shaded_fraction` and
15-
`pvlib.tracking.linear_shade_loss` to calculate row-to-row shade and apply
14+
* added functions `pvlib.shading.tracker_shaded_fraction` and
15+
`pvlib.shading.linear_shade_loss` to calculate row-to-row shade and apply
1616
linear shade loss for thin film CdTe modules like First Solar.
1717
(:issue:`1689`, :issue:`1690`, :pull:`1725`)
1818

pvlib/shading.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,88 @@ def projected_solar_zenith_angle(solar_zenith, solar_azimuth,
305305
# Eq. (5); angle between sun's beam and surface
306306
theta_T = np.degrees(np.arctan2(sx_prime, sz_prime))
307307
return theta_T
308+
309+
310+
def tracker_shaded_fraction(tracker_theta, gcr, projected_solar_zenith,
311+
cross_axis_slope=0):
312+
"""
313+
Shade fraction (FS) for trackers with a common angle on an east-west slope.
314+
315+
Parameters
316+
----------
317+
tracker_theta : numeric
318+
The tracker rotation angle in degrees from horizontal.
319+
gcr : float
320+
The ground coverage ratio as a fraction equal to the collector width
321+
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
326+
Angle of the plane containing the tracker axes in degrees from
327+
horizontal.
328+
329+
Returns
330+
-------
331+
shade_fraction : numeric
332+
The fraction of the collector width shaded by an adjacent row. A
333+
value of 1 is completely shaded and zero is no shade.
334+
335+
References
336+
----------
337+
Mark A. Mikofski, "First Solar Irradiance Shade Losses on Sloped Terrain,"
338+
PVPMC, 2023
339+
"""
340+
theta_g_rad = np.radians(cross_axis_slope)
341+
# angle opposite shadow cast on the ground, z
342+
angle_z = (
343+
np.pi / 2 - np.radians(tracker_theta)
344+
+ np.radians(projected_solar_zenith))
345+
# angle opposite the collector width, L
346+
angle_gcr = (
347+
np.pi / 2 - np.radians(projected_solar_zenith)
348+
- theta_g_rad)
349+
# ratio of shadow, z, to pitch, P
350+
zp = gcr * np.sin(angle_z) / np.sin(angle_gcr)
351+
# there's only row-to-row shade loss if the shadow on the ground, z, is
352+
# longer than row-to-row pitch projected on the ground, P*cos(theta_g)
353+
zp_cos_g = zp*np.cos(theta_g_rad)
354+
# shade fraction
355+
fs = np.where(zp_cos_g <= 1, 0, 1 - 1/zp_cos_g)
356+
return fs
357+
358+
359+
def linear_shade_loss(shade_fraction, diffuse_fraction):
360+
"""
361+
Fraction of power lost to linear shade loss applicable to CdTe modules like
362+
First Solar.
363+
364+
Parameters
365+
----------
366+
shade_fraction : numeric
367+
The fraction of the collector width shaded by an adjacent row. A
368+
value of 1 is completely shaded and zero is no shade.
369+
diffuse_fraction : numeric
370+
The ratio of diffuse plane of array (poa) irradiance to global poa.
371+
A value of 1 is completely diffuse and zero is no diffuse.
372+
373+
Returns
374+
-------
375+
linear_shade_loss : numeric
376+
The fraction of power lost due to linear shading. A value of 1 is all
377+
power lost and zero is no loss.
378+
379+
See also
380+
--------
381+
pvlib.tracking.tracker_shaded_fraction
382+
383+
Example
384+
-------
385+
>>> from pvlib import tracking
386+
>>> fs = tracking.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
387+
>>> loss = tracking.linear_shade_loss(fs, 0.2)
388+
>>> P_no_shade = 100 # [kWdc] DC output from modules
389+
>>> P_linear_shade = P_no_shade * (1-loss) # [kWdc] output after loss
390+
# 90.71067811865476 [kWdc]
391+
"""
392+
return shade_fraction * (1 - diffuse_fraction)

pvlib/tests/test_shading.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,46 @@ def test_projected_solar_zenith_angle_datatypes(
223223
)
224224
psz = psz_func(sun_apparent_zenith, axis_azimuth, axis_tilt, axis_azimuth)
225225
assert isinstance(psz, cast_type)
226+
227+
228+
@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)
260+
# 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])
263+
# 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)

pvlib/tests/test_tracking.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -472,28 +472,3 @@ def test_calc_surface_orientation_special():
472472
# in a modulo-360 sense.
473473
np.testing.assert_allclose(np.round(out['surface_azimuth'], 4) % 360,
474474
expected_azimuths, rtol=1e-5, atol=1e-5)
475-
476-
477-
@pytest.fixture
478-
def expected_fs():
479-
# trivial case, 80% gcr, no slope, trackers & psz at 45-deg
480-
z = np.sqrt(2*0.8*0.8)
481-
return 1 - 1/z
482-
483-
484-
def test_tracker_shade_fraction(expected_fs):
485-
"""closes gh1690"""
486-
fs = tracking.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
487-
assert np.isclose(fs, expected_fs)
488-
# same trivial case with 40%, shadow is only 0.565-m long < 1-m r2r P
489-
zero_fs = tracking.tracker_shaded_fraction(45.0, 0.4, 45.0, 0)
490-
assert np.isclose(zero_fs, 0)
491-
492-
493-
def test_linear_shade_loss(expected_fs):
494-
loss = tracking.linear_shade_loss(expected_fs, 0.2)
495-
assert np.isclose(loss, 0.09289321881345258)
496-
loss_no_df = tracking.linear_shade_loss(expected_fs, 0)
497-
assert np.isclose(loss_no_df, expected_fs)
498-
no_loss = tracking.linear_shade_loss(expected_fs, 1.0)
499-
assert np.isclose(no_loss, 0)

pvlib/tracking.py

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -406,88 +406,3 @@ def calc_cross_axis_tilt(
406406
# equation 26
407407
beta_c = _calc_beta_c(v, delta_gamma, axis_tilt)
408408
return np.degrees(beta_c)
409-
410-
411-
def tracker_shaded_fraction(tracker_theta, gcr, projected_solar_zenith,
412-
cross_axis_slope):
413-
"""
414-
Shade fraction (FS) for trackers with a common angle on an east-west slope.
415-
416-
Parameters
417-
----------
418-
tracker_theta : numeric
419-
The tracker rotation angle in degrees from horizontal.
420-
gcr : float
421-
The ground coverage ratio as a fraction equal to the collector width
422-
over the horizontal row-to-row pitch.
423-
projected_solar_zenith : numeric
424-
Zenith angle in degrees of the solar vector projected into the plane
425-
perpendicular to the tracker axes.
426-
cross_axis_slope : float
427-
Angle of the plane containing the tracker axes in degrees from
428-
horizontal.
429-
430-
Returns
431-
-------
432-
shade_fraction : numeric
433-
The fraction of the collector width shaded by an adjacent row. A
434-
value of 1 is completely shaded and zero is no shade.
435-
436-
References
437-
----------
438-
Mark A. Mikofski, "First Solar Irradiance Shade Losses on Sloped Terrain,"
439-
PVPMC, 2023
440-
"""
441-
theta_g_rad = np.radians(cross_axis_slope)
442-
# angle opposite shadow cast on the ground, z
443-
angle_z = (
444-
np.pi / 2 - np.radians(tracker_theta)
445-
+ np.radians(projected_solar_zenith))
446-
# angle opposite the collector width, L
447-
angle_gcr = (
448-
np.pi / 2 - np.radians(projected_solar_zenith)
449-
- theta_g_rad)
450-
# ratio of shadow, z, to pitch, P
451-
zp = gcr * np.sin(angle_z) / np.sin(angle_gcr)
452-
# there's only row-to-row shade loss if the shadow on the ground, z, is
453-
# longer than row-to-row pitch projected on the ground, P*cos(theta_g)
454-
zp_cos_g = zp*np.cos(theta_g_rad)
455-
# shade fraction
456-
fs = 0 if zp_cos_g <= 1 else 1 - 1/zp_cos_g
457-
return fs
458-
459-
460-
def linear_shade_loss(shade_fraction, diffuse_fraction):
461-
"""
462-
Fraction of power lost to linear shade loss applicable to CdTe modules like
463-
First Solar.
464-
465-
Parameters
466-
----------
467-
shade_fraction : numeric
468-
The fraction of the collector width shaded by an adjacent row. A
469-
value of 1 is completely shaded and zero is no shade.
470-
diffuse_fraction : numeric
471-
The ratio of diffuse plane of array (poa) irradiance to global poa.
472-
A value of 1 is completely diffuse and zero is no diffuse.
473-
474-
Returns
475-
-------
476-
linear_shade_loss : numeric
477-
The fraction of power lost due to linear shading. A value of 1 is all
478-
power lost and zero is no loss.
479-
480-
See also
481-
--------
482-
pvlib.tracking.tracker_shaded_fraction
483-
484-
Example
485-
-------
486-
>>> from pvlib import tracking
487-
>>> fs = tracking.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
488-
>>> loss = tracking.linear_shade_loss(fs, 0.2)
489-
>>> P_no_shade = 100 # [kWdc] DC output from modules
490-
>>> P_linear_shade = P_no_shade * (1-loss) # [kWdc] output after loss
491-
# 90.71067811865476 [kWdc]
492-
"""
493-
return shade_fraction * (1 - diffuse_fraction)

0 commit comments

Comments
 (0)