Beispiel #1
0
    def _get_cartesian_edge_set(self):
        """
        For the GC2 calculations a set of cartesian representations of the
        fault edges are needed. In this present case we use a common cartesian
        framework for all edges, as opposed to defining a separate orthographic
        projection per edge
        """
        # Get projection space for cartesian projection
        edge_sets = numpy.vstack(self.edge_set)
        west, east, north, south = utils.get_spherical_bounding_box(
            edge_sets[:, 0],
            edge_sets[:, 1])
        self.proj = utils.OrthographicProjection(west, east, north, south)

        for edges in self.edge_set:
            # Project edges into cartesian space
            px, py = self.proj(edges[:, 0], edges[:, 1])
            # Store the two end-points of the trace
            self.cartesian_endpoints.append(
                numpy.array([[px[0], py[0], edges[0, 2]],
                             [px[-1], py[-1], edges[-1, 2]]]))
            self.cartesian_edges.append(numpy.column_stack([px, py,
                                                            edges[:, 2]]))
            # Get surface length vector for the trace - easier in cartesian
            lengths = numpy.sqrt((px[:-1] - px[1:]) ** 2. +
                                 (py[:-1] - py[1:]) ** 2.)
            self.length_set.append(lengths)
            # Get cumulative surface length vector
            self.cum_length_set.append(
                numpy.hstack([0., numpy.cumsum(lengths)]))
        return edge_sets
Beispiel #2
0
def _define_bins(bins_data, mag_bin_width, dist_bin_width, coord_bin_width, truncation_level, n_epsilons):
    """
    Define bin edges for disaggregation histograms.

    Given bins data as provided by :func:`_collect_bins_data`, this function
    finds edges of histograms, taking into account maximum and minimum values
    of magnitude, distance and coordinates as well as requested sizes/numbers
    of bins.
    """
    mags, dists, lons, lats, tect_reg_types, trt_bins, _ = bins_data

    mag_bins = mag_bin_width * numpy.arange(
        int(numpy.floor(mags.min() / mag_bin_width)), int(numpy.ceil(mags.max() / mag_bin_width) + 1)
    )

    dist_bins = dist_bin_width * numpy.arange(
        int(numpy.floor(dists.min() / dist_bin_width)), int(numpy.ceil(dists.max() / dist_bin_width) + 1)
    )

    west, east, north, south = get_spherical_bounding_box(lons, lats)
    west = numpy.floor(west / coord_bin_width) * coord_bin_width
    east = numpy.ceil(east / coord_bin_width) * coord_bin_width
    lon_extent = get_longitudinal_extent(west, east)
    lon_bins, _, _ = npoints_between(west, 0, 0, east, 0, 0, numpy.round(lon_extent / coord_bin_width + 1))

    lat_bins = coord_bin_width * numpy.arange(
        int(numpy.floor(south / coord_bin_width)), int(numpy.ceil(north / coord_bin_width) + 1)
    )

    eps_bins = numpy.linspace(-truncation_level, truncation_level, n_epsilons + 1)

    return mag_bins, dist_bins, lon_bins, lat_bins, eps_bins, trt_bins
Beispiel #3
0
    def _from_2d(cls, polygon2d, proj):
        """
        Create a polygon object from a 2d polygon and a projection.

        :param polygon2d:
            Instance of ``shapely.geometry.Polygon``.
        :param proj:
            Projection object created
            by
            :class:`~openquake.hazardlib.geo.utils.OrthographicProjection`
            that was used to project ``polygon2d``. That projection
            will be used for projecting it back to get spherical
            coordinates from Cartesian ones.
        :returns:
            New :class:`Polygon` object. Note that spherical coordinates
            of that polygon do not get upsampled even for longer edges.
        """
        # avoid calling class' constructor
        polygon = object.__new__(cls)
        # project polygon2d back on the sphere
        # NOTE(LB): We use 'exterior' here in case the `polygon2d` has
        # interiors (holes) defined. In our use cases, we don't care about
        # polygon interiors, so we simply discard these exteriors.
        xx, yy = numpy.transpose(polygon2d.exterior.coords)
        # need to cut off the last point -- it repeats the first one
        polygon.lons, polygon.lats = proj(xx[:-1], yy[:-1], reverse=True)
        # initialize the instance (as constructor would do)
        polygon._bbox = utils.get_spherical_bounding_box(
            polygon.lons, polygon.lats)
        polygon._polygon2d = polygon2d
        polygon._projection = proj
        return polygon
Beispiel #4
0
    def _get_proj_enclosing_polygon(self):
        """
        See :meth:`Mesh._get_proj_enclosing_polygon`.

        :class:`RectangularMesh` contains an information about relative
        positions of points, so it allows to define the minimum polygon,
        containing the projection of the mesh, which doesn't necessarily
        have to be convex (in contrast to :class:`Mesh` implementation).

        :returns:
            Same structure as :meth:`Mesh._get_proj_convex_hull`.
        """
        if self.lons.size < 4:
            # the mesh doesn't contain even a single cell, use :class:`Mesh`
            # method implementation (which would dilate the point or the line)
            return super(RectangularMesh, self)._get_proj_enclosing_polygon()

        proj = geo_utils.get_orthographic_projection(
            *geo_utils.get_spherical_bounding_box(self.lons.flatten(),
                                                  self.lats.flatten())
        )
        mesh2d = numpy.array(proj(self.lons.transpose(),
                                  self.lats.transpose())).transpose()
        lines = iter(mesh2d)
        # we iterate over horizontal stripes, keeping the "previous"
        # line of points. we keep it reversed, such that together
        # with the current line they define the sequence of points
        # around the stripe.
        prev_line = next(lines)[::-1]
        polygons = []
        for i, line in enumerate(lines):
            coords = numpy.concatenate((prev_line, line, prev_line[0:1]))
            # create the shapely polygon object from the stripe
            # coordinates and simplify it (remove redundant points,
            # if there are any lying on the straight line).
            stripe = shapely.geometry.LineString(coords) \
                                     .simplify(self.DIST_TOLERANCE) \
                                     .buffer(self.DIST_TOLERANCE, 2)
            polygons.append(shapely.geometry.Polygon(stripe.exterior))
            prev_line = line[::-1]
        try:
            # create a final polygon as the union of all the stripe ones
            polygon = shapely.ops.cascaded_union(polygons) \
                                 .simplify(self.DIST_TOLERANCE)
        except ValueError:
            # NOTE(larsbutler): In some rare cases, we've observed ValueErrors
            # ("No Shapely geometry can be created from null value") with very
            # specific sets of polygons such that there are two unique
            # and many duplicates of one.
            # This bug is very difficult to reproduce consistently (except on
            # specific platforms) so the work around here is to remove the
            # duplicate polygons. In fact, we only observed this error on our
            # CI/build machine. None of our dev environments or production
            # machines has encountered this error, at least consistently. >:(
            polygons = [shapely.wkt.loads(x) for x in
                        list(set(p.wkt for p in polygons))]
            polygon = shapely.ops.cascaded_union(polygons) \
                                 .simplify(self.DIST_TOLERANCE)
        return proj, polygon
Beispiel #5
0
    def get_bounding_box(self):
        """
        Compute surface geographical bounding box.

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """
        return utils.get_spherical_bounding_box(self.mesh.lons, self.mesh.lats)
Beispiel #6
0
    def get_bounding_box(self):
        """
        Compute surface bounding box from plane's corners coordinates. Calls
        :meth:`openquake.hazardlib.geo.utils.get_spherical_bounding_box`

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """

        return geo_utils.get_spherical_bounding_box(self.corner_lons, self.corner_lats)
Beispiel #7
0
    def get_bounding_box(self):
        """
        Compute surface bounding box from surface mesh representation. That is
        extract longitudes and latitudes of mesh points and calls:
        :meth:`openquake.hazardlib.geo.utils.get_spherical_bounding_box`

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """
        mesh = self.get_mesh()
        return utils.get_spherical_bounding_box(mesh.lons, mesh.lats)
Beispiel #8
0
    def get_bounding_box(self):
        """
        Compute surface bounding box from surface mesh representation. That is
        extract longitudes and latitudes of mesh points and calls:
        :meth:`openquake.hazardlib.geo.utils.get_spherical_bounding_box`

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """
        mesh = self.get_mesh()
        return utils.get_spherical_bounding_box(mesh.lons, mesh.lats)
Beispiel #9
0
    def get_bounding_box(self):
        """
        Compute surface bounding box from plane's corners coordinates. Calls
        :meth:`openquake.hazardlib.geo.utils.get_spherical_bounding_box`

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """

        return geo_utils.get_spherical_bounding_box(self.corner_lons,
                                                    self.corner_lats)
Beispiel #10
0
    def get_bounding_box(self):
        """
        Compute bounding box for each surface element, and then return
        the bounding box of all surface elements' bounding boxes.

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """
        lons = []
        lats = []
        for surf in self.surfaces:
            west, east, north, south = surf.get_bounding_box()
            lons.extend([west, east])
            lats.extend([north, south])
        return utils.get_spherical_bounding_box(lons, lats)
Beispiel #11
0
    def get_bounding_box(self):
        """
        Compute bounding box for each surface element, and then return
        the bounding box of all surface elements' bounding boxes.

        :return:
            A tuple of four items. These items represent western, eastern,
            northern and southern borders of the bounding box respectively.
            Values are floats in decimal degrees.
        """
        lons = []
        lats = []
        for surf in self.surfaces:
            west, east, north, south = surf.get_bounding_box()
            lons.extend([west, east])
            lats.extend([north, south])
        return utils.get_spherical_bounding_box(lons, lats)
Beispiel #12
0
    def _get_proj_enclosing_polygon(self):
        """
        See :meth:`Mesh._get_proj_enclosing_polygon`.

        :class:`RectangularMesh` contains an information about relative
        positions of points, so it allows to define the minimum polygon,
        containing the projection of the mesh, which doesn't necessarily
        have to be convex (in contrast to :class:`Mesh` implementation).

        :returns:
            Same structure as :meth:`Mesh._get_proj_convex_hull`.
        """
        if self.lons.size < 4:
            # the mesh doesn't contain even a single cell, use :class:`Mesh`
            # method implementation (which would dilate the point or the line)
            return super(RectangularMesh, self)._get_proj_enclosing_polygon()

        proj = geo_utils.get_orthographic_projection(
            *geo_utils.get_spherical_bounding_box(self.lons, self.lats)
        )
        mesh2d = numpy.array(proj(self.lons.transpose(),
                                  self.lats.transpose())).transpose()
        lines = iter(mesh2d)
        # we iterate over horizontal stripes, keeping the "previous"
        # line of points. we keep it reversed, such that together
        # with the current line they define the sequence of points
        # around the stripe.
        prev_line = lines.next()[::-1]
        polygons = []
        for i, line in enumerate(lines):
            coords = numpy.concatenate((prev_line, line, prev_line[0:1]))
            # create the shapely polygon object from the stripe
            # coordinates and simplify it (remove redundant points,
            # if there are any lying on the straight line).
            stripe = shapely.geometry.LineString(coords) \
                                     .simplify(self.DIST_TOLERANCE) \
                                     .buffer(self.DIST_TOLERANCE, 2)
            polygons.append(shapely.geometry.Polygon(stripe.exterior))
            prev_line = line[::-1]
        # create a final polygon as the union of all the stripe ones
        polygon = shapely.ops.cascaded_union(polygons) \
                             .simplify(self.DIST_TOLERANCE)
        return proj, polygon
Beispiel #13
0
    def get_joyner_boore_distance(self, mesh):

        # Get indexes of the finite points composing the edges
        iupp = np.nonzero(np.isfinite(self.mesh.lons[0, :]))[0]
        ilow = np.flipud(np.nonzero(np.isfinite(self.mesh.lons[-1, :]))[0])
        irig = np.nonzero(np.isfinite(self.mesh.lons[:, -1]))[0]
        ilef = np.flipud(np.nonzero(np.isfinite(self.mesh.lons[:, 0]))[0])

        # Building the polygon
        pnts = []
        for corner in [(0, iupp), (irig, -1), (-1, ilow), (ilef, 0)]:
            pnts.extend(
                zip(self.mesh.lons[corner], self.mesh.lats[corner],
                    self.mesh.depths[corner]))
        perimeter = np.array(pnts)

        distances = geodetic.min_geodetic_distance(
            (perimeter[:, 0], perimeter[:, 1]), (mesh.lons, mesh.lats))

        idxs = (distances < 40).nonzero()[0]  # indices on the first dimension
        if not len(idxs):
            # no point is close enough, return distances as they are
            return distances

        # Get the projection
        proj = geo_utils.OrthographicProjection(
            *geo_utils.get_spherical_bounding_box(perimeter[:,
                                                            0], perimeter[:,
                                                                          1]))

        # Mesh projected coordinates
        mesh_xx, mesh_yy = proj(mesh.lons[idxs], mesh.lats[idxs])

        # Create the shapely Polygon using projected coordinates
        xp, yp = proj(perimeter[:, 0], perimeter[:, 1])
        polygon = Polygon([[x, y] for x, y in zip(xp, yp)])

        # Calculate the distances
        distances[idxs] = geo_utils.point_to_polygon_distance(
            polygon, mesh_xx, mesh_yy)

        return distances
Beispiel #14
0
    def update(self, dists, lons, lats):
        """
        Compare the current bounding box with the value in the arrays
        dists, lons, lats and enlarge it if needed.

        :param dists:
            a sequence of distances
        :param lons:
            a sequence of longitudes
        :param lats:
            a sequence of latitudes
        """
        if self.min_dist is not None:
            dists = [self.min_dist, self.max_dist] + dists
        if self.west is not None:
            lons = [self.west, self.east] + lons
        if self.south is not None:
            lats = [self.south, self.north] + lats
        self.min_dist, self.max_dist = min(dists), max(dists)
        self.west, self.east, self.north, self.south = \
            get_spherical_bounding_box(lons, lats)
Beispiel #15
0
    def _get_proj_convex_hull(self):
        """
        Create a projection centered in the center of this mesh and define
        a convex polygon in that projection, enveloping all the points
        of the mesh.

        :returns:
            Tuple of two items: projection function and shapely 2d polygon.
            Note that the result geometry can be line or point depending
            on number of points in the mesh and their arrangement.
        """
        # create a projection centered in the center of points collection
        proj = geo_utils.OrthographicProjection(
            *geo_utils.get_spherical_bounding_box(self.lons, self.lats))

        # project all the points and create a shapely multipoint object.
        # need to copy an array because otherwise shapely misinterprets it
        coords = numpy.transpose(proj(self.lons.flat, self.lats.flat)).copy()
        multipoint = shapely.geometry.MultiPoint(coords)
        # create a 2d polygon from a convex hull around that multipoint
        return proj, multipoint.convex_hull
Beispiel #16
0
    def _get_proj_convex_hull(self):
        """
        Create a projection centered in the center of this mesh and define
        a convex polygon in that projection, enveloping all the points
        of the mesh.

        :returns:
            Tuple of two items: projection function and shapely 2d polygon.
            Note that the result geometry can be line or point depending
            on number of points in the mesh and their arrangement.
        """
        # create a projection centered in the center of points collection
        proj = geo_utils.OrthographicProjection(
            *geo_utils.get_spherical_bounding_box(self.lons, self.lats))

        # project all the points and create a shapely multipoint object.
        # need to copy an array because otherwise shapely misinterprets it
        coords = numpy.transpose(proj(self.lons.flat, self.lats.flat)).copy()
        multipoint = shapely.geometry.MultiPoint(coords)
        # create a 2d polygon from a convex hull around that multipoint
        return proj, multipoint.convex_hull
Beispiel #17
0
    def update(self, dists, lons, lats):
        """
        Compare the current bounding box with the value in the arrays
        dists, lons, lats and enlarge it if needed.

        :param dists:
            a sequence of distances
        :param lons:
            a sequence of longitudes
        :param lats:
            a sequence of latitudes
        """
        if self.min_dist:
            dists = [self.min_dist, self.max_dist] + dists
        if self.west:
            lons = [self.west, self.east] + lons
        if self.south:
            lats = [self.south, self.north] + lats
        self.min_dist, self.max_dist = min(dists), max(dists)
        self.west, self.east, self.north, self.south = \
            get_spherical_bounding_box(lons, lats)
Beispiel #18
0
    def _init_polygon2d(self):
        """
        Spherical bounding box, projection, and Cartesian polygon are all
        cached to prevent redundant computations.

        If any of them are `None`, recalculate all of them.
        """
        if self._polygon2d is None or self._projection is None or self._bbox is None:
            # resample polygon line segments:
            lons, lats = get_resampled_coordinates(self.lons, self.lats)

            # find the bounding box of a polygon in spherical coordinates:
            self._bbox = utils.get_spherical_bounding_box(lons, lats)

            # create a projection that is centered in a polygon center:
            self._projection = utils.get_orthographic_projection(*self._bbox)

            # project polygon vertices to the Cartesian space and create
            # a shapely polygon object:
            xx, yy = self._projection(lons, lats)
            self._polygon2d = shapely.geometry.Polygon(zip(xx, yy))
Beispiel #19
0
def _define_bins(bins_data, mag_bin_width, dist_bin_width,
                 coord_bin_width, truncation_level, n_epsilons):
    """
    Define bin edges for disaggregation histograms.

    Given bins data as provided by :func:`_collect_bins_data`, this function
    finds edges of histograms, taking into account maximum and minimum values
    of magnitude, distance and coordinates as well as requested sizes/numbers
    of bins.
    """
    mags, dists, lons, lats, tect_reg_types, trt_bins, _ = bins_data

    mag_bins = mag_bin_width * numpy.arange(
        int(numpy.floor(mags.min() / mag_bin_width)),
        int(numpy.ceil(mags.max() / mag_bin_width) + 1)
    )

    dist_bins = dist_bin_width * numpy.arange(
        int(numpy.floor(dists.min() / dist_bin_width)),
        int(numpy.ceil(dists.max() / dist_bin_width) + 1)
    )

    west, east, north, south = get_spherical_bounding_box(lons, lats)
    west = numpy.floor(west / coord_bin_width) * coord_bin_width
    east = numpy.ceil(east / coord_bin_width) * coord_bin_width
    lon_extent = get_longitudinal_extent(west, east)
    lon_bins, _, _ = npoints_between(
        west, 0, 0, east, 0, 0,
        numpy.round(lon_extent / coord_bin_width + 1)
    )

    lat_bins = coord_bin_width * numpy.arange(
        int(numpy.floor(south / coord_bin_width)),
        int(numpy.ceil(north / coord_bin_width) + 1)
    )

    eps_bins = numpy.linspace(-truncation_level, truncation_level,
                              n_epsilons + 1)

    return mag_bins, dist_bins, lon_bins, lat_bins, eps_bins, trt_bins
Beispiel #20
0
    def _init_polygon2d(self):
        """
        Spherical bounding box, projection, and Cartesian polygon are all
        cached to prevent redundant computations.

        If any of them are `None`, recalculate all of them.
        """
        if (self._polygon2d is None or self._projection is None
                or self._bbox is None):
            # resample polygon line segments:
            lons, lats = get_resampled_coordinates(self.lons, self.lats)

            # find the bounding box of a polygon in spherical coordinates:
            self._bbox = utils.get_spherical_bounding_box(lons, lats)

            # create a projection that is centered in a polygon center:
            self._projection = utils.OrthographicProjection(*self._bbox)

            # project polygon vertices to the Cartesian space and create
            # a shapely polygon object:
            xx, yy = self._projection(lons, lats)
            self._polygon2d = shapely.geometry.Polygon(list(zip(xx, yy)))
Beispiel #21
0
    def _get_cartesian_edge_set(self):
        """
        For the GC2 calculations a set of cartesian representations of the
        fault edges are needed. In this present case we use a common cartesian
        framework for all edges, as opposed to defining a separate orthographic
        projection per edge
        """
        # Get projection space for cartesian projection
        edge_sets = numpy.vstack(self.edge_set)
        idx = numpy.isfinite(edge_sets[:, 0])
        edge_sets = edge_sets[idx, :]
        # Get bounding box
        west, east, north, south = utils.get_spherical_bounding_box(
            edge_sets[:, 0],
            edge_sets[:, 1])
        self.proj = utils.OrthographicProjection(west, east, north, south)

        for edges in self.edge_set:
            idx = numpy.isfinite(edges[:, 0])
            edges = edges[idx, :]
            # Project edges into cartesian space
            px, py = self.proj(edges[:, 0], edges[:, 1])
            # Store the two end-points of the trace
            self.cartesian_endpoints.append(
                numpy.array([[px[0], py[0], edges[0, 2]],
                             [px[-1], py[-1], edges[-1, 2]]]))
            self.cartesian_edges.append(numpy.column_stack([px, py,
                                                            edges[:, 2]]))
            # Get surface length vector for the trace - easier in cartesian
            lengths = numpy.sqrt((px[:-1] - px[1:]) ** 2. +
                                 (py[:-1] - py[1:]) ** 2.)
            self.length_set.append(lengths)
            # Get cumulative surface length vector
            self.cum_length_set.append(
                numpy.hstack([0., numpy.cumsum(lengths)]))
        return edge_sets
Beispiel #22
0
    def get_joyner_boore_distance(self, mesh) -> np.ndarray:
        """
        Computes the Rjb distance between the rupture and the points included
        in the mesh provided.

        :param mesh:
            An instance of :class:`openquake.hazardlib.geo.mesh.Mesh`
        :returns:
            A :class:`numpy.ndarray` instance with the Rjb values
        """

        blo, bla = self._get_external_boundary()
        distances = geodetic.min_geodetic_distance((blo, bla),
                                                   (mesh.lons, mesh.lats))

        idxs = (distances < 40).nonzero()[0]  # indices on the first dimension
        if len(idxs) < 1:
            # no point is close enough, return distances as they are
            return distances

        # Get the projection
        proj = geo_utils.OrthographicProjection(
            *geo_utils.get_spherical_bounding_box(blo, bla))

        # Mesh projected coordinates
        mesh_xx, mesh_yy = proj(mesh.lons[idxs], mesh.lats[idxs])

        # Create the shapely Polygon using projected coordinates
        xp, yp = proj(blo, bla)
        polygon = Polygon([[x, y] for x, y in zip(xp, yp)])

        # Calculate the distances
        distances[idxs] = geo_utils.point_to_polygon_distance(
            polygon, mesh_xx, mesh_yy)

        return distances
Beispiel #23
0
    def _get_proj_enclosing_polygon(self):
        """
        See :meth:`Mesh._get_proj_enclosing_polygon`.

        :class:`RectangularMesh` contains an information about relative
        positions of points, so it allows to define the minimum polygon,
        containing the projection of the mesh, which doesn't necessarily
        have to be convex (in contrast to :class:`Mesh` implementation).

        :returns:
            Same structure as :meth:`Mesh._get_proj_convex_hull`.
        """
        if self.lons.size < 4:
            # the mesh doesn't contain even a single cell
            return self._get_proj_convex_hull()

        proj = geo_utils.OrthographicProjection(
            *geo_utils.get_spherical_bounding_box(self.lons, self.lats))
        if len(self.lons.shape) == 1:  # 1D mesh
            lons = self.lons.reshape(len(self.lons), 1)
            lats = self.lats.reshape(len(self.lats), 1)
        else:  # 2D mesh
            lons = self.lons.T
            lats = self.lats.T
        mesh2d = numpy.array(proj(lons, lats)).T
        lines = iter(mesh2d)
        # we iterate over horizontal stripes, keeping the "previous"
        # line of points. we keep it reversed, such that together
        # with the current line they define the sequence of points
        # around the stripe.
        prev_line = next(lines)[::-1]
        polygons = []
        for i, line in enumerate(lines):
            coords = numpy.concatenate((prev_line, line, prev_line[0:1]))
            # create the shapely polygon object from the stripe
            # coordinates and simplify it (remove redundant points,
            # if there are any lying on the straight line).
            stripe = shapely.geometry.LineString(coords) \
                                     .simplify(self.DIST_TOLERANCE) \
                                     .buffer(self.DIST_TOLERANCE, 2)
            polygons.append(shapely.geometry.Polygon(stripe.exterior))
            prev_line = line[::-1]
        try:
            # create a final polygon as the union of all the stripe ones
            polygon = shapely.ops.cascaded_union(polygons) \
                                 .simplify(self.DIST_TOLERANCE)
        except ValueError:
            # NOTE(larsbutler): In some rare cases, we've observed ValueErrors
            # ("No Shapely geometry can be created from null value") with very
            # specific sets of polygons such that there are two unique
            # and many duplicates of one.
            # This bug is very difficult to reproduce consistently (except on
            # specific platforms) so the work around here is to remove the
            # duplicate polygons. In fact, we only observed this error on our
            # CI/build machine. None of our dev environments or production
            # machines has encountered this error, at least consistently. >:(
            polygons = [
                shapely.wkt.loads(x)
                for x in list(set(p.wkt for p in polygons))
            ]
            polygon = shapely.ops.cascaded_union(polygons) \
                                 .simplify(self.DIST_TOLERANCE)
        return proj, polygon