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
97 changes: 93 additions & 4 deletions flow360/component/simulation/framework/entity_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,14 +624,34 @@ def _process_selectors(
entity_database: EntityDictDatabase,
selectors_value: list,
selector_cache: dict,
known_selectors: dict[str, dict] = None,
) -> tuple[dict[str, list[dict]], list[str]]:
"""Process selectors and return additions grouped by class."""
"""Process selectors and return additions grouped by class.

This function iterates over the list of selectors (which can be full dictionaries or
string tokens).
- If a selector is a string token, it looks up the full definition in `known_selectors`.
- If a selector is a dictionary, it uses it directly.
- It then applies the selector logic to find matching entities from the database.
- Results are cached in `selector_cache` to avoid re-computation for the same selector.
"""
additions_by_class: dict[str, list[dict]] = {}
ordered_target_classes: list[str] = []

for selector_dict in selectors_value:
if not isinstance(selector_dict, dict):
if known_selectors is None:
known_selectors = {}

for item in selectors_value:
selector_dict = None
# Check if the item is a token (string) or a full selector definition (dict)
if isinstance(item, str):
selector_dict = known_selectors.get(item)
elif isinstance(item, dict):
selector_dict = item

if selector_dict is None:
continue

target_class = selector_dict.get("target_class")
pool = _get_entity_pool(entity_database, target_class)
if not pool:
Expand Down Expand Up @@ -680,6 +700,7 @@ def _expand_node_selectors(
node: dict,
selector_cache: dict,
merge_mode: Literal["merge", "replace"],
known_selectors: dict[str, dict] = None,
) -> None:
"""
Expand selectors on one node and write results into stored_entities.
Expand All @@ -692,7 +713,7 @@ def _expand_node_selectors(
return

additions_by_class, ordered_target_classes = _process_selectors(
entity_database, selectors_value, selector_cache
entity_database, selectors_value, selector_cache, known_selectors=known_selectors
)

existing = node.get("stored_entities", [])
Expand All @@ -703,6 +724,60 @@ def _expand_node_selectors(
node["stored_entities"] = base_entities


def collect_and_tokenize_selectors_in_place( # pylint: disable=too-many-branches
params_as_dict: dict,
) -> dict:
"""
Collect all matched/defined selectors into AssetCache and replace them with tokens (names).

This optimization reduces the size of the JSON and allows for efficient re-use of
selector definitions.
1. It traverses the `params_as_dict` to find all `EntitySelector` definitions (dicts with "name").
2. It moves these definitions into `private_attribute_asset_cache["selectors"]`.
3. It replaces the original dictionary definition in the `selectors` list with just the name (token).
"""
known_selectors = {}

# Pre-populate from existing AssetCache if any
asset_cache = params_as_dict.setdefault("private_attribute_asset_cache", {})
if isinstance(asset_cache, dict):
if "selectors" in asset_cache and isinstance(asset_cache["selectors"], list):
for s in asset_cache["selectors"]:
if isinstance(s, dict) and "name" in s:
known_selectors[s["name"]] = s

queue = deque([params_as_dict])
while queue:
node = queue.popleft()
if isinstance(node, dict):
selectors = node.get("selectors")
if isinstance(selectors, list):
new_selectors = []
for item in selectors:
if isinstance(item, dict) and "name" in item:
name = item["name"]
known_selectors[name] = item
new_selectors.append(name)
else:
new_selectors.append(item)
node["selectors"] = new_selectors

# Recurse
for value in node.values():
if isinstance(value, (dict, list)):
queue.append(value)

elif isinstance(node, list):
for item in node:
if isinstance(item, (dict, list)):
queue.append(item)

# Update AssetCache
if isinstance(asset_cache, dict):
asset_cache["selectors"] = list(known_selectors.values())
return params_as_dict


def expand_entity_selectors_in_place(
entity_database: EntityDictDatabase,
params_as_dict: dict,
Expand All @@ -722,6 +797,12 @@ def expand_entity_selectors_in_place(
- This avoids repeated pool scans and matcher compilation across the tree
while preserving stable result ordering.

Token Support
-------------
This function now also builds a `known_selectors` map from `private_attribute_asset_cache["selectors"]`.
This map is passed down to `_process_selectors` to allow resolving string tokens back to their
full selector definitions.

Merge policy
------------
- merge_mode="merge" (default): keep explicit `stored_entities` first, then
Expand All @@ -730,6 +811,13 @@ def expand_entity_selectors_in_place(
- merge_mode="replace": for classes targeted by selectors in the node,
drop explicit items of those classes and use selector results instead.
"""
# Build known_selectors map from AssetCache if available
known_selectors = {}
selectors_list = params_as_dict.get("private_attribute_asset_cache", {}).get("selectors", [])
for s in selectors_list:
if isinstance(s, dict) and "name" in s:
known_selectors[s["name"]] = s

queue: deque[Any] = deque([params_as_dict])
selector_cache: dict = {}
while queue:
Expand All @@ -740,6 +828,7 @@ def expand_entity_selectors_in_place(
node,
selector_cache=selector_cache,
merge_mode=merge_mode,
known_selectors=known_selectors,
)
for value in node.values():
if isinstance(value, (dict, list)):
Expand Down
5 changes: 4 additions & 1 deletion flow360/component/simulation/framework/param_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class AssetCache(Flow360BaseModel):
variable_context: Optional[VariableContextList] = pd.Field(
None, description="List of user variables that are used in all the `Expression` instances."
)
selectors: Optional[List[dict]] = pd.Field(
None, description="Collected entity selectors for token reference."
)

@property
def boundaries(self):
Expand All @@ -68,7 +71,7 @@ def preprocess(
required_by: List[str] = None,
flow360_unit_system=None,
) -> Flow360BaseModel:
exclude_asset_cache = exclude + ["variable_context"]
exclude_asset_cache = exclude + ["variable_context", "selectors"]
return super().preprocess(
params=params,
exclude=exclude_asset_cache,
Expand Down
16 changes: 15 additions & 1 deletion flow360/component/simulation/services_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def has_any_entity_selectors(params_as_dict: dict) -> bool:
selectors = node.get("selectors")
if isinstance(selectors, list) and len(selectors) > 0:
first = selectors[0]
if isinstance(first, str):
# Tokens
return True
if isinstance(first, dict) and "target_class" in first and "children" in first:
return True

Expand Down Expand Up @@ -88,8 +91,19 @@ def strip_selector_matches_inplace(params_as_dict: dict) -> dict:
entity_database = get_entity_database_for_selectors(params_as_dict)
selector_cache: dict = {}

known_selectors = {}
asset_cache = params_as_dict.get("private_attribute_asset_cache", {})
if isinstance(asset_cache, dict):
selectors_list = asset_cache.get("selectors")
if isinstance(selectors_list, list):
for s in selectors_list:
if isinstance(s, dict) and "name" in s:
known_selectors[s["name"]] = s

def _matched_keyset_for_selectors(selectors_value: list) -> set:
additions_by_class, _ = _process_selectors(entity_database, selectors_value, selector_cache)
additions_by_class, _ = _process_selectors(
entity_database, selectors_value, selector_cache, known_selectors=known_selectors
)
keys: set = set()
for items in additions_by_class.values():
for d in items:
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 @@ -19,6 +19,9 @@
from flow360.cloud.rest_api import RestApi
from flow360.component.interfaces import DraftInterface
from flow360.component.resource_base import Flow360Resource, ResourceDraft
from flow360.component.simulation.framework.entity_selector import (
collect_and_tokenize_selectors_in_place,
)
from flow360.component.simulation.services_utils import strip_selector_matches_inplace
from flow360.component.utils import (
check_existence_of_one_file,
Expand Down Expand Up @@ -138,6 +141,7 @@ def update_simulation_params(self, params):
# Serialize to dict and strip selector-matched entities so that UI can distinguish handpicked items
params_dict = params.model_dump(mode="json", exclude_none=True)
params_dict = strip_selector_matches_inplace(params_dict)
params_dict = collect_and_tokenize_selectors_in_place(params_dict)

self.post(
json={
Expand Down
29 changes: 3 additions & 26 deletions tests/simulation/converter/ref/ref_monitor.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
"unit_system": {
"name": "SI"
},
"meshing": null,
"reference_geometry": null,
"operating_condition": null,
"models": [
{
"material": {
Expand All @@ -28,7 +25,6 @@
},
"initial_condition": {
"type_name": "NavierStokesInitialCondition",
"constants": null,
"rho": "rho",
"u": "u",
"v": "v",
Expand All @@ -43,19 +39,15 @@
"order_of_accuracy": 2,
"equation_evaluation_frequency": 1,
"linear_solver": {
"max_iterations": 30,
"absolute_tolerance": null,
"relative_tolerance": null
"max_iterations": 30
},
"private_attribute_dict": null,
"CFL_multiplier": 1.0,
"kappa_MUSCL": -1.0,
"numerical_dissipation_factor": 1.0,
"limit_velocity": false,
"limit_pressure_density": false,
"type_name": "Compressible",
"low_mach_preconditioner": false,
"low_mach_preconditioner_threshold": null,
"update_jacobian_frequency": 4,
"max_force_jac_update_physical_steps": 0
},
Expand All @@ -65,11 +57,8 @@
"order_of_accuracy": 2,
"equation_evaluation_frequency": 4,
"linear_solver": {
"max_iterations": 20,
"absolute_tolerance": null,
"relative_tolerance": null
"max_iterations": 20
},
"private_attribute_dict": null,
"CFL_multiplier": 2.0,
"type_name": "SpalartAllmaras",
"reconstruction_gradient_limiter": 0.5,
Expand All @@ -92,9 +81,7 @@
},
"update_jacobian_frequency": 4,
"max_force_jac_update_physical_steps": 0,
"hybrid_model": null,
"rotation_correction": false,
"controls": null,
"low_reynolds_correction": false
},
"transition_model_solver": {
Expand All @@ -113,14 +100,11 @@
"convergence_limiting_factor": 0.25
}
},
"user_defined_dynamics": null,
"run_control": null,
"user_defined_fields": [],
"outputs": [
{
"name": "R1",
"entities": {
"selectors": null,
"stored_entities": [
{
"private_attribute_entity_type_name": "Point",
Expand All @@ -141,13 +125,11 @@
"primitiveVars"
]
},
"moving_statistic": null,
"output_type": "ProbeOutput"
},
{
"name": "V3",
"entities": {
"selectors": null,
"stored_entities": [
{
"private_attribute_entity_type_name": "Point",
Expand Down Expand Up @@ -192,16 +174,11 @@
"mut"
]
},
"moving_statistic": null,
"output_type": "ProbeOutput"
}
],
"private_attribute_asset_cache": {
"project_length_unit": null,
"project_entity_info": null,
"use_inhouse_mesher": false,
"variable_context": null,
"use_geometry_AI": false
}
}

}
3 changes: 2 additions & 1 deletion tests/simulation/converter/test_monitor_flow360_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_flow360_monitor_convert():

with u.SI_unit_system:
params = SimulationParams(outputs=[*monitor_list])

print(params.private_attribute_asset_cache)
params_dict = params.model_dump(
mode="json",
exclude={
Expand All @@ -42,6 +42,7 @@ def test_flow360_monitor_convert():
"private_attribute_input_cache",
"private_attribute_dict",
},
exclude_none=True,
)
del params_dict["outputs"][0]["private_attribute_id"]
del params_dict["outputs"][0]["entities"]["stored_entities"][0]["private_attribute_id"]
Expand Down
Loading