"""Classes for implementation of ground geometry"""
from pvfactors import PVFactorsError
from pvfactors.config import (
MAX_X_GROUND, MIN_X_GROUND, Y_GROUND, DISTANCE_TOLERANCE, COLOR_DIC)
from pvfactors.geometry.base import (
BaseSide, PVSegment, ShadeCollection, PVSurface)
from pvfactors.geometry.timeseries import (
TsShadeCollection, TsLineCoords, TsPointCoords, TsSurface,
_get_params_at_idx)
from shapely.geometry import LineString
import numpy as np
from copy import deepcopy
[docs]class TsGround(object):
"""Timeseries ground class: this class is a vectorized version of the
PV ground geometry class, and it will store timeseries shaded ground
and illuminated ground elements, as well as pv row cut points."""
# TODO: this needs to be passed at initialization for flexibility
x_min = MIN_X_GROUND
x_max = MAX_X_GROUND
[docs] def __init__(self, shadow_elements, illum_elements, param_names=None,
flag_overlap=None, cut_point_coords=None, y_ground=None):
"""Initialize timeseries ground using list of timeseries surfaces
for the ground shadows
Parameters
----------
shadow_elements : \
list of :py:class:`~pvfactors.geometry.pvground.TsGroundElement`
Timeseries shaded ground elements
illum_elements : \
list of :py:class:`~pvfactors.geometry.pvground.TsGroundElement`
Timeseries illuminated ground elements
param_names : list of str, optional
List of names of surface parameters to use when creating geometries
(Default = None)
flag_overlap : list of bool, optional
Flags indicating if the ground shadows are overlapping, for all
time steps (Default=None). I.e. is there direct shading on pv rows?
cut_point_coords : \
list of :py:class:`~pvfactors.geometry.timeseries.TsPointCoords`, \
optional
List of cut point coordinates, as calculated for timeseries PV rows
(Default = None)
y_ground : float, optional
Y coordinate of flat ground [m] (Default=None)
"""
# Lists of timeseries ground elements
self.shadow_elements = shadow_elements
self.illum_elements = illum_elements
# Shade collections
list_shaded_surf = []
list_illum_surf = []
for shadow_el in shadow_elements:
list_shaded_surf += shadow_el.all_ts_surfaces
for illum_el in illum_elements:
list_illum_surf += illum_el.all_ts_surfaces
self.illum = TsShadeCollection(list_illum_surf, False)
self.shaded = TsShadeCollection(list_shaded_surf, True)
# Other ground attributes
self.param_names = [] if param_names is None else param_names
self.flag_overlap = flag_overlap
self.cut_point_coords = [] if cut_point_coords is None \
else cut_point_coords
self.y_ground = y_ground
self.shaded_params = dict.fromkeys(self.param_names)
self.illum_params = dict.fromkeys(self.param_names)
@classmethod
def from_ts_pvrows_and_angles(cls, list_ts_pvrows, alpha_vec, rotation_vec,
y_ground=Y_GROUND, flag_overlap=None,
param_names=None):
"""Create timeseries ground from list of timeseries PV rows, and
PV array and solar angles.
Parameters
----------
list_ts_pvrows : \
list of :py:class:`~pvfactors.geometry.pvrow.TsPVRow`
Timeseries PV rows to use to calculate timeseries ground shadows
alpha_vec : np.ndarray
Angle made by 2d solar vector and PV array x-axis [rad]
rotation_vec : np.ndarray
Timeseries rotation values of the PV row [deg]
y_ground : float, optional
Fixed y coordinate of flat ground [m] (Default = Y_GROUND constant)
flag_overlap : list of bool, optional
Flags indicating if the ground shadows are overlapping, for all
time steps (Default=None). I.e. is there direct shading on pv rows?
param_names : list of str, optional
List of names of surface parameters to use when creating geometries
(Default = None)
"""
rotation_vec = np.deg2rad(rotation_vec)
n_steps = len(rotation_vec)
# Calculate coords of ground shadows and cutting points
ground_shadow_coords = []
cut_point_coords = []
for ts_pvrow in list_ts_pvrows:
# Get pvrow coords
x1s_pvrow = ts_pvrow.full_pvrow_coords.b1.x
y1s_pvrow = ts_pvrow.full_pvrow_coords.b1.y
x2s_pvrow = ts_pvrow.full_pvrow_coords.b2.x
y2s_pvrow = ts_pvrow.full_pvrow_coords.b2.y
# --- Shadow coords calculation
# Calculate x coords of shadow
x1s_shadow = x1s_pvrow - (y1s_pvrow - y_ground) / np.tan(alpha_vec)
x2s_shadow = x2s_pvrow - (y2s_pvrow - y_ground) / np.tan(alpha_vec)
# Order x coords from left to right
x1s_on_left = x1s_shadow <= x2s_shadow
xs_left_shadow = np.where(x1s_on_left, x1s_shadow, x2s_shadow)
xs_right_shadow = np.where(x1s_on_left, x2s_shadow, x1s_shadow)
# Append shadow coords to list
ground_shadow_coords.append(
[[xs_left_shadow, y_ground * np.ones(n_steps)],
[xs_right_shadow, y_ground * np.ones(n_steps)]])
# --- Cutting points coords calculation
dx = (y1s_pvrow - y_ground) / np.tan(rotation_vec)
cut_point_coords.append(
TsPointCoords(x1s_pvrow - dx, y_ground * np.ones(n_steps)))
ground_shadow_coords = np.array(ground_shadow_coords)
return cls.from_ordered_shadows_coords(
ground_shadow_coords, flag_overlap=flag_overlap,
cut_point_coords=cut_point_coords, param_names=param_names,
y_ground=y_ground)
@classmethod
def from_ordered_shadows_coords(cls, shadow_coords, flag_overlap=None,
param_names=None, cut_point_coords=None,
y_ground=Y_GROUND):
"""Create timeseries ground from list of ground shadow coordinates.
Parameters
----------
shadow_coords : np.ndarray
List of ordered ground shadow coordinates (from left to right)
flag_overlap : list of bool, optional
Flags indicating if the ground shadows are overlapping, for all
time steps (Default=None). I.e. is there direct shading on pv rows?
param_names : list of str, optional
List of names of surface parameters to use when creating geometries
(Default = None)
cut_point_coords : \
list of :py:class:`~pvfactors.geometry.timeseries.TsPointCoords`, \
optional
List of cut point coordinates, as calculated for timeseries PV rows
(Default = None)
y_ground : float, optional
Fixed y coordinate of flat ground [m] (Default = Y_GROUND constant)
"""
# Get cut point coords if any
cut_point_coords = cut_point_coords or []
# Create shadow coordinate objects
list_shadow_coords = [TsLineCoords.from_array(coords)
for coords in shadow_coords]
# If the overlap flags were passed, make sure shadows don't overlap
if flag_overlap is not None:
if len(list_shadow_coords) > 1:
for idx, coords in enumerate(list_shadow_coords[:-1]):
coords.b2.x = np.where(flag_overlap,
list_shadow_coords[idx + 1].b1.x,
coords.b2.x)
# Create shaded ground elements
ts_shadows_elements = cls._shadow_elements_from_coords_and_cut_pts(
list_shadow_coords, cut_point_coords, param_names)
# Create illuminated ground elements
ts_illum_elements = cls._illum_elements_from_coords_and_cut_pts(
ts_shadows_elements, cut_point_coords, param_names, y_ground)
return cls(ts_shadows_elements, ts_illum_elements,
param_names=param_names, flag_overlap=flag_overlap,
cut_point_coords=cut_point_coords, y_ground=y_ground)
def at(self, idx, x_min_max=None, merge_if_flag_overlap=True,
with_cut_points=True):
"""Generate a PV ground geometry for the desired index. This will
only return non-point surfaces within the ground bounds, i.e.
surfaces that are not points, and which are within x_min and x_max.
Parameters
----------
idx : int
Index to use to generate PV ground geometry
x_min_max : tuple, optional
List of minimum and maximum x coordinates for the flat surface [m]
(Default = None)
merge_if_flag_overlap : bool, optional
Decide whether to merge all shadows if they overlap or not
(Default = True)
with_cut_points : bool, optional
Decide whether to include the saved cut points in the created
PV ground geometry (Default = True)
Returns
-------
pvground : :py:class:`~pvfactors.geometry.pvground.PVGround`
"""
# Get shadow elements that are not points at the given index
non_pt_shadow_elements = [
shadow_el for shadow_el in self.shadow_elements
if shadow_el.coords.length[idx] > DISTANCE_TOLERANCE]
if with_cut_points:
# We want the ground surfaces broken up at the cut points
if merge_if_flag_overlap:
# We want to merge the shadow surfaces when they overlap
list_shadow_surfaces = self._merge_shadow_surfaces(
idx, non_pt_shadow_elements)
else:
# No need to merge the shadow surfaces
list_shadow_surfaces = []
for shadow_el in non_pt_shadow_elements:
list_shadow_surfaces += \
shadow_el.non_point_surfaces_at(idx)
# Get the illuminated surfaces
list_illum_surfaces = []
for illum_el in self.illum_elements:
list_illum_surfaces += illum_el.non_point_surfaces_at(idx)
else:
# No need to break up the surfaces at the cut points
# We will need to build up new surfaces (since not done by classes)
# Get the parameters at the given index
illum_params = _get_params_at_idx(idx, self.illum_params)
shaded_params = _get_params_at_idx(idx, self.shaded_params)
if merge_if_flag_overlap and (self.flag_overlap is not None):
# We want to merge the shadow surfaces when they overlap
is_overlap = self.flag_overlap[idx]
if is_overlap and (len(non_pt_shadow_elements) > 1):
coords = [non_pt_shadow_elements[0].b1.at(idx),
non_pt_shadow_elements[-1].b2.at(idx)]
list_shadow_surfaces = [PVSurface(
coords, shaded=True, param_names=self.param_names,
params=shaded_params)]
else:
# No overlap for the given index or config
list_shadow_surfaces = [
PVSurface(shadow_el.coords.at(idx),
shaded=True, params=shaded_params,
param_names=self.param_names)
for shadow_el in non_pt_shadow_elements
if shadow_el.coords.length[idx]
> DISTANCE_TOLERANCE]
else:
# No need to merge the shadow surfaces
list_shadow_surfaces = [
PVSurface(shadow_el.coords.at(idx),
shaded=True, params=shaded_params,
param_names=self.param_names)
for shadow_el in non_pt_shadow_elements
if shadow_el.coords.length[idx]
> DISTANCE_TOLERANCE]
# Get the illuminated surfaces
list_illum_surfaces = [PVSurface(illum_el.coords.at(idx),
shaded=False, params=illum_params,
param_names=self.param_names)
for illum_el in self.illum_elements
if illum_el.coords.length[idx]
> DISTANCE_TOLERANCE]
# Pass the created lists to the PVGround builder
return PVGround.from_lists_surfaces(
list_shadow_surfaces, list_illum_surfaces,
param_names=self.param_names, y_ground=self.y_ground,
x_min_max=x_min_max)
def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'],
color_illum=COLOR_DIC['pvrow_illum'], x_min_max=None,
merge_if_flag_overlap=True, with_cut_points=True,
with_surface_index=False):
"""Plot timeseries ground at a certain index.
Parameters
----------
idx : int
Index to use to plot timeseries side
ax : :py:class:`matplotlib.pyplot.axes` object
Axes for plotting
color_shaded : str, optional
Color to use for plotting the shaded surfaces (Default =
COLOR_DIC['pvrow_shaded'])
color_shaded : str, optional
Color to use for plotting the illuminated surfaces (Default =
COLOR_DIC['pvrow_illum'])
x_min_max : tuple, optional
List of minimum and maximum x coordinates for the flat surface [m]
(Default = None)
merge_if_flag_overlap : bool, optional
Decide whether to merge all shadows if they overlap or not
(Default = True)
with_cut_points : bool, optional
Decide whether to include the saved cut points in the created
PV ground geometry (Default = True)
with_surface_index : bool, optional
Plot the surfaces with their index values (Default = False)
"""
pvground = self.at(idx, x_min_max=x_min_max,
merge_if_flag_overlap=merge_if_flag_overlap,
with_cut_points=with_cut_points)
pvground.plot(ax, color_shaded=color_shaded, color_illum=color_illum,
with_index=with_surface_index)
def update_params(self, new_dict):
"""Update the illuminated parameters with new ones, not only for the
timeseries ground, but also for its ground elements and the timeseries
surfaces of the ground elements, so that they are all synced.
Parameters
----------
new_dict : dict
New parameters
"""
self.update_illum_params(new_dict)
self.update_shaded_params(new_dict)
def update_illum_params(self, new_dict):
"""Update the illuminated parameters with new ones, not only for the
timeseries ground, but also for its ground elements and the timeseries
surfaces of the ground elements, so that they are all synced.
Parameters
----------
new_dict : dict
New parameters
"""
self.illum_params.update(new_dict)
for illum_el in self.illum_elements:
illum_el.params.update(new_dict)
for surf in illum_el.surface_list:
surf.params.update(new_dict)
def update_shaded_params(self, new_dict):
"""Update the shaded parameters with new ones, not only for the
timeseries ground, but also for its ground elements and the timeseries
surfaces of the ground elements, so that they are all synced.
Parameters
----------
new_dict : dict
New parameters
"""
self.shaded_params.update(new_dict)
for shaded_el in self.shadow_elements:
shaded_el.params.update(new_dict)
for surf in shaded_el.surface_list:
surf.params.update(new_dict)
def get_param_weighted(self, param):
"""Get timeseries parameter for the ts ground, after weighting by
surface length.
Parameters
----------
param : str
Name of parameter
Returns
-------
np.ndarray
Weighted parameter values
"""
return self.get_param_ww(param) / self.length
def get_param_ww(self, param):
"""Get timeseries parameter from the ground's surfaces with weight,
i.e. after multiplying by the surface lengths.
Parameters
----------
param: str
Surface parameter to return
Returns
-------
np.ndarray
Timeseries parameter values multiplied by weights
Raises
------
KeyError
if parameter name not in a surface parameters
"""
value = 0.
for shadow_el in self.shadow_elements:
value += shadow_el.get_param_ww(param)
for illum_el in self.illum_elements:
value += illum_el.get_param_ww(param)
return value
def shadow_coords_left_of_cut_point(self, idx_cut_pt):
"""Get coordinates of shadows located on the left side of the cut point
with given index. The coordinates of the shadows will be bounded
by the coordinates of the cut point and the default minimum
ground x values.
Parameters
----------
idx_cut_pt : int
Index of the cut point of interest
Returns
-------
list of :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Coordinates of the shadows on the left side of the cut point
"""
cut_pt_coords = self.cut_point_coords[idx_cut_pt]
return [shadow_el._coords_left_of_cut_point(shadow_el.coords,
cut_pt_coords)
for shadow_el in self.shadow_elements]
def shadow_coords_right_of_cut_point(self, idx_cut_pt):
"""Get coordinates of shadows located on the right side of the cut
point with given index. The coordinates of the shadows will be bounded
by the coordinates of the cut point and the default maximum
ground x values.
Parameters
----------
idx_cut_pt : int
Index of the cut point of interest
Returns
-------
list of :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Coordinates of the shadows on the right side of the cut point
"""
cut_pt_coords = self.cut_point_coords[idx_cut_pt]
return [shadow_el._coords_right_of_cut_point(shadow_el.coords,
cut_pt_coords)
for shadow_el in self.shadow_elements]
def ts_surfaces_side_of_cut_point(self, side, idx_cut_pt):
"""Get a list of all the ts ground surfaces an a request side of
a cut point
Parameters
----------
side : str
Side of the cut point, either 'left' or 'right'
idx_cut_pt : int
Index of the cut point, on whose side we want to get the ground
surfaces
Returns
-------
list
List of timeseries ground surfaces on the side of the cut point
"""
list_ts_surfaces = []
for shadow_el in self.shadow_elements:
list_ts_surfaces += shadow_el.surface_dict[idx_cut_pt][side]
for illum_el in self.illum_elements:
list_ts_surfaces += illum_el.surface_dict[idx_cut_pt][side]
return list_ts_surfaces
@property
def n_ts_surfaces(self):
"""Number of timeseries surfaces in the ts ground"""
return self.n_ts_shaded_surfaces + self.n_ts_illum_surfaces
@property
def n_ts_shaded_surfaces(self):
"""Number of shaded timeseries surfaces in the ts ground"""
n_ts_surfaces = 0
for shadow_el in self.shadow_elements:
n_ts_surfaces += shadow_el.n_ts_surfaces
return n_ts_surfaces
@property
def n_ts_illum_surfaces(self):
"""Number of illuminated timeseries surfaces in the ts ground"""
n_ts_surfaces = 0
for illum_el in self.illum_elements:
n_ts_surfaces += illum_el.n_ts_surfaces
return n_ts_surfaces
@property
def all_ts_surfaces(self):
"""Number of timeseries surfaces in the ts ground"""
all_ts_surfaces = []
for shadow_el in self.shadow_elements:
all_ts_surfaces += shadow_el.all_ts_surfaces
for illum_el in self.illum_elements:
all_ts_surfaces += illum_el.all_ts_surfaces
return all_ts_surfaces
@property
def length(self):
"""Length of the timeseries ground"""
length = 0
for shadow_el in self.shadow_elements:
length += shadow_el.length
for illum_el in self.illum_elements:
length += illum_el.length
return length
@property
def shaded_length(self):
"""Length of the timeseries ground"""
length = 0
for shadow_el in self.shadow_elements:
length += shadow_el.length
return length
def non_point_shaded_surfaces_at(self, idx):
"""Return a list of shaded surfaces, that are not points
at given index
Parameters
----------
idx : int
Index at which we want the surfaces not to be points
Returns
-------
list of :py:class:`~pvfactors.geometry.base.PVSurface`
"""
list_surfaces = []
for shadow_el in self.shadow_elements:
list_surfaces += shadow_el.non_point_surfaces_at(0)
return list_surfaces
def non_point_illum_surfaces_at(self, idx):
"""Return a list of illuminated surfaces, that are not
points at given index
Parameters
----------
idx : int
Index at which we want the surfaces not to be points
Returns
-------
list of :py:class:`~pvfactors.geometry.base.PVSurface`
"""
list_surfaces = []
for illum_el in self.illum_elements:
list_surfaces += illum_el.non_point_surfaces_at(0)
return list_surfaces
def non_point_surfaces_at(self, idx):
"""Return a list of all surfaces that are not
points at given index
Parameters
----------
idx : int
Index at which we want the surfaces not to be points
Returns
-------
list of :py:class:`~pvfactors.geometry.base.PVSurface`
"""
return self.non_point_illum_surfaces_at(idx) \
+ self.non_point_shaded_surfaces_at(idx)
def n_non_point_surfaces_at(self, idx):
"""Return the number of :py:class:`~pvfactors.geometry.base.PVSurface`
that are not points at given index
Parameters
----------
idx : int
Index at which we want the surfaces not to be points
Returns
-------
int
"""
return len(self.non_point_surfaces_at(idx))
@staticmethod
def _shadow_elements_from_coords_and_cut_pts(
list_shadow_coords, cut_point_coords, param_names):
"""Create ground shadow elements from a list of ordered shadow
coordinates (from left to right), and the ground cut point coordinates.
Notes
-----
This method will clip the shadow coords to the limit of ground,
i.e. the shadow coordinates shouldn't be outside of the range
[MIN_X_GROUND, MAX_X_GROUND].
Parameters
----------
list_shadow_coords : \
list of :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
List of ordered ground shadow coordinates (from left to right)
cut_point_coords : \
list of :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
List of cut point coordinates (from left to right)
param_names : list
List of parameter names for the ground elements
Returns
-------
list_shadow_elements : \
list of :py:class:`~pvfactors.geometry.pvground.TsGroundElement`
Ordered list of shadow elements (from left to right)
"""
list_shadow_elements = []
# FIXME: x_min and x_max should be passed as inputs
for shadow_coords in list_shadow_coords:
shadow_coords.b1.x = np.clip(shadow_coords.b1.x, MIN_X_GROUND,
MAX_X_GROUND)
shadow_coords.b2.x = np.clip(shadow_coords.b2.x, MIN_X_GROUND,
MAX_X_GROUND)
list_shadow_elements.append(
TsGroundElement(shadow_coords,
list_ordered_cut_pts_coords=cut_point_coords,
param_names=param_names, shaded=True))
return list_shadow_elements
@staticmethod
def _illum_elements_from_coords_and_cut_pts(
list_shadow_elements, cut_pt_coords, param_names, y_ground):
"""Create ground illuminated elements from a list of ordered shadow
elements (from left to right), and the ground cut point coordinates.
This method will make sure that the illuminated ground elements are
all within the ground limits [MIN_X_GROUND, MAX_X_GROUND].
Parameters
----------
list_shadow_coords : \
list of :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
List of ordered ground shadow coordinates (from left to right)
cut_point_coords : \
list of :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
List of cut point coordinates (from left to right)
param_names : list
List of parameter names for the ground elements
Returns
-------
list_shadow_elements : \
list of :py:class:`~pvfactors.geometry.pvground.TsGroundElement`
Ordered list of shadow elements (from left to right)
"""
list_illum_elements = []
if len(list_shadow_elements) == 0:
msg = """There must be at least one shadow element on the ground,
otherwise it probably means that no PV rows were created, so
there's no point in running a simulation..."""
raise PVFactorsError(msg)
n_steps = len(list_shadow_elements[0].coords.b1.x)
y_ground_vec = y_ground * np.ones(n_steps)
# FIXME: x_min and x_max should be passed as inputs
next_x = MIN_X_GROUND * np.ones(n_steps)
# Build the groud elements from left to right, starting at x_min
# and covering the ground with illuminated elements where there's no
# shadow
for shadow_element in list_shadow_elements:
x1 = next_x
x2 = shadow_element.coords.b1.x
coords = TsLineCoords.from_array(
np.array([[x1, y_ground_vec], [x2, y_ground_vec]]))
list_illum_elements.append(TsGroundElement(
coords, list_ordered_cut_pts_coords=cut_pt_coords,
param_names=param_names, shaded=False))
next_x = shadow_element.coords.b2.x
# Add the last illuminated element to the list
coords = TsLineCoords.from_array(
np.array([[next_x, y_ground_vec],
[MAX_X_GROUND * np.ones(n_steps), y_ground_vec]]))
list_illum_elements.append(TsGroundElement(
coords, list_ordered_cut_pts_coords=cut_pt_coords,
param_names=param_names, shaded=False))
return list_illum_elements
def _merge_shadow_surfaces(self, idx, non_pt_shadow_elements):
"""Merge the shadow surfaces in a list of shadow elements
at the shadow boundaries only, at a given index, but keep the shadow
surfaces broken up at the cut points.
Parameters
----------
idx : int
Index at which we want to merge the surfaces
non_pt_shadow_elements : \
list of :py:class:`~pvfactors.geometry.pvground.TsGroundElement`
List of non point shadow elements
Returns
-------
list_shadow_surfaces : \
list of :py:class:`~pvfactors.geometry.base.PVSurface`
List of shadow surfaces at a given index
(ordered from left to right)
"""
# TODO: check if it would be faster to merge the ground elements first,
# and then break it down with the cut points
# Decide whether to merge all shadows or not
list_shadow_surfaces = []
if self.flag_overlap is not None:
# Get the overlap flags
is_overlap = self.flag_overlap[idx]
n_shadow_elements = len(non_pt_shadow_elements)
if is_overlap and (n_shadow_elements > 1):
# If there's only one shadow, not point in going through this
# Now go from left to right and merge shadow surfaces
surface_to_merge = None
for i_el, shadow_el in enumerate(non_pt_shadow_elements):
surfaces = shadow_el.non_point_surfaces_at(idx)
n_surf = len(surfaces)
for i_surf, surface in enumerate(surfaces):
if i_surf == n_surf - 1:
# last surface, could also be first
if i_surf == 0:
# Need to merge with preceding if exists
if surface_to_merge is not None:
coords = [surface_to_merge.boundary[0],
surface.boundary[1]]
surface = PVSurface(
coords, shaded=True,
param_names=self.param_names,
params=surface.params,
index=surface.index)
if i_el == n_shadow_elements - 1:
# last surface of last shadow element
list_shadow_surfaces.append(surface)
else:
# keep for merging with next element
surface_to_merge = surface
elif i_surf == 0:
# first surface but definitely not last either
if surface_to_merge is not None:
coords = [surface_to_merge.boundary[0],
surface.boundary[1]]
list_shadow_surfaces.append(
PVSurface(coords, shaded=True,
param_names=self.param_names,
params=surface.params,
index=surface.index))
else:
list_shadow_surfaces.append(surface)
else:
# not first nor last surface
list_shadow_surfaces.append(surface)
else:
# There's no need to merge anything
for shadow_el in non_pt_shadow_elements:
list_shadow_surfaces += \
shadow_el.non_point_surfaces_at(idx)
else:
# There's no need to merge anything
for shadow_el in non_pt_shadow_elements:
list_shadow_surfaces += shadow_el.non_point_surfaces_at(idx)
return list_shadow_surfaces
[docs]class TsGroundElement(object):
"""Special class for timeseries ground elements: a ground element has known
timeseries coordinate boundaries, but it will also have a break down of
its area into n+1 timeseries surfaces located in the n+1 ground zones
defined by the n ground cutting points.
This is crucial to calculate view factors in a vectorized way."""
[docs] def __init__(self, coords, list_ordered_cut_pts_coords=None,
param_names=None, shaded=False):
"""Initialize the timeseries ground element using its timeseries
line coordinates, and build the timeseries surfaces for all the
cut point zones.
Parameters
----------
coords : :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Timeseries line coordinates of the ground element
list_ordered_cut_pts_coords : list, optional
List of all the cut point timeseries coordinates
(Default = [])
param_names : list of str, optional
List of names of surface parameters to use when creating geometries
(Default = None)
shaded : bool, optional
Flag specifying is element is a shadow or not (Default = False)
"""
self.coords = coords
self.param_names = param_names or []
self.params = dict.fromkeys(self.param_names)
self.shaded = shaded
self.surface_dict = None # will be necessary for view factor calcs
self.surface_list = [] # will be necessary for vf matrix formation
list_ordered_cut_pts_coords = list_ordered_cut_pts_coords or []
if len(list_ordered_cut_pts_coords) > 0:
self._create_all_ts_surfaces(list_ordered_cut_pts_coords)
self.n_ts_surfaces = len(self.surface_list)
@property
def b1(self):
"""Timeseries coordinates of first boundary point"""
return self.coords.b1
@property
def b2(self):
"""Timeseries coordinates of second boundary point"""
return self.coords.b2
@property
def centroid(self):
"""Timeseries point coordinates of the element's centroid"""
return self.coords.centroid
@property
def length(self):
"""Timeseries length of the ground"""
return self.coords.length
@property
def all_ts_surfaces(self):
"""List of all ts surfaces making up the ts ground element"""
return self.surface_list
def surfaces_at(self, idx):
"""Return list of surfaces (from left to right) at given index that
make up the ground element.
Parameters
----------
idx : int
Index of interest
Returns
-------
list of :py:class:`~pvfactors.geometry.base.PVSurface`
"""
return [surface.at(idx)
for surface in self.surface_list]
def non_point_surfaces_at(self, idx):
"""Return list of non-point surfaces (from left to right) at given
index that make up the ground element.
Parameters
----------
idx : int
Index of interest
Returns
-------
list of :py:class:`~pvfactors.geometry.base.PVSurface`
"""
return [surface.at(idx)
for surface in self.surface_list
if surface.length[idx] > DISTANCE_TOLERANCE]
def get_param_weighted(self, param):
"""Get timeseries parameter for the ground element, after weighting by
surface length.
Parameters
----------
param : str
Name of parameter
Returns
-------
np.ndarray
Weighted parameter values
"""
return self.get_param_ww(param) / self.length
def get_param_ww(self, param):
"""Get timeseries parameter from the ground element with weight,
i.e. after multiplying by the surface lengths.
Parameters
----------
param: str
Surface parameter to return
Returns
-------
np.ndarray
Timeseries parameter values multiplied by weights
Raises
------
KeyError
if parameter name not in a surface parameters
"""
value = 0.
for ts_surf in self.surface_list:
value += ts_surf.length * ts_surf.get_param(param)
return value
def _create_all_ts_surfaces(self, list_ordered_cut_pts):
"""Create all the n+1 timeseries surfaces that make up the timeseries
ground element, and which are located in the n+1 zones defined by
the n cut points.
Parameters
----------
list_ordered_cut_pts : list of :py:class:`~pvfactors.geometry.timeseries.TsPointCoords`
List of timeseries coordinates of all cut points, ordered from
left to right
"""
# Initialize dict
self.surface_dict = {i: {'right': [], 'left': []}
for i in range(len(list_ordered_cut_pts))}
n_cut_pts = len(list_ordered_cut_pts)
next_coords = self.coords
for idx_pt, cut_pt_coords in enumerate(list_ordered_cut_pts):
# Get coords on left of cut pt
coords_left = self._coords_left_of_cut_point(next_coords,
cut_pt_coords)
# Save that surface in the required structures
surface_left = TsSurface(coords_left, param_names=self.param_names,
shaded=self.shaded)
self.surface_list.append(surface_left)
for i in range(idx_pt, n_cut_pts):
self.surface_dict[i]['left'].append(surface_left)
for j in range(0, idx_pt):
self.surface_dict[j]['right'].append(surface_left)
next_coords = self._coords_right_of_cut_point(next_coords,
cut_pt_coords)
# Save the right most portion
next_surface = TsSurface(next_coords, param_names=self.param_names,
shaded=self.shaded)
self.surface_list.append(next_surface)
for j in range(0, n_cut_pts):
self.surface_dict[j]['right'].append(next_surface)
@staticmethod
def _coords_right_of_cut_point(coords, cut_pt_coords):
"""Calculate timeseries line coordinates that are right of the given
cut point coordinates, but still within the ground area
Parameters
----------
coords : :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Original timeseries coordinates
cut_pt_coords :
:py:class:`~pvfactors.geometry.timeseries.TsPointCoords`
Timeseries coordinates of cut point
Returns
-------
:py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Timeseries line coordinates that are located right of the cut
point
"""
coords = deepcopy(coords)
# FIXME: should be using x_min x_max inputs instead of global constant
coords.b1.x = np.maximum(coords.b1.x, cut_pt_coords.x)
coords.b1.x = np.minimum(coords.b1.x, MAX_X_GROUND)
coords.b2.x = np.maximum(coords.b2.x, cut_pt_coords.x)
coords.b2.x = np.minimum(coords.b2.x, MAX_X_GROUND)
return coords
@staticmethod
def _coords_left_of_cut_point(coords, cut_pt_coords):
"""Calculate timeseries line coordinates that are left of the given
cut point coordinates, but still within the ground area
Parameters
----------
coords : :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Original timeseries coordinates
cut_pt_coords :
:py:class:`~pvfactors.geometry.timeseries.TsPointCoords`
Timeseries coordinates of cut point
Returns
-------
:py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Timeseries line coordinates that are located left of the cut
point
"""
coords = deepcopy(coords)
# FIXME: should be using x_min x_max inputs instead of global constant
coords.b1.x = np.minimum(coords.b1.x, cut_pt_coords.x)
coords.b1.x = np.maximum(coords.b1.x, MIN_X_GROUND)
coords.b2.x = np.minimum(coords.b2.x, cut_pt_coords.x)
coords.b2.x = np.maximum(coords.b2.x, MIN_X_GROUND)
return coords
[docs]class PVGround(BaseSide):
"""Class that defines the ground geometry in PV arrays."""
[docs] def __init__(self, list_segments=None, original_linestring=None):
"""Initialize PV ground geometry.
Parameters
----------
list_segments : list of :py:class:`~pvfactors.geometry.base.PVSegment`, optional
List of PV segments that will constitute the ground (Default = [])
original_linestring : :py:class:`shapely.geometry.LineString`, optional
Full continuous linestring that the ground will be made of
(Default = None)
"""
list_segments = list_segments or []
self.original_linestring = original_linestring
super(PVGround, self).__init__(list_segments)
@classmethod
def as_flat(cls, x_min_max=None, shaded=False, y_ground=Y_GROUND,
param_names=None):
"""Build a horizontal flat ground surface, made of 1 PV segment.
Parameters
----------
x_min_max : tuple, optional
List of minimum and maximum x coordinates for the flat surface [m]
(Default = None)
shaded : bool, optional
Shaded status of the created PV surfaces (Default = False)
y_ground : float, optional
Location of flat ground on y axis in [m] (Default = Y_GROUND)
param_names : list of str, optional
Names of the surface parameters, eg reflectivity, total incident
irradiance, temperature, etc. (Default = [])
Returns
-------
PVGround object
"""
param_names = param_names or []
# Get ground boundaries
if x_min_max is None:
x_min, x_max = MIN_X_GROUND, MAX_X_GROUND
else:
x_min, x_max = x_min_max
# Create PV segment for flat ground
coords = [(x_min, y_ground), (x_max, y_ground)]
seg = PVSegment.from_linestring_coords(coords, shaded=shaded,
normal_vector=[0., 1.],
param_names=param_names)
return cls(list_segments=[seg], original_linestring=LineString(coords))
@classmethod
def from_lists_surfaces(
cls, list_shaded_surfaces, list_illum_surfaces, x_min_max=None,
y_ground=Y_GROUND, param_names=None):
"""Create ground from lists of shaded and illuminated PV surfaces.
Parameters
----------
list_shaded_surfaces : \
list of :py:class:`~pvfactors.geometry.base.PVSurface`
List of shaded ground PV surfaces
list_illum_surfaces : \
list of :py:class:`~pvfactors.geometry.base.PVSurface`
List of illuminated ground PV surfaces
x_min_max : tuple, optional
List of minimum and maximum x coordinates for the flat surface [m]
(Default = None)
y_ground : float, optional
Location of flat ground on y axis in [m] (Default = Y_GROUND)
param_names : list of str, optional
Names of the surface parameters, eg reflectivity, total incident
irradiance, temperature, etc. (Default = [])
Returns
-------
PVGround object
"""
param_names = param_names or []
# Get ground boundaries
if x_min_max is None:
x_min, x_max = MIN_X_GROUND, MAX_X_GROUND
else:
x_min, x_max = x_min_max
full_extent_coords = [(x_min, y_ground), (x_max, y_ground)]
# Create the shade collections
shaded_collection = ShadeCollection(
list_surfaces=list_shaded_surfaces, shaded=True,
param_names=param_names)
illum_collection = ShadeCollection(
list_surfaces=list_illum_surfaces, shaded=False,
param_names=param_names)
# Create the ground segment
segment = PVSegment(illum_collection=illum_collection,
shaded_collection=shaded_collection)
return cls(list_segments=[segment],
original_linestring=LineString(full_extent_coords))
@property
def boundary(self):
"""Boundaries of the ground's original linestring."""
return self.original_linestring.boundary