Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,17 @@
pitch=pitch,
)

unshaded_ground_fraction = pvlib.bifacial.utils._unshaded_ground_fraction(
surface_tilt=tracking_orientations['surface_tilt'],
surface_azimuth=tracking_orientations['surface_azimuth'],
projected_zenith_angle_tan = pvlib.bifacial.utils._solar_projection_tangent(
solar_zenith=solpos['apparent_zenith'],
solar_azimuth=solpos['azimuth'],
surface_azimuth=tracking_orientations['surface_azimuth'],
)

unshaded_ground_fraction = pvlib.bifacial.utils._unshaded_ground_fraction(
surface_tilt=tracking_orientations['surface_tilt'],
tan_phi=projected_zenith_angle_tan,
gcr=gcr,
solar_zenith=solpos['apparent_zenith'],
)

crop_avg_irradiance = (unshaded_ground_fraction * clearsky['dni']
Expand Down
2 changes: 2 additions & 0 deletions docs/sphinx/source/whatsnew/v0.13.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Bug fixes

Enhancements
~~~~~~~~~~~~
* Eliminate some repeated calculations in :py:mod:`pvlib.bifacial.infinite_sheds`. (:pull:`2897`)


Documentation
Expand All @@ -42,4 +43,5 @@ Maintenance

Contributors
~~~~~~~~~~~~
* Kevin Anderson (:ghuser:`kandersolar`)

54 changes: 23 additions & 31 deletions pvlib/bifacial/infinite_sheds.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pandas as pd
from pvlib.tools import cosd, sind, tand
from pvlib.bifacial import utils
from pvlib.irradiance import beam_component, aoi, haydavies
from pvlib.irradiance import aoi, haydavies, poa_components


def _poa_ground_shadows(ghi, dhi, albedo, f_gnd_beam, vf_gnd_sky):
Expand Down Expand Up @@ -121,8 +121,7 @@ def _poa_ground_pv(poa_ground, gcr, surface_tilt):
return poa_ground * vf_integ


def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,
surface_azimuth, gcr):
def _shaded_fraction(surface_tilt, tan_phi, aoi, gcr):
"""
Calculate fraction (from the bottom) of row slant height that is shaded
from direct irradiance by the row in front toward the sun.
Expand All @@ -138,16 +137,15 @@ def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,

Parameters
----------
solar_zenith : numeric
Apparent (refraction-corrected) solar zenith. [degrees]
solar_azimuth : numeric
Solar azimuth. [degrees]
surface_tilt : numeric
Row tilt from horizontal, e.g. surface facing up = 0, surface facing
horizon = 90. [degrees]
surface_azimuth : numeric
Azimuth angle of the row surface. North=0, East=90, South=180,
West=270. [degrees]
tan_phi : numeric
Tangent of the angle between vertical and the projection of the
sun direction onto the YZ plane. [unitless]
aoi : numeric
Angle of incidence of direct irradiance on the module surface.
[degrees]
gcr : numeric
Ground coverage ratio, which is the ratio of row slant length to row
spacing (pitch). [unitless]
Expand All @@ -168,14 +166,11 @@ def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,
Single-Axis Trackers", Technical Report NREL/TP-5K00-76626, July 2020.
https://www.nrel.gov/docs/fy20osti/76626.pdf
"""
tan_phi = utils._solar_projection_tangent(
solar_zenith, solar_azimuth, surface_azimuth)
# length of shadow behind a row as a fraction of pitch
x = gcr * (sind(surface_tilt) * tan_phi + cosd(surface_tilt))
f_x = 1 - 1. / x
# set f_x to be 1 when sun is behind the array
ao = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth)
f_x = np.where(ao < 90, f_x, 1.)
f_x = np.where(aoi < 90, f_x, 1.)
# when x < 1, the shadow is not long enough to fall on the row surface
f_x = np.where(x > 1., f_x, 0.)
return f_x
Expand Down Expand Up @@ -315,14 +310,19 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
dni = dni + circumsolar_normal

# Calculate some geometric quantities
angle_of_incidence = aoi(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth)
# tangent of the projected solar zenith angle
tan_phi = utils._solar_projection_tangent(
solar_zenith, solar_azimuth, surface_azimuth)
# rows to consider in front and behind current row
# ensures that view factors to the sky are computed to within 5 degrees
# of the horizon
max_rows = np.ceil(height / (pitch * tand(5)))
# fraction of ground between rows that is illuminated accounting for
# shade from panels. [1], Eq. 4
f_gnd_beam = utils._unshaded_ground_fraction(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr)
f_gnd_beam = utils._unshaded_ground_fraction(surface_tilt, tan_phi, gcr,
solar_zenith)
# integrated view factor from the ground to the sky, integrated between
# adjacent rows interior to the array
# method differs from [1], Eq. 7 and Eq. 8; height is defined at row
Expand All @@ -331,8 +331,7 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
surface_tilt, gcr, height, pitch, max_rows, npoints,
vectorize)
# fraction of row slant height that is shaded from direct irradiance
f_x = _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,
surface_azimuth, gcr)
f_x = _shaded_fraction(surface_tilt, tan_phi, angle_of_incidence, gcr)

# Total sky diffuse received by both shaded and unshaded portions
poa_sky_pv = _poa_sky_diffuse_pv(dhi, gcr, surface_tilt)
Expand All @@ -355,19 +354,12 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
# this quantity by a ratio of view factors.
poa_gnd_pv = _poa_ground_pv(ground_diffuse, gcr, surface_tilt)

# add sky and ground-reflected irradiance on the row by irradiance
# component
poa_diffuse = poa_gnd_pv + poa_sky_pv
# beam on plane, make an array for consistency with poa_diffuse
poa_beam = np.atleast_1d(beam_component(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni))
poa_direct = poa_beam * (1 - f_x) * iam # direct only on the unshaded part
poa_global = poa_direct + poa_diffuse

output = {
'poa_global': poa_global, 'poa_direct': poa_direct,
'poa_diffuse': poa_diffuse, 'poa_ground_diffuse': poa_gnd_pv,
'poa_sky_diffuse': poa_sky_pv, 'shaded_fraction': f_x}
output = poa_components(angle_of_incidence,
dni * (1 - f_x) * iam,
poa_sky_pv,
poa_gnd_pv)
output['shaded_fraction'] = f_x

if isinstance(ghi, pd.Series):
output = pd.DataFrame(output)
return output
Expand Down
18 changes: 7 additions & 11 deletions pvlib/bifacial/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def _solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth):
return tan_phi


def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith,
solar_azimuth, gcr, max_zenith=87):
def _unshaded_ground_fraction(surface_tilt, tan_phi, gcr,
solar_zenith, max_zenith=87):
r"""
Calculate the fraction of the ground with incident direct irradiance.

Expand All @@ -56,16 +56,14 @@ def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith,
Surface tilt angle. The tilt angle is defined as
degrees from horizontal, e.g., surface facing up = 0, surface facing
horizon = 90. [degree]
surface_azimuth : numeric
Azimuth of the module surface, i.e., North=0, East=90, South=180,
West=270. [degree]
solar_zenith : numeric
Solar zenith angle. [degree].
solar_azimuth : numeric
Solar azimuth. [degree].
tan_phi : numeric
Tangent of the angle between vertical and the projection of the
sun direction onto the YZ plane. [unitless]
gcr : float
Ground coverage ratio, which is the ratio of row slant length to row
spacing (pitch). [unitless]
solar_zenith : numeric
Solar zenith angle. [degree].
max_zenith : numeric, default 87
Maximum zenith angle. For solar_zenith > max_zenith, unshaded ground
fraction is set to 0. [degree]
Expand All @@ -83,8 +81,6 @@ def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith,
Photovoltaic Specialists Conference (PVSC), 2019, pp. 1282-1287.
:doi:`10.1109/PVSC40753.2019.8980572`.
"""
tan_phi = _solar_projection_tangent(solar_zenith, solar_azimuth,
surface_azimuth)
f_gnd_beam = 1.0 - np.minimum(
1.0, gcr * np.abs(cosd(surface_tilt) + sind(surface_tilt) * tan_phi))
# [1], Eq. 4
Expand Down
15 changes: 8 additions & 7 deletions tests/bifacial/test_infinite_sheds.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,21 @@ def test__poa_ground_shadows():


def test__shaded_fraction_floats():
# inputs correspond to solar_zenith=60, surface_azimuth=solar_azimuth=180
result = infinite_sheds._shaded_fraction(
solar_zenith=60., solar_azimuth=180., surface_tilt=60.,
surface_azimuth=180., gcr=1.0)
surface_tilt=60., tan_phi=np.sqrt(3), aoi=0, gcr=1.0)
assert np.isclose(result, 0.5)


def test__shaded_fraction_array():
solar_zenith = np.array([0., 60., 90., 60.])
solar_azimuth = np.array([180., 180., 180., 180.])
surface_azimuth = np.array([180., 180., 180., 210.])
# solar_zenith = np.array([0., 60., 90., 60.])
# solar_azimuth = np.array([180., 180., 180., 180.])
# surface_azimuth = np.array([180., 180., 180., 210.])
surface_tilt = np.array([30., 60., 0., 30.])
aoi = np.array([30, 0, 90, 36.09778103])
tan_phi = np.array([0, np.sqrt(3), 1e10, 1.5])
gcr = 1.0
result = infinite_sheds._shaded_fraction(
solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, gcr)
result = infinite_sheds._shaded_fraction(surface_tilt, tan_phi, aoi, gcr)
x = 0.75 + np.sqrt(3) / 2
expected = np.array([0.0, 0.5, 0., (x - 1) / x])
assert np.allclose(result, expected)
Expand Down
32 changes: 15 additions & 17 deletions tests/bifacial/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,27 @@ def test__solar_projection_tangent():


@pytest.mark.parametrize(
"gcr,surface_tilt,surface_azimuth,solar_zenith,solar_azimuth,expected",
[(0.5, 0., 180., 0., 180., 0.5),
(1.0, 0., 180., 0., 180., 0.0),
(1.0, 90., 180., 0., 180., 1.0),
(0.5, 45., 180., 45., 270., 1.0 - np.sqrt(2) / 4),
(0.5, 45., 180., 90., 180., 0.),
(np.sqrt(2) / 2, 45, 180, 0, 180, 0.5),
(np.sqrt(2) / 2, 45, 180, 45, 180, 0.0),
(np.sqrt(2) / 2, 45, 180, 45, 90, 0.5),
(np.sqrt(2) / 2, 45, 180, 45, 0, 1.0),
(np.sqrt(2) / 2, 45, 180, 45, 135, 0.5 * (1 - np.sqrt(2) / 2)),
"gcr,surface_tilt,tan_phi,solar_zenith,expected",
[(0.5, 0., 0., 0., 0.5),
(1.0, 0., 0., 0., 0.0),
(1.0, 90., 0., 0., 1.0),
(0.5, 45., 0., 45., 1.0 - np.sqrt(2) / 4),
(0.5, 45., 1e10, 90., 0.),
(np.sqrt(2) / 2, 45, 0, 0, 0.5),
(np.sqrt(2) / 2, 45, 1, 45, 0.0),
(np.sqrt(2) / 2, 45, 0, 45, 0.5),
(np.sqrt(2) / 2, 45, -1, 45, 1.0),
(np.sqrt(2) / 2, 45, np.sqrt(2) / 2, 45, 0.5 * (1 - np.sqrt(2) / 2)),
])
def test__unshaded_ground_fraction(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr,
expected):
surface_tilt, tan_phi, solar_zenith, gcr, expected):
# frontside, same for both sides
f_sky_beam_f = utils._unshaded_ground_fraction(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr)
surface_tilt, tan_phi, gcr, solar_zenith)
assert np.allclose(f_sky_beam_f, expected)
# backside, should be the same as frontside
f_sky_beam_b = utils._unshaded_ground_fraction(
180. - surface_tilt, surface_azimuth - 180., solar_zenith,
solar_azimuth, gcr)
180. - surface_tilt, -tan_phi, gcr, solar_zenith)
assert np.allclose(f_sky_beam_b, expected)


Expand Down Expand Up @@ -161,7 +159,7 @@ def test_vf_row_ground_2d(test_system_fixed_tilt):
assert np.allclose(vf, expected)


def test_vf_ground_2d_integ(test_system_fixed_tilt):
def test_vf_row_ground_2d_integ(test_system_fixed_tilt):
ts, _, _ = test_system_fixed_tilt
# with float input, check end position
with np.errstate(invalid='ignore'):
Expand Down