Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linear shade gh1690 #1725

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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
mikofski marked this conversation as resolved.
Show resolved Hide resolved
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):
mikofski marked this conversation as resolved.
Show resolved Hide resolved
"""
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
mikofski marked this conversation as resolved.
Show resolved Hide resolved
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.
mikofski marked this conversation as resolved.
Show resolved Hide resolved

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)