def get_sindex(gdf): """Get or build an R-Tree spatial index. Particularly useful for geopandas<0.2.0;>0.7.0;0.9.0 """ sindex = None if (hasattr(gdf, '_rtree_sindex')): return getattr(gdf, '_rtree_sindex') if (isinstance(gdf, geopandas.GeoDataFrame) and hasattr(gdf.geometry, 'sindex')): sindex = gdf.geometry.sindex elif isinstance(gdf, geopandas.GeoSeries) and hasattr(gdf, 'sindex'): sindex = gdf.sindex if sindex is not None: if (hasattr(sindex, "nearest") and sindex.__class__.__name__ != "PyGEOSSTRTreeIndex"): # probably rtree.index.Index return sindex else: # probably PyGEOSSTRTreeIndex but unfortunately, 'nearest' # with 'num_results' is required sindex = None if rtree and len(gdf) >= rtree_threshold: # Manually populate a 2D spatial index for speed sindex = Index() # slow, but reliable for idx, item in enumerate(gdf.bounds.itertuples()): sindex.add(idx, item[1:]) # cache the index for later setattr(gdf, '_rtree_sindex', sindex) return sindex
def get_sindex(gdf): """Helper function to get or build a spatial index Particularly useful for geopandas<0.2.0 """ assert isinstance(gdf, geopandas.GeoDataFrame) has_sindex = hasattr(gdf, 'sindex') if has_sindex: sindex = gdf.geometry.sindex elif rtree and len(gdf) >= rtree_threshold: # Manually populate a 2D spatial index for speed sindex = Index() # slow, but reliable for idx, (segnum, row) in enumerate(gdf.bounds.iterrows()): sindex.add(idx, tuple(row)) else: sindex = None return sindex
class Domain(object): ''' A class used to facilitate computational geometry opperations on a domain defined by a closed collection of simplices (e.g., line segments or triangular facets). This class can optionally also make use of an R-tree which can substantially reduce the computational complexity of some operations. Parameters ---------- vertices : (n, d) float array The vertices making up the domain simplices : (m, d) int array The connectivity of the vertices ''' def __init__(self, vertices, simplices): vertices = np.asarray(vertices, dtype=float) simplices = np.asarray(simplices, dtype=int) assert_shape(vertices, (None, None), 'vertices') dim = vertices.shape[1] assert_shape(simplices, (None, dim), 'simplices') self.vertices = vertices self.simplices = simplices self.dim = dim self.rtree = None self.normals = geo.simplex_normals(vertices, simplices) def __repr__(self): return ('<Domain : ' 'vertex count=%s, ' 'simplex count=%s, ' 'using R-tree=%s>' % (self.vertices.shape[0], self.simplices.shape[0], self.rtree is not None)) def __getstate__(self): # Define how pickling behaves for this class. The __getstate__ # and __setstate__ methods are required because `rtree` does # not properly pickle. So we instead save a flag indicating # whether we need to rebuild `rtree` upon unpickling. # create a shallow copy of the instances dict so that we do # not mess with its attributes state = dict(self.__dict__) rtree = state.pop('rtree') if rtree is None: state['has_rtree'] = False else: logger.debug( 'the R-tree cannot be pickled and it will be rebuilt ' 'upon unpickling') state['has_rtree'] = True return state def __setstate__(self, state): has_rtree = state.pop('has_rtree') self.__dict__ = state self.rtree = None if has_rtree: self.build_rtree() def build_rtree(self): ''' Construct an R-tree for the domain. This may reduce the computational complexity of the methods `intersection_count`, `contains`, `orient_simplices`, and `snap`. ''' # create a bounding box for each simplex and add those # bounding boxes to the R-tree if self.rtree is not None: # do nothing because the R-tree already exists logger.debug('R-tree already exists') return smp_min = self.vertices[self.simplices].min(axis=1) smp_max = self.vertices[self.simplices].max(axis=1) bounds = np.hstack((smp_min, smp_max)) p = Property() p.dimension = self.dim self.rtree = Index(properties=p) for i, bnd in enumerate(bounds): self.rtree.add(i, bnd) def orient_simplices(self): ''' Orient the simplices so that the normal vectors point outward. ''' # length scale of the domain scale = self.vertices.ptp(axis=0).max() dx = 1e-10*scale # find the normal for each simplex norms = geo.simplex_normals(self.vertices, self.simplices) # find the centroid for each simplex points = np.mean(self.vertices[self.simplices], axis=1) # push points in the direction of the normals points += dx*norms # find which simplices are oriented such that their normals # point inside faces_inside = self.contains(points) # make a copy of simplices because we are modifying it in # place new_smp = np.array(self.simplices, copy=True) # flip the order of the simplices that are backwards flip_smp = new_smp[faces_inside] flip_smp[:, [0, 1]] = flip_smp[:, [1, 0]] new_smp[faces_inside] = flip_smp self.simplices = new_smp # remake the normal vectors with the reoriented simplices self.normals = geo.simplex_normals(self.vertices, new_smp) def intersection_count(self, start_points, end_points): ''' Counts the number times the line segments intersect the boundary. Parameters ---------- start_points, end_points : (n, d) float array The ends of the line segments Returns ------- (n,) int array The number of boundary intersection ''' start_points = np.asarray(start_points, dtype=float) end_points = np.asarray(end_points, dtype=float) assert_shape(start_points, (None, self.dim), 'start_points') assert_shape(end_points, start_points.shape, 'end_points') n = start_points.shape[0] if self.rtree is None: return geo.intersection_count( start_points, end_points, self.vertices, self.simplices) else: out = np.zeros(n, dtype=int) # get the bounding boxes around each segment bounds = np.hstack((np.minimum(start_points, end_points), np.maximum(start_points, end_points))) for i, bnd in enumerate(bounds): # get a list of simplices which could potentially be # intersected by segment i potential_smpid = list(self.rtree.intersection(bnd)) if not potential_smpid: # if the segment bounding box does not intersect # and simplex bounding boxes, then there is no # intersection continue out[[i]] = geo.intersection_count( start_points[[i]], end_points[[i]], self.vertices, self.simplices[potential_smpid]) return out def intersection_point(self, start_points, end_points): ''' Finds the point on the boundary intersected by the line segments. A `ValueError` is raised if no intersection is found. Parameters ---------- start_points, end_points : (n, d) float array The ends of the line segments Returns ------- (n, d) float array The intersection point (n,) int array The simplex containing the intersection point ''' # dont bother using the tree for this one return geo.intersection_point( start_points, end_points, self.vertices, self.simplices) def contains(self, points): ''' Identifies whether the points are within the domain Parameters ---------- points : (n, d) float array Returns ------- (n,) bool array ''' points = np.asarray(points, dtype=float) assert_shape(points, (None, self.dim), 'points') # to find out if the points are inside the domain, we create # another set of points which are definitively outside the # domain, and then we count the number of boundary # intersections between `points` and the new points. # get the min value and width of the domain along axis 0 xwidth = self.vertices[:, 0].ptp() xmin = self.vertices[:, 0].min() # the outside points are directly to the left of `points` plus # a small random perturbation. The subsequent bounding boxes # are going to be very narrow, meaning that the R-tree will # efficiently winnow down the potential intersecting # simplices. outside_points = np.array(points, copy=True) outside_points[:, 0] = xmin - xwidth outside_points += np.random.uniform( -0.001*xwidth, 0.001*xwidth, points.shape) count = self.intersection_count(points, outside_points) # If the segment intersects the boundary an odd number of # times, then the point is inside the domain, otherwise it is # outside out = np.array(count % 2, dtype=bool) return out def snap(self, points, delta=0.5): ''' Snaps `points` to the nearest points on the boundary if they are sufficiently close to the boundary. A point is sufficiently close if the distance to the boundary is less than `delta` times the distance to its nearest neighbor. Parameters ---------- points : (n, d) float array delta : float, optional Returns ------- (n, d) float array The new points after snapping to the boundary (n,) int array The simplex that the points are snapped to. If a point is not snapped to the boundary then its corresponding value will be -1. ''' points = np.asarray(points, dtype=float) assert_shape(points, (None, self.dim), 'points') n = points.shape[0] out_smpid = np.full(n, -1, dtype=int) out_points = np.array(points, copy=True) nbr_dist = KDTree(points).query(points, 2)[0][:, 1] snap_dist = delta*nbr_dist if self.rtree is None: nrst_pnt, nrst_smpid = geo.nearest_point( points, self.vertices, self.simplices) nrst_dist = np.linalg.norm(nrst_pnt - points, axis=1) snap = nrst_dist < snap_dist out_points[snap] = nrst_pnt[snap] out_smpid[snap] = nrst_smpid[snap] else: # creating bounding boxes around the snapping regions for # each point bounds = np.hstack((points - snap_dist[:, None], points + snap_dist[:, None])) for i, bnd in enumerate(bounds): # get a list of simplices which node i could # potentially snap to potential_smpid = list(self.rtree.intersection(bnd)) # sort the list to ensure consistent output potential_smpid.sort() if not potential_smpid: # no simplices are within the snapping distance continue # get the nearest point to the potential simplices and # the simplex containing the nearest point nrst_pnt, nrst_smpid = geo.nearest_point( points[[i]], self.vertices, self.simplices[potential_smpid]) nrst_dist = np.linalg.norm(points[i] - nrst_pnt[0]) # if the nearest point is within the snapping distance # then snap if nrst_dist < snap_dist[i]: out_points[i] = nrst_pnt[0] out_smpid[i] = potential_smpid[nrst_smpid[0]] return out_points, out_smpid