コード例 #1
0
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
コード例 #2
0
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
コード例 #3
0
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