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
12 changes: 10 additions & 2 deletions flow360/component/simulation/meshing_param/meshing_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ class MeshingDefaults(Flow360BaseModel):
None,
description="Target number of surface mesh nodes. When specified, the surface mesher "
"will rescale the meshing parameters to achieve approximately this number of nodes. "
"This option is only supported when using geometry AI and can not be overridden per face.",
"This option is only supported by the beta surface mesher or when using geometry AI, "
"and can not be overridden per face.",
context=SURFACE_MESH,
)

Expand Down Expand Up @@ -337,7 +338,6 @@ def invalid_geometry_accuracy(cls, value, param_info: ParamsValidationInfo):
@contextual_field_validator(
"surface_max_aspect_ratio",
"surface_max_adaptation_iterations",
"target_surface_node_count",
"resolve_face_boundaries",
"preserve_thin_geometry",
"sealing_size",
Expand All @@ -350,6 +350,14 @@ def ensure_geometry_ai_features(cls, value, info, param_info: ParamsValidationIn
"""Validate that the feature is only used when Geometry AI is enabled."""
return check_geometry_ai_features(cls, value, info, param_info)

@contextual_field_validator("target_surface_node_count", mode="after")
@classmethod
def ensure_target_surface_node_count_mesher(cls, value, param_info: ParamsValidationInfo):
"""Validate that target_surface_node_count is only used with geometry AI or beta mesher."""
if value is not None and not (param_info.use_geometry_AI or param_info.is_beta_mesher):
raise ValueError("target_surface_node_count is not supported by the legacy mesher.")
return value

@contextual_field_validator("octree_spacing", mode="after")
@classmethod
def _set_default_octree_spacing(cls, octree_spacing, param_info: ParamsValidationInfo):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint:disable = too-many-lines
"""Surface meshing parameter translator."""

from copy import deepcopy
Expand All @@ -6,6 +7,7 @@
from unyt import unyt_array

from flow360.component.simulation.entity_info import GeometryEntityInfo
from flow360.component.simulation.framework.updater_utils import recursive_remove_key
from flow360.component.simulation.meshing_param import snappy
from flow360.component.simulation.meshing_param.edge_params import SurfaceEdgeRefinement
from flow360.component.simulation.meshing_param.face_params import SurfaceRefinement
Expand Down Expand Up @@ -636,6 +638,12 @@ def legacy_mesher_json(input_params: SimulationParams):

translated["faces"] = face_config

##:: >> Step 5.2: Get target_surface_node_count [OPTIONAL]
if input_params.meshing.defaults.target_surface_node_count is not None:
translated["target_surface_node_count"] = (
input_params.meshing.defaults.target_surface_node_count
)

##:: >> Step 6: Tell surface mesher how do we group boundaries.
translated["boundaries"] = {}
grouped_faces: List[Surface] = (
Expand Down Expand Up @@ -965,6 +973,12 @@ def filter_simulation_json(input_params: SimulationParams, mesh_units):
# Filter the JSON to only include the GAI surface meshing parameters
filtered_json = _traverse_and_filter(json_data, whitelist)

# Remove selectors from the filtered JSON. After selector expansion (done by
# @preprocess_input), selectors are redundant — only stored_entities matter
# for meshing. Selectors contain random UUIDs (selector_id) that would cause
# hash inconsistency between identical runs.
recursive_remove_key(filtered_json, "selectors")

return filtered_json


Expand Down
12 changes: 12 additions & 0 deletions flow360/examples/DTU_WindTurbine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
DTU Wind Turbine example
"""

from .base_test_case import BaseTestCase


class DTU_WindTurbine(BaseTestCase):
name = "dtu_wind_turbine"

class url:
geometry = "https://simcloud-public-1.s3.amazonaws.com/dtu10MWrwt/geometry.egads"
2 changes: 2 additions & 0 deletions flow360/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .cylinder3D import Cylinder3D
from .DARPA_SUBOFF import DARPA_SUBOFF
from .drivaer import DrivAer
from .DTU_WindTurbine import DTU_WindTurbine
from .evtol import EVTOL
from .f1_2025 import F1_2025
from .isolated_propeller import IsolatedPropeller
Expand Down Expand Up @@ -43,6 +44,7 @@
"Cylinder2D",
"Cylinder3D",
"DARPA_SUBOFF",
"DTU_WindTurbine",
"DrivAer",
"EVTOL",
"F1_2025",
Expand Down
105 changes: 104 additions & 1 deletion tests/simulation/translator/test_surface_meshing_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,83 @@ def test_gai_translator_hashing_ignores_id():
), f"Hashes should be identical despite different UUIDs:\n Hash 1: {hashes[0]}\n Hash 2: {hashes[1]}"


def test_gai_translator_selectors_do_not_affect_hash():
"""Test that selectors (with random selector_id) are removed from GAI translation output,
ensuring identical inputs produce identical hashes across runs."""

from flow360.component.simulation.framework.entity_selector import SurfaceSelector

hashes = []

for _ in range(2):
geometry = Geometry.from_local_storage(
geometry_id="geo-e5c01a98-2180-449e-b255-d60162854a83",
local_storage_path=os.path.join(
os.path.dirname(__file__), "data", "gai_geometry_entity_info"
),
meta_data=GeometryMeta(
**local_metadata_builder(
id="geo-e5c01a98-2180-449e-b255-d60162854a83",
name="aaa",
cloud_path_prefix="aaa",
status="processed",
)
),
)
geometry.group_faces_by_tag("faceId")
geometry.group_edges_by_tag("edgeId")
geometry.group_bodies_by_tag("groupByFile")

with create_draft(new_run_from=geometry) as draft:
with SI_unit_system:
selector = SurfaceSelector(name="all_faces").match("*")

params = SimulationParams(
meshing=MeshingParams(
defaults=MeshingDefaults(
geometry_accuracy=0.05 * u.m,
surface_max_edge_length=0.2,
),
refinements=[
SurfaceRefinement(
name="selector_based_refinement",
max_edge_length=0.1,
faces=[selector],
),
],
),
operating_condition=AerospaceCondition(
velocity_magnitude=10 * u.m / u.s,
),
)

params = set_up_params_for_uploading(
root_asset=geometry,
length_unit=1 * u.m,
params=params,
use_beta_mesher=True,
use_geometry_AI=True,
)

params, err, _ = validate_params_with_context(params, "Geometry", "SurfaceMesh")
assert err is None, f"Validation error: {err}"

translated = get_surface_meshing_json(params, mesh_unit=1 * u.m)

# Verify no selectors in translated output
translated_str = json.dumps(translated)
assert (
"selectors" not in translated_str
), "Translated GAI output should not contain selectors after expansion"

hash_value = SimulationParams._calculate_hash(translated)
hashes.append(hash_value)

assert (
hashes[0] == hashes[1]
), f"Hashes should be identical across runs:\n Hash 1: {hashes[0]}\n Hash 2: {hashes[1]}"


def test_gai_analytic_wind_tunnel_farfield():
with SI_unit_system:
wind_tunnel = WindTunnelFarfield(
Expand Down Expand Up @@ -1853,8 +1930,34 @@ def test_gai_target_surface_node_count_absent():
assert "target_surface_node_count" not in translated["meshing"]["defaults"]


def test_beta_mesher_target_surface_node_count_set(get_om6wing_geometry):
"""target_surface_node_count is accepted and translated when using the beta surface mesher."""
my_geometry = TempGeometry("om6wing.csm")
with SI_unit_system:
params = SimulationParams(
private_attribute_asset_cache=AssetCache(
project_entity_info=my_geometry._get_entity_info(),
use_inhouse_mesher=True,
),
meshing=MeshingParams(
defaults=MeshingDefaults(
surface_edge_growth_rate=1.2,
curvature_resolution_angle=12 * u.deg,
surface_max_edge_length=1 * u.m,
target_surface_node_count=50000,
edge_split_layers=0,
),
),
)

params, err, warnings = validate_params_with_context(params, "Geometry", "SurfaceMesh")
assert err is None, f"Unexpected validation error: {err}"
translated = get_surface_meshing_json(params, mesh_unit=get_om6wing_geometry.mesh_unit)
assert translated["target_surface_node_count"] == 50000


def test_legacy_target_surface_node_count_rejected(get_om6wing_geometry):
"""target_surface_node_count is rejected for legacy (non-GAI) flows."""
"""target_surface_node_count is rejected for the legacy mesher."""
my_geometry = TempGeometry("om6wing.csm")
with SI_unit_system:
params = SimulationParams(
Expand Down
Loading