From 4c9ff069ca6a41b651c803b60b4b75928807c141 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Fri, 10 Oct 2025 11:19:44 -0400 Subject: [PATCH 1/5] [FXC-3622] Add for UserDefinedFarfield and AutomatedFarfield. --- .../simulation/meshing_param/volume_params.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index bf5ffda76..e78204838 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -340,7 +340,19 @@ class RotationCylinder(RotationVolume): entities: EntityList[Cylinder] = pd.Field() -class AutomatedFarfield(Flow360BaseModel): +class _FarfieldBase(Flow360BaseModel): + """Base class for farfield parameters.""" + + enforced_half_model: Optional[Literal["Y+", "Y-"]] = ( + pd.Field( # In the future, we will support more half models via Union. + None, + description="If set, trim to a half-model by slicing the geometry with the global Y=0 plane; " + "keep the 'Y+' or 'Y-' side for meshing and simulation.", + ) + ) + + +class AutomatedFarfield(_FarfieldBase): """ Settings for automatic farfield volume zone generation. @@ -393,7 +405,7 @@ def symmetry_planes(self): raise ValueError(f"Unsupported method: {self.method}") -class UserDefinedFarfield(Flow360BaseModel): +class UserDefinedFarfield(_FarfieldBase): """ Setting for user defined farfield zone generation. This means the "farfield" boundaries are coming from the supplied geometry file From 203f42567c2b058c0c77f1fc35d6dc911a2140e6 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Fri, 17 Oct 2025 12:32:49 -0400 Subject: [PATCH 2/5] Added interface --- .../simulation/meshing_param/volume_params.py | 18 +++++++++++++++--- .../translator/surface_meshing_translator.py | 12 ++++++++++++ .../translator/volume_meshing_translator.py | 5 +++++ .../test_meshing_param_validation.py | 10 ++++++++++ .../surface_meshing/gai_surface_mesher.json | 11 ++++++++++- .../test_surface_meshing_translator.py | 2 +- .../test_volume_meshing_translator.py | 4 ++-- 7 files changed, 55 insertions(+), 7 deletions(-) diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 0cfab02fe..2ea57aee0 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -343,14 +343,26 @@ class RotationCylinder(RotationVolume): class _FarfieldBase(Flow360BaseModel): """Base class for farfield parameters.""" - enforced_half_model: Optional[Literal["Y+", "Y-"]] = ( - pd.Field( # In the future, we will support more half models via Union. + enforced_half_model: Optional[Literal["+y", "-y"]] = ( + pd.Field( # In the future, we will support more half model types via Union. None, description="If set, trim to a half-model by slicing the geometry with the global Y=0 plane; " - "keep the 'Y+' or 'Y-' side for meshing and simulation.", + "keep the '+y' or '-y' side for meshing and simulation.", ) ) + @pd.field_validator("enforced_half_model", mode="after") + def _validate_only_in_beta_mesher(cls, value): + """ + Ensure that enforced_half_model objects are only processed with the beta mesher. + """ + validation_info = get_validation_info() + if validation_info is None: + return value + if not value: + return value + raise ValueError("`enforced_half_model` is only supported with the beta mesher.") + class AutomatedFarfield(_FarfieldBase): """ diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 7994e6dab..4fdd6f9f3 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -147,6 +147,17 @@ def _get_surface_refinements(refinement_list: list[dict]): ] +def _get_volume_zones(volume_zones_list: list[dict]): + """ + Get the volume zones from the input_params. + """ + return [ + item + for item in volume_zones_list + if item["type"] in ("AutomatedFarfield", "UserDefinedFarfield") + ] + + GAI_SETTING_WHITELIST = { "meshing": { "defaults": { @@ -159,6 +170,7 @@ def _get_surface_refinements(refinement_list: list[dict]): "surface_max_adaptation_iterations": None, }, "refinements": _get_surface_refinements, + "volume_zones": _get_volume_zones, }, "private_attribute_asset_cache": { "project_entity_info": { diff --git a/flow360/component/simulation/translator/volume_meshing_translator.py b/flow360/component/simulation/translator/volume_meshing_translator.py index 34931f1eb..3cb540cad 100644 --- a/flow360/component/simulation/translator/volume_meshing_translator.py +++ b/flow360/component/simulation/translator/volume_meshing_translator.py @@ -261,6 +261,8 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units): for zone in input_params.meshing.volume_zones: if isinstance(zone, UserDefinedFarfield): translated["farfield"] = {"type": "user-defined"} + if zone.enforced_half_model is not None: + translated["farfield"]["enforcedHalfModel"] = zone.enforced_half_model break if isinstance(zone, AutomatedFarfield): @@ -272,6 +274,9 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units): translated["farfield"]["periodic"] = {"type": "translational"} else: translated["farfield"]["type"] = zone.method + + if zone.enforced_half_model is not None: + translated["farfield"]["enforcedHalfModel"] = zone.enforced_half_model break if "farfield" not in translated: 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 7a06765ec..05c2ca045 100644 --- a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py +++ b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py @@ -454,3 +454,13 @@ def test_quasi_3d_periodic_only_in_legacy_mesher(): # does not raise with legacy mesher on with ValidationContext(VOLUME_MESH, non_beta_mesher_context): my_farfield = AutomatedFarfield(method="quasi-3d-periodic") + + +def test_enforced_half_model_only_in_beta_mesher(): + # raises when beta mesher is off + with pytest.raises( + pd.ValidationError, + match=r"`enforced_half_model` is only supported with the beta mesher.", + ): + with ValidationContext(VOLUME_MESH, non_beta_mesher_context): + AutomatedFarfield(enforced_half_model="+y") diff --git a/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json b/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json index 2808d091d..80cd2c286 100644 --- a/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json +++ b/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json @@ -184,6 +184,15 @@ "units": "flow360_length_unit" } } + ], + "volume_zones": [ + { + "enforced_half_model": "+y", + "type": "AutomatedFarfield", + "name": "Automated Farfield", + "method": "auto", + "relative_size": 50.0 + } ] }, "private_attribute_asset_cache": { @@ -631,4 +640,4 @@ ] } } -} +} \ 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 0f8220a7f..094a2a5f8 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -527,7 +527,7 @@ def test_gai_surface_mesher_refinements(): ) geometry["cube-holes.egads"].transformation = transformation geometry["cylinder.stl"].transformation = transformation - farfield = AutomatedFarfield() + farfield = AutomatedFarfield(enforced_half_model="+y") params = SimulationParams( meshing=MeshingParams( defaults=MeshingDefaults( diff --git a/tests/simulation/translator/test_volume_meshing_translator.py b/tests/simulation/translator/test_volume_meshing_translator.py index d6344d16f..c38b179c4 100644 --- a/tests/simulation/translator/test_volume_meshing_translator.py +++ b/tests/simulation/translator/test_volume_meshing_translator.py @@ -183,7 +183,7 @@ def get_test_param(): Surface(name="interface2"), ], ), - UserDefinedFarfield(), + UserDefinedFarfield(enforced_half_model="-y"), RotationVolume( name="we_do_not_use_this_anyway", entities=inner_cylinder, @@ -248,7 +248,7 @@ def test_param_to_json(get_test_param, get_surface_mesh): ref_dict = { "refinementFactor": 1.45, - "farfield": {"type": "user-defined"}, + "farfield": {"type": "user-defined", "enforcedHalfModel": "-y"}, "volume": { "firstLayerThickness": 1.35e-06, "growthRate": 1.04, From 5b33dc29ced7a143c5afa2d237aac97eb3d36327 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Fri, 17 Oct 2025 12:36:02 -0400 Subject: [PATCH 3/5] Fixed linting --- flow360/component/simulation/meshing_param/volume_params.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 2ea57aee0..ed7f5eb20 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -352,6 +352,7 @@ class _FarfieldBase(Flow360BaseModel): ) @pd.field_validator("enforced_half_model", mode="after") + @classmethod def _validate_only_in_beta_mesher(cls, value): """ Ensure that enforced_half_model objects are only processed with the beta mesher. From f4f825dd495a0962badc0660bf3df9ff6340e81c Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Mon, 20 Oct 2025 10:20:07 -0400 Subject: [PATCH 4/5] changed to domain type --- .../simulation/meshing_param/volume_params.py | 18 +++++++++++------- .../translator/volume_meshing_translator.py | 8 ++++---- .../test_meshing_param_validation.py | 4 ++-- .../surface_meshing/gai_surface_mesher.json | 2 +- .../test_surface_meshing_translator.py | 2 +- .../test_volume_meshing_translator.py | 4 ++-- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index ed7f5eb20..6afa122ca 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -343,26 +343,30 @@ class RotationCylinder(RotationVolume): class _FarfieldBase(Flow360BaseModel): """Base class for farfield parameters.""" - enforced_half_model: Optional[Literal["+y", "-y"]] = ( - pd.Field( # In the future, we will support more half model types via Union. + domain_type: Optional[Literal["half_body_positive_y", "half_body_negative_y"]] = ( + pd.Field( # In the future, we will support more flexible half model types and full model via Union. None, - description="If set, trim to a half-model by slicing the geometry with the global Y=0 plane; " - "keep the '+y' or '-y' side for meshing and simulation.", + description=""" + - half_body_positive_y: Trim to a half-model by slicing with the global Y=0 plane; keep the '+y' side for meshing and simulation. + - half_body_negative_y: Trim to a half-model by slicing with the global Y=0 plane; keep the '-y' side for meshing and simulation. + + Warning: When using AutomatedFarfield, setting `domain_type` overrides the 'auto' symmetry plane behavior. + """, ) ) - @pd.field_validator("enforced_half_model", mode="after") + @pd.field_validator("domain_type", mode="after") @classmethod def _validate_only_in_beta_mesher(cls, value): """ - Ensure that enforced_half_model objects are only processed with the beta mesher. + Ensure that domain_type is only used with the beta mesher. """ validation_info = get_validation_info() if validation_info is None: return value if not value: return value - raise ValueError("`enforced_half_model` is only supported with the beta mesher.") + raise ValueError("`domain_type` is only supported with the beta mesher.") class AutomatedFarfield(_FarfieldBase): diff --git a/flow360/component/simulation/translator/volume_meshing_translator.py b/flow360/component/simulation/translator/volume_meshing_translator.py index 3cb540cad..b14dfd0a7 100644 --- a/flow360/component/simulation/translator/volume_meshing_translator.py +++ b/flow360/component/simulation/translator/volume_meshing_translator.py @@ -261,8 +261,8 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units): for zone in input_params.meshing.volume_zones: if isinstance(zone, UserDefinedFarfield): translated["farfield"] = {"type": "user-defined"} - if zone.enforced_half_model is not None: - translated["farfield"]["enforcedHalfModel"] = zone.enforced_half_model + if zone.domain_type is not None: + translated["farfield"]["domainType"] = zone.domain_type break if isinstance(zone, AutomatedFarfield): @@ -275,8 +275,8 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units): else: translated["farfield"]["type"] = zone.method - if zone.enforced_half_model is not None: - translated["farfield"]["enforcedHalfModel"] = zone.enforced_half_model + if zone.domain_type is not None: + translated["farfield"]["domainType"] = zone.domain_type break if "farfield" not in translated: 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 05c2ca045..bb500465d 100644 --- a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py +++ b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py @@ -460,7 +460,7 @@ def test_enforced_half_model_only_in_beta_mesher(): # raises when beta mesher is off with pytest.raises( pd.ValidationError, - match=r"`enforced_half_model` is only supported with the beta mesher.", + match=r"`domain_type` is only supported with the beta mesher.", ): with ValidationContext(VOLUME_MESH, non_beta_mesher_context): - AutomatedFarfield(enforced_half_model="+y") + AutomatedFarfield(domain_type="half_body_positive_y") diff --git a/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json b/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json index 80cd2c286..07212d79d 100644 --- a/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json +++ b/tests/simulation/translator/ref/surface_meshing/gai_surface_mesher.json @@ -187,7 +187,7 @@ ], "volume_zones": [ { - "enforced_half_model": "+y", + "domain_type": "half_body_positive_y", "type": "AutomatedFarfield", "name": "Automated Farfield", "method": "auto", diff --git a/tests/simulation/translator/test_surface_meshing_translator.py b/tests/simulation/translator/test_surface_meshing_translator.py index 094a2a5f8..166e454ae 100644 --- a/tests/simulation/translator/test_surface_meshing_translator.py +++ b/tests/simulation/translator/test_surface_meshing_translator.py @@ -527,7 +527,7 @@ def test_gai_surface_mesher_refinements(): ) geometry["cube-holes.egads"].transformation = transformation geometry["cylinder.stl"].transformation = transformation - farfield = AutomatedFarfield(enforced_half_model="+y") + farfield = AutomatedFarfield(domain_type="half_body_positive_y") params = SimulationParams( meshing=MeshingParams( defaults=MeshingDefaults( diff --git a/tests/simulation/translator/test_volume_meshing_translator.py b/tests/simulation/translator/test_volume_meshing_translator.py index c38b179c4..90a9ff0e0 100644 --- a/tests/simulation/translator/test_volume_meshing_translator.py +++ b/tests/simulation/translator/test_volume_meshing_translator.py @@ -183,7 +183,7 @@ def get_test_param(): Surface(name="interface2"), ], ), - UserDefinedFarfield(enforced_half_model="-y"), + UserDefinedFarfield(domain_type="half_body_negative_y"), RotationVolume( name="we_do_not_use_this_anyway", entities=inner_cylinder, @@ -248,7 +248,7 @@ def test_param_to_json(get_test_param, get_surface_mesh): ref_dict = { "refinementFactor": 1.45, - "farfield": {"type": "user-defined", "enforcedHalfModel": "-y"}, + "farfield": {"type": "user-defined", "domainType": "half_body_negative_y"}, "volume": { "firstLayerThickness": 1.35e-06, "growthRate": 1.04, From d277b165b0130df08f48de2b89579cc7329c2478 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Mon, 20 Oct 2025 10:35:33 -0400 Subject: [PATCH 5/5] improved validation and added full_model --- .../simulation/meshing_param/volume_params.py | 13 +++++++++---- .../test_meshing_param_validation.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 6afa122ca..1e9dc34a8 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -343,12 +343,13 @@ class RotationCylinder(RotationVolume): class _FarfieldBase(Flow360BaseModel): """Base class for farfield parameters.""" - domain_type: Optional[Literal["half_body_positive_y", "half_body_negative_y"]] = ( + domain_type: Optional[Literal["half_body_positive_y", "half_body_negative_y", "full_body"]] = ( pd.Field( # In the future, we will support more flexible half model types and full model via Union. None, description=""" - half_body_positive_y: Trim to a half-model by slicing with the global Y=0 plane; keep the '+y' side for meshing and simulation. - half_body_negative_y: Trim to a half-model by slicing with the global Y=0 plane; keep the '-y' side for meshing and simulation. + - full_body: Keep the full body for meshing and simulation without attempting to add symmetry planes. Warning: When using AutomatedFarfield, setting `domain_type` overrides the 'auto' symmetry plane behavior. """, @@ -359,14 +360,18 @@ class _FarfieldBase(Flow360BaseModel): @classmethod def _validate_only_in_beta_mesher(cls, value): """ - Ensure that domain_type is only used with the beta mesher. + Ensure that domain_type is only used with the beta mesher and GAI. """ validation_info = get_validation_info() if validation_info is None: return value - if not value: + if not value or ( + validation_info.use_geometry_AI is True and validation_info.is_beta_mesher is True + ): return value - raise ValueError("`domain_type` is only supported with the beta mesher.") + raise ValueError( + "`domain_type` is only supported when using both GAI surface mesher and beta volume mesher." + ) class AutomatedFarfield(_FarfieldBase): 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 bb500465d..64a4719b0 100644 --- a/tests/simulation/params/meshing_validation/test_meshing_param_validation.py +++ b/tests/simulation/params/meshing_validation/test_meshing_param_validation.py @@ -26,6 +26,9 @@ non_beta_mesher_context = ParamsValidationInfo({}, []) non_beta_mesher_context.is_beta_mesher = False +non_gai_context = ParamsValidationInfo({}, []) +non_gai_context.use_geometry_AI = False + beta_mesher_context = ParamsValidationInfo({}, []) beta_mesher_context.is_beta_mesher = True @@ -460,7 +463,15 @@ def test_enforced_half_model_only_in_beta_mesher(): # raises when beta mesher is off with pytest.raises( pd.ValidationError, - match=r"`domain_type` is only supported with the beta mesher.", + match=r"`domain_type` is only supported when using both GAI surface mesher and beta volume mesher.", ): with ValidationContext(VOLUME_MESH, non_beta_mesher_context): AutomatedFarfield(domain_type="half_body_positive_y") + + # raise when GAI is off + with pytest.raises( + pd.ValidationError, + match=r"`domain_type` is only supported when using both GAI surface mesher and beta volume mesher.", + ): + with ValidationContext(VOLUME_MESH, non_gai_context): + AutomatedFarfield(domain_type="full_body")