Skip to content

Commit 015500d

Browse files
committed
allow specifying custom row/ground subsets
1 parent 258d5a6 commit 015500d

2 files changed

Lines changed: 74 additions & 46 deletions

File tree

pvlib/bifacial/ants2d.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def _apply_ground_slope(height, pitch, gcr, tracker_rotation, ghi, dni, dhi,
285285
def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
286286
gcr, height, pitch, ghi, dhi, dni,
287287
albedo, model='perez', dni_extra=None, airmass=None,
288-
n_row_segments=1, n_ground_segments=10, axis_tilt=0,
288+
row_segments=1, ground_segments=10, axis_tilt=0,
289289
cross_axis_slope=0, max_rows=None,
290290
return_ground_components=False):
291291
"""
@@ -346,12 +346,19 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
346346
``model='haydavies'`` or ``model='perez'``. [Wm⁻²]
347347
airmass : numeric, optional
348348
Relative airmass. Required when ``model='perez'``. [unitless]
349-
n_row_segments : int, default 1
350-
Number of segments to partition the row surface into. Irradiance
351-
will be computed and returned for each segment.
352-
n_ground_segments : int, default 10
353-
Number of segments to partition the ground surface into. ``albedo``
354-
can be specified for each segment.
349+
row_segments : int or list of pairs, default 1
350+
If ``row_segments`` is an int, it defines the number of equal-length
351+
segments the row width is divided into. Otherwise, it must be a list
352+
of pairs ``(x0, x1)`` where ``x0`` and ``x1`` are fractions of the
353+
row width and ``x0 < x1``. Irradiance will be computed and returned
354+
for each segment.
355+
ground_segments : int or list of pairs, default 10
356+
If ``ground_segments`` is an int, it defines the number of equal-length
357+
segments the ground surface is divided into. Otherwise, it must be
358+
a list of pairs ``(g0, g1)`` where ``g0`` and ``g1`` are fractions of
359+
the pitch and ``g0 < g1``. The pairs must be non-overlapping and must
360+
cover the entire ground surface. ``albedo`` can be specified for
361+
each segment.
355362
axis_tilt : numeric, default 0
356363
Tilt of the axis of rotation with respect to horizontal. [degree]
357364
cross_axis_slope : numeric, default 0
@@ -376,8 +383,8 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
376383
-------
377384
output : dict or DataFrame
378385
``output`` is a DataFrame when inputs are Series
379-
and ``n_row_segments=1``, a dict of scalars when inputs are scalars
380-
and ``n_row_segments=1``, and a dict of ``np.ndarray``
386+
and ``row_segments=1``, a dict of scalars when inputs are scalars
387+
and ``row_segments=1``, and a dict of ``np.ndarray``
381388
otherwise. The following quantities are included:
382389
383390
- ``poa_global``: sum of front- and back-side incident irradiance.
@@ -433,6 +440,7 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
433440
np.isscalar(x) or x is None for x in maybe_array_inputs
434441
])
435442
try:
443+
# get the index of the first pandas input, if there is one
436444
pd_index = next(
437445
x.index for x in maybe_array_inputs if isinstance(x, pd.Series)
438446
)
@@ -453,13 +461,19 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
453461
cross_axis_slope
454462
)
455463

456-
x_row = np.linspace(0, 1, n_row_segments+1)
457-
x0 = x_row[:-1]
458-
x1 = x_row[1:]
464+
if np.isscalar(row_segments):
465+
x_row = np.linspace(0, 1, row_segments+1)
466+
x0, x1 = x_row[:-1], x_row[1:]
467+
else:
468+
x0 = np.array([pair[0] for pair in row_segments])
469+
x1 = np.array([pair[1] for pair in row_segments])
459470

460-
x_ground = np.linspace(0, 1, n_ground_segments+1)
461-
g0 = x_ground[:-1]
462-
g1 = x_ground[1:]
471+
if np.isscalar(ground_segments):
472+
x_ground = np.linspace(0, 1, ground_segments+1)
473+
g0, g1 = x_ground[:-1], x_ground[1:]
474+
else:
475+
g0 = np.array([pair[0] for pair in ground_segments])
476+
g1 = np.array([pair[1] for pair in ground_segments])
463477

464478
# dimensions: ground segment, row segment, time
465479
albedo = np.atleast_2d(albedo)[:, np.newaxis, :]
@@ -500,15 +514,16 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
500514
# inputs shared between front and back calculations
501515
params = dict(phi=phi, gcr=gcr, height=height, pitch=pitch, dni=dni,
502516
dhi=dhi, ground_irradiance=ground_total, albedo=albedo,
503-
x0=x0, x1=x1, g0=g0, g1=g1, max_rows=max_rows)
517+
g0=g0, g1=g1, max_rows=max_rows)
504518

505519
# front
506520
front_orientation = calc_surface_orientation(true_tracker_rotation,
507521
axis_tilt, axis_azimuth)
508522
cos_aoi_front = aoi_projection(**front_orientation,
509523
solar_zenith=solar_zenith,
510524
solar_azimuth=solar_azimuth)
511-
poa_front = _ants2d_singleside(tracker_rotation, cos_aoi_front, **params)
525+
poa_front = _ants2d_singleside(tracker_rotation, cos_aoi_front,
526+
x0=x0, x1=x1, **params)
512527

513528
# back
514529
tracker_rotation_back = true_tracker_rotation + 180
@@ -521,10 +536,7 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
521536
tracker_rotation_back = tracker_rotation + 180
522537
tracker_rotation_back = ((tracker_rotation_back + 180) % 360) - 180
523538
poa_back = _ants2d_singleside(tracker_rotation_back, cos_aoi_back,
524-
**params)
525-
526-
for key, value in poa_back.items():
527-
poa_back[key] = value[::-1, :] # invert x0/x1 dimension
539+
x0=1-x1, x1=1-x0, **params)
528540

529541
colmap_front = {
530542
'poa_global': 'poa_front',
@@ -543,7 +555,7 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
543555
poa_back[new_key] = poa_back.pop(old_key)
544556
out = {**poa_front, **poa_back}
545557

546-
if n_row_segments == 1 :
558+
if row_segments == 1:
547559
for k, v in out.items():
548560
out[k] = v[0] # drop row segment dimension
549561

@@ -557,7 +569,7 @@ def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth,
557569

558570
if return_ground_components:
559571
squeeze = []
560-
if n_ground_segments == 1:
572+
if ground_segments == 1:
561573
squeeze.append(0) # drop ground segment dimension
562574
squeeze.append(1) # always drop the row segment dimension
563575
if all_scalar_inputs:

tests/bifacial/test_ants2d.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def ants_params():
206206

207207
def test_get_irradiance_return_type(ants_params):
208208
# verify pandas in -> pandas out, and shapes of numpy outputs
209-
out = ants2d.get_irradiance(**ants_params, n_row_segments=1)
209+
out = ants2d.get_irradiance(**ants_params, row_segments=1)
210210
assert isinstance(out, pd.DataFrame) # DataFrame, since n_row_segments=1
211211
expected_keys = ['poa_front', 'poa_front_direct', 'poa_front_diffuse',
212212
'poa_front_sky_diffuse', 'poa_front_ground_diffuse',
@@ -216,16 +216,16 @@ def test_get_irradiance_return_type(ants_params):
216216
assert set(out.columns) == set(expected_keys)
217217
assert len(out) == 2 # 2 timestamps
218218

219-
out = ants2d.get_irradiance(**ants_params, n_row_segments=3)
220-
assert isinstance(out, dict) # dict, since n_row_segments>1
219+
out = ants2d.get_irradiance(**ants_params, row_segments=3)
220+
assert isinstance(out, dict) # dict, since row_segments>1
221221
assert set(out.keys()) == set(expected_keys)
222222
for k, v in out.items():
223223
assert v.shape == (3, 2), k # 3 row segments, 2 timestamps
224224

225225

226226
def test_get_irradiance_symmetry(ants_params):
227227
# check symmetries for normal tracker
228-
out = ants2d.get_irradiance(**ants_params, n_row_segments=1)
228+
out = ants2d.get_irradiance(**ants_params, row_segments=1)
229229
# symmetrical/mirrored inputs should produce equal outputs
230230
pd.testing.assert_series_equal(out.iloc[0, :], out.iloc[1, :],
231231
check_names=False)
@@ -242,7 +242,7 @@ def test_get_irradiance_vertical(ants_params, solar_zenith, tracker_rotation):
242242
index=ants_params['ghi'].index)
243243
ants_params['tracker_rotation'] = pd.Series(tracker_rotation,
244244
index=ants_params['ghi'].index)
245-
out = ants2d.get_irradiance(**ants_params, n_row_segments=1)
245+
out = ants2d.get_irradiance(**ants_params, row_segments=1)
246246
# inputs are symmetrical morning/afternoon, so morning front should equal
247247
# afternoon back, and vice versa
248248
front_keys = ['poa_front', 'poa_front_direct', 'poa_front_diffuse',
@@ -256,7 +256,7 @@ def test_get_irradiance_vertical(ants_params, solar_zenith, tracker_rotation):
256256
out.iloc[0][back_key])
257257

258258
# now with >1 row segment
259-
out = ants2d.get_irradiance(**ants_params, n_row_segments=2)
259+
out = ants2d.get_irradiance(**ants_params, row_segments=2)
260260
lower_half = 0
261261
upper_half = 1
262262
morning = 0
@@ -284,7 +284,7 @@ def test_get_irradiance_limit(ants_params):
284284
ants_params['dni'], ants_params['ghi'], ants_params['dhi'],
285285
albedo=ants_params['albedo'], model='isotropic')
286286

287-
ants = ants2d.get_irradiance(**ants_params, n_row_segments=1,
287+
ants = ants2d.get_irradiance(**ants_params, row_segments=1,
288288
model='isotropic')
289289
# 15 W/m2 happens to be just below the difference (determined empirically)
290290
diff_sky = irrad['poa_sky_diffuse'] - ants['poa_front_sky_diffuse']
@@ -296,7 +296,7 @@ def test_get_irradiance_limit(ants_params):
296296
# output of get_total_irradiance
297297
ants_params['pitch'] *= 1000
298298
ants_params['gcr'] /= 1000
299-
ants = ants2d.get_irradiance(**ants_params, n_row_segments=1,
299+
ants = ants2d.get_irradiance(**ants_params, row_segments=1,
300300
model='isotropic')
301301

302302
colmap = {'poa_front': 'poa_global', 'poa_front_direct': 'poa_direct',
@@ -357,10 +357,10 @@ def test_get_irradiance_direct_shading(ants_params_fixed):
357357

358358

359359
def test_get_irradiance_multiple_row_segments(ants_params_fixed):
360-
# check that granular sims average to the same value as n_row_segments=1
361-
out4 = ants2d.get_irradiance(**ants_params_fixed, n_row_segments=4)
362-
out2 = ants2d.get_irradiance(**ants_params_fixed, n_row_segments=2)
363-
out1 = ants2d.get_irradiance(**ants_params_fixed, n_row_segments=1)
360+
# check that granular sims average to the same value as row_segments=1
361+
out4 = ants2d.get_irradiance(**ants_params_fixed, row_segments=4)
362+
out2 = ants2d.get_irradiance(**ants_params_fixed, row_segments=2)
363+
out1 = ants2d.get_irradiance(**ants_params_fixed, row_segments=1)
364364

365365
for k in out4:
366366
# check two bottom quarters average to the bottom half, and top
@@ -372,6 +372,22 @@ def test_get_irradiance_multiple_row_segments(ants_params_fixed):
372372
np.testing.assert_allclose(np.mean(out2[k][:, 0]), out1[k])
373373

374374

375+
def test_get_irradiance_custom_x0x1(ants_params_fixed):
376+
# different ways of specifying the lower and upper halves
377+
expected = ants2d.get_irradiance(**ants_params_fixed, row_segments=2)
378+
actual = ants2d.get_irradiance(**ants_params_fixed,
379+
row_segments=[(0.0, 0.5), (0.5, 1.0)])
380+
for key in expected:
381+
np.testing.assert_allclose(expected[key], actual[key])
382+
383+
# specify only one part of the module
384+
expected = ants2d.get_irradiance(**ants_params_fixed, row_segments=4)
385+
actual = ants2d.get_irradiance(**ants_params_fixed,
386+
row_segments=[(0.25, 0.5)])
387+
for key in expected:
388+
np.testing.assert_allclose(expected[key][1], actual[key][0])
389+
390+
375391
def test_get_irradiance_slope(ants_params_fixed):
376392
# check the slope affects direct & diffuse shading
377393
flat = ants2d.get_irradiance(cross_axis_slope=0, **ants_params_fixed)
@@ -400,12 +416,12 @@ def test_get_irradiance_nonuniform_albedo():
400416
'albedo': np.array([[0.5]*10 + [0.1]*10]).T,
401417
'model': 'isotropic'
402418
}
403-
out = ants2d.get_irradiance(n_ground_segments=20,
404-
n_row_segments=10000,
419+
out = ants2d.get_irradiance(ground_segments=20,
420+
row_segments=10000,
405421
max_rows=2,
406422
**inputs)
407423
# check far left and right segments, on the edge of the module.
408-
# need a large n_row_segments so that these segments are very thin
424+
# need a large row_segments so that these segments are very thin
409425
left, right = out['poa_back_ground_diffuse'][[0, -1], 0]
410426
# divide by two because ~half the visible ground is fully shaded
411427
assert_allclose(left, 0.1 * 1000 / 2, rtol=0.002)
@@ -423,7 +439,7 @@ def test_get_irradiance_nonuniform_albedo_limit():
423439
'ghi': 300,
424440
'dni': 0, # set dni to zero so that shadows don't confound results
425441
'dhi': 300,
426-
'n_ground_segments': 2,
442+
'ground_segments': 2,
427443
'max_rows': 10000,
428444
'model': 'isotropic',
429445
}
@@ -511,7 +527,7 @@ def test_ground_components():
511527
'dni': 1000,
512528
'dhi': 300,
513529
'albedo': 0.2,
514-
'n_ground_segments': 1,
530+
'ground_segments': 1,
515531
'model': 'isotropic',
516532
}
517533

@@ -538,7 +554,7 @@ def test_ground_components():
538554
assert_allclose(out['ground_diffuse'], 150, atol=1e-3)
539555

540556
# flat array, four segments: perfect direct shading, diffuse symmetry
541-
inputs['n_ground_segments'] = 4
557+
inputs['ground_segments'] = 4
542558
inputs['solar_zenith'] = 0
543559
_, out = ants2d.get_irradiance(tracker_rotation=0, axis_azimuth=180,
544560
**inputs,
@@ -550,7 +566,7 @@ def test_ground_components():
550566

551567
# flat array, many rows, very high: ground_diffuse -> dhi * gcr
552568
inputs['height'] = 200
553-
inputs['n_ground_segments'] = 4
569+
inputs['ground_segments'] = 4
554570
_, out = ants2d.get_irradiance(tracker_rotation=0, axis_azimuth=180,
555571
**inputs,
556572
max_rows=5000,
@@ -562,15 +578,15 @@ def test_ground_components_types(ants_params, ants_params_fixed):
562578
# test second return value type/shape when return_ground_components=True
563579

564580
# scalar inputs, single ground segment
565-
_, out = ants2d.get_irradiance(**ants_params_fixed, n_ground_segments=1,
581+
_, out = ants2d.get_irradiance(**ants_params_fixed, ground_segments=1,
566582
return_ground_components=True)
567583
assert isinstance(out, dict)
568584
assert set(out.keys()) == {'ground_direct', 'ground_diffuse'}
569585
for key, value in out.items():
570586
assert isinstance(value, float), key
571587

572588
# scalar inputs, multiple ground segments
573-
_, out = ants2d.get_irradiance(**ants_params_fixed, n_ground_segments=10,
589+
_, out = ants2d.get_irradiance(**ants_params_fixed, ground_segments=10,
574590
return_ground_components=True)
575591
assert isinstance(out, dict)
576592
assert set(out.keys()) == {'ground_direct', 'ground_diffuse'}
@@ -579,14 +595,14 @@ def test_ground_components_types(ants_params, ants_params_fixed):
579595
assert value.shape == (10,), key
580596

581597
# series inputs, single ground segment
582-
_, out = ants2d.get_irradiance(**ants_params, n_ground_segments=1,
598+
_, out = ants2d.get_irradiance(**ants_params, ground_segments=1,
583599
return_ground_components=True)
584600
assert isinstance(out, pd.DataFrame)
585601
assert set(out.columns) == {'ground_direct', 'ground_diffuse'}
586602
assert len(out) == 2
587603

588604
# series inputs, multiple ground segments
589-
_, out = ants2d.get_irradiance(**ants_params, n_ground_segments=10,
605+
_, out = ants2d.get_irradiance(**ants_params, ground_segments=10,
590606
return_ground_components=True)
591607
assert isinstance(out, dict)
592608
assert set(out.keys()) == {'ground_direct', 'ground_diffuse'}

0 commit comments

Comments
 (0)