Skip to content

Commit

Permalink
Vectorize shading (#64)
Browse files Browse the repository at this point in the history
* Start testing multiple shading conditions to establish baseline

* Started implementing direct shading timeseries

* Cleaning up and preparing ground work

* Test failing because of matplotlib, fixing

* Update docstrings
  • Loading branch information
anomam authored Aug 15, 2019
1 parent 81871aa commit af202db
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 25 deletions.
108 changes: 83 additions & 25 deletions pvfactors/geometry/pvarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def __init__(self, axis_azimuth=None, gcr=None, pvrow_height=None,
self.n_states = None
self.has_direct_shading = None
self.surface_tilt = None
self.shaded_length_front = None
self.shaded_length_back = None

# These attributes will be transformed at each iteration
self.pvrows = None
Expand Down Expand Up @@ -162,13 +164,47 @@ def fit(self, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth):
"""

self.n_states = len(solar_zenith)
# Save surface tilt angles
self.surface_tilt = surface_tilt
# Calculate rotation angles
rotation_vec = _get_rotation_from_tilt_azimuth(
surface_azimuth, self.axis_azimuth, surface_tilt)

# Calculate the solar 2D vectors for all timestamps
self.solar_2d_vectors = _get_solar_2d_vectors(
solar_zenith, solar_azimuth, self.axis_azimuth)
# Calculate the angle made by 2D sun vector and x-axis
alpha_vec = np.arctan2(self.solar_2d_vectors[1],
self.solar_2d_vectors[0])

# Calculate the coordinates of all PV rows for all timestamps
self._calculate_pvrow_elements_coords(surface_tilt, surface_azimuth,
alpha_vec, rotation_vec)

# Calculate ground elements coordinates for all timestamps
self._calculate_ground_elements_coords(alpha_vec, rotation_vec)

def _calculate_pvrow_elements_coords(self, surface_tilt, surface_azimuth,
alpha_vec, rotation_vec):
"""Calculate PV row coordinate elements in a vectorized way, such as
PV row boundary coordinates and shaded lengths.
Parameters
----------
surface_tilt : array-like or float
Surface tilt angles, from 0 to 180 [deg]
surface_azimuth : array-like or float
Surface azimuth angles [deg]
alpha_vec : array-like or float
Angle made by 2d solar vector and x-axis [rad]
rotation_vec : array-like or float
Rotation angle of the PV rows [deg]
"""

# Calculate interrow direct shading lengths
self._calculate_interrow_shading_vec(alpha_vec, rotation_vec)

# Calculate coordinates of segments of each pv row side
xy_centers = [(X_ORIGIN_PVROWS + idx * self.distance,
self.height + self.y_ground)
for idx in range(self.n_pvrows)]
Expand All @@ -179,16 +215,47 @@ def fit(self, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth):
self.axis_azimuth))
self.pvrow_coords = np.array(self.pvrow_coords)

# Calculate ground elements coordinates
self._calculate_ground_elements_coords(surface_tilt, surface_azimuth)
def _calculate_interrow_shading_vec(self, alpha_vec, rotation_vec):
"""Calculate the shaded length on front and back side of PV rows when
direct shading happens, and in a vectorized way.
Parameters
----------
alpha_vec : array-like or float
Angle made by 2d solar vector and x-axis [rad]
rotation_vec : array-like or float
Rotation angle of the PV rows [deg]
"""

# Determine when there's direct shading
self.has_direct_shading = np.zeros(self.n_states, dtype=bool)
if self.n_pvrows > 1:
# If the shadows are crossing (or close), there's direct shading
self.has_direct_shading = (
self.ground_shadow_coords[1][0][0] + DISTANCE_TOLERANCE
< self.ground_shadow_coords[0][1][0])
# Calculate intermediate values for direct shading
alpha_vec_deg = np.rad2deg(alpha_vec)
theta_t = 90. - rotation_vec
theta_t_rad = np.deg2rad(theta_t)
beta = theta_t + alpha_vec_deg
beta_rad = np.deg2rad(beta)
delta = self.distance * (
np.sin(theta_t_rad) - np.cos(theta_t_rad) * np.tan(beta_rad))
# Calculate temporary shaded lengths
tmp_shaded_length_front = np.maximum(0, self.width - delta)
tmp_shaded_length_back = np.maximum(0, self.width + delta)
# The shaded length can't be longer than PV row (meaning sun can't
# be under the horizon or something...)
self.shaded_length_front = np.where(
tmp_shaded_length_front > self.width, 0,
tmp_shaded_length_front)
self.shaded_length_back = np.where(
tmp_shaded_length_back > self.width, 0,
tmp_shaded_length_back)
else:
# Since there's 1 row, there can't be any direct shading
self.shaded_length_front = np.zeros(self.n_states)
self.shaded_length_back = np.zeros(self.n_states)

# Flag times when there's direct shading
self.has_direct_shading = (
(self.shaded_length_front > DISTANCE_TOLERANCE)
| (self.shaded_length_back > DISTANCE_TOLERANCE))

def transform(self, idx):
"""
Expand Down Expand Up @@ -245,7 +312,7 @@ def transform(self, idx):
idx, self.n_states - 1)
raise PVFactorsError(msg)

def _calculate_ground_elements_coords(self, surface_tilt, surface_azimuth):
def _calculate_ground_elements_coords(self, alpha_vec, rotation_vec):
"""This private method is run at fitting time to calculate the ground
element coordinates: i.e. the coordinates of the ground shadows and
cut points.
Expand All @@ -254,18 +321,12 @@ def _calculate_ground_elements_coords(self, surface_tilt, surface_azimuth):
Parameters
----------
surface_tilt : array-like or float
Surface tilt angles, from 0 to 180 [deg]
surface_azimuth : array-like or float
Surface azimuth angles [deg]
alpha_vec : array-like or float
Angle made by 2d solar vector and x-axis [rad]
rotation_vec : array-like or float
Rotation angle of the PV rows [deg]
"""

# Calculate the angle made by 2D sun vector and x-axis
alpha_vec = np.arctan2(self.solar_2d_vectors[1],
self.solar_2d_vectors[0])
# Calculate rotation angles
rotation_vec = _get_rotation_from_tilt_azimuth(
surface_azimuth, self.axis_azimuth, surface_tilt)
rotation_vec = np.deg2rad(rotation_vec)
# Calculate coords of ground shadows and cutting points
for pvrow_coord in self.pvrow_coords:
Expand Down Expand Up @@ -465,8 +526,7 @@ def _build_view_matrix(self):
gnd_that_front_sees = []
gnd_that_back_sees = []
for idx_gnd, gnd_surface in enumerate(ground_surfaces):
gnd_surface_on_the_right = \
gnd_centroids[idx_gnd].x > edge_point.x
gnd_surface_on_the_right = gnd_centroids[idx_gnd].x > edge_point.x
if gnd_surface_on_the_right:
gnd_that_front_sees.append(gnd_surface.index)
else:
Expand All @@ -478,8 +538,7 @@ def _build_view_matrix(self):
if len(gnd_that_back_sees):
# PVRow <---> Ground: update views
view_matrix[back_indices[:, np.newaxis],
gnd_that_back_sees] = \
VIEW_DICT["back_gnd_obst"]
gnd_that_back_sees] = VIEW_DICT["back_gnd_obst"]
view_matrix[gnd_that_back_sees[:, np.newaxis],
back_indices] = VIEW_DICT["gnd_back_obst"]
# PVRow <---> Ground: obstruction
Expand All @@ -490,8 +549,7 @@ def _build_view_matrix(self):
if len(gnd_that_front_sees):
# PVRow <---> Ground: update views
view_matrix[front_indices[:, np.newaxis],
gnd_that_front_sees] = \
VIEW_DICT["front_gnd_obst"]
gnd_that_front_sees] = VIEW_DICT["front_gnd_obst"]
view_matrix[gnd_that_front_sees[:, np.newaxis],
front_indices] = VIEW_DICT["gnd_front_obst"]
# PVRow <---> Ground: obstruction
Expand Down
41 changes: 41 additions & 0 deletions pvfactors/tests/test_geometry/test_pvarray.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import numpy as np
import pandas as pd
from pvfactors.geometry import OrderedPVArray, PVGround, PVSurface
from pvfactors.geometry.utils import contains
from pvfactors.config import MAX_X_GROUND, MIN_X_GROUND
Expand Down Expand Up @@ -611,3 +612,43 @@ def test_ordered_pvarray_from_dict_w_direct_shading():
distance_between_pvrows = \
pvarray.pvrows[1].centroid.x - pvarray.pvrows[0].centroid.x
assert distance_between_pvrows == 2.5


def test_ordered_pvarray_direct_shading():
"""Test that direct shading is calculated correctly in the following
5 situations:
- PV rows tilted to the left and front side shading
- PV rows tilted to the right and front side shading
- PV rows tilted to the left and back side shading
- PV rows tilted to the right and back side shading
- no shading
"""
# Base params
params = {
'n_pvrows': 3,
'pvrow_height': 1,
'pvrow_width': 1,
'axis_azimuth': 0.,
'gcr': 0.5
}
# Timeseries inputs
df_inputs = pd.DataFrame({
'solar_zenith': [70., 80., 80., 70., 10.],
'solar_azimuth': [270., 90., 270., 90., 90.],
'surface_tilt': [45., 45., 45., 45., 45.],
'surface_azimuth': [270., 270., 90., 90., 90.]})

# Initialize and fit pv array
pvarray = OrderedPVArray.init_from_dict(params)
# Fit pv array to timeseries data
pvarray.fit(df_inputs.solar_zenith, df_inputs.solar_azimuth,
df_inputs.surface_tilt, df_inputs.surface_azimuth)

expected_ts_front_shading = [0.24524505, 0., 0., 0.24524505, 0.]
expected_ts_back_shading = [0., 0.39450728, 0.39450728, 0., 0.]

# Test that timeseries shading calculated correctly
np.testing.assert_allclose(expected_ts_front_shading,
pvarray.shaded_length_front)
np.testing.assert_allclose(expected_ts_back_shading,
pvarray.shaded_length_back)

0 comments on commit af202db

Please sign in to comment.