Skip to content

Use Hottel's method for bifacial.utils.*_integ functions #1865

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
33 changes: 23 additions & 10 deletions pvlib/bifacial/infinite_sheds.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt,
def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
solar_azimuth, gcr, height, pitch, ghi, dhi, dni,
albedo, model='isotropic', dni_extra=None, iam=1.0,
npoints=100, vectorize=False):
npoints=None, vectorize=None):
r"""
Calculate plane-of-array (POA) irradiance on one side of a row of modules.

Expand Down Expand Up @@ -250,12 +250,19 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
on the surface that is not reflected away. [unitless]

npoints : int, default 100
Number of discretization points for calculating integrated view
factors.

.. deprecated:: v0.11.2

This parameter has no effect; integrated view factors are now
calculated exactly instead of with discretized approximations.

vectorize : bool, default False
If True, vectorize the view factor calculation across ``surface_tilt``.
This increases speed with the cost of increased memory usage.

.. deprecated:: v0.11.2

This parameter has no effect; calculations are now vectorized
with no memory usage penality.


Returns
-------
Expand Down Expand Up @@ -381,7 +388,7 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni,
albedo, model='isotropic', dni_extra=None, iam_front=1.0,
iam_back=1.0, bifaciality=0.8, shade_factor=-0.02,
transmission_factor=0, npoints=100, vectorize=False):
transmission_factor=0, npoints=None, vectorize=None):
"""
Get front and rear irradiance using the infinite sheds model.

Expand Down Expand Up @@ -472,12 +479,18 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
etc. A negative value is a reduction in back irradiance. [unitless]

npoints : int, default 100
Number of discretization points for calculating integrated view
factors.

.. deprecated:: v0.11.2

This parameter has no effect; integrated view factors are now
calculated exactly instead of with discretized approximations.

vectorize : bool, default False
If True, vectorize the view factor calculation across ``surface_tilt``.
This increases speed with the cost of increased memory usage.

.. deprecated:: v0.11.2

This parameter has no effect; calculations are now vectorized
with no memory usage penality.

Returns
-------
Expand Down
92 changes: 73 additions & 19 deletions pvlib/bifacial/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"""
import numpy as np
from pvlib.tools import sind, cosd, tand
from scipy.integrate import trapezoid
import warnings
from pvlib._deprecation import pvlibDeprecationWarning


def _solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth):
Expand Down Expand Up @@ -173,8 +174,34 @@ def vf_ground_sky_2d(rotation, gcr, x, pitch, height, max_rows=10):
return vf


def _dist(p1, p2):
return ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)**0.5


def _angle(p1, p2):
return np.arctan2(p2[1] - p1[1], p2[0] - p1[0])


def _obstructed_string_length(p1, p2, ob_left, ob_right):
# string length calculations for Hottel's crossed strings method,
# considering view obstructions from the left and right.
# all inputs are (x, y) points.

# unobstructed length
d = _dist(p1, p2)
# obstructed on the left
d = np.where(_angle(p1, p2) > _angle(p1, ob_left),
_dist(p1, ob_left) + _dist(ob_left, p2),
d)
# obstructed on the right
d = np.where(_angle(p1, p2) < _angle(p1, ob_right),
_dist(p1, ob_right) + _dist(ob_right, p2),
d)
return d


def vf_ground_sky_2d_integ(surface_tilt, gcr, height, pitch, max_rows=10,
npoints=100, vectorize=False):
npoints=None, vectorize=None):
"""
Integrated view factor to the sky from the ground underneath
interior rows of the array.
Expand Down Expand Up @@ -205,23 +232,50 @@ def vf_ground_sky_2d_integ(surface_tilt, gcr, height, pitch, max_rows=10,
Integration of view factor over the length between adjacent, interior
rows. Shape matches that of ``surface_tilt``. [unitless]
"""
# Abuse vf_ground_sky_2d by supplying surface_tilt in place
# of a signed rotation. This is OK because
# 1) z span the full distance between 2 rows, and
# 2) max_rows is set to be large upstream, and
# 3) _vf_ground_sky_2d considers [-max_rows, +max_rows]
# The VFs to the sky will thus be symmetric around z=0.5
z = np.linspace(0, 1, npoints)
rotation = np.atleast_1d(surface_tilt)
if vectorize:
fz_sky = vf_ground_sky_2d(rotation, gcr, z, pitch, height, max_rows)
else:
fz_sky = np.zeros((npoints, len(rotation)))
for k, r in enumerate(rotation):
vf = vf_ground_sky_2d(r, gcr, z, pitch, height, max_rows)
fz_sky[:, k] = vf[:, 0] # remove spurious rotation dimension
# calculate the integrated view factor for all of the ground between rows
return trapezoid(fz_sky, z, axis=0)
if npoints is not None or vectorize is not None:
msg = (
"The `npoints` and `vectorize` parameters have no effect and will "
"be removed in a future version." # TODO make this better
)
warnings.warn(msg, pvlibDeprecationWarning)

input_is_scalar = np.isscalar(surface_tilt)

collector_width = pitch * gcr
surface_tilt = np.atleast_2d(np.abs(surface_tilt))

# TODO seems like this should be np.arange(-max_rows, max_rows+1)?
# see GH #1867
k = np.arange(-max_rows, max_rows)[:, np.newaxis]

# primary crossed string points:
# a, b: boundaries of ground segment
# c, d: upper module edges
a = (0, 0)
b = (pitch, 0)
c = ((k+1)*pitch - 0.5 * collector_width * cosd(surface_tilt),
height + 0.5 * collector_width * sind(surface_tilt))
d = (c[0] - pitch, c[1])

# view obstruction points (lower module edges)
obs_left = (d[0] + collector_width * cosd(surface_tilt),
d[1] - collector_width * sind(surface_tilt))
obs_right = (obs_left[0] + pitch, obs_left[1])

# hottel string lengths, considering obstructions
ac = _obstructed_string_length(a, c, obs_left, obs_right)
ad = _obstructed_string_length(a, d, obs_left, obs_right)
bc = _obstructed_string_length(b, c, obs_left, obs_right)
bd = _obstructed_string_length(b, d, obs_left, obs_right)

# crossed string formula for VF
vf_per_slat = np.maximum(0.5 * (1/pitch) * ((ac + bd) - (bc + ad)), 0)
vf_total = np.sum(vf_per_slat, axis=0)

if input_is_scalar:
vf_total = vf_total.item()

return vf_total


def _vf_poly(surface_tilt, gcr, x, delta):
Expand Down
54 changes: 36 additions & 18 deletions pvlib/tests/bifacial/test_infinite_sheds.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from pvlib.bifacial import infinite_sheds
from ..conftest import assert_series_equal

from pvlib._deprecation import pvlibDeprecationWarning

import pytest


Expand Down Expand Up @@ -92,11 +94,10 @@ def test_get_irradiance_poa():
dni = 700
albedo = 0
iam = 1.0
npoints = 100
res = infinite_sheds.get_irradiance_poa(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni,
albedo, iam=iam, npoints=npoints)
albedo, iam=iam)
expected_diffuse = np.array([300.])
expected_direct = np.array([700.])
expected_global = expected_diffuse + expected_direct
Expand All @@ -122,7 +123,7 @@ def test_get_irradiance_poa():
res = infinite_sheds.get_irradiance_poa(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni,
albedo, iam=iam, npoints=npoints)
albedo, iam=iam)
assert np.allclose(res['poa_global'], expected_global)
assert np.allclose(res['poa_diffuse'], expected_diffuse)
assert np.allclose(res['poa_direct'], expected_direct)
Expand All @@ -144,7 +145,7 @@ def test_get_irradiance_poa():
res = infinite_sheds.get_irradiance_poa(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni,
albedo, iam=iam, npoints=npoints)
albedo, iam=iam)
assert isinstance(res, pd.DataFrame)
assert_series_equal(res['poa_global'], expected_global)
assert_series_equal(res['shaded_fraction'], expected_shaded_fraction)
Expand All @@ -161,8 +162,7 @@ def test__backside_tilt():
assert np.allclose(back_az, np.array([0., 330., 90., 180.]))


@pytest.mark.parametrize("vectorize", [True, False])
def test_get_irradiance(vectorize):
def test_get_irradiance():
# singleton inputs
solar_zenith = 0.
solar_azimuth = 180.
Expand All @@ -177,12 +177,10 @@ def test_get_irradiance(vectorize):
albedo = 0.
iam_front = 1.0
iam_back = 1.0
npoints = 100
result = infinite_sheds.get_irradiance(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back,
bifaciality=0.8, shade_factor=-0.02, transmission_factor=0,
npoints=npoints, vectorize=vectorize)
bifaciality=0.8, shade_factor=-0.02, transmission_factor=0)
expected_front_diffuse = np.array([300.])
expected_front_direct = np.array([700.])
expected_front_global = expected_front_diffuse + expected_front_direct
Expand All @@ -205,12 +203,11 @@ def test_get_irradiance(vectorize):
result = infinite_sheds.get_irradiance(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back,
bifaciality=0.8, shade_factor=-0.02, transmission_factor=0,
npoints=npoints, vectorize=vectorize)
bifaciality=0.8, shade_factor=-0.02, transmission_factor=0)
result_front = infinite_sheds.get_irradiance_poa(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni,
albedo, iam=iam_front, vectorize=vectorize)
albedo, iam=iam_front)
assert isinstance(result, pd.DataFrame)
expected_poa_global = pd.Series(
[1000., 500., result_front['poa_global'][2] * (1 + 0.8 * 0.98),
Expand All @@ -223,6 +220,30 @@ def test_get_irradiance(vectorize):
expected_shaded_fraction)


def test_get_irradiance_deprecated():
kwargs = {"surface_tilt": 0, "surface_azimuth": 0, "solar_zenith": 0,
"solar_azimuth": 0, "gcr": 0.5, "height": 1, "pitch": 1,
"ghi": 1000, "dhi": 200, "dni": 800, "albedo": 0.2}

with pytest.warns(pvlibDeprecationWarning, match="have no effect"):
_ = infinite_sheds.get_irradiance(**kwargs, npoints=10)

with pytest.warns(pvlibDeprecationWarning, match="have no effect"):
_ = infinite_sheds.get_irradiance(**kwargs, vectorize=True)


def test_get_irradiance_poa_deprecated():
kwargs = {"surface_tilt": 0, "surface_azimuth": 0, "solar_zenith": 0,
"solar_azimuth": 0, "gcr": 0.5, "height": 1, "pitch": 1,
"ghi": 1000, "dhi": 200, "dni": 800, "albedo": 0.2}

with pytest.warns(pvlibDeprecationWarning, match="have no effect"):
_ = infinite_sheds.get_irradiance_poa(**kwargs, npoints=10)

with pytest.warns(pvlibDeprecationWarning, match="have no effect"):
_ = infinite_sheds.get_irradiance_poa(**kwargs, vectorize=True)


def test_get_irradiance_limiting_gcr():
# test confirms that irradiance on widely spaced rows is approximately
# the same as for a single row array
Expand All @@ -239,12 +260,10 @@ def test_get_irradiance_limiting_gcr():
albedo = 1.
iam_front = 1.0
iam_back = 1.0
npoints = 100
result = infinite_sheds.get_irradiance(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back,
bifaciality=1., shade_factor=-0.00, transmission_factor=0.,
npoints=npoints)
bifaciality=1., shade_factor=-0.00, transmission_factor=0.)
expected_ground_diffuse = np.array([500.])
expected_sky_diffuse = np.array([150.])
expected_direct = np.array([0.])
Expand Down Expand Up @@ -289,12 +308,11 @@ def test_get_irradiance_with_haydavies():
model = 'haydavies'
iam_front = 1.0
iam_back = 1.0
npoints = 100
result = infinite_sheds.get_irradiance(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni, albedo, model, dni_extra,
iam_front, iam_back, bifaciality=0.8, shade_factor=-0.02,
transmission_factor=0, npoints=npoints)
transmission_factor=0)
expected_front_diffuse = np.array([151.38])
expected_front_direct = np.array([848.62])
expected_front_global = expected_front_diffuse + expected_front_direct
Expand All @@ -314,4 +332,4 @@ def test_get_irradiance_with_haydavies():
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
gcr, height, pitch, ghi, dhi, dni, albedo, model, None,
iam_front, iam_back, bifaciality=0.8, shade_factor=-0.02,
transmission_factor=0, npoints=npoints)
transmission_factor=0)
30 changes: 19 additions & 11 deletions pvlib/tests/bifacial/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pvlib.tools import cosd
from scipy.integrate import trapezoid

from pvlib._deprecation import pvlibDeprecationWarning


@pytest.fixture
def test_system_fixed_tilt():
Expand Down Expand Up @@ -91,17 +93,23 @@ def test__vf_ground_sky_2d(test_system_fixed_tilt):
assert np.isclose(vf, vfs_gnd_sky[0])


@pytest.mark.parametrize("vectorize", [True, False])
def test_vf_ground_sky_2d_integ(test_system_fixed_tilt, vectorize):
ts, pts, vfs_gnd_sky = test_system_fixed_tilt
# pass rotation here since max_rows=1 for the hand-solved case in
# the fixture test_system, which means the ground-to-sky view factor
# isn't summed over enough rows for symmetry to hold.
vf_integ = utils.vf_ground_sky_2d_integ(
ts['rotation'], ts['gcr'], ts['height'], ts['pitch'],
max_rows=1, npoints=3, vectorize=vectorize)
expected_vf_integ = trapezoid(vfs_gnd_sky, pts, axis=0)
assert np.isclose(vf_integ, expected_vf_integ, rtol=0.1)
def test_vf_ground_sky_2d_integ():
# test against numerical integration with vf_ground_sky_2d
x = np.linspace(0, 1, num=1000)
kwargs = dict(gcr=0.4, pitch=5.0, height=1.5)
vf_x = utils.vf_ground_sky_2d(rotation=-60, x=x, **kwargs)
vf_expected = trapezoid(vf_x, x, axis=0)

vf_actual = utils.vf_ground_sky_2d_integ(surface_tilt=60, **kwargs)
assert np.isclose(vf_expected, vf_actual)


def test_vf_ground_sky_2d_integ_deprecated():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_vf_ground_sky_2d_integ_deprecated():
@fail_on_pvlib_version(...)
def test_vf_ground_sky_2d_integ_deprecated():

Let's make sure docs and code gets cleaned in the future? v0.12 or v0.13, whatever feels right. I would also add the future version to deprecate in the docs admonition.

with pytest.warns(pvlibDeprecationWarning, match="have no effect"):
_ = utils.vf_ground_sky_2d_integ(0, 0.5, 1, 1, npoints=10)

with pytest.warns(pvlibDeprecationWarning, match="have no effect"):
_ = utils.vf_ground_sky_2d_integ(0, 0.5, 1, 1, vectorize=True)


def test_vf_row_sky_2d(test_system_fixed_tilt):
Expand Down