Exemplo n.º 1
0
def test_crop_to_target_area(keep_column_data: bool):
    """
    Test crop to target area.
    """
    (
        valid_geoseries,
        invalid_geoseries,
        valid_areas_geoseries,
        invalid_areas_geoseries,
    ) = trace_builder.main(snap_threshold=Helpers.snap_threshold)
    valid_result = general.crop_to_target_areas(
        valid_geoseries,
        valid_areas_geoseries,
        keep_column_data=keep_column_data,
    )
    try:
        _ = general.crop_to_target_areas(
            invalid_geoseries,
            invalid_areas_geoseries,
            keep_column_data=keep_column_data,
        )
        assert False
    except TypeError:
        pass
    assert isinstance(valid_result, (gpd.GeoDataFrame, gpd.GeoSeries))
    assert valid_geoseries.geometry.length.mean(
    ) > valid_result.geometry.length.mean()
Exemplo n.º 2
0
def test_crop_to_target_areas(keep_column_data: bool, file_regression):
    """
    Test cropping traces to target area with known right example data results.

    Also does regression testing with known right data.
    """
    trace_data = general.read_geofile(
        Path("tests/sample_data/mls_crop_samples/traces.gpkg"))
    area_data = general.read_geofile(
        Path("tests/sample_data/mls_crop_samples/mls_area.gpkg"))
    cropped_traces = general.crop_to_target_areas(
        traces=trace_data,
        areas=area_data,
        keep_column_data=keep_column_data,
    )
    assert isinstance(cropped_traces, gpd.GeoDataFrame)
    cropped_traces.sort_index(inplace=True)
    file_regression.check(cropped_traces.to_json(indent=1))
Exemplo n.º 3
0
    def resolve_samples(candidates, sample_circle):
        """
        Crop traces or branches to sample circle

        First check if any geometries intersect.
        If not: sample_features is an empty GeoDataFrame.
        """
        if any(
                candidate.intersects(sample_circle)
                for candidate in candidates.geometry.values):
            samples = crop_to_target_areas(
                traces=candidates,
                areas=gpd.GeoSeries([sample_circle]),
                is_filtered=True,
                keep_column_data=False,
            )
        else:
            samples = candidates.iloc[0:0]
        return samples
Exemplo n.º 4
0
def populate_sample_cell(
    sample_cell: Polygon,
    sample_cell_area: float,
    traces_sindex: PyGEOSSTRTreeIndex,
    traces: gpd.GeoDataFrame,
    nodes: gpd.GeoDataFrame,
    snap_threshold: float,
    resolve_branches_and_nodes: bool,
) -> Dict[str, float]:
    """
    Take a single grid polygon and populate it with parameters.

    Mauldon determination requires that E-nodes are defined for
    every single sample circle. If correct Mauldon values are
    wanted `resolve_branches_and_nodes` must be passed as True.
    This will result in much longer analysis time.

    """
    _centroid = sample_cell.centroid
    if not isinstance(_centroid, Point):
        raise TypeError("Expected Point centroid.")
    centroid = _centroid
    sample_circle = safe_buffer(centroid, np.sqrt(sample_cell_area) * 1.5)
    sample_circle_area = sample_circle.area
    assert sample_circle_area > 0

    # Choose geometries that are either within the sample_circle or
    # intersect it
    # Use spatial indexing to filter to only spatially relevant traces,
    # traces and nodes
    trace_candidates_idx = spatial_index_intersection(
        traces_sindex, geom_bounds(sample_circle))
    trace_candidates = traces.iloc[trace_candidates_idx]

    assert isinstance(trace_candidates, gpd.GeoDataFrame)

    if len(trace_candidates) == 0:
        return determine_topology_parameters(
            trace_length_array=np.array([]),
            node_counts=determine_node_type_counts(np.array([]),
                                                   branches_defined=True),
            area=sample_circle_area,
        )
    if resolve_branches_and_nodes:
        # Solve branches and nodes for each cell if wanted
        # Only way to make sure Mauldon parameters are correct
        _, nodes = branches_and_nodes(
            traces=trace_candidates,
            areas=gpd.GeoSeries([sample_circle], crs=traces.crs),
            snap_threshold=snap_threshold,
        )
    # node_candidates_idx = list(nodes_sindex.intersection(sample_circle.bounds))
    node_candidates_idx = spatial_index_intersection(
        spatial_index=pygeos_spatial_index(nodes),
        coordinates=geom_bounds(sample_circle),
    )

    node_candidates = nodes.iloc[node_candidates_idx]

    # Crop traces to sample circle
    # First check if any geometries intersect
    # If not: sample_features is an empty GeoDataFrame
    if any(
            trace_candidate.intersects(sample_circle)
            for trace_candidate in trace_candidates.geometry.values):
        sample_traces = crop_to_target_areas(
            traces=trace_candidates,
            areas=gpd.GeoSeries([sample_circle]),
            is_filtered=True,
            keep_column_data=False,
        )
    else:
        sample_traces = traces.iloc[0:0]
    if any(node.intersects(sample_circle) for node in nodes.geometry.values):
        # if any(nodes.intersects(sample_circle)):
        # TODO: Is node clipping stable?
        sample_nodes = gpd.clip(node_candidates, sample_circle)
        assert sample_nodes is not None
        assert all(
            isinstance(val, Point) for val in sample_nodes.geometry.values)
    else:
        sample_nodes = nodes.iloc[0:0]

    assert isinstance(sample_nodes, gpd.GeoDataFrame)
    assert isinstance(sample_traces, gpd.GeoDataFrame)

    sample_node_type_values = sample_nodes[CLASS_COLUMN].values
    assert isinstance(sample_node_type_values, np.ndarray)

    node_counts = determine_node_type_counts(sample_node_type_values,
                                             branches_defined=True)

    topology_parameters = determine_topology_parameters(
        trace_length_array=sample_traces.geometry.length.values,
        node_counts=node_counts,
        area=sample_circle_area,
        correct_mauldon=resolve_branches_and_nodes,
    )
    return topology_parameters
Exemplo n.º 5
0
    def __post_init__(self):
        """
        Copy GeoDataFrames instead of changing inputs.

        If the data is passed later to attribute, __setattr__ will also
        handle copying.

        :raises ValueError: If trace ``GeoDataFrame`` is empty after
            ``crop_to_target_areas``.
        """
        self.topology_determined = False
        if self.area_gdf.empty or self.area_gdf.geometry.iloc[0].is_empty:
            raise ValueError(
                "Passed area_gdf or geometry at index 0 is empty.\n"
                "Either pass a non-empty area_gdf or alternatively use\n"
                "fractopo.general.bounding_polygon to create an enveloping\n"
                "non-intersecting Polygon around your trace_gdf.")
        if self.circular_target_area:
            if not self.truncate_traces:
                raise ValueError("Traces must be truncated to the target area"
                                 " to perform circular area trace weighting. "
                                 "\n(To fix: pass truncate_traces=True.)")
        # Copy geodataframes instead of using pointers
        # Traces
        self.trace_gdf = self.trace_gdf.copy()
        # Area
        self.area_gdf = self.area_gdf.copy()
        # Branches
        self.branch_gdf = self.branch_gdf.copy()
        # Branches
        self.node_gdf = self.node_gdf.copy()

        if self.truncate_traces:
            self.trace_gdf = gpd.GeoDataFrame(
                crop_to_target_areas(
                    self.trace_gdf,
                    self.area_gdf,
                    keep_column_data=True,
                ))
            self.trace_gdf.reset_index(inplace=True, drop=True)
            if self.trace_gdf.shape[0] == 0:
                raise ValueError(
                    "Empty trace GeoDataFrame after crop_to_target_areas.")

        self.trace_data = LineData(
            _line_gdf=self.trace_gdf,
            azimuth_set_ranges=self.azimuth_set_ranges,
            azimuth_set_names=self.azimuth_set_names,
            length_set_ranges=self.trace_length_set_ranges,
            length_set_names=self.trace_length_set_names,
            area_boundary_intersects=self.
            trace_intersects_target_area_boundary,
        )

        empty_branches_and_nodes = self.branch_gdf.empty and self.node_gdf.empty
        if self.determine_branches_nodes and empty_branches_and_nodes:
            logging.info(
                "Determining branches and nodes.",
                extra=dict(
                    empty_branches_and_nodes=empty_branches_and_nodes,
                    network_name=self.name,
                ),
            )
            self.assign_branches_nodes()
        elif not empty_branches_and_nodes:
            logging.info(
                "Found branch_gdf and node_gdf in inputs. Using them.",
                extra=dict(
                    empty_branches_and_nodes=empty_branches_and_nodes,
                    network_name=self.name,
                ),
            )
            self.assign_branches_nodes(branches=self.branch_gdf,
                                       nodes=self.node_gdf)
            self.topology_determined = True

        logging.info(
            "Created and initialized Network instance.",
            # Network has .name attribute which will overwrite logging
            # attribute!!!
            extra={
                f"network_{key}": value
                for key, value in self.__dict__.items()
            },
        )
Exemplo n.º 6
0
def branches_and_nodes(
    traces: Union[gpd.GeoSeries, gpd.GeoDataFrame],
    areas: Union[gpd.GeoSeries, gpd.GeoDataFrame],
    snap_threshold: float,
    allowed_loops=10,
    already_clipped: bool = False,
    # unary_size_threshold: int = 5000,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """
    Determine branches and nodes of given traces.

    The traces will be cropped to the given target area(s) if not already
    clipped(already_clipped).

    TODO: unary_union will not respect identical traces or near-identical.
    Therefore cannot test if there are more branches than traces because
    there might be less due to this issue.
    """
    logging.info(
        "Starting determination of branches and nodes.",
        extra=dict(
            len_traces=len(traces),
            len_areas=len(areas),
            snap_threshold=snap_threshold,
            already_clipped=already_clipped,
            allowed_loops=allowed_loops,
        ),
    )
    traces_geosrs: gpd.GeoSeries = traces.geometry

    # Filter out traces that are not unique by wkt
    # unary_union will fail to take them into account any way
    traces_geosrs = filter_non_unique_traces(traces_geosrs,
                                             snap_threshold=snap_threshold)

    areas_geosrs: gpd.GeoSeries = areas.geometry

    # Collect into lists
    areas_list = [
        poly for poly in areas_geosrs.geometry.values
        if isinstance(poly, (Polygon, MultiPolygon))
    ]

    # Collect into lists
    traces_list = [
        trace for trace in traces.geometry.values
        if isinstance(trace, LineString)
    ]

    # Snapping occurs multiple times due to possible side effects of each snap
    loops = 0
    traces_list, any_changes_applied = snap_traces(traces_list,
                                                   snap_threshold,
                                                   areas=areas_list)
    # Snapping causes changes that might cause new errors. The snapping is looped
    # as many times as there are changed made to the data.
    # If loop count reaches allowed_loops, error is raised.
    while any_changes_applied:
        traces_list, any_changes_applied = snap_traces(
            traces_list,
            snap_threshold,
            final_allowed_loop=loops == allowed_loops,
            areas=areas_list,
        )
        loops += 1
        report_snapping_loop(loops, allowed_loops=allowed_loops)

    traces_geosrs = gpd.GeoSeries(traces_list, crs=traces.crs)

    # Clip if necessary
    if not already_clipped:
        traces_geosrs = crop_to_target_areas(
            traces_geosrs,
            areas_geosrs,
            keep_column_data=False,
        ).geometry

    # Remove too small geometries.
    traces_geosrs = traces_geosrs.loc[
        traces_geosrs.geometry.length > snap_threshold * 2.01]

    # Branches are determined with shapely/geopandas unary_union
    unary_union_result = traces_geosrs.unary_union
    if isinstance(unary_union_result, MultiLineString):
        branches_all = list(unary_union_result.geoms)
    elif isinstance(unary_union_result, LineString):
        branches_all = [unary_union_result]
    else:
        raise TypeError(
            "Expected unary_union_result to be of type (Multi)LineString."
            f" Got: {type(unary_union_result), unary_union_result}")

    # branches_all = list(
    #     safer_unary_union(
    #         traces_geosrs,
    #         snap_threshold=snap_threshold,
    #         size_threshold=unary_size_threshold,
    #     ).geoms
    # )

    # Filter out very short branches
    branches = gpd.GeoSeries(
        [b for b in branches_all if b.length > snap_threshold * 1.01],
        crs=traces_geosrs.crs,
    )

    # Report and error possibly unary_union failure
    if len(branches_all) < len(traces_geosrs):

        # unary_union can fail with too large datasets
        logging.critical(
            "Expected more branches than traces. Possible unary_union failure."
        )

    # Determine nodes and identities
    nodes, node_identities = node_identities_from_branches(
        branches=branches, areas=areas_geosrs, snap_threshold=snap_threshold)

    # Collect to GeoSeries
    nodes_geosrs = gpd.GeoSeries(nodes)

    # Determine branch identities
    branch_identities = get_branch_identities(branches, nodes_geosrs,
                                              node_identities, snap_threshold)

    # Collect to GeoDataFrames
    node_gdf = gpd.GeoDataFrame(
        {
            GEOMETRY_COLUMN: nodes_geosrs,
            CLASS_COLUMN: node_identities
        },
        crs=traces.crs)
    branch_gdf = gpd.GeoDataFrame(
        {
            GEOMETRY_COLUMN: branches,
            CONNECTION_COLUMN: branch_identities
        },
        crs=traces.crs,
    )
    return branch_gdf, node_gdf