Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/sphinx/source/reference/tracking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ Functions
tracking.calc_axis_tilt
tracking.calc_cross_axis_tilt
tracking.calc_surface_orientation
tracking.tracker_shaded_fraction
tracking.linear_shade_loss
9 changes: 7 additions & 2 deletions docs/sphinx/source/whatsnew/v0.9.6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ Deprecations

Enhancements
~~~~~~~~~~~~

* added functions `pvlib.tracking.tracker_shaded_fraction` and
`pvlib.tracking.linear_shade_loss` to calculate row-to-row shade and apply
linear shade loss for thin film CdTe modules like First Solar.
(:issue:`1689`, :issue:`1690`, :pull:`1725`)

Bug fixes
~~~~~~~~~
* `data` can no longer be left unspecified in
:py:meth:`pvlib.modelchain.ModelChain.run_model_from_effective_irradiance`. (:issue:`1713`, :pull:`1720`)
:py:meth:`pvlib.modelchain.ModelChain.run_model_from_effective_irradiance`.
(:issue:`1713`, :pull:`1720`)

Testing
~~~~~~~
Expand All @@ -39,3 +43,4 @@ Contributors
~~~~~~~~~~~~
* Adam R. Jensen (:ghuser:`adamrjensen`)
* Siddharth Kaul (:ghuser:`k10blogger`)
* Mark A. Mikofski (:ghuser:`mikofski`)
25 changes: 25 additions & 0 deletions pvlib/tests/test_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,28 @@ def test_calc_surface_orientation_special():
# in a modulo-360 sense.
np.testing.assert_allclose(np.round(out['surface_azimuth'], 4) % 360,
expected_azimuths, rtol=1e-5, atol=1e-5)


@pytest.fixture
def expected_fs():
# trivial case, 80% gcr, no slope, trackers & psz at 45-deg
z = np.sqrt(2*0.8*0.8)
return 1 - 1/z


def test_tracker_shade_fraction(expected_fs):
"""closes gh1690"""
fs = tracking.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
assert np.isclose(fs, expected_fs)
# same trivial case with 40%, shadow is only 0.565-m long < 1-m r2r P
zero_fs = tracking.tracker_shaded_fraction(45.0, 0.4, 45.0, 0)
assert np.isclose(zero_fs, 0)


def test_linear_shade_loss(expected_fs):
loss = tracking.linear_shade_loss(expected_fs, 0.2)
assert np.isclose(loss, 0.09289321881345258)
loss_no_df = tracking.linear_shade_loss(expected_fs, 0)
assert np.isclose(loss_no_df, expected_fs)
no_loss = tracking.linear_shade_loss(expected_fs, 1.0)
assert np.isclose(no_loss, 0)
85 changes: 85 additions & 0 deletions pvlib/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,3 +686,88 @@ def calc_cross_axis_tilt(
# equation 26
beta_c = _calc_beta_c(v, delta_gamma, axis_tilt)
return np.degrees(beta_c)


def tracker_shaded_fraction(tracker_theta, gcr, projected_solar_zenith,
cross_axis_slope):
"""
Shade fraction (FS) for trackers with a common angle on an east-west slope.

Parameters
----------
tracker_theta : numeric
The tracker rotation angle in degrees from horizontal.
gcr : float
The ground coverage ratio as a fraction equal to the collector width
over the horizontal row-to-row pitch.
projected_solar_zenith : numeric
Zenith angle in degrees of the solar vector projected into the plane
perpendicular to the tracker axes.
cross_axis_slope : float
Angle of the plane containing the tracker axes in degrees from
horizontal.

Returns
-------
shade_fraction : numeric
The fraction of the collector width shaded by an adjacent row. A
value of 1 is completely shaded and zero is no shade.

References
----------
Mark A. Mikofski, "First Solar Irradiance Shade Losses on Sloped Terrain,"
PVPMC, 2023
"""
theta_g_rad = np.radians(cross_axis_slope)
# angle opposite shadow cast on the ground, z
angle_z = (
np.pi / 2 - np.radians(tracker_theta)
+ np.radians(projected_solar_zenith))
# angle opposite the collector width, L
angle_gcr = (
np.pi / 2 - np.radians(projected_solar_zenith)
- theta_g_rad)
# ratio of shadow, z, to pitch, P
zp = gcr * np.sin(angle_z) / np.sin(angle_gcr)
# there's only row-to-row shade loss if the shadow on the ground, z, is
# longer than row-to-row pitch projected on the ground, P/cos(theta_g)
zp_cos_g = zp*np.cos(theta_g_rad)
# shade fraction
fs = 0 if zp_cos_g <= 1 else 1 - 1/zp_cos_g
return fs


def linear_shade_loss(shade_fraction, diffuse_fraction):
"""
Fraction of power lost to linear shade loss applicable to CdTe modules like
First Solar.

Parameters
----------
shade_fraction : numeric
The fraction of the collector width shaded by an adjacent row. A
value of 1 is completely shaded and zero is no shade.
diffuse_fraction : numeric
The ratio of diffuse plane of array (poa) irradiance to global poa.
A value of 1 is completely diffuse and zero is no diffuse.

Returns
-------
linear_shade_loss : numeric
The fraction of power lost due to linear shading. A value of 1 is all
power lost and zero is no loss.

See also
--------
pvlib.tracking.tracker_shaded_fraction

Example
-------
>>> from pvlib import tracking
>>> fs = tracking.tracker_shaded_fraction(45.0, 0.8, 45.0, 0)
>>> loss = tracking.linear_shade_loss(fs, 0.2)
>>> P_no_shade = 100 # [kWdc] DC output from modules
>>> P_linear_shade = P_no_shade * (1-loss) # [kWdc] output after loss
# 90.71067811865476 [kWdc]
"""
return shade_fraction * (1 - diffuse_fraction)