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
6 changes: 6 additions & 0 deletions flow360/component/simulation/simulation_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
_check_numerical_dissipation_factor_output,
_check_parent_volume_is_rotating,
_check_time_average_output,
_check_unique_selector_names,
_check_unsteadiness_to_use_hybrid_model,
_check_valid_models_for_liquid,
)
Expand Down Expand Up @@ -556,6 +557,11 @@ def check_duplicate_entities_in_models(self, param_info: ParamsValidationInfo):
"""Only allow each Surface/Volume entity to appear once in the Surface/Volume model"""
return _check_duplicate_entities_in_models(self, param_info)

@contextual_model_validator(mode="after")
def check_unique_selector_names(self):
"""Ensure all EntitySelector names are unique"""
return _check_unique_selector_names(self)

@pd.model_validator(mode="after")
def check_numerical_dissipation_factor_output(self):
"""Only allow numericalDissipationFactor output field when the NS solver has low numerical dissipation"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,32 @@ def _check_actuator_disk_names(models):
_check_actuator_disk_names(models)

return models


def _check_unique_selector_names(params):
"""Check that all EntitySelector names are unique across the entire SimulationParams.

This validator checks the asset_cache.used_selectors field, which is populated
during the tokenization process in set_up_params_for_uploading().
"""
asset_cache = getattr(params, "private_attribute_asset_cache", None)
if asset_cache is None:
return params

used_selectors = getattr(asset_cache, "used_selectors", None)
if not used_selectors:
return params

selector_names: set[str] = set() # name -> first occurrence info

for selector in used_selectors:
selector_name = selector.name
if selector_name in selector_names:
raise ValueError(
f"Duplicate selector name '{selector_name}' found. "
f"Each selector must have a unique name."
)
# Store location info for better error messages
selector_names.add(selector_name)

return params
91 changes: 91 additions & 0 deletions tests/simulation/params/test_validators_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2577,3 +2577,94 @@ def test_deleted_surfaces_domain_type():

assert len(errors) == 1
assert "Boundary `pos_surf` will likely be deleted" in errors[0]["msg"]


def test_unique_selector_names():
"""Test that duplicate selector names are detected and raise an error."""
from flow360.component.simulation.framework.entity_selector import (
SurfaceSelector,
collect_and_tokenize_selectors_in_place,
)
from flow360.component.simulation.models.surface_models import Wall
from flow360.component.simulation.primitives import Surface

# Create actual Surface entities to avoid selector expansion issues
surface1 = Surface(name="surface1")
surface2 = Surface(name="surface2")

# Create selectors with duplicate names
selector1 = SurfaceSelector(name="duplicate_name").match("wing*")
selector2 = SurfaceSelector(name="duplicate_name").match("tail*")

# Test duplicate selector names in different EntityLists (different Wall models)
with SI_unit_system:
params = SimulationParams(
models=[
Wall(entities=[surface1, selector1]),
Wall(entities=[surface2, selector2]),
],
)

# Tokenize selectors to populate used_selectors (simulating what happens in set_up_params_for_uploading)
params_dict = params.model_dump(mode="json", exclude_none=True)
params_dict = collect_and_tokenize_selectors_in_place(params_dict)

# Now validate using validate_model which will materialize and validate
_, errors, _ = validate_model(
params_as_dict=params_dict,
validated_by=ValidationCalledBy.LOCAL,
root_item_type=None,
validation_level=None,
)

assert errors is not None
assert len(errors) == 1
assert "Duplicate selector name 'duplicate_name'" in errors[0]["msg"]

# Test duplicate selector names in the same EntityList
with SI_unit_system:
params2 = SimulationParams(
models=[
Wall(entities=[surface1, selector1, selector2]),
],
)

params_dict2 = params2.model_dump(mode="json", exclude_none=True)
params_dict2 = collect_and_tokenize_selectors_in_place(params_dict2)

_, errors2, _ = validate_model(
params_as_dict=params_dict2,
validated_by=ValidationCalledBy.LOCAL,
root_item_type=None,
validation_level=None,
)

assert errors2 is not None
assert len(errors2) == 1
assert "Duplicate selector name 'duplicate_name'" in errors2[0]["msg"]

# Test that unique selector names work fine
selector3 = SurfaceSelector(name="unique_name_1").match("wing*")
selector4 = SurfaceSelector(name="unique_name_2").match("tail*")

with SI_unit_system:
params3 = SimulationParams(
models=[
Wall(entities=[surface1, selector3]),
Wall(entities=[surface2, selector4]),
],
)

params_dict3 = params3.model_dump(mode="json", exclude_none=True)
params_dict3 = collect_and_tokenize_selectors_in_place(params_dict3)

validated_params, errors3, _ = validate_model(
params_as_dict=params_dict3,
validated_by=ValidationCalledBy.LOCAL,
root_item_type=None,
validation_level=None,
)

# Should not have errors for unique names
assert errors3 is None or len(errors3) == 0
assert validated_params is not None
Loading