Skip to content

Commit 769f94c

Browse files
committed
Squashed commit of the following:
commit f435e1c Author: Adam R. Jensen <[email protected]> Date: Wed Mar 2 15:52:15 2022 +0100 Add tests (#16) * Create twoaxistrackerfield.py * Add TwoAxisTrackerField to __init__.py and documentation.rst * Add _calculate_l_min to layout * Fix linter error * Update twoaxistrackerfield.py * Reorder layout ValueError warnings * Fix typo * Remove layout_type from generate_field_layout function * Add layout_type code to TwoAxisTrackerField * L_min -> min_tracker_spacing * Add whatsnew entry * Fix linting errors * Update twoaxistrackerfield.py * Correct horizon shading * Update twoaxistracking/twoaxistrackerfield.py Co-authored-by: Kevin Anderson <[email protected]> * Update twoaxistracking/twoaxistrackerfield.py Co-authored-by: Kevin Anderson <[email protected]> * Update twoaxistracking/twoaxistrackerfield.py Co-authored-by: Kevin Anderson <[email protected]> * Return scalar when scalar is passed * Correct use of np.isscalar * Update documentation * Update whatsnew * Rework intro_tutorial.ipynb * Update README.md * Create test_layout.py * Update test_layout.py * Use test folder in package folder * Delete test_placeholder.py * Add test files * Fix linting errors * Update scalar test * Add pandas to test in setup.cfg * Revert "package metadata and doc build updates (#17)" This reverts commit e9b5810. * Undo commits * Revert "Undo commits" This reverts commit 147ba2a. * Add test files * Switch to using np.testing.assert_allclose * Add test for sloped field layout * Move test_twoaxistracker to twoaxistracker PR * Add conftest.py to avoid duplicate fixtures * Remove imports of fixtures from conftest * Remove unnecessary imports * Increase test tolerance for test_field_slope * Add multi-polygon active area test * Add empty __init__.py * Use "from .conftest" as Kevin said.. * Base square_field_layout_slope fixture off square_field_layout * Fix linter error * Remove backslash in conftest.py Co-authored-by: Kevin Anderson <[email protected]>
1 parent 0fb8057 commit 769f94c

File tree

9 files changed

+351
-14
lines changed

9 files changed

+351
-14
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ install_requires =
2828
test =
2929
pytest
3030
pytest-cov
31+
pandas
3132
doc =
3233
sphinx==4.4.0
3334
myst-nb==0.13.2

twoaxistracking/plotting.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from shapely import geometry
55
import matplotlib.colors as mcolors
66
from matplotlib import cm
7+
import numpy as np
78

89

910
def _plot_field_layout(X, Y, Z, min_tracker_spacing):
@@ -40,10 +41,12 @@ def _polygons_to_patch_collection(geometries, **kwargs):
4041
4142
kwargs are passed to PatchCollection
4243
"""
43-
# Convert geometries to a MultiPolygon if it is a Polygon
44+
# Convert geometries to a list
4445
if isinstance(geometries, geometry.Polygon):
45-
geometries = geometry.MultiPolygon([geometries])
46-
exteriors = [patches.Polygon(g.exterior) for g in geometries]
46+
geometries = [geometries]
47+
elif isinstance(geometries, geometry.MultiPolygon):
48+
geometries = list(geometries.geoms)
49+
exteriors = [patches.Polygon(np.array(g.exterior)) for g in geometries]
4750
path_collection = collections.PatchCollection(exteriors, **kwargs)
4851
return path_collection
4952

twoaxistracking/tests/__init__.py

Whitespace-only changes.

twoaxistracking/tests/conftest.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
from shapely import geometry
3+
import numpy as np
4+
from twoaxistracking import layout
5+
6+
7+
@pytest.fixture
8+
def rectangular_geometry():
9+
collector_geometry = geometry.box(-2, -1, 2, 1)
10+
total_collector_area = collector_geometry.area
11+
min_tracker_spacing = layout._calculate_min_tracker_spacing(collector_geometry)
12+
return collector_geometry, total_collector_area, min_tracker_spacing
13+
14+
15+
@pytest.fixture
16+
def active_geometry_split():
17+
active_collector_geometry = geometry.MultiPolygon([
18+
geometry.box(-1.9, -0.9, -0.1, -0.1),
19+
geometry.box(0.1, -0.9, 1.9, -0.1),
20+
geometry.box(-1.9, 0.1, -0.1, 0.9),
21+
geometry.box(0.1, 0.1, 1.9, 0.9)])
22+
return active_collector_geometry
23+
24+
25+
@pytest.fixture
26+
def square_field_layout():
27+
# Corresponds to GCR 0.125 with the rectangular_geometry
28+
X = np.array([-8, 0, 8, -8, 8, -8, 0, 8])
29+
Y = np.array([-8, -8, -8, 0, 0, 8, 8, 8])
30+
tracker_distance = (X**2 + Y**2)**0.5
31+
relative_azimuth = np.array([225, 180, 135, 270, 90, 315, 0, 45])
32+
Z = np.zeros(8)
33+
relative_slope = np.zeros(8)
34+
return X, Y, Z, tracker_distance, relative_azimuth, relative_slope
35+
36+
37+
@pytest.fixture
38+
def square_field_layout_sloped(square_field_layout):
39+
# Corresponds to GCR 0.125 with the rectangular_geometry and tilted slope
40+
# Based on the square_field_layout
41+
X, Y, _, tracker_distance, relative_azimuth, _ = square_field_layout
42+
Z = np.array([0.12372765, 0.06186383, 0, 0.06186383,
43+
-0.06186383, 0, -0.06186383, -0.12372765])
44+
relative_slope = np.array([5, 3.53553391, 0, 3.53553391,
45+
-3.53553391, 0, -3.53553391, -5])
46+
return X, Y, Z, tracker_distance, relative_azimuth, relative_slope
47+
48+
49+
def assert_isinstance(obj, klass):
50+
assert isinstance(obj, klass), f'got {type(obj)}, expected {klass}'
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from twoaxistracking import layout
2+
from shapely import geometry
3+
import numpy as np
4+
import pytest
5+
6+
7+
def test_min_tracker_spacing_rectangle(rectangular_geometry):
8+
# Test calculation of min_tracker_spacing for a rectangular collector
9+
min_tracker_spacing = layout._calculate_min_tracker_spacing(rectangular_geometry[0])
10+
np.testing.assert_allclose(min_tracker_spacing, np.sqrt(4**2+2**2))
11+
12+
13+
def test_min_tracker_spacing_circle():
14+
# Test calculation of min_tracker_spacing for a circular collector with radius 1
15+
collector_geometry = geometry.Point(0, 0).buffer(1)
16+
min_tracker_spacing = layout._calculate_min_tracker_spacing(collector_geometry)
17+
assert min_tracker_spacing == 2
18+
19+
20+
def test_min_tracker_spacing_circle_offcenter():
21+
# Test calculation of min_tracker_spacing for a circular collector with radius 1 rotating
22+
# off-center around the point (0, 1)
23+
collector_geometry = geometry.Point(0, 1).buffer(1)
24+
min_tracker_spacing = layout._calculate_min_tracker_spacing(collector_geometry)
25+
assert min_tracker_spacing == 4
26+
27+
28+
def test_min_tracker_spacingpolygon():
29+
# Test calculation of min_tracker_spacing for a polygon
30+
collector_geometry = geometry.Polygon([(-1, -1), (3, 2), (4, 4), (1, 2), (-1, -1)])
31+
min_tracker_spacing = layout._calculate_min_tracker_spacing(collector_geometry)
32+
np.testing.assert_allclose(min_tracker_spacing, 2 * np.sqrt(4**2 + 4**2))
33+
34+
35+
def test_square_layout_generation(rectangular_geometry, square_field_layout):
36+
# Test that a square field layout is returned correctly
37+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
38+
X_exp, Y_exp, Z_exp, tracker_distance_exp, relative_azimuth_exp, relative_slope_exp = \
39+
square_field_layout
40+
41+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
42+
layout.generate_field_layout(
43+
gcr=0.125,
44+
total_collector_area=total_collector_area,
45+
min_tracker_spacing=min_tracker_spacing,
46+
neighbor_order=1,
47+
aspect_ratio=1,
48+
offset=0,
49+
rotation=0)
50+
np.testing.assert_allclose(X, X_exp)
51+
np.testing.assert_allclose(Y, Y_exp)
52+
np.testing.assert_allclose(Z, Z_exp)
53+
np.testing.assert_allclose(tracker_distance_exp, tracker_distance_exp)
54+
np.testing.assert_allclose(relative_azimuth, relative_azimuth_exp)
55+
np.testing.assert_allclose(relative_slope, relative_slope_exp)
56+
57+
58+
def test_field_slope(rectangular_geometry, square_field_layout_sloped):
59+
# Test that a square field layout on tilted surface is returned correctly
60+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
61+
X_exp, Y_exp, Z_exp, tracker_distance_exp, relative_azimuth_exp, relative_slope_exp = \
62+
square_field_layout_sloped
63+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
64+
layout.generate_field_layout(
65+
gcr=0.125,
66+
total_collector_area=total_collector_area,
67+
min_tracker_spacing=min_tracker_spacing,
68+
neighbor_order=1,
69+
aspect_ratio=1,
70+
offset=0,
71+
rotation=0,
72+
slope_azimuth=45,
73+
slope_tilt=5)
74+
np.testing.assert_allclose(X, X_exp)
75+
np.testing.assert_allclose(Y, Y_exp)
76+
np.testing.assert_allclose(Z, Z_exp, atol=10**-9)
77+
np.testing.assert_allclose(tracker_distance_exp, tracker_distance_exp)
78+
np.testing.assert_allclose(relative_azimuth, relative_azimuth_exp)
79+
np.testing.assert_allclose(relative_slope, relative_slope_exp, atol=10**-9)
80+
81+
82+
def test_layout_generation_value_error(rectangular_geometry):
83+
# Test if value errors are correctly raised
84+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
85+
86+
# Test if ValueError is raised if offset is out of range
87+
with pytest.raises(ValueError, match="offset is outside the valid range"):
88+
_ = layout.generate_field_layout(
89+
gcr=0.25, total_collector_area=total_collector_area,
90+
min_tracker_spacing=min_tracker_spacing, neighbor_order=1,
91+
aspect_ratio=1, offset=1.1, rotation=0)
92+
93+
# Test if ValueError is raised if aspect ratio is too low
94+
with pytest.raises(ValueError, match="Aspect ratio is too low"):
95+
_ = layout.generate_field_layout(
96+
gcr=0.25, total_collector_area=total_collector_area,
97+
min_tracker_spacing=min_tracker_spacing, neighbor_order=1,
98+
aspect_ratio=0.6, offset=0, rotation=0)
99+
100+
# Test if ValueError is raised if aspect ratio is too high
101+
with pytest.raises(ValueError, match="Aspect ratio is too high"):
102+
_ = layout.generate_field_layout(
103+
gcr=0.25, total_collector_area=total_collector_area,
104+
min_tracker_spacing=min_tracker_spacing, neighbor_order=1,
105+
aspect_ratio=5, offset=0, rotation=0)
106+
107+
# Test if ValueError is raised if rotation is greater than 180 degrees
108+
with pytest.raises(ValueError, match="rotation is outside the valid range"):
109+
_ = layout.generate_field_layout(
110+
gcr=0.25, total_collector_area=total_collector_area,
111+
min_tracker_spacing=min_tracker_spacing, neighbor_order=1,
112+
aspect_ratio=1.2, offset=0, rotation=190)
113+
114+
# Test if ValueError is raised if rotation is less than 0
115+
with pytest.raises(ValueError, match="rotation is outside the valid range"):
116+
_ = layout.generate_field_layout(
117+
gcr=0.5, total_collector_area=total_collector_area,
118+
min_tracker_spacing=min_tracker_spacing, neighbor_order=1,
119+
aspect_ratio=1, offset=0, rotation=-1)
120+
121+
# Test if ValueError is raised if min_tracker_spacing is outside valid range
122+
with pytest.raises(ValueError, match="Lmin is not physically possible"):
123+
_ = layout.generate_field_layout(
124+
gcr=0.25, total_collector_area=total_collector_area,
125+
min_tracker_spacing=1, neighbor_order=1, aspect_ratio=1.2,
126+
offset=0, rotation=90)
127+
128+
# Test if ValueError is raised if maximum ground cover ratio is exceeded
129+
with pytest.raises(ValueError, match="Maximum ground cover ratio exceded"):
130+
_ = layout.generate_field_layout(
131+
gcr=0.5, total_collector_area=total_collector_area,
132+
min_tracker_spacing=min_tracker_spacing, neighbor_order=1,
133+
aspect_ratio=1, offset=0, rotation=0)
134+
135+
136+
def test_neighbor_order(rectangular_geometry):
137+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
138+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
139+
layout.generate_field_layout(
140+
gcr=0.125,
141+
total_collector_area=total_collector_area,
142+
min_tracker_spacing=min_tracker_spacing,
143+
neighbor_order=3,
144+
aspect_ratio=1,
145+
offset=0,
146+
rotation=0)
147+
assert len(X) == (7*7-1)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import matplotlib.pyplot as plt
2+
from twoaxistracking import plotting, twoaxistrackerfield
3+
from .conftest import assert_isinstance
4+
import numpy as np
5+
6+
7+
def test_field_layout_plot():
8+
X = np.array([-8, 0, 8, -8, 8, -8, 0, 8])
9+
Y = np.array([-8, -8, -8, 0, 0, 8, 8, 8])
10+
Z = np.array([0.1237, 0.0619, 0, 0.0619, -0.0619, 0, -0.0619, -0.1237])
11+
L_min = 4.4721
12+
result = plotting._plot_field_layout(X, Y, Z, L_min)
13+
assert_isinstance(result, plt.Figure)
14+
plt.close('all')
15+
16+
17+
def test_shading_plot(rectangular_geometry, active_geometry_split):
18+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
19+
result = plotting._plot_shading(
20+
active_geometry_split,
21+
collector_geometry,
22+
collector_geometry,
23+
min_tracker_spacing)
24+
assert_isinstance(result, plt.Figure)
25+
plt.close('all')
26+
27+
28+
def test_plotting_of_field_layout(rectangular_geometry):
29+
# Test if plot_field_layout returns a figure object
30+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
31+
field = twoaxistrackerfield.TwoAxisTrackerField(
32+
total_collector_geometry=collector_geometry,
33+
active_collector_geometry=collector_geometry,
34+
neighbor_order=1,
35+
gcr=0.25,
36+
aspect_ratio=1,
37+
offset=0.45,
38+
rotation=30,
39+
)
40+
result = field.plot_field_layout()
41+
assert_isinstance(result, plt.Figure)
42+
plt.close('all')
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from twoaxistracking import shading
2+
import numpy as np
3+
4+
5+
def test_shading(rectangular_geometry, active_geometry_split, square_field_layout):
6+
# Test shading calculation
7+
# Also plots the geometry (ensures no errors are raised)
8+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
9+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
10+
square_field_layout
11+
shaded_fraction = shading.shaded_fraction(
12+
solar_elevation=3,
13+
solar_azimuth=120,
14+
total_collector_geometry=collector_geometry,
15+
active_collector_geometry=active_geometry_split,
16+
min_tracker_spacing=min_tracker_spacing,
17+
tracker_distance=tracker_distance,
18+
relative_azimuth=relative_azimuth,
19+
relative_slope=relative_slope,
20+
slope_azimuth=0,
21+
slope_tilt=0,
22+
plot=True)
23+
np.testing.assert_allclose(shaded_fraction, 0.190320666774)
24+
25+
26+
def test_shading_zero_solar_elevation(rectangular_geometry, square_field_layout):
27+
# Test shading when geometries completely overlap
28+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
29+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
30+
square_field_layout
31+
shaded_fraction = shading.shaded_fraction(
32+
solar_elevation=0,
33+
solar_azimuth=180,
34+
total_collector_geometry=collector_geometry,
35+
active_collector_geometry=collector_geometry,
36+
min_tracker_spacing=min_tracker_spacing,
37+
tracker_distance=tracker_distance,
38+
relative_azimuth=relative_azimuth,
39+
relative_slope=relative_slope,
40+
slope_azimuth=0,
41+
slope_tilt=0,
42+
plot=False)
43+
assert shaded_fraction == 1
44+
45+
46+
def test_no_shading(rectangular_geometry, square_field_layout):
47+
# Test shading calculation when there is no shading (high solar elevation)
48+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
49+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
50+
square_field_layout
51+
shaded_fraction = shading.shaded_fraction(
52+
solar_elevation=45,
53+
solar_azimuth=180,
54+
total_collector_geometry=collector_geometry,
55+
active_collector_geometry=collector_geometry,
56+
min_tracker_spacing=min_tracker_spacing,
57+
tracker_distance=tracker_distance,
58+
relative_azimuth=relative_azimuth,
59+
relative_slope=relative_slope,
60+
slope_azimuth=0,
61+
slope_tilt=0,
62+
plot=False)
63+
assert shaded_fraction == 0
64+
65+
66+
def test_shading_below_horizon(rectangular_geometry, square_field_layout):
67+
# Test shading calculation when sun is below the horizon (elevation<0)
68+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
69+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
70+
square_field_layout
71+
shaded_fraction = shading.shaded_fraction(
72+
solar_elevation=-5.1,
73+
solar_azimuth=180,
74+
total_collector_geometry=collector_geometry,
75+
active_collector_geometry=collector_geometry,
76+
min_tracker_spacing=min_tracker_spacing,
77+
tracker_distance=tracker_distance,
78+
relative_azimuth=relative_azimuth,
79+
relative_slope=relative_slope,
80+
slope_azimuth=0,
81+
slope_tilt=0,
82+
plot=False)
83+
assert np.isnan(shaded_fraction)
84+
85+
86+
def test_shading_below_hill_horizon(rectangular_geometry, square_field_layout):
87+
# Test shading when sun is below horizon line caused by sloped surface
88+
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry
89+
X, Y, Z, tracker_distance, relative_azimuth, relative_slope = \
90+
square_field_layout
91+
shaded_fraction = shading.shaded_fraction(
92+
solar_elevation=9,
93+
solar_azimuth=180,
94+
total_collector_geometry=collector_geometry,
95+
active_collector_geometry=collector_geometry,
96+
min_tracker_spacing=min_tracker_spacing,
97+
tracker_distance=tracker_distance,
98+
relative_azimuth=relative_azimuth,
99+
relative_slope=relative_slope,
100+
slope_azimuth=0,
101+
slope_tilt=10,
102+
plot=False)
103+
assert shaded_fraction == 1

twoaxistracking/tests/test_twoaxistrackerfield.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1-
from twoaxistracking import layout, twoaxistrackerfield
2-
from shapely import geometry
1+
from twoaxistracking import twoaxistrackerfield
32
import numpy as np
43
import pandas as pd
54
import pytest
65

76

8-
@pytest.fixture
9-
def rectangular_geometry():
10-
collector_geometry = geometry.box(-2, -1, 2, 1)
11-
total_collector_area = collector_geometry.area
12-
min_tracker_spacing = layout._calculate_min_tracker_spacing(collector_geometry)
13-
return collector_geometry, total_collector_area, min_tracker_spacing
14-
15-
167
def test_invalid_layout_type(rectangular_geometry):
178
# Test if ValueError is raised when an incorrect layout_type is specified
189
collector_geometry, total_collector_area, min_tracker_spacing = rectangular_geometry

0 commit comments

Comments
 (0)