Skip to content

Commit 9beb7a6

Browse files
[Hotfix Main]: [SNAPPY] Added validator for duplicate refinements on an entity (#1813)
Co-authored-by: piotrkluba <piotr.kluba@flexcompute.com>
1 parent e1dd9ff commit 9beb7a6

File tree

3 files changed

+160
-2
lines changed

3 files changed

+160
-2
lines changed

flow360/component/simulation/meshing_param/snappy/snappy_params.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from flow360.component.simulation.meshing_param.meshing_specs import OctreeSpacing
1010
from flow360.component.simulation.meshing_param.snappy.snappy_mesh_refinements import (
1111
BodyRefinement,
12+
RegionRefinement,
1213
SnappyEntityRefinement,
1314
SnappySurfaceRefinementTypes,
1415
SurfaceEdgeRefinement,
@@ -75,6 +76,37 @@ def _check_body_refinements_w_defaults(self):
7576
)
7677
return self
7778

79+
@pd.field_validator("refinements", mode="after")
80+
@classmethod
81+
def _check_duplicate_refinements_per_entity(cls, refinements):
82+
"""Raise if the same refinement type is applied more than once to the same entity."""
83+
if refinements is None:
84+
return refinements
85+
86+
entity_refinement_map: dict[tuple[str, str], dict[str, int]] = {}
87+
refinement_types_with_entities = (BodyRefinement, RegionRefinement, SurfaceEdgeRefinement)
88+
89+
for refinement in refinements:
90+
if not isinstance(refinement, refinement_types_with_entities):
91+
continue
92+
if refinement.entities is None:
93+
continue
94+
refinement_type_name = type(refinement).__name__
95+
for entity in refinement.entities.stored_entities:
96+
entity_key = (type(entity).__name__, entity.name)
97+
counts = entity_refinement_map.setdefault(entity_key, {})
98+
counts[refinement_type_name] = counts.get(refinement_type_name, 0) + 1
99+
100+
for entity_key, type_counts in entity_refinement_map.items():
101+
for refinement_type_name, count in type_counts.items():
102+
if count > 1:
103+
raise ValueError(
104+
f"`{refinement_type_name}` is applied {count} times "
105+
f"to entity `{entity_key[1]}`. Each refinement type "
106+
f"can only be applied once per entity."
107+
)
108+
return refinements
109+
78110
@contextual_model_validator(mode="after")
79111
def _check_uniform_refinement_entities(self):
80112
# pylint: disable=no-member

tests/simulation/params/meshing_validation/test_meshing_param_validation.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,132 @@ def test_bad_refinements():
937937
)
938938

939939

940+
def test_duplicate_refinement_type_per_entity():
941+
"""Raise when the same refinement type is applied twice to one entity."""
942+
body = SnappyBody(name="car_body", surfaces=[])
943+
surface = Surface(name="wing")
944+
defaults = snappy.SurfaceMeshingDefaults(
945+
min_spacing=1 * u.mm, max_spacing=5 * u.mm, gap_resolution=0.01 * u.mm
946+
)
947+
948+
# -- Two BodyRefinements targeting the same SnappyBody --
949+
with pytest.raises(
950+
pd.ValidationError,
951+
match=r"`BodyRefinement` is applied 2 times to entity `car_body`",
952+
):
953+
snappy.SurfaceMeshingParams(
954+
defaults=defaults,
955+
refinements=[
956+
snappy.BodyRefinement(min_spacing=2 * u.mm, bodies=[body]),
957+
snappy.BodyRefinement(max_spacing=4 * u.mm, bodies=[body]),
958+
],
959+
)
960+
961+
# -- Two RegionRefinements targeting the same Surface --
962+
with pytest.raises(
963+
pd.ValidationError,
964+
match=r"`RegionRefinement` is applied 2 times to entity `wing`",
965+
):
966+
snappy.SurfaceMeshingParams(
967+
defaults=defaults,
968+
refinements=[
969+
snappy.RegionRefinement(
970+
min_spacing=1 * u.mm, max_spacing=3 * u.mm, regions=[surface]
971+
),
972+
snappy.RegionRefinement(
973+
min_spacing=2 * u.mm, max_spacing=4 * u.mm, regions=[surface]
974+
),
975+
],
976+
)
977+
978+
# -- Two SurfaceEdgeRefinements targeting the same SnappyBody --
979+
with pytest.raises(
980+
pd.ValidationError,
981+
match=r"`SurfaceEdgeRefinement` is applied 2 times to entity `car_body`",
982+
):
983+
snappy.SurfaceMeshingParams(
984+
defaults=defaults,
985+
refinements=[
986+
snappy.SurfaceEdgeRefinement(spacing=0.5 * u.mm, entities=[body]),
987+
snappy.SurfaceEdgeRefinement(spacing=1 * u.mm, entities=[body]),
988+
],
989+
)
990+
991+
# -- Two SurfaceEdgeRefinements targeting the same Surface --
992+
with pytest.raises(
993+
pd.ValidationError,
994+
match=r"`SurfaceEdgeRefinement` is applied 2 times to entity `wing`",
995+
):
996+
snappy.SurfaceMeshingParams(
997+
defaults=defaults,
998+
refinements=[
999+
snappy.SurfaceEdgeRefinement(spacing=0.5 * u.mm, entities=[surface]),
1000+
snappy.SurfaceEdgeRefinement(spacing=1 * u.mm, entities=[surface]),
1001+
],
1002+
)
1003+
1004+
1005+
def test_duplicate_refinement_different_types_is_allowed():
1006+
"""Different refinement types on the same entity should NOT raise."""
1007+
body = SnappyBody(name="car_body", surfaces=[])
1008+
surface = Surface(name="wing")
1009+
defaults = snappy.SurfaceMeshingDefaults(
1010+
min_spacing=1 * u.mm, max_spacing=5 * u.mm, gap_resolution=0.01 * u.mm
1011+
)
1012+
1013+
# BodyRefinement + SurfaceEdgeRefinement on the same SnappyBody is fine
1014+
snappy.SurfaceMeshingParams(
1015+
defaults=defaults,
1016+
refinements=[
1017+
snappy.BodyRefinement(min_spacing=2 * u.mm, bodies=[body]),
1018+
snappy.SurfaceEdgeRefinement(spacing=0.5 * u.mm, entities=[body]),
1019+
],
1020+
)
1021+
1022+
# RegionRefinement + SurfaceEdgeRefinement on the same Surface is fine
1023+
snappy.SurfaceMeshingParams(
1024+
defaults=defaults,
1025+
refinements=[
1026+
snappy.RegionRefinement(min_spacing=1 * u.mm, max_spacing=3 * u.mm, regions=[surface]),
1027+
snappy.SurfaceEdgeRefinement(spacing=0.5 * u.mm, entities=[surface]),
1028+
],
1029+
)
1030+
1031+
1032+
def test_duplicate_refinement_different_entities_is_allowed():
1033+
"""Same refinement type on different entities should NOT raise."""
1034+
body1 = SnappyBody(name="car_body", surfaces=[])
1035+
body2 = SnappyBody(name="other_body", surfaces=[])
1036+
defaults = snappy.SurfaceMeshingDefaults(
1037+
min_spacing=1 * u.mm, max_spacing=5 * u.mm, gap_resolution=0.01 * u.mm
1038+
)
1039+
1040+
snappy.SurfaceMeshingParams(
1041+
defaults=defaults,
1042+
refinements=[
1043+
snappy.BodyRefinement(min_spacing=2 * u.mm, bodies=[body1]),
1044+
snappy.BodyRefinement(min_spacing=3 * u.mm, bodies=[body2]),
1045+
],
1046+
)
1047+
1048+
1049+
def test_duplicate_refinement_body_and_surface_same_name_is_allowed():
1050+
"""SurfaceEdgeRefinement on a SnappyBody and a Surface sharing a name should NOT raise."""
1051+
body = SnappyBody(name="shared_name", surfaces=[])
1052+
surface = Surface(name="shared_name")
1053+
defaults = snappy.SurfaceMeshingDefaults(
1054+
min_spacing=1 * u.mm, max_spacing=5 * u.mm, gap_resolution=0.01 * u.mm
1055+
)
1056+
1057+
snappy.SurfaceMeshingParams(
1058+
defaults=defaults,
1059+
refinements=[
1060+
snappy.SurfaceEdgeRefinement(spacing=0.5 * u.mm, entities=[body]),
1061+
snappy.SurfaceEdgeRefinement(spacing=1 * u.mm, entities=[surface]),
1062+
],
1063+
)
1064+
1065+
9401066
def test_box_entity_enclosed_only_in_beta_mesher():
9411067
# raises when beta mesher is off
9421068
with pytest.raises(

tests/simulation/params/meshing_validation/test_refinements_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ def test_snappy_edge_refinement_validators():
160160
snappy.SurfaceEdgeRefinement(
161161
spacing=[2 * u.mm], distances=[5 * u.mm], entities=[Surface(name="test")]
162162
),
163-
snappy.SurfaceEdgeRefinement(spacing=2 * u.mm, entities=[Surface(name="test")]),
164-
snappy.SurfaceEdgeRefinement(entities=[Surface(name="test")]),
163+
snappy.SurfaceEdgeRefinement(spacing=2 * u.mm, entities=[Surface(name="test2")]),
164+
snappy.SurfaceEdgeRefinement(entities=[Surface(name="test3")]),
165165
],
166166
)
167167

0 commit comments

Comments
 (0)