diff --git a/docs/examples/agrivoltaics/plot_agrivoltaics_ground_irradiance.py b/docs/examples/agrivoltaics/plot_agrivoltaics_ground_irradiance.py index 521c68cb55..b858032d65 100644 --- a/docs/examples/agrivoltaics/plot_agrivoltaics_ground_irradiance.py +++ b/docs/examples/agrivoltaics/plot_agrivoltaics_ground_irradiance.py @@ -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'] diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index e12f7277e5..3b50ad3763 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -27,7 +27,7 @@ Enhancements :py:func:`~pvlib.singlediode.bishop88_mpp`, :py:func:`~pvlib.singlediode.bishop88_v_from_i`, and :py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`) - +* Eliminate some repeated calculations in :py:mod:`pvlib.bifacial.infinite_sheds`. (:pull:`2897`) Documentation @@ -53,4 +53,5 @@ Maintenance Contributors ~~~~~~~~~~~~ +* Kevin Anderson (:ghuser:`kandersolar`) diff --git a/pvlib/bifacial/infinite_sheds.py b/pvlib/bifacial/infinite_sheds.py index 2418fd71bb..db3a52835f 100644 --- a/pvlib/bifacial/infinite_sheds.py +++ b/pvlib/bifacial/infinite_sheds.py @@ -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): @@ -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. @@ -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] @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/pvlib/bifacial/utils.py b/pvlib/bifacial/utils.py index a69a3e18d4..3fd2e5d598 100644 --- a/pvlib/bifacial/utils.py +++ b/pvlib/bifacial/utils.py @@ -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. @@ -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] @@ -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 diff --git a/tests/bifacial/test_infinite_sheds.py b/tests/bifacial/test_infinite_sheds.py index 5459b9d575..33e24a11ee 100644 --- a/tests/bifacial/test_infinite_sheds.py +++ b/tests/bifacial/test_infinite_sheds.py @@ -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) diff --git a/tests/bifacial/test_utils.py b/tests/bifacial/test_utils.py index e24af42b3e..623156368e 100644 --- a/tests/bifacial/test_utils.py +++ b/tests/bifacial/test_utils.py @@ -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) @@ -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'):