diff --git a/flow360/component/project.py b/flow360/component/project.py index a89562f01..e40641e66 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -48,6 +48,7 @@ from flow360.component.volume_mesh import VolumeMeshV2 from flow360.exceptions import Flow360FileError, Flow360ValueError, Flow360WebError from flow360.log import log +from flow360.plugins.report.report import get_default_report_summary_template from flow360.version import __solver_version__ AssetOrResource = Union[type[AssetBase], type[Flow360Resource]] @@ -1275,6 +1276,7 @@ def _run( root asset (Geometry or VolumeMesh) is not initialized. """ + # pylint: disable=too-many-branches if use_beta_mesher is None: if use_geometry_AI is True: log.info("Beta mesher is enabled to use Geometry AI.") @@ -1550,4 +1552,10 @@ def run_case( tags=tags, **kwargs, ) + report_template = get_default_report_summary_template() + report_template.create_in_cloud( + name="ResultSummary", + cases=[case], + solver_version=solver_version if solver_version else self.solver_version, + ) return case diff --git a/flow360/component/simulation/framework/updater_utils.py b/flow360/component/simulation/framework/updater_utils.py index e163f2bbe..4852a1f9e 100644 --- a/flow360/component/simulation/framework/updater_utils.py +++ b/flow360/component/simulation/framework/updater_utils.py @@ -51,7 +51,7 @@ def compare_lists(list1, list2, atol=1e-15, rtol=1e-10, ignore_keys=None): if len(list1) != len(list2): return False - if list1 and not isinstance(list1[0], dict): + if list1 and not any(isinstance(item, dict) for item in list1): list1, list2 = sorted(list1), sorted(list2) for item1, item2 in zip(list1, list2): diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index ce6a3931c..4a461170e 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -55,6 +55,7 @@ ValidationContext, ) from flow360.exceptions import Flow360RuntimeError, Flow360TranslationError +from flow360.plugins.report.report import get_default_report_summary_template from flow360.version import __version__ unit_system_map = { @@ -764,3 +765,16 @@ def update_simulation_json(*, params_as_dict: dict, target_python_api_version: s # Expected exceptions errors.append(str(e)) return updated_params_as_dict, errors + + +def get_default_report_config() -> dict: + """ + Get the default report config + Returns + ------- + dict + default report config + """ + return get_default_report_summary_template().model_dump( + exclude_none=True, + ) diff --git a/flow360/plugins/report/report.py b/flow360/plugins/report/report.py index 7c74f8516..c3f632ad4 100644 --- a/flow360/plugins/report/report.py +++ b/flow360/plugins/report/report.py @@ -11,9 +11,9 @@ # pylint: disable=import-error from pylatex import Section, Subsection -from flow360 import Case from flow360.cloud.flow360_requests import NewReportRequest from flow360.cloud.rest_api import RestApi +from flow360.component.case import Case from flow360.component.interfaces import ReportInterface from flow360.component.resource_base import AssetMetaBaseModel, Flow360Resource from flow360.component.simulation.framework.base_model import Flow360BaseModel @@ -32,6 +32,8 @@ Table, ) from flow360.plugins.report.utils import ( + Average, + DataItem, RequirementItem, get_requirements_from_data_path, ) @@ -319,3 +321,39 @@ def create_pdf( item.get_doc_item(case_context, self.settings) report_doc.generate_pdf(os.path.join(data_storage, filename)) + + +def get_default_report_summary_template() -> ReportTemplate: + """ + Returns default report template for result summary. + """ + avg = Average(fraction=0.1) + + data = [ + "volume_mesh/bounding_box/length", + "volume_mesh/bounding_box/height", + "volume_mesh/bounding_box/width", + "params/reference_geometry/moment_length", + "params/reference_geometry/area", + DataItem(data="surface_forces/totalCL", title="CL", operations=avg), + DataItem(data="surface_forces/totalCD", title="CD", operations=avg), + DataItem(data="surface_forces/totalCFy", title="CFy", operations=avg), + DataItem(data="surface_forces/totalCMx", title="CMx", operations=avg), + DataItem(data="surface_forces/totalCMy", title="CMy", operations=avg), + DataItem(data="surface_forces/totalCMz", title="CMz", operations=avg), + ] + headers = [ + "OAL", + "OAH", + "OAW", + "Reference Length", + "Reference Area", + "CL", + "CD", + "CFy", + "CMx", + "CMy", + "CMz", + ] + table = Table(data=data, section_title="result_summary", headers=headers) + return ReportTemplate(items=[table], settings=Settings(dump_table_csv=True)) diff --git a/flow360/plugins/report/report_context.py b/flow360/plugins/report/report_context.py index e54c78574..11e157b61 100644 --- a/flow360/plugins/report/report_context.py +++ b/flow360/plugins/report/report_context.py @@ -9,7 +9,7 @@ # pylint: disable=import-error from pylatex import Document, Section, Subsection -from flow360 import Case +from flow360.component.case import Case class ReportContext(pd.BaseModel): diff --git a/flow360/plugins/report/report_items.py b/flow360/plugins/report/report_items.py index 06017f66c..646d654f0 100644 --- a/flow360/plugins/report/report_items.py +++ b/flow360/plugins/report/report_items.py @@ -33,13 +33,14 @@ # pylint: disable=import-error from pylatex.utils import bold, escape_latex -from flow360 import Case, SimulationParams +from flow360.component.case import Case from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.outputs.output_fields import ( IsoSurfaceFieldNames, SurfaceFieldNames, get_unit_for_field, ) +from flow360.component.simulation.simulation_params import SimulationParams from flow360.component.simulation.time_stepping.time_stepping import Unsteady from flow360.component.simulation.unit_system import ( DimensionedTypes, @@ -108,7 +109,10 @@ class Settings(Flow360BaseModel): If not specified, defaults to 300. """ + # pylint: disable=fixme + # TODO: Create a setting class for each type of report items. dpi: Optional[pd.PositiveInt] = 300 + dump_table_csv: Optional[pd.StrictBool] = False class ReportItem(Flow360BaseModel): @@ -408,6 +412,10 @@ def get_doc_item(self, context: ReportContext, settings: Settings = None) -> Non table.add_row(formatted) table.add_hline() + if settings is not None and settings.dump_table_csv: + df = self.to_dataframe(context=context) + df.to_csv(f"{self.section_title}.csv", index=False) + class PatternCaption(Flow360BaseModel): """ diff --git a/flow360/plugins/report/utils.py b/flow360/plugins/report/utils.py index 8ee766a5b..3cb94e293 100644 --- a/flow360/plugins/report/utils.py +++ b/flow360/plugins/report/utils.py @@ -26,7 +26,7 @@ # pylint: disable=import-error from pylatex import NoEscape, Package, Tabular -from flow360 import Case +from flow360.component.case import Case from flow360.component.results import base_results, case_results from flow360.component.simulation.framework.base_model import ( Conflicts, diff --git a/flow360/plugins/report/uvf_shutter.py b/flow360/plugins/report/uvf_shutter.py index 4e7a8a6e5..d754e818f 100644 --- a/flow360/plugins/report/uvf_shutter.py +++ b/flow360/plugins/report/uvf_shutter.py @@ -15,8 +15,8 @@ import pydantic as pd -from flow360 import Env from flow360.component.simulation.framework.base_model import Flow360BaseModel +from flow360.environment import Env from flow360.exceptions import ( Flow360RuntimeError, Flow360WebError, diff --git a/tests/data/mock_webapi/report_meta_resp.json b/tests/data/mock_webapi/report_meta_resp.json new file mode 100644 index 000000000..9b97ddd21 --- /dev/null +++ b/tests/data/mock_webapi/report_meta_resp.json @@ -0,0 +1,17 @@ +{ + "data": { + "userId": "AIDAU77I6BZ2QYZLLVSRW", + "id": "rep-00508a80-2566-45bf-9572-56228a3161fd", + "status": "submitted", + "name": "ResultSummary", + "resources": [ + { + "type": "Case", + "id": "case-f813742a-61d3-497c-8447-3ac8b0c997f1", + "rawData": null + } + ], + "createdAt": "2025-05-19T18:30:51.476347Z", + "updatedAt": "2025-05-19T18:30:51.476347Z" + } +} \ No newline at end of file diff --git a/tests/mock_server.py b/tests/mock_server.py index b7393e757..8d5792947 100644 --- a/tests/mock_server.py +++ b/tests/mock_server.py @@ -526,6 +526,16 @@ def json(self): return res +class MockResponseReportSubmit(MockResponse): + """response for report_template.create_in_cloud's meta json""" + + @staticmethod + def json(): + with open(os.path.join(here, "data/mock_webapi/report_meta_resp.json")) as fh: + res = json.load(fh) + return res + + GET_RESPONSE_MAP = { "/volumemeshes/00112233-4455-6677-8899-aabbccddeeff": MockResponseVolumeMesh, "/volumemeshes/00000000-0000-0000-0000-000000000000": MockResponseVolumeMesh, @@ -574,6 +584,7 @@ def json(self): "/v2/drafts/vm-7c3681cd-8c6c-4db7-a62c-1742d825e9d3/run": MockResponseProjectVolumeMesh, "/v2/drafts/case-84d4604e-f3cd-4c6b-8517-92a80a3346d3/simulation/file": MockResponseProjectCaseForkSimConfig, "/v2/drafts/case-84d4604e-f3cd-4c6b-8517-92a80a3346d3/run": MockResponseProjectCaseFork, + "/v2/report": MockResponseReportSubmit, } diff --git a/tests/simulation/service/ref/default_report_config.json b/tests/simulation/service/ref/default_report_config.json new file mode 100644 index 000000000..f4e61d6ba --- /dev/null +++ b/tests/simulation/service/ref/default_report_config.json @@ -0,0 +1,112 @@ +{ + "items": [ + { + "data": [ + "volume_mesh/bounding_box/length", + "volume_mesh/bounding_box/height", + "volume_mesh/bounding_box/width", + "params/reference_geometry/moment_length", + "params/reference_geometry/area", + { + "data": "surface_forces/totalCL", + "title": "CL", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + }, + { + "data": "surface_forces/totalCD", + "title": "CD", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + }, + { + "data": "surface_forces/totalCFy", + "title": "CFy", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + }, + { + "data": "surface_forces/totalCMx", + "title": "CMx", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + }, + { + "data": "surface_forces/totalCMy", + "title": "CMy", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + }, + { + "data": "surface_forces/totalCMz", + "title": "CMz", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + } + ], + "section_title": "result_summary", + "headers": [ + "OAL", + "OAH", + "OAW", + "Reference Length", + "Reference Area", + "CL", + "CD", + "CFy", + "CMx", + "CMy", + "CMz" + ], + "type_name": "Table", + "formatter": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + } + ], + "include_case_by_case": false, + "settings": { + "dpi": 300, + "dump_table_csv": true + } +} \ No newline at end of file diff --git a/tests/simulation/service/test_services_v2.py b/tests/simulation/service/test_services_v2.py index 861eb2363..6e5c0e3b5 100644 --- a/tests/simulation/service/test_services_v2.py +++ b/tests/simulation/service/test_services_v2.py @@ -1258,3 +1258,10 @@ def _get_all_units(value): f"Unit {unit_system_dimension_string} (A.K.A {field_name}) is not supported by the front-end.", "Please ensure front end team is aware of this new unit and add its support.", ) + + +def test_get_default_report_config_json(): + report_config_dict = services.get_default_report_config() + with open("ref/default_report_config.json", "r") as fp: + ref_dict = json.load(fp) + assert compare_values(report_config_dict, ref_dict, ignore_keys=["formatter"])