Skip to content

Commit

Permalink
Add geometry match checks (#423)
Browse files Browse the repository at this point in the history
* Check if geometries for orifice, weir and pipe match their connection nodes

* Extend LinestringLocationCheck for all checks for linestring geometries that should match objects

* Replace CrossSectionLocationCheck with generic PointLocationCheck
  • Loading branch information
margrietpalm authored Dec 19, 2024
1 parent 4ea7ac4 commit d5faa59
Show file tree
Hide file tree
Showing 7 changed files with 505 additions and 138 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Changelog of threedi-modelchecker
- Collect all foreign key checks and give them a uniform error or warning (0001)
- Add unique check for boundary_condition_1d.connection_node_id
- Add checks for dry_weather_flow_distribution.distribution format, length and sum
- Check if geometries for orifice, weir and pipe match their connection nodes
- Check if geometries for control_measure_map, dry_weather_flow_map, surface_map and pump_map match the object they connect
- Check if windshielding geometry matches with that of the linked channel
- Check if the geometry of boundary_condition_1d, control_measure_location, lateral_1d, and pump matches with that of the linked connection node
- Check if the geometry of memory_control or table_control matches to that of the linked object


2.14.1 (2024-11-25)
Expand Down
166 changes: 166 additions & 0 deletions threedi_modelchecker/checks/location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from typing import List, NamedTuple

from sqlalchemy import func
from sqlalchemy.orm import aliased, Session
from threedi_schema.domain import models

from threedi_modelchecker.checks.base import BaseCheck
from threedi_modelchecker.checks.geo_query import distance


class PointLocationCheck(BaseCheck):
"""Check if cross section locations are within {max_distance} of their channel."""

def __init__(
self,
ref_column,
ref_table,
max_distance,
*args,
**kwargs,
):
self.max_distance = max_distance
self.ref_column = ref_column
self.ref_table = ref_table
super().__init__(*args, **kwargs)

def get_invalid(self, session):
# get all channels with more than 1 cross section location
return (
self.to_check(session)
.join(
self.ref_table,
self.ref_table.id == self.ref_column,
)
.filter(distance(self.column, self.ref_table.geom) > self.max_distance)
.all()
)

def description(self):
return (
f"{self.column_name} does not match the position of the object that "
f"{self.table.name}.{self.ref_column} refers to"
)


class LinestringLocationCheck(BaseCheck):
"""Check that linestring geometry starts / ends are close to their connection nodes
This allows for reversing the geometries. threedi-gridbuilder will reverse the geometries if
that lowers the distance to the connection nodes.
"""

def __init__(
self,
ref_column_start,
ref_column_end,
ref_table_start,
ref_table_end,
max_distance,
*args,
**kwargs,
):
self.max_distance = max_distance
self.ref_column_start = ref_column_start
self.ref_column_end = ref_column_end
self.ref_table_start = ref_table_start
self.ref_table_end = ref_table_end
super().__init__(*args, **kwargs)

def get_invalid(self, session: Session) -> List[NamedTuple]:
start_node = aliased(self.ref_table_start)
end_node = aliased(self.ref_table_end)

tol = self.max_distance
start_point = func.ST_PointN(self.column, 1)
end_point = func.ST_PointN(self.column, func.ST_NPoints(self.column))

start_ok = distance(start_point, start_node.geom) <= tol
end_ok = distance(end_point, end_node.geom) <= tol
start_ok_if_reversed = distance(end_point, start_node.geom) <= tol
end_ok_if_reversed = distance(start_point, end_node.geom) <= tol
return (
self.to_check(session)
.join(start_node, start_node.id == self.ref_column_start)
.join(end_node, end_node.id == self.ref_column_end)
.filter(
~(start_ok & end_ok),
~(start_ok_if_reversed & end_ok_if_reversed),
)
.all()
)

def description(self) -> str:
ref_start_name = f"{self.table.name}.{self.ref_column_start.name}"
ref_end_name = f"{self.table.name}.{self.ref_column_end.name}"
return f"{self.column_name} does not start or end at its connection nodes: {ref_start_name} and {ref_end_name} (tolerance = {self.max_distance} m)"


class ConnectionNodeLinestringLocationCheck(LinestringLocationCheck):
def __init__(self, column, *args, **kwargs):
table = column.table
super().__init__(
ref_column_start=table.c.connection_node_id_start,
ref_column_end=table.c.connection_node_id_end,
ref_table_start=models.ConnectionNode,
ref_table_end=models.ConnectionNode,
column=column,
*args,
**kwargs,
)

def description(self) -> str:
return f"{self.column_name} does not start or end at its connection node (tolerance = {self.max_distance} m)"


class ControlMeasureMapLinestringMapLocationCheck(LinestringLocationCheck):
def __init__(self, control_table, filters, *args, **kwargs):
super().__init__(
ref_column_start=models.ControlMeasureMap.measure_location_id,
ref_column_end=models.ControlMeasureMap.control_id,
ref_table_start=models.ControlMeasureLocation,
ref_table_end=control_table,
column=models.ControlMeasureMap.geom,
filters=filters,
*args,
**kwargs,
)


class DWFMapLinestringLocationCheck(LinestringLocationCheck):
def __init__(self, *args, **kwargs):
super().__init__(
ref_column_start=models.DryWeatherFlowMap.connection_node_id,
ref_column_end=models.DryWeatherFlowMap.dry_weather_flow_id,
ref_table_start=models.ConnectionNode,
ref_table_end=models.DryWeatherFlow,
column=models.DryWeatherFlowMap.geom,
*args,
**kwargs,
)


class PumpMapLinestringLocationCheck(LinestringLocationCheck):
def __init__(self, *args, **kwargs):
super().__init__(
ref_column_start=models.PumpMap.pump_id,
ref_column_end=models.PumpMap.connection_node_id_end,
ref_table_start=models.Pump,
ref_table_end=models.ConnectionNode,
column=models.PumpMap.geom,
*args,
**kwargs,
)


class SurfaceMapLinestringLocationCheck(LinestringLocationCheck):
def __init__(self, *args, **kwargs):
super().__init__(
ref_column_start=models.SurfaceMap.surface_id,
ref_column_end=models.SurfaceMap.connection_node_id,
ref_table_start=models.Surface,
ref_table_end=models.ConnectionNode,
column=models.SurfaceMap.geom,
*args,
**kwargs,
)
67 changes: 0 additions & 67 deletions threedi_modelchecker/checks/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,35 +54,6 @@ def description(self) -> str:
)


class CrossSectionLocationCheck(BaseCheck):
"""Check if cross section locations are within {max_distance} of their channel."""

def __init__(self, max_distance, *args, **kwargs):
super().__init__(column=models.CrossSectionLocation.geom, *args, **kwargs)
self.max_distance = max_distance

def get_invalid(self, session):
# get all channels with more than 1 cross section location
return (
self.to_check(session)
.join(
models.Channel,
models.Channel.id == models.CrossSectionLocation.channel_id,
)
.filter(
distance(models.CrossSectionLocation.geom, models.Channel.geom)
> self.max_distance
)
.all()
)

def description(self):
return (
f"cross_section_location.geom is invalid: the cross-section location "
f"should be located on the channel geometry (tolerance = {self.max_distance} m)"
)


class CrossSectionSameConfigurationCheck(BaseCheck):
"""Check the cross-sections on the object are either all open or all closed."""

Expand Down Expand Up @@ -494,44 +465,6 @@ def description(self) -> str:
)


class LinestringLocationCheck(BaseCheck):
"""Check that linestring geometry starts / ends are close to their connection nodes
This allows for reversing the geometries. threedi-gridbuilder will reverse the geometries if
that lowers the distance to the connection nodes.
"""

def __init__(self, *args, **kwargs):
self.max_distance = kwargs.pop("max_distance")
super().__init__(*args, **kwargs)

def get_invalid(self, session: Session) -> List[NamedTuple]:
start_node = aliased(models.ConnectionNode)
end_node = aliased(models.ConnectionNode)

tol = self.max_distance
start_point = func.ST_PointN(self.column, 1)
end_point = func.ST_PointN(self.column, func.ST_NPoints(self.column))

start_ok = distance(start_point, start_node.geom) <= tol
end_ok = distance(end_point, end_node.geom) <= tol
start_ok_if_reversed = distance(end_point, start_node.geom) <= tol
end_ok_if_reversed = distance(start_point, end_node.geom) <= tol
return (
self.to_check(session)
.join(start_node, start_node.id == self.table.c.connection_node_id_start)
.join(end_node, end_node.id == self.table.c.connection_node_id_end)
.filter(
~(start_ok & end_ok),
~(start_ok_if_reversed & end_ok_if_reversed),
)
.all()
)

def description(self) -> str:
return f"{self.column_name} does not start or end at its connection node (tolerance = {self.max_distance} m)"


class BoundaryCondition1DObjectNumberCheck(BaseCheck):
"""Check that the number of connected objects to 1D boundary connections is 1."""

Expand Down
Loading

0 comments on commit d5faa59

Please sign in to comment.