From df6b9c203ab35d389ae8942e2091cf11db5a59eb Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:23:04 -0700 Subject: [PATCH 1/2] make matplotlib an optional dependency --- README.rst | 4 +- pvfactors/geometry/_optional_imports.py | 68 ++++ pvfactors/geometry/base.py | 331 +++++++++++------- pvfactors/geometry/plot.py | 33 +- pvfactors/geometry/pvground.py | 423 +++++++++++++++--------- pvfactors/geometry/pvrow.py | 366 +++++++++++++------- pvfactors/geometry/timeseries.py | 57 ++-- pyproject.toml | 19 +- 8 files changed, 883 insertions(+), 418 deletions(-) create mode 100644 pvfactors/geometry/_optional_imports.py diff --git a/README.rst b/README.rst index bec0a03..cd1bc1f 100644 --- a/README.rst +++ b/README.rst @@ -117,7 +117,9 @@ The user can quickly create a PV array with ``pvfactors``, and manipulate it wit df_inputs.albedo) The user can then plot the PV array geometry at any given time of the simulation: - +Please note that the plot is generated using matplotlib, which is +not included in the default dependencies, it can be installed +using `pip install solarfactors[plot]` .. code:: python diff --git a/pvfactors/geometry/_optional_imports.py b/pvfactors/geometry/_optional_imports.py new file mode 100644 index 0000000..a1edca4 --- /dev/null +++ b/pvfactors/geometry/_optional_imports.py @@ -0,0 +1,68 @@ +"""Utility module for handling optional dependencies for plotting functionality.""" + +import functools + + +def _matplotlib_import_error_message(): + """Generate helpful error message for missing matplotlib.""" + return ( + "matplotlib is required for plotting functionality. " + "Install it using one of the following methods:\n" + " pip install solarfactors[plot]\n" + " pip install matplotlib\n" + " conda install matplotlib" + ) + + +def _check_matplotlib(): + """ + Check if matplotlib is available and raise ImportError if not. + """ + try: + import matplotlib # noqa: F401 + + return True + except ImportError: + raise ImportError(_matplotlib_import_error_message()) + + +def requires_matplotlib(func): + """Decorator to check for matplotlib availability before executing + plotting functions. + + Parameters + ---------- + func : callable + Function that requires matplotlib + + Returns + ------- + callable + Wrapped function that checks for matplotlib before execution + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + _check_matplotlib() + return func(*args, **kwargs) + + return wrapper + + +def optional_matplotlib_import(): + """Import matplotlib.pyplot with optional handling. + + Returns + ------- + module or None + matplotlib.pyplot module if available, None otherwise + + Raises + ------ + ImportError + If matplotlib is not available with helpful installation message + """ + _check_matplotlib() + import matplotlib.pyplot as plt # noqa: F401 + + return plt diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 58a5df2..dd06277 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -1,16 +1,28 @@ """Base classes for pvfactors geometry subpackage.""" import numpy as np -from pvfactors import PVFactorsError -from pvfactors.config import ( - DEFAULT_NORMAL_VEC, COLOR_DIC, DISTANCE_TOLERANCE, PLOT_FONTSIZE, - ALPHA_TEXT, MAX_X_GROUND) -from pvfactors.geometry.plot import plot_coords, plot_bounds, plot_line -from pvfactors.geometry.utils import \ - is_collinear, check_collinear, are_2d_vecs_collinear, difference, contains +from pvlib.tools import cosd, sind from shapely.geometry import GeometryCollection, LineString from shapely.ops import linemerge -from pvlib.tools import cosd, sind + +from pvfactors import PVFactorsError +from pvfactors.config import ( + ALPHA_TEXT, + COLOR_DIC, + DEFAULT_NORMAL_VEC, + DISTANCE_TOLERANCE, + MAX_X_GROUND, + PLOT_FONTSIZE, +) +from pvfactors.geometry._optional_imports import requires_matplotlib +from pvfactors.geometry.plot import plot_bounds, plot_coords, plot_line +from pvfactors.geometry.utils import ( + are_2d_vecs_collinear, + check_collinear, + contains, + difference, + is_collinear, +) def _check_uniform_shading(list_elements): @@ -37,8 +49,9 @@ def _check_uniform_shading(list_elements): raise PVFactorsError(msg) -def _coords_from_center_tilt_length(xy_center, tilt, length, - surface_azimuth, axis_azimuth): +def _coords_from_center_tilt_length( + xy_center, tilt, length, surface_azimuth, axis_azimuth +): """Calculate ``shapely`` :py:class:`LineString` coordinates from center coords, surface angles and length of line. The axis azimuth indicates the axis of rotation of the pvrows (if single- @@ -81,13 +94,12 @@ def _coords_from_center_tilt_length(xy_center, tilt, length, """ # PV row params x_center, y_center = xy_center - radius = length / 2. + radius = length / 2.0 # Get rotation - rotation = _get_rotation_from_tilt_azimuth(surface_azimuth, axis_azimuth, - tilt) + rotation = _get_rotation_from_tilt_azimuth(surface_azimuth, axis_azimuth, tilt) # Calculate coords - x1 = radius * cosd(rotation + 180.) + x_center - y1 = radius * sind(rotation + 180.) + y_center + x1 = radius * cosd(rotation + 180.0) + x_center + y1 = radius * sind(rotation + 180.0) + y_center x2 = radius * cosd(rotation) + x_center y2 = radius * sind(rotation) + y_center @@ -119,7 +131,7 @@ def _get_rotation_from_tilt_azimuth(surface_azimuth, axis_azimuth, tilt): """ # Calculate rotation of PV row (signed tilt angle) - is_pointing_right = ((surface_azimuth - axis_azimuth) % 360.) > 180. + is_pointing_right = ((surface_azimuth - axis_azimuth) % 360.0) > 180.0 rotation = np.where(is_pointing_right, tilt, -tilt) rotation[tilt == 0] = -0.0 # GH 125 return rotation @@ -150,10 +162,13 @@ def _get_solar_2d_vectors(solar_zenith, solar_azimuth, axis_azimuth): Two vector components of the solar vector in the 2D plane, with the form [x, y], where x and y can be arrays """ - solar_2d_vector = np.array([ - # a drawing really helps understand the following - sind(solar_zenith) * cosd(solar_azimuth - axis_azimuth - 90.), - cosd(solar_zenith)]) + solar_2d_vector = np.array( + [ + # a drawing really helps understand the following + sind(solar_zenith) * cosd(solar_azimuth - axis_azimuth - 90.0), + cosd(solar_zenith), + ] + ) return solar_2d_vector @@ -164,8 +179,9 @@ class BaseSurface: So two surfaces could use the same linestring, but have opposite orientations.""" - def __init__(self, coords, normal_vector=None, index=None, - param_names=None, params=None): + def __init__( + self, coords, normal_vector=None, index=None, param_names=None, params=None + ): """Create a surface using linestring coordinates. Normal vector can have two directions for a given LineString, so the user can provide it in order to be specific, @@ -197,8 +213,7 @@ def __init__(self, coords, normal_vector=None, index=None, self.n_vector = np.array(normal_vector) self.index = index self.param_names = param_names - self.params = params if params is not None \ - else dict.fromkeys(self.param_names) + self.params = params if params is not None else dict.fromkeys(self.param_names) @property def is_empty(self): @@ -249,6 +264,7 @@ def _calculate_n_vector(self): else: return DEFAULT_NORMAL_VEC + @requires_matplotlib def plot(self, ax, color=None, with_index=False): """Plot the surface on the given axes. @@ -274,10 +290,14 @@ def plot(self, ax, color=None, with_index=False): y = centroid.y + alpha * v_norm[1] # Add text # FIXME: hack to get a nice plot in jupyter notebook - if np.abs(x) < MAX_X_GROUND / 2.: - ax.text(x, y, '{}'.format(self.index), - verticalalignment='center', - horizontalalignment='center') + if np.abs(x) < MAX_X_GROUND / 2.0: + ax.text( + x, + y, + "{}".format(self.index), + verticalalignment="center", + horizontalalignment="center", + ) def difference(self, linestring): """Calculate remaining surface after removing part belonging from @@ -331,8 +351,15 @@ class PVSurface(BaseSurface): that PV surfaces have a ``shaded`` attribute. """ - def __init__(self, coords=None, normal_vector=None, shaded=False, - index=None, param_names=None, params=None): + def __init__( + self, + coords=None, + normal_vector=None, + shaded=False, + index=None, + param_names=None, + params=None, + ): """Initialize PV surface. Parameters @@ -353,8 +380,9 @@ def __init__(self, coords=None, normal_vector=None, shaded=False, Surface float parameters (Default = None) """ param_names = [] if param_names is None else param_names - super(PVSurface, self).__init__(coords, normal_vector, index=index, - param_names=param_names, params=params) + super(PVSurface, self).__init__( + coords, normal_vector, index=index, param_names=param_names, params=params + ) self.shaded = shaded @@ -393,7 +421,9 @@ def __init__(self, list_surfaces=None, shaded=None, param_names=None): def geometry(self): """Return a Shapely GeometryCollection built from the current surfaces.""" if not self._geometry_valid: - self._geometry = GeometryCollection([_.geometry for _ in self.list_surfaces]) + self._geometry = GeometryCollection( + [_.geometry for _ in self.list_surfaces] + ) self._geometry_valid = True return self._geometry @@ -433,6 +463,7 @@ def _get_shading(self, shaded): else: return shaded + @requires_matplotlib def plot(self, ax, color=None, with_index=False): """Plot the surfaces in the shade collection. @@ -461,9 +492,12 @@ def add_linestring(self, linestring, normal_vector=None): """ if normal_vector is None: normal_vector = self.n_vector - surf = PVSurface(coords=linestring.coords, - normal_vector=normal_vector, shaded=self.shaded, - param_names=self.param_names) + surf = PVSurface( + coords=linestring.coords, + normal_vector=normal_vector, + shaded=self.shaded, + param_names=self.param_names, + ) self.add_pvsurface(surf) def add_pvsurface(self, pvsurface): @@ -502,9 +536,11 @@ def remove_linestring(self, linestring): for new_geom in geoms: if not new_geom.is_empty: new_surface = PVSurface( - new_geom.coords, normal_vector=surface.n_vector, + new_geom.coords, + normal_vector=surface.n_vector, shaded=surface.shaded, - param_names=surface.param_names) + param_names=surface.param_names, + ) new_list_surfaces.append(new_surface) else: new_list_surfaces.append(surface) @@ -520,8 +556,10 @@ def merge_surfaces(self): surf_1 = self.list_surfaces[0] new_pvsurf = PVSurface( coords=[(minx, miny), (maxx, maxy)], - shaded=self.shaded, normal_vector=surf_1.n_vector, - param_names=surf_1.param_names) + shaded=self.shaded, + normal_vector=surf_1.n_vector, + param_names=surf_1.param_names, + ) self.list_surfaces = [new_pvsurf] self._geometry_valid = False @@ -545,13 +583,17 @@ def cut_at_point(self, point): coords_2 = [point, b2] # TODO: not sure what to do about index yet new_surf_1 = PVSurface( - coords_1, normal_vector=surface.n_vector, + coords_1, + normal_vector=surface.n_vector, shaded=surface.shaded, - param_names=surface.param_names) + param_names=surface.param_names, + ) new_surf_2 = PVSurface( - coords_2, normal_vector=surface.n_vector, + coords_2, + normal_vector=surface.n_vector, shaded=surface.shaded, - param_names=surface.param_names) + param_names=surface.param_names, + ) # Now update collection self.list_surfaces[idx] = new_surf_1 self.list_surfaces.append(new_surf_2) @@ -633,8 +675,9 @@ def surface_indices(self): return [surf.index for surf in self.list_surfaces] @classmethod - def from_linestring_coords(cls, coords, shaded, normal_vector=None, - param_names=None): + def from_linestring_coords( + cls, coords, shaded, normal_vector=None, param_names=None + ): """Create a shade collection with a single PV surface. Parameters @@ -649,8 +692,12 @@ def from_linestring_coords(cls, coords, shaded, normal_vector=None, Names of the surface parameters, eg reflectivity, total incident irradiance, temperature, etc. (Default = None) """ - surf = PVSurface(coords=coords, normal_vector=normal_vector, - shaded=shaded, param_names=param_names) + surf = PVSurface( + coords=coords, + normal_vector=normal_vector, + shaded=shaded, + param_names=param_names, + ) return cls([surf], shaded=shaded, param_names=param_names) @@ -659,8 +706,12 @@ class PVSegment: shade collections, a shaded one and an illuminated one. """ - def __init__(self, illum_collection=ShadeCollection(shaded=False), - shaded_collection=ShadeCollection(shaded=True), index=None): + def __init__( + self, + illum_collection=ShadeCollection(shaded=False), + shaded_collection=ShadeCollection(shaded=True), + index=None, + ): """Initialize PV segment. Parameters @@ -688,8 +739,9 @@ def __init__(self, illum_collection=ShadeCollection(shaded=False), @property def geometry(self): if not self._geometry_valid: - self._geometry = GeometryCollection([self._shaded_collection.geometry, - self._illum_collection.geometry]) + self._geometry = GeometryCollection( + [self._shaded_collection.geometry, self._illum_collection.geometry] + ) self._geometry_valid = True return self._geometry @@ -728,14 +780,19 @@ def _check_collinear(self, illum_collection, shaded_collection): assert shaded_collection.is_collinear # Check that if none or all of the collection is empty, n_vectors are # equal - if (not illum_collection.is_empty) \ - and (not shaded_collection.is_empty): + if (not illum_collection.is_empty) and (not shaded_collection.is_empty): n_vec_ill = illum_collection.n_vector n_vec_shaded = shaded_collection.n_vector assert are_2d_vecs_collinear(n_vec_ill, n_vec_shaded) - def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum'], with_index=False): + @requires_matplotlib + def plot( + self, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + with_index=False, + ): """Plot the surfaces in the PV Segment. Parameters @@ -751,10 +808,8 @@ def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], with_index : bool Flag to annotate surfaces with their indices (Default = False) """ - self._shaded_collection.plot(ax, color=color_shaded, - with_index=with_index) - self._illum_collection.plot(ax, color=color_illum, - with_index=with_index) + self._shaded_collection.plot(ax, color=color_shaded, with_index=with_index) + self._illum_collection.plot(ax, color=color_illum, with_index=with_index) def cast_shadow(self, linestring): """Cast shadow on PV segment using linestring: will rearrange the @@ -769,12 +824,14 @@ def cast_shadow(self, linestring): # Using a buffer may slow things down, but it's quite crucial # in order for shapely to get the intersection accurately see: # https://stackoverflow.com/questions/28028910/how-to-deal-with-rounding-errors-in-shapely - intersection = (self._illum_collection.geometry.buffer(DISTANCE_TOLERANCE) - .intersection(linestring)) + intersection = self._illum_collection.geometry.buffer( + DISTANCE_TOLERANCE + ).intersection(linestring) if not intersection.is_empty: # Split up only if intersects the illuminated collection - self._shaded_collection.add_linestring(intersection, - normal_vector=self.n_vector) + self._shaded_collection.add_linestring( + intersection, normal_vector=self.n_vector + ) self._illum_collection.remove_linestring(intersection) self._geometry_valid = False @@ -861,8 +918,9 @@ def n_vector(self): @property def n_surfaces(self): """Number of surfaces in collection.""" - n_surfaces = self._illum_collection.n_surfaces \ - + self._shaded_collection.n_surfaces + n_surfaces = ( + self._illum_collection.n_surfaces + self._shaded_collection.n_surfaces + ) return n_surfaces @property @@ -874,8 +932,9 @@ def surface_indices(self): return list_indices @classmethod - def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, - index=None, param_names=None): + def from_linestring_coords( + cls, coords, shaded=False, normal_vector=None, index=None, param_names=None + ): """Create a PV segment with a single PV surface. Parameters @@ -894,18 +953,17 @@ def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, irradiance, temperature, etc. (Default = None) """ col = ShadeCollection.from_linestring_coords( - coords, shaded=shaded, normal_vector=normal_vector, - param_names=param_names) + coords, shaded=shaded, normal_vector=normal_vector, param_names=param_names + ) # Realized that needed to instantiate other_col, otherwise could # end up with shared collection among different PV segments - other_col = ShadeCollection(list_surfaces=[], shaded=not shaded, - param_names=param_names) + other_col = ShadeCollection( + list_surfaces=[], shaded=not shaded, param_names=param_names + ) if shaded: - return cls(illum_collection=other_col, - shaded_collection=col, index=index) + return cls(illum_collection=other_col, shaded_collection=col, index=index) else: - return cls(illum_collection=col, - shaded_collection=other_col, index=index) + return cls(illum_collection=col, shaded_collection=other_col, index=index) @property def shaded_collection(self): @@ -927,8 +985,7 @@ def shaded_collection(self, new_collection): @shaded_collection.deleter def shaded_collection(self): - """Delete shaded collection of PV segment and replace with empty one. - """ + """Delete shaded collection of PV segment and replace with empty one.""" self._shaded_collection = ShadeCollection(shaded=True) self._geometry_valid = False @@ -977,8 +1034,9 @@ def all_surfaces(self): list of :py:class:`~pvfactors.geometry.base.PVSurface` PV surfaces in the PV segment """ - return self._illum_collection.list_surfaces + \ - self._shaded_collection.list_surfaces + return ( + self._illum_collection.list_surfaces + self._shaded_collection.list_surfaces + ) class BaseSide: @@ -1020,8 +1078,15 @@ def intersects(self, *args, **kwargs): return self.geometry.intersects(*args, **kwargs) @classmethod - def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, - index=None, n_segments=1, param_names=None): + def from_linestring_coords( + cls, + coords, + shaded=False, + normal_vector=None, + index=None, + n_segments=1, + param_names=None, + ): """Create a Side with a single PV surface, or multiple discretized identical ones. @@ -1043,22 +1108,34 @@ def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, irradiance, temperature, etc. (Default = None) """ if n_segments == 1: - list_pvsegments = [PVSegment.from_linestring_coords( - coords, shaded=shaded, normal_vector=normal_vector, - index=index, param_names=param_names)] + list_pvsegments = [ + PVSegment.from_linestring_coords( + coords, + shaded=shaded, + normal_vector=normal_vector, + index=index, + param_names=param_names, + ) + ] else: # Discretize coords and create segments accordingly linestring = LineString(coords) - fractions = np.linspace(0., 1., num=n_segments + 1) - list_points = [linestring.interpolate(fraction, normalized=True) - for fraction in fractions] + fractions = np.linspace(0.0, 1.0, num=n_segments + 1) + list_points = [ + linestring.interpolate(fraction, normalized=True) + for fraction in fractions + ] list_pvsegments = [] for idx in range(n_segments): - new_coords = list_points[idx:idx + 2] + new_coords = list_points[idx : idx + 2] # TODO: not clear what to do with the index here pvsegment = PVSegment.from_linestring_coords( - new_coords, shaded=shaded, normal_vector=normal_vector, - index=index, param_names=param_names) + new_coords, + shaded=shaded, + normal_vector=normal_vector, + index=index, + param_names=param_names, + ) list_pvsegments.append(pvsegment) return cls(list_segments=list_pvsegments) @@ -1073,7 +1150,7 @@ def n_vector(self): @property def shaded_length(self): """Shaded length of the Side.""" - shaded_length = 0. + shaded_length = 0.0 for segment in self.list_segments: shaded_length += segment.shaded_length return shaded_length @@ -1103,8 +1180,14 @@ def surface_indices(self): list_indices += seg.surface_indices return list_indices - def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum'], with_index=False): + @requires_matplotlib + def plot( + self, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + with_index=False, + ): """Plot the surfaces in the Side object. Parameters @@ -1121,8 +1204,12 @@ def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], Flag to annotate surfaces with their indices (Default = False) """ for segment in self.list_segments: - segment.plot(ax, color_shaded=color_shaded, - color_illum=color_illum, with_index=with_index) + segment.plot( + ax, + color_shaded=color_shaded, + color_illum=color_illum, + with_index=with_index, + ) def cast_shadow(self, linestring): """Cast shadow on Side using linestring: will rearrange the @@ -1214,8 +1301,15 @@ class BasePVArray(object): """Base class for PV arrays in pvfactors. Will provide basic capabilities.""" - registry_cols = ['geom', 'line_type', 'pvrow_index', 'side', - 'pvsegment_index', 'shaded', 'surface_index'] + registry_cols = [ + "geom", + "line_type", + "pvrow_index", + "side", + "pvsegment_index", + "shaded", + "surface_index", + ] def __init__(self, axis_azimuth=None): """Initialize Base of PV array. @@ -1255,9 +1349,16 @@ def ts_surface_indices(self): """List of indices of all the timeseries surfaces""" return [ts_surf.index for ts_surf in self.all_ts_surfaces] - def plot_at_idx(self, idx, ax, merge_if_flag_overlap=True, - with_cut_points=True, x_min_max=None, - with_surface_index=False): + @requires_matplotlib + def plot_at_idx( + self, + idx, + ax, + merge_if_flag_overlap=True, + with_cut_points=True, + x_min_max=None, + with_surface_index=False, + ): """Plot all the PV rows and the ground in the PV array at a desired step index. This can be called before transforming the array, and after fitting it. @@ -1282,26 +1383,32 @@ def plot_at_idx(self, idx, ax, merge_if_flag_overlap=True, """ # Plot pv array structures self.ts_ground.plot_at_idx( - idx, ax, color_shaded=COLOR_DIC['ground_shaded'], - color_illum=COLOR_DIC['ground_illum'], + idx, + ax, + color_shaded=COLOR_DIC["ground_shaded"], + color_illum=COLOR_DIC["ground_illum"], merge_if_flag_overlap=merge_if_flag_overlap, - with_cut_points=with_cut_points, x_min_max=x_min_max, - with_surface_index=with_surface_index) + with_cut_points=with_cut_points, + x_min_max=x_min_max, + with_surface_index=with_surface_index, + ) for ts_pvrow in self.ts_pvrows: ts_pvrow.plot_at_idx( - idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum'], - with_surface_index=with_surface_index) + idx, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + with_surface_index=with_surface_index, + ) # Plot formatting - ax.axis('equal') + ax.axis("equal") if self.distance is not None: n_pvrows = self.n_pvrows - ax.set_xlim(- 0.5 * self.distance, - (n_pvrows - 0.5) * self.distance) + ax.set_xlim(-0.5 * self.distance, (n_pvrows - 0.5) * self.distance) if self.height is not None: - ax.set_ylim(- self.height, 2 * self.height) + ax.set_ylim(-self.height, 2 * self.height) ax.set_xlabel("x [m]", fontsize=PLOT_FONTSIZE) ax.set_ylabel("y [m]", fontsize=PLOT_FONTSIZE) diff --git a/pvfactors/geometry/plot.py b/pvfactors/geometry/plot.py index 235e794..ac10aaa 100644 --- a/pvfactors/geometry/plot.py +++ b/pvfactors/geometry/plot.py @@ -1,6 +1,9 @@ """Base functions used to plot 2D PV geometries""" +from ._optional_imports import requires_matplotlib + +@requires_matplotlib def plot_coords(ax, ob): """Plot coordinates of shapely objects @@ -14,13 +17,14 @@ def plot_coords(ax, ob): """ try: x, y = ob.geometry.xy - ax.plot(x, y, 'o', color='#999999', zorder=1) + ax.plot(x, y, "o", color="#999999", zorder=1) except NotImplementedError: for line in ob.geometry.geoms: x, y = line.xy - ax.plot(x, y, 'o', color='#999999', zorder=1) + ax.plot(x, y, "o", color="#999999", zorder=1) +@requires_matplotlib def plot_bounds(ax, ob): """Plot boundaries of shapely object @@ -37,9 +41,10 @@ def plot_bounds(ax, ob): x, y = ob.coords[0] else: x, y = zip(*list((p.x, p.y) for p in ob.boundary.geoms)) - ax.plot(x, y, 'o', color='#000000', zorder=1) + ax.plot(x, y, "o", color="#000000", zorder=1) +@requires_matplotlib def plot_line(ax, ob, line_color): """Plot boundaries of shapely line @@ -55,10 +60,24 @@ def plot_line(ax, ob, line_color): """ try: x, y = ob.geometry.xy - ax.plot(x, y, color=line_color, alpha=0.7, - linewidth=3, solid_capstyle='round', zorder=2) + ax.plot( + x, + y, + color=line_color, + alpha=0.7, + linewidth=3, + solid_capstyle="round", + zorder=2, + ) except NotImplementedError: for line in ob.geometry.geoms: x, y = line.xy - ax.plot(x, y, color=line_color, - alpha=0.7, linewidth=3, solid_capstyle='round', zorder=2) + ax.plot( + x, + y, + color=line_color, + alpha=0.7, + linewidth=3, + solid_capstyle="round", + zorder=2, + ) diff --git a/pvfactors/geometry/pvground.py b/pvfactors/geometry/pvground.py index 93d2f4c..efb7ea7 100644 --- a/pvfactors/geometry/pvground.py +++ b/pvfactors/geometry/pvground.py @@ -1,15 +1,27 @@ """Classes for implementation of ground geometry""" + +from copy import deepcopy + +import numpy as np +from shapely.geometry import LineString + 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) + COLOR_DIC, + DISTANCE_TOLERANCE, + MAX_X_GROUND, + MIN_X_GROUND, + Y_GROUND, +) +from pvfactors.geometry._optional_imports import requires_matplotlib +from pvfactors.geometry.base import BaseSide, PVSegment, PVSurface, ShadeCollection 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 + TsLineCoords, + TsPointCoords, + TsShadeCollection, + TsSurface, + _get_params_at_idx, +) class TsGround(object): @@ -21,8 +33,15 @@ class TsGround(object): x_min = MIN_X_GROUND x_max = MAX_X_GROUND - def __init__(self, shadow_elements, illum_elements, param_names=None, - flag_overlap=None, cut_point_coords=None, y_ground=None): + 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 @@ -63,16 +82,21 @@ def __init__(self, shadow_elements, illum_elements, param_names=None, # 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.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): + 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. @@ -115,23 +139,35 @@ def from_ts_pvrows_and_angles(cls, list_ts_pvrows, alpha_vec, rotation_vec, 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)]]) + [ + [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))) + 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) + 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): + 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 @@ -156,27 +192,34 @@ def from_ordered_shadows_coords(cls, shadow_coords, flag_overlap=None, # 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] + 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) + 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) + 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): + 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. @@ -201,21 +244,23 @@ def at(self, idx, x_min_max=None, merge_if_flag_overlap=True, """ # 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] + 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) + 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) + list_shadow_surfaces += shadow_el.non_point_surfaces_at(idx) # Get the illuminated surfaces list_illum_surfaces = [] for illum_el in self.illum_elements: @@ -232,47 +277,75 @@ def at(self, idx, x_min_max=None, merge_if_flag_overlap=True, # 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)] + 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) + 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] + 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) + 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] + 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] + 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): + list_shadow_surfaces, + list_illum_surfaces, + param_names=self.param_names, + y_ground=self.y_ground, + x_min_max=x_min_max, + ) + + @requires_matplotlib + 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 @@ -299,11 +372,18 @@ def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], 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) + 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 @@ -385,7 +465,7 @@ def get_param_ww(self, param): KeyError if parameter name not in a surface parameters """ - value = 0. + value = 0.0 for shadow_el in self.shadow_elements: value += shadow_el.get_param_ww(param) for illum_el in self.illum_elements: @@ -409,9 +489,10 @@ def shadow_coords_left_of_cut_point(self, idx_cut_pt): 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] + 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 @@ -430,9 +511,10 @@ def shadow_coords_right_of_cut_point(self, idx_cut_pt): 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] + 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 @@ -556,8 +638,9 @@ def non_point_surfaces_at(self, idx): ------- list of :py:class:`~pvfactors.geometry.base.PVSurface` """ - return self.non_point_illum_surfaces_at(idx) \ - + self.non_point_shaded_surfaces_at(idx) + 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` @@ -576,7 +659,8 @@ def n_non_point_surfaces_at(self, idx): @staticmethod def _shadow_elements_from_coords_and_cut_pts( - list_shadow_coords, cut_point_coords, param_names): + 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. @@ -607,20 +691,23 @@ def _shadow_elements_from_coords_and_cut_pts( 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) + 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)) + 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): + 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 @@ -661,18 +748,34 @@ def _illum_elements_from_coords_and_cut_pts( 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)) + 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)) + 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 @@ -719,13 +822,17 @@ def _merge_shadow_surfaces(self, idx, non_pt_shadow_elements): if i_surf == 0: # Need to merge with preceding if exists if surface_to_merge is not None: - coords = [surface_to_merge.boundary.geoms[0], - surface.boundary.geoms[1]] + coords = [ + surface_to_merge.boundary.geoms[0], + surface.boundary.geoms[1], + ] surface = PVSurface( - coords, shaded=True, + coords, + shaded=True, param_names=self.param_names, params=surface.params, - index=surface.index) + index=surface.index, + ) if i_el == n_shadow_elements - 1: # last surface of last shadow element list_shadow_surfaces.append(surface) @@ -735,13 +842,19 @@ def _merge_shadow_surfaces(self, idx, non_pt_shadow_elements): elif i_surf == 0: # first surface but definitely not last either if surface_to_merge is not None: - coords = [surface_to_merge.boundary.geoms[0], - surface.boundary.geoms[1]] + coords = [ + surface_to_merge.boundary.geoms[0], + surface.boundary.geoms[1], + ] list_shadow_surfaces.append( - PVSurface(coords, shaded=True, - param_names=self.param_names, - params=surface.params, - index=surface.index)) + PVSurface( + coords, + shaded=True, + param_names=self.param_names, + params=surface.params, + index=surface.index, + ) + ) else: list_shadow_surfaces.append(surface) else: @@ -750,8 +863,7 @@ def _merge_shadow_surfaces(self, idx, non_pt_shadow_elements): 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) + 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: @@ -767,8 +879,9 @@ class TsGroundElement(object): defined by the n ground cutting points. This is crucial to calculate view factors in a vectorized way.""" - def __init__(self, coords, list_ordered_cut_pts_coords=None, - param_names=None, shaded=False): + 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. @@ -835,8 +948,7 @@ def surfaces_at(self, idx): ------- list of :py:class:`~pvfactors.geometry.base.PVSurface` """ - return [surface.at(idx) - for surface in self.surface_list] + 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 @@ -851,9 +963,11 @@ def non_point_surfaces_at(self, idx): ------- list of :py:class:`~pvfactors.geometry.base.PVSurface` """ - return [surface.at(idx) - for surface in self.surface_list - if surface.length[idx] > DISTANCE_TOLERANCE] + 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 @@ -890,7 +1004,7 @@ def get_param_ww(self, param): KeyError if parameter name not in a surface parameters """ - value = 0. + value = 0.0 for ts_surf in self.surface_list: value += ts_surf.length * ts_surf.get_param(param) return value @@ -907,31 +1021,32 @@ def _create_all_ts_surfaces(self, list_ordered_cut_pts): left to right """ # Initialize dict - self.surface_dict = {i: {'right': [], 'left': []} - for i in range(len(list_ordered_cut_pts))} + 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) + 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) + 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) + 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) + 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) + 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) + self.surface_dict[j]["right"].append(next_surface) @staticmethod def _coords_right_of_cut_point(coords, cut_pt_coords): @@ -1005,8 +1120,7 @@ def __init__(self, list_segments=None, original_linestring=None): super(PVGround, self).__init__(list_segments) @classmethod - def as_flat(cls, x_min_max=None, shaded=False, y_ground=Y_GROUND, - param_names=None): + 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 @@ -1034,15 +1148,20 @@ def as_flat(cls, x_min_max=None, shaded=False, y_ground=Y_GROUND, 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) + seg = PVSegment.from_linestring_coords( + coords, shaded=shaded, normal_vector=[0.0, 1.0], 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): + 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 @@ -1076,18 +1195,20 @@ def from_lists_surfaces( # Create the shade collections shaded_collection = ShadeCollection( - list_surfaces=list_shaded_surfaces, shaded=True, - param_names=param_names) + 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) + 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) + segment = PVSegment( + illum_collection=illum_collection, shaded_collection=shaded_collection + ) - return cls(list_segments=[segment], - original_linestring=LineString(full_extent_coords)) + return cls( + list_segments=[segment], original_linestring=LineString(full_extent_coords) + ) @property def boundary(self): diff --git a/pvfactors/geometry/pvrow.py b/pvfactors/geometry/pvrow.py index edc2da1..b277abf 100644 --- a/pvfactors/geometry/pvrow.py +++ b/pvfactors/geometry/pvrow.py @@ -1,14 +1,14 @@ """Module will classes related to PV row geometries""" import numpy as np -from shapely.ops import unary_union, linemerge -from pvfactors.config import COLOR_DIC -from pvfactors.geometry.base import \ - BaseSide, _coords_from_center_tilt_length, PVSegment -from shapely.geometry import LineString -from pvfactors.geometry.timeseries import \ - TsShadeCollection, TsLineCoords, TsSurface from pvlib.tools import cosd, sind +from shapely.geometry import LineString +from shapely.ops import linemerge, unary_union + +from pvfactors.config import COLOR_DIC +from pvfactors.geometry._optional_imports import requires_matplotlib +from pvfactors.geometry.base import BaseSide, PVSegment, _coords_from_center_tilt_length +from pvfactors.geometry.timeseries import TsLineCoords, TsShadeCollection, TsSurface class TsPVRow(object): @@ -16,8 +16,9 @@ class TsPVRow(object): PV row geometries. The coordinates and attributes (front and back sides) are all vectorized.""" - def __init__(self, ts_front_side, ts_back_side, xy_center, index=None, - full_pvrow_coords=None): + def __init__( + self, ts_front_side, ts_back_side, xy_center, index=None, full_pvrow_coords=None + ): """Initialize timeseries PV row with its front and back sides. Parameters @@ -42,9 +43,17 @@ def __init__(self, ts_front_side, ts_back_side, xy_center, index=None, self.full_pvrow_coords = full_pvrow_coords @classmethod - def from_raw_inputs(cls, xy_center, width, rotation_vec, - cut, shaded_length_front, shaded_length_back, - index=None, param_names=None): + def from_raw_inputs( + cls, + xy_center, + width, + rotation_vec, + cut, + shaded_length_front, + shaded_length_back, + index=None, + param_names=None, + ): """Create timeseries PV row using raw inputs. Note: shading will always be zero when pv rows are flat. @@ -74,25 +83,35 @@ def from_raw_inputs(cls, xy_center, width, rotation_vec, New timeseries PV row object """ # Calculate full pvrow coords - pvrow_coords = TsPVRow._calculate_full_coords( - xy_center, width, rotation_vec) + pvrow_coords = TsPVRow._calculate_full_coords(xy_center, width, rotation_vec) # Calculate normal vectors dx = pvrow_coords.b2.x - pvrow_coords.b1.x dy = pvrow_coords.b2.y - pvrow_coords.b1.y normal_vec_front = np.array([-dy, dx]) # Calculate front side coords ts_front = TsSide.from_raw_inputs( - xy_center, width, rotation_vec, cut.get('front', 1), - shaded_length_front, n_vector=normal_vec_front, - param_names=param_names) + xy_center, + width, + rotation_vec, + cut.get("front", 1), + shaded_length_front, + n_vector=normal_vec_front, + param_names=param_names, + ) # Calculate back side coords ts_back = TsSide.from_raw_inputs( - xy_center, width, rotation_vec, cut.get('back', 1), - shaded_length_back, n_vector=-normal_vec_front, - param_names=param_names) - - return cls(ts_front, ts_back, xy_center, index=index, - full_pvrow_coords=pvrow_coords) + xy_center, + width, + rotation_vec, + cut.get("back", 1), + shaded_length_back, + n_vector=-normal_vec_front, + param_names=param_names, + ) + + return cls( + ts_front, ts_back, xy_center, index=index, full_pvrow_coords=pvrow_coords + ) @staticmethod def _calculate_full_coords(xy_center, width, rotation): @@ -113,10 +132,10 @@ def _calculate_full_coords(xy_center, width, rotation): Timeseries coordinates of full PV row """ x_center, y_center = xy_center - radius = width / 2. + radius = width / 2.0 # Calculate coords - x1 = radius * cosd(rotation + 180.) + x_center - y1 = radius * sind(rotation + 180.) + y_center + x1 = radius * cosd(rotation + 180.0) + x_center + y1 = radius * sind(rotation + 180.0) + y_center x2 = radius * cosd(rotation) + x_center y2 = radius * sind(rotation) + y_center coords = TsLineCoords.from_array(np.array([[x1, y1], [x2, y2]])) @@ -139,9 +158,15 @@ def surfaces_at_idx(self, idx): pvrow = self.at(idx) return pvrow.all_surfaces - def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum'], - with_surface_index=False): + @requires_matplotlib + def plot_at_idx( + self, + idx, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + with_surface_index=False, + ): """Plot timeseries PV row at a certain index. Parameters @@ -160,8 +185,12 @@ def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], Plot the surfaces with their index values (Default = False) """ pvrow = self.at(idx) - pvrow.plot(ax, color_shaded=color_shaded, - color_illum=color_illum, with_index=with_surface_index) + pvrow.plot( + ax, + color_shaded=color_shaded, + color_illum=color_illum, + with_index=with_surface_index, + ) def at(self, idx): """Generate a PV row geometry for the desired index. @@ -177,10 +206,13 @@ def at(self, idx): """ front_geom = self.front.at(idx) back_geom = self.back.at(idx) - original_line = LineString( - self.full_pvrow_coords.as_array[:, :, idx]) - pvrow = PVRow(front_side=front_geom, back_side=back_geom, - index=self.index, original_linestring=original_line) + original_line = LineString(self.full_pvrow_coords.as_array[:, :, idx]) + pvrow = PVRow( + front_side=front_geom, + back_side=back_geom, + index=self.index, + original_linestring=original_line, + ) return pvrow def update_params(self, new_dict): @@ -207,8 +239,11 @@ def all_ts_surfaces(self): @property def centroid(self): """Centroid point of the timeseries pv row""" - centroid = (self.full_pvrow_coords.centroid - if self.full_pvrow_coords is not None else None) + centroid = ( + self.full_pvrow_coords.centroid + if self.full_pvrow_coords is not None + else None + ) return centroid @property @@ -219,8 +254,11 @@ def length(self): @property def highest_point(self): """Timeseries point coordinates of highest point of PV row""" - high_pt = (self.full_pvrow_coords.highest_point - if self.full_pvrow_coords is not None else None) + high_pt = ( + self.full_pvrow_coords.highest_point + if self.full_pvrow_coords is not None + else None + ) return high_pt @@ -243,8 +281,16 @@ def __init__(self, segments, n_vector=None): self.n_vector = n_vector @classmethod - def from_raw_inputs(cls, xy_center, width, rotation_vec, cut, - shaded_length, n_vector=None, param_names=None): + def from_raw_inputs( + cls, + xy_center, + width, + rotation_vec, + cut, + shaded_length, + n_vector=None, + param_names=None, + ): """Create timeseries side using raw PV row inputs. Note: shading will always be zero when PV rows are flat. @@ -276,20 +322,22 @@ def from_raw_inputs(cls, xy_center, width, rotation_vec, cut, # Create Ts segments x_center, y_center = xy_center - radius = width / 2. + radius = width / 2.0 segment_length = width / cut - is_not_flat = rotation_vec != 0. + is_not_flat = rotation_vec != 0.0 # Calculate coords of shading point r_shade = radius - shaded_length x_sh = np.where( mask_tilted_to_left, - r_shade * cosd(rotation_vec + 180.) + x_center, - r_shade * cosd(rotation_vec) + x_center) + r_shade * cosd(rotation_vec + 180.0) + x_center, + r_shade * cosd(rotation_vec) + x_center, + ) y_sh = np.where( mask_tilted_to_left, - r_shade * sind(rotation_vec + 180.) + y_center, - r_shade * sind(rotation_vec) + y_center) + r_shade * sind(rotation_vec + 180.0) + y_center, + r_shade * sind(rotation_vec) + y_center, + ) # Calculate coords list_segments = [] @@ -297,12 +345,11 @@ def from_raw_inputs(cls, xy_center, width, rotation_vec, cut, # Calculate segment coords r1 = radius - i * segment_length r2 = radius - (i + 1) * segment_length - x1 = r1 * cosd(rotation_vec + 180.) + x_center - y1 = r1 * sind(rotation_vec + 180.) + y_center + x1 = r1 * cosd(rotation_vec + 180.0) + x_center + y1 = r1 * sind(rotation_vec + 180.0) + y_center x2 = r2 * cosd(rotation_vec + 180) + x_center y2 = r2 * sind(rotation_vec + 180) + y_center - segment_coords = TsLineCoords.from_array( - np.array([[x1, y1], [x2, y2]])) + segment_coords = TsLineCoords.from_array(np.array([[x1, y1], [x2, y2]])) # Determine lowest and highest points of segment x_highest = np.where(mask_tilted_to_left, x2, x1) y_highest = np.where(mask_tilted_to_left, y2, y1) @@ -310,11 +357,14 @@ def from_raw_inputs(cls, xy_center, width, rotation_vec, cut, y_lowest = np.where(mask_tilted_to_left, y1, y2) # Calculate illum and shaded coords x2_illum, y2_illum = x_highest, y_highest - x1_shaded, y1_shaded, x2_shaded, y2_shaded = \ - x_lowest, y_lowest, x_lowest, y_lowest + x1_shaded, y1_shaded, x2_shaded, y2_shaded = ( + x_lowest, + y_lowest, + x_lowest, + y_lowest, + ) mask_all_shaded = (y_sh > y_highest) & (is_not_flat) - mask_partial_shaded = (y_sh > y_lowest) & (~ mask_all_shaded) \ - & (is_not_flat) + mask_partial_shaded = (y_sh > y_lowest) & (~mask_all_shaded) & (is_not_flat) # Calculate second boundary point of shade x2_shaded = np.where(mask_all_shaded, x_highest, x2_shaded) x2_shaded = np.where(mask_partial_shaded, x_sh, x2_shaded) @@ -323,23 +373,40 @@ def from_raw_inputs(cls, xy_center, width, rotation_vec, cut, x1_illum = x2_shaded y1_illum = y2_shaded illum_coords = TsLineCoords.from_array( - np.array([[x1_illum, y1_illum], [x2_illum, y2_illum]])) + np.array([[x1_illum, y1_illum], [x2_illum, y2_illum]]) + ) shaded_coords = TsLineCoords.from_array( - np.array([[x1_shaded, y1_shaded], [x2_shaded, y2_shaded]])) + np.array([[x1_shaded, y1_shaded], [x2_shaded, y2_shaded]]) + ) # Create illuminated and shaded collections is_shaded = False illum = TsShadeCollection( - [TsSurface(illum_coords, n_vector=n_vector, - param_names=param_names, shaded=is_shaded)], - is_shaded) + [ + TsSurface( + illum_coords, + n_vector=n_vector, + param_names=param_names, + shaded=is_shaded, + ) + ], + is_shaded, + ) is_shaded = True shaded = TsShadeCollection( - [TsSurface(shaded_coords, n_vector=n_vector, - param_names=param_names, shaded=is_shaded)], - is_shaded) + [ + TsSurface( + shaded_coords, + n_vector=n_vector, + param_names=param_names, + shaded=is_shaded, + ) + ], + is_shaded, + ) # Create segment - segment = TsSegment(segment_coords, illum, shaded, - n_vector=n_vector, index=i) + segment = TsSegment( + segment_coords, illum, shaded, n_vector=n_vector, index=i + ) list_segments.append(segment) return cls(list_segments, n_vector=n_vector) @@ -379,8 +446,14 @@ def at(self, idx): side = BaseSide(list_geom_segments) return side - def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum']): + @requires_matplotlib + def plot_at_idx( + self, + idx, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + ): """Plot timeseries side at a certain index. Parameters @@ -397,13 +470,14 @@ def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], COLOR_DIC['pvrow_illum']) """ side_geom = self.at(idx) - side_geom.plot(ax, color_shaded=color_shaded, color_illum=color_illum, - with_index=False) + side_geom.plot( + ax, color_shaded=color_shaded, color_illum=color_illum, with_index=False + ) @property def shaded_length(self): """Timeseries shaded length of the side.""" - length = 0. + length = 0.0 for seg in self.list_segments: length += seg.shaded.length return length @@ -411,7 +485,7 @@ def shaded_length(self): @property def length(self): """Timeseries length of side.""" - length = 0. + length = 0.0 for seg in self.list_segments: length += seg.length return length @@ -451,7 +525,7 @@ def get_param_ww(self, param): KeyError if parameter name not in a surface parameters """ - value = 0. + value = 0.0 for seg in self.list_segments: value += seg.get_param_ww(param) return value @@ -488,8 +562,9 @@ class TsSegment(object): """A TsSegment is a timeseries segment that has a timeseries shaded collection and a timeseries illuminated collection.""" - def __init__(self, coords, illum_collection, shaded_collection, - index=None, n_vector=None): + def __init__( + self, coords, illum_collection, shaded_collection, index=None, n_vector=None + ): """Initialize timeseries segment using segment coordinates and timeseries illuminated and shaded surfaces. @@ -531,8 +606,14 @@ def surfaces_at_idx(self, idx): segment = self.at(idx) return segment.all_surfaces - def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum']): + @requires_matplotlib + def plot_at_idx( + self, + idx, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + ): """Plot timeseries segment at a certain index. Parameters @@ -549,8 +630,9 @@ def plot_at_idx(self, idx, ax, color_shaded=COLOR_DIC['pvrow_shaded'], COLOR_DIC['pvrow_illum']) """ segment = self.at(idx) - segment.plot(ax, color_shaded=color_shaded, color_illum=color_illum, - with_index=False) + segment.plot( + ax, color_shaded=color_shaded, color_illum=color_illum, with_index=False + ) def at(self, idx): """Generate a PV segment geometry for the desired index. @@ -569,9 +651,11 @@ def at(self, idx): # Create shaded collection shaded_collection = self.shaded.at(idx) # Create PV segment - segment = PVSegment(illum_collection=illum_collection, - shaded_collection=shaded_collection, - index=self.index) + segment = PVSegment( + illum_collection=illum_collection, + shaded_collection=shaded_collection, + index=self.index, + ) return segment @property @@ -676,8 +760,13 @@ def __init__(self, list_segments=[]): class PVRow: """A PV row is made of two PV row sides, a front and a back one.""" - def __init__(self, front_side=PVRowSide(), back_side=PVRowSide(), - index=None, original_linestring=None): + def __init__( + self, + front_side=PVRowSide(), + back_side=PVRowSide(), + index=None, + original_linestring=None, + ): """Initialize PV row. Parameters @@ -699,8 +788,11 @@ def __init__(self, front_side=PVRowSide(), back_side=PVRowSide(), # Compute the union of the front and back sides, assumedly a # linestring with only two points (TODO: check this assumption / # issue a warning here) - self._linestring = LineString(linemerge(unary_union( - [front_side.geometry, back_side.geometry])).boundary.geoms) + self._linestring = LineString( + linemerge( + unary_union([front_side.geometry, back_side.geometry]) + ).boundary.geoms + ) else: self._linestring = original_linestring @@ -725,8 +817,15 @@ def intersects(self, line): return self._linestring.intersects(line) @classmethod - def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, - index=None, cut={}, param_names=[]): + def from_linestring_coords( + cls, + coords, + shaded=False, + normal_vector=None, + index=None, + cut={}, + param_names=[], + ): """Create a PV row with a single PV surface and using linestring coordinates. @@ -754,24 +853,46 @@ def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, """ index_single_segment = 0 front_side = PVRowSide.from_linestring_coords( - coords, shaded=shaded, normal_vector=normal_vector, - index=index_single_segment, n_segments=cut.get('front', 1), - param_names=param_names) + coords, + shaded=shaded, + normal_vector=normal_vector, + index=index_single_segment, + n_segments=cut.get("front", 1), + param_names=param_names, + ) if normal_vector is not None: - back_n_vec = - np.array(normal_vector) + back_n_vec = -np.array(normal_vector) else: - back_n_vec = - front_side.n_vector + back_n_vec = -front_side.n_vector back_side = PVRowSide.from_linestring_coords( - coords, shaded=shaded, normal_vector=back_n_vec, - index=index_single_segment, n_segments=cut.get('back', 1), - param_names=param_names) - return cls(front_side=front_side, back_side=back_side, index=index, - original_linestring=LineString(coords)) + coords, + shaded=shaded, + normal_vector=back_n_vec, + index=index_single_segment, + n_segments=cut.get("back", 1), + param_names=param_names, + ) + return cls( + front_side=front_side, + back_side=back_side, + index=index, + original_linestring=LineString(coords), + ) @classmethod - def from_center_tilt_width(cls, xy_center, tilt, width, surface_azimuth, - axis_azimuth, shaded=False, normal_vector=None, - index=None, cut={}, param_names=[]): + def from_center_tilt_width( + cls, + xy_center, + tilt, + width, + surface_azimuth, + axis_azimuth, + shaded=False, + normal_vector=None, + index=None, + cut={}, + param_names=[], + ): """Create a PV row using mainly the coordinates of the line center, a tilt angle, and its length. @@ -806,15 +927,26 @@ def from_center_tilt_width(cls, xy_center, tilt, width, surface_azimuth, ------- :py:class:`~pvfactors.geometry.pvrow.PVRow` object """ - coords = _coords_from_center_tilt_length(xy_center, tilt, width, - surface_azimuth, axis_azimuth) - return cls.from_linestring_coords(coords, shaded=shaded, - normal_vector=normal_vector, - index=index, cut=cut, - param_names=param_names) - - def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], - color_illum=COLOR_DIC['pvrow_illum'], with_index=False): + coords = _coords_from_center_tilt_length( + xy_center, tilt, width, surface_azimuth, axis_azimuth + ) + return cls.from_linestring_coords( + coords, + shaded=shaded, + normal_vector=normal_vector, + index=index, + cut=cut, + param_names=param_names, + ) + + @requires_matplotlib + def plot( + self, + ax, + color_shaded=COLOR_DIC["pvrow_shaded"], + color_illum=COLOR_DIC["pvrow_illum"], + with_index=False, + ): """Plot the surfaces of the PV Row. Parameters @@ -831,10 +963,18 @@ def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], Flag to annotate surfaces with their indices (Default = False) """ - self.front.plot(ax, color_shaded=color_shaded, color_illum=color_illum, - with_index=with_index) - self.back.plot(ax, color_shaded=color_shaded, color_illum=color_illum, - with_index=with_index) + self.front.plot( + ax, + color_shaded=color_shaded, + color_illum=color_illum, + with_index=with_index, + ) + self.back.plot( + ax, + color_shaded=color_shaded, + color_illum=color_illum, + with_index=with_index, + ) @property def boundary(self): diff --git a/pvfactors/geometry/timeseries.py b/pvfactors/geometry/timeseries.py index 863f147..15182ab 100644 --- a/pvfactors/geometry/timeseries.py +++ b/pvfactors/geometry/timeseries.py @@ -2,9 +2,11 @@ calculations.""" import numpy as np +from shapely.geometry import GeometryCollection + from pvfactors.config import DISTANCE_TOLERANCE +from pvfactors.geometry._optional_imports import requires_matplotlib from pvfactors.geometry.base import PVSurface, ShadeCollection -from shapely.geometry import GeometryCollection class TsShadeCollection(object): @@ -35,7 +37,7 @@ def list_ts_surfaces(self): @property def length(self): """Total length of the collection""" - length = 0. + length = 0.0 for ts_surf in self._list_ts_surfaces: length += ts_surf.length return length @@ -104,8 +106,11 @@ def at(self, idx): ------- 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] + 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) @@ -113,8 +118,9 @@ class TsSurface(object): """Timeseries surface class: vectorized representation of PV surface geometries.""" - def __init__(self, coords, n_vector=None, param_names=None, index=None, - shaded=False): + def __init__( + self, coords, n_vector=None, param_names=None, index=None, shaded=False + ): """Initialize timeseries surface using timeseries coordinates. Parameters @@ -160,17 +166,21 @@ def at(self, idx): return GeometryCollection() else: # Get normal vector at idx - n_vector = (self.n_vector[:, idx] if self.n_vector is not None - else None) + 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) - + return PVSurface( + self.coords.at(idx), + shaded=self.shaded, + index=self.index, + normal_vector=n_vector, + param_names=self.param_names, + params=params, + ) + + @requires_matplotlib def plot_at_idx(self, idx, ax, color): """Plot timeseries PV row at a certain index, only if it's not too small. @@ -245,8 +255,11 @@ def lowest_point(self): @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, :]])) + u_vector = ( + None + if self.n_vector is None + else np.array([-self.n_vector[1, :], self.n_vector[0, :]]) + ) return u_vector @property @@ -302,8 +315,7 @@ def from_array(cls, coords_array): @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) + return np.sqrt((self.b2.y - self.b1.y) ** 2 + (self.b2.x - self.b1.x) ** 2) @property def as_array(self): @@ -406,6 +418,11 @@ def _get_params_at_idx(idx, params_dict): 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()} + 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() + } diff --git a/pyproject.toml b/pyproject.toml index c428e5d..fc72473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ dynamic = ["version"] description = "2D View Factor Model to calculate the irradiance incident on various surfaces of PV arrays" readme = "README.rst" authors = [ - {name = "SunPower and pvlib python Developers", email = "pvlib-admin@googlegroups.com"}, + { name = "SunPower and pvlib python Developers", email = "pvlib-admin@googlegroups.com" }, ] -license = {text = "BSD 3-Clause"} +license = { text = "BSD 3-Clause" } classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", @@ -25,20 +25,11 @@ classifiers = [ "Topic :: Scientific/Engineering", ] requires-python = ">=3.9" -dependencies = [ - "pvlib>=0.9.0", - "shapely>=2.0", - "matplotlib", - "numpy", - "pandas", -] +dependencies = ["pvlib>=0.9.0", "shapely>=2.0", "numpy", "pandas"] [project.optional-dependencies] -test = [ - "pytest>=3.2.1", - "pytest-mock>=1.10.0", - "mock", -] +plot = ["matplotlib"] +test = ["pytest>=3.2.1", "pytest-mock>=1.10.0", "mock", "matplotlib"] doc = [ "sphinx", "sphinx_rtd_theme", From 82b01dfe9073ef929d0c1629981ba3d0d948802a Mon Sep 17 00:00:00 2001 From: Kurt Rhee <33131958+kurt-rhee@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:28:27 -0700 Subject: [PATCH 2/2] add 1.6.2 whatsnew --- docs/sphinx/whatsnew/v1.6.2.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/sphinx/whatsnew/v1.6.2.rst diff --git a/docs/sphinx/whatsnew/v1.6.2.rst b/docs/sphinx/whatsnew/v1.6.2.rst new file mode 100644 index 0000000..30b1be5 --- /dev/null +++ b/docs/sphinx/whatsnew/v1.6.2.rst @@ -0,0 +1,17 @@ +.. _whatsnew_162: + +v1.6.2 (??) +========== + + +Maintenance +----------- +* Matplotlib is now an optional dependency. (:issue:`31`) + + +Documentation +------------- + +Contributors +------------ +* Kurt Rhee (:ghuser:`kurt-rhee`)