From 3bc306308f3e8c29f5ae9c43f1df02e937874d4f Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Tue, 8 Jul 2025 13:32:16 +0200 Subject: [PATCH 1/4] Add wrapping settings for surface mesh generation (snappy integration) - Introduced new classes: MeshQuality, SnapControls, GeometrySettings, and WrappingSettings to manage mesh generation parameters. - Updated MeshingParams to include wrapping_settings. - Enhanced surface meshing translator to handle wrapping settings in JSON output. - Added unit tests for wrapping settings functionality and validation against expected JSON output. --- .../simulation/meshing_param/params.py | 151 ++++++++++ .../translator/surface_meshing_translator.py | 23 ++ .../surface_mesh_snappy_config.json | 275 ++++++++++++++++++ .../test_surface_meshing_translator.py | 105 ++++++- 4 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 tests/simulation/translator/ref/surface_meshing/surface_mesh_snappy_config.json diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index c2dc0fe96..c7443af73 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -30,6 +30,9 @@ ) from flow360.component.simulation.validation.validation_utils import EntityUsageMap +from flow360.component.simulation.entity_info import Surface + + RefinementTypes = Annotated[ Union[ SurfaceEdgeRefinement, @@ -178,6 +181,150 @@ def invalid_geometry_accuracy(cls, value): return value +class MeshQuality(Flow360BaseModel): + """ + Mesh quality settings for the surface mesh generation. + """ + + max_non_ortho: float = pd.Field( + 85, + description="Maximum non-orthogonality allowed in the mesh.", + ) + + max_boundary_skewness: float = pd.Field( + 20, + description="Maximum boundary skewness allowed in the mesh.", + ) + + max_internal_skewness: float = pd.Field( + 50, + description="Maximum internal skewness allowed in the mesh.", + ) + + max_concave: float = pd.Field( + 50, + description="Maximum concavity allowed in the mesh.", + ) + + min_vol: float = pd.Field( + -1e+30, + description="Minimum volume allowed in the mesh.", + ) + + min_tet_quality: float = pd.Field( + -1e+30, + description="Minimum tetrahedral quality allowed in the mesh.", + ) + + min_area: float = pd.Field( + -1, + description="Minimum area allowed in the mesh.", + ) + + min_twist: float = pd.Field( + -2, + description="Minimum twist allowed in the mesh.", + ) + + min_determinant: float = pd.Field( + 0, + description="Minimum determinant allowed in the mesh.", + ) + + min_vol_ratio: float = pd.Field( + 0, + description="Minimum volume ratio allowed in the mesh.", + ) + + min_face_weight: float = pd.Field( + 0, + description="Minimum face weight allowed in the mesh.", + ) + + min_triangle_twist: float = pd.Field( + -1, + description="Minimum triangle twist allowed in the mesh.", + ) + + n_smooth_scale: int = pd.Field( + 4, + description="Number of smoothing scale iterations.", + ) + + error_reduction: float = pd.Field( + 0.75, + description="Error reduction factor for mesh smoothing.", + ) + + min_vol_collapse_ratio: float = pd.Field( + 0, + description="Minimum volume collapse ratio allowed in the mesh.", + ) + +class SnapControls(Flow360BaseModel): + """ + Snap controls for the surface mesh generation. + """ + + tolerance: Optional[pd.PositiveFloat] = pd.Field( + 2, + description="Tolerance for the surface mesh generation.", + ) + + n_feature_snap_iter: int = pd.Field( + 15, + description="Number of feature snap iterations.", + ) + multi_region_feature_snap: bool = pd.Field( + True, + description="Whether to use multi-region feature snap.", + ) + +class GeometrySettings(Flow360BaseModel): + """ + Geometry settings for the surface mesh generation. + """ + + entities: List[Surface] = pd.Field( + default=[], + description="List of entities to be wrapped.", + ) + + spec: dict = pd.Field( + default={}, + description="Specification for the geometry settings. Example: " + '{"spacing": {"min": 5, "max": 50}, ' + '"edges": {"edgeSpacing": 2, "includedAngle": 140, "minElem": 3, "minLen": 10}, ' + '"gap": 0.001, "regions": [], "gapSpacingReduction": null, "includedAngle": 140}', + ) + + + +class WrappingSettings(Flow360BaseModel): + geometry: Optional[List[GeometrySettings]] = pd.Field( + default=None, + description="List of settings geometry entities to be wrapped.", + ) + + mesh_quality: MeshQuality = pd.Field( + MeshQuality(), + description="Mesh quality settings for the surface mesh generation.", + ) + + snap_controls: SnapControls = pd.Field( + SnapControls(), + description="Snap controls for the surface mesh generation.", + ) + location_in_mesh: Optional[LengthType.Point] = pd.Field( + None, + description="Point in the mesh that will be used to determine side for wrapping.", + ) + cad_is_fluid: bool = pd.Field( + False, + description="Whether the CAD represents a fluid or solid.", + ) + + class MeshingParams(Flow360BaseModel): """ Meshing parameters for volume and/or surface mesher. This contains all the meshing related settings. @@ -243,6 +390,10 @@ class MeshingParams(Flow360BaseModel): default=None, description="Creation of new volume zones." ) + wrapping_settings: Optional[WrappingSettings] = pd.Field( + default=None, description="Wrapping settings for the surface mesh generation." + ) + @pd.field_validator("volume_zones", mode="after") @classmethod def _check_volume_zones_has_farfied(cls, v): diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 626ab8d55..84d42272d 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -134,4 +134,27 @@ def get_surface_meshing_json(input_params: SimulationParams, mesh_units): for face_id in surface.private_attribute_sub_components: translated["boundaries"][face_id] = {"boundaryName": surface.name} + + + ##:: >> Step 7: Get wrapping settings (for snappy) + if input_params.meshing.wrapping_settings is not None: + translated["meshingMethod"] = "wrapping" + translated["geometry"] = { + "bodies": [] + } + for geometry_settings in input_params.meshing.wrapping_settings.geometry: + for entity in geometry_settings.entities: + translated["geometry"]["bodies"].append({ + "bodyName": entity.name, + **geometry_settings.spec + }) + translated["mesherSettings"] = { + "snappyHexMesh": { + "snapControls": input_params.meshing.wrapping_settings.snap_controls.model_dump(by_alias=True), + }, + "meshQuality": input_params.meshing.wrapping_settings.mesh_quality.model_dump(by_alias=True), + } + translated["locationInMesh"] = input_params.meshing.wrapping_settings.location_in_mesh.value.tolist() + translated["cadIsFluid"] = input_params.meshing.wrapping_settings.cad_is_fluid + return translated diff --git a/tests/simulation/translator/ref/surface_meshing/surface_mesh_snappy_config.json b/tests/simulation/translator/ref/surface_meshing/surface_mesh_snappy_config.json new file mode 100644 index 000000000..ee8e7eb1b --- /dev/null +++ b/tests/simulation/translator/ref/surface_meshing/surface_mesh_snappy_config.json @@ -0,0 +1,275 @@ +{ + "meshingMethod": "wrapping", + "curvatureResolutionAngle": 12.00000000000000, + "growthRate": 1.2, + "faces": { + "body01_face001": { + "maxEdgeLength": 1.0 + }, + "body01_face002": { + "maxEdgeLength": 1.0 + }, + "body01_face003": { + "maxEdgeLength": 1.0 + }, + "body01_face004": { + "maxEdgeLength": 1.0 + }, + "body01_face005": { + "maxEdgeLength": 1.0 + }, + "body01_face006": { + "maxEdgeLength": 1.0 + }, + "body01_face007": { + "maxEdgeLength": 1.0 + }, + "body01_face008": { + "maxEdgeLength": 1.0 + }, + "body01_face009": { + "maxEdgeLength": 1.0 + }, + "body01_face010": { + "maxEdgeLength": 1.0 + } + }, + "boundaries": { + "body01_face001": { + "boundaryName": "Wing" + }, + "body01_face002": { + "boundaryName": "Wing" + }, + "body01_face003": { + "boundaryName": "Wing" + }, + "body01_face004": { + "boundaryName": "Wing" + }, + "body01_face005": { + "boundaryName": "Fuselage" + }, + "body01_face006": { + "boundaryName": "Fuselage" + }, + "body01_face007": { + "boundaryName": "Fuselage" + }, + "body01_face008": { + "boundaryName": "Stab" + }, + "body01_face009": { + "boundaryName": "Stab" + }, + "body01_face010": { + "boundaryName": "Fin" + } + }, + "geometry": { + "bodies": [ + { + "bodyName": "body01_face001", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face002", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face003", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face004", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face005", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face006", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face007", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face008", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face009", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + }, + { + "bodyName": "body01_face010", + "spacing": { + "min": 5, + "max": 50 + }, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": null, + "includedAngle": 140 + } + ] + }, + "locationInMesh": [ + 3000, + 0, + 800 + ], + "cadIsFluid": true, + "mesherSettings": { + "snappyHexMesh": { + "snapControls": { + "tolerance": 2, + "nFeatureSnapIter": 15, + "multiRegionFeatureSnap": true + } + }, + "meshQuality": { + "maxNonOrtho": 85, + "maxBoundarySkewness": 20, + "maxInternalSkewness": 50, + "maxConcave": 50, + "minVol": -1e+30, + "minTetQuality": -1e+30, + "minArea": -1, + "minTwist": -2, + "minDeterminant": 0, + "minVolRatio": 0, + "minFaceWeight": 0, + "minTriangleTwist": -1, + "nSmoothScale": 4, + "errorReduction": 0.75, + "minVolCollapseRatio": 0 + } + } +} \ No newline at end of file diff --git a/tests/simulation/translator/test_surface_meshing_translator.py b/tests/simulation/translator/test_surface_meshing_translator.py index 799311428..e4357fc13 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -18,6 +18,10 @@ from flow360.component.simulation.meshing_param.params import ( MeshingDefaults, MeshingParams, + WrappingSettings, + GeometrySettings, + MeshQuality, + SnapControls, ) from flow360.component.simulation.primitives import Edge, Surface from flow360.component.simulation.simulation_params import SimulationParams @@ -425,6 +429,80 @@ def rotor_surface_mesh(): return param +@pytest.fixture() +def airplane_surface_mesh_with_wrapping(): + my_geometry = TempGeometry("geometry.egads") + from numpy import pi + + with SI_unit_system: + param = SimulationParams( + private_attribute_asset_cache=AssetCache( + project_entity_info=my_geometry._get_entity_info() + ), + meshing=MeshingParams( + defaults=MeshingDefaults( + surface_max_edge_length=100 * u.cm, + ), + wrapping_settings=WrappingSettings( + geometry=[ + GeometrySettings( + entities=[ + my_geometry["body01_face001"], + my_geometry["body01_face002"], + my_geometry["body01_face003"], + my_geometry["body01_face004"], + my_geometry["body01_face005"], + my_geometry["body01_face006"], + my_geometry["body01_face007"], + my_geometry["body01_face008"], + my_geometry["body01_face009"], + my_geometry["body01_face010"], + ], + spec={ + "spacing": {"min": 5, "max": 50}, + "edges": { + "edgeSpacing": 2, + "includedAngle": 140, + "minElem": 3, + "minLen": 10 + }, + "gap": 0.001, + "regions": [], + "gapSpacingReduction": None, + "includedAngle": 140 + } + ) + ], + mesh_quality=MeshQuality( + max_non_ortho=85, + max_boundary_skewness=20, + max_internal_skewness=50, + max_concave=50, + min_vol=-1e+30, + min_tet_quality=-1e+30, + min_area=-1, + min_twist=-2, + min_determinant=0, + min_vol_ratio=0, + min_face_weight=0, + min_triangle_twist=-1, + n_smooth_scale=4, + error_reduction=0.75, + min_vol_collapse_ratio=0 + ), + snap_controls=SnapControls( + tolerance=2, + n_feature_snap_iter=15, + multi_region_feature_snap=True + ), + location_in_mesh=(3000, 0, 800) * u.m, + cad_is_fluid=True + ) + ), + ) + return param + + def _translate_and_compare(param, mesh_unit, ref_json_file: str): translated = get_surface_meshing_json(param, mesh_unit=mesh_unit) with open( @@ -433,7 +511,24 @@ def _translate_and_compare(param, mesh_unit, ref_json_file: str): ) ) as fh: ref_dict = json.load(fh) - assert compare_values(ref_dict, translated) + + print("\n" + "="*80) + print("REFERENCE JSON:") + print("="*80) + print(json.dumps(ref_dict, indent=2)) + + print("\n" + "="*80) + print("GENERATED JSON:") + print("="*80) + print(json.dumps(translated, indent=2)) + + print("\n" + "="*80) + print("COMPARISON RESULT:") + print("="*80) + result = compare_values(ref_dict, translated) + print(f"Comparison result: {result}") + + assert result def test_om6wing_tutorial( @@ -475,3 +570,11 @@ def test_rotor_surface_mesh(get_rotor_geometry, rotor_surface_mesh): get_rotor_geometry.mesh_unit, "rotor.json", ) + + +def test_wrapping_settings(get_airplane_geometry, airplane_surface_mesh_with_wrapping): + _translate_and_compare( + airplane_surface_mesh_with_wrapping, + get_airplane_geometry.mesh_unit, + "surface_mesh_snappy_config.json", + ) From 7394bf98035ec104ff513bb12d76eba994ef452f Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Tue, 8 Jul 2025 13:42:43 +0200 Subject: [PATCH 2/4] Refactor geometry entity handling in surface meshing - Updated GeometrySettings to use EntityList for storing entities. - Modified surface meshing translator to access stored entities correctly. - Simplified test cases by allowing wildcard entity selection in geometry settings. --- flow360/component/simulation/meshing_param/params.py | 4 ++-- .../translator/surface_meshing_translator.py | 2 +- .../translator/test_surface_meshing_translator.py | 11 +---------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index c7443af73..f9f542070 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -31,7 +31,7 @@ from flow360.component.simulation.validation.validation_utils import EntityUsageMap from flow360.component.simulation.entity_info import Surface - +from flow360.component.simulation.framework.entity_base import EntityList RefinementTypes = Annotated[ Union[ @@ -285,7 +285,7 @@ class GeometrySettings(Flow360BaseModel): Geometry settings for the surface mesh generation. """ - entities: List[Surface] = pd.Field( + entities: EntityList[Surface] = pd.Field( default=[], description="List of entities to be wrapped.", ) diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 84d42272d..1c20dfcd2 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -143,7 +143,7 @@ def get_surface_meshing_json(input_params: SimulationParams, mesh_units): "bodies": [] } for geometry_settings in input_params.meshing.wrapping_settings.geometry: - for entity in geometry_settings.entities: + for entity in geometry_settings.entities.stored_entities: translated["geometry"]["bodies"].append({ "bodyName": entity.name, **geometry_settings.spec diff --git a/tests/simulation/translator/test_surface_meshing_translator.py b/tests/simulation/translator/test_surface_meshing_translator.py index e4357fc13..27f0b9c40 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -447,16 +447,7 @@ def airplane_surface_mesh_with_wrapping(): geometry=[ GeometrySettings( entities=[ - my_geometry["body01_face001"], - my_geometry["body01_face002"], - my_geometry["body01_face003"], - my_geometry["body01_face004"], - my_geometry["body01_face005"], - my_geometry["body01_face006"], - my_geometry["body01_face007"], - my_geometry["body01_face008"], - my_geometry["body01_face009"], - my_geometry["body01_face010"], + my_geometry["*"], ], spec={ "spacing": {"min": 5, "max": 50}, From 89f94c104942732128a42dc8ba9dc1b6f7139083 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 10 Jul 2025 08:08:10 +0000 Subject: [PATCH 3/4] updated translator --- .../simulation/translator/surface_meshing_translator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 1c20dfcd2..bb6f9e9fd 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -154,7 +154,9 @@ def get_surface_meshing_json(input_params: SimulationParams, mesh_units): }, "meshQuality": input_params.meshing.wrapping_settings.mesh_quality.model_dump(by_alias=True), } - translated["locationInMesh"] = input_params.meshing.wrapping_settings.location_in_mesh.value.tolist() + location_in_mesh = input_params.meshing.wrapping_settings.location_in_mesh + if location_in_mesh is not None: + translated["locationInMesh"] = location_in_mesh.value.tolist() translated["cadIsFluid"] = input_params.meshing.wrapping_settings.cad_is_fluid return translated From 376f7f9cb5a0d9f271adfad4e1d124636dcda113 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 10 Jul 2025 19:19:23 +0200 Subject: [PATCH 4/4] Refactor edge grouping logic in GeometryEntityInfo and update cad_is_fluid type in WrappingSettings - Modified edge grouping logic to only apply when edge_ids are present, - Changed cad_is_fluid from bool to Optional in WrappingSettings --- flow360/component/simulation/entity_info.py | 17 +++++++++-------- .../simulation/meshing_param/params.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index ba689be26..0373a7d9f 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -367,15 +367,16 @@ def get_registry(self, internal_registry, **_) -> EntityRegistry: "face", face_group_tag, registry=internal_registry ) - if self.edge_group_tag is None: - edge_group_tag = self._get_default_grouping_tag("edge") - log.info(f"Using `{edge_group_tag}` as default grouping for edges.") - else: - edge_group_tag = self.edge_group_tag + if len(self.edge_ids) > 0: + if self.edge_group_tag is None: + edge_group_tag = self._get_default_grouping_tag("edge") + log.info(f"Using `{edge_group_tag}` as default grouping for edges.") + else: + edge_group_tag = self.edge_group_tag - internal_registry = self._group_entity_by_tag( - "edge", edge_group_tag, registry=internal_registry - ) + internal_registry = self._group_entity_by_tag( + "edge", edge_group_tag, registry=internal_registry + ) if self.body_attribute_names: # Post-25.5 geometry asset. For Pre 25.5 we just skip body grouping. diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index f9f542070..c7a8ee899 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -319,7 +319,7 @@ class WrappingSettings(Flow360BaseModel): None, description="Point in the mesh that will be used to determine side for wrapping.", ) - cad_is_fluid: bool = pd.Field( + cad_is_fluid: Optional[bool] = pd.Field( False, description="Whether the CAD represents a fluid or solid.", )