Skip to content

[FL-727][FLPY-3] Support group by boundary and by body in surface force csv result model #1084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 27, 2025
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
80 changes: 80 additions & 0 deletions flow360/component/results/case_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import re
from collections import defaultdict
from enum import Enum
from typing import Callable, Dict, List, Optional

Expand All @@ -18,6 +19,8 @@
ResultTarGZModel,
)
from flow360.component.simulation.conversion import unit_converter as unit_converter_v2
from flow360.component.simulation.entity_info import GeometryEntityInfo
from flow360.component.simulation.models.surface_models import BoundaryBase
from flow360.component.simulation.simulation_params import SimulationParams
from flow360.component.simulation.unit_system import (
Flow360UnitSystem,
Expand Down Expand Up @@ -218,6 +221,83 @@ def _preprocess(self, filter_physical_steps_only: bool = True, include_time: boo
def reload_data(self, filter_physical_steps_only: bool = True, include_time: bool = True):
return super().reload_data(filter_physical_steps_only, include_time)

def _create_surface_forces_group(
self, entity_groups: Dict[str, List[str]]
) -> SurfaceForcesGroupResultCSVModel:
"""
Create the SurfaceForcesGroupResultCSVModel for the given entity groups.
"""
raw_values = {}
for x_column in self._x_columns:
raw_values[x_column] = np.array(self.raw_values[x_column])
for name, entities in entity_groups.items():
self.filter(include=entities)
for variable in self._variables:
if f"{name}_{variable}" not in raw_values:
raw_values[f"{name}_{variable}"] = np.array(self.values[f"total{variable}"])
continue
raw_values[f"{name}_{variable}"] += np.array(self.values[f"total{variable}"])

raw_values = {key: val.tolist() for key, val in raw_values.items()}
entity_groups = {key: sorted(val) for key, val in entity_groups.items()}

return SurfaceForcesGroupResultCSVModel.from_dict(data=raw_values, group=entity_groups)

def by_boundary_condition(self, params: SimulationParams) -> SurfaceForcesGroupResultCSVModel:
"""
Group entities by boundary condition's name and create a
SurfaceForcesGroupResultCSVModel.
Forces from different boundaries but with the same type and name will be summed together.
"""

entity_groups = defaultdict(list)
for model in params.models:
if not isinstance(model, BoundaryBase):
continue
boundary_name = model.name if model.name is not None else model.type
entity_groups[boundary_name].extend(
[entity.name for entity in model.entities.stored_entities]
)
return self._create_surface_forces_group(entity_groups=entity_groups)

def by_body_group(self, params: SimulationParams) -> SurfaceForcesGroupResultCSVModel:
"""
Group entities by body group's name and create a
SurfaceForcesGroupResultCSVModel
"""
if not isinstance(
params.private_attribute_asset_cache.project_entity_info, GeometryEntityInfo
):
raise Flow360ValueError(
"Group surface forces by body group is only supported for case starting from geometry."
)
entity_info = params.private_attribute_asset_cache.project_entity_info
if (
not hasattr(entity_info, "body_attribute_names")
or "groupByBodyId" not in entity_info.face_attribute_names
):
raise Flow360ValueError(
"The geometry in this case does not contain the necessary body group information, "
"please upgrade the project to the latest version and re-run the case."
)
entity_groups = entity_info.get_body_group_to_face_group_name_map()
return self._create_surface_forces_group(entity_groups=entity_groups)


class SurfaceForcesGroupResultCSVModel(SurfaceForcesResultCSVModel):
"""SurfaceForcesGroupResultCSVModel"""

remote_file_name: str = pd.Field(None, frozen=True) # Unused dummy field
_entity_groups: dict = pd.PrivateAttr()

@classmethod
# pylint: disable=arguments-differ
def from_dict(cls, data: dict, group: dict):
obj = super().from_dict(data)
# pylint: disable=protected-access
obj._entity_groups = group
return obj


class LegacyForceDistributionResultCSVModel(ResultCSVModel):
"""ForceDistributionResultCSVModel"""
Expand Down
56 changes: 56 additions & 0 deletions flow360/component/simulation/entity_info.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Deserializer for entity info retrieved from asset metadata pipeline."""

from abc import ABCMeta, abstractmethod
from collections import defaultdict
from typing import Annotated, List, Literal, Optional, Union

import pydantic as pd
Expand Down Expand Up @@ -411,6 +412,61 @@ def compute_transformation_matrices(self):
body_group.transformation.get_transformation_matrix().flatten().tolist()
)

def get_body_group_to_face_group_name_map(self) -> dict[str, list[str]]:
"""
Returns bodyId to file name mapping.
"""

# pylint: disable=too-many-locals
def create_group_to_sub_component_mapping(group):
mapping = defaultdict(list)
for item in group:
mapping[item.private_attribute_id].extend(item.private_attribute_sub_components)
return mapping

body_group_to_body = create_group_to_sub_component_mapping(
self._get_list_of_entities(entity_type_name="body", attribute_name=self.body_group_tag)
)
boundary_to_face = create_group_to_sub_component_mapping(
self._get_list_of_entities(entity_type_name="face", attribute_name=self.face_group_tag)
)
face_group_by_body_id_to_face = create_group_to_sub_component_mapping(
self._get_list_of_entities(entity_type_name="face", attribute_name="groupByBodyId")
)

body_group_to_face = defaultdict(list)
for body_group, body_ids in body_group_to_body.items():
for body_id in body_ids:
body_group_to_face[body_group].extend(face_group_by_body_id_to_face[body_id])

face_to_body_group = {}
for body_group_name, face_ids in body_group_to_face.items():
for face_id in face_ids:
face_to_body_group[face_id] = body_group_name

body_group_to_boundary = defaultdict(list)
for boundary_name, face_ids in boundary_to_face.items():
body_group_in_this_face_group = set()
for face_id in face_ids:
owning_body = face_to_body_group.get(face_id)
if owning_body is None:
raise ValueError(
f"Face ID '{face_id}' found in face group '{boundary_name}' "
"but not found in any body group."
)
body_group_in_this_face_group.add(owning_body)
if len(body_group_in_this_face_group) > 1:
raise ValueError(
f"Face group '{boundary_name}' contains faces belonging to multiple body groups: "
f"{list(sorted(body_group_in_this_face_group))}. "
"The mapping between body and face groups cannot be created."
)

owning_body = list(body_group_in_this_face_group)[0]
body_group_to_boundary[owning_body].append(boundary_name)

return body_group_to_boundary


class VolumeMeshEntityInfo(EntityInfoModel):
"""Data model for volume mesh entityInfo.json"""
Expand Down
Loading