diff --git a/flow360/component/simulation/meshing_param/snappy/snappy_specs.py b/flow360/component/simulation/meshing_param/snappy/snappy_specs.py index fe6215d52..714a46942 100644 --- a/flow360/component/simulation/meshing_param/snappy/snappy_specs.py +++ b/flow360/component/simulation/meshing_param/snappy/snappy_specs.py @@ -54,11 +54,12 @@ class QualityMetrics(Flow360BaseModel): alias="max_concave", description="Maximum cell concavity. Set to False to disable this metric.", ) - min_pyramid_cell_volume: Union[float, Literal[False]] = pd.Field( - default=1e-15, + min_pyramid_cell_volume: Optional[Union[float, Literal[False]]] = pd.Field( + default=None, alias="min_vol", description="Minimum cell pyramid volume [mesh_unit³]. " - + "Set to False to disable this metric (uses -1e30 internally).", + + "Set to False to disable this metric (uses -1e30 internally). " + + "Defaults to (effective_min_spacing³) * 1e-10 when not specified.", ) min_tetrahedron_quality: Union[float, Literal[False]] = pd.Field( default=1e-9, diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 6ea7fd9bb..2788f0dfd 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -287,6 +287,55 @@ def apply_UniformRefinement_w_snappy( translated["geometry"]["refinementVolumes"].append(volume_body) +def _none_tolerant_min(current, candidate): + """Return the smaller of two spacing quantities, comparing by raw value.""" + if candidate is not None and candidate < current: + return candidate + return current + + +def _get_effective_min_spacing(input_params, spacing_system: OctreeSpacing): + """ + Get the effective minimum spacing across all refinements, + taking proximity_spacing (gap spacing reduction), edge spacings, and + projected volume refinements into account. + The result is cast to the nearest lower spacing in the octree series. + """ + surface_meshing_params = input_params.meshing.surface_meshing + min_spacing = surface_meshing_params.defaults.min_spacing + + for refinement in surface_meshing_params.refinements or []: + if isinstance(refinement, (snappy.BodyRefinement, snappy.RegionRefinement)): + min_spacing = _none_tolerant_min(min_spacing, refinement.min_spacing) + min_spacing = _none_tolerant_min(min_spacing, refinement.proximity_spacing) + elif ( + isinstance(refinement, snappy.SurfaceEdgeRefinement) and refinement.spacing is not None + ): + edge_spacing = ( + refinement.spacing[0] + if isinstance(refinement.spacing, unyt_array) + and isinstance(refinement.distances, unyt_array) + and len(refinement.spacing) > 0 + else refinement.spacing + ) + min_spacing = _none_tolerant_min(min_spacing, edge_spacing) + elif isinstance(refinement, UniformRefinement): + min_spacing = _none_tolerant_min(min_spacing, refinement.spacing) + + # Also consider projected volume meshing refinements + if input_params.meshing.volume_meshing is not None: + for refinement in input_params.meshing.volume_meshing.refinements: + if isinstance(refinement, UniformRefinement) and refinement.project_to_surface in [ + True, + None, + ]: + min_spacing = _none_tolerant_min(min_spacing, refinement.spacing) + + # Cast to the nearest lower spacing in the octree series + level = spacing_system.to_level(min_spacing)[0] + return spacing_system[level].value.item() + + # pylint: disable=too-many-branches,too-many-statements,too-many-locals def snappy_mesher_json(input_params: SimulationParams): """ @@ -412,9 +461,13 @@ def snappy_mesher_json(input_params: SimulationParams): else 180 ), "minVol": ( - quality_settings.min_pyramid_cell_volume - if quality_settings.min_pyramid_cell_volume - else -1e30 + -1e30 + if quality_settings.min_pyramid_cell_volume is False + else ( + quality_settings.min_pyramid_cell_volume + if quality_settings.min_pyramid_cell_volume is not None + else (1e-10 * (_get_effective_min_spacing(input_params, spacing_system) ** 3)) + ) ), "minTetQuality": ( quality_settings.min_tetrahedron_quality diff --git a/tests/simulation/translator/ref/surface_meshing/default_snappy.json b/tests/simulation/translator/ref/surface_meshing/default_snappy.json index 1f8c0b444..8ffe5d3a5 100644 --- a/tests/simulation/translator/ref/surface_meshing/default_snappy.json +++ b/tests/simulation/translator/ref/surface_meshing/default_snappy.json @@ -86,7 +86,7 @@ "maxBoundarySkewness": 20, "maxInternalSkewness": 50, "maxConcave": 50, - "minVol": 1e-15, + "minVol": 8e-10, "minTetQuality": 1e-9, "minArea": 1e-12, "minTwist": -2, diff --git a/tests/simulation/translator/ref/surface_meshing/snappy_basic_refinements.json b/tests/simulation/translator/ref/surface_meshing/snappy_basic_refinements.json index 580001ac2..f92e0d513 100644 --- a/tests/simulation/translator/ref/surface_meshing/snappy_basic_refinements.json +++ b/tests/simulation/translator/ref/surface_meshing/snappy_basic_refinements.json @@ -179,7 +179,7 @@ "maxBoundarySkewness": 20, "maxInternalSkewness": 50, "maxConcave": 50, - "minVol": 1e-15, + "minVol": 1.308441162109375e-13, "minTetQuality": 1e-9, "minArea": 1e-12, "minTwist": -2, diff --git a/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements.json b/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements.json index 49cf5f72f..34e45d619 100644 --- a/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements.json +++ b/tests/simulation/translator/ref/surface_meshing/snappy_coupled_refinements.json @@ -103,7 +103,7 @@ "maxBoundarySkewness": 20, "maxInternalSkewness": 50, "maxConcave": 50, - "minVol": 1e-15, + "minVol": 1.953125e-10, "minTetQuality": 1e-9, "minArea": 1e-12, "minTwist": -2, diff --git a/tests/simulation/translator/ref/surface_meshing/snappy_no_regions.json b/tests/simulation/translator/ref/surface_meshing/snappy_no_regions.json index da7c821ae..271937930 100644 --- a/tests/simulation/translator/ref/surface_meshing/snappy_no_regions.json +++ b/tests/simulation/translator/ref/surface_meshing/snappy_no_regions.json @@ -59,7 +59,7 @@ "maxBoundarySkewness": 20, "maxInternalSkewness": 50, "maxConcave": 50, - "minVol": 1e-15, + "minVol": 1.953125e-13, "minTetQuality": 1e-9, "minArea": 1e-12, "minTwist": -2, diff --git a/tests/simulation/translator/ref/surface_meshing/snappy_refinements_multiple_regions.json b/tests/simulation/translator/ref/surface_meshing/snappy_refinements_multiple_regions.json index d8ad58e6d..e329968ad 100644 --- a/tests/simulation/translator/ref/surface_meshing/snappy_refinements_multiple_regions.json +++ b/tests/simulation/translator/ref/surface_meshing/snappy_refinements_multiple_regions.json @@ -129,7 +129,7 @@ "maxBoundarySkewness": 20, "maxInternalSkewness": 50, "maxConcave": 50, - "minVol": 1e-15, + "minVol": 2.7e-09, "minTetQuality": 1e-9, "minArea": 1e-12, "minTwist": -2, diff --git a/tests/simulation/translator/test_surface_meshing_translator.py b/tests/simulation/translator/test_surface_meshing_translator.py index 9d8cf3be2..bb57f2a90 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -1046,7 +1046,7 @@ def test_rotor_surface_mesh(get_rotor_geometry, rotor_surface_mesh): def test_snappy_default(get_snappy_geometry, snappy_all_defaults): _translate_and_compare( - snappy_all_defaults, get_snappy_geometry.mesh_unit, "default_snappy.json" + snappy_all_defaults, get_snappy_geometry.mesh_unit, "default_snappy.json", atol=1e-6 ) @@ -1073,11 +1073,14 @@ def test_snappy_multiple_regions(get_snappy_geometry, snappy_refinements_multipl snappy_refinements_multiple_regions, get_snappy_geometry.mesh_unit, "snappy_refinements_multiple_regions.json", + atol=1e-6, ) def test_snappy_settings(get_snappy_geometry, snappy_settings): - _translate_and_compare(snappy_settings, get_snappy_geometry.mesh_unit, "snappy_settings.json") + _translate_and_compare( + snappy_settings, get_snappy_geometry.mesh_unit, "snappy_settings.json", atol=1e-6 + ) def test_snappy_settings_off_position(get_snappy_geometry, snappy_settings_off_position): @@ -1088,7 +1091,10 @@ def test_snappy_settings_off_position(get_snappy_geometry, snappy_settings_off_p def test_snappy_no_refinements(get_snappy_geometry, snappy_refinements_no_regions): _translate_and_compare( - snappy_refinements_no_regions, get_snappy_geometry.mesh_unit, "snappy_no_regions.json" + snappy_refinements_no_regions, + get_snappy_geometry.mesh_unit, + "snappy_no_regions.json", + atol=1e-6, )