Skip to content

Expand report to support more result types #1356

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,6 @@ tmp/
/.vscode

# test residual
flow360/examples/cylinder2D/flow360mesh.json
flow360/examples/cylinder2D/flow360mesh.json

/local_tests
9 changes: 9 additions & 0 deletions flow360/component/results/base_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,12 @@ def _filtered_sum(self):
regex_pattern = rf"^(?!total).*{variable}$"
df[new_col_name] = list(df.filter(regex=regex_pattern).sum(axis=1))
self.update(df)

def reload_data(self, filter_physical_steps_only: bool = False, include_time: bool = False):
"""
Change default behavior of data loader, reload
"""
super().reload_data(
filter_physical_steps_only=filter_physical_steps_only, include_time=include_time
)
self._filtered_sum()
2 changes: 2 additions & 0 deletions flow360/component/results/case_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from flow360.component.results.base_results import (
_PHYSICAL_STEP,
_PSEUDO_STEP,
_TIME,
PerEntityResultCSVModel,
ResultBaseModel,
ResultCSVModel,
Expand Down Expand Up @@ -355,6 +356,7 @@ class SurfaceHeatTransferResultCSVModel(PerEntityResultCSVModel, TimeSeriesResul
class AeroacousticsResultCSVModel(TimeSeriesResultCSVModel):
"""AeroacousticsResultCSVModel"""

_x_columns: List[str] = [_PHYSICAL_STEP, _TIME]
remote_file_name: str = pd.Field(CaseDownloadable.AEROACOUSTICS.value, frozen=True)


Expand Down
4 changes: 3 additions & 1 deletion flow360/plugins/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ def _generate_shutter_screenshots(self, context: ReportContext):
service.process_requests(context)

def _get_baseline_requirements(self):
return get_requirements_from_data_path(["volume_mesh", "surface_mesh", "geometry"])
return get_requirements_from_data_path(
["volume_mesh", "surface_mesh", "geometry", "params"]
)

def get_requirements(self) -> List[RequirementItem]:
"""
Expand Down
24 changes: 8 additions & 16 deletions flow360/plugins/report/report_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
Delta,
Grouper,
OperationTypes,
RequirementItem,
Tabulary,
_requirements_mapping,
data_from_path,
downsample_image_to_relative_width,
generate_colorbar_from_image,
Expand Down Expand Up @@ -170,7 +170,7 @@ class Inputs(ReportItem):
"""

type_name: Literal["Inputs"] = Field("Inputs", frozen=True)
_requirements: List[str] = [_requirements_mapping["params"]]
_requirements: List[RequirementItem] = [RequirementItem.from_data_key(data_key="params")]

# pylint: disable=too-many-arguments
def get_doc_item(self, context: ReportContext, settings: Settings = None) -> None:
Expand Down Expand Up @@ -1033,19 +1033,12 @@ def _check_dimensions_consistency(self, data):
return True
return False

def _unpack_data_to_multiline(self, x_data: list, y_data: list):
if (
def _is_multiline_data(self, x_data: list, y_data: list):
return (
len(x_data) == 1
and isinstance(x_data[0], list)
and len(y_data) == 1
and isinstance(y_data[0], list)
):
return x_data[0], y_data[0]
return x_data, y_data

def _is_multiline_data(self, x_data: list, y_data: list):
return all(not isinstance(data, list) for data in x_data) and all(
not isinstance(data, list) for data in y_data
)

@abstractmethod
Expand Down Expand Up @@ -1277,7 +1270,7 @@ def get_data(self, cases: List[Case], context: ReportContext) -> PlotModel:
# pylint: disable=protected-access
background_png = background._get_images([cases[0]], context)[0]

x_data, y_data = self._unpack_data_to_multiline(x_data=x_data, y_data=y_data)
# x_data, y_data = self._unpack_data_to_multiline(x_data=x_data, y_data=y_data)

legend = self._handle_legend(cases, x_data, y_data)

Expand Down Expand Up @@ -1449,7 +1442,6 @@ class Chart2D(BaseChart2D):
None,
description='Background type for the chart; set to "geometry" or None. Defaults to ``None``.',
)
_requirements: List[str] = [_requirements_mapping["total_forces"]]
type_name: Literal["Chart2D"] = Field("Chart2D", frozen=True)

@pd.model_validator(mode="after")
Expand Down Expand Up @@ -1527,8 +1519,6 @@ def _handle_legend(self, cases, x_data, y_data):
return self.group_by.arrange_legend()

if self._is_multiline_data(x_data, y_data):
x_data = [float(data) for data in x_data]
y_data = [float(data) for data in y_data]
legend = None
elif isinstance(self.y, list) and (len(self.y) > 1):
legend = []
Expand Down Expand Up @@ -1677,7 +1667,9 @@ class NonlinearResiduals(BaseChart2D):
"nonlinear_residuals/pseudo_step", frozen=True
)
y_log: Literal[True] = Field(True, frozen=True)
_requirements: List[str] = [_requirements_mapping["nonlinear_residuals"]]
_requirements: List[RequirementItem] = [
RequirementItem.from_data_key(data_key="nonlinear_residuals")
]
type_name: Literal["NonlinearResiduals"] = Field("NonlinearResiduals", frozen=True)

def get_requirements(self):
Expand Down
155 changes: 115 additions & 40 deletions flow360/plugins/report/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import uuid
from abc import ABCMeta, abstractmethod
from numbers import Number
from typing import Annotated, Any, List, Literal, Optional, Tuple, Union
from typing import Annotated, Any, ClassVar, List, Literal, Optional, Tuple, Union

import matplotlib.pyplot as plt
import numexpr as ne
Expand Down Expand Up @@ -49,39 +49,111 @@ class RequirementItem(pd.BaseModel):

model_config = {"frozen": True}

# pylint: disable=protected-access
_requirements_mapping: ClassVar[dict] = {
"params": {"resource_type": "case", "filename": "simulation.json"},
"cfl": {
"resource_type": "case",
"filename": case_results.CFLResultCSVModel()._remote_path(),
},
"total_forces": {
"resource_type": "case",
"filename": case_results.TotalForcesResultCSVModel()._remote_path(),
},
"surface_forces": {
"resource_type": "case",
"filename": case_results.SurfaceForcesResultCSVModel()._remote_path(),
},
"linear_residuals": {
"resource_type": "case",
"filename": case_results.LinearResidualsResultCSVModel()._remote_path(),
},
"nonlinear_residuals": {
"resource_type": "case",
"filename": case_results.NonlinearResidualsResultCSVModel()._remote_path(),
},
"x_slicing_force_distribution": {
"resource_type": "case",
"filename": case_results.XSlicingForceDistributionResultCSVModel()._remote_path(),
},
"y_slicing_force_distribution": {
"resource_type": "case",
"filename": case_results.YSlicingForceDistributionResultCSVModel()._remote_path(),
},
"volume_mesh": {"resource_type": "volume_mesh", "filename": "simulation.json"},
"volume_mesh/stats": {
"resource_type": "volume_mesh",
"filename": VolumeMeshV2._mesh_stats_file,
},
"volume_mesh/bounding_box": {
"resource_type": "volume_mesh",
"filename": VolumeMeshDownloadable.BOUNDING_BOX.value,
},
"surface_mesh": {"resource_type": "surface_mesh", "filename": "simulation.json"},
"geometry": {"resource_type": "geometry", "filename": "simulation.json"},
"bet_forces": {
"resource_type": "case",
"filename": case_results.BETForcesResultCSVModel()._remote_path(),
},
"bet_forces_radial_distribution": {
"resource_type": "case",
"filename": case_results.BETForcesRadialDistributionResultCSVModel()._remote_path(),
},
"actuator_disks": {
"resource_type": "case",
"filename": case_results.ActuatorDiskResultCSVModel()._remote_path(),
},
"aeroacoustics": {
"resource_type": "case",
"filename": case_results.AeroacousticsResultCSVModel()._remote_path(),
},
"surface_heat_transfer": {
"resource_type": "case",
"filename": case_results.SurfaceHeatTransferResultCSVModel()._remote_path(),
},
}

_named_results_mapping: ClassVar[dict] = {
"monitors": {
"resource_type": "case",
"pattern": case_results.CaseDownloadable.MONITOR_PATTERN.value,
},
"user_defined_dynamics": {
"resource_type": "case",
"pattern": case_results.CaseDownloadable.USER_DEFINED_DYNAMICS_PATTERN.value,
},
}

# pylint: disable=protected-access
_requirements_mapping = {
"params": RequirementItem(filename="simulation.json"),
"cfl": RequirementItem(filename=case_results.CFLResultCSVModel()._remote_path()),
"total_forces": RequirementItem(
filename=case_results.TotalForcesResultCSVModel()._remote_path()
),
"surface_forces": RequirementItem(
filename=case_results.SurfaceForcesResultCSVModel()._remote_path()
),
"linear_residuals": RequirementItem(
filename=case_results.LinearResidualsResultCSVModel()._remote_path()
),
"nonlinear_residuals": RequirementItem(
filename=case_results.NonlinearResidualsResultCSVModel()._remote_path()
),
"x_slicing_force_distribution": RequirementItem(
filename=case_results.XSlicingForceDistributionResultCSVModel()._remote_path()
),
"y_slicing_force_distribution": RequirementItem(
filename=case_results.YSlicingForceDistributionResultCSVModel()._remote_path()
),
"volume_mesh": RequirementItem(resource_type="volume_mesh", filename="simulation.json"),
"volume_mesh/stats": RequirementItem(
resource_type="volume_mesh", filename=VolumeMeshV2._mesh_stats_file
),
"volume_mesh/bounding_box": RequirementItem(
resource_type="volume_mesh", filename=VolumeMeshDownloadable.BOUNDING_BOX.value
),
"surface_mesh": RequirementItem(resource_type="surface_mesh", filename="simulation.json"),
"geometry": RequirementItem(resource_type="geometry", filename="simulation.json"),
}
@classmethod
@pd.validate_call
def from_data_key(cls, data_key: str, results_name: Optional[str] = None):
"""
Return a RequirementItem object based on the data key (a type of data that is required).

Examples
--------
>>> RequirementItem.from_data_key(data_key="nonlinear_residuals")
>>> RequirementItem.from_data_key(data_key="surface_mesh")
>>> RequirementItem.from_data_key(data_key="monitors", results_name="massFluxExhaust")
"""
if data_key in cls._requirements_mapping:
return cls(
resource_type=cls._requirements_mapping.get(data_key).get("resource_type"),
filename=cls._requirements_mapping.get(data_key).get("filename"),
)
if data_key in cls._named_results_mapping:
if results_name is None:
raise ValueError(f"Results name is required for {data_key}.")
return cls(
resource_type=cls._named_results_mapping.get(data_key).get("resource_type"),
filename="results/"
+ cls._named_results_mapping.get(data_key)
.get("pattern")
.replace("(.+)", results_name),
)
raise NotImplementedError(
f"Unknown data key: {data_key}. Can not use this data for report generation yet."
)


def get_requirements_from_data_path(data_path: List) -> List[RequirementItem]:
Expand All @@ -108,13 +180,16 @@ def get_requirements_from_data_path(data_path: List) -> List[RequirementItem]:
requirements = set()
for item in data_path:
root_path = get_root_path(item)
matched_requirement = None
for key, value in _requirements_mapping.items():
if root_path.startswith(key):
matched_requirement = value
requirements.add(matched_requirement)
if matched_requirement is None:
raise ValueError(f"Unknown result type: {item}")
root_path = split_path(root_path)
results_name = None
if root_path[0] == "results":
data_key = root_path[1]
results_name = root_path[2]
else:
data_key = root_path[0]

requirement = RequirementItem.from_data_key(data_key=data_key, results_name=results_name)
requirements.add(requirement)
return list(requirements)


Expand Down
Loading
Loading