Skip to content

[Hotfix 25.2]: [FL-727] Support group by boundary and by body in surface force csv result model #1100

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

Closed
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
96 changes: 96 additions & 0 deletions flow360/component/results/case_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

import os
import re
<<<<<<< HEAD
import shutil
import tempfile
import time
import uuid
=======
from collections import defaultdict
>>>>>>> 04671918 ([FL-727] Support group by boundary and by body in surface force csv result model (#1084))
from enum import Enum
from itertools import chain, product
from typing import Callable, Dict, List, Optional
Expand All @@ -16,6 +20,21 @@
import pandas
import pydantic as pd

<<<<<<< HEAD
=======
from flow360.cloud.s3_utils import CloudFileNotFoundError
from flow360.component.results.base_results import (
_PHYSICAL_STEP,
PerEntityResultCSVModel,
ResultBaseModel,
ResultCSVModel,
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
>>>>>>> 04671918 ([FL-727] Support group by boundary and by body in surface force csv result model (#1084))
from flow360.component.simulation.unit_system import (
Flow360UnitSystem,
ForceType,
Expand Down Expand Up @@ -785,6 +804,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 @@ -180,6 +181,61 @@ def _search_and_replace(grouped_entities, entity_registry: EntityRegistry):
_search_and_replace(self.grouped_faces, param_entity_registry)
_search_and_replace(self.grouped_edges, param_entity_registry)

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