From d4b9077d339bf8574099f07e4b45f3470d78bc96 Mon Sep 17 00:00:00 2001 From: Ben <106089368+benflexcompute@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:00:03 -0500 Subject: [PATCH] =?UTF-8?q?Revert=20"refactor():=20Moved=20implementation?= =?UTF-8?q?=20of=20the=20report=20functionality=20out=20of=20F=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d103a560d9731d1357d2ee80097dfaa17c4f86e2. --- .../report/automotive_report.py | 424 + .../report/geometry_to_report.py | 194 + .../report/sweep_launch_report.py | 336 + .../report/sweep_launch_template.py | 289 + .../special_features/bet_disk.py | 35 +- .../special_features/heat_transfer.py | 69 +- .../notebooks/notebook_automotive.ipynb | 333 +- flow360/__init__.py | 2 + flow360/cloud/flow360_requests.py | 14 + flow360/component/project.py | 7 + flow360/component/simulation/services.py | 14 + flow360/plugins/__init__.py | 22 - flow360/plugins/report/README.md | 311 + flow360/plugins/report/__init__.py | 69 + .../plugins/report/fonts/TWKEverett-Bold.otf | Bin 0 -> 178056 bytes .../report/fonts/TWKEverett-BoldItalic.otf | Bin 0 -> 182960 bytes .../report/fonts/TWKEverett-Regular.otf | Bin 0 -> 150540 bytes .../report/fonts/TWKEverett-RegularItalic.otf | Bin 0 -> 155060 bytes flow360/plugins/report/img/FC_logo.pdf | Bin 0 -> 2127 bytes flow360/plugins/report/img/Flow360-logo.svg | 157 + flow360/plugins/report/img/Flow360_logo.pdf | Bin 0 -> 16430 bytes flow360/plugins/report/img/background.pdf | Bin 0 -> 94114 bytes .../img/colorbar_rainbow_banded_25.jpeg | Bin 0 -> 1303 bytes .../report/img/colorbar_rainbow_banded_30.png | Bin 0 -> 100 bytes .../report/img/colorbar_rainbow_smooth.png | Bin 0 -> 99 bytes flow360/plugins/report/img/cover_logo.pdf | Bin 0 -> 17283 bytes flow360/plugins/report/img/flow360.png | Bin 0 -> 7245 bytes .../plugins/report/img/flow360_logo_grey.pdf | Bin 0 -> 3522 bytes flow360/plugins/report/report.py | 365 + flow360/plugins/report/report_context.py | 31 + flow360/plugins/report/report_doc.py | 247 + flow360/plugins/report/report_items.py | 2184 ++++ flow360/plugins/report/utils.py | 1216 ++ flow360/plugins/report/uvf_shutter.py | 964 ++ poetry.lock | 58 +- pyproject.toml | 1 + .../results/nonlinear_residual_v2.csv | 202 + .../results/surface_forces_v2.csv | 202 + .../results/total_forces_v2.csv | 202 + .../simulation.json | 763 ++ .../results/nonlinear_residual_v2.csv | 10001 ++++++++++++++++ .../results/surface_forces_v2.csv | 10001 ++++++++++++++++ .../results/surface_heat_transfer_v2.csv | 4001 +++++++ .../results/total_acoustics_v3.csv | 2001 ++++ .../results/total_forces_v2.csv | 10001 ++++++++++++++++ .../simulation.json | 474 + .../results/nonlinear_residual_v2.csv | 10001 ++++++++++++++++ .../results/surface_forces_v2.csv | 10001 ++++++++++++++++ .../results/total_forces_v2.csv | 10001 ++++++++++++++++ .../simulation.json | 475 + .../results/monitor_massFluxExhaust_v2.csv | 202 + .../results/monitor_massFluxIntake_v2.csv | 202 + .../udd_massInflowController_Exhaust_v2.csv | 202 + .../udd_massOutflowController_Intake_v2.csv | 202 + .../simulation.json | 1391 +++ .../meshBoundaryBoundingBox.json | 98 + .../meshStats.json | 10 + .../simulation.json | 371 + .../meshBoundaryBoundingBox.json | 34 + .../meshStats.json | 10 + .../simulation.json | 342 + .../simulation.json | 1391 +++ tests/report/report_testing_fixtures.py | 397 + tests/report/test_report.py | 129 + tests/report/test_report_items.py | 1869 +++ tests/report/test_report_utils.py | 112 + tests/report/test_shutter.py | 520 + .../service/ref/default_report_config.json | 112 + tests/simulation/service/test_services_v2.py | 7 + tests/utils.py | 19 + 70 files changed, 83255 insertions(+), 33 deletions(-) create mode 100644 examples/post_processing/report/automotive_report.py create mode 100644 examples/post_processing/report/geometry_to_report.py create mode 100644 examples/post_processing/report/sweep_launch_report.py create mode 100644 examples/post_processing/report/sweep_launch_template.py delete mode 100644 flow360/plugins/__init__.py create mode 100644 flow360/plugins/report/README.md create mode 100644 flow360/plugins/report/__init__.py create mode 100644 flow360/plugins/report/fonts/TWKEverett-Bold.otf create mode 100644 flow360/plugins/report/fonts/TWKEverett-BoldItalic.otf create mode 100644 flow360/plugins/report/fonts/TWKEverett-Regular.otf create mode 100644 flow360/plugins/report/fonts/TWKEverett-RegularItalic.otf create mode 100644 flow360/plugins/report/img/FC_logo.pdf create mode 100644 flow360/plugins/report/img/Flow360-logo.svg create mode 100644 flow360/plugins/report/img/Flow360_logo.pdf create mode 100644 flow360/plugins/report/img/background.pdf create mode 100644 flow360/plugins/report/img/colorbar_rainbow_banded_25.jpeg create mode 100644 flow360/plugins/report/img/colorbar_rainbow_banded_30.png create mode 100644 flow360/plugins/report/img/colorbar_rainbow_smooth.png create mode 100644 flow360/plugins/report/img/cover_logo.pdf create mode 100644 flow360/plugins/report/img/flow360.png create mode 100644 flow360/plugins/report/img/flow360_logo_grey.pdf create mode 100644 flow360/plugins/report/report.py create mode 100644 flow360/plugins/report/report_context.py create mode 100644 flow360/plugins/report/report_doc.py create mode 100644 flow360/plugins/report/report_items.py create mode 100644 flow360/plugins/report/utils.py create mode 100644 flow360/plugins/report/uvf_shutter.py create mode 100644 tests/data/case-333333333-333333-3333333333-33333333/results/nonlinear_residual_v2.csv create mode 100644 tests/data/case-333333333-333333-3333333333-33333333/results/surface_forces_v2.csv create mode 100644 tests/data/case-333333333-333333-3333333333-33333333/results/total_forces_v2.csv create mode 100644 tests/data/case-333333333-333333-3333333333-33333333/simulation.json create mode 100644 tests/data/case-444444444-444444-4444444444-44444444/results/nonlinear_residual_v2.csv create mode 100644 tests/data/case-444444444-444444-4444444444-44444444/results/surface_forces_v2.csv create mode 100644 tests/data/case-444444444-444444-4444444444-44444444/results/surface_heat_transfer_v2.csv create mode 100644 tests/data/case-444444444-444444-4444444444-44444444/results/total_acoustics_v3.csv create mode 100644 tests/data/case-444444444-444444-4444444444-44444444/results/total_forces_v2.csv create mode 100644 tests/data/case-444444444-444444-4444444444-44444444/simulation.json create mode 100644 tests/data/case-5555-5555555-5555555555-555555555555/results/nonlinear_residual_v2.csv create mode 100644 tests/data/case-5555-5555555-5555555555-555555555555/results/surface_forces_v2.csv create mode 100644 tests/data/case-5555-5555555-5555555555-555555555555/results/total_forces_v2.csv create mode 100644 tests/data/case-5555-5555555-5555555555-555555555555/simulation.json create mode 100644 tests/data/case-666666666-66666666-666-6666666666666/results/monitor_massFluxExhaust_v2.csv create mode 100644 tests/data/case-666666666-66666666-666-6666666666666/results/monitor_massFluxIntake_v2.csv create mode 100644 tests/data/case-666666666-66666666-666-6666666666666/results/udd_massInflowController_Exhaust_v2.csv create mode 100644 tests/data/case-666666666-66666666-666-6666666666666/results/udd_massOutflowController_Intake_v2.csv create mode 100644 tests/data/case-666666666-66666666-666-6666666666666/simulation.json create mode 100644 tests/data/vm-11111111-1111-1111-1111-111111111111/meshBoundaryBoundingBox.json create mode 100644 tests/data/vm-11111111-1111-1111-1111-111111111111/meshStats.json create mode 100644 tests/data/vm-11111111-1111-1111-1111-111111111111/simulation.json create mode 100644 tests/data/vm-22222222-22222222-2222-2222-22222222/meshBoundaryBoundingBox.json create mode 100644 tests/data/vm-22222222-22222222-2222-2222-22222222/meshStats.json create mode 100644 tests/data/vm-22222222-22222222-2222-2222-22222222/simulation.json create mode 100644 tests/data/vm-33333333-33333-3333333-333333333333/simulation.json create mode 100644 tests/report/report_testing_fixtures.py create mode 100644 tests/report/test_report.py create mode 100644 tests/report/test_report_items.py create mode 100644 tests/report/test_report_utils.py create mode 100644 tests/report/test_shutter.py create mode 100644 tests/simulation/service/ref/default_report_config.json diff --git a/examples/post_processing/report/automotive_report.py b/examples/post_processing/report/automotive_report.py new file mode 100644 index 000000000..f95b66149 --- /dev/null +++ b/examples/post_processing/report/automotive_report.py @@ -0,0 +1,424 @@ +import flow360 as fl +from flow360 import u +from flow360.examples import DrivAer +from flow360.log import log +from flow360.plugins.report.report import ReportTemplate +from flow360.plugins.report.report_items import ( + BottomCamera, + Chart2D, + Chart3D, + FrontCamera, + FrontLeftBottomCamera, + FrontLeftTopCamera, + Inputs, + LeftCamera, + RearCamera, + RearLeftTopCamera, + RearRightBottomCamera, + Settings, + Summary, + Table, + TopCamera, +) +from flow360.plugins.report.utils import Average, DataItem, Delta, Expression, Variable + +DrivAer.get_files() + +project = fl.Project.from_volume_mesh( + DrivAer.mesh_filename, + name="Automotive DrivAer", +) + +vm = project.volume_mesh + +log.info("Volume mesh contains the following boundaries:") +for boundary in vm.boundary_names: + log.info("Boundary: " + boundary) + +freestream_surfaces = ["blk-1/WT_side1", "blk-1/WT_side2", "blk-1/WT_inlet", "blk-1/WT_outlet"] +slip_wall_surfaces = ["blk-1/WT_ceiling", "blk-1/WT_ground_front", "blk-1/WT_ground"] +wall_surfaces = list(set(vm.boundary_names) - set(freestream_surfaces) - set(slip_wall_surfaces)) + +cases = [] + +for beta in [0, 5, 10]: + with fl.SI_unit_system: + params = fl.SimulationParams( + meshing=None, + reference_geometry=fl.ReferenceGeometry(area=2.17, moment_length=2.7862), + operating_condition=fl.AerospaceCondition(velocity_magnitude=40, beta=beta * u.deg), + models=[ + fl.Wall(surfaces=[vm[i] for i in wall_surfaces], use_wall_function=True), + fl.Freestream( + surfaces=[vm[i] for i in freestream_surfaces], + ), + fl.SlipWall( + surfaces=[vm[i] for i in slip_wall_surfaces], + ), + ], + user_defined_fields=[ + fl.UserDefinedField( + name="Cpx", + expression="double prel = primitiveVars[4] - pressureFreestream;" + + "double PressureForce_X = prel * nodeNormals[0]; " + + "Cpx = PressureForce_X / (0.5 * MachRef * MachRef) / magnitude(nodeNormals);", + ), + ], + outputs=[ + fl.SurfaceOutput( + surfaces=vm["*"], + output_fields=[ + "Cp", + "Cf", + "yPlus", + "CfVec", + "primitiveVars", + "wall_shear_stress_magnitude", + "Cpx", + ], + ), + fl.SliceOutput( + entities=[ + *[ + fl.Slice( + name=f"slice_y_{name}", + normal=(0, 1, 0), + origin=(0, y, 0), + ) + for name, y in zip( + ["0", "0_2", "0_4", "0_6", "0_8"], [0, 0.2, 0.4, 0.6, 0.8] + ) + ], + *[ + fl.Slice( + name=f"slice_z_{name}", + normal=(0, 0, 1), + origin=(0, 0, z), + ) + for name, z in zip( + ["neg0_2", "0", "0_2", "0_4", "0_6", "0_8"], + [-0.2, 0, 0.2, 0.4, 0.6, 0.8], + ) + ], + ], + output_fields=["velocity", "velocity_x", "velocity_y", "velocity_z"], + ), + fl.IsosurfaceOutput( + output_fields=["Cp", "Mach"], + isosurfaces=[ + fl.Isosurface( + name="isosurface-cpt", + iso_value=-1, + field="Cpt", + ), + ], + ), + fl.ProbeOutput( + entities=[fl.Point(name="point1", location=(10, 0, 1))], + output_fields=["velocity"], + ), + ], + ) + + case_new = project.run_case(params=params, name=f"DrivAer 5.7M - beta={beta}") + + cases.append(case_new) + +# wait until all cases finish running +for case in cases: + case.wait() + +exclude = ["blk-1/WT_ground_close", "blk-1/WT_ground_patch"] +size = "5.7M" + +exclude += freestream_surfaces + slip_wall_surfaces + +top_camera = TopCamera(pan_target=(1.5, 0, 0), dimension=5, dimension_dir="width") +top_camera_slice = TopCamera(pan_target=(2.5, 0, 0), dimension=8, dimension_dir="width") +side_camera = LeftCamera(pan_target=(1.5, 0, 0), dimension=5, dimension_dir="width") +side_camera_slice = LeftCamera(pan_target=(2.5, 0, 1.5), dimension=8, dimension_dir="width") +rear_camera = RearCamera(dimension=2.5, dimension_dir="width") +front_camera = FrontCamera(dimension=2.5, dimension_dir="width") +bottom_camera = BottomCamera(pan_target=(1.5, 0, 0), dimension=5, dimension_dir="width") +front_left_bottom_camera = FrontLeftBottomCamera( + pan_target=(1.5, 0, 0), dimension=5, dimension_dir="width" +) +rear_right_bottom_camera = RearRightBottomCamera( + pan_target=(1.5, 0, 0), dimension=6, dimension_dir="width" +) +front_left_top_camera = FrontLeftTopCamera( + pan_target=(1.5, 0, 0), dimension=6, dimension_dir="width" +) +rear_left_top_camera = RearLeftTopCamera(pan_target=(1.5, 0, 0), dimension=6, dimension_dir="width") + +cameras_geo = [ + top_camera, + side_camera, + rear_camera, + bottom_camera, + front_left_bottom_camera, + rear_right_bottom_camera, +] + +limits_cp = [(-1, 1), (-1, 1), (-1, 1), (-0.3, 0), (-0.3, 0), (-1, 1), (-1, 1), (-1, 1)] +cameras_cp = [ + front_camera, + front_left_top_camera, + side_camera, + rear_left_top_camera, + rear_camera, + bottom_camera, + front_left_bottom_camera, + rear_right_bottom_camera, +] + +avg = Average(fraction=0.1) +CD = DataItem(data="surface_forces/totalCD", exclude=exclude, title="CD", operations=avg) + +CL = DataItem(data="surface_forces/totalCL", exclude=exclude, title="CL", operations=avg) + +CDA = DataItem( + data="surface_forces", + exclude=exclude, + title="CD*area", + variables=[Variable(name="area", data="params.reference_geometry.area")], + operations=[Expression(expr="totalCD * area"), avg], +) + +CLf = DataItem( + data="surface_forces", + exclude=exclude, + title="CLf", + operations=[Expression(expr="1/2*totalCL + totalCMy"), avg], +) + +CLr = DataItem( + data="surface_forces", + exclude=exclude, + title="CLr", + operations=[Expression(expr="1/2*totalCL - totalCMy"), avg], +) + +CFy = DataItem(data="surface_forces/totalCFy", exclude=exclude, title="CS", operations=avg) + +statistical_data = [ + "params/reference_geometry/area", + CD, + CDA, + Delta(data=CD), + CL, + CLf, + CLr, + CFy, + "volume_mesh/stats/n_nodes", + "params/time_stepping/max_steps", +] +statistical_table = Table( + data=statistical_data, + section_title="Statistical data", + formatter=[ + ( + None + if d + in [ + "params/reference_geometry/area", + "volume_mesh/stats/n_nodes", + "params/time_stepping/max_steps", + ] + else ".4f" + ) + for d in statistical_data + ], +) + +geometry_screenshots = [ + Chart3D( + section_title="Geometry", + items_in_row=2, + force_new_page=True, + show="boundaries", + camera=camera, + exclude=exclude, + fig_name=f"geo_{i}", + ) + for i, camera in enumerate(cameras_geo) +] +cpt_screenshots = [ + Chart3D( + section_title="Isosurface, Cpt=-1", + items_in_row=2, + force_new_page=True, + show="isosurface", + iso_field="Cpt", + exclude=exclude, + camera=camera, + ) + for camera in cameras_cp +] +cfvec_screenshots = [ + Chart3D( + section_title="CfVec", + items_in_row=2, + force_new_page=True, + show="boundaries", + field="CfVec", + mode="lic", + limits=(1e-4, 10), + is_log_scale=True, + exclude=exclude, + camera=camera, + ) + for camera in cameras_cp +] +y_slices_lic_screenshots = [ + Chart3D( + section_title=f"Slice velocity LIC y={y}", + items_in_row=2, + force_new_page=True, + show="slices", + include=[f"slice_y_{name}"], + field="velocityVec", + mode="lic", + limits=(0 * u.m / u.s, 50 * u.m / u.s), + camera=side_camera_slice, + fig_name=f"slice_y_vec_{name}", + ) + for name, y in zip(["0", "0_2", "0_4", "0_6", "0_8"], [0, 0.2, 0.4, 0.6, 0.8]) +] +y_slices_screenshots = [ + Chart3D( + section_title=f"Slice velocity y={y}", + items_in_row=2, + force_new_page=True, + show="slices", + include=[f"slice_y_{name}"], + field="velocity", + limits=(0 * u.m / u.s, 50 * u.m / u.s), + camera=side_camera_slice, + fig_name=f"slice_y_{name}", + ) + for name, y in zip(["0", "0_2", "0_4", "0_6", "0_8"], [0, 0.2, 0.4, 0.6, 0.8]) +] +y_slices_lic_screenshots = [ + Chart3D( + section_title=f"Slice velocity LIC y={y}", + items_in_row=2, + force_new_page=True, + show="slices", + include=[f"slice_y_{name}"], + field="velocityVec", + mode="lic", + limits=(0 * u.m / u.s, 50 * u.m / u.s), + camera=side_camera_slice, + fig_name=f"slice_y_vec_{name}", + ) + for name, y in zip(["0", "0_2", "0_4", "0_6", "0_8"], [0, 0.2, 0.4, 0.6, 0.8]) +] +z_slices_screenshots = [ + Chart3D( + section_title=f"Slice velocity z={z}", + items_in_row=2, + force_new_page=True, + show="slices", + include=[f"slice_z_{name}"], + field="velocity", + limits=(0 * u.m / u.s, 50 * u.m / u.s), + camera=top_camera_slice, + fig_name=f"slice_z_{name}", + ) + for name, z in zip(["neg0_2", "0", "0_2", "0_4", "0_6", "0_8"], [-0.2, 0, 0.2, 0.4, 0.6, 0.8]) +] +y_plus_screenshots = [ + Chart3D( + section_title="y+", + items_in_row=2, + show="boundaries", + field="yPlus", + exclude=exclude, + limits=(0, 5), + camera=camera, + fig_name=f"yplus_{i}", + ) + for i, camera in enumerate([top_camera, bottom_camera]) +] +cp_screenshots = [ + Chart3D( + section_title="Cp", + items_in_row=2, + show="boundaries", + field="Cp", + exclude=exclude, + limits=limits, + camera=camera, + fig_name=f"cp_{i}", + ) + for i, (limits, camera) in enumerate(zip(limits_cp, cameras_cp)) +] +cpx_screenshots = [ + Chart3D( + section_title="Cpx", + items_in_row=2, + show="boundaries", + field="Cpx", + exclude=exclude, + limits=(-0.3, 0.3), + camera=camera, + fig_name=f"cpx_{i}", + ) + for i, camera in enumerate(cameras_cp) +] +wall_shear_screenshots = [ + Chart3D( + section_title="Wall shear stress magnitude", + items_in_row=2, + show="boundaries", + field="wallShearMag", + exclude=exclude, + limits=(0 * u.Pa, 5 * u.Pa), + camera=camera, + fig_name=f"wallShearMag_{i}", + ) + for i, camera in enumerate(cameras_cp) +] + +report = ReportTemplate( + title="Aerodynamic analysis of DrivAer", + items=[ + Summary(), + Inputs(), + statistical_table, + Chart2D( + x="x_slicing_force_distribution/X", + y="x_slicing_force_distribution/totalCumulative_CD_Curve", + fig_name="totalCumulative_CD_Curve", + background="geometry", + exclude=exclude, + ), + Chart2D( + x="surface_forces/pseudo_step", + y="surface_forces/totalCD", + section_title="Drag Coefficient", + fig_name="cd_fig", + exclude=exclude, + focus_x=(1 / 3, 1), + ), + *geometry_screenshots, + *cp_screenshots, + *cpx_screenshots, + *cpt_screenshots, + *y_slices_screenshots, + *y_slices_lic_screenshots, + *z_slices_screenshots, + *y_plus_screenshots, + *wall_shear_screenshots, + ], + settings=Settings(dpi=150), +) + +report = report.create_in_cloud( + f"{size}-{len(cases)}cases-slices-using-groups-Cpt, Cpx, wallShear, dpi=default", + cases, +) + +report.wait() +report.download("report.pdf") diff --git a/examples/post_processing/report/geometry_to_report.py b/examples/post_processing/report/geometry_to_report.py new file mode 100644 index 000000000..8176d1d33 --- /dev/null +++ b/examples/post_processing/report/geometry_to_report.py @@ -0,0 +1,194 @@ +import flow360 as fl +from flow360 import u +from flow360.examples import Airplane +from flow360.plugins.report.report import ReportTemplate +from flow360.plugins.report.report_items import ( + Camera, + Chart2D, + Chart3D, + Inputs, + Settings, + Summary, + Table, +) +from flow360.plugins.report.utils import Average, DataItem, Delta + +project_id = None # if running for the first time + +# then replace it with your project ID to avoid re-creation of projects. You can find project ID on web GUI: +# project_id = "prj-...." + +if project_id is not None: + project = fl.Project.from_cloud(project_id) +else: + project = fl.Project.from_geometry( + Airplane.geometry, name="Python Project (Geometry, from file) - for Report" + ) + +geo = project.geometry +geo.group_faces_by_tag("groupName") + + +def simulation_params(angle_of_attack): + with fl.SI_unit_system: + far_field_zone = fl.AutomatedFarfield() + params = fl.SimulationParams( + meshing=fl.MeshingParams( + defaults=fl.MeshingDefaults( + boundary_layer_first_layer_thickness=0.001, + surface_max_edge_length=1, + ), + volume_zones=[far_field_zone], + ), + reference_geometry=fl.ReferenceGeometry(area=1, moment_length=1), + operating_condition=fl.AerospaceCondition( + velocity_magnitude=100, + alpha=angle_of_attack * fl.u.deg, + ), + time_stepping=fl.Steady(max_steps=1000), + models=[ + fl.Wall( + surfaces=[geo["*"]], + ), + fl.Freestream( + surfaces=[far_field_zone.farfield], + ), + ], + outputs=[ + fl.SurfaceOutput( + surfaces=geo["*"], + output_fields=[ + "Cp", + "Cf", + "yPlus", + "CfVec", + "primitiveVars", + ], + ), + ], + ) + return params + + +cases: list[fl.Case] = [] +for alpha in [0, 2, 4]: + case = project.run_case(params=simulation_params(alpha), name=f"Case for report, alpha={alpha}") + cases.append(case) + +[print(case.short_description()) for case in cases] + +# waiting explicitly for all the cases to finish (report pipeline will not wait for cases) +[case.wait() for case in cases] + + +top_camera = Camera( + position=(0, 0, 1), + look_at=(0, 0, 0), + pan_target=(5, 0, 0), + up=(0, 1, 0), + dimension=15, + dimension_dir="width", +) +side_camera = Camera( + position=(0, -1, 0), + look_at=(0, 0, 0), + pan_target=(5, 0, 0), + up=(0, 0, 1), + dimension=12, + dimension_dir="width", +) +front_left_bottom_camera = Camera( + position=(-1, -1, -1), + look_at=(0, 0, 0), + pan_target=(4, 0, 0), + up=(0, 0, 1), + dimension=15, + dimension_dir="width", +) +rear_right_bottom_camera = Camera( + position=(1, 1, -1), + look_at=(0, 0, 0), + pan_target=(4, 0, 0), + up=(0, 0, 1), + dimension=15, + dimension_dir="width", +) + +cameras_geo = [ + top_camera, + side_camera, + front_left_bottom_camera, + rear_right_bottom_camera, +] + +avg = Average(fraction=0.1) + +CL = DataItem(data="surface_forces/totalCL", title="CL", operations=avg) + +CD = DataItem(data="surface_forces/totalCD", title="CD", operations=avg) + +statistical_data = [ + "params/operating_condition/alpha", + "params/reference_geometry/area", + CL, + Delta(data=CL), + CD, + "volume_mesh/stats/n_nodes", + "params/time_stepping/max_steps", +] +statistical_table = Table( + data=statistical_data, + section_title="Statistical data", + formatter=[ + ( + None + if d + in [ + "params/reference_geometry/area", + "volume_mesh/stats/n_nodes", + "params/time_stepping/max_steps", + ] + else ".4f" + ) + for d in statistical_data + ], +) + +geometry_screenshots = [ + Chart3D( + section_title="Geometry", + items_in_row=2, + force_new_page=True, + show="boundaries", + camera=camera, + fig_name=f"geo_{i}", + ) + for i, camera in enumerate(cameras_geo) +] + +report = ReportTemplate( + title="Geometry to report", + items=[ + Summary(), + Inputs(), + statistical_table, + Chart2D( + x="total_forces/pseudo_step", + y="total_forces/CL", + section_title="Lift Coefficient", + fig_name="cl_fig", + focus_x=(1 / 3, 1), + ), + *geometry_screenshots, + ], + settings=Settings(dpi=150), +) + + +report = report.create_in_cloud( + f"Geometry to report - Report, dpi=150", + cases, +) + +report.wait() +report.download("report.pdf") diff --git a/examples/post_processing/report/sweep_launch_report.py b/examples/post_processing/report/sweep_launch_report.py new file mode 100644 index 000000000..cc32a1919 --- /dev/null +++ b/examples/post_processing/report/sweep_launch_report.py @@ -0,0 +1,336 @@ +"""This script is used by sweep_launch_template.py to create a report.""" + +import pandas as pd + +import flow360 as fl +from flow360.plugins.report.report import ReportTemplate +from flow360.plugins.report.report_items import ( + BottomCamera, + Camera, + Chart2D, + Chart3D, + FrontCamera, + FrontLeftTopCamera, + Inputs, + LeftCamera, + NonlinearResiduals, + PatternCaption, + RearCamera, + RearRightBottomCamera, + Settings, + SubsetLimit, + Summary, + Table, + TopCamera, +) +from flow360.plugins.report.utils import Average, DataItem + + +def main(): + + csv_path = "PATH/TO/CSV/FILE.csv" # Replace with the actual path to your CSV file generated by sweep_launch_template.py + + generate_report( + *csv_reader(csv_path), + include_geometry=True, + include_general_tables=True, + include_residuals=True, + include_cfl=True, + include_forces_moments_table=True, + include_forces_moments_charts=True, + include_forces_moments_alpha_charts=True, + include_forces_moments_beta_charts=True, + include_cf_vec=True, + include_cp=True, + include_yplus=True, + include_qcriterion=True, + ) + + +def csv_reader( + file_path, +): + csv_case_specific = pd.read_csv(file_path, sep=",", skiprows=2) + + case_list = [] + + for case_id in csv_case_specific["case_id"]: + case = fl.Case(case_id) + case_list.append(case) + + params = case_list[0].params + + return case_list, params + + +def generate_report( + cases, + params, + include_geometry: bool = False, + include_general_tables: bool = False, + include_residuals: bool = False, + include_cfl: bool = False, + include_forces_moments_table: bool = False, + include_forces_moments_charts: bool = False, + include_forces_moments_alpha_charts: bool = False, + include_forces_moments_beta_charts: bool = False, + include_cf_vec: bool = False, + include_cp: bool = False, + include_yplus: bool = False, + include_qcriterion: bool = False, +): + items = [] + + freestream_surfaces = ["fluid/farfield"] + + exclude = freestream_surfaces + + top_camera = TopCamera(pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="height") + bottom_camera = BottomCamera(pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="height") + front_camera = FrontCamera(pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width") + rear_camera = RearCamera(pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width") + left_camera = LeftCamera(pan_target=(3.5, 0, -0.5), dimension=10, dimension_dir="width") + right_camera = Camera( + pan_target=(3.5, 0, -0.5), + position=(0.0, -1.0, 0.0), + look_at=(0.0, 0.0, 0.0), + up=(0.0, 0.0, 1.0), + dimension=10, + dimension_dir="width", + ) + front_left_top_camera = FrontLeftTopCamera( + pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width" + ) + rear_right_bottom_camera = RearRightBottomCamera( + pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width" + ) + + if params.time_stepping.type_name == "Unsteady": + step_type = "physical_step" + else: + step_type = "pseudo_step" + + geo_cameras = [ + top_camera, + bottom_camera, + front_camera, + rear_camera, + left_camera, + right_camera, + front_left_top_camera, + rear_right_bottom_camera, + ] + + geo_camera_names = [ + "top_camera", + "bottom_camera", + "front_camera", + "rear_camera", + "left_camera", + "right_camera", + "front_left_top_camera", + "rear_right_bottom_camera", + ] + + avg = Average(fraction=0.1) + + force_list = [ + "CD", + "CL", + "CFx", + "CFy", + "CFz", + "CMx", + "CMy", + "CMz", + ] + + for model in params.models: + if model.type == "Fluid": + turbulence_solver = model.turbulence_model_solver.type_name + + CD = DataItem(data="surface_forces/totalCD", exclude=exclude, title="CD", operations=avg) + CL = DataItem(data="surface_forces/totalCL", exclude=exclude, title="CL", operations=avg) + CFX = DataItem(data="surface_forces/totalCFx", exclude=exclude, title="CFx", operations=avg) + CFY = DataItem(data="surface_forces/totalCFy", exclude=exclude, title="CFy", operations=avg) + CFZ = DataItem(data="surface_forces/totalCFz", exclude=exclude, title="CFz", operations=avg) + CMX = DataItem(data="surface_forces/totalCMx", exclude=exclude, title="CMx", operations=avg) + CMY = DataItem(data="surface_forces/totalCMy", exclude=exclude, title="CMy", operations=avg) + CMZ = DataItem(data="surface_forces/totalCMz", exclude=exclude, title="CMz", operations=avg) + + table_data = [ + CD, + CL, + CFX, + CFY, + CFZ, + CMX, + CMY, + CMZ, + ] + + if include_geometry: + geometry_screenshots = [ + Chart3D( + section_title="Geometry", + items_in_row=2, + force_new_page=True, + show="boundaries", + camera=front_left_top_camera, + exclude=exclude, + fig_name="Geometry_view", + ) + ] + items.extend(geometry_screenshots) + + if include_general_tables: + items.append(Summary()) + items.append(Inputs()) + + if include_forces_moments_table: + table = Table( + data=table_data, + section_title="Quantities of interest", + ) + items.append(table) + + if include_residuals: + residual_chart = NonlinearResiduals( + force_new_page=True, section_title="Nonlinear residuals", fig_name=f"nonlin-res_fig" + ) + items.append(residual_chart) + + if include_cfl and params.time_stepping.CFL.type == "adaptive": + cfl_chart = Chart2D( + x=f"cfl/{step_type}", + y=["cfl/0_NavierStokes_cfl", f"cfl/1_{turbulence_solver}_cfl"], + force_new_page=True, + section_title="CFL", + fig_name="cfl_fig", + y_log=True, + ) + items.append(cfl_chart) + + if include_forces_moments_charts: + force_charts = [ + Chart2D( + x=f"surface_forces/{step_type}", + y=f"surface_forces/total{force}", + force_new_page=True, + section_title="Forces/Moments", + fig_name=f"{force}_fig", + exclude=exclude, + ylim=SubsetLimit(subset=(0.5, 1), offset=0.25), + ) + for force in force_list + ] + items.extend(force_charts) + + if include_forces_moments_alpha_charts: + force_alpha_charts = [ + Chart2D( + x=f"params/operating_condition/alpha", + y=f"total_forces/averages/{force}", + force_new_page=True, + section_title="Averaged Forces/Moments against alpha", + fig_name=f"{force}_alpha_fig", + ) + for force in force_list + ] + items.extend(force_alpha_charts) + + if include_forces_moments_beta_charts: + force_beta_charts = [ + Chart2D( + x=f"params/operating_condition/beta", + y=f"total_forces/averages/{force}", + force_new_page=True, + section_title="Averaged Forces/Moments against beta", + fig_name=f"{force}_beta_fig", + ) + for force in force_list + ] + items.extend(force_beta_charts) + + if include_yplus: + y_plus_screenshots = [ + Chart3D( + caption=PatternCaption(pattern=f"y+_{camera_name}_[case.name]"), + show="boundaries", + field="yPlus", + exclude=exclude, + limits=(0, 5), + camera=camera, + fig_name=f"yplus_{camera_name}_fig", + fig_size=1, + ) + for camera_name, camera in zip(geo_camera_names, geo_cameras) + ] + items.extend(y_plus_screenshots) + + if include_cp: + cp_screenshots = [ + Chart3D( + caption=PatternCaption(pattern=f"Cp_{camera_name}_[case.name]"), + show="boundaries", + field="Cp", + exclude=exclude, + limits=(-1, 1), + camera=camera, + fig_name=f"cp_{camera_name}_fig", + fig_size=1, + ) + for camera_name, camera in zip(geo_camera_names, geo_cameras) + ] + items.extend(cp_screenshots) + + if include_cf_vec: + cfvec_screenshots = [ + Chart3D( + caption=PatternCaption(pattern=f"Cf_vec_{camera_name}_[case.name]"), + show="boundaries", + field="CfVec", + mode="lic", + exclude=exclude, + limits=(0, 0.025), + camera=camera, + fig_name=f"cfvec_{camera_name}_fig", + fig_size=1, + ) + for camera_name, camera in zip(geo_camera_names, geo_cameras) + ] + items.extend(cfvec_screenshots) + + if include_qcriterion: + qcriterion_screenshots = [ + Chart3D( + caption=PatternCaption(pattern=f"Isosurface_q_criterion_{camera_name}_[case.name]"), + show="isosurface", + iso_field="qcriterion", + exclude=exclude, + limits=(0, 0.8), + camera=camera, + fig_name=f"qcriterion_{camera_name}_fig", + fig_size=1, + ) + for camera_name, camera in zip(geo_camera_names, geo_cameras) + ] + items.extend(qcriterion_screenshots) + + report = ReportTemplate( + title="Sweep Template Report", + items=items, + settings=Settings(dpi=150), + ) + + report = report.create_in_cloud( + "sweep-script-report", + cases, + ) + + report.wait() + report.download("report.pdf") + + +if __name__ == "__main__": + main() diff --git a/examples/post_processing/report/sweep_launch_template.py b/examples/post_processing/report/sweep_launch_template.py new file mode 100644 index 000000000..4c70de5c5 --- /dev/null +++ b/examples/post_processing/report/sweep_launch_template.py @@ -0,0 +1,289 @@ +""" +Sample Flow 360 API scripts. +Requires a volume mesh or CAD file that you are ready to upload and run cases on. + +This script will: +1: + Either: + 1.a- create a project and upload a volume mesh + 1.b-create a project and upload a CAD. Mesh parameters will need to be given and mesh will creation will need to be launched + 1.c- connect to an existing project and associate with a volume mesh from that project. + +2: + Create the directory structure on the local machine to organize the data that will be generated + +3: + -Launch all cases required for the sweep + -Save all the caseID and other relevant data in the directory created in step 2: above +""" + +import os + +import pandas as pd +from sweep_launch_report import csv_reader, generate_report + +import flow360 as fl +from flow360 import u +from flow360.examples import EVTOL + +# Variables we want to export in our volume and surface solution files. Many more are available. +vol_fields = ["Mach", "Cp", "mut", "mutRatio", "primitiveVars", "qcriterion"] +surf_fields = ["Cp", "yPlus", "Cf", "CfVec", "primitiveVars", "wallDistance"] + +velocity_magnitude = 100 * u.m / u.s + + +###################################################################################################################### +def make_run_params(mesh_object, models): + """ + Create the params object that contains all the run parameters. + Needs the mesh_object to get the list of surfaces. + """ + with fl.SI_unit_system: + params = fl.SimulationParams( + # Dimensions can be either in inches, m, mm or many other units + reference_geometry=fl.ReferenceGeometry( + moment_center=(0, 0, 0) * u.m, moment_length=1 * u.m, area=1 * u.m * u.m + ), + operating_condition=fl.AerospaceCondition( + velocity_magnitude=velocity_magnitude, alpha=0 * u.deg + ), + time_stepping=fl.Steady(max_steps=5000, CFL=fl.AdaptiveCFL()), + models=[ + *models, + fl.Fluid( + navier_stokes_solver=fl.NavierStokesSolver(), + turbulence_model_solver=fl.SpalartAllmaras(), + ), + ], + outputs=[ + fl.VolumeOutput(output_format="tecplot", output_fields=vol_fields), + # mesh_object['*'] will select all the boundaries in the mesh and export the surface results. + # Regular expressions can be used to filter for certain boundaries. + fl.SurfaceOutput( + surfaces=[mesh_object["*"]], output_fields=surf_fields, output_format="tecplot" + ), + ], + ) + + # Add meshing params in case the project starts from geometry. + params.meshing = mesh_object.params.meshing + + return params + + +###################################################################################################################### +def launch_sweep(params, project, mesh_object, dir_path): + """ + Launch a sweep of cases. + """ + + case_list = [] + + # Create the csv file where we will store all relevant sweep data. + os.makedirs(dir_path, exist_ok=True) + csv_path = os.path.join(dir_path, "sweep_saved_data.csv") + general_info = { + "Sweep name": os.path.basename(dir_path), + "Project name": project.metadata.name, + "Project ID": project.id, + "Volume mesh ID": mesh_object.id, + "Velocity magnitude": velocity_magnitude, + } + df = pd.DataFrame.from_dict([general_info]) + df.to_csv(csv_path, index=False) + + # For example let's vary alpha: + alphas = [-10, -5, 0, 5, 10, 12, 14] * u.deg + + cases_params = [] + for i, alpha_angle in enumerate(alphas): + # modify the alpha + params.operating_condition.alpha = alpha_angle + + case = project.run_case(params=params, name=f"alpha_{alpha_angle.value}_case") + data = { + "case_id": case.id, + "alpha(deg)": params.operating_condition.alpha.value, + "beta(deg)": params.operating_condition.beta.value, + } + + print(f"The case ID is: {case.id} with {alpha_angle=} ") + case_list.append(case) + cases_params.append(data) + + # fraction used for averaging over the last % of iterations, 0.1 -> last 10% + fraction = 0.1 + + cases_forces = [] + for i, case in enumerate(case_list): + case.wait() + results = case.results + avg_total_forces = results.total_forces.get_averages(fraction).to_dict() + forces = { + "CL(avg)": avg_total_forces["CL"], + "CD(avg)": avg_total_forces["CD"], + "CFx(avg)": avg_total_forces["CFx"], + "CFy(avg)": avg_total_forces["CFy"], + "CFz(avg)": avg_total_forces["CFz"], + "CMx(avg)": avg_total_forces["CMx"], + "CMy(avg)": avg_total_forces["CMy"], + "CMz(avg)": avg_total_forces["CMz"], + } + cases_forces.append(forces) + + df_data = pd.DataFrame.from_dict(cases_params) + df_forces = pd.DataFrame.from_dict(cases_forces) + df = df_data.join(df_forces) + df.to_csv(csv_path, index=False, mode="a") + + return csv_path + + +###################################################################################################################### +def create_mesh_params(project): + """ + + Create the parameters object containing all the information required by the solver to generate the mesh + + Parameters + ---------- + project + + Returns + ------- + params object + """ + + geometry = project.geometry + geometry.group_faces_by_tag("faceName") + geometry.group_edges_by_tag("edgeName") + with fl.SI_unit_system: + params = fl.SimulationParams( + meshing=fl.MeshingParams( + defaults=fl.MeshingDefaults( + boundary_layer_first_layer_thickness=1e-5, surface_max_edge_length=1 + ), + volume_zones=[fl.AutomatedFarfield()], + refinements=[ + fl.SurfaceEdgeRefinement( + name="leading_edges", + edges=[geometry["leadingEdge"]], + method=fl.AngleBasedRefinement(value=2 * u.deg), + ) + ], + ) + ) + return params + + +def assign_boundary_conditions(project): + + if project.project_tree.root.asset_type == "Geometry": + geo = project.geometry + geo.group_faces_by_tag("faceName") + models = [ + fl.Wall( + surfaces=[ + # "*" will select all geometry boundaries + geo["*"], + ], + ), + fl.Freestream(surfaces=fl.AutomatedFarfield().farfield), + ] + else: + vm = project.volume_mesh + models = [ + fl.Wall( + surfaces=[ + vm["fluid/fuselage"], + # *_pylon will select all boundaries ending with _pylon + vm["fluid/*_pylon"], + vm["fluid/left_wing"], + vm["fluid/right_wing"], + vm["fluid/h_tail"], + vm["fluid/v_tail"], + ], + ), + fl.Freestream(surfaces=vm["fluid/farfield"]), + ] + return models + + +def project_from_volume_mesh(): + EVTOL.get_files() + project_name = "sweep_evtol_from_mesh" + project = fl.Project.from_volume_mesh( + EVTOL.mesh_filename, # mesh could also be in ugrid format + name=project_name, + length_unit="m", # length_unit should be 'm', 'mm', 'cm', 'inch' or 'ft' + ) + + return project + + +def project_from_geometry(): + EVTOL.get_files() + project_name = "sweep_evtol_from_geometry" + project = fl.Project.from_geometry(EVTOL.geometry, name=project_name) + mesh_params = create_mesh_params(project) + project.generate_volume_mesh(params=mesh_params, name="mesh_name") + + return project + + +###################################################################################################################### +def main(): + """ + Main function that drives the functions to achieve all the steps mentioned at the top. + """ + + # Step1: Connect to an existing project and volume mesh. + # Chose one of two options below + + # Option 1a: If you want to upload a new mesh and create a new project. + project = project_from_volume_mesh() + + # Option 1b: If you want to upload a CAD geometry and create a new project. + # project = project_from_geometry() + + # Option 1c: if you want to run from an existing project. + # project = fl.Project.from_cloud( + # 'prj-XXXXXXXXXXX') # where prj-XXXXXXXXXX is an ID that can be saved from a previously created project or read off the WEBUI + + vm = project.volume_mesh + # If the project has more then one mesh then use hte line below to choose a specific mesh instead. + # vm = project.get_volume_mesh(asset_id='vm-XXXXXXXXXXXXXXX') + + print(f"The project id is {project.id}") + print(f"The volume mesh contains the following boundaries:{vm.boundary_names}") + print(f"The volume mesh ID is: {vm.id}") + + # Step 2: Create the directories to locally store relevant data. + dir_name = "evtol_alpha_sweep" + + # Step3: Launch the cases and save the relevant data. + models = assign_boundary_conditions(project) + params = make_run_params(vm, models) + csv_path = launch_sweep(params, project, vm, dir_name) + + generate_report( + *csv_reader(csv_path), + include_geometry=True, + include_general_tables=True, + include_residuals=True, + include_cfl=True, + include_forces_moments_table=True, + include_forces_moments_charts=True, + include_forces_moments_alpha_charts=True, + include_forces_moments_beta_charts=True, + include_cf_vec=True, + include_cp=True, + include_yplus=True, + include_qcriterion=True, + ) + + +###################################################################################################################### +if __name__ == "__main__": + main() diff --git a/examples/post_processing/special_features/bet_disk.py b/examples/post_processing/special_features/bet_disk.py index 2501b3e30..20903fe4e 100644 --- a/examples/post_processing/special_features/bet_disk.py +++ b/examples/post_processing/special_features/bet_disk.py @@ -4,6 +4,14 @@ import flow360 as fl from flow360.examples import BETExampleData +from flow360.plugins.report.report import ReportTemplate +from flow360.plugins.report.report_items import ( + Chart3D, + FrontCamera, + Inputs, + Settings, + Summary, +) BETExampleData.get_files() @@ -86,4 +94,29 @@ # save converted results to a new CSV file: results.bet_forces.to_file(os.path.join(case.name, "bet_forces_in_SI.csv")) -# Report workflows have moved to the standalone `flow360-report` package. +cases = [case] + +front_camera_slice = FrontCamera(dimension=350, dimension_dir="height") + +bet_slice_screenshot = Chart3D( + section_title="BET effective AoA", + items_in_row=2, + force_new_page=True, + show="slices", + include=["slice_x"], + field="betMetrics_AlphaDegrees", + limits=(-18, 0), + camera=front_camera_slice, + fig_name="slice_x", +) + +report = ReportTemplate( + title="BET results screenshots", + items=[Summary(), Inputs(), bet_slice_screenshot], + settings=Settings(dpi=150), +) + +report = report.create_in_cloud("BET, dpi=default", cases) + +report.wait() +report.download("report.pdf") diff --git a/examples/post_processing/special_features/heat_transfer.py b/examples/post_processing/special_features/heat_transfer.py index d67ba3dfd..1c3e26b7b 100644 --- a/examples/post_processing/special_features/heat_transfer.py +++ b/examples/post_processing/special_features/heat_transfer.py @@ -1,6 +1,16 @@ import flow360 as fl from flow360 import u from flow360.examples import TutorialCHTSolver +from flow360.plugins.report.report import ReportTemplate +from flow360.plugins.report.report_items import ( + Camera, + Chart3D, + FrontCamera, + Inputs, + LeftCamera, + Settings, + Summary, +) TutorialCHTSolver.get_files() @@ -105,4 +115,61 @@ surface_heat_transfer = results.surface_heat_transfer.as_dataframe() print(surface_heat_transfer) -# Report generation utilities now live in the external `flow360-report` package. +cases = [case] + +exclude = ["fluid/farfield", "solid/interface_fluid", "solid/adiabatic"] + +front_camera_slice = FrontCamera(dimension=1, dimension_dir="width") +side_camera_slice = LeftCamera(pan_target=(0.35, 0, 0), dimension=2, dimension_dir="width") +front_right_top_camera = Camera( + position=(-1, -1, 1), look_at=(0.35, 0, 0), dimension=1, dimension_dir="width" +) + +x_slice_screenshot = Chart3D( + section_title="Slice temperature at x=0.35", + items_in_row=2, + force_new_page=True, + show="slices", + include=["slice_x"], + field="T", + limits=(285 * u.K, 395 * u.K), + camera=front_camera_slice, + fig_name="slice_x", +) + +y_slice_screenshot = Chart3D( + section_title="Slice temperature at y=0", + items_in_row=2, + force_new_page=True, + show="slices", + include=["slice_y"], + field="T", + limits=(285 * u.K, 395 * u.K), + camera=side_camera_slice, + fig_name="slice_y", +) + +surface_screenshot = Chart3D( + section_title="Surface temperature", + items_in_row=2, + force_new_page=True, + show="boundaries", + field="T", + limits=(285 * u.K, 395 * u.K), + exclude=exclude, + camera=front_right_top_camera, +) + +report = ReportTemplate( + title="CHT results screenshots", + items=[Summary(), Inputs(), x_slice_screenshot, y_slice_screenshot, surface_screenshot], + settings=Settings(dpi=150), +) + +report = report.create_in_cloud( + "CHT, dpi=default", + cases, +) + +report.wait() +report.download("report.pdf") diff --git a/examples/tutorials/notebooks/notebook_automotive.ipynb b/examples/tutorials/notebooks/notebook_automotive.ipynb index 175cada4a..2a1790b55 100644 --- a/examples/tutorials/notebooks/notebook_automotive.ipynb +++ b/examples/tutorials/notebooks/notebook_automotive.ipynb @@ -785,6 +785,41 @@ ">Note: Importing units (u) here is only for the sake of convenience." ] }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from flow360 import u\n", + "from flow360.plugins.report.report import ReportTemplate\n", + "from flow360.plugins.report.report_items import (\n", + " BottomCamera,\n", + " Chart2D,\n", + " Chart3D,\n", + " FrontCamera,\n", + " FrontLeftBottomCamera,\n", + " FrontLeftTopCamera,\n", + " Inputs,\n", + " LeftCamera,\n", + " RearCamera,\n", + " RearLeftTopCamera,\n", + " RearRightBottomCamera,\n", + " Settings,\n", + " Summary,\n", + " Table,\n", + " TopCamera,\n", + ")\n", + "from flow360.plugins.report.utils import (\n", + " Average,\n", + " DataItem,\n", + " Delta,\n", + " Expression,\n", + " Variable,\n", + " GetAttribute,\n", + ")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -994,12 +1029,308 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizations\n", + "\n", + "Here we will define separate lists of `Chart3D`, which are essentially 3D visualizations (you can think of them as screenshots of your model). These come with a variety of options that can be used to customize the way data is shown.\n", + "\n", + "One of the common things is that pretty much all of those charts contain either an exclude or include field. They provide control over what parts of the simulation domain will be excluded/included in the image.\n", + "\n", + ">Note: If you took a peek at the charts you may have noticed that list comprehension can used to iterate over camera positions." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "forces = [\n", + " Chart2D(\n", + " x=\"x_slicing_force_distribution/X\",\n", + " y=\"x_slicing_force_distribution/totalCumulative_CD_Curve\",\n", + " fig_name=\"totalCumulative_CD_Curve\",\n", + " background=\"geometry\",\n", + " exclude=exclude,\n", + " ),\n", + " Chart2D(\n", + " x=\"surface_forces/pseudo_step\",\n", + " y=\"surface_forces/totalCD\",\n", + " section_title=\"Drag Coefficient\",\n", + " fig_name=\"cd_fig\",\n", + " exclude=exclude,\n", + " focus_x=(1 / 3, 1),\n", + " ),\n", + "]\n", + "geometry_screenshots = [\n", + " Chart3D(\n", + " section_title=\"Geometry\",\n", + " items_in_row=2,\n", + " force_new_page=True,\n", + " show=\"boundaries\",\n", + " camera=camera,\n", + " exclude=exclude,\n", + " fig_name=f\"geo_{i}\",\n", + " )\n", + " for i, camera in enumerate(cameras_geo)\n", + "]\n", + "cpt_screenshots = [\n", + " Chart3D(\n", + " section_title=\"Isosurface, Cpt=-1\",\n", + " items_in_row=2,\n", + " force_new_page=True,\n", + " show=\"isosurface\",\n", + " iso_field=\"Cpt\",\n", + " exclude=exclude,\n", + " camera=camera,\n", + " )\n", + " for camera in cameras_cp\n", + "]\n", + "cfvec_screenshots = [\n", + " Chart3D(\n", + " section_title=\"CfVec\",\n", + " items_in_row=2,\n", + " force_new_page=True,\n", + " show=\"boundaries\",\n", + " field=\"CfVec\",\n", + " mode=\"lic\",\n", + " limits=(1e-4, 10),\n", + " is_log_scale=True,\n", + " exclude=exclude,\n", + " camera=camera,\n", + " )\n", + " for camera in cameras_cp\n", + "]\n", + "y_slices_screenshots = [\n", + " Chart3D(\n", + " section_title=f\"Slice velocity y={y}\",\n", + " items_in_row=2,\n", + " force_new_page=True,\n", + " show=\"slices\",\n", + " include=[f\"slice_y_{name}\"],\n", + " field=\"velocity\",\n", + " limits=(0 * u.m / u.s, 50 * u.m / u.s),\n", + " camera=side_camera_slice,\n", + " fig_name=f\"slice_y_{name}\",\n", + " )\n", + " for name, y in zip([\"0\", \"0_2\", \"0_4\", \"0_6\", \"0_8\"], [0, 0.2, 0.4, 0.6, 0.8])\n", + "]\n", + "y_slices_lic_screenshots = [\n", + " Chart3D(\n", + " section_title=f\"Slice velocity LIC y={y}\",\n", + " items_in_row=2,\n", + " force_new_page=True,\n", + " show=\"slices\",\n", + " include=[f\"slice_y_{name}\"],\n", + " field=\"velocityVec\",\n", + " mode=\"lic\",\n", + " limits=(0 * u.m / u.s, 50 * u.m / u.s),\n", + " camera=side_camera_slice,\n", + " fig_name=f\"slice_y_vec_{name}\",\n", + " )\n", + " for name, y in zip([\"0\", \"0_2\", \"0_4\", \"0_6\", \"0_8\"], [0, 0.2, 0.4, 0.6, 0.8])\n", + "]\n", + "z_slices_screenshots = [\n", + " Chart3D(\n", + " section_title=f\"Slice velocity z={z}\",\n", + " items_in_row=2,\n", + " force_new_page=True,\n", + " show=\"slices\",\n", + " include=[f\"slice_z_{name}\"],\n", + " field=\"velocity\",\n", + " limits=(0 * u.m / u.s, 50 * u.m / u.s),\n", + " camera=top_camera_slice,\n", + " fig_name=f\"slice_z_{name}\",\n", + " )\n", + " for name, z in zip([\"neg0_2\", \"0\", \"0_2\", \"0_4\", \"0_6\", \"0_8\"], [-0.2, 0, 0.2, 0.4, 0.6, 0.8])\n", + "]\n", + "y_plus_screenshots = [\n", + " Chart3D(\n", + " section_title=\"y+\",\n", + " items_in_row=2,\n", + " show=\"boundaries\",\n", + " field=\"yPlus\",\n", + " exclude=exclude,\n", + " limits=(0, 5),\n", + " camera=camera,\n", + " fig_name=f\"yplus_{i}\",\n", + " )\n", + " for i, camera in enumerate([top_camera, bottom_camera])\n", + "]\n", + "cp_screenshots = [\n", + " Chart3D(\n", + " section_title=\"Cp\",\n", + " items_in_row=2,\n", + " show=\"boundaries\",\n", + " field=\"Cp\",\n", + " exclude=exclude,\n", + " limits=limits,\n", + " camera=camera,\n", + " fig_name=f\"cp_{i}\",\n", + " )\n", + " for i, (limits, camera) in enumerate(zip(limits_cp, cameras_cp))\n", + "]\n", + "cpx_screenshots = [\n", + " Chart3D(\n", + " section_title=\"Cpx\",\n", + " items_in_row=2,\n", + " show=\"boundaries\",\n", + " field=\"Cpx\",\n", + " exclude=exclude,\n", + " limits=(-0.3, 0.3),\n", + " camera=camera,\n", + " fig_name=f\"cpx_{i}\",\n", + " )\n", + " for i, camera in enumerate(cameras_cp)\n", + "]\n", + "wall_shear_screenshots = [\n", + " Chart3D(\n", + " section_title=\"Wall shear stress magnitude\",\n", + " items_in_row=2,\n", + " show=\"boundaries\",\n", + " field=\"wallShearMag\",\n", + " exclude=exclude,\n", + " limits=(0 * u.Pa, 5 * u.Pa),\n", + " camera=camera,\n", + " fig_name=f\"wallShearMag_{i}\",\n", + " )\n", + " for i, camera in enumerate(cameras_cp)\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Report assembly\n", + "\n", + "Once we have all of the components of our report, we can create a `ReportTemplate` that will allow us to then generate the report itself. In our case, we will also add two `Chart2D`s related to drag coefficient." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "report_template = ReportTemplate(\n", + " title=\"Aerodynamic analysis of DrivAer\",\n", + " items=[\n", + " Summary(),\n", + " Inputs(),\n", + " statistical_table_general,\n", + " statistical_table_forces,\n", + " *forces,\n", + " *geometry_screenshots,\n", + " *cp_screenshots,\n", + " *cpx_screenshots,\n", + " *cpt_screenshots,\n", + " *y_slices_screenshots,\n", + " *y_slices_lic_screenshots,\n", + " *z_slices_screenshots,\n", + " *y_plus_screenshots,\n", + " *wall_shear_screenshots,\n", + " ],\n", + " settings=Settings(dpi=150),\n", + ")" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "All that is left now is to create the report by calling the `create_in_cloud()` function, then downloading it. To ensure that the download will only start after the report has actually been generated, we use `wait()`." ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "87aaf851429a4b2ba59e36507a9dd1b8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n" + ], + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "69c8a4f4849f48898412f0dc4755f344", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n" + ], + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "[14:15:44] INFO: Saved to report.pdf \n", + "\n" + ], + "text/plain": [ + "\u001b[2;36m[14:15:44]\u001b[0m\u001b[2;36m \u001b[0m\u001b[36mINFO\u001b[0m: Saved to report.pdf \n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'report.pdf'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "report = report_template.create_in_cloud(\n", + " f\"{size}-{len(cases)}cases-slices-using-groups-Cpt, Cpx, wallShear, dpi=default\",\n", + " cases,\n", + ")\n", + "\n", + "report.wait()\n", + "report.download(\"report.pdf\")" + ] } ], "metadata": { @@ -1023,4 +1354,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/flow360/__init__.py b/flow360/__init__.py index 30fe688d7..c8ad893b9 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -203,6 +203,7 @@ from flow360.component.surface_mesh_v2 import SurfaceMeshV2 as SurfaceMesh from flow360.component.volume_mesh import VolumeMeshV2 as VolumeMesh from flow360.environment import Env +from flow360.plugins import report from flow360.version_utils import warn_if_prerelease_version as _warn_prerelease __all__ = [ @@ -346,6 +347,7 @@ "UserVariable", "math", "solution", + "report", "snappy", "ModularMeshingWorkflow", "SeedpointVolume", diff --git a/flow360/cloud/flow360_requests.py b/flow360/cloud/flow360_requests.py index 4e294ebce..79410ed38 100644 --- a/flow360/cloud/flow360_requests.py +++ b/flow360/cloud/flow360_requests.py @@ -183,6 +183,20 @@ class NewSurfaceMeshDependencyRequest(Flow360RequestsV2): icon: str = pd_v2.Field(default="", description="project description") +class _Resource(Flow360RequestsV2): + type: Literal["Case", "Project"] + id: str + + +class NewReportRequest(Flow360RequestsV2): + "New report request" + + name: str + resources: List[_Resource] + config_json: str + solver_version: str + + class DraftCreateRequest(Flow360RequestsV2): """Data model for draft create request""" diff --git a/flow360/component/project.py b/flow360/component/project.py index cab7a6c55..ce4f632e4 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -80,6 +80,7 @@ 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]] @@ -2214,4 +2215,10 @@ def run_case( draft = case_or_draft return draft case = case_or_draft + report_template = get_default_report_summary_template() + report_template.create_in_cloud( + name=f"{name}-summary", + cases=[case], + solver_version=solver_version if solver_version else self.solver_version, + ) return case diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index c341a7540..b4e7d8c44 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -94,6 +94,7 @@ Flow360TranslationError, Flow360ValueError, ) +from flow360.plugins.report.report import get_default_report_summary_template from flow360.version import __version__ # Required for correct global scope initialization @@ -1181,6 +1182,19 @@ def translate_xfoil_c81_bet_disk( return bet_dict_list, 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, + ) + + def _parse_root_item_type_from_simulation_json(*, param_as_dict: dict): """[External] Deduct the root item entity type from simulation.json""" try: diff --git a/flow360/plugins/__init__.py b/flow360/plugins/__init__.py deleted file mode 100644 index 205e11b5b..000000000 --- a/flow360/plugins/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Flow360 plugin namespace. - -Report-related functionality has moved to the standalone `flow360-report` package. -Install it via `pip install flow360-report` if you need reporting utilities. -""" - -from typing import Any - -_REPORT_HINT = ( - "flow360 reporting utilities now live in the `flow360-report` package. " - "Install it via `pip install flow360-report` and import from `flow360_report.plugins.report`." -) - - -def __getattr__(name: str) -> Any: - if name == "report": - raise ImportError(_REPORT_HINT) - raise AttributeError(f"module 'flow360.plugins' has no attribute '{name}'") - - -__all__: list[str] = [] diff --git a/flow360/plugins/report/README.md b/flow360/plugins/report/README.md new file mode 100644 index 000000000..23ea80864 --- /dev/null +++ b/flow360/plugins/report/README.md @@ -0,0 +1,311 @@ +# Report Plugin + +The **Report Plugin** provides automated PDF report generation for simulation results. It integrates with Flow360 (or similar frameworks) to pull data from simulation cases, produce tables and charts, and optionally render 3D images or screenshots. Users can easily configure how the PDF is structured, which items to include (e.g., summaries, inputs, tables, 2D and 3D charts), and how to style each item. + +--- + +## Table of Contents +- [Report Plugin](#report-plugin) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Features](#features) + - [Folder and File Structure](#folder-and-file-structure) + - [Installation](#installation) + - [Usage](#usage) + - [1. Create or Load Simulation Cases](#1-create-or-load-simulation-cases) + - [2. Prepare a Report Configuration](#2-prepare-a-report-configuration) + - [3. Invoke the Plugin](#3-invoke-the-plugin) + - [Configuration and Customization](#configuration-and-customization) + - [Generating Screenshots](#generating-screenshots) + - [Camera controls:](#camera-controls) + - [Key Camera Attributes](#key-camera-attributes) + - [Camera Examples](#camera-examples) + - [Examples](#examples) + - [Minimal Example](#minimal-example) + +--- + +## Overview + +This plugin: +- Loads simulation data from one or more **Case** objects. +- Extracts relevant metrics, such as velocities, forces, boundaries, and other fields. +- Creates custom sections (e.g. *Summary*, *Inputs*, *Tables*, *Charts*) and assembles them into a single LaTeX-based PDF document. +- Can optionally include 3D images or screenshots of the geometry, surfaces, or isosurfaces for an at-a-glance view of simulation geometry or results. + +--- + +## Features + +1. **Section-Based Reporting** + - Easily add sections like `Summary`, `Inputs`, and custom sections with specialized content. + +2. **Tables** + - Collect data from one or multiple simulation cases and display in tabular form using the `Table` class. + - Built-in support for custom formatting (e.g., `.5g` or specialized formatting functions). + +3. **2D Charts** + - Plot data such as forces, residuals, or general x-y plots with `Chart2D`. + - Supports logarithmic scaling if needed (e.g., for residual plots). + - Can combine multiple cases into one plot or generate separate plots per case. + +4. **3D Visualization** + - Automatically generate (or embed) images of your 3D geometry (e.g., surfaces, isosurfaces, different camera angles). + - Users can opt to generate screenshots of simulation geometry in various positions (requires an external service or local tool to produce the images; see [Generating Screenshots](#generating-screenshots) below). + +5. **Flexible Layout** + - Choose how items are arranged (one item per page, multiple items per row, etc.). + - Control figure sizes, captions, new pages, etc. + +--- + +## Folder and File Structure + +A high-level view of the relevant files: + +flow360/plugins/report/ +├── report.py # Main file that defines the ReportTemplate class +├── report_items.py # Contains classes for different report "items" like Summary, Table, Chart2D, Chart3D, etc. +├── report_context.py # (Imported by others) Manages the context in which the report is generated +├── utils.py # Utility functions for data extraction, data formatting, etc. +└── uvf_shutter.py # (Optional) Helper for generating 3D geometry screenshots + +Example files in the repo: + +examples/ +└── automotive_example_in_cloud.py # Shows how to configure and instantiate a report + +## Installation + +1. Make sure you have Python 3.8+ installed. +2. Install flow360 package: + ```bash + pip install flow360 + ``` + + + + +--- + +## Usage + +### 1. Create or Load Simulation Cases + +You need one or more **Case** objects that contain all relevant simulation data. The plugin references fields in these cases to build tables and charts. + +### 2. Prepare a Report Configuration + +Typically, you have a JSON or Python-based config specifying which `ReportItem` objects you want in your report, e.g.: + +```python +report = ReportTemplate( + title="Aerodynamic analysis of DrivAer", + items=[ + Summary(text="Short summary..."), + Inputs(), + Table( + data=[ + "params/reference_geometry/area", + DataItem(data="surface_forces/totalCD", operations=Average(fraction=0.1)), + "params/time_stepping/max_steps", + ], + section_title="Statistical data", + ), + Chart2D( + x="surface_forces/pseudo_step", + y="surface_forces/totalCD", + section_title="Drag Coefficient", + fig_name="cd_fig", + ) + ] +) +report.to_file('path/to/reportTemplate.json"') +``` +(This might be stored in a file like reportTemplate.json.) + +```JSON +{ + "include_case_by_case": false, + "items": [ + { + "text": "Short summary...", + "type_name": "Summary" + }, + { + "type_name": "Inputs" + }, + { + "data": [ + "params/reference_geometry/area", + { + "data": "surface_forces/totalCD", + "operations": [ + { + "fraction": 0.1, + "type_name": "Average" + } + ], + "type_name": "DataItem" + }, + "params/time_stepping/max_steps" + ], + "section_title": "Statistical data", + "type_name": "Table" + }, + { + "fig_name": "cd_fig", + "section_title": "Drag Coefficient", + "type_name": "Chart2D", + "x": "surface_forces/pseudo_step", + "y": "surface_forces/totalCD" + } + ], + "title": "Aerodynamic analysis of DrivAer" +} +``` + + + +### 3. Invoke the Plugin +Use the main ReportTemplate class (from report.py) to build and export your PDF. For example (similar to automotive_example_in_cloud.py): + +```python +from flow360.plugins.report.report import ReportTemplate +from flow360.component.case import Case + +# Suppose you have a list of Case objects +cases = [...] + +# Point to your config: +report = ReportTemplate(filename="path/to/reportTemplate.json") + +# Generate the PDF: +report.create_pdf( + output_folder="my_report_output", + cases=cases +) +``` + + +## Configuration and Customization + +- **Report Items**: The core building blocks, each corresponding to a section or figure in the PDF. + - *Summary*: Adds a textual overview. + - *Inputs*: Collects key input parameters from your cases (e.g., velocity, time-stepping). + - *Table*: Aggregates data from multiple cases into one table. + - *Chart2D*: Produces an XY plot from data paths in your case(s). + - *Chart3D*: Optionally display geometry or color-field surfaces (like `Cp`, `yPlus`, etc.). + +- **Custom Data Paths**: Many items accept `data` arrays where each entry references a JSON path inside each case, e.g. `"params/operating_condition/velocity_magnitude"`. + - If the plugin finds that path in each case’s data, it extracts the values and populates the table/plot. + +- **Formatting**: + - Use built-in format specifiers or pass a custom function to format numeric data. + - For tables, set `formatter` as a string (e.g. `".5g"`) or a callable for specialized formatting. + +- **Multiple Cases**: If you provide multiple cases in a single run, the plugin can: + - Combine them side by side in tables. + - Overlay them on the same chart. + - Generate separate pages or subplots (use `separate_plots=True` or `items_in_row=...` to configure layout). + + + +## Generating Screenshots + +The plugin can produce screenshots of your simulation geometry or results. You can activate this in `Chart3D` items by specifying a field or object type to display. The actual screenshot generation leverages an external tool, but the plugin manages how the images are embedded in the final PDF. + + +### Camera controls: + +The `Camera` class specifies a 3D viewpoint and zoom for rendering geometry or visualizing simulation results. By adjusting properties such as `position`, `look_at`, and `dimension`, users can control the angle, orientation, and scale of the view. Below are the attributes you can configure (note that all lengths are in the same units used in your geometry or volume mesh): + + +### Key Camera Attributes + +- **Position** + The camera’s eye position in 3D space. Typically, you can think of it as a point on a sphere, looking inward toward `look_at`. Adjusting `position` can rotate the view around the object of interest. + +- **Up** + A vector that determines which way is “up” in the final image. Changing this can rotate the view around the axis from the camera’s position to the `look_at` point. + +- **Look At** + The point at which the camera is aimed. By default, if unspecified, it’s often the bounding-box center of your model. + +- **Pan Target** + A specific point to which you want to pan your camera’s center. If left unset, `look_at` remains the default center of view. + +- **Dimension & Dimension Direction** + Together, these control the zoom level. For example, if `dimension_dir` is `"width"` and `dimension` is `2.0`, the rendered view’s width represents 2 model units. Similarly, use `"height"` or `"diagonal"` to scale in those directions. + + +--- + +### Camera Examples + +
n>0N=g{}^iTQC)dJtwd$0C3z0|xC(70p;qK0z#GB^
zq$2+YD0?X13AG@ vss-v%9i~G8B
zt5#iJxA^(Z>l;!TSRD8`;NgY4*M8@Aw%bnTCmNrVFl(_5n3W(r*bNVcAq`M6cZfxJ
z9kqyAc~i-}Y4InPy=SmDaS?|xPu|>>AG! TQVqUZm(XldLwbNOWi;88Y>qC2-(oYkT3S!GBwK(h
z{S~q`*$VxDek9w%H pQdrxhgHZ#*)^66udN-=#f*U`4uzb98h+ICl{G+~xC@R&eFylKdQunB2
zTk5v-rmvU0S*c=f-*51M$+nD{u@_ETBsjUOnHai&ugROfS0}eU>IIh;U~F|(^G+jR
zv)hfcPhPY-;bW6t-wS48I00=++_`c6p45$F`VR;h-PegV!}zo~SQ>(e8*;GhQHGge
zmxbI3-pEo$j|^0hWg-I)$7N74UsE6o{96ie0>)nxS#L`YjPVw|F-zGoN0y~=x^1ur
zd{EZ_belHn2_eg
zH-U#5kAF3)eq!@dZ~25vm_|S~EvKq~-p;jQqs!cx
z1Mfd&J(q?>BYxQ~HW^79j7xE^Sv#U&?6fwMDmE8ukBRo}v$pBG=)i%=cNb{;+}`Uu
zmK@mBegV_R`r~aaKN|4il*ma_$9);&**AMeYVv5`b&RyO40~lo!KiXITRii^H(j<5
z@@*PoTl(6kL;FW2rwnS