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
53 changes: 38 additions & 15 deletions flow360/component/simulation/framework/entity_expansion_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,38 @@
from flow360.component.simulation.framework.entity_registry import EntityRegistry


def _register_mirror_entities_in_registry(registry: "EntityRegistry", mirror_status: Any) -> None:
"""Register mirror-related entities (planes + derived mirrored entities) into registry.

This helper is shared by both dict-based and params-based registry builders to ensure
consistent selector expansion coverage.
"""
if not mirror_status:
return

# pylint: disable=import-outside-toplevel
from flow360.component.simulation.draft_context.mirror import (
MirrorPlane,
MirrorStatus,
)

# Dict path: deserialize to MirrorStatus
if isinstance(mirror_status, dict):
mirror_status = MirrorStatus.model_validate(mirror_status)

# Object path: MirrorStatus (or compatible) with is_empty()
if hasattr(mirror_status, "is_empty") and mirror_status.is_empty():
return

for plane in getattr(mirror_status, "mirror_planes", []) or []:
if isinstance(plane, MirrorPlane):
registry.register(plane)
for mirrored_group in getattr(mirror_status, "mirrored_geometry_body_groups", []) or []:
registry.register(mirrored_group)
for mirrored_surface in getattr(mirror_status, "mirrored_surfaces", []) or []:
registry.register(mirrored_surface)


def expand_entity_list_in_context(
entity_list,
params,
Expand Down Expand Up @@ -129,21 +161,7 @@ def get_registry_from_params(params) -> EntityRegistry:
# Register mirror entities from mirror_status so selector expansion can include mirrored types
# (e.g. SurfaceSelector can expand to include MirroredSurface).
mirror_status = getattr(asset_cache, "mirror_status", None)
if mirror_status is None or mirror_status.is_empty():
return registry

# pylint: disable=import-outside-toplevel
from flow360.component.simulation.draft_context.mirror import MirrorPlane

for plane in mirror_status.mirror_planes:
if isinstance(plane, MirrorPlane):
registry.register(plane)

for mirrored_group in mirror_status.mirrored_geometry_body_groups:
registry.register(mirrored_group)

for mirrored_surface in mirror_status.mirrored_surfaces:
registry.register(mirrored_surface)
_register_mirror_entities_in_registry(registry, mirror_status)

return registry

Expand Down Expand Up @@ -227,4 +245,9 @@ def get_entity_info_and_registry_from_dict(params_as_dict: dict) -> tuple:
entity_info = parse_entity_info_model(entity_info_dict)
registry = EntityRegistry.from_entity_info(entity_info)

# Register mirror entities from mirror_status so selector expansion can include mirrored types
# (e.g. SurfaceSelector can expand to include MirroredSurface) during validation.
mirror_status_dict = asset_cache.get("mirror_status")
_register_mirror_entities_in_registry(registry, mirror_status_dict)

return entity_info, registry
62 changes: 60 additions & 2 deletions tests/simulation/framework/test_entity_dict_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ def _load_simulation_json(relative_path: str) -> dict:
class _AssetCache:
"""Simple object to hold asset cache data."""

def __init__(self, project_entity_info, selectors):
def __init__(self, project_entity_info, selectors, mirror_status=None):
self.project_entity_info = project_entity_info
self.selectors = selectors
self.mirror_status = mirror_status


class _DummyParams:
Expand All @@ -52,8 +53,15 @@ def __init__(self, params_dict: dict, entity_info_obj=None):
entity_info_obj = parse_entity_info_model(entity_info_dict)

selectors = asset_cache_dict.get("selectors")
mirror_status = None
mirror_status_dict = asset_cache_dict.get("mirror_status")
if isinstance(mirror_status_dict, dict) and mirror_status_dict:
# pylint: disable=import-outside-toplevel
from flow360.component.simulation.draft_context.mirror import MirrorStatus

mirror_status = MirrorStatus.model_validate(mirror_status_dict)
self.private_attribute_asset_cache = _AssetCache(
project_entity_info=entity_info_obj, selectors=selectors
project_entity_info=entity_info_obj, selectors=selectors, mirror_status=mirror_status
)

def model_dump(self, **kwargs):
Expand Down Expand Up @@ -89,6 +97,56 @@ def _build_simple_params_dict():
}


def _build_simple_params_dict_with_mirror_status():
params_as_dict = _build_simple_params_dict()
params_as_dict["private_attribute_asset_cache"]["mirror_status"] = {
"mirror_planes": [
{
"name": "plane-1",
"normal": [0, 1, 0],
"center": {"value": [0, 0, 0], "units": "m"},
"private_attribute_entity_type_name": "MirrorPlane",
"private_attribute_id": "mirror-plane-1",
}
],
"mirrored_geometry_body_groups": [
{
"name": "body-1_<mirror>",
"geometry_body_group_id": "body-1",
"mirror_plane_id": "mirror-plane-1",
"private_attribute_entity_type_name": "MirroredGeometryBodyGroup",
"private_attribute_id": "mirrored-body-1",
}
],
"mirrored_surfaces": [
{
"name": "wall_<mirror>",
"surface_id": "wall",
"mirror_plane_id": "mirror-plane-1",
"private_attribute_entity_type_name": "MirroredSurface",
"private_attribute_id": "mirrored-surface-1",
}
],
}
return params_as_dict


def test_get_registry_from_params_matches_dict_with_mirror_status():
params_as_dict = _build_simple_params_dict_with_mirror_status()
dummy_params = _DummyParams(params_as_dict)

_, dict_registry = get_entity_info_and_registry_from_dict(params_as_dict)
instance_registry = get_registry_from_params(dummy_params)

dict_mirrored_surfaces = dict_registry.find_by_type_name("MirroredSurface")
instance_mirrored_surfaces = instance_registry.find_by_type_name("MirroredSurface")
assert _entity_names(dict_mirrored_surfaces) == _entity_names(instance_mirrored_surfaces)

dict_planes = dict_registry.find_by_type_name("MirrorPlane")
instance_planes = instance_registry.find_by_type_name("MirrorPlane")
assert _entity_names(dict_planes) == _entity_names(instance_planes)


def test_get_registry_for_geometry_entity_info():
"""
Test get_entity_info_and_registry_from_dict with GeometryEntityInfo.
Expand Down
13 changes: 13 additions & 0 deletions tests/simulation/services/test_entity_processing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ def test_expand_entity_list_in_context_includes_mirrored_entities_from_mirror_st
)
],
private_attribute_asset_cache=AssetCache(
use_inhouse_mesher=True,
use_geometry_AI=True,
project_entity_info=SurfaceMeshEntityInfo(
boundaries=[Surface(name="front", private_attribute_id="s-1")]
),
Expand All @@ -291,6 +293,17 @@ def test_expand_entity_list_in_context_includes_mirrored_entities_from_mirror_st
),
)

# Validate schema-level correctness (skip contextual validation since this test doesn't
# provide full Case-level required fields like meshing/models/operating_condition).
validated, errors, _warnings = validate_model(
params_as_dict=params.model_dump(exclude_none=True),
validated_by=ValidationCalledBy.LOCAL,
root_item_type=None,
validation_level=None,
)
assert errors is None
assert validated is not None

expanded = expand_entity_list_in_context(params.outputs[0].entities, params, return_names=False)
expanded_type_names = {entity.private_attribute_entity_type_name for entity in expanded}
assert "Surface" in expanded_type_names
Expand Down
Loading