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: 0 additions & 6 deletions flow360/component/simulation/framework/entity_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import hashlib
import uuid
from abc import ABCMeta
from collections import defaultdict
from typing import Annotated, ClassVar, List, Optional, Union, get_args, get_origin
Expand All @@ -15,11 +14,6 @@
from flow360.component.simulation.utils import is_exact_instance


def generate_uuid():
"""generate a unique identifier for non-persistent entities. Required by front end."""
return str(uuid.uuid4())


class EntityBase(Flow360BaseModel, metaclass=ABCMeta):
"""
Base class for dynamic entity types.
Expand Down
48 changes: 42 additions & 6 deletions flow360/component/simulation/framework/entity_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing_extensions import Self

from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_utils import generate_uuid

# These corresponds to the private_attribute_entity_type_name of supported entity types.
TargetClass = Literal["Surface", "Edge", "GenericVolume", "GeometryBodyGroup"]
Expand Down Expand Up @@ -49,6 +50,14 @@ class EntitySelector(Flow360BaseModel):
"""

target_class: TargetClass = pd.Field()
description: Optional[str] = pd.Field(
None, description="Customizable description of the selector."
)
selector_id: str = pd.Field(
default_factory=generate_uuid,
description="[Internal] Unique identifier for the selector.",
frozen=True,
)
# Unique name for global reuse
name: str = pd.Field(description="Unique name for this selector.")
logic: Literal["AND", "OR"] = pd.Field("AND")
Expand Down Expand Up @@ -204,6 +213,7 @@ def match(
attribute: Literal["name"] = "name",
syntax: Literal["glob", "regex"] = "glob",
logic: Literal["AND", "OR"] = "AND",
description: Optional[str] = None,
) -> EntitySelector:
"""
Create an EntitySelector for this class and seed it with one matches predicate.
Expand All @@ -227,7 +237,10 @@ def match(
_validate_selector_pattern("match", pattern)

selector = generate_entity_selector_from_class(
selector_name=name, entity_class=cls, logic=logic
selector_name=name,
entity_class=cls,
logic=logic,
selector_description=description,
)
selector.match(pattern, attribute=attribute, syntax=syntax)
return selector
Expand All @@ -243,6 +256,7 @@ def not_match(
attribute: Literal["name"] = "name",
syntax: Literal["glob", "regex"] = "glob",
logic: Literal["AND", "OR"] = "AND",
description: Optional[str] = None,
) -> EntitySelector:
"""Create an EntitySelector and seed a notMatches predicate.

Expand All @@ -261,11 +275,15 @@ def not_match(
_validate_selector_pattern("not_match", pattern)

selector = generate_entity_selector_from_class(
selector_name=name, entity_class=cls, logic=logic
selector_name=name,
entity_class=cls,
logic=logic,
selector_description=description,
)
selector.not_match(pattern, attribute=attribute, syntax=syntax)
return selector

# pylint: disable=too-many-arguments
@classmethod
def any_of(
cls,
Expand All @@ -275,6 +293,7 @@ def any_of(
name: str,
attribute: Literal["name"] = "name",
logic: Literal["AND", "OR"] = "AND",
description: Optional[str] = None,
) -> EntitySelector:
"""Create an EntitySelector and seed an in predicate.

Expand All @@ -292,11 +311,15 @@ def any_of(
_validate_selector_values("any_of", values)

selector = generate_entity_selector_from_class(
selector_name=name, entity_class=cls, logic=logic
selector_name=name,
entity_class=cls,
logic=logic,
selector_description=description,
)
selector.any_of(values, attribute=attribute)
return selector

# pylint: disable=too-many-arguments
@classmethod
def not_any_of(
cls,
Expand All @@ -306,6 +329,7 @@ def not_any_of(
name: str,
attribute: Literal["name"] = "name",
logic: Literal["AND", "OR"] = "AND",
description: Optional[str] = None,
) -> EntitySelector:
"""Create an EntitySelector and seed a notIn predicate.

Expand All @@ -320,14 +344,20 @@ def not_any_of(
_validate_selector_values("not_any_of", values) # type: ignore[arg-type]

selector = generate_entity_selector_from_class(
selector_name=name, entity_class=cls, logic=logic
selector_name=name,
entity_class=cls,
logic=logic,
selector_description=description,
)
selector.not_any_of(values, attribute=attribute)
return selector


def generate_entity_selector_from_class(
selector_name: str, entity_class: type, logic: Literal["AND", "OR"] = "AND"
selector_name: str,
entity_class: type,
logic: Literal["AND", "OR"] = "AND",
selector_description: Optional[str] = None,
) -> EntitySelector:
"""
Create a new selector for the given entity class.
Expand All @@ -340,7 +370,13 @@ def generate_entity_selector_from_class(
class_name in allowed_classes
), f"Unknown entity class: {entity_class} for generating entity selector."

return EntitySelector(name=selector_name, target_class=class_name, logic=logic, children=[])
return EntitySelector(
name=selector_name,
description=selector_description,
target_class=class_name,
logic=logic,
children=[],
)


########## EXPANSION IMPLEMENTATION ##########
Expand Down
8 changes: 8 additions & 0 deletions flow360/component/simulation/framework/entity_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Shared utilities for entity operations."""

import uuid


def generate_uuid():
"""generate a unique identifier for non-persistent entities. Required by front end."""
return str(uuid.uuid4())
2 changes: 1 addition & 1 deletion flow360/component/simulation/framework/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import re
from typing import Any

from flow360.component.simulation.framework.entity_base import generate_uuid
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.framework.updater_functions import (
fix_ghost_sphere_schema,
populate_entity_id_with_name,
Expand Down
3 changes: 2 additions & 1 deletion flow360/component/simulation/models/surface_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

import flow360.component.simulation.units as u
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import EntityList, generate_uuid
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.framework.expressions import StringExpression
from flow360.component.simulation.framework.single_attribute_base import (
SingleAttributeModel,
Expand Down
3 changes: 2 additions & 1 deletion flow360/component/simulation/models/volume_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

import flow360.component.simulation.units as u
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import EntityList, generate_uuid
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.framework.expressions import (
StringExpression,
validate_angle_expression_of_t_seconds,
Expand Down
3 changes: 2 additions & 1 deletion flow360/component/simulation/outputs/output_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import pydantic as pd

from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import EntityBase, generate_uuid
from flow360.component.simulation.framework.entity_base import EntityBase
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.outputs.output_fields import IsoSurfaceFieldNames
from flow360.component.simulation.unit_system import LengthType
from flow360.component.simulation.user_code.core.types import (
Expand Down
3 changes: 2 additions & 1 deletion flow360/component/simulation/outputs/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

import flow360.component.simulation.units as u
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import EntityList, generate_uuid
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.framework.expressions import StringExpression
from flow360.component.simulation.framework.unique_list import UniqueItemList
from flow360.component.simulation.models.surface_models import EntityListAllowingGhost
Expand Down
7 changes: 2 additions & 5 deletions flow360/component/simulation/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@
rotation_matrix_from_axis_and_angle,
)
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import (
EntityBase,
EntityList,
generate_uuid,
)
from flow360.component.simulation.framework.entity_base import EntityBase, EntityList
from flow360.component.simulation.framework.entity_selector import SelectorFactory
from flow360.component.simulation.framework.entity_utils import generate_uuid
from flow360.component.simulation.framework.multi_constructor_model_base import (
MultiConstructorBaseModel,
)
Expand Down
13 changes: 13 additions & 0 deletions tests/simulation/framework/test_entity_selector_fluent_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,16 @@ def test_edge_class_basic_match():
assert [e["name"] for e in stored if e["private_attribute_entity_type_name"] == "Edge"] == [
"edgeA"
]


def test_selector_factory_propagates_description():
"""
Test: SelectorFactory methods propagate description into EntitySelector instances.

Expected behavior:
- Passing description to Surface.match() stores it on the resulting selector.
- model_dump() contains the provided description for serialization/round-trip.
"""
selector = Surface.match("*", name="desc_selector", description="Select all surfaces")
assert selector.description == "Select all surfaces"
assert selector.model_dump()["description"] == "Select all surfaces"
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def test_validate_model_materializes_dict_and_preserves_selectors():
"target_class": "Surface",
"name": "some_selector_name",
"logic": "AND",
"selector_id": "some_selector_id",
"children": [{"attribute": "name", "operator": "matches", "value": "*"}],
}
outputs = params.get("outputs") or []
Expand Down