From fd1d56cc79ce6e9a1edaa9846f53599300d4d36e Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:30:47 +0200 Subject: [PATCH 01/30] Add instructions for testing --- README.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 51d75da..68e98b1 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,24 @@ The project can be installed from PyPI using ``pip install solarfactors``. Note that the package is still used from python under the ``pvfactors`` name, i.e. with ``from pvfactors.geometry import OrderedPVArray``. -The original ``pvfactors`` is preserved below: +Testing +------- + +Install test dependencies by running: + +.. code:: sh + + $ pip install pytest mock + +Then run the tests using: + +.. code:: sh + + $ python -m pytest + +You will need to close manually the plots that are generated during the tests. + +The original ``pvfactors`` README is preserved below: pvfactors: irradiance modeling made simple From 676d999f72cffe84d4a83012a34af5eea1b93653 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:35:26 +0200 Subject: [PATCH 02/30] Fix the check on shapely/GEOS --- pvfactors/__init__.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pvfactors/__init__.py b/pvfactors/__init__.py index 1e30107..e4ac5ee 100644 --- a/pvfactors/__init__.py +++ b/pvfactors/__init__.py @@ -5,20 +5,14 @@ logging.basicConfig() try: - from shapely.geos import lgeos # noqa: F401 -except OSError as err: - # https://github.com/SunPower/pvfactors/issues/109 + from shapely import geos_version, geos_capi_version # noqa: F401 +except ImportError as err: msg = ( - "pvfactors encountered an error when importing the shapely package. " - "This often happens when a binary dependency is missing because " - "shapely was installed from PyPI using pip. Try reinstalling shapely " - "from another source like conda-forge with " - "`conda install -c conda-forge shapely`, or alternatively from " - "Christoph Gohlke's website if you're on Windows: " - "https://www.lfd.uci.edu/~gohlke/pythonlibs/#shapely" + "pvfactors detected that the shapely package is not correctly installed. " + "Make sure that you installed the prerequisites, including Shapely and " + "PyGeos, in a supported environment." ) - err.strerror += "; " + msg - raise err + raise ImportError(msg) from err class PVFactorsError(Exception): From 3a5c7f9d5549862a869d204c23c73ddb7d107435 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:17:07 +0200 Subject: [PATCH 03/30] WIP on base.py - compose instead of inheriting --- pvfactors/geometry/base.py | 93 +++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 4cc7d1d..5105fd4 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -9,7 +9,6 @@ from pvfactors.geometry.utils import \ is_collinear, check_collinear, are_2d_vecs_collinear, difference, contains from shapely.geometry import GeometryCollection, LineString -from shapely.geometry.collection import geos_geometrycollection_from_py from shapely.ops import linemerge from pvlib.tools import cosd, sind @@ -159,8 +158,8 @@ def _get_solar_2d_vectors(solar_zenith, solar_azimuth, axis_azimuth): return solar_2d_vector -class BaseSurface(LineString): - """Base surfaces will be extensions of :py:class:`LineString` classes, +class BaseSurface: + """BaseSurface will wrap the :py:class:`LineString` class, but adding an orientation to it (normal vector). So two surfaces could use the same linestring, but have opposite orientations.""" @@ -190,9 +189,8 @@ def __init__(self, coords, normal_vector=None, index=None, params : dict, optional Surface float parameters (Default = None) """ - + self.geometry = LineString(coords) # Composition param_names = [] if param_names is None else param_names - super(BaseSurface, self).__init__(coords) if normal_vector is None: self.n_vector = self._calculate_n_vector() else: @@ -202,10 +200,15 @@ def __init__(self, coords, normal_vector=None, index=None, self.params = params if params is not None \ else dict.fromkeys(self.param_names) + @property + def is_empty(self): + """Check if the surface is empty.""" + return self.geometry.is_empty + def _calculate_n_vector(self): """Calculate normal vector of the surface, if surface is not empty""" - if not self.is_empty: - b1, b2 = self.boundary + if not self.geometry.is_empty: + b1, b2 = self.geometry.boundary.geoms dx = b2.x - b1.x dy = b2.y - b1.y return np.array([-dy, dx]) @@ -231,7 +234,7 @@ def plot(self, ax, color=None, with_index=False): # Prepare text location v = self.n_vector v_norm = v / np.linalg.norm(v) - centroid = self.centroid + centroid = self.geometry.centroid alpha = ALPHA_TEXT x = centroid.x + alpha * v_norm[0] y = centroid.y + alpha * v_norm[1] @@ -256,7 +259,7 @@ def difference(self, linestring): :py:class:`shapely.geometry.LineString` Resulting difference of current surface minus given linestring """ - return difference(self, linestring) + return difference(self.geometry, linestring) def get_param(self, param): """Get parameter value from surface. @@ -289,7 +292,7 @@ def update_params(self, new_dict): class PVSurface(BaseSurface): - """PV surfaces inherit from + """PVSurface inherits from :py:class:`~pvfactors.geometry.base.BaseSurface`. The only difference is that PV surfaces have a ``shaded`` attribute. """ @@ -315,14 +318,13 @@ def __init__(self, coords=None, normal_vector=None, shaded=False, params : dict, optional 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) self.shaded = shaded -class ShadeCollection(GeometryCollection): +class ShadeCollection: """A group of :py:class:`~pvfactors.geometry.base.PVSurface` objects that all have the same shading status. The PV surfaces are not necessarily contiguous or collinear.""" @@ -350,7 +352,21 @@ def __init__(self, list_surfaces=None, shaded=None, param_names=None): self.shaded = self._get_shading(shaded) self.is_collinear = is_collinear(list_surfaces) self.param_names = param_names - super(ShadeCollection, self).__init__(list_surfaces) + + @property + def geometry(self): + """Return a Shapely GeometryCollection built from the current surfaces.""" + return GeometryCollection([_.geometry for _ in self.list_surfaces]) + + @property + def is_empty(self): + """Check if the collection is empty.""" + return self.geometry.is_empty + + @property + def length(self): + """Convenience property to get the total length of all lines in the collection.""" + return self.geometry.length def _get_shading(self, shaded): """Get the surface shading from the provided list of pv surfaces. @@ -413,7 +429,6 @@ def add_pvsurface(self, pvsurface): """ self.list_surfaces.append(pvsurface) self.is_collinear = is_collinear(self.list_surfaces) - super(ShadeCollection, self).__init__(self.list_surfaces) def remove_linestring(self, linestring): """Remove linestring from shade collection. @@ -443,21 +458,7 @@ def remove_linestring(self, linestring): new_list_surfaces.append(new_surface) else: new_list_surfaces.append(surface) - self.list_surfaces = new_list_surfaces - # Force update, even if list is empty - self.update_geom_collection(self.list_surfaces) - - def update_geom_collection(self, list_surfaces): - """Force update of geometry collection, even if list is empty - https://github.com/Toblerity/Shapely/blob/master/shapely/geometry/collection.py#L42 - - Parameters - ---------- - list_surfaces : list of :py:class:`~pvfactors.geometry.base.PVSurface` - New list of PV surfaces to update the shade collection in place - """ - self._geom, self._ndim = geos_geometrycollection_from_py(list_surfaces) def merge_surfaces(self): """Merge all surfaces in the shade collection into one contiguous @@ -602,11 +603,9 @@ def from_linestring_coords(cls, coords, shaded, normal_vector=None, return cls([surf], shaded=shaded, param_names=param_names) -class PVSegment(GeometryCollection): +class PVSegment: """A PV segment will be a collection of 2 collinear and contiguous - shade collections, a shaded one and an illuminated one. It inherits from - :py:class:`shapely.geometry.GeometryCollection` so that users can still - call basic geometrical methods and properties on it, eg call length, etc. + shade collections, a shaded one and an illuminated one. """ def __init__(self, illum_collection=ShadeCollection(shaded=False), @@ -633,8 +632,19 @@ def __init__(self, illum_collection=ShadeCollection(shaded=False), self._illum_collection = illum_collection self.index = index self._all_surfaces = None - super(PVSegment, self).__init__([self._shaded_collection, - self._illum_collection]) + + @property + def geometry(self): + return GeometryCollection([self._shaded_collection.geometry, + self._illum_collection.geometry]) + + @property + def is_empty(self): + return self.geometry.is_empty + + @property + def length(self): + return self.geometry.length def _check_collinear(self, illum_collection, shaded_collection): """Check that all the surfaces in the PV segment are collinear. @@ -920,7 +930,7 @@ def all_surfaces(self): return self._all_surfaces -class BaseSide(GeometryCollection): +class BaseSide: """A side represents a fixed collection of PV segments objects that should all be collinear, with the same normal vector""" @@ -936,7 +946,18 @@ def __init__(self, list_segments=None): check_collinear(list_segments) self.list_segments = tuple(list_segments) self._all_surfaces = None - super(BaseSide, self).__init__(list_segments) + + @property + def geometry(self): + return GeometryCollection([_.geometry for _ in self.list_segments]) + + @property + def is_empty(self): + return self.geometry.is_empty + + @property + def length(self): + return self.geometry.length @classmethod def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, From 1a4a6663328b22b8e5813bdcd1a5d9e89cb936e4 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:49:20 +0200 Subject: [PATCH 04/30] 12 tests of base and 4 of utils pass --- pvfactors/geometry/base.py | 50 +++++++++++++++++++++---------------- pvfactors/geometry/utils.py | 6 ++--- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 5105fd4..5de26d7 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -205,9 +205,31 @@ def is_empty(self): """Check if the surface is empty.""" return self.geometry.is_empty + @property + def length(self): + """Return the length of the surface.""" + return self.geometry.length + + @property + def boundary(self): + """Return the boundary of the surface.""" + return self.geometry.boundary + + def interpolate(self, *args, **kwargs): + """Interpolate along the linestring by the given distance.""" + return self.geometry.interpolate(*args, **kwargs) + + def distance(self, *args, **kwargs): + """Distance between the surface and another geometry.""" + return self.geometry.distance(*args, **kwargs) + + def buffer(self, *args, **kwargs): + """Buffer the surface.""" + return self.geometry.buffer(*args, **kwargs) + def _calculate_n_vector(self): """Calculate normal vector of the surface, if surface is not empty""" - if not self.geometry.is_empty: + if not self.is_empty: b1, b2 = self.geometry.boundary.geoms dx = b2.x - b1.x dy = b2.y - b1.y @@ -486,7 +508,7 @@ def cut_at_point(self, point): for idx, surface in enumerate(self.list_surfaces): if contains(surface, point): # Make sure that not hitting a boundary - b1, b2 = surface.boundary + b1, b2 = surface.boundary.geoms not_hitting_b1 = b1.distance(point) > DISTANCE_TOLERANCE not_hitting_b2 = b2.distance(point) > DISTANCE_TOLERANCE if not_hitting_b1 and not_hitting_b2: @@ -631,7 +653,6 @@ def __init__(self, illum_collection=ShadeCollection(shaded=False), self._shaded_collection = shaded_collection self._illum_collection = illum_collection self.index = index - self._all_surfaces = None @property def geometry(self): @@ -708,7 +729,7 @@ 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.buffer(DISTANCE_TOLERANCE) + intersection = (self._illum_collection.geometry.buffer(DISTANCE_TOLERANCE) .intersection(linestring)) if not intersection.is_empty: # Split up only if interesects the illuminated collection @@ -718,8 +739,6 @@ def cast_shadow(self, linestring): # print(self._shaded_collection.length) self._illum_collection.remove_linestring(intersection) # print(self._illum_collection.length) - super(PVSegment, self).__init__([self._shaded_collection, - self._illum_collection]) def cut_at_point(self, point): """Cut PV segment at point if the segment contains it. @@ -865,16 +884,12 @@ def shaded_collection(self, new_collection): """ assert new_collection.shaded, "surface should be shaded" self._shaded_collection = new_collection - super(PVSegment, self).__init__([self._shaded_collection, - self._illum_collection]) @shaded_collection.deleter def shaded_collection(self): """Delete shaded collection of PV segment and replace with empty one. """ self._shaded_collection = ShadeCollection(shaded=True) - super(PVSegment, self).__init__([self._shaded_collection, - self._illum_collection]) @property def illum_collection(self): @@ -892,16 +907,12 @@ def illum_collection(self, new_collection): """ assert not new_collection.shaded, "surface should not be shaded" self._illum_collection = new_collection - super(PVSegment, self).__init__([self._shaded_collection, - self._illum_collection]) @illum_collection.deleter def illum_collection(self): """Delete illuminated collection of PV segment and replace with empty one.""" self._illum_collection = ShadeCollection(shaded=False) - super(PVSegment, self).__init__([self._shaded_collection, - self._illum_collection]) @property def shaded_length(self): @@ -923,11 +934,8 @@ def all_surfaces(self): list of :py:class:`~pvfactors.geometry.base.PVSurface` PV surfaces in the PV segment """ - if self._all_surfaces is None: - self._all_surfaces = [] - self._all_surfaces += self._illum_collection.list_surfaces - self._all_surfaces += self._shaded_collection.list_surfaces - return self._all_surfaces + return self._illum_collection.list_surfaces + \ + self._shaded_collection.list_surfaces class BaseSide: @@ -1091,11 +1099,11 @@ def cut_at_point(self, point): Point where to cut side geometry, if the latter contains the former """ - if contains(self, point): + if contains(self.geometry, point): for segment in self.list_segments: # Nothing will happen to the segments that do not contain # the point - segment.cut_at_point(point) + segment.geometry.cut_at_point(point) def get_param_weighted(self, param): """Get the parameter from the side's surfaces, after weighting diff --git a/pvfactors/geometry/utils.py b/pvfactors/geometry/utils.py index 8025768..611a846 100644 --- a/pvfactors/geometry/utils.py +++ b/pvfactors/geometry/utils.py @@ -23,8 +23,8 @@ def difference(u, v): :py:class:`shapely.geometry.LineString` Resulting difference of current surface minus given linestring """ - ub1, ub2 = u.boundary - vb1, vb2 = v.boundary + ub1, ub2 = u.boundary.geoms + vb1, vb2 = v.boundary.geoms u_contains_vb1 = contains(u, vb1) u_contains_vb2 = contains(u, vb2) v_contains_ub1 = contains(v, ub1) @@ -130,7 +130,7 @@ def projection(point, vector, linestring, must_contain=True): a, b = -vector[1], vector[0] c = - (a * point.x + b * point.y) # Define equation d*x + e*y +f = 0 - b1, b2 = linestring.boundary + b1, b2 = linestring.boundary.geoms d, e = - (b2.y - b1.y), b2.x - b1.x f = - (d * b1.x + e * b1.y) # TODO: check that two lines are not parallel From f47880192ae26fbd64c3dd83272e3c242c4733a2 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:49:58 +0200 Subject: [PATCH 05/30] Whitespace --- pvfactors/tests/test_geometry/test_base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pvfactors/tests/test_geometry/test_base.py b/pvfactors/tests/test_geometry/test_base.py index 1813aae..8a45e64 100644 --- a/pvfactors/tests/test_geometry/test_base.py +++ b/pvfactors/tests/test_geometry/test_base.py @@ -10,9 +10,7 @@ def test_baseside(pvsegments): """Test that the basic BaseSide functionalities work""" - side = BaseSide(pvsegments) - np.testing.assert_array_equal(side.n_vector, [0, 1]) assert side.shaded_length == 1. @@ -147,7 +145,6 @@ def test_cast_shadow_side(): def test_pvsurface_difference_precision_error(): """This would lead to wrong result using shapely ``difference`` method""" - surf_1 = PVSurface([(0, 0), (3, 2)]) surf_2 = PVSurface([surf_1.interpolate(1), Point(6, 4)]) diff = surf_1.difference(surf_2) From 8ca29b658e3d9642a2693520e61da8c01b49982b Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:50:13 +0200 Subject: [PATCH 06/30] Fix in plot --- pvfactors/geometry/plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvfactors/geometry/plot.py b/pvfactors/geometry/plot.py index 8a2dc6b..5b7afb3 100644 --- a/pvfactors/geometry/plot.py +++ b/pvfactors/geometry/plot.py @@ -33,10 +33,10 @@ def plot_bounds(ax, ob): """ # Check if shadow reduces to one point (for very specific sun alignment) - if len(ob.boundary) == 0: + if len(ob.boundary.geoms) == 0: x, y = ob.coords[0] else: - x, y = zip(*list((p.x, p.y) for p in ob.boundary)) + x, y = zip(*list((p.x, p.y) for p in ob.boundary.geoms)) ax.plot(x, y, 'o', color='#000000', zorder=1) From 1ddd5436f0c215865f100f71f8fc411fd8088a4c Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:55:09 +0200 Subject: [PATCH 07/30] 6 utils test pass, with 1 warning --- pvfactors/geometry/base.py | 3 +++ pvfactors/tests/test_geometry/test_utils.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 5de26d7..8622242 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -967,6 +967,9 @@ def is_empty(self): def length(self): return self.geometry.length + def distance(self, *args, **kwargs): + return self.geometry.distance(*args, **kwargs) + @classmethod def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, index=None, n_segments=1, param_names=None): diff --git a/pvfactors/tests/test_geometry/test_utils.py b/pvfactors/tests/test_geometry/test_utils.py index 6f81e15..bb47dc0 100644 --- a/pvfactors/tests/test_geometry/test_utils.py +++ b/pvfactors/tests/test_geometry/test_utils.py @@ -22,17 +22,17 @@ def test_projection(): # Should be 2nd boundary line_3 = LineString([(0, 0), (1, 0)]) inter = projection(pt_1, vector, line_3) - assert inter == line_3.boundary[1] + assert inter == line_3.boundary.geoms[1] # Should be 1st boundary pt_2 = Point(0, 1) inter = projection(pt_2, vector, line_3) - assert inter == line_3.boundary[0] + assert inter == line_3.boundary.geoms[0] # Should be 1st boundary: very close pt_3 = Point(0 + 1e-9, 1) inter = projection(pt_3, vector, line_3) - assert inter == line_3.boundary[0] + assert inter == line_3.boundary.geoms[0] # Should be empty: very close pt_4 = Point(0 - 1e-9, 1) From 92bee9df99fd7f764a93c83d1b01a135266d6e17 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:04:49 +0200 Subject: [PATCH 08/30] More adaptations -> 86 passed, 16 failed --- pvfactors/geometry/base.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 8622242..be1610a 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -390,6 +390,10 @@ def length(self): """Convenience property to get the total length of all lines in the collection.""" return self.geometry.length + def distance(self, *args, **kwargs): + """Distance between the collection and another geometry.""" + return self.geometry.distance(*args, **kwargs) + def _get_shading(self, shaded): """Get the surface shading from the provided list of pv surfaces. @@ -486,7 +490,7 @@ def merge_surfaces(self): """Merge all surfaces in the shade collection into one contiguous surface, even if they're not contiguous, by using bounds.""" if len(self.list_surfaces) > 1: - merged_lines = linemerge(self.list_surfaces) + merged_lines = linemerge(self.geometry) minx, miny, maxx, maxy = merged_lines.bounds surf_1 = self.list_surfaces[0] new_pvsurf = PVSurface( @@ -494,7 +498,6 @@ def merge_surfaces(self): shaded=self.shaded, normal_vector=surf_1.n_vector, param_names=surf_1.param_names) self.list_surfaces = [new_pvsurf] - self.update_geom_collection(self.list_surfaces) def cut_at_point(self, point): """Cut collection at point if the collection contains it. @@ -526,7 +529,6 @@ def cut_at_point(self, point): # Now update collection self.list_surfaces[idx] = new_surf_1 self.list_surfaces.append(new_surf_2) - self.update_geom_collection(self.list_surfaces) # No need to continue the loop break @@ -667,6 +669,9 @@ def is_empty(self): def length(self): return self.geometry.length + def distance(self, *args, **kwargs): + return self.geometry.distance(*args, **kwargs) + def _check_collinear(self, illum_collection, shaded_collection): """Check that all the surfaces in the PV segment are collinear. @@ -1106,7 +1111,7 @@ def cut_at_point(self, point): for segment in self.list_segments: # Nothing will happen to the segments that do not contain # the point - segment.geometry.cut_at_point(point) + segment.cut_at_point(point) def get_param_weighted(self, param): """Get the parameter from the side's surfaces, after weighting From f1386cf725dd50553777c6397464ab795c789f50 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:13:46 +0200 Subject: [PATCH 09/30] Implemen cache of _geometry for ShadeCollection and PVSegment --- pvfactors/geometry/base.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index be1610a..7188149 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -374,11 +374,16 @@ def __init__(self, list_surfaces=None, shaded=None, param_names=None): self.shaded = self._get_shading(shaded) self.is_collinear = is_collinear(list_surfaces) self.param_names = param_names + self._geometry_valid = False + self._geometry = None @property def geometry(self): """Return a Shapely GeometryCollection built from the current surfaces.""" - return GeometryCollection([_.geometry for _ in self.list_surfaces]) + if not self._geometry_valid: + self._geometry = GeometryCollection([_.geometry for _ in self.list_surfaces]) + self._geometry_valid = True + return self._geometry @property def is_empty(self): @@ -455,6 +460,7 @@ def add_pvsurface(self, pvsurface): """ self.list_surfaces.append(pvsurface) self.is_collinear = is_collinear(self.list_surfaces) + self._geometry_valid = False def remove_linestring(self, linestring): """Remove linestring from shade collection. @@ -485,6 +491,7 @@ def remove_linestring(self, linestring): else: new_list_surfaces.append(surface) self.list_surfaces = new_list_surfaces + self._geometry_valid = False def merge_surfaces(self): """Merge all surfaces in the shade collection into one contiguous @@ -498,6 +505,7 @@ def merge_surfaces(self): shaded=self.shaded, normal_vector=surf_1.n_vector, param_names=surf_1.param_names) self.list_surfaces = [new_pvsurf] + self._geometry_valid = False def cut_at_point(self, point): """Cut collection at point if the collection contains it. @@ -530,6 +538,7 @@ def cut_at_point(self, point): self.list_surfaces[idx] = new_surf_1 self.list_surfaces.append(new_surf_2) # No need to continue the loop + self._geometry_valid = False break def get_param_weighted(self, param): @@ -655,11 +664,16 @@ def __init__(self, illum_collection=ShadeCollection(shaded=False), self._shaded_collection = shaded_collection self._illum_collection = illum_collection self.index = index + self._geometry_valid = False + self._geometry = None @property def geometry(self): - return GeometryCollection([self._shaded_collection.geometry, - self._illum_collection.geometry]) + if not self._geometry_valid: + self._geometry = GeometryCollection([self._shaded_collection.geometry, + self._illum_collection.geometry]) + self._geometry_valid = True + return self._geometry @property def is_empty(self): @@ -737,13 +751,11 @@ def cast_shadow(self, linestring): intersection = (self._illum_collection.geometry.buffer(DISTANCE_TOLERANCE) .intersection(linestring)) if not intersection.is_empty: - # Split up only if interesects the illuminated collection - # print(intersection) + # Split up only if intersects the illuminated collection self._shaded_collection.add_linestring(intersection, normal_vector=self.n_vector) - # print(self._shaded_collection.length) self._illum_collection.remove_linestring(intersection) - # print(self._illum_collection.length) + self._geometry_valid = False def cut_at_point(self, point): """Cut PV segment at point if the segment contains it. @@ -759,6 +771,7 @@ def cut_at_point(self, point): self._illum_collection.cut_at_point(point) else: self._shaded_collection.cut_at_point(point) + self._geometry_valid = False def get_param_weighted(self, param): """Get the parameter from the segment's surfaces, after weighting @@ -889,12 +902,14 @@ def shaded_collection(self, new_collection): """ assert new_collection.shaded, "surface should be shaded" self._shaded_collection = new_collection + self._geometry_valid = False @shaded_collection.deleter def shaded_collection(self): """Delete shaded collection of PV segment and replace with empty one. """ self._shaded_collection = ShadeCollection(shaded=True) + self._geometry_valid = False @property def illum_collection(self): @@ -912,12 +927,14 @@ def illum_collection(self, new_collection): """ assert not new_collection.shaded, "surface should not be shaded" self._illum_collection = new_collection + self._geometry_valid = False @illum_collection.deleter def illum_collection(self): """Delete illuminated collection of PV segment and replace with empty one.""" self._illum_collection = ShadeCollection(shaded=False) + self._geometry_valid = False @property def shaded_length(self): From c511b4d0e9f52106a952ba1bef4e18954a265c0f Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:26:16 +0200 Subject: [PATCH 10/30] Property coords and iterate over difference geometries 90 vs. 12 --- pvfactors/geometry/base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 7188149..832c2ad 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -215,6 +215,10 @@ def boundary(self): """Return the boundary of the surface.""" return self.geometry.boundary + @property + def coords(self): + return self.geometry.coords + def interpolate(self, *args, **kwargs): """Interpolate along the linestring by the given distance.""" return self.geometry.interpolate(*args, **kwargs) @@ -479,9 +483,11 @@ def remove_linestring(self, linestring): difference = surface.difference(linestring) # We want to make sure we can iterate on it, as # ``difference`` can be a multi-part geometry or not - if not hasattr(difference, '__iter__'): - difference = [difference] - for new_geom in difference: + if isinstance(difference, LineString): + geoms = [difference] + else: + geoms = difference.geoms + for new_geom in geoms: if not new_geom.is_empty: new_surface = PVSurface( new_geom.coords, normal_vector=surface.n_vector, From d90f5a7dc0b0314e7234b82c9e942eb67d6bfa94 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:47:37 +0200 Subject: [PATCH 11/30] Add method intersects to BaseSide --- pvfactors/geometry/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 832c2ad..9015ad9 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -967,7 +967,7 @@ def all_surfaces(self): class BaseSide: - """A side represents a fixed collection of PV segments objects that should + """A side represents a fixed collection of PV segment objects that should all be collinear, with the same normal vector""" def __init__(self, list_segments=None): @@ -998,6 +998,9 @@ def length(self): def distance(self, *args, **kwargs): return self.geometry.distance(*args, **kwargs) + 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): From 7525d929fce6d2b437a1b49e84ec42a081bfe7ee Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:48:47 +0200 Subject: [PATCH 12/30] Migrate PVRow * Remove inheritance from GeometryCollection * Add length property and intersects function --- pvfactors/geometry/pvrow.py | 39 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/pvfactors/geometry/pvrow.py b/pvfactors/geometry/pvrow.py index 66fe966..fd11573 100644 --- a/pvfactors/geometry/pvrow.py +++ b/pvfactors/geometry/pvrow.py @@ -654,9 +654,10 @@ def n_ts_surfaces(self): class PVRowSide(BaseSide): """A PV row side represents the whole surface of one side of a PV row. + At its core it will contain a fixed number of :py:class:`~pvfactors.geometry.base.PVSegment` objects that will together - constitue one side of a PV row: a PV row side can also be + constitute one side of a PV row: a PV row side can also be "discretized" into multiple segments""" def __init__(self, list_segments=[]): @@ -671,7 +672,7 @@ def __init__(self, list_segments=[]): super(PVRowSide, self).__init__(list_segments) -class PVRow(GeometryCollection): +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(), @@ -689,14 +690,34 @@ def __init__(self, front_side=PVRowSide(), back_side=PVRowSide(), original_linestring : :py:class:`shapely.geometry.LineString`, optional Full continuous linestring that the PV row will be made of (Default = None) - """ self.front = front_side self.back = back_side self.index = index self.original_linestring = original_linestring - self._all_surfaces = None - super(PVRow, self).__init__([self.front, self.back]) + + @property + def length(self): + """Length of the PV row.""" + return self.front.length + self.back.length + + def intersects(self, line): + """Check if the PV row intersects with a line. + + Parameters + ---------- + line : :py:class:`shapely.geometry.LineString` + Line to check for intersection + + Returns + ------- + bool + True if the PV row intersects with the line, False otherwise + """ + if self.original_linestring is not None: + return self.original_linestring.intersects(line) + else: + return self.front.intersects(line) or self.back.intersects(line) @classmethod def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, @@ -812,7 +833,7 @@ def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], @property def boundary(self): - """Boundaries of the PV Row's orginal linestring.""" + """Boundaries of the PV Row's original linestring.""" return self.original_linestring.boundary @property @@ -832,11 +853,7 @@ def lowest_point(self): @property def all_surfaces(self): """List of all the surfaces in the PV row.""" - if self._all_surfaces is None: - self._all_surfaces = [] - self._all_surfaces += self.front.all_surfaces - self._all_surfaces += self.back.all_surfaces - return self._all_surfaces + return self.front.all_surfaces + self.back.all_surfaces @property def surface_indices(self): From a4746fb097677e6e741bab74267cb8a0fbfcee9c Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:53:55 +0200 Subject: [PATCH 13/30] Add centroid property to BaseSurface --- pvfactors/geometry/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index 9015ad9..b5ff792 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -219,6 +219,10 @@ def boundary(self): def coords(self): return self.geometry.coords + @property + def centroid(self): + return self.geometry.centroid + def interpolate(self, *args, **kwargs): """Interpolate along the linestring by the given distance.""" return self.geometry.interpolate(*args, **kwargs) From 7f9cbe35a14a0c1baa2d4656c0db61841b1938ed Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:54:08 +0200 Subject: [PATCH 14/30] Fix plots --- pvfactors/geometry/plot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvfactors/geometry/plot.py b/pvfactors/geometry/plot.py index 5b7afb3..235e794 100644 --- a/pvfactors/geometry/plot.py +++ b/pvfactors/geometry/plot.py @@ -13,10 +13,10 @@ def plot_coords(ax, ob): """ try: - x, y = ob.xy + x, y = ob.geometry.xy ax.plot(x, y, 'o', color='#999999', zorder=1) except NotImplementedError: - for line in ob: + for line in ob.geometry.geoms: x, y = line.xy ax.plot(x, y, 'o', color='#999999', zorder=1) @@ -54,11 +54,11 @@ def plot_line(ax, ob, line_color): """ try: - x, y = ob.xy + x, y = ob.geometry.xy ax.plot(x, y, color=line_color, alpha=0.7, linewidth=3, solid_capstyle='round', zorder=2) except NotImplementedError: - for line in ob: + 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) From 2fabbe4c5ad894f9a08e9486f28a01016f668d46 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:21:47 +0200 Subject: [PATCH 15/30] All tests pass --- pvfactors/geometry/pvground.py | 8 ++++---- pvfactors/geometry/pvrow.py | 25 +++++++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pvfactors/geometry/pvground.py b/pvfactors/geometry/pvground.py index f336c2a..93d2f4c 100644 --- a/pvfactors/geometry/pvground.py +++ b/pvfactors/geometry/pvground.py @@ -719,8 +719,8 @@ 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[0], - surface.boundary[1]] + coords = [surface_to_merge.boundary.geoms[0], + surface.boundary.geoms[1]] surface = PVSurface( coords, shaded=True, param_names=self.param_names, @@ -735,8 +735,8 @@ 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[0], - surface.boundary[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, diff --git a/pvfactors/geometry/pvrow.py b/pvfactors/geometry/pvrow.py index fd11573..abd52ba 100644 --- a/pvfactors/geometry/pvrow.py +++ b/pvfactors/geometry/pvrow.py @@ -1,6 +1,7 @@ """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 @@ -694,13 +695,24 @@ def __init__(self, front_side=PVRowSide(), back_side=PVRowSide(), self.front = front_side self.back = back_side self.index = index - self.original_linestring = original_linestring + if original_linestring is None: + # 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) + else: + self._linestring = original_linestring @property def length(self): """Length of the PV row.""" return self.front.length + self.back.length + @property + def boundary(self): + return self._linestring.boundary + def intersects(self, line): """Check if the PV row intersects with a line. @@ -714,10 +726,7 @@ def intersects(self, line): bool True if the PV row intersects with the line, False otherwise """ - if self.original_linestring is not None: - return self.original_linestring.intersects(line) - else: - return self.front.intersects(line) or self.back.intersects(line) + return self._linestring.intersects(line) @classmethod def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, @@ -740,7 +749,7 @@ def from_linestring_coords(cls, coords, shaded=False, normal_vector=None, Eg {'front': 3, 'back': 2} will lead to 3 segments on front side and 2 segments on back side. (Default = {}) param_names : list of str, optional - Names of the surface parameters, eg reflectivity, total incident + Names of the surface parameters, e.g. reflectivity, total incident irradiance, temperature, etc. (Default = []) Returns @@ -834,12 +843,12 @@ def plot(self, ax, color_shaded=COLOR_DIC['pvrow_shaded'], @property def boundary(self): """Boundaries of the PV Row's original linestring.""" - return self.original_linestring.boundary + return self._linestring.boundary @property def highest_point(self): """Highest point of the PV Row.""" - b1, b2 = self.boundary + b1, b2 = self.boundary.geoms highest_point = b1 if b1.y > b2.y else b2 return highest_point From 7d38df6e3c890092afeb50072e91fde063c76309 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:23:22 +0200 Subject: [PATCH 16/30] Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da5c165..3975118 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pvlib>=0.9.0 -shapely>=1.6.4.post2,<2 +shapely>=2.0 matplotlib future six From 36e39f8751248cae6ba6a7d76f0df2efbb1bcf92 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 18:45:56 +0200 Subject: [PATCH 17/30] Removed a line --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index 68e98b1..dc4c9ef 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,6 @@ This fork exists so that the pvfactors model can continue to be used with `pvlib python `_ even though the original repository is no longer maintained. The objective is to provide a working dependency for the existing pvfactors functionality currently in pvlib python. -New features may be added, but don't count on it. Documentation for this fork can be found at `Read The Docs `_. From 594b1224e08789eb106d44e5f96683794147b65b Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:14:20 +0200 Subject: [PATCH 18/30] Bring README to relevance (links, etc.) --- README.rst | 81 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index dc4c9ef..9515b74 100644 --- a/README.rst +++ b/README.rst @@ -10,31 +10,12 @@ This fork exists so that the pvfactors model can continue to be used with repository is no longer maintained. The objective is to provide a working dependency for the existing pvfactors functionality currently in pvlib python. -Documentation for this fork can be found at `Read The Docs `_. +Documentation for this fork can be found at `Read the Docs `_. The project can be installed from PyPI using ``pip install solarfactors``. Note that the package is still used from python under the ``pvfactors`` name, i.e. with ``from pvfactors.geometry import OrderedPVArray``. -Testing -------- - -Install test dependencies by running: - -.. code:: sh - - $ pip install pytest mock - -Then run the tests using: - -.. code:: sh - - $ python -m pytest - -You will need to close manually the plots that are generated during the tests. - -The original ``pvfactors`` README is preserved below: - pvfactors: irradiance modeling made simple ========================================== @@ -50,6 +31,8 @@ equations to account for reflections between all of the surfaces. pvfactors was originally ported from the SunPower developed 'vf_model' package, which was introduced at the IEEE PV Specialist Conference 44 2017 (see [#pvfactors_paper]_ and link_ to paper). +This fork, `pvlib/solarfactors `_ is maintained by the pvlib project with contributions from the pvlib community. + ------------------------------------------ .. contents:: Table of contents @@ -60,8 +43,8 @@ pvfactors was originally ported from the SunPower developed 'vf_model' package, Documentation ------------- -The documentation can be found `here `_. -It includes a lot of tutorials_ that describe the different ways of using pvfactors. +The documentation of this fork can be found `here `_. +It includes a lot of tutorials_ that describe the different ways of using solarfactors. Quick Start @@ -203,11 +186,11 @@ The users can also create a "report" while running the simulations that will rel Installation ------------ -pvfactors is currently compatible and tested with 3.6+, and is available in `PyPI `_. The easiest way to install pvfactors is to use pip_ as follows: +pvfactors is currently compatible and tested with Python 3.11 and Shapely 2.0.6, and is available in `PyPI `_. The easiest way to install solarfactors is to use pip_ as follows: .. code:: sh - $ pip install pvfactors + $ pip install solarfactors The package wheel files are also available in the `release section`_ of the Github repository. @@ -219,13 +202,13 @@ Requirements are included in the ``requirements.txt`` file of the package. Here * `numpy `_ * `pvlib-python `_ -* `shapely `_ +* `shapely `_ (version >= 2.0) Citing pvfactors ---------------- -We appreciate your use of pvfactors. If you use pvfactors in a published work, we kindly ask that you cite: +If you use openfactors in a published work, cite the following paper: .. parsed-literal:: @@ -236,8 +219,9 @@ We appreciate your use of pvfactors. If you use pvfactors in a published work, w Contributing ------------ -Contributions are needed in order to improve pvfactors. -If you wish to contribute, you can start by forking and cloning the repository, and then installing pvfactors using pip_ in the root folder of the package: +Contributions are needed in order to improve openfactors. + +If you wish to contribute, you can start by forking and cloning the repository, and then installing openfactors using pip_ in the root folder of the package: .. code:: sh @@ -250,6 +234,25 @@ To install the package in editable mode, you can use: $ pip install -e . + +Testing ++++++++ + +Install test dependencies by running: + +.. code:: sh + + $ pip install pytest mock + +Then run the tests using: + +.. code:: sh + + $ python -m pytest + +You will need to close manually the plots that are generated during the tests, unless you define the ``CI`` environment variable, which will disable the tests that generate plots. + + Releasing +++++++++ @@ -270,27 +273,27 @@ References .. _link: https://pdfs.semanticscholar.org/ebb2/35e3c3796b158e1a3c45b40954e60d876ea9.pdf -.. _tutorials: https://sunpower.github.io/pvfactors/tutorials/index.html +.. _tutorials: https://solarfactors.readthedocs.io/en/latest/tutorials/index.html -.. _`full mode`: https://sunpower.github.io/pvfactors/theory/problem_formulation.html#full-simulations +.. _`full mode`: https://solarfactors.readthedocs.io/en/latest/theory/problem_formulation.html#full-simulations -.. _`fast mode`: https://sunpower.github.io/pvfactors/theory/problem_formulation.html#fast-simulations +.. _`fast mode`: https://solarfactors.readthedocs.io/en/latest/theory/problem_formulation.html#fast-simulations .. _pip: https://pip.pypa.io/en/stable/ -.. _`release section`: https://github.com/SunPower/pvfactors/releases +.. _`release section`: https://github.com/pvlib/solarfactors/releases -.. |Logo| image:: https://raw.githubusercontent.com/SunPower/pvfactors/master/docs/sphinx/_static/logo.png - :target: http://sunpower.github.io/pvfactors/ +.. |Logo| image:: https://github.com/pvlib/solarfactors/blob/main/docs/sphinx/_static/logo_small.png?raw=true + :target: https://solarfactors.readthedocs.io/en/latest/index.html .. |CircleCI| image:: https://circleci.com/gh/SunPower/pvfactors.svg?style=shield :target: https://circleci.com/gh/SunPower/pvfactors .. |License| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg - :target: https://github.com/SunPower/pvfactors/blob/master/LICENSE + :target: https://github.com/pvlib/solarfactors/blob/main/LICENSE -.. |PyPI-Status| image:: https://img.shields.io/pypi/v/pvfactors.svg - :target: https://pypi.org/project/pvfactors +.. |PyPI-Status| image:: https://img.shields.io/pypi/v/solarfactors.svg + :target: https://pypi.org/project/solarfactors/ -.. |PyPI-Versions| image:: https://img.shields.io/pypi/pyversions/pvfactors.svg?logo=python&logoColor=white - :target: https://pypi.org/project/pvfactors +.. |PyPI-Versions| image:: https://img.shields.io/pypi/pyversions/solarfactors.svg?logo=python&logoColor=white + :target: https://pypi.org/project/solarfactors/ From 783c4517246b8ec30f1dcc4a70d1794435318eb4 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:20:46 +0200 Subject: [PATCH 19/30] Use dwithin rather than distance The distance method would raise "RuntimeWarning: invalid value encountered in distance" when acting on a linestring which is a single point. The dwithin method does not. --- pvfactors/geometry/base.py | 14 ++++++++++++++ pvfactors/geometry/utils.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pvfactors/geometry/base.py b/pvfactors/geometry/base.py index b5ff792..58a5df2 100644 --- a/pvfactors/geometry/base.py +++ b/pvfactors/geometry/base.py @@ -231,6 +231,10 @@ def distance(self, *args, **kwargs): """Distance between the surface and another geometry.""" return self.geometry.distance(*args, **kwargs) + def dwithin(self, *args, **kwargs): + """Check if the surface is within a certain distance of another geometry.""" + return self.geometry.dwithin(*args, **kwargs) + def buffer(self, *args, **kwargs): """Buffer the surface.""" return self.geometry.buffer(*args, **kwargs) @@ -407,6 +411,10 @@ def distance(self, *args, **kwargs): """Distance between the collection and another geometry.""" return self.geometry.distance(*args, **kwargs) + def dwithin(self, *args, **kwargs): + """Check if the collection is within a certain distance of another geometry.""" + return self.geometry.dwithin(*args, **kwargs) + def _get_shading(self, shaded): """Get the surface shading from the provided list of pv surfaces. @@ -696,6 +704,9 @@ def length(self): def distance(self, *args, **kwargs): return self.geometry.distance(*args, **kwargs) + def dwithin(self, *args, **kwargs): + return self.geometry.dwithin(*args, **kwargs) + def _check_collinear(self, illum_collection, shaded_collection): """Check that all the surfaces in the PV segment are collinear. @@ -1002,6 +1013,9 @@ def length(self): def distance(self, *args, **kwargs): return self.geometry.distance(*args, **kwargs) + def dwithin(self, *args, **kwargs): + return self.geometry.dwithin(*args, **kwargs) + def intersects(self, *args, **kwargs): return self.geometry.intersects(*args, **kwargs) diff --git a/pvfactors/geometry/utils.py b/pvfactors/geometry/utils.py index 611a846..1906e47 100644 --- a/pvfactors/geometry/utils.py +++ b/pvfactors/geometry/utils.py @@ -73,7 +73,7 @@ def difference(u, v): def contains(linestring, point, tol_distance=DISTANCE_TOLERANCE): """Fixing floating point errors obtained in shapely for contains""" - return linestring.distance(point) < tol_distance + return linestring.dwithin(point, tol_distance) def is_collinear(list_elements): From ff383e364c15a65d984b4a786830c255da299f48 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:05:21 +0200 Subject: [PATCH 20/30] Fix all but 3 divide-by-zero RuntimeWarning Where np.where was used, replaced by np.divide with a where argument --- pvfactors/viewfactors/vfmethods.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pvfactors/viewfactors/vfmethods.py b/pvfactors/viewfactors/vfmethods.py index 272abb6..aeb1645 100644 --- a/pvfactors/viewfactors/vfmethods.py +++ b/pvfactors/viewfactors/vfmethods.py @@ -176,9 +176,9 @@ def vf_pvrow_surf_to_gnd_surf_obstruction_hottel( # Use reciprocity to calculate ts vf from gnd surf to pv row surface gnd_surf_length = gnd_surf.length - vf_gnd_to_pvrow_surf = np.where( - gnd_surf_length > DISTANCE_TOLERANCE, - vf_pvrow_to_gnd_surf * pvrow_surf_length / gnd_surf_length, 0.) + vf_gnd_to_pvrow_surf = np.divide( + vf_pvrow_to_gnd_surf * pvrow_surf_length, gnd_surf_length, + where=gnd_surf_length > DISTANCE_TOLERANCE, out=np.zeros_like(gnd_surf_length)) return vf_pvrow_to_gnd_surf, vf_gnd_to_pvrow_surf @@ -239,9 +239,10 @@ def vf_pvrow_to_pvrow(self, ts_pvrows, tilted_to_left, vf_matrix): vf_i_to_j = self._vf_surface_to_surface( surf_i.coords, surf_j.coords, length_i) vf_i_to_j = np.where(tilted_to_left, vf_i_to_j, 0.) - vf_j_to_i = np.where( - surf_j.length > DISTANCE_TOLERANCE, - vf_i_to_j * length_i / length_j, 0.) + vf_j_to_i = np.divide( + vf_i_to_j * length_i , length_j, + where=length_j > DISTANCE_TOLERANCE, + out=np.zeros_like(length_j)) vf_matrix[i, j, :] = vf_i_to_j vf_matrix[j, i, :] = vf_j_to_i @@ -529,9 +530,10 @@ def _vf_hottel_gnd_surf(self, high_pt_pv, low_pt_pv, left_pt_gnd, shadow_is_left) d2 = self._hottel_string_length(low_pt_pv, right_pt_gnd, obstr_pt, shadow_is_left) - vf_1_to_2 = (d1 + d2 - l1 - l2) / (2. * width) # The formula doesn't work if surface is a point - vf_1_to_2 = np.where(width > DISTANCE_TOLERANCE, vf_1_to_2, 0.) + vf_1_to_2 = np.divide(d1 + d2 - l1 - l2, 2. * width, + where=width > DISTANCE_TOLERANCE, + out=np.zeros_like(width)) return vf_1_to_2 @@ -605,9 +607,10 @@ def _vf_surface_to_surface(self, line_1, line_2, width_1): length_4 = self._distance(line_1.b2, line_2.b1) sum_1 = length_1 + length_2 sum_2 = length_3 + length_4 - vf_1_to_2 = np.abs(sum_2 - sum_1) / (2. * width_1) # The formula doesn't work if the line is a point - vf_1_to_2 = np.where(width_1 > DISTANCE_TOLERANCE, vf_1_to_2, 0.) + vf_1_to_2 = np.divide(np.abs(sum_2 - sum_1), 2. * width_1, + where=width_1 > DISTANCE_TOLERANCE, + out=np.zeros_like(width_1)) return vf_1_to_2 From c7748615353f9b852340b58c7258b77c38843b65 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:44:10 +0200 Subject: [PATCH 21/30] Fix an error message and README --- README.rst | 2 +- pvfactors/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 9515b74..9c064dc 100644 --- a/README.rst +++ b/README.rst @@ -186,7 +186,7 @@ The users can also create a "report" while running the simulations that will rel Installation ------------ -pvfactors is currently compatible and tested with Python 3.11 and Shapely 2.0.6, and is available in `PyPI `_. The easiest way to install solarfactors is to use pip_ as follows: +solarfactors is currently compatible and tested with Python 3.11 and Shapely 2.0.6, and is available in `PyPI `_. The easiest way to install solarfactors is to use pip_ as follows: .. code:: sh diff --git a/pvfactors/__init__.py b/pvfactors/__init__.py index e4ac5ee..0d21107 100644 --- a/pvfactors/__init__.py +++ b/pvfactors/__init__.py @@ -8,9 +8,9 @@ from shapely import geos_version, geos_capi_version # noqa: F401 except ImportError as err: msg = ( - "pvfactors detected that the shapely package is not correctly installed. " - "Make sure that you installed the prerequisites, including Shapely and " - "PyGeos, in a supported environment." + "pvfactors detected that the Shapely package is not correctly installed. " + "Make sure that you installed the prerequisites, including Shapely version " + "2.0+, in a supported environment." ) raise ImportError(msg) from err From 0fb57bbc8764c808a0a9f38ca83b0a7521351d90 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:06:18 +0200 Subject: [PATCH 22/30] Fix for failing Read the Docs build We were getting the following error: "The sphinx.configuration key is missing. This key is now required, see our blog post for more information." With the following blog post link: https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/ --- readthedocs.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/readthedocs.yml b/readthedocs.yml index a75f7f2..477db5e 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -3,11 +3,12 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.7" + python: "3.11" -python: - install: - - method: pip - path: . - extra_requirements: - - doc +sphinx: + configuration: docs/sphinx/conf.py + python: + install: + - method: pip + extra_requirements: + - doc From f684812be87b0343c7314c683b0dd29754f07bb0 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:08:12 +0200 Subject: [PATCH 23/30] Fix to fix? --- readthedocs.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/readthedocs.yml b/readthedocs.yml index 477db5e..1dddad9 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -5,10 +5,11 @@ build: tools: python: "3.11" +python: + install: + - method: pip + extra_requirements: + - doc + sphinx: configuration: docs/sphinx/conf.py - python: - install: - - method: pip - extra_requirements: - - doc From 7317e7c9d99cd528e1e11ace32029bb3ffd868f2 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:09:07 +0200 Subject: [PATCH 24/30] Restore missing path --- readthedocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/readthedocs.yml b/readthedocs.yml index 1dddad9..7e4ec71 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -8,6 +8,7 @@ build: python: install: - method: pip + path: . extra_requirements: - doc From d5f8980766d77295b3560454a3a84ad2adce7dae Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:33:57 +0200 Subject: [PATCH 25/30] Remove ~=4.0 in sphinx requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cabb0cb..1b8acdf 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ install_requires=INSTALL_REQUIRES, extras_require={ 'test': TESTS_REQUIRES, - 'doc': ['Sphinx~=4.0', 'sphinx_rtd_theme', 'nbsphinx', + 'doc': ['sphinx', 'sphinx_rtd_theme', 'nbsphinx', 'sphinxcontrib_github_alt', 'ipykernel'] }, From 90b31281af9a3462cdecec5fc229c738d2909a71 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:36:49 +0200 Subject: [PATCH 26/30] Fix sphinx exception TypeError: not all arguments converted during string formatting --- docs/sphinx/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py index 9cd5999..b2b6830 100644 --- a/docs/sphinx/conf.py +++ b/docs/sphinx/conf.py @@ -341,8 +341,8 @@ def setup(app): extlinks = { - 'issue': ('https://github.com/pvlib/solarfactors/issues/%s', 'GH'), - 'pull': ('https://github.com/pvlib/solarfactors/pull/%s', 'GH'), - 'doi': ('http://dx.doi.org/%s', 'DOI: '), - 'ghuser': ('https://github.com/%s', '@') + 'issue': ('https://github.com/pvlib/solarfactors/issues/%s', 'GH #%s'), + 'pull': ('https://github.com/pvlib/solarfactors/pull/%s', 'GH #%s'), + 'doi': ('http://dx.doi.org/%s', 'DOI:%s'), + 'ghuser': ('https://github.com/%s', '@%s') } From 9cedd87e5aebf7e207da09d206b46272103d4d0c Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:08:29 +0200 Subject: [PATCH 27/30] Fix README.rst (solarfactors instead of openfactors) Co-authored-by: Kevin Anderson --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9c064dc..120a685 100644 --- a/README.rst +++ b/README.rst @@ -208,7 +208,7 @@ Requirements are included in the ``requirements.txt`` file of the package. Here Citing pvfactors ---------------- -If you use openfactors in a published work, cite the following paper: +If you use solarfactors in a published work, cite the following paper: .. parsed-literal:: From 4a9011ab611493c4d042a0adfd54847c64ab663d Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:08:39 +0200 Subject: [PATCH 28/30] Fix README.rst (solarfactors instead of openfactors) Co-authored-by: Kevin Anderson --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 120a685..a938dbb 100644 --- a/README.rst +++ b/README.rst @@ -219,7 +219,7 @@ If you use solarfactors in a published work, cite the following paper: Contributing ------------ -Contributions are needed in order to improve openfactors. +Contributions are needed in order to improve solarfactors. If you wish to contribute, you can start by forking and cloning the repository, and then installing openfactors using pip_ in the root folder of the package: From 218085adfa9a9291ef1b2066f1fbf95826b6f604 Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:38:05 +0200 Subject: [PATCH 29/30] Update README.rst Co-authored-by: Kevin Anderson --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a938dbb..af67f92 100644 --- a/README.rst +++ b/README.rst @@ -221,7 +221,7 @@ Contributing Contributions are needed in order to improve solarfactors. -If you wish to contribute, you can start by forking and cloning the repository, and then installing openfactors using pip_ in the root folder of the package: +If you wish to contribute, you can start by forking and cloning the repository, and then installing solarfactors using pip_ in the root folder of the package: .. code:: sh From ca5894488cab260a008cd52e3544ba63aebd8ebf Mon Sep 17 00:00:00 2001 From: "Joseph S." <11660030+joseph-sch@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:38:23 +0200 Subject: [PATCH 30/30] Update README.rst Co-authored-by: Kevin Anderson --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index af67f92..f12d0cc 100644 --- a/README.rst +++ b/README.rst @@ -238,11 +238,11 @@ To install the package in editable mode, you can use: Testing +++++++ -Install test dependencies by running: +Install test dependencies using the ``test`` extra: .. code:: sh - $ pip install pytest mock + $ pip install .[test] Then run the tests using: