Skip to content

Commit 4bca09d

Browse files
committed
Merge remote-tracking branch 'upstream/main' into ants2d
2 parents ac2424b + 5f70c5a commit 4bca09d

7 files changed

Lines changed: 93 additions & 8 deletions

File tree

docs/sphinx/source/whatsnew/v0.15.2.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ Enhancements
4242
* Accelerate :py:func:`~pvlib.bifacial.utils.vf_ground_sky_2d_integ` by one or
4343
two orders of magnitude. This also makes :py:mod:`pvlib.bifacial.infinite_sheds` faster.
4444
(:pull:`2740`)
45+
* Fixes a regression in :py:func:`pvlib.tracking.calc_surface_orientation`
46+
introduced in v0.15.1 (:pull:`2702`) that caused a broadcasting
47+
``ValueError`` when ``tracker_theta`` was a 2-D (or higher rank) array.
48+
(:issue:`2747`, :pull:`2749`)
49+
50+
Enhancements
51+
~~~~~~~~~~~~
52+
* Add the ``front_side_fraction`` parameter to
53+
:py:func:`pvlib.snow.loss_townsend` to support Townsend snow-loss
54+
workflows for bifacial systems. (:issue:`2755`, :pull:`2756`)
55+
56+
* Added mapping of the parameter ``"albedo"`` in
57+
:py:func:`~pvlib.iotools.get_nasa_power` when ``map_variables=True``
58+
(:pull:`2753`)
4559

4660

4761
Documentation
@@ -74,3 +88,6 @@ Contributors
7488
~~~~~~~~~~~~
7589
* :ghuser:`Omesh37`
7690
* Cliff Hansen (:ghuser:`cwhanse`)
91+
* :ghuser:`shethkajal7`
92+
* Arthur Onno (:ghuser:`ArthurOnnoTerabase`)
93+
* Adam R. Jensen (:ghuser:`AdamRJensen`)

pvlib/iotools/nasa_power.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'T2M': 'temp_air',
1919
'WS2M': 'wind_speed_2m',
2020
'WS10M': 'wind_speed',
21+
'ALLSKY_SRF_ALB': 'albedo',
2122
}
2223

2324

pvlib/snow.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ def _townsend_effective_snow(snow_total, snow_events):
248248

249249
def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
250250
temp_air, poa_global, slant_height, lower_edge_height,
251-
string_factor=1.0, angle_of_repose=40):
251+
string_factor=1.0, angle_of_repose=40,
252+
front_side_fraction=1.0):
252253
'''
253254
Calculates monthly snow loss based on the Townsend monthly snow loss
254255
model.
@@ -293,6 +294,12 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
293294
Piled snow angle, assumed to stabilize at 40°, the midpoint of
294295
25°-55° avalanching slope angles. [deg]
295296
297+
front_side_fraction : numeric or array-like, default 1.0
298+
Fraction of monthly energy from front-side insolation. [unitless]
299+
Multiplies the calculated loss fraction. For example,
300+
use 0.9 when 90% of monthly energy is from the front side
301+
of a bifacial system and 10% is from the rear side.
302+
296303
Returns
297304
-------
298305
loss : array-like
@@ -310,6 +317,10 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
310317
publication of [1]_, as described in [2]_.
311318
The definition for snow events documented above is based on [3]_.
312319
320+
For bifacial systems, [2]_ recommends including both front-side and
321+
rear-side insolation in ``poa_global``. The resulting loss is
322+
scaled by the front-side energy fraction ``front_side_fraction``.
323+
313324
References
314325
----------
315326
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
@@ -384,4 +395,6 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
384395
* string_factor
385396
)
386397

398+
loss_fraction = loss_fraction * front_side_fraction
399+
387400
return np.clip(loss_fraction, 0, 1)

pvlib/tracking.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,11 @@ def _unit_normal(axis_azimuth, axis_tilt, theta):
233233
Returns
234234
-------
235235
ndarray
236-
Shape (N,3) where theta has length N
236+
Shape ``theta.shape + (3,)``, with a minimum rank of 2. For 1-D
237+
``theta`` of length N this is ``(N, 3)``.
237238
"""
238239

239-
theta = np.asarray(theta)
240+
theta = np.atleast_1d(np.asarray(theta))
240241

241242
cA, sA = cosd(-axis_azimuth), sind(-axis_azimuth)
242243
cT, sT = cosd(-axis_tilt), sind(-axis_tilt)
@@ -248,7 +249,7 @@ def _unit_normal(axis_azimuth, axis_tilt, theta):
248249
y = sA * sTh - cA * sT * cTh
249250
z = cT * cTh
250251

251-
result = np.column_stack((x, y, z))
252+
result = np.stack((x, y, z), axis=-1)
252253

253254
return result
254255

@@ -296,7 +297,7 @@ def calc_surface_orientation(tracker_theta, axis_tilt=0, axis_azimuth=0):
296297

297298
# project unit_normal to x-y plane to calculate azimuth
298299
surface_azimuth = np.degrees(
299-
np.arctan2(unit_normal[:, 0], unit_normal[:, 1]))
300+
np.arctan2(unit_normal[..., 0], unit_normal[..., 1]))
300301

301302
surface_azimuth = np.where(surface_tilt == 0., axis_azimuth - 90.,
302303
surface_azimuth)

tests/iotools/test_meteonorm.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ def expected_meta():
3434
'description': 'Global horizontal irradiance',
3535
'name': 'global_horizontal_irradiance',
3636
'unit': {
37-
'description': 'Watt per square meter', 'name': 'W/m**2'}},
37+
'description': 'watt per square meter', 'name': 'W/m**2'}},
3838
{'aggregation_method': 'average',
3939
'description': 'Global horizontal irradiance with shading taken into account', # noqa: E501
4040
'name': 'global_horizontal_irradiance_with_shading',
41-
'unit': {'description': 'Watt per square meter',
41+
'unit': {'description': 'watt per square meter',
4242
'name': 'W/m**2'}},
4343
],
4444
'surface_azimuth': 180,
@@ -245,7 +245,7 @@ def expected_meteonorm_tmy_meta():
245245
'aggregation_method': 'average',
246246
'description': 'Diffuse horizontal irradiance',
247247
'name': 'diffuse_horizontal_irradiance',
248-
'unit': {'description': 'Watt per square meter',
248+
'unit': {'description': 'watt per square meter',
249249
'name': 'W/m**2'},
250250
}],
251251
'surface_azimuth': 90,

tests/test_snow.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,28 @@ def test_loss_townsend_cases(poa_global, surface_tilt, slant_height,
247247
poa_global, slant_height, lower_edge_height, string_factor)
248248
actual = np.around(actual * 100)
249249
assert np.allclose(expected, actual)
250+
251+
252+
def test_loss_townsend_front_side_fraction():
253+
snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7,
254+
25.4])
255+
snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
256+
surface_tilt = 20
257+
relative_humidity = np.full(12, 80)
258+
temp_air = np.full(12, 0)
259+
poa_global = np.full(12, 350000)
260+
slant_height = 2.54
261+
lower_edge_height = 0.254
262+
front_side_fraction = 0.9
263+
264+
unadjusted = snow.loss_townsend(
265+
snow_total, snow_events, surface_tilt, relative_humidity, temp_air,
266+
poa_global, slant_height, lower_edge_height)
267+
268+
adjusted = snow.loss_townsend(
269+
snow_total, snow_events, surface_tilt, relative_humidity, temp_air,
270+
poa_global, slant_height, lower_edge_height,
271+
front_side_fraction=front_side_fraction)
272+
273+
np.testing.assert_allclose(adjusted, unadjusted * front_side_fraction)
274+

tests/test_tracking.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,31 @@ def test_calc_surface_orientation_special():
557557
# in a modulo-360 sense.
558558
np.testing.assert_allclose(np.round(out['surface_azimuth'], 4) % 360,
559559
expected_azimuths, rtol=1e-5, atol=1e-5)
560+
561+
562+
@pytest.mark.parametrize('shape', [(3, 5), (1, 7), (4, 1), (2, 3, 4)])
563+
def test_calc_surface_orientation_2d(shape):
564+
# Regression test for GH#2747: calc_surface_orientation must accept
565+
# tracker_theta of arbitrary rank, not just 1-D. Compare the >1-D result
566+
# to the 1-D result computed on the flattened input.
567+
rotations_flat = np.linspace(-90, 90, int(np.prod(shape)))
568+
rotations_nd = rotations_flat.reshape(shape)
569+
570+
out_1d = tracking.calc_surface_orientation(rotations_flat,
571+
axis_tilt=20,
572+
axis_azimuth=180)
573+
out_nd = tracking.calc_surface_orientation(rotations_nd,
574+
axis_tilt=20,
575+
axis_azimuth=180)
576+
577+
assert out_nd['surface_tilt'].shape == shape
578+
assert out_nd['surface_azimuth'].shape == shape
579+
np.testing.assert_allclose(out_nd['surface_tilt'].reshape(-1),
580+
out_1d['surface_tilt'])
581+
np.testing.assert_allclose(out_nd['surface_azimuth'].reshape(-1),
582+
out_1d['surface_azimuth'])
583+
584+
# _unit_normal must preserve the input rank, appending a trailing axis
585+
# of length 3.
586+
unorm = tracking._unit_normal(180., 20., rotations_nd)
587+
assert unorm.shape == shape + (3,)

0 commit comments

Comments
 (0)