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 5 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
52 changes: 19 additions & 33 deletions flow360/component/results/case_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
from __future__ import annotations

import re
import shutil
import tempfile
import time
import uuid
from collections import defaultdict
from enum import Enum
from typing import Callable, Dict, List, Optional
Expand All @@ -23,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 All @@ -36,27 +34,7 @@
from flow360.exceptions import Flow360ValueError
from flow360.log import log

from ...cloud.s3_utils import (
CloudFileNotFoundError,
get_local_filename_and_create_folders,
)
from ...exceptions import Flow360ValueError
from ...log import log
from ..simulation.conversion import unit_converter as unit_converter_v2
from ..simulation.entity_info import GeometryEntityInfo
from ..simulation.models.surface_models import BoundaryBase
from ..simulation.primitives import GeometryBodyGroup, Surface
from ..simulation.simulation_params import SimulationParams
from ..v1.conversions import unit_converter as unit_converter_v1
from ..v1.flow360_params import Flow360Params
from .base_results import (
_PHYSICAL_STEP,
PerEntityResultCSVModel,
ResultBaseModel,
ResultCSVModel,
ResultTarGZModel,
)

# pylint:disable=invalid-name
_PSEUDO_STEP = "pseudo_step"
_CL = "CL"
_CD = "CD"
Expand Down Expand Up @@ -255,7 +233,7 @@ def _create_surface_forces_group(
for name, entities in entity_groups.items():
self.filter(include=entities)
for variable in self._variables:
if f"total{variable}" not in raw_values:
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}"])
Expand All @@ -268,16 +246,15 @@ def _create_surface_forces_group(
def by_boundary_condition(self, params: SimulationParams) -> SurfaceForcesGroupResultCSVModel:
"""
Group entities by boundary condition's name and create a
SurfaceForcesGroupResultCSVModel
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 boundary_name is None:
boundary_name = model.type
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]
)
Expand All @@ -295,18 +272,27 @@ def by_body_group(self, params: SimulationParams) -> SurfaceForcesGroupResultCSV
"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)
entity_groups: Optional[dict] = pd.Field(None)
remote_file_name: str = pd.Field(None, frozen=True, description="Not used dummy field.")
entity_groups: dict = pd.Field(None, description="The sub-components of each entity group.")

@classmethod
def from_dict(cls, data: dict, group: dict = None):
# pylint: disable=arguments-differ
def from_dict(cls, data: dict, group: dict):
obj = super().from_dict(data)
obj.entity_groups = group
return obj
Expand Down
38 changes: 17 additions & 21 deletions flow360/component/simulation/entity_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,52 +424,48 @@ def create_group_to_sub_component_mapping(group):
mapping[item.private_attribute_id].extend(item.private_attribute_sub_components)
return mapping

body_group_name_to_id_current = create_group_to_sub_component_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)
)
face_group_name_to_id_current = create_group_to_sub_component_mapping(
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_name_to_id_by_body_id = create_group_to_sub_component_mapping(
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_name_to_face_id = defaultdict(list)
for body_group_name, body_ids in body_group_name_to_id_current.items():
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_name_to_face_id[body_group_name].extend(
face_group_name_to_id_by_body_id[body_id]
)
body_group_to_face[body_group].extend(face_group_by_body_id_to_face[body_id])

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

body_group_name_to_face_group_name = defaultdict(list)
for fact_group_name, face_ids in face_group_name_to_id_current.items():
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_id_to_body_group_name.get(face_id)
owning_body = face_to_body_group.get(face_id)
if owning_body is None:
# A face in a face group was not found in any body group
raise ValueError(
f"Face ID '{face_id}' found in face group '{fact_group_name}' "
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 '{fact_group_name}' contains faces belonging to multiple body groups: "
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."
)

if len(body_group_in_this_face_group) == 1:
owning_body = list(body_group_in_this_face_group)[0]
body_group_name_to_face_group_name[owning_body].append(fact_group_name)
owning_body = list(body_group_in_this_face_group)[0]
body_group_to_boundary[owning_body].append(boundary_name)

return body_group_name_to_face_group_name
return body_group_to_boundary


class VolumeMeshEntityInfo(EntityInfoModel):
Expand Down