diff --git a/fractopo/tval/trace_validation.py b/fractopo/tval/trace_validation.py index ca804a2..e6e01a1 100644 --- a/fractopo/tval/trace_validation.py +++ b/fractopo/tval/trace_validation.py @@ -55,6 +55,7 @@ class Validation: AREA_EDGE_SNAP_MULTIPLIER: float = 1.5 TRIANGLE_ERROR_SNAP_MULTIPLIER: float = 10.0 OVERLAP_DETECTION_MULTIPLIER: float = 50.0 + STACKED_DETECTOR_BUFFER_MULTIPLIER: float = 5.0 # TODO: Separate SHARP TURNS to major and minor SHARP_AVG_THRESHOLD: float = 135.0 SHARP_PREV_SEG_THRESHOLD: float = 100.0 @@ -270,6 +271,7 @@ def run_validation( snap_threshold_error_multiplier=self.SNAP_THRESHOLD_ERROR_MULTIPLIER, overlap_detection_multiplier=self.OVERLAP_DETECTION_MULTIPLIER, triangle_error_snap_multiplier=self.TRIANGLE_ERROR_SNAP_MULTIPLIER, + stacked_detector_buffer_multiplier=self.STACKED_DETECTOR_BUFFER_MULTIPLIER, trace_candidates=trace_candidates, sharp_avg_threshold=self.SHARP_AVG_THRESHOLD, sharp_prev_seg_threshold=self.SHARP_PREV_SEG_THRESHOLD, diff --git a/fractopo/tval/trace_validation_utils.py b/fractopo/tval/trace_validation_utils.py index 0f03b17..f3e1403 100644 --- a/fractopo/tval/trace_validation_utils.py +++ b/fractopo/tval/trace_validation_utils.py @@ -27,6 +27,7 @@ def segment_within_buffer( snap_threshold: float, snap_threshold_error_multiplier: float, overlap_detection_multiplier: float, + stacked_detector_buffer_multiplier: float, ) -> bool: """ Check if segment is within buffer of multilinestring. @@ -48,7 +49,9 @@ def segment_within_buffer( return True buffered_linestring = safe_buffer( - linestring, snap_threshold * snap_threshold_error_multiplier + linestring, + (snap_threshold * snap_threshold_error_multiplier) + * stacked_detector_buffer_multiplier, ) assert isinstance(linestring, LineString) assert isinstance(buffered_linestring, Polygon) diff --git a/fractopo/tval/trace_validators.py b/fractopo/tval/trace_validators.py index 6acc4fa..2e4618f 100644 --- a/fractopo/tval/trace_validators.py +++ b/fractopo/tval/trace_validators.py @@ -569,6 +569,7 @@ def validation_method( snap_threshold_error_multiplier: float, overlap_detection_multiplier: float, triangle_error_snap_multiplier: float, + stacked_detector_buffer_multiplier: float, **_, ) -> bool: """ @@ -580,6 +581,7 @@ def validation_method( >>> snap_threshold_error_multiplier = 1.1 >>> overlap_detection_multiplier = 50 >>> triangle_error_snap_multiplier = 10 + >>> stacked_detector_buffer_multiplier = 5 >>> StackedTracesValidator.validation_method( ... geom, ... trace_candidates, @@ -587,6 +589,7 @@ def validation_method( ... snap_threshold_error_multiplier, ... overlap_detection_multiplier, ... triangle_error_snap_multiplier, + ... stacked_detector_buffer_multiplier, ... ) False @@ -596,6 +599,7 @@ def validation_method( >>> snap_threshold_error_multiplier = 1.1 >>> overlap_detection_multiplier = 50 >>> triangle_error_snap_multiplier = 10 + >>> stacked_detector_buffer_multiplier = 10 >>> StackedTracesValidator.validation_method( ... geom, ... trace_candidates, @@ -603,6 +607,7 @@ def validation_method( ... snap_threshold_error_multiplier, ... overlap_detection_multiplier, ... triangle_error_snap_multiplier, + ... stacked_detector_buffer_multiplier, ... ) True @@ -630,6 +635,7 @@ def validation_method( snap_threshold=snap_threshold, snap_threshold_error_multiplier=snap_threshold_error_multiplier, overlap_detection_multiplier=overlap_detection_multiplier, + stacked_detector_buffer_multiplier=stacked_detector_buffer_multiplier, ): return False diff --git a/tests/__init__.py b/tests/__init__.py index a7e44e8..8410ad7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,7 +17,7 @@ from functools import lru_cache, wraps from pathlib import Path from traceback import print_tb -from typing import Any, List +from typing import Any, List, NamedTuple, Optional, Union import geopandas as gpd import matplotlib.pyplot as plt @@ -441,6 +441,16 @@ def make_invalid_target_areas(): Path("tests/sample_data/vuosnaisteninter_invalid_traces.geojson") ) +stacked_and_v_node_error = gpd.read_file( + Path("tests/sample_data/validation_errors_202410/stacked_and_v_node_error.geojson") +) + +expected_stacked_traces_error = gpd.read_file( + Path( + "tests/sample_data/validation_errors_202410/expected_stacked_traces_error.geojson" + ) +) + def get_nice_traces(): """ @@ -771,29 +781,37 @@ def get_geosrs_identicals(): # Straight line which is intersected twice by same line intersected_3_times = LineString([Point(-3, -4), Point(-3, -1)]) -test_validation_params = [ - ( + + +class ValidationParamType(NamedTuple): + traces: gpd.GeoDataFrame + area: Union[gpd.GeoDataFrame, gpd.GeoSeries] + name: str + assume_errors: Optional[list] + allow_fix: bool = True + snap_threshold: float = 0.01 + + +test_validation_params: List[ValidationParamType] = [ + ValidationParamType( kb7_traces_50, # traces kb7_area, # area "kb7", # name - True, # auto_fix [SharpCornerValidator.ERROR], # assume_errors ), - ( + ValidationParamType( kb7_traces_50_z, # traces kb7_area, # area "kb7_z_coordinates", # name - True, # auto_fix [SharpCornerValidator.ERROR], # assume_errors ), - ( + ValidationParamType( hastholmen_traces, # traces hastholmen_area, # area "hastholmen_traces", # name - True, # auto_fix [GeomTypeValidator.ERROR], # assume_errors ), - ( + ValidationParamType( gpd.GeoDataFrame( geometry=make_invalid_traces( snap_threshold=0.01, snap_threshold_error_multiplier=1.1 @@ -801,17 +819,15 @@ def get_geosrs_identicals(): ), # traces gpd.GeoDataFrame(geometry=make_invalid_target_areas()), # area "invalid_traces", # name - True, # auto_fix None, # assume_errors ), - ( + ValidationParamType( gpd.GeoDataFrame(geometry=[LineString([(0, 0), (0, 1)])]), # traces gpd.GeoDataFrame(geometry=[box(-1, -1, 1, 1.011)]), # area "TargetAreaSnapValidator error", # name - True, # auto_fix [TargetAreaSnapValidator.ERROR], # assume_errors ), - ( + ValidationParamType( gpd.GeoDataFrame( geometry=[LineString([(0, 0), (0, 1)]), LineString([(5, 5), (5, 6)])] ), # traces @@ -822,10 +838,9 @@ def get_geosrs_identicals(): ] ), # area "TargetAreaSnapValidator error", # name - True, # auto_fix [TargetAreaSnapValidator.ERROR], # assume_errors ), - ( + ValidationParamType( gpd.GeoDataFrame( geometry=[LineString([(0, 0), (0, 1)]), LineString([(5, 5), (5, 6)])] ), # traces @@ -840,10 +855,9 @@ def get_geosrs_identicals(): ] ), # area "TargetAreaSnapValidator error", # name - True, # auto_fix [TargetAreaSnapValidator.ERROR], # assume_errors ), - ( + ValidationParamType( gpd.GeoDataFrame( geometry=[ LineString([(0, 0, 0), (1, 1, 1)]), @@ -856,28 +870,45 @@ def get_geosrs_identicals(): ] ), # area "traces-with-z-coordinates", # name - True, # auto_fix [], # assume_errors ), - ( + ValidationParamType( vuosnaisten_invalid_traces, # traces gpd.GeoSeries( [general.bounding_polygon(vuosnaisten_invalid_traces)], crs=vuosnaisten_invalid_traces.crs, ), # area "vuosnaisten_invalid_traces", # name - True, # auto_fix [StackedTracesValidator.ERROR], # assume_errors ), - ( + ValidationParamType( gpd.GeoDataFrame(geometry=[]), # traces gpd.GeoSeries( [box(0, 0, 10, 10)], ), # area "empty_geodataframe", # name - True, # auto_fix [], # assume_errors ), + ValidationParamType( + stacked_and_v_node_error, # traces + gpd.GeoSeries( + [general.bounding_polygon(stacked_and_v_node_error)], + crs=stacked_and_v_node_error.crs, + ), # area + "stacked_and_v_node_error", # name + [StackedTracesValidator.ERROR], # assume_errors + snap_threshold=0.001, + ), + ValidationParamType( + expected_stacked_traces_error, # traces + gpd.GeoSeries( + [general.bounding_polygon(expected_stacked_traces_error)], + crs=expected_stacked_traces_error.crs, + ), # area + "expected_stacked_traces_error", # name + [StackedTracesValidator.ERROR], # assume_errors + snap_threshold=0.001, + ), ] test_determine_v_nodes_params = [ @@ -1369,14 +1400,15 @@ def get_geosrs_identicals(): ] test_segment_within_buffer_params = [ - (valid_geom, invalid_geom_multilinestring, 0.001, 1.1, 50, True), - (valid_geom, mergeable_geom_multilinestring, 0.001, 1.1, 50, True), + (valid_geom, invalid_geom_multilinestring, 0.001, 1.1, 50, 5, True), + (valid_geom, mergeable_geom_multilinestring, 0.001, 1.1, 50, 5, True), ( valid_geom, MultiLineString([LineString([(10, 10), (50, 50)])]), 0.001, 1.1, 50, + 5, False, ), ( @@ -1386,6 +1418,7 @@ def get_geosrs_identicals(): 0.001, 1.1, 50, + 5, True, ), ] @@ -1578,6 +1611,7 @@ def get_geosrs_identicals(): UnderlappingSnapValidator._OVERLAPPING ] = known_non_underlaping_gdfs_but_overlapping + # False Positives # =============== diff --git a/tests/sample_data/validation_errors_202410/expected_stacked_traces_error.geojson b/tests/sample_data/validation_errors_202410/expected_stacked_traces_error.geojson new file mode 100644 index 0000000..d6d78c7 --- /dev/null +++ b/tests/sample_data/validation_errors_202410/expected_stacked_traces_error.geojson @@ -0,0 +1,10 @@ +{ +"type": "FeatureCollection", +"name": "expected_multi_junction_error", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::3067" } }, +"features": [ +{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 184435.832299999892712, 6721901.704199999570847 ], [ 184435.827499999664724, 6721901.625399999320507 ], [ 184435.820299999788404, 6721901.509199999272823 ] ] } }, +{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 184435.832799999974668, 6721901.755599999800324 ], [ 184435.826999999582767, 6721901.616800000891089 ], [ 184435.831399999558926, 6721901.506799999624491 ] ] } }, +{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 184435.826999999582767, 6721901.616800000891089 ], [ 184435.879900000058115, 6721901.643699999898672 ], [ 184435.931300000287592, 6721901.667199999094009 ], [ 184435.9682, 6721901.685900000855327 ], [ 184435.980200000107288, 6721901.710899999365211 ], [ 184436.00760000012815, 6721901.725299999117851 ], [ 184436.033099999651313, 6721901.72440000064671 ], [ 184436.08490000013262, 6721901.761800000444055 ], [ 184436.186700000427663, 6721901.82039999961853 ], [ 184436.244800000451505, 6721901.872300000861287 ], [ 184436.280399999581277, 6721901.911100000143051 ], [ 184436.354299999773502, 6721901.988900000229478 ] ] } } +] +} diff --git a/tests/sample_data/validation_errors_202410/stacked_and_v_node_error.geojson b/tests/sample_data/validation_errors_202410/stacked_and_v_node_error.geojson new file mode 100644 index 0000000..de92624 --- /dev/null +++ b/tests/sample_data/validation_errors_202410/stacked_and_v_node_error.geojson @@ -0,0 +1,9 @@ +{ +"type": "FeatureCollection", +"name": "stacked_and_v_node_error", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::3067" } }, +"features": [ +{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 184471.715300001262221, 6721913.799100005067885 ], [ 184471.13750000204891, 6721913.4857 ], [ 184470.177800001343712, 6721913.2507000034675 ], [ 184469.394400000572205, 6721913.005899999290705 ], [ 184468.718613977252971, 6721912.836023729294538 ] ] } }, +{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 184471.481800000183284, 6721914.574100000783801 ], [ 184471.497899999842048, 6721914.501800000667572 ], [ 184471.533999999985099, 6721914.4054000005126 ], [ 184471.613400000147521, 6721914.317900000140071 ], [ 184471.667200000025332, 6721914.211799999698997 ], [ 184471.698699999600649, 6721914.100099999457598 ], [ 184471.710799999535084, 6721913.947499999776483 ], [ 184471.635300000198185, 6721913.755699999630451 ], [ 184471.567999999970198, 6721913.719200000166893 ] ] } } +] +} diff --git a/tests/tval/test_trace_validation.py b/tests/tval/test_trace_validation.py index 2387c63..f24eff4 100644 --- a/tests/tval/test_trace_validation.py +++ b/tests/tval/test_trace_validation.py @@ -30,10 +30,12 @@ def test__validate(validator, geom, current_errors, allow_fix, assumed_result): @pytest.mark.parametrize( - "traces,area,name,allow_fix,assume_errors", - tests.test_validation_params, + "traces,area,name,assume_errors,allow_fix,snap_threshold", + [pytest.param(*param, id=param.name) for param in tests.test_validation_params], ) -def test_validation(traces, area, name, allow_fix, assume_errors: Optional[List[str]]): +def test_validation( + traces, area, name, allow_fix, assume_errors: Optional[List[str]], snap_threshold +): """ Test Validation. """ @@ -44,7 +46,12 @@ def test_validation(traces, area, name, allow_fix, assume_errors: Optional[List[ ) validated_gdf = Validation( - traces, area, name, allow_fix, **additional_kwargs + traces, + area, + name, + allow_fix, + **additional_kwargs, + SNAP_THRESHOLD=snap_threshold, ).run_validation() if traces.shape[0] == 0: assert validated_gdf.shape[0] == 0 diff --git a/tests/tval/test_trace_validation_utils.py b/tests/tval/test_trace_validation_utils.py index df9957c..257de87 100644 --- a/tests/tval/test_trace_validation_utils.py +++ b/tests/tval/test_trace_validation_utils.py @@ -1,6 +1,7 @@ """ Test trace_validation_utils. """ + import pytest from shapely.geometry import LineString @@ -10,7 +11,7 @@ @pytest.mark.parametrize( "linestring,multilinestring,snap_threshold,snap_threshold_error_multiplier," - "overlap_detection_multiplier,assume_result", + "overlap_detection_multiplier,stacked_detector_buffer_multiplier,assume_result", tests.test_segment_within_buffer_params, ) def test_segment_within_buffer( @@ -19,6 +20,7 @@ def test_segment_within_buffer( snap_threshold, snap_threshold_error_multiplier, overlap_detection_multiplier, + stacked_detector_buffer_multiplier, assume_result, ): """ @@ -30,6 +32,7 @@ def test_segment_within_buffer( snap_threshold, snap_threshold_error_multiplier, overlap_detection_multiplier, + stacked_detector_buffer_multiplier, ) assert isinstance(result, bool) assert assume_result == result