"""Timeseries geometry tools. They allow the vectorization of geometry
calculations."""
import numpy as np
from pvfactors.config import DISTANCE_TOLERANCE
from pvfactors.geometry.base import PVSurface, ShadeCollection
from shapely.geometry import GeometryCollection
[docs]class TsShadeCollection(object):
"""Collection of timeseries surfaces that are all either shaded or
illuminated. This will be used by both ground and PV row
geometries."""
[docs] def __init__(self, list_ts_surfaces, shaded):
"""Initialize using list of surfaces and shading status
Parameters
----------
list_ts_surfaces : \
list of :py:class:`~pvfactors.geometry.timeseries.TsSurface`
List of timeseries surfaces in collection
shaded : bool
Shading status of the collection
"""
self._list_ts_surfaces = list_ts_surfaces
self.shaded = shaded
# TODO: maybe the ts surfaces should have a "shaded" attribute
@property
def list_ts_surfaces(self):
"""List of timeseries surfaces in collection"""
return self._list_ts_surfaces
@property
def length(self):
"""Total length of the collection"""
length = 0.
for ts_surf in self._list_ts_surfaces:
length += ts_surf.length
return length
@property
def n_ts_surfaces(self):
"""Number of timeseries surfaces in the collection"""
return len(self._list_ts_surfaces)
def get_param_weighted(self, param):
"""Get timeseries parameter for the collection, 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 collection 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
"""
value = 0
for ts_surf in self._list_ts_surfaces:
value += ts_surf.length * ts_surf.get_param(param)
return value
def update_params(self, new_dict):
"""Update timeseries surface parameters of the segment.
Parameters
----------
new_dict : dict
Parameters to add or update for the surfaces
"""
for ts_surf in self._list_ts_surfaces:
ts_surf.params.update(new_dict)
def at(self, idx):
"""Generate a ponctual shade collection for the desired index.
Parameters
----------
idx : int
Index to use to generate shade collection
Returns
-------
collection : :py:class:`~pvfactors.geometry.base.ShadeCollection`
"""
list_surfaces = [ts_surf.at(idx) for ts_surf in self._list_ts_surfaces
if not ts_surf.at(idx).is_empty]
return ShadeCollection(list_surfaces, shaded=self.shaded)
[docs]class TsSurface(object):
"""Timeseries surface class: vectorized representation of PV surface
geometries."""
[docs] def __init__(self, coords, n_vector=None, param_names=None, index=None,
shaded=False):
"""Initialize timeseries surface using timeseries coordinates.
Parameters
----------
coords : :py:class:`~pvfactors.geometry.timeseries.TsLineCoords`
Timeseries coordinates of full segment
index : int, optional
Index of segment (Default = None)
n_vector : np.ndarray, optional
Timeseries normal vectors of the side (Default = None)
index : int, optional
Index of the timeseries surfaces (Default = None)
shaded : bool, optional
Is the surface shaded or not (Default = False)
"""
# TODO: ts surfaces should have a shaded attribute
self.coords = coords
self.param_names = [] if param_names is None else param_names
# TODO: the following should probably be turned into properties,
# because if the coords change, they won't be altered. But speed...
self.n_vector = n_vector
self.params = dict.fromkeys(self.param_names)
self.index = index
self.shaded = shaded
def at(self, idx):
"""Generate a PV segment geometry for the desired index.
Parameters
----------
idx : int
Index to use to generate PV segment geometry
Returns
-------
segment : :py:class:`~pvfactors.geometry.base.PVSurface` \
or :py:class:`~shapely.geometry.GeometryCollection`
The returned object will be an empty geometry if its length is
really small, otherwise it will be a PV surface geometry
"""
if self.length[idx] < DISTANCE_TOLERANCE:
# return an empty geometry
return GeometryCollection()
else:
# Get normal vector at idx
n_vector = (self.n_vector[:, idx] if self.n_vector is not None
else None)
# Get params at idx
# TODO: should find faster solution
params = _get_params_at_idx(idx, self.params)
# Return a pv surface geometry with given params
return PVSurface(self.coords.at(idx), shaded=self.shaded,
index=self.index, normal_vector=n_vector,
param_names=self.param_names,
params=params)
def plot_at_idx(self, idx, ax, color):
"""Plot timeseries PV row at a certain index, only if it's not
too small.
Parameters
----------
idx : int
Index to use to plot timeseries PV surface
ax : :py:class:`matplotlib.pyplot.axes` object
Axes for plotting
color_shaded : str, optional
Color to use for plotting the PV surface
"""
if self.length[idx] > DISTANCE_TOLERANCE:
self.at(idx).plot(ax, color=color)
@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 surface's centroid"""
return self.coords.centroid
def get_param(self, param):
"""Get timeseries parameter values of surface
Parameters
----------
param: str
Surface parameter to return
Returns
-------
np.ndarray
Timeseries parameter values
"""
return self.params[param]
def update_params(self, new_dict):
"""Update timeseries surface parameters.
Parameters
----------
new_dict : dict
Parameters to add or update for the surface
"""
self.params.update(new_dict)
@property
def length(self):
"""Timeseries length of the surface"""
return self.coords.length
@property
def highest_point(self):
"""Timeseries point coordinates of highest point of surface"""
return self.coords.highest_point
@property
def lowest_point(self):
"""Timeseries point coordinates of lowest point of surface"""
return self.coords.lowest_point
@property
def u_vector(self):
"""Vector orthogonal to the surface's normal vector"""
u_vector = (None if self.n_vector is None else
np.array([-self.n_vector[1, :], self.n_vector[0, :]]))
return u_vector
@property
def is_empty(self):
"""Check if surface is "empty" by checking if its length is always
zero"""
return np.nansum(self.length) < DISTANCE_TOLERANCE
[docs]class TsLineCoords(object):
"""Timeseries line coordinates class: will provide a helpful shapely-like
API to invoke timeseries coordinates."""
[docs] def __init__(self, b1_ts_coords, b2_ts_coords, coords=None):
"""Initialize timeseries line coordinates using the timeseries
coordinates of its boundaries.
Parameters
----------
b1_ts_coords : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords`
Timeseries coordinates of first boundary point
b2_ts_coords : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords`
Timeseries coordinates of second boundary point
coords : np.ndarray, optional
Timeseries coordinates as numpy array
"""
self.b1 = b1_ts_coords
self.b2 = b2_ts_coords
def at(self, idx):
"""Get coordinates at a given index
Parameters
----------
idx : int
Index to use to get coordinates
"""
return self.as_array[:, :, idx]
@classmethod
def from_array(cls, coords_array):
"""Create timeseries line coordinates from numpy array of coordinates.
Parameters
----------
coords_array : np.ndarray
Numpy array of coordinates.
"""
b1 = TsPointCoords.from_array(coords_array[0, :, :])
b2 = TsPointCoords.from_array(coords_array[1, :, :])
return cls(b1, b2)
@property
def length(self):
"""Timeseries length of the line."""
return np.sqrt((self.b2.y - self.b1.y)**2
+ (self.b2.x - self.b1.x)**2)
@property
def as_array(self):
"""Timeseries line coordinates as numpy array"""
return np.array([[self.b1.x, self.b1.y], [self.b2.x, self.b2.y]])
@property
def centroid(self):
"""Timeseries point coordinates of the line coordinates"""
dy = self.b2.y - self.b1.y
dx = self.b2.x - self.b1.x
return TsPointCoords(self.b1.x + 0.5 * dx, self.b1.y + 0.5 * dy)
@property
def highest_point(self):
"""Timeseries point coordinates of highest point of timeseries
line coords"""
is_b1_highest = self.b1.y >= self.b2.y
x = np.where(is_b1_highest, self.b1.x, self.b2.x)
y = np.where(is_b1_highest, self.b1.y, self.b2.y)
return TsPointCoords(x, y)
@property
def lowest_point(self):
"""Timeseries point coordinates of lowest point of timeseries
line coords"""
is_b1_highest = self.b1.y >= self.b2.y
x = np.where(is_b1_highest, self.b2.x, self.b1.x)
y = np.where(is_b1_highest, self.b2.y, self.b1.y)
return TsPointCoords(x, y)
def __repr__(self):
"""Use the numpy array representation of the coords"""
return str(self.as_array)
[docs]class TsPointCoords(object):
"""Timeseries point coordinates: provides a shapely-like API for timeseries
point coordinates."""
[docs] def __init__(self, x, y):
"""Initialize timeseries point coordinates using numpy array of coords.
Parameters
----------
x : np.ndarray
Timeseries x coordinates
y : np.ndarray
Timeseries y coordinates
"""
self.x = x
self.y = y
def at(self, idx):
"""Get coordinates at a given index
Parameters
----------
idx : int
Index to use to get coordinates
"""
return self.as_array[:, idx]
@property
def as_array(self):
"""Timeseries point coordinates as numpy array"""
return np.array([self.x, self.y])
@classmethod
def from_array(cls, coords_array):
"""Create timeseries point coords from numpy array of coordinates.
Parameters
----------
coords_array : np.ndarray
Numpy array of coordinates.
"""
return cls(coords_array[0, :], coords_array[1, :])
def __repr__(self):
"""Use the numpy array representation of the point"""
return str(self.as_array)
def _get_params_at_idx(idx, params_dict):
"""Get the parameter values at given index. Return the whole parameter
when it's None, a scalar, or a dictionary
Parameters
----------
idx : int
Index at which we want the parameter values
params_dict : dict
Dictionary of parameters
Returns
-------
Parameter value at index
"""
if params_dict is None:
return None
else:
return {k: (val if (val is None) or np.isscalar(val)
or isinstance(val, dict) else val[idx])
for k, val in params_dict.items()}