Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add geometry match checks #423

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading