From 145602368d397adb60f0bbb7d862b9ae91612ec7 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Wed, 11 Feb 2026 15:35:49 -0500 Subject: [PATCH] Fix edge_split_layers implicit-default warning noise --- flow360/component/project_utils.py | 2 + .../component/simulation/services_utils.py | 35 ++++- flow360/component/simulation/web/draft.py | 4 + .../services/test_services_utils.py | 132 ++++++++++++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 tests/simulation/services/test_services_utils.py diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 2c12a4a0d..1d7e2325c 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -32,6 +32,7 @@ Surface, ) from flow360.component.simulation.services_utils import ( + strip_implicit_edge_split_layers_inplace, strip_selector_matches_and_broken_entities_inplace, ) from flow360.component.simulation.simulation_params import SimulationParams @@ -725,6 +726,7 @@ def validate_params_with_context(params, root_item_type, up_to): ) params_as_dict = params.model_dump(mode="json", exclude_none=True) + params_as_dict = strip_implicit_edge_split_layers_inplace(params, params_as_dict) params, errors, warnings = services.validate_model( params_as_dict=params_as_dict, diff --git a/flow360/component/simulation/services_utils.py b/flow360/component/simulation/services_utils.py index bdf9c2012..f5816f975 100644 --- a/flow360/component/simulation/services_utils.py +++ b/flow360/component/simulation/services_utils.py @@ -1,6 +1,6 @@ """Utility functions for the simulation services.""" -from typing import Any +from typing import TYPE_CHECKING, Any from flow360.component.simulation.framework.entity_expansion_utils import ( get_registry_from_params, @@ -12,6 +12,39 @@ _MIRRORED_ENTITY_TYPE_NAMES = ("MirroredSurface", "MirroredGeometryBodyGroup") +if TYPE_CHECKING: + from flow360.component.simulation.simulation_params import SimulationParams + + +def strip_implicit_edge_split_layers_inplace(params: "SimulationParams", params_dict: dict) -> dict: + """ + Remove implicitly injected `edge_split_layers` from serialized params. + This extra and specific function was added due to a change in schema during lifecycle of a release (uncommon) + + Why not use `exclude_unset` or `exclude_defaults` globally during `model_dump()`? + - `exclude_unset` strips many unrelated defaulted fields and can affect downstream workflows. + - `exclude_defaults` also strips explicitly user-set values that equal the default. + """ + meshing = getattr(params, "meshing", None) + defaults = getattr(meshing, "defaults", None) + if defaults is None: + return params_dict + + if "edge_split_layers" in defaults.model_fields_set: + # Keep explicit user setting (including explicit value equal to default). + return params_dict + + meshing_dict = params_dict.get("meshing") + if not isinstance(meshing_dict, dict): + return params_dict + + defaults_dict = meshing_dict.get("defaults") + if not isinstance(defaults_dict, dict): + return params_dict + + defaults_dict.pop("edge_split_layers", None) + return params_dict + def strip_selector_matches_and_broken_entities_inplace(params) -> Any: """ diff --git a/flow360/component/simulation/web/draft.py b/flow360/component/simulation/web/draft.py index 5ef8355b2..8aeae1613 100644 --- a/flow360/component/simulation/web/draft.py +++ b/flow360/component/simulation/web/draft.py @@ -21,6 +21,9 @@ from flow360.component.simulation.framework.entity_selector import ( collect_and_tokenize_selectors_in_place, ) +from flow360.component.simulation.services_utils import ( + strip_implicit_edge_split_layers_inplace, +) from flow360.component.utils import formatting_validation_errors, validate_type from flow360.environment import Env from flow360.exceptions import Flow360RuntimeError, Flow360WebError @@ -132,6 +135,7 @@ def from_cloud(cls, draft_id: IDStringType) -> Draft: def update_simulation_params(self, params): """update the SimulationParams of the draft""" params_dict = params.model_dump(mode="json", exclude_none=True) + params_dict = strip_implicit_edge_split_layers_inplace(params, params_dict) params_dict = collect_and_tokenize_selectors_in_place(params_dict) self.post( diff --git a/tests/simulation/services/test_services_utils.py b/tests/simulation/services/test_services_utils.py new file mode 100644 index 000000000..495242047 --- /dev/null +++ b/tests/simulation/services/test_services_utils.py @@ -0,0 +1,132 @@ +import json +from types import SimpleNamespace + +import flow360 as fl +from flow360.component.project_utils import validate_params_with_context +from flow360.component.simulation.framework.param_utils import AssetCache +from flow360.component.simulation.meshing_param.meshing_specs import MeshingDefaults +from flow360.component.simulation.services_utils import ( + strip_implicit_edge_split_layers_inplace, +) +from flow360.component.simulation.web.draft import Draft + + +def _build_dummy_params(defaults: MeshingDefaults): + return SimpleNamespace(meshing=SimpleNamespace(defaults=defaults)) + + +def _build_simulation_params(*, edge_split_layers=None): + defaults_kwargs = dict( + boundary_layer_first_layer_thickness=1e-4, + surface_max_edge_length=1e-2, + ) + if edge_split_layers is not None: + defaults_kwargs["edge_split_layers"] = edge_split_layers + + with fl.SI_unit_system: + return fl.SimulationParams( + meshing=fl.MeshingParams( + defaults=fl.MeshingDefaults(**defaults_kwargs), + volume_zones=[fl.AutomatedFarfield()], + ), + private_attribute_asset_cache=AssetCache(use_inhouse_mesher=False), + ) + + +def test_strip_implicit_edge_split_layers_removes_default_injected_field(): + with fl.SI_unit_system: + defaults = MeshingDefaults(boundary_layer_first_layer_thickness=1e-4) + + params = _build_dummy_params(defaults) + params_dict = { + "meshing": {"defaults": {"edge_split_layers": 1, "surface_edge_growth_rate": 1.2}} + } + + out = strip_implicit_edge_split_layers_inplace(params, params_dict) + + assert out is params_dict + assert "edge_split_layers" not in out["meshing"]["defaults"] + assert out["meshing"]["defaults"]["surface_edge_growth_rate"] == 1.2 + + +def test_strip_implicit_edge_split_layers_keeps_explicit_default_value(): + with fl.SI_unit_system: + defaults = MeshingDefaults(boundary_layer_first_layer_thickness=1e-4, edge_split_layers=1) + + params = _build_dummy_params(defaults) + params_dict = { + "meshing": {"defaults": {"edge_split_layers": 1, "surface_edge_growth_rate": 1.2}} + } + + out = strip_implicit_edge_split_layers_inplace(params, params_dict) + + assert out["meshing"]["defaults"]["edge_split_layers"] == 1 + + +def test_strip_implicit_edge_split_layers_keeps_explicit_non_default_value(): + with fl.SI_unit_system: + defaults = MeshingDefaults(boundary_layer_first_layer_thickness=1e-4, edge_split_layers=3) + + params = _build_dummy_params(defaults) + params_dict = { + "meshing": {"defaults": {"edge_split_layers": 3, "surface_edge_growth_rate": 1.2}} + } + + out = strip_implicit_edge_split_layers_inplace(params, params_dict) + + assert out["meshing"]["defaults"]["edge_split_layers"] == 3 + + +def test_validate_params_with_context_no_warning_for_implicit_default(): + params = _build_simulation_params() + + _, errors, warnings = validate_params_with_context(params, "Geometry", "VolumeMesh") + + assert errors is None + assert warnings == [] + + +def test_validate_params_with_context_warning_for_explicit_default_value(): + params = _build_simulation_params(edge_split_layers=1) + + _, errors, warnings = validate_params_with_context(params, "Geometry", "VolumeMesh") + + assert errors is None + assert len(warnings) == 1 + assert warnings[0]["msg"] == ( + "`edge_split_layers` is only supported by the beta mesher; this setting will be ignored." + ) + + +def test_draft_upload_payload_omits_implicit_default_edge_split_layers(monkeypatch): + params = _build_simulation_params() + uploaded_payload = {} + + def _capture_post(self, *, json=None, method=None, **_kwargs): + uploaded_payload["json"] = json + uploaded_payload["method"] = method + return {} + + monkeypatch.setattr(Draft, "post", _capture_post, raising=True) + Draft(draft_id="00000000-0000-0000-0000-000000000000").update_simulation_params(params) + + assert uploaded_payload["method"] == "simulation/file" + uploaded_dict = json.loads(uploaded_payload["json"]["data"]) + assert "edge_split_layers" not in uploaded_dict["meshing"]["defaults"] + + +def test_draft_upload_payload_keeps_explicit_default_edge_split_layers(monkeypatch): + params = _build_simulation_params(edge_split_layers=1) + uploaded_payload = {} + + def _capture_post(self, *, json=None, method=None, **_kwargs): + uploaded_payload["json"] = json + uploaded_payload["method"] = method + return {} + + monkeypatch.setattr(Draft, "post", _capture_post, raising=True) + Draft(draft_id="00000000-0000-0000-0000-000000000000").update_simulation_params(params) + + assert uploaded_payload["method"] == "simulation/file" + uploaded_dict = json.loads(uploaded_payload["json"]["data"]) + assert uploaded_dict["meshing"]["defaults"]["edge_split_layers"] == 1