Skip to content

Commit 0fb8057

Browse files
Add TwoAxisTrackerField class (#14)
* 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 * Add test_twoaxistrackerfield.py * Update whatsnew.md * Update README.md Co-authored-by: Kevin Anderson <[email protected]>
1 parent e9b5810 commit 0fb8057

File tree

10 files changed

+657
-304
lines changed

10 files changed

+657
-304
lines changed

docs/source/documentation.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ Code documentation
88
:toctree: generated/
99

1010
shaded_fraction
11-
generate_field_layout
11+
generate_field_layout
12+
TwoAxisTrackerField
13+
TwoAxisTrackerField.get_shaded_fraction
14+
TwoAxisTrackerField.plot_field_layout

docs/source/notebooks/intro_tutorial.ipynb

Lines changed: 211 additions & 202 deletions
Large diffs are not rendered by default.

docs/source/whatsnew.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Tilted fields can now be simulated by specifyig the keywords ``slope_azimuth`` and
1414
``slope_tilt`` (see PR#7).
1515
- The code now is able to differentiate between the active area and total area (see PR#11).
16-
16+
- The class TwoAxisTrackerField has been added, which is now the recommended way for using
17+
the package and is sufficient for most use cases.
1718

1819
### Changed
1920
- Divide code into modules: shading, plotting, and layout
2021
- Changed the overall file structure to become a Python package
2122
- Changed names of notebooks
2223
- Change repository name from "two_axis_tracker_shading" to
2324
"twoaxistracking"
25+
- Changed naming of ``L_min`` to ``min_tracker_spacing``
26+
- Changed naming of ``collector_area`` to ``total_collector_area``
27+
- The field layout parameters are now required when using the
28+
{py:func}`twoaxistracking.layout.generate_field_layout` function. The standard field layouts
29+
are only available through the TwoAxisTrackerField class.
2430

2531
### Testing
2632
- Linting using flake8 was added in PR#11

twoaxistracking/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .layout import generate_field_layout # noqa: F401
22
from .shading import shaded_fraction # noqa: F401
3-
3+
from .twoaxistrackerfield import TwoAxisTrackerField # noqa: F401
44

55
try:
66
from shapely.geos import lgeos # noqa: F401

twoaxistracking/layout.py

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import numpy as np
2-
from twoaxistracking import plotting
2+
from shapely import geometry
33

44

55
def _rotate_origin(x, y, rotation_deg):
@@ -11,22 +11,19 @@ def _rotate_origin(x, y, rotation_deg):
1111
return xx, yy
1212

1313

14-
def generate_field_layout(gcr, total_collector_area, L_min, neighbor_order,
15-
aspect_ratio=None, offset=None, rotation=None,
16-
layout_type=None, slope_azimuth=0,
17-
slope_tilt=0, plot=False):
14+
def _calculate_min_tracker_spacing(collector_geometry):
15+
min_tracker_spacing = 2 * collector_geometry.hausdorff_distance(geometry.Point(0, 0))
16+
return min_tracker_spacing
17+
18+
19+
def generate_field_layout(gcr, total_collector_area, min_tracker_spacing,
20+
neighbor_order, aspect_ratio, offset, rotation,
21+
slope_azimuth=0, slope_tilt=0):
1822
"""
1923
Generate a regularly-spaced collector field layout.
2024
2125
Field layout parameters and limits are described in [1]_.
2226
23-
Notes
24-
-----
25-
The field layout can be specified either by selecting a standard layout
26-
using the layout_type argument or by specifying the individual layout
27-
parameters aspect_ratio, offset, and rotation. For both cases the ground
28-
cover ratio (gcr) needs to be specified.
29-
3027
Any length unit can be used as long as the usage is consistent with the
3128
collector geometry.
3229
@@ -36,26 +33,22 @@ def generate_field_layout(gcr, total_collector_area, L_min, neighbor_order,
3633
Ground cover ratio. Ratio of collector area to ground area.
3734
total_collector_area: float
3835
Surface area of one collector.
39-
L_min: float
36+
min_tracker_spacing: float
4037
Minimum distance between collectors.
4138
neighbor_order: int
4239
Order of neighbors to include in layout. neighbor_order=1 includes only
4340
the 8 directly adjacent collectors.
44-
aspect_ratio: float, optional
41+
aspect_ratio: float
4542
Ratio of the spacing in the primary direction to the secondary.
46-
offset: float, optional
43+
offset: float
4744
Relative row offset in the secondary direction as fraction of the
4845
spacing in the primary direction. -0.5 <= offset < 0.5.
49-
rotation: float, optional
46+
rotation: float
5047
Counterclockwise rotation of the field in degrees. 0 <= rotation < 180
51-
layout_type: {square, square_rotated, hexagon_e_w, hexagon_n_s}, optional
52-
Specification of the special layout type (only depend on gcr).
5348
slope_azimuth : float, optional
5449
Direction of normal to slope on horizontal [degrees]
5550
slope_tilt : float, optional
5651
Tilt of slope relative to horizontal [degrees]
57-
plot: bool, default: False
58-
Whether to plot the field layout.
5952
6053
Returns
6154
-------
@@ -81,48 +74,22 @@ def generate_field_layout(gcr, total_collector_area, L_min, neighbor_order,
8174
.. [1] `Shading and land use in regularly-spaced sun-tracking collectors, Cumpston & Pye.
8275
<https://doi.org/10.1016/j.solener.2014.06.012>`_
8376
"""
84-
# Consider special layouts which can be defined only by GCR
85-
if layout_type == 'square':
86-
aspect_ratio = 1
87-
offset = 0
88-
rotation = 0
89-
# Diagonal layout is the square layout rotated 45 degrees
90-
elif layout_type == 'diagonal':
91-
aspect_ratio = 1
92-
offset = 0
93-
rotation = 45
94-
# Hexagonal layouts are defined by aspect_ratio=0.866 and offset=-0.5
95-
elif layout_type == 'hexagonal_n_s':
96-
aspect_ratio = np.sqrt(3)/2
97-
offset = -0.5
98-
rotation = 0
99-
# The hexagonal E-W layout is the hexagonal N-S layout rotated 90 degrees
100-
elif layout_type == 'hexagonal_e_w':
101-
aspect_ratio = np.sqrt(3)/2
102-
offset = -0.5
103-
rotation = 90
104-
elif layout_type is not None:
105-
raise ValueError('The layout type specified was not recognized.')
106-
elif ((aspect_ratio is None) or (offset is None) or (rotation is None)):
107-
raise ValueError('Aspect ratio, offset, and rotation needs to be '
108-
'specified when no layout type has not been selected')
109-
11077
# Check parameters are within their ranges
111-
if aspect_ratio < np.sqrt(1-offset**2):
112-
raise ValueError('Aspect ratio is too low and not feasible')
113-
if aspect_ratio > total_collector_area/(gcr*L_min**2):
114-
raise ValueError('Apsect ratio is too high and not feasible')
11578
if (offset < -0.5) | (offset >= 0.5):
11679
raise ValueError('The specified offset is outside the valid range.')
11780
if (rotation < 0) | (rotation >= 180):
11881
raise ValueError('The specified rotation is outside the valid range.')
119-
# Check if mimimum and maximum ground cover ratios are exceded
120-
gcr_max = total_collector_area / (L_min**2 * np.sqrt(1-offset**2))
121-
if (gcr < 0) or (gcr > gcr_max):
122-
raise ValueError('Maximum ground cover ratio exceded.')
12382
# Check if Lmin is physically possible given the collector area.
124-
if (L_min < np.sqrt(4*total_collector_area/np.pi)):
83+
if (min_tracker_spacing < np.sqrt(4*total_collector_area/np.pi)):
12584
raise ValueError('Lmin is not physically possible.')
85+
# Check if mimimum and maximum ground cover ratios are exceded
86+
gcr_max = total_collector_area / (min_tracker_spacing**2 * np.sqrt(1-offset**2))
87+
if (gcr < 0) or (gcr > gcr_max):
88+
raise ValueError('Maximum ground cover ratio exceded or less than 0.')
89+
if aspect_ratio < np.sqrt(1-offset**2):
90+
raise ValueError('Aspect ratio is too low and not feasible')
91+
if aspect_ratio > total_collector_area/(gcr*min_tracker_spacing**2):
92+
raise ValueError('Aspect ratio is too high and not feasible')
12693

12794
N = 1 + 2 * neighbor_order # Number of collectors along each side
12895

@@ -156,8 +123,4 @@ def generate_field_layout(gcr, total_collector_area, L_min, neighbor_order,
156123
# positive means collector is higher than reference collector
157124
relative_slope = -np.cos(np.deg2rad(slope_azimuth - relative_azimuth)) * slope_tilt # noqa: E501
158125

159-
# Visualize layout
160-
if plot:
161-
plotting._plot_field_layout(X, Y, Z, L_min)
162-
163126
return X, Y, Z, tracker_distance, relative_azimuth, relative_slope

twoaxistracking/plotting.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,33 @@
66
from matplotlib import cm
77

88

9-
def _plot_field_layout(X, Y, Z, L_min):
10-
"""Plot field layout."""
9+
def _plot_field_layout(X, Y, Z, min_tracker_spacing):
10+
"""Create a plot of the field layout."""
1111
# Collector heights is illustrated with colors from a colormap
1212
norm = mcolors.Normalize(vmin=min(Z)-0.000001, vmax=max(Z)+0.000001)
13-
# 0.000001 is added/subtracted for the limits in order for the colormap
13+
# 0.000001 is added/subtracted to/from the limits in order for the colormap
1414
# to correctly display the middle color when all tracker Z coords are zero
1515
cmap = cm.viridis_r
1616
colors = cmap(norm(Z))
1717
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw={'aspect': 'equal'})
18-
# Plot a circle for each neighboring collector (diameter equals L_min)
18+
# Plot a circle for each neighboring collector (diameter equals min_tracker_spacing)
1919
ax.add_collection(collections.EllipseCollection(
20-
widths=L_min, heights=L_min, angles=0, units='xy', facecolors=colors,
21-
edgecolors=("black",), linewidths=(1,), offsets=list(zip(X, Y)),
22-
transOffset=ax.transData))
20+
widths=min_tracker_spacing, heights=min_tracker_spacing, angles=0,
21+
units='xy', facecolors=colors, edgecolors=("black",), linewidths=(1,),
22+
offsets=list(zip(X, Y)), transOffset=ax.transData))
2323
# Similarly, add a circle for the origin
2424
ax.add_collection(collections.EllipseCollection(
25-
widths=L_min, heights=L_min, angles=0, units='xy', facecolors='red',
26-
edgecolors=("black",), linewidths=(1,), offsets=[0, 0],
27-
transOffset=ax.transData))
28-
plt.axis('equal')
25+
widths=min_tracker_spacing, heights=min_tracker_spacing, angles=0,
26+
units='xy', facecolors='red', edgecolors=("black",), linewidths=(1,),
27+
offsets=[0, 0], transOffset=ax.transData))
2928
fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax, shrink=0.8,
3029
label='Relative tracker height (vertical)')
3130
# Set limits
32-
lower_lim = min(min(X), min(Y)) - L_min
33-
upper_lim = max(max(X), max(Y)) + L_min
34-
ax.set_ylim(lower_lim, upper_lim)
31+
lower_lim = min(min(X), min(Y)) - min_tracker_spacing
32+
upper_lim = max(max(X), max(Y)) + min_tracker_spacing
3533
ax.set_xlim(lower_lim, upper_lim)
34+
ax.set_ylim(lower_lim, upper_lim)
35+
return fig
3636

3737

3838
def _polygons_to_patch_collection(geometries, **kwargs):
@@ -49,7 +49,7 @@ def _polygons_to_patch_collection(geometries, **kwargs):
4949

5050

5151
def _plot_shading(active_collector_geometry, unshaded_geometry,
52-
shading_geometries, L_min):
52+
shading_geometries, min_tracker_spacing):
5353
"""Plot the shaded and unshaded area for a specific solar position."""
5454
active_patches = _polygons_to_patch_collection(
5555
active_collector_geometry, facecolor='red', linewidth=0.5, alpha=0.5)
@@ -59,12 +59,12 @@ def _plot_shading(active_collector_geometry, unshaded_geometry,
5959
shading_geometries, facecolor='blue', linewidth=0.5, alpha=0.5)
6060

6161
fig, axes = plt.subplots(1, 2, subplot_kw=dict(aspect='equal'))
62-
axes[0].set_title('Unshaded and shading areas')
62+
axes[0].set_title('Total area and shading areas')
6363
axes[0].add_collection(active_patches, autolim=True)
6464
axes[0].add_collection(shading_patches, autolim=True)
6565
axes[1].set_title('Unshaded area')
6666
axes[1].add_collection(unshaded_patches, autolim=True)
6767
for ax in axes:
68-
ax.set_xlim(-L_min, L_min)
69-
ax.set_ylim(-L_min, L_min)
70-
plt.show()
68+
ax.set_xlim(-min_tracker_spacing, min_tracker_spacing)
69+
ax.set_ylim(-min_tracker_spacing, min_tracker_spacing)
70+
return fig

twoaxistracking/shading.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,10 @@
33
from twoaxistracking import plotting
44

55

6-
def _rotate_origin(x, y, rotation_deg):
7-
"""Rotate a set of 2D points counterclockwise around the origin (0, 0)."""
8-
rotation_rad = np.deg2rad(rotation_deg)
9-
# Rotation is set negative to make counterclockwise rotation
10-
xx = x * np.cos(-rotation_rad) + y * np.sin(-rotation_rad)
11-
yy = -x * np.sin(-rotation_rad) + y * np.cos(-rotation_rad)
12-
return xx, yy
13-
14-
156
def shaded_fraction(solar_elevation, solar_azimuth,
167
total_collector_geometry, active_collector_geometry,
17-
L_min, tracker_distance, relative_azimuth, relative_slope,
18-
slope_azimuth=0, slope_tilt=0, plot=False):
8+
min_tracker_spacing, tracker_distance, relative_azimuth,
9+
relative_slope, slope_azimuth=0, slope_tilt=0, plot=False):
1910
"""Calculate the shaded fraction for any layout of two-axis tracking collectors.
2011
2112
Parameters
@@ -28,7 +19,7 @@ def shaded_fraction(solar_elevation, solar_azimuth,
2819
Polygon corresponding to the total collector area.
2920
active_collector_geometry: Shapely Polygon or MultiPolygon
3021
One or more polygons defining the active collector area.
31-
L_min: float
22+
min_tracker_spacing: float
3223
Minimum distance between collectors. Used for selecting possible
3324
shading collectors.
3425
tracker_distance: array of floats
@@ -57,7 +48,9 @@ def shaded_fraction(solar_elevation, solar_azimuth,
5748
return np.nan
5849
# Set shaded fraction to 1 (fully shaded) if the solar elevation is below
5950
# the horizon line caused by the tilted ground
60-
elif solar_elevation < - np.cos(np.deg2rad(slope_azimuth-solar_azimuth)) * slope_tilt:
51+
elif np.tan(np.deg2rad(solar_elevation)) <= (
52+
- np.cos(np.deg2rad(slope_azimuth-solar_azimuth))
53+
* np.tan(np.deg2rad(slope_tilt))):
6154
return 1
6255

6356
azimuth_difference = solar_azimuth - relative_azimuth
@@ -75,7 +68,7 @@ def shaded_fraction(solar_elevation, solar_azimuth,
7568
unshaded_geometry = active_collector_geometry
7669
shading_geometries = []
7770
for i, (x, y) in enumerate(zip(xoff, yoff)):
78-
if np.sqrt(x**2+y**2) < L_min:
71+
if np.sqrt(x**2+y**2) < min_tracker_spacing:
7972
# Project the geometry of the shading collector (total area) onto
8073
# the plane of the reference collector
8174
shading_geometry = shapely.affinity.translate(total_collector_geometry, x, y) # noqa: E501
@@ -86,7 +79,7 @@ def shaded_fraction(solar_elevation, solar_azimuth,
8679

8780
if plot:
8881
plotting._plot_shading(active_collector_geometry, unshaded_geometry,
89-
shading_geometries, L_min)
82+
shading_geometries, min_tracker_spacing)
9083

9184
shaded_fraction = 1 - unshaded_geometry.area / active_collector_geometry.area
9285
return shaded_fraction

twoaxistracking/tests/test_placeholder.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)