Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flow360/component/project_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions flow360/component/simulation/meshing_param/meshing_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ class MeshingDefaults(Flow360BaseModel):
edge_split_layers: int = pd.Field(
1,
ge=0,
# Skip default-value validation so warnings are emitted only when users explicitly set this field.
validate_default=False,
description="The number of layers that are considered for edge splitting in the boundary layer mesh."
+ "This only affects beta mesher.",
)
Expand Down
35 changes: 34 additions & 1 deletion flow360/component/simulation/services_utils.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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:
"""
Expand Down
4 changes: 4 additions & 0 deletions flow360/component/simulation/web/draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 22 additions & 0 deletions tests/simulation/params/test_validators_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2482,6 +2482,28 @@ def test_beta_mesher_only_features(mock_validation_context):
assert errors[0]["loc"] == ()


def test_edge_split_layers_default_no_warning_for_dict_input():
non_beta_context = ParamsValidationInfo({}, [])
non_beta_context.is_beta_mesher = False

with SI_unit_system, ValidationContext(VOLUME_MESH, non_beta_context) as validation_context:
defaults = MeshingDefaults.model_validate({"boundary_layer_first_layer_thickness": 1e-4})

assert "edge_split_layers" not in defaults.model_fields_set
assert validation_context.validation_warnings == []


def test_edge_split_layers_default_no_warning_for_constructor_input():
non_beta_context = ParamsValidationInfo({}, [])
non_beta_context.is_beta_mesher = False

with SI_unit_system, ValidationContext(VOLUME_MESH, non_beta_context) as validation_context:
defaults = MeshingDefaults(boundary_layer_first_layer_thickness=1e-4)

assert "edge_split_layers" not in defaults.model_fields_set
assert validation_context.validation_warnings == []


def test_geometry_AI_only_features():
with SI_unit_system:
params = SimulationParams(
Expand Down
132 changes: 132 additions & 0 deletions tests/simulation/services/test_services_utils.py
Original file line number Diff line number Diff line change
@@ -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
Loading