From 4fe6f1f3527999cbf2f40b5e3fcd91d92da1b902 Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Thu, 26 Feb 2026 13:07:11 +0000 Subject: [PATCH 1/9] added validator to allow certain geometries of uniform refinement to snappy --- .../meshing_param/meshing_validators.py | 28 +++ .../simulation/meshing_param/params.py | 17 ++ .../meshing_param/snappy/snappy_params.py | 20 +- .../test_refinements_validation.py | 179 +++++++++++++++++- 4 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 flow360/component/simulation/meshing_param/meshing_validators.py diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py new file mode 100644 index 000000000..0eff72d9a --- /dev/null +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -0,0 +1,28 @@ +from flow360.component.simulation.meshing_param.volume_params import UniformRefinement +from flow360.component.simulation.primitives import Box, Cylinder, Sphere +import flow360.component.simulation.units as u + + +def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): + """Validate that a UniformRefinement's entities are compatible with snappyHexMesh. + + Raises ValueError if any Box has a non-axis-aligned rotation or any Cylinder is hollow. + """ + for entity in refinement.entities.stored_entities: + if not isinstance(entity, (Box, Cylinder, Sphere)): + raise ValueError( + "UniformRefinement for snappy only supports entities of type Box, Cylinder, or Sphere. " + f"Got {type(entity).__name__} instead." + ) + if ( + isinstance(entity, Box) + and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg + ): + raise ValueError( + "UniformRefinement for snappy accepts only Boxes with axes aligned" + + " with the global coordinate system (angle_of_rotation=0)." + ) + if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: + raise ValueError( + "UniformRefinement for snappy accepts only full cylinders (where inner_radius = 0)." + ) diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index ca75c1f7e..b6811b4e8 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -22,6 +22,9 @@ MeshingDefaults, VolumeMeshingDefaults, ) +from flow360.component.simulation.meshing_param.meshing_validators import ( + validate_snappy_uniform_refinement_entities, +) from flow360.component.simulation.meshing_param.volume_params import ( AutomatedFarfield, AxisymmetricRefinement, @@ -34,6 +37,7 @@ UniformRefinement, UserDefinedFarfield, WindTunnelFarfield, + validate_snappy_uniform_refinement_entities, ) from flow360.component.simulation.primitives import SeedpointVolume from flow360.component.simulation.validation.validation_context import ( @@ -458,6 +462,19 @@ def _check_sizing_against_octree_series(self, param_info: ParamsValidationInfo): return self + @contextual_model_validator(mode="after") + def _check_snappy_uniform_refinement_entities(self, param_info: ParamsValidationInfo): + """Validate projected UniformRefinement entities are compatible with snappyHexMesh.""" + if not param_info.use_snappy: + return self + for refinement in self.refinements: # pylint: disable=not-an-iterable + if ( + isinstance(refinement, UniformRefinement) + and refinement.project_to_surface is not False + ): + validate_snappy_uniform_refinement_entities(refinement) + return self + SurfaceMeshingParams = Annotated[ Union[snappy.SurfaceMeshingParams], pd.Field(discriminator="type_name") diff --git a/flow360/component/simulation/meshing_param/snappy/snappy_params.py b/flow360/component/simulation/meshing_param/snappy/snappy_params.py index 5cecd6adf..35b0b710e 100644 --- a/flow360/component/simulation/meshing_param/snappy/snappy_params.py +++ b/flow360/component/simulation/meshing_param/snappy/snappy_params.py @@ -4,9 +4,11 @@ import pydantic as pd -import flow360.component.simulation.units as u from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.meshing_param.meshing_specs import OctreeSpacing +from flow360.component.simulation.meshing_param.meshing_validators import ( + validate_snappy_uniform_refinement_entities, +) from flow360.component.simulation.meshing_param.snappy.snappy_mesh_refinements import ( BodyRefinement, RegionRefinement, @@ -22,7 +24,6 @@ SurfaceMeshingDefaults, ) from flow360.component.simulation.meshing_param.volume_params import UniformRefinement -from flow360.component.simulation.primitives import Box, Cylinder from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.validation.validation_context import ( ParamsValidationInfo, @@ -124,20 +125,7 @@ def _check_uniform_refinement_entities(self): return self for refinement in self.refinements: if isinstance(refinement, UniformRefinement): - # No expansion needed since we only allow Draft entities here. - for entity in refinement.entities.stored_entities: - if ( - isinstance(entity, Box) - and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg - ): - raise ValueError( - "UniformRefinement for snappy accepts only Boxes with axes aligned" - + " with the global coordinate system (angle_of_rotation=0)." - ) - if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: - raise ValueError( - "UniformRefinement for snappy accepts only full cylinders (where inner_radius = 0)." - ) + validate_snappy_uniform_refinement_entities(refinement) return self diff --git a/tests/simulation/params/meshing_validation/test_refinements_validation.py b/tests/simulation/params/meshing_validation/test_refinements_validation.py index d4a9aa6f7..b92dcb177 100644 --- a/tests/simulation/params/meshing_validation/test_refinements_validation.py +++ b/tests/simulation/params/meshing_validation/test_refinements_validation.py @@ -5,8 +5,20 @@ import flow360.component.simulation.units as u from flow360.component.simulation.meshing_param import snappy -from flow360.component.simulation.meshing_param.volume_params import UniformRefinement +from flow360.component.simulation.meshing_param.meshing_specs import ( + VolumeMeshingDefaults, +) +from flow360.component.simulation.meshing_param.params import ( + ModularMeshingWorkflow, + VolumeMeshingParams, +) +from flow360.component.simulation.meshing_param.volume_params import ( + AutomatedFarfield, + UniformRefinement, +) from flow360.component.simulation.primitives import Box, Cylinder, SnappyBody, Surface +from flow360.component.simulation.services import ValidationCalledBy, validate_model +from flow360.component.simulation.simulation_params import SimulationParams from flow360.component.simulation.unit_system import SI_unit_system @@ -211,3 +223,168 @@ def test_snappy_body_refinement_validator(): snappy.BodyRefinement( bodies=SnappyBody(name="body1", surfaces=[Surface(name="surface")]), gap_resolution=2 * u.mm ) + + +def _make_snappy_params_with_volume_uniform_refinement(refinement): + """Helper to build SimulationParams with a UniformRefinement in volume meshing.""" + with SI_unit_system: + return SimulationParams( + meshing=ModularMeshingWorkflow( + surface_meshing=snappy.SurfaceMeshingParams( + defaults=snappy.SurfaceMeshingDefaults( + min_spacing=1 * u.mm, + max_spacing=10 * u.mm, + gap_resolution=0.1 * u.mm, + ) + ), + volume_meshing=VolumeMeshingParams( + defaults=VolumeMeshingDefaults( + boundary_layer_first_layer_thickness=1 * u.mm, + ), + refinements=[refinement], + ), + zones=[AutomatedFarfield()], + ) + ) + + +def test_volume_uniform_refinement_rotated_box_project_to_surface(): + """ + A UniformRefinement with a rotated Box placed in volume meshing with + project_to_surface=True must trigger the same snappy validation error + as if it were placed directly in the surface meshing refinements. + """ + rotated_box = Box( + center=[0, 0, 0] * u.m, + size=[1, 1, 1] * u.m, + axis_of_rotation=[0, 0, 1], + angle_of_rotation=45 * u.deg, + name="rotated_box", + ) + + refinement = UniformRefinement( + spacing=5 * u.mm, + entities=[rotated_box], + project_to_surface=True, + ) + + params = _make_snappy_params_with_volume_uniform_refinement(refinement) + + _, errors, _ = validate_model( + params_as_dict=params.model_dump(mode="json"), + validated_by=ValidationCalledBy.LOCAL, + root_item_type="Geometry", + validation_level="VolumeMesh", + ) + + assert errors is not None, ( + "Expected validation error for rotated Box in volume UniformRefinement " + "with project_to_surface=True" + ) + error_messages = [e["msg"] for e in errors] + assert any("angle_of_rotation" in msg or "axes aligned" in msg for msg in error_messages) + + +def test_volume_uniform_refinement_hollow_cylinder_project_to_surface(): + """ + A UniformRefinement with a hollow Cylinder (inner_radius > 0) placed in + volume meshing with project_to_surface=True must trigger the same snappy + validation error as if it were in the surface meshing refinements. + """ + hollow_cylinder = Cylinder( + name="hollow_cyl", + inner_radius=3 * u.mm, + outer_radius=7 * u.mm, + axis=[0, 0, 1], + center=[0, 0, 0] * u.m, + height=10 * u.mm, + ) + + refinement = UniformRefinement( + spacing=5 * u.mm, + entities=[hollow_cylinder], + project_to_surface=True, + ) + + params = _make_snappy_params_with_volume_uniform_refinement(refinement) + + _, errors, _ = validate_model( + params_as_dict=params.model_dump(mode="json"), + validated_by=ValidationCalledBy.LOCAL, + root_item_type="Geometry", + validation_level="VolumeMesh", + ) + + assert errors is not None, ( + "Expected validation error for hollow Cylinder in volume UniformRefinement " + "with project_to_surface=True" + ) + error_messages = [e["msg"] for e in errors] + assert any("inner_radius" in msg or "full cylinders" in msg for msg in error_messages) + + +def test_volume_uniform_refinement_default_project_to_surface(): + """ + When project_to_surface is None (the default, which acts as True for snappy), + the same snappy constraints should be enforced. + """ + rotated_box = Box( + center=[0, 0, 0] * u.m, + size=[1, 1, 1] * u.m, + axis_of_rotation=[0, 0, 1], + angle_of_rotation=90 * u.deg, + name="rotated_box_default", + ) + + refinement = UniformRefinement( + spacing=5 * u.mm, + entities=[rotated_box], + ) + + params = _make_snappy_params_with_volume_uniform_refinement(refinement) + + _, errors, _ = validate_model( + params_as_dict=params.model_dump(mode="json"), + validated_by=ValidationCalledBy.LOCAL, + root_item_type="Geometry", + validation_level="VolumeMesh", + ) + + assert errors is not None, ( + "Expected validation error for rotated Box in volume UniformRefinement " + "with default project_to_surface (None)" + ) + error_messages = [e["msg"] for e in errors] + assert any("angle_of_rotation" in msg or "axes aligned" in msg for msg in error_messages) + + +def test_volume_uniform_refinement_project_to_surface_false_skips_validation(): + """ + When project_to_surface=False, snappy-specific constraints on entities + should NOT be enforced since the refinement won't be projected to the + surface mesh. + """ + rotated_box = Box( + center=[0, 0, 0] * u.m, + size=[1, 1, 1] * u.m, + axis_of_rotation=[0, 0, 1], + angle_of_rotation=45 * u.deg, + name="rotated_box_no_project", + ) + + refinement = UniformRefinement( + spacing=5 * u.mm, + entities=[rotated_box], + project_to_surface=False, + ) + + params = _make_snappy_params_with_volume_uniform_refinement(refinement) + + _, errors, _ = validate_model( + params_as_dict=params.model_dump(mode="json"), + validated_by=ValidationCalledBy.LOCAL, + root_item_type="Geometry", + validation_level="VolumeMesh", + ) + + assert errors is None, "No snappy validation error expected when project_to_surface=False" From 4af1328444bc9d6cb22d067efbfb604d18540a8b Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Thu, 26 Feb 2026 13:40:59 +0000 Subject: [PATCH 2/9] linter --- .../simulation/meshing_param/meshing_validators.py | 8 +++++--- flow360/component/simulation/meshing_param/params.py | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py index 0eff72d9a..f02b6f432 100644 --- a/flow360/component/simulation/meshing_param/meshing_validators.py +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -1,6 +1,8 @@ +"""Shared validation helpers for meshing parameters.""" + +import flow360.component.simulation.units as u from flow360.component.simulation.meshing_param.volume_params import UniformRefinement from flow360.component.simulation.primitives import Box, Cylinder, Sphere -import flow360.component.simulation.units as u def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): @@ -16,13 +18,13 @@ def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): ) if ( isinstance(entity, Box) - and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg + and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg # pylint: disable=no-member ): raise ValueError( "UniformRefinement for snappy accepts only Boxes with axes aligned" + " with the global coordinate system (angle_of_rotation=0)." ) - if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: + if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: # pylint: disable=no-member raise ValueError( "UniformRefinement for snappy accepts only full cylinders (where inner_radius = 0)." ) diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index 7db938c27..55384cf17 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -37,7 +37,6 @@ UniformRefinement, UserDefinedFarfield, WindTunnelFarfield, - validate_snappy_uniform_refinement_entities, ) from flow360.component.simulation.primitives import SeedpointVolume from flow360.component.simulation.validation.validation_context import ( From a9cf729a98b791b12ff5b2050a9e7743eebcd5f5 Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Thu, 26 Feb 2026 13:57:31 +0000 Subject: [PATCH 3/9] black --- .../simulation/meshing_param/meshing_validators.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py index f02b6f432..d3bda38cb 100644 --- a/flow360/component/simulation/meshing_param/meshing_validators.py +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -18,13 +18,16 @@ def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): ) if ( isinstance(entity, Box) - and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg # pylint: disable=no-member + and entity.angle_of_rotation.to("deg") % (360 * u.deg) + != 0 * u.deg # pylint: disable=no-member ): raise ValueError( "UniformRefinement for snappy accepts only Boxes with axes aligned" + " with the global coordinate system (angle_of_rotation=0)." ) - if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: # pylint: disable=no-member + if ( + isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m + ): # pylint: disable=no-member raise ValueError( "UniformRefinement for snappy accepts only full cylinders (where inner_radius = 0)." ) From cba7c6213800548c93c9cdc1f6757675e2b8c0cc Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Thu, 26 Feb 2026 14:34:29 +0000 Subject: [PATCH 4/9] linter --- .../simulation/meshing_param/meshing_validators.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py index d3bda38cb..89183dcf8 100644 --- a/flow360/component/simulation/meshing_param/meshing_validators.py +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -10,6 +10,7 @@ def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): Raises ValueError if any Box has a non-axis-aligned rotation or any Cylinder is hollow. """ + # pylint: disable=no-member for entity in refinement.entities.stored_entities: if not isinstance(entity, (Box, Cylinder, Sphere)): raise ValueError( @@ -18,16 +19,13 @@ def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): ) if ( isinstance(entity, Box) - and entity.angle_of_rotation.to("deg") % (360 * u.deg) - != 0 * u.deg # pylint: disable=no-member + and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg ): raise ValueError( "UniformRefinement for snappy accepts only Boxes with axes aligned" + " with the global coordinate system (angle_of_rotation=0)." ) - if ( - isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m - ): # pylint: disable=no-member + if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: raise ValueError( "UniformRefinement for snappy accepts only full cylinders (where inner_radius = 0)." ) From f777ec1df9f3bdef8ba6a960fad47d9511db468c Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Thu, 26 Feb 2026 15:54:15 +0000 Subject: [PATCH 5/9] other way of verifying the entity types --- .../meshing_param/meshing_validators.py | 5 - .../simulation/meshing_param/volume_params.py | 20 +++ .../test_meshing_param_validation.py | 58 +++++++- ...nappy_coupled_refinements_with_sphere.json | 131 ++++++++++++++++++ .../test_surface_meshing_translator.py | 55 ++++++++ 5 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py index 89183dcf8..0eb738cf8 100644 --- a/flow360/component/simulation/meshing_param/meshing_validators.py +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -12,11 +12,6 @@ def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): """ # pylint: disable=no-member for entity in refinement.entities.stored_entities: - if not isinstance(entity, (Box, Cylinder, Sphere)): - raise ValueError( - "UniformRefinement for snappy only supports entities of type Box, Cylinder, or Sphere. " - f"Got {type(entity).__name__} instead." - ) if ( isinstance(entity, Box) and entity.angle_of_rotation.to("deg") % (360 * u.deg) != 0 * u.deg diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 4feb8f3cd..d193812eb 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -102,6 +102,26 @@ def check_entities_used_with_beta_mesher(cls, values, param_info: ParamsValidati return values + @contextual_field_validator("entities", mode="after") + @classmethod + def check_entities_used_with_snappy(cls, values, param_info: ParamsValidationInfo): + """Check that only Box, Cylinder, and Sphere entities are used with snappyHexMesh.""" + + if values is None: + return values + if not param_info.use_snappy: + return values + + expanded = param_info.expand_entity_list(values) + for entity in expanded: + if not isinstance(entity, (Box, Cylinder, Sphere)): + raise ValueError( + f"`{type(entity).__name__}` entity for `UniformRefinement` is not supported " + "with snappyHexMesh. Only `Box`, `Cylinder`, and `Sphere` are allowed." + ) + + return values + @contextual_model_validator(mode="after") def check_project_to_surface_with_snappy(self, param_info: ParamsValidationInfo): """Check that project_to_surface is used only with snappy.""" diff --git a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py index 8eb382d5c..c172b85d4 100644 --- a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py +++ b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py @@ -63,6 +63,10 @@ beta_mesher_context.is_beta_mesher = True beta_mesher_context.project_length_unit = "mm" +snappy_context = ParamsValidationInfo({}, []) +snappy_context.use_snappy = True +snappy_context.is_beta_mesher = True + def test_structured_box_only_in_beta_mesher(): # raises when beta mesher is off @@ -882,7 +886,20 @@ def test_sphere_in_uniform_refinement(): ], ) - # raises without beta mesher + # also allowed with snappy + with ValidationContext(VOLUME_MESH, snappy_context): + with CGS_unit_system: + sphere = Sphere( + name="s_snappy", + center=(0, 0, 0), + radius=1.0, + ) + UniformRefinement( + entities=[sphere], + spacing=0.1, + ) + + # raises without beta mesher or snappy with pytest.raises( pd.ValidationError, match=r"`Sphere` entity for `UniformRefinement` is supported only with beta mesher", @@ -900,6 +917,45 @@ def test_sphere_in_uniform_refinement(): ) +def test_uniform_refinement_snappy_entity_restrictions(): + """With snappy, UniformRefinement only accepts Box, Cylinder, and Sphere entities.""" + # Box, Cylinder, Sphere all allowed + with ValidationContext(VOLUME_MESH, snappy_context): + with CGS_unit_system: + UniformRefinement( + entities=[ + Box(center=(0, 0, 0), size=(1, 1, 1), name="box"), + Cylinder( + name="cyl", + axis=(0, 0, 1), + center=(0, 0, 0), + height=1.0, + outer_radius=0.5, + ), + Sphere(name="sph", center=(0, 0, 0), radius=1.0), + ], + spacing=0.1, + ) + + # AxisymmetricBody rejected with snappy + with pytest.raises( + pd.ValidationError, + match=r"`AxisymmetricBody` entity for `UniformRefinement` is not supported with snappyHexMesh", + ): + with ValidationContext(VOLUME_MESH, snappy_context): + with CGS_unit_system: + axisymmetric_body = AxisymmetricBody( + name="axisymm", + axis=(0, 0, 1), + center=(0, 0, 0), + profile_curve=[(-1, 0), (-1, 1), (1, 1), (1, 0)], + ) + UniformRefinement( + entities=[axisymmetric_body], + spacing=0.1, + ) + + def test_require_mesh_zones(): with SI_unit_system: ModularMeshingWorkflow( diff --git a/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json b/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json new file mode 100644 index 000000000..fdb9a1361 --- /dev/null +++ b/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json @@ -0,0 +1,131 @@ +{ + "geometry": { + "bodies": [ + { + "bodyName": "body0", + "gap": 1.0, + "spacing": { + "min": 3.0, + "max": 4.0 + }, + "regions": [ + { + "patchName": "patch0" + }, + { + "patchName": "patch1" + } + ] + }, + { + "bodyName": "body1", + "gap": 1.0, + "spacing": { + "min": 3.0, + "max": 4.0 + }, + "regions": [ + { + "patchName": "patch0" + }, + { + "patchName": "patch1" + }, + { + "patchName": "patch2" + } + ] + }, + { + "bodyName": "body2", + "gap": 1.0, + "spacing": { + "min": 3.0, + "max": 4.0 + }, + "regions": [ + { + "patchName": "patch0" + } + ] + }, + { + "bodyName": "body3", + "gap": 1.0, + "spacing": { + "min": 3.0, + "max": 4.0 + }, + "regions": [ + { + "patchName": "patch0" + } + ] + } + ], + "refinementVolumes": [ + { + "spacing": 2.0, + "name": "sphere0", + "type": "sphere", + "centre": { + "x": 5.0, + "y": 10.0, + "z": 15.0 + }, + "radius": 25.0 + } + ] + }, + "mesherSettings": { + "snappyHexMesh": { + "castellatedMeshControls": { + "resolveFeatureAngle": 25.0, + "nCellsBetweenLevels": 1, + "minRefinementCells": 10 + }, + "snapControls": { + "nSmoothPatch": 3, + "tolerance": 2.0, + "nSolveIter": 30, + "nRelaxIter": 5, + "nFeatureSnapIter": 15, + "multiRegionFeatureSnap": true, + "strictRegionSnap": false + } + }, + "meshQuality": { + "maxNonOrtho": 85.0, + "maxBoundarySkewness": 20.0, + "maxInternalSkewness": 50.0, + "maxConcave": 50.0, + "minVol": 1.9531250000000004e-19, + "minTetQuality": 1e-09, + "minArea": 1e-12, + "minTwist": -2, + "minDeterminant": -100000.0, + "minVolRatio": 0, + "minFaceWeight": 0, + "minTriangleTwist": -1, + "nSmoothScale": 4, + "errorReduction": 0.75, + "zMetricThreshold": 0.8, + "featureEdgeDeduplicationTolerance": 0.2, + "minVolCollapseRatio": 0 + } + }, + "smoothingControls": { + "lambda": 0.7, + "mu": 0.71, + "iter": 5 + }, + "enforcedSpacing": 5.0, + "cadIsFluid": true, + "locationInMesh": { + "farfield": [ + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/tests/simulation/translator/test_surface_meshing_translator.py b/tests/simulation/translator/test_surface_meshing_translator.py index 95f8b62f2..4b9f1740b 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -60,6 +60,7 @@ MirroredGeometryBodyGroup, MirroredSurface, SeedpointVolume, + Sphere, Surface, ) from flow360.component.simulation.simulation_params import SimulationParams @@ -766,6 +767,51 @@ def snappy_coupled_refinements(): return param +@pytest.fixture() +def snappy_coupled_refinements_with_sphere(): + test_geometry = TempGeometry("tester.stl", True) + with SI_unit_system: + surf_meshing_params = snappy.SurfaceMeshingParams( + defaults=snappy.SurfaceMeshingDefaults( + min_spacing=3 * u.mm, max_spacing=4 * u.mm, gap_resolution=1 * u.mm + ), + octree_spacing=OctreeSpacing(base_spacing=5 * u.mm), + refinements=[], + smooth_controls=snappy.SmoothControls(), + ) + vol_meshing_params = VolumeMeshingParams( + defaults=VolumeMeshingDefaults( + boundary_layer_first_layer_thickness=1 * u.mm, boundary_layer_growth_rate=1.2 + ), + refinements=[ + UniformRefinement( + spacing=2 * u.mm, + entities=[ + Sphere(name="sphere0", center=[5, 10, 15] * u.mm, radius=25 * u.mm), + ], + project_to_surface=True, + ), + ], + ) + param = SimulationParams( + private_attribute_asset_cache=AssetCache( + project_entity_info=test_geometry._get_entity_info(), + project_length_unit=1 * u.mm, + use_inhouse_mesher=True, + ), + meshing=ModularMeshingWorkflow( + surface_meshing=surf_meshing_params, + volume_meshing=vol_meshing_params, + zones=[ + CustomZones( + entities=[SeedpointVolume(name="farfield", point_in_mesh=[0, 0, 0] * u.mm)] + ) + ], + ), + ) + return param + + @pytest.fixture() def snappy_refinements_multiple_regions(): test_geometry = TempGeometry("tester.stl", True) @@ -1076,6 +1122,15 @@ def test_snappy_coupled(get_snappy_geometry, snappy_coupled_refinements): ) +def test_snappy_coupled_with_sphere(get_snappy_geometry, snappy_coupled_refinements_with_sphere): + _translate_and_compare( + snappy_coupled_refinements_with_sphere, + get_snappy_geometry.mesh_unit, + "snappy_coupled_refinements_with_sphere.json", + atol=1e-6, + ) + + def test_snappy_multiple_regions(get_snappy_geometry, snappy_refinements_multiple_regions): _translate_and_compare( snappy_refinements_multiple_regions, From 5676cb5735a216f7baed4fe62e5acc1c91925413 Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Thu, 26 Feb 2026 16:54:54 +0000 Subject: [PATCH 6/9] linter --- .../component/simulation/meshing_param/meshing_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py index 0eb738cf8..300b83a6b 100644 --- a/flow360/component/simulation/meshing_param/meshing_validators.py +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -2,7 +2,7 @@ import flow360.component.simulation.units as u from flow360.component.simulation.meshing_param.volume_params import UniformRefinement -from flow360.component.simulation.primitives import Box, Cylinder, Sphere +from flow360.component.simulation.primitives import Box, Cylinder def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): From 54e44371693fda621dadeda528f7282d26914b59 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 12 Mar 2026 10:10:32 -0400 Subject: [PATCH 7/9] Sorted --- ...nappy_coupled_refinements_with_sphere.json | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json b/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json index fdb9a1361..be6ffddce 100644 --- a/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json +++ b/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements_with_sphere.json @@ -1,13 +1,11 @@ { + "cadIsFluid": true, + "enforcedSpacing": 5.0, "geometry": { "bodies": [ { "bodyName": "body0", "gap": 1.0, - "spacing": { - "min": 3.0, - "max": 4.0 - }, "regions": [ { "patchName": "patch0" @@ -15,15 +13,15 @@ { "patchName": "patch1" } - ] + ], + "spacing": { + "max": 4.0, + "min": 3.0 + } }, { "bodyName": "body1", "gap": 1.0, - "spacing": { - "min": 3.0, - "max": 4.0 - }, "regions": [ { "patchName": "patch0" @@ -34,98 +32,100 @@ { "patchName": "patch2" } - ] + ], + "spacing": { + "max": 4.0, + "min": 3.0 + } }, { "bodyName": "body2", "gap": 1.0, - "spacing": { - "min": 3.0, - "max": 4.0 - }, "regions": [ { "patchName": "patch0" } - ] + ], + "spacing": { + "max": 4.0, + "min": 3.0 + } }, { "bodyName": "body3", "gap": 1.0, - "spacing": { - "min": 3.0, - "max": 4.0 - }, "regions": [ { "patchName": "patch0" } - ] + ], + "spacing": { + "max": 4.0, + "min": 3.0 + } } ], "refinementVolumes": [ { - "spacing": 2.0, - "name": "sphere0", - "type": "sphere", "centre": { "x": 5.0, "y": 10.0, "z": 15.0 }, - "radius": 25.0 + "name": "sphere0", + "radius": 25.0, + "spacing": 2.0, + "type": "sphere" } ] }, + "locationInMesh": { + "farfield": [ + 0.0, + 0.0, + 0.0 + ] + }, "mesherSettings": { - "snappyHexMesh": { - "castellatedMeshControls": { - "resolveFeatureAngle": 25.0, - "nCellsBetweenLevels": 1, - "minRefinementCells": 10 - }, - "snapControls": { - "nSmoothPatch": 3, - "tolerance": 2.0, - "nSolveIter": 30, - "nRelaxIter": 5, - "nFeatureSnapIter": 15, - "multiRegionFeatureSnap": true, - "strictRegionSnap": false - } - }, "meshQuality": { - "maxNonOrtho": 85.0, + "errorReduction": 0.75, + "featureEdgeDeduplicationTolerance": 0.2, "maxBoundarySkewness": 20.0, - "maxInternalSkewness": 50.0, "maxConcave": 50.0, - "minVol": 1.9531250000000004e-19, - "minTetQuality": 1e-09, + "maxInternalSkewness": 50.0, + "maxNonOrtho": 85.0, "minArea": 1e-12, - "minTwist": -2, "minDeterminant": -100000.0, - "minVolRatio": 0, "minFaceWeight": 0, + "minTetQuality": 1e-09, "minTriangleTwist": -1, + "minTwist": -2, + "minVol": 1.9531250000000004e-19, + "minVolCollapseRatio": 0, + "minVolRatio": 0, "nSmoothScale": 4, - "errorReduction": 0.75, - "zMetricThreshold": 0.8, - "featureEdgeDeduplicationTolerance": 0.2, - "minVolCollapseRatio": 0 + "zMetricThreshold": 0.8 + }, + "snappyHexMesh": { + "castellatedMeshControls": { + "minRefinementCells": 10, + "nCellsBetweenLevels": 1, + "resolveFeatureAngle": 25.0 + }, + "snapControls": { + "multiRegionFeatureSnap": true, + "nFeatureSnapIter": 15, + "nRelaxIter": 5, + "nSmoothPatch": 3, + "nSolveIter": 30, + "strictRegionSnap": false, + "tolerance": 2.0 + } } }, "smoothingControls": { + "iter": 5, "lambda": 0.7, - "mu": 0.71, - "iter": 5 - }, - "enforcedSpacing": 5.0, - "cadIsFluid": true, - "locationInMesh": { - "farfield": [ - 0.0, - 0.0, - 0.0 - ] + "mu": 0.71 } } From 28eadd5bdac51747f181f78efae09774d8627a33 Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Mon, 16 Mar 2026 14:32:39 +0000 Subject: [PATCH 8/9] adress bot review --- .../meshing_param/meshing_validators.py | 6 +++- .../test_refinements_validation.py | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/flow360/component/simulation/meshing_param/meshing_validators.py b/flow360/component/simulation/meshing_param/meshing_validators.py index 300b83a6b..44c84352f 100644 --- a/flow360/component/simulation/meshing_param/meshing_validators.py +++ b/flow360/component/simulation/meshing_param/meshing_validators.py @@ -20,7 +20,11 @@ def validate_snappy_uniform_refinement_entities(refinement: UniformRefinement): "UniformRefinement for snappy accepts only Boxes with axes aligned" + " with the global coordinate system (angle_of_rotation=0)." ) - if isinstance(entity, Cylinder) and entity.inner_radius.to("m") != 0 * u.m: + if ( + isinstance(entity, Cylinder) + and entity.inner_radius is not None + and entity.inner_radius.to("m") != 0 * u.m + ): raise ValueError( "UniformRefinement for snappy accepts only full cylinders (where inner_radius = 0)." ) diff --git a/tests/simulation/params/meshing_validation/test_refinements_validation.py b/tests/simulation/params/meshing_validation/test_refinements_validation.py index b92dcb177..1711f53d5 100644 --- a/tests/simulation/params/meshing_validation/test_refinements_validation.py +++ b/tests/simulation/params/meshing_validation/test_refinements_validation.py @@ -323,6 +323,40 @@ def test_volume_uniform_refinement_hollow_cylinder_project_to_surface(): assert any("inner_radius" in msg or "full cylinders" in msg for msg in error_messages) +def test_volume_uniform_refinement_cylinder_none_inner_radius_project_to_surface(): + """ + A Cylinder with inner_radius=None is a full cylinder (equivalent to inner_radius=0) + and must NOT trigger a validation error. + """ + full_cylinder = Cylinder( + name="full_cyl_none", + inner_radius=None, + outer_radius=7 * u.mm, + axis=[0, 0, 1], + center=[0, 0, 0] * u.m, + height=10 * u.mm, + ) + + refinement = UniformRefinement( + spacing=5 * u.mm, + entities=[full_cylinder], + project_to_surface=True, + ) + + params = _make_snappy_params_with_volume_uniform_refinement(refinement) + + _, errors, _ = validate_model( + params_as_dict=params.model_dump(mode="json"), + validated_by=ValidationCalledBy.LOCAL, + root_item_type="Geometry", + validation_level="VolumeMesh", + ) + + assert errors is None, ( + "Cylinder with inner_radius=None is a full cylinder and should pass snappy validation" + ) + + def test_volume_uniform_refinement_default_project_to_surface(): """ When project_to_surface is None (the default, which acts as True for snappy), From c115a006d326a7c317ed85aaf41c75430103506e Mon Sep 17 00:00:00 2001 From: piotrkluba Date: Mon, 16 Mar 2026 15:00:40 +0000 Subject: [PATCH 9/9] black --- .../meshing_validation/test_refinements_validation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/simulation/params/meshing_validation/test_refinements_validation.py b/tests/simulation/params/meshing_validation/test_refinements_validation.py index 1711f53d5..16e745a27 100644 --- a/tests/simulation/params/meshing_validation/test_refinements_validation.py +++ b/tests/simulation/params/meshing_validation/test_refinements_validation.py @@ -352,9 +352,9 @@ def test_volume_uniform_refinement_cylinder_none_inner_radius_project_to_surface validation_level="VolumeMesh", ) - assert errors is None, ( - "Cylinder with inner_radius=None is a full cylinder and should pass snappy validation" - ) + assert ( + errors is None + ), "Cylinder with inner_radius=None is a full cylinder and should pass snappy validation" def test_volume_uniform_refinement_default_project_to_surface():