From a213419a4ca0e1e45e39256be8a861f6e1f2b78a Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Thu, 19 Jun 2025 12:05:43 -0400 Subject: [PATCH 1/7] Added basic data models for render output --- .../simulation/outputs/output_entities.py | 64 +++++++++++++++++- .../component/simulation/outputs/outputs.py | 46 +++++++++++++ .../translator/solver_translator.py | 65 ++++++++++++++++--- .../component/simulation/translator/utils.py | 12 ++++ flow360/component/types.py | 1 + .../translator/test_solver_translator.py | 23 +++++++ 6 files changed, 199 insertions(+), 12 deletions(-) diff --git a/flow360/component/simulation/outputs/output_entities.py b/flow360/component/simulation/outputs/output_entities.py index d20fc97eb..ef38bae4c 100644 --- a/flow360/component/simulation/outputs/output_entities.py +++ b/flow360/component/simulation/outputs/output_entities.py @@ -9,7 +9,7 @@ from flow360.component.simulation.framework.entity_base import EntityBase from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.outputs.output_fields import IsoSurfaceFieldNames -from flow360.component.simulation.unit_system import LengthType +from flow360.component.simulation.unit_system import AngleType, LengthType from flow360.component.simulation.user_code.core.types import ( Expression, UnytQuantity, @@ -22,7 +22,7 @@ solver_variable_to_user_variable, ) from flow360.component.simulation.user_code.core.utils import is_runtime_expression -from flow360.component.types import Axis +from flow360.component.types import Axis, Color, Vector class _OutputItemBase(Flow360BaseModel): @@ -290,3 +290,63 @@ class PointArray2D(_PointEntityBase): v_axis_vector: LengthType.Axis = pd.Field(description="The scaled v-axis of the parallelogram.") u_number_of_points: int = pd.Field(ge=2, description="The number of points along the u axis.") v_number_of_points: int = pd.Field(ge=2, description="The number of points along the v axis.") + + +# Linear interpolation between keyframes +class KeyframeCamera(Flow360BaseModel): + pass + + +# Follow a cubic spline curve +class SplineCamera(Flow360BaseModel): + pass + + +# Orbit a point (start and end points specified in spherical coordinates) +class OrbitCamera(Flow360BaseModel): + pass + + +# Static camera setup in Cartesian coordinates +class StaticCamera(Flow360BaseModel): + position: LengthType.Point = pd.Field(description="Position of the camera in the scene") + target: LengthType.Point = pd.Field(description="Target point of the camera") + up: Optional[Vector] = pd.Field((0, 1, 0), description="Up vector, if not specified assume Y+") + + +# Ortho projection, parallel lines stay parallel +class OrthographicProjection(Flow360BaseModel): + width: LengthType = pd.Field() + near: LengthType = pd.Field() + far: LengthType = pd.Field() + + +# Perspective projection +class PerspectiveProjection(Flow360BaseModel): + fov: AngleType = pd.Field() + near: LengthType = pd.Field() + far: LengthType = pd.Field() + + +# Only basic static camera with ortho projection supported currently +class RenderCameraConfig(Flow360BaseModel): + view: StaticCamera = pd.Field() + projection: OrthographicProjection = pd.Field() + + +# Ambient light, added by default to all pixels in the scene, simulates global illumination +class AmbientLight(Flow360BaseModel): + intensity: float = pd.Field() + color: Color = pd.Field() + + +# Ambient light, added by default to all pixels in the scene, simulates global illumination +class DirectionalLight(Flow360BaseModel): + intensity: float = pd.Field() + color: Color = pd.Field() + direction: Vector = pd.Field() + + +class RenderLightingConfig(Flow360BaseModel): + ambient: AmbientLight = pd.Field() + directional: DirectionalLight = pd.Field() diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 2e8e9d727..85e2fe79b 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -22,6 +22,8 @@ Point, PointArray, PointArray2D, + RenderCameraConfig, + RenderLightingConfig, Slice, ) from flow360.component.simulation.outputs.output_fields import ( @@ -676,6 +678,49 @@ def allow_only_simulation_surfaces_or_imported_surfaces(cls, value): return value +class RenderOutput(_AnimationSettings): + """ + + :class:`RenderOutput` class for backend rendered output settings. + + Example + ------- + + Define the :class:`RenderOutput` of :code:`qcriterion` on two isosurfaces: + + >>> fl.RenderOutput( + ... isosurfaces=[ + ... fl.Isosurface( + ... name="Isosurface_T_0.1", + ... iso_value=0.1, + ... field="T", + ... ), + ... fl.Isosurface( + ... name="Isosurface_p_0.5", + ... iso_value=0.5, + ... field="p", + ... ), + ... ], + ... output_field="qcriterion", + ... ) + + ==== + """ + + name: Optional[str] = pd.Field("Render output", description="Name of the `IsosurfaceOutput`.") + entities: UniqueItemList[Isosurface] = pd.Field( + alias="isosurfaces", + description="List of :class:`~flow360.Isosurface` entities.", + ) + output_fields: UniqueItemList[Union[CommonFieldNames, str]] = pd.Field( + description="List of output variables. Including " + ":ref:`universal output variables` and :class:`UserDefinedField`." + ) + camera: RenderCameraConfig = pd.Field(description="Camera settings") + lighting: RenderLightingConfig = pd.Field(description="Lighting settings") + output_type: Literal["RenderOutput"] = pd.Field("RenderOutput", frozen=True) + + class ProbeOutput(_OutputBase): """ :class:`ProbeOutput` class for setting output data probed at monitor points. @@ -1366,6 +1411,7 @@ class TimeAverageForceDistributionOutput(ForceDistributionOutput): TimeAverageStreamlineOutput, ForceDistributionOutput, TimeAverageForceDistributionOutput, + RenderOutput, ], pd.Field(discriminator="output_type"), ] diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index 5af5bb2c2..b78e3f843 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -67,6 +67,7 @@ IsosurfaceOutput, MonitorOutputType, ProbeOutput, + RenderOutput, Slice, SliceOutput, StreamlineOutput, @@ -104,6 +105,7 @@ from flow360.component.simulation.translator.utils import ( _get_key_name, convert_tuples_to_lists, + get_all_entries_of_type, get_global_setting_from_first_instance, has_instance_in_list, inline_expressions_in_dict, @@ -112,6 +114,7 @@ replace_dict_key, translate_setting_and_apply_to_all_entities, translate_value_or_expression_object, + update_dict_recursively, ) from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.user_code.core.types import ( @@ -192,10 +195,11 @@ def init_output_base(obj_list, class_type: Type, is_average: bool): class_type, "output_format", ) - assert output_format is not None - if output_format == "both": - output_format = "paraview,tecplot" - base["outputFormat"] = output_format + if not no_format: + assert output_format is not None + if output_format == "both": + output_format = "paraview,tecplot" + base["outputFormat"] = output_format if is_average: base = init_average_output(base, obj_list, class_type) @@ -384,6 +388,14 @@ def get_monitor_locations(entities: EntityList): return monitor_locations +def inject_render_info(entity: Isosurface): + """inject entity info""" + return { + "surfaceField": entity.field, + "surfaceFieldMagnitude": entity.iso_value, + } + + def inject_probe_info(entity: EntityList): """inject entity info""" @@ -567,6 +579,35 @@ def translate_isosurface_output( return translated_output +def translate_render_output(output_params: list, injection_function): + """Translate render output settings.""" + + renders = get_all_entries_of_type(output_params, RenderOutput) + + translated_outputs = [] + + for render in renders: + camera = render.camera.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) + lighting = render.lighting.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) + + translated_output = { + "animationFrequency": render.frequency, + "animationFrequencyOffset": render.frequency_offset, + "isoSurfaces": translate_setting_and_apply_to_all_entities( + [render], + RenderOutput, + translation_func=translate_output_fields, + to_list=False, + entity_injection_func=injection_function, + ), + "camera": remove_units_in_dict(camera), + "lighting": remove_units_in_dict(lighting), + } + translated_outputs.append(translated_output) + + return translated_outputs + + def translate_surface_slice_output( output_params: list, output_class: Union[SurfaceSliceOutput], @@ -951,7 +992,11 @@ def translate_output(input_params: SimulationParams, translated: dict): if imported_surface_output_configs["surfaces"]: translated["timeAverageImportedSurfaceOutput"] = imported_surface_output_configs - ##:: Step6: Get translated["monitorOutput"] + ##:: Step6: Get translated["renderOutput"] + if has_instance_in_list(outputs, RenderOutput): + translated["renderOutput"] = translate_render_output(outputs, inject_isosurface_info) + + ##:: Step7: Get translated["monitorOutput"] probe_output = {} probe_output_average = {} integral_output = {} @@ -973,7 +1018,7 @@ def translate_output(input_params: SimulationParams, translated: dict): if probe_output or integral_output: translated["monitorOutput"] = merge_monitor_output(probe_output, integral_output) - ##:: Step6.1: Get translated["surfaceMonitorOutput"] + ##:: Step7.1: Get translated["surfaceMonitorOutput"] surface_monitor_output = {} surface_monitor_output_average = {} if has_instance_in_list(outputs, SurfaceProbeOutput): @@ -995,17 +1040,17 @@ def translate_output(input_params: SimulationParams, translated: dict): surface_monitor_output, surface_monitor_output_average ) - ##:: Step6: Get translated["surfaceSliceOutput"] + ##:: Step8: Get translated["surfaceSliceOutput"] surface_slice_output = {} if has_instance_in_list(outputs, SurfaceSliceOutput): surface_slice_output = translate_surface_slice_output(outputs, SurfaceSliceOutput) translated["surfaceSliceOutput"] = surface_slice_output - ##:: Step7: Get translated["aeroacousticOutput"] + ##:: Step9: Get translated["aeroacousticOutput"] if has_instance_in_list(outputs, AeroAcousticOutput): translated["aeroacousticOutput"] = translate_acoustic_output(outputs) - ##:: Step8: Get translated["streamlineOutput"] + ##:: Step10: Get translated["streamlineOutput"] if has_instance_in_list(outputs, StreamlineOutput): translated["streamlineOutput"] = translate_streamline_output(outputs, StreamlineOutput) @@ -1014,7 +1059,7 @@ def translate_output(input_params: SimulationParams, translated: dict): outputs, TimeAverageStreamlineOutput ) - ##:: Step9: Get translated["importedSurfaceIntegralOutput"] + ##:: Step11: Get translated["importedSurfaceIntegralOutput"] if has_instance_in_list(outputs, SurfaceIntegralOutput): imported_surface_integral_output_configs = translate_imported_surface_integral_output( outputs, diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index be692907a..72bea0adb 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -8,6 +8,7 @@ from typing import Union import numpy as np +import pydantic as pd import unyt as u from flow360.component.simulation.framework.base_model_config import snake_to_camel @@ -220,6 +221,17 @@ def getattr_by_path(obj, path: Union[str, list], *args): return obj +def get_all_entries_of_type(obj_list: list, class_type: type[pd.BaseModel]): + entries = [] + + if obj_list is not None: + for obj in obj_list: + if is_exact_instance(obj, class_type): + entries.append(obj) + + return entries + + def get_global_setting_from_first_instance( obj_list: list, class_type, diff --git a/flow360/component/types.py b/flow360/component/types.py index e15b88dc9..20eb73c98 100644 --- a/flow360/component/types.py +++ b/flow360/component/types.py @@ -14,6 +14,7 @@ List2D = List[List[float]] # we use tuple for fixed length lists, beacause List is a mutable, variable length structure Coordinate = Tuple[float, float, float] +Color = Tuple[int, int, int] class Vector(Coordinate): diff --git a/tests/simulation/translator/test_solver_translator.py b/tests/simulation/translator/test_solver_translator.py index a7e04cf48..336d2544f 100644 --- a/tests/simulation/translator/test_solver_translator.py +++ b/tests/simulation/translator/test_solver_translator.py @@ -58,16 +58,24 @@ ThermalState, ) from flow360.component.simulation.outputs.output_entities import ( + AmbientLight, + DirectionalLight, + Isosurface, + OrthographicProjection, Point, PointArray, PointArray2D, + RenderCameraConfig, + RenderLightingConfig, Slice, + StaticCamera, ) from flow360.component.simulation.outputs.outputs import ( Isosurface, IsosurfaceOutput, MovingStatistic, ProbeOutput, + RenderOutput, SliceOutput, StreamlineOutput, SurfaceIntegralOutput, @@ -184,6 +192,7 @@ def get_om6Wing_tutorial_param(): my_wall = Surface(name="1") my_symmetry_plane = Surface(name="2") my_freestream = Surface(name="3") + my_isosurface = Isosurface(name="iso", field="Mach", iso_value=0.5) with SI_unit_system: param = SimulationParams( reference_geometry=ReferenceGeometry( @@ -247,6 +256,20 @@ def get_om6Wing_tutorial_param(): output_format="paraview", output_fields=["Cp"], ), + RenderOutput( + entities=[my_isosurface], + output_fields=["qcriterion"], + camera=RenderCameraConfig( + view=StaticCamera(position=(20, 20, 20), target=(0, 0, 0)), + projection=OrthographicProjection(width=30, near=0.01, far=100), + ), + lighting=RenderLightingConfig( + ambient=AmbientLight(intensity=0.4, color=(255, 255, 255)), + directional=DirectionalLight( + intensity=1.5, color=(255, 255, 255), direction=(-1.0, -1.0, -1.0) + ), + ), + ), ], ) return param From 1b43ee6667cf5a1100990f61f91382a13267d29c Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Fri, 20 Jun 2025 16:47:29 +0000 Subject: [PATCH 2/7] Update module imports --- flow360/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flow360/__init__.py b/flow360/__init__.py index 3bbb64442..611b7f894 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -128,6 +128,16 @@ PointArray, PointArray2D, Slice, + RenderCameraConfig, + RenderLightingConfig, + AmbientLight, + DirectionalLight, + OrthographicProjection, + PerspectiveProjection, + StaticCamera, + OrbitCamera, + SplineCamera, + KeyframeCamera ) from flow360.component.simulation.outputs.outputs import ( AeroAcousticOutput, @@ -152,6 +162,7 @@ TimeAverageVolumeOutput, UserDefinedField, VolumeOutput, + RenderOutput ) from flow360.component.simulation.primitives import ( AxisymmetricBody, From 753850dd089a56052f33d58cfc2f1b87297ba18c Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Tue, 24 Jun 2025 17:22:05 -0400 Subject: [PATCH 3/7] Add additional config options to render output --- flow360/__init__.py | 22 ++- .../simulation/outputs/output_entities.py | 60 -------- .../simulation/outputs/output_render_types.py | 129 ++++++++++++++++++ .../component/simulation/outputs/outputs.py | 22 ++- .../translator/solver_translator.py | 32 ++++- .../translator/test_solver_translator.py | 31 ++++- 6 files changed, 215 insertions(+), 81 deletions(-) create mode 100644 flow360/component/simulation/outputs/output_render_types.py diff --git a/flow360/__init__.py b/flow360/__init__.py index 611b7f894..fa24b97a6 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -128,16 +128,26 @@ PointArray, PointArray2D, Slice, - RenderCameraConfig, - RenderLightingConfig, +) +from flow360.component.simulation.outputs.output_render_types import ( AmbientLight, + AnimatedCamera, + ColorKey, DirectionalLight, + FieldMaterial, + Keyframe, OrthographicProjection, + PBRMaterial, PerspectiveProjection, + RenderCameraConfig, + RenderEnvironmentConfig, + RenderLightingConfig, + RenderMaterialConfig, + SkyboxBackground, + SkyboxTexture, + SolidBackground, StaticCamera, - OrbitCamera, - SplineCamera, - KeyframeCamera + Transform, ) from flow360.component.simulation.outputs.outputs import ( AeroAcousticOutput, @@ -146,6 +156,7 @@ MovingStatistic, Observer, ProbeOutput, + RenderOutput, SliceOutput, StreamlineOutput, SurfaceIntegralOutput, @@ -162,7 +173,6 @@ TimeAverageVolumeOutput, UserDefinedField, VolumeOutput, - RenderOutput ) from flow360.component.simulation.primitives import ( AxisymmetricBody, diff --git a/flow360/component/simulation/outputs/output_entities.py b/flow360/component/simulation/outputs/output_entities.py index ef38bae4c..eee8705b1 100644 --- a/flow360/component/simulation/outputs/output_entities.py +++ b/flow360/component/simulation/outputs/output_entities.py @@ -290,63 +290,3 @@ class PointArray2D(_PointEntityBase): v_axis_vector: LengthType.Axis = pd.Field(description="The scaled v-axis of the parallelogram.") u_number_of_points: int = pd.Field(ge=2, description="The number of points along the u axis.") v_number_of_points: int = pd.Field(ge=2, description="The number of points along the v axis.") - - -# Linear interpolation between keyframes -class KeyframeCamera(Flow360BaseModel): - pass - - -# Follow a cubic spline curve -class SplineCamera(Flow360BaseModel): - pass - - -# Orbit a point (start and end points specified in spherical coordinates) -class OrbitCamera(Flow360BaseModel): - pass - - -# Static camera setup in Cartesian coordinates -class StaticCamera(Flow360BaseModel): - position: LengthType.Point = pd.Field(description="Position of the camera in the scene") - target: LengthType.Point = pd.Field(description="Target point of the camera") - up: Optional[Vector] = pd.Field((0, 1, 0), description="Up vector, if not specified assume Y+") - - -# Ortho projection, parallel lines stay parallel -class OrthographicProjection(Flow360BaseModel): - width: LengthType = pd.Field() - near: LengthType = pd.Field() - far: LengthType = pd.Field() - - -# Perspective projection -class PerspectiveProjection(Flow360BaseModel): - fov: AngleType = pd.Field() - near: LengthType = pd.Field() - far: LengthType = pd.Field() - - -# Only basic static camera with ortho projection supported currently -class RenderCameraConfig(Flow360BaseModel): - view: StaticCamera = pd.Field() - projection: OrthographicProjection = pd.Field() - - -# Ambient light, added by default to all pixels in the scene, simulates global illumination -class AmbientLight(Flow360BaseModel): - intensity: float = pd.Field() - color: Color = pd.Field() - - -# Ambient light, added by default to all pixels in the scene, simulates global illumination -class DirectionalLight(Flow360BaseModel): - intensity: float = pd.Field() - color: Color = pd.Field() - direction: Vector = pd.Field() - - -class RenderLightingConfig(Flow360BaseModel): - ambient: AmbientLight = pd.Field() - directional: DirectionalLight = pd.Field() diff --git a/flow360/component/simulation/outputs/output_render_types.py b/flow360/component/simulation/outputs/output_render_types.py new file mode 100644 index 000000000..704f95b1f --- /dev/null +++ b/flow360/component/simulation/outputs/output_render_types.py @@ -0,0 +1,129 @@ +import abc +from enum import Enum +from typing import Dict, List, Optional, Union + +import pydantic as pd + +from flow360.component.simulation.framework.base_model import Flow360BaseModel +from flow360.component.simulation.unit_system import AngleType, LengthType +from flow360.component.types import Color, Vector + + +class StaticCamera(Flow360BaseModel): + position: LengthType.Point = pd.Field(description="Position of the camera in the scene") + target: LengthType.Point = pd.Field(description="Target point of the camera") + up: Optional[Vector] = pd.Field( + default=(0, 1, 0), description="Up vector, if not specified assume Y+" + ) + + +class Keyframe(Flow360BaseModel): + time: pd.confloat(ge=0) = pd.Field(0) + view: StaticCamera = pd.Field() + + +class AnimatedCamera(Flow360BaseModel): + keyframes: List[Keyframe] = pd.Field([]) + + +AllCameraTypes = Union[StaticCamera, AnimatedCamera] + + +class OrthographicProjection(Flow360BaseModel): + type: str = pd.Field(default="orthographic", frozen=True) + width: LengthType = pd.Field() + near: LengthType = pd.Field() + far: LengthType = pd.Field() + + +class PerspectiveProjection(Flow360BaseModel): + type: str = pd.Field(default="perspective", frozen=True) + fov: AngleType = pd.Field() + near: LengthType = pd.Field() + far: LengthType = pd.Field() + + +class RenderCameraConfig(Flow360BaseModel): + view: AllCameraTypes = pd.Field() + projection: Union[OrthographicProjection, PerspectiveProjection] = pd.Field() + + +class AmbientLight(Flow360BaseModel): + intensity: float = pd.Field() + color: Color = pd.Field() + + +class DirectionalLight(Flow360BaseModel): + intensity: float = pd.Field() + color: Color = pd.Field() + direction: Vector = pd.Field() + + +class RenderLightingConfig(Flow360BaseModel): + directional: DirectionalLight = pd.Field() + ambient: Optional[AmbientLight] = pd.Field(None) + + +class RenderBackgroundBase(Flow360BaseModel, metaclass=abc.ABCMeta): + type: str = pd.Field(default="", frozen=True) + + +class SolidBackground(RenderBackgroundBase): + type: str = pd.Field(default="solid", frozen=True) + color: Color = pd.Field() + + +class SkyboxTexture(str, Enum): + SKY = "sky" + + +class SkyboxBackground(RenderBackgroundBase): + type: str = pd.Field(default="skybox", frozen=True) + texture: SkyboxTexture = pd.Field(SkyboxTexture.SKY) + + +AllBackgroundTypes = Union[SolidBackground, SkyboxBackground] + + +class RenderEnvironmentConfig(Flow360BaseModel): + background: AllBackgroundTypes = pd.Field() + + +class RenderMaterialBase(Flow360BaseModel, metaclass=abc.ABCMeta): + type: str = pd.Field(default="", frozen=True) + + +class PBRMaterial(RenderMaterialBase): + color: Color = pd.Field(default=[255, 255, 255]) + alpha: float = pd.Field(default=1) + roughness: float = pd.Field(default=0.5) + f0: Vector = pd.Field(default=(0.03, 0.03, 0.03)) + type: str = pd.Field(default="pbr", frozen=True) + + +class ColorKey(Flow360BaseModel): + color: Color = pd.Field(default=[255, 255, 255]) + value: pd.confloat(ge=0, le=1) = pd.Field(default=0.5) + + +class FieldMaterial(RenderMaterialBase): + alpha: float = pd.Field(default=1) + output_field: str = pd.Field(default="") + min: float = pd.Field(default=0) + max: float = pd.Field(default=0) + colormap: List[ColorKey] = pd.Field() + type: str = pd.Field(default="field", frozen=True) + + +AllMaterialTypes = Union[PBRMaterial, FieldMaterial] + + +class RenderMaterialConfig(Flow360BaseModel): + materials: List[AllMaterialTypes] = pd.Field([]) + mappings: Dict[str, int] = pd.Field({}) + + +class Transform(Flow360BaseModel): + translation: LengthType.Point = pd.Field(default=[0, 0, 0]) + rotation: AngleType.Vector = pd.Field(default=[0, 0, 0]) + scale: Vector = pd.Field(default=[1, 1, 1]) diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 85e2fe79b..557d53e28 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -22,8 +22,6 @@ Point, PointArray, PointArray2D, - RenderCameraConfig, - RenderLightingConfig, Slice, ) from flow360.component.simulation.outputs.output_fields import ( @@ -35,6 +33,13 @@ VolumeFieldNames, get_field_values, ) +from flow360.component.simulation.outputs.output_render_types import ( + RenderCameraConfig, + RenderEnvironmentConfig, + RenderLightingConfig, + RenderMaterialConfig, + Transform, +) from flow360.component.simulation.primitives import ( GhostCircularPlane, GhostSphere, @@ -708,9 +713,11 @@ class RenderOutput(_AnimationSettings): """ name: Optional[str] = pd.Field("Render output", description="Name of the `IsosurfaceOutput`.") - entities: UniqueItemList[Isosurface] = pd.Field( - alias="isosurfaces", - description="List of :class:`~flow360.Isosurface` entities.", + isosurfaces: Optional[UniqueItemList[Isosurface]] = pd.Field( + None, description="List of :class:`~flow360.Isosurface` entities." + ) + surfaces: Optional[EntityList[Surface]] = pd.Field( + None, description="List of of :class:`~flow360.Surface` entities." ) output_fields: UniqueItemList[Union[CommonFieldNames, str]] = pd.Field( description="List of output variables. Including " @@ -718,6 +725,11 @@ class RenderOutput(_AnimationSettings): ) camera: RenderCameraConfig = pd.Field(description="Camera settings") lighting: RenderLightingConfig = pd.Field(description="Lighting settings") + environment: RenderEnvironmentConfig = pd.Field(description="Environment settings") + materials: RenderMaterialConfig = pd.Field(description="Material settings") + transform: Optional[Transform] = pd.Field( + None, description="Optional model transform to apply to all entities" + ) output_type: Literal["RenderOutput"] = pd.Field("RenderOutput", frozen=True) diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index b78e3f843..48a203303 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -195,11 +195,10 @@ def init_output_base(obj_list, class_type: Type, is_average: bool): class_type, "output_format", ) - if not no_format: - assert output_format is not None - if output_format == "both": - output_format = "paraview,tecplot" - base["outputFormat"] = output_format + assert output_format is not None + if output_format == "both": + output_format = "paraview,tecplot" + base["outputFormat"] = output_format if is_average: base = init_average_output(base, obj_list, class_type) @@ -589,6 +588,12 @@ def translate_render_output(output_params: list, injection_function): for render in renders: camera = render.camera.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) lighting = render.lighting.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) + environment = render.environment.model_dump( + exclude_none=True, exclude_unset=True, by_alias=True + ) + materials = render.materials.model_dump( + exclude_none=True, exclude_unset=True, by_alias=True + ) translated_output = { "animationFrequency": render.frequency, @@ -599,10 +604,27 @@ def translate_render_output(output_params: list, injection_function): translation_func=translate_output_fields, to_list=False, entity_injection_func=injection_function, + entity_list_attribute_name="isosurfaces", + ), + "surfaces": translate_setting_and_apply_to_all_entities( + [render], + RenderOutput, + translation_func=translate_output_fields, + to_list=False, + entity_list_attribute_name="surfaces", ), "camera": remove_units_in_dict(camera), "lighting": remove_units_in_dict(lighting), + "environment": remove_units_in_dict(environment), + "materials": remove_units_in_dict(materials), } + + if render.transform: + transform = render.transform.model_dump( + exclude_none=True, exclude_unset=True, by_alias=True + ) + translated_output["transform"] = remove_units_in_dict(transform) + translated_outputs.append(translated_output) return translated_outputs diff --git a/tests/simulation/translator/test_solver_translator.py b/tests/simulation/translator/test_solver_translator.py index 336d2544f..f3bd2b86d 100644 --- a/tests/simulation/translator/test_solver_translator.py +++ b/tests/simulation/translator/test_solver_translator.py @@ -58,16 +58,23 @@ ThermalState, ) from flow360.component.simulation.outputs.output_entities import ( - AmbientLight, - DirectionalLight, Isosurface, - OrthographicProjection, Point, PointArray, PointArray2D, + Slice, +) +from flow360.component.simulation.outputs.output_render_types import ( + AmbientLight, + DirectionalLight, + OrthographicProjection, + PBRMaterial, RenderCameraConfig, + RenderEnvironmentConfig, RenderLightingConfig, - Slice, + RenderMaterialConfig, + SkyboxBackground, + SkyboxTexture, StaticCamera, ) from flow360.component.simulation.outputs.outputs import ( @@ -257,7 +264,7 @@ def get_om6Wing_tutorial_param(): output_fields=["Cp"], ), RenderOutput( - entities=[my_isosurface], + isosurfaces=[my_isosurface], output_fields=["qcriterion"], camera=RenderCameraConfig( view=StaticCamera(position=(20, 20, 20), target=(0, 0, 0)), @@ -269,6 +276,20 @@ def get_om6Wing_tutorial_param(): intensity=1.5, color=(255, 255, 255), direction=(-1.0, -1.0, -1.0) ), ), + environment=RenderEnvironmentConfig( + background=SkyboxBackground(texture=SkyboxTexture.SKY) + ), + materials=RenderMaterialConfig( + materials=[ + PBRMaterial( + color=(245, 245, 246), + alpha=1.0, + roughness=0.3, + f0=(0.56, 0.56, 0.56), + ) + ], + mappings={"iso": 0}, + ), ), ], ) From 7bc6722e8d7f5c983b385647c207777abec55189 Mon Sep 17 00:00:00 2001 From: andrzej-krupka Date: Fri, 21 Nov 2025 13:33:34 +0100 Subject: [PATCH 4/7] Fix render output translation logic --- .../component/simulation/outputs/outputs.py | 6 +++--- .../translator/solver_translator.py | 19 ++++++++++++++--- .../component/simulation/translator/utils.py | 21 ++++++++++--------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 557d53e28..9417c7c14 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -713,12 +713,12 @@ class RenderOutput(_AnimationSettings): """ name: Optional[str] = pd.Field("Render output", description="Name of the `IsosurfaceOutput`.") + entities: Optional[EntityList[Surface, Slice]] = pd.Field( + None, description="List of of :class:`~flow360.Surface` or `~flow360.Slice` entities." + ) isosurfaces: Optional[UniqueItemList[Isosurface]] = pd.Field( None, description="List of :class:`~flow360.Isosurface` entities." ) - surfaces: Optional[EntityList[Surface]] = pd.Field( - None, description="List of of :class:`~flow360.Surface` entities." - ) output_fields: UniqueItemList[Union[CommonFieldNames, str]] = pd.Field( description="List of output variables. Including " ":ref:`universal output variables` and :class:`UserDefinedField`." diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index 48a203303..565ddae0a 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -578,7 +578,11 @@ def translate_isosurface_output( return translated_output -def translate_render_output(output_params: list, injection_function): +def translate_render_output( + input_params: SimulationParams, + output_params: list, + injection_function +): """Translate render output settings.""" renders = get_all_entries_of_type(output_params, RenderOutput) @@ -604,14 +608,23 @@ def translate_render_output(output_params: list, injection_function): translation_func=translate_output_fields, to_list=False, entity_injection_func=injection_function, + entity_type_to_include=Isosurface, entity_list_attribute_name="isosurfaces", + entity_injection_input_params=input_params, ), "surfaces": translate_setting_and_apply_to_all_entities( [render], RenderOutput, translation_func=translate_output_fields, to_list=False, - entity_list_attribute_name="surfaces", + entity_type_to_include=Surface, + ), + "slices": translate_setting_and_apply_to_all_entities( + [render], + RenderOutput, + translation_func=translate_output_fields, + to_list=False, + entity_type_to_include=Slice, ), "camera": remove_units_in_dict(camera), "lighting": remove_units_in_dict(lighting), @@ -1016,7 +1029,7 @@ def translate_output(input_params: SimulationParams, translated: dict): ##:: Step6: Get translated["renderOutput"] if has_instance_in_list(outputs, RenderOutput): - translated["renderOutput"] = translate_render_output(outputs, inject_isosurface_info) + translated["renderOutput"] = translate_render_output(input_params, outputs, inject_isosurface_info) ##:: Step7: Get translated["monitorOutput"] probe_output = {} diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index 72bea0adb..529fba481 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -287,6 +287,7 @@ def translate_setting_and_apply_to_all_entities( use_instance_name_as_key=False, use_sub_item_as_key=False, entity_type_to_include=None, + entity_list_attribute_name="entities", **kwargs, ): """ @@ -369,23 +370,23 @@ def translate_setting_and_apply_to_all_entities( # pylint: disable=too-many-nested-blocks for obj in obj_list: if class_type and is_exact_instance(obj, class_type): - list_of_entities = [] - if "entities" in obj.__class__.model_fields: - if obj.entities is None or ( - "stored_entities" in obj.entities.__class__.model_fields - and obj.entities.stored_entities is None + if entity_list_attribute_name in obj.__class__.model_fields: + entity_list = getattr(obj, entity_list_attribute_name) + if entity_list is None or ( + "stored_entities" in entity_list.__class__.model_fields + and entity_list.stored_entities is None ): # unique item list does not allow None "items" for now. continue - if isinstance(obj.entities, EntityList): + if isinstance(entity_list, EntityList): list_of_entities = ( - obj.entities.stored_entities + entity_list.stored_entities if lump_list_of_entities is False - else [obj.entities] + else [entity_list] ) - elif isinstance(obj.entities, UniqueItemList): + elif isinstance(entity_list, UniqueItemList): list_of_entities = ( - obj.entities.items if lump_list_of_entities is False else [obj.entities] + entity_list.items if lump_list_of_entities is False else [entity_list] ) elif "entity_pairs" in obj.__class__.model_fields: # Note: This is only used in Periodic BC and lump_list_of_entities is not relavant From 1ff3107251e3b34531def1fb87db7c0bd117db3b Mon Sep 17 00:00:00 2001 From: andrzej-krupka Date: Tue, 25 Nov 2025 17:33:30 +0000 Subject: [PATCH 5/7] Add defaults for render classes --- .../simulation/outputs/output_render_types.py | 138 +++++++++++++++++- .../component/simulation/outputs/outputs.py | 10 +- .../translator/solver_translator.py | 13 +- 3 files changed, 147 insertions(+), 14 deletions(-) diff --git a/flow360/component/simulation/outputs/output_render_types.py b/flow360/component/simulation/outputs/output_render_types.py index 704f95b1f..5958fe0dd 100644 --- a/flow360/component/simulation/outputs/output_render_types.py +++ b/flow360/component/simulation/outputs/output_render_types.py @@ -4,17 +4,17 @@ import pydantic as pd +import flow360.component.simulation.units as u from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.unit_system import AngleType, LengthType from flow360.component.types import Color, Vector + class StaticCamera(Flow360BaseModel): position: LengthType.Point = pd.Field(description="Position of the camera in the scene") target: LengthType.Point = pd.Field(description="Target point of the camera") - up: Optional[Vector] = pd.Field( - default=(0, 1, 0), description="Up vector, if not specified assume Y+" - ) + up: Optional[Vector] = pd.Field(default=(0, 1, 0), description="Up vector, if not specified assume Y+") class Keyframe(Flow360BaseModel): @@ -47,6 +47,34 @@ class RenderCameraConfig(Flow360BaseModel): view: AllCameraTypes = pd.Field() projection: Union[OrthographicProjection, PerspectiveProjection] = pd.Field() + @classmethod + def orthographic(cls, x=1, y=1, z=1, scale=1): + return RenderCameraConfig( + view=StaticCamera( + position=(x * scale, y * scale, z * scale) * u.m, + target=(0, 0, 0) * u.m + ), + projection=OrthographicProjection( + width=scale * u.m, + near=0.01 * u.m, + far=50 * scale * u.m + ) + ) + + @classmethod + def perspective(cls, x=1, y=1, z=1, scale=1): + return RenderCameraConfig( + view=StaticCamera( + position=(x * scale, y * scale, z * scale) * u.m, + target=(0, 0, 0) * u.m + ), + projection=PerspectiveProjection( + fov=60 * u.deg, + near=0.01 * u.m, + far=50 * scale * u.m + ) + ) + class AmbientLight(Flow360BaseModel): intensity: float = pd.Field() @@ -63,10 +91,24 @@ class RenderLightingConfig(Flow360BaseModel): directional: DirectionalLight = pd.Field() ambient: Optional[AmbientLight] = pd.Field(None) + @classmethod + def default(cls): + return RenderLightingConfig( + ambient=AmbientLight( + intensity=0.5, + color=(255, 255, 255) + ), + directional=DirectionalLight( + intensity=1.5, + color=(255, 255, 255), + direction=(-1.0, -1.0, -1.0) + ) + ) + class RenderBackgroundBase(Flow360BaseModel, metaclass=abc.ABCMeta): type: str = pd.Field(default="", frozen=True) - + class SolidBackground(RenderBackgroundBase): type: str = pd.Field(default="solid", frozen=True) @@ -75,6 +117,7 @@ class SolidBackground(RenderBackgroundBase): class SkyboxTexture(str, Enum): SKY = "sky" + GRADIENT = "gradient" class SkyboxBackground(RenderBackgroundBase): @@ -88,6 +131,30 @@ class SkyboxBackground(RenderBackgroundBase): class RenderEnvironmentConfig(Flow360BaseModel): background: AllBackgroundTypes = pd.Field() + @classmethod + def simple(cls): + return RenderEnvironmentConfig( + background=SolidBackground( + color=(207, 226, 230) + ) + ) + + @classmethod + def sky(cls): + return RenderEnvironmentConfig( + background=SkyboxBackground( + texture=SkyboxTexture.SKY + ) + ) + + @classmethod + def gradient(cls): + return RenderEnvironmentConfig( + background=SkyboxBackground( + texture=SkyboxTexture.GRADIENT + ) + ) + class RenderMaterialBase(Flow360BaseModel, metaclass=abc.ABCMeta): type: str = pd.Field(default="", frozen=True) @@ -100,6 +167,24 @@ class PBRMaterial(RenderMaterialBase): f0: Vector = pd.Field(default=(0.03, 0.03, 0.03)) type: str = pd.Field(default="pbr", frozen=True) + @classmethod + def metal(cls, shine=0.5, alpha=1.0): + return PBRMaterial( + color=(255, 255, 255), + alpha=alpha, + roughness=1 - shine, + f0=(0.56, 0.56, 0.56) + ) + + @classmethod + def plastic(cls, shine=0.5, alpha=1.0): + return PBRMaterial( + color=(255, 255, 255), + alpha=alpha, + roughness=1 - shine, + f0=(0.03, 0.03, 0.03) + ) + class ColorKey(Flow360BaseModel): color: Color = pd.Field(default=[255, 255, 255]) @@ -110,10 +195,53 @@ class FieldMaterial(RenderMaterialBase): alpha: float = pd.Field(default=1) output_field: str = pd.Field(default="") min: float = pd.Field(default=0) - max: float = pd.Field(default=0) + max: float = pd.Field(default=1) colormap: List[ColorKey] = pd.Field() type: str = pd.Field(default="field", frozen=True) + @classmethod + def greyscale(cls, field, min=0, max=1, alpha=1): + return FieldMaterial( + alpha=alpha, + output_field=field, + min=min, + max=max, + colormap = [ + ColorKey(color=(0, 0, 0), value=0), + ColorKey(color=(255, 255, 255), value=1.0) + ] + ) + + @classmethod + def hot_cold(cls, field, min=0, max=1, alpha=1): + return FieldMaterial( + alpha=alpha, + output_field=field, + min=min, + max=max, + colormap = [ + ColorKey(color=(0, 0, 255), value=0), + ColorKey(color=(255, 255, 255), value=0.5), + ColorKey(color=(255, 0, 0), value=1.0) + ] + ) + + @classmethod + def rainbow(cls, field, min=0, max=1, alpha=1): + return FieldMaterial( + alpha=alpha, + output_field=field, + min=min, + max=max, + colormap = [ + ColorKey(color=(0, 0, 255), value=0), + ColorKey(color=(0, 255, 255), value=0.25), + ColorKey(color=(0, 255, 0), value=0.5), + ColorKey(color=(255, 255, 0), value=0.75), + ColorKey(color=(255, 0, 0), value=1.0) + ] + ) + AllMaterialTypes = Union[PBRMaterial, FieldMaterial] diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 9417c7c14..baf802f36 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -723,13 +723,11 @@ class RenderOutput(_AnimationSettings): description="List of output variables. Including " ":ref:`universal output variables` and :class:`UserDefinedField`." ) - camera: RenderCameraConfig = pd.Field(description="Camera settings") - lighting: RenderLightingConfig = pd.Field(description="Lighting settings") - environment: RenderEnvironmentConfig = pd.Field(description="Environment settings") + camera: RenderCameraConfig = pd.Field(description="Camera settings", default_factory=RenderCameraConfig.orthographic) + lighting: RenderLightingConfig = pd.Field(description="Lighting settings", default_factory=RenderLightingConfig.default) + environment: RenderEnvironmentConfig = pd.Field(description="Environment settings", default_factory=RenderEnvironmentConfig.simple) materials: RenderMaterialConfig = pd.Field(description="Material settings") - transform: Optional[Transform] = pd.Field( - None, description="Optional model transform to apply to all entities" - ) + transform: Optional[Transform] = pd.Field(None, description="Optional model transform to apply to all entities") output_type: Literal["RenderOutput"] = pd.Field("RenderOutput", frozen=True) diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index 565ddae0a..4934ec794 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -581,7 +581,8 @@ def translate_isosurface_output( def translate_render_output( input_params: SimulationParams, output_params: list, - injection_function + isosurface_injection_function, + slice_injection_function ): """Translate render output settings.""" @@ -607,7 +608,7 @@ def translate_render_output( RenderOutput, translation_func=translate_output_fields, to_list=False, - entity_injection_func=injection_function, + entity_injection_func=isosurface_injection_function, entity_type_to_include=Isosurface, entity_list_attribute_name="isosurfaces", entity_injection_input_params=input_params, @@ -624,6 +625,7 @@ def translate_render_output( RenderOutput, translation_func=translate_output_fields, to_list=False, + entity_injection_func=slice_injection_function, entity_type_to_include=Slice, ), "camera": remove_units_in_dict(camera), @@ -1029,7 +1031,12 @@ def translate_output(input_params: SimulationParams, translated: dict): ##:: Step6: Get translated["renderOutput"] if has_instance_in_list(outputs, RenderOutput): - translated["renderOutput"] = translate_render_output(input_params, outputs, inject_isosurface_info) + translated["renderOutput"] = translate_render_output( + input_params, + outputs, + inject_isosurface_info, + inject_slice_info + ) ##:: Step7: Get translated["monitorOutput"] probe_output = {} From 38101f4bb605e020842730d60847f20bcae177bf Mon Sep 17 00:00:00 2001 From: andrzej-krupka Date: Wed, 26 Nov 2025 13:34:03 +0000 Subject: [PATCH 6/7] Refactor render interface after MC meeting --- flow360/__init__.py | 1 - .../simulation/outputs/output_render_types.py | 7 +--- .../component/simulation/outputs/outputs.py | 16 ++++---- .../translator/solver_translator.py | 40 +++++++++++++------ .../translator/test_solver_translator.py | 20 ++++------ 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/flow360/__init__.py b/flow360/__init__.py index fa24b97a6..c08f9a071 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -142,7 +142,6 @@ RenderCameraConfig, RenderEnvironmentConfig, RenderLightingConfig, - RenderMaterialConfig, SkyboxBackground, SkyboxTexture, SolidBackground, diff --git a/flow360/component/simulation/outputs/output_render_types.py b/flow360/component/simulation/outputs/output_render_types.py index 5958fe0dd..96d9364dc 100644 --- a/flow360/component/simulation/outputs/output_render_types.py +++ b/flow360/component/simulation/outputs/output_render_types.py @@ -244,12 +244,7 @@ def rainbow(cls, field, min=0, max=1, alpha=1): AllMaterialTypes = Union[PBRMaterial, FieldMaterial] - - -class RenderMaterialConfig(Flow360BaseModel): - materials: List[AllMaterialTypes] = pd.Field([]) - mappings: Dict[str, int] = pd.Field({}) - + class Transform(Flow360BaseModel): translation: LengthType.Point = pd.Field(default=[0, 0, 0]) diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index baf802f36..7a394bf02 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -6,7 +6,7 @@ """ # pylint: disable=too-many-lines -from typing import Annotated, List, Literal, Optional, Union, get_args +from typing import Annotated, Dict, List, Literal, Optional, Union, get_args import pydantic as pd @@ -34,10 +34,10 @@ get_field_values, ) from flow360.component.simulation.outputs.output_render_types import ( + AllMaterialTypes, RenderCameraConfig, RenderEnvironmentConfig, RenderLightingConfig, - RenderMaterialConfig, Transform, ) from flow360.component.simulation.primitives import ( @@ -713,20 +713,22 @@ class RenderOutput(_AnimationSettings): """ name: Optional[str] = pd.Field("Render output", description="Name of the `IsosurfaceOutput`.") - entities: Optional[EntityList[Surface, Slice]] = pd.Field( - None, description="List of of :class:`~flow360.Surface` or `~flow360.Slice` entities." + surfaces: Optional[EntityList[Surface]] = pd.Field( + None, description="List of of :class:`~flow360.Surface` entities." + ) + slices: Optional[EntityList[Slice]] = pd.Field( + None, description="List of of :class:`~flow360.Slice` entities." ) isosurfaces: Optional[UniqueItemList[Isosurface]] = pd.Field( None, description="List of :class:`~flow360.Isosurface` entities." ) output_fields: UniqueItemList[Union[CommonFieldNames, str]] = pd.Field( - description="List of output variables. Including " - ":ref:`universal output variables` and :class:`UserDefinedField`." + [], description="List of output variables." ) camera: RenderCameraConfig = pd.Field(description="Camera settings", default_factory=RenderCameraConfig.orthographic) lighting: RenderLightingConfig = pd.Field(description="Lighting settings", default_factory=RenderLightingConfig.default) environment: RenderEnvironmentConfig = pd.Field(description="Environment settings", default_factory=RenderEnvironmentConfig.simple) - materials: RenderMaterialConfig = pd.Field(description="Material settings") + materials: Dict[str, AllMaterialTypes] = pd.Field(description="Material settings per entity") transform: Optional[Transform] = pd.Field(None, description="Optional model transform to apply to all entities") output_type: Literal["RenderOutput"] = pd.Field("RenderOutput", frozen=True) diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index 4934ec794..292082978 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -99,6 +99,7 @@ ) from flow360.component.simulation.simulation_params import SimulationParams from flow360.component.simulation.time_stepping.time_stepping import Steady, Unsteady +from flow360.component.simulation.outputs.output_render_types import FieldMaterial from flow360.component.simulation.translator.user_expression_utils import ( udf_prepending_code, ) @@ -596,29 +597,31 @@ def translate_render_output( environment = render.environment.model_dump( exclude_none=True, exclude_unset=True, by_alias=True ) - materials = render.materials.model_dump( - exclude_none=True, exclude_unset=True, by_alias=True - ) + + materials = {} + + for name, material in render.materials.items(): + material = material.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) + materials[name] = material + + if "outputField" in material and material["outputField"] not in render.output_fields: + print(f"Adding material field: {material['outputField']}") + render.output_fields.append(material["outputField"]) + + print(f"Render output fields are: {render.output_fields}") + + print(f"Materials are: {materials}") translated_output = { "animationFrequency": render.frequency, "animationFrequencyOffset": render.frequency_offset, - "isoSurfaces": translate_setting_and_apply_to_all_entities( - [render], - RenderOutput, - translation_func=translate_output_fields, - to_list=False, - entity_injection_func=isosurface_injection_function, - entity_type_to_include=Isosurface, - entity_list_attribute_name="isosurfaces", - entity_injection_input_params=input_params, - ), "surfaces": translate_setting_and_apply_to_all_entities( [render], RenderOutput, translation_func=translate_output_fields, to_list=False, entity_type_to_include=Surface, + entity_list_attribute_name="surfaces" ), "slices": translate_setting_and_apply_to_all_entities( [render], @@ -627,6 +630,17 @@ def translate_render_output( to_list=False, entity_injection_func=slice_injection_function, entity_type_to_include=Slice, + entity_list_attribute_name="slices" + ), + "isoSurfaces": translate_setting_and_apply_to_all_entities( + [render], + RenderOutput, + translation_func=translate_output_fields, + to_list=False, + entity_injection_func=isosurface_injection_function, + entity_type_to_include=Isosurface, + entity_list_attribute_name="isosurfaces", + entity_injection_input_params=input_params, ), "camera": remove_units_in_dict(camera), "lighting": remove_units_in_dict(lighting), diff --git a/tests/simulation/translator/test_solver_translator.py b/tests/simulation/translator/test_solver_translator.py index f3bd2b86d..b7a15975a 100644 --- a/tests/simulation/translator/test_solver_translator.py +++ b/tests/simulation/translator/test_solver_translator.py @@ -72,7 +72,6 @@ RenderCameraConfig, RenderEnvironmentConfig, RenderLightingConfig, - RenderMaterialConfig, SkyboxBackground, SkyboxTexture, StaticCamera, @@ -279,17 +278,14 @@ def get_om6Wing_tutorial_param(): environment=RenderEnvironmentConfig( background=SkyboxBackground(texture=SkyboxTexture.SKY) ), - materials=RenderMaterialConfig( - materials=[ - PBRMaterial( - color=(245, 245, 246), - alpha=1.0, - roughness=0.3, - f0=(0.56, 0.56, 0.56), - ) - ], - mappings={"iso": 0}, - ), + materials={ + "iso": PBRMaterial( + color=(245, 245, 246), + alpha=1.0, + roughness=0.3, + f0=(0.56, 0.56, 0.56), + ) + } ), ], ) From 42400986ce43844b79c993eb4ec0ee3868af47ce Mon Sep 17 00:00:00 2001 From: andrzej-krupka Date: Mon, 1 Dec 2025 10:36:23 +0000 Subject: [PATCH 7/7] Refactor material mapping interface --- flow360/__init__.py | 1 + .../simulation/outputs/output_render_types.py | 3 +- .../component/simulation/outputs/outputs.py | 28 ++++--- .../translator/solver_translator.py | 78 +++++++++---------- .../component/simulation/translator/utils.py | 2 +- 5 files changed, 55 insertions(+), 57 deletions(-) diff --git a/flow360/__init__.py b/flow360/__init__.py index c08f9a071..13037206d 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -155,6 +155,7 @@ MovingStatistic, Observer, ProbeOutput, + RenderOutputGroup, RenderOutput, SliceOutput, StreamlineOutput, diff --git a/flow360/component/simulation/outputs/output_render_types.py b/flow360/component/simulation/outputs/output_render_types.py index 96d9364dc..fb620538d 100644 --- a/flow360/component/simulation/outputs/output_render_types.py +++ b/flow360/component/simulation/outputs/output_render_types.py @@ -10,7 +10,6 @@ from flow360.component.types import Color, Vector - class StaticCamera(Flow360BaseModel): position: LengthType.Point = pd.Field(description="Position of the camera in the scene") target: LengthType.Point = pd.Field(description="Target point of the camera") @@ -243,7 +242,7 @@ def rainbow(cls, field, min=0, max=1, alpha=1): ) -AllMaterialTypes = Union[PBRMaterial, FieldMaterial] +AnyMaterial = Union[PBRMaterial, FieldMaterial] class Transform(Flow360BaseModel): diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 7a394bf02..15dac1256 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -34,7 +34,7 @@ get_field_values, ) from flow360.component.simulation.outputs.output_render_types import ( - AllMaterialTypes, + AnyMaterial, RenderCameraConfig, RenderEnvironmentConfig, RenderLightingConfig, @@ -683,6 +683,19 @@ def allow_only_simulation_surfaces_or_imported_surfaces(cls, value): return value +class RenderOutputGroup(Flow360BaseModel): + surfaces: Optional[EntityList[Surface]] = pd.Field( + None, description="List of of :class:`~flow360.Surface` entities." + ) + slices: Optional[EntityList[Slice]] = pd.Field( + None, description="List of of :class:`~flow360.Slice` entities." + ) + isosurfaces: Optional[UniqueItemList[Isosurface]] = pd.Field( + None, description="List of :class:`~flow360.Isosurface` entities." + ) + material: AnyMaterial = pd.Field() + + class RenderOutput(_AnimationSettings): """ @@ -712,23 +725,14 @@ class RenderOutput(_AnimationSettings): ==== """ - name: Optional[str] = pd.Field("Render output", description="Name of the `IsosurfaceOutput`.") - surfaces: Optional[EntityList[Surface]] = pd.Field( - None, description="List of of :class:`~flow360.Surface` entities." - ) - slices: Optional[EntityList[Slice]] = pd.Field( - None, description="List of of :class:`~flow360.Slice` entities." - ) - isosurfaces: Optional[UniqueItemList[Isosurface]] = pd.Field( - None, description="List of :class:`~flow360.Isosurface` entities." - ) + name: str = pd.Field("Render output", description="Name of the `RenderOutput`.") + groups: List[RenderOutputGroup] = pd.Field("Render groups") output_fields: UniqueItemList[Union[CommonFieldNames, str]] = pd.Field( [], description="List of output variables." ) camera: RenderCameraConfig = pd.Field(description="Camera settings", default_factory=RenderCameraConfig.orthographic) lighting: RenderLightingConfig = pd.Field(description="Lighting settings", default_factory=RenderLightingConfig.default) environment: RenderEnvironmentConfig = pd.Field(description="Environment settings", default_factory=RenderEnvironmentConfig.simple) - materials: Dict[str, AllMaterialTypes] = pd.Field(description="Material settings per entity") transform: Optional[Transform] = pd.Field(None, description="Optional model transform to apply to all entities") output_type: Literal["RenderOutput"] = pd.Field("RenderOutput", frozen=True) diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index 292082978..4052f12b2 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -84,7 +84,7 @@ TimeAverageSurfaceProbeOutput, TimeAverageVolumeOutput, UserDefinedField, - VolumeOutput, + VolumeOutput, RenderOutputGroup, ) from flow360.component.simulation.primitives import ( BOUNDARY_FULL_NAME_WHEN_NOT_FOUND, @@ -115,7 +115,6 @@ replace_dict_key, translate_setting_and_apply_to_all_entities, translate_value_or_expression_object, - update_dict_recursively, ) from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.user_code.core.types import ( @@ -598,56 +597,51 @@ def translate_render_output( exclude_none=True, exclude_unset=True, by_alias=True ) - materials = {} - - for name, material in render.materials.items(): - material = material.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) - materials[name] = material - + for render_group in render.groups: + material = render_group.material.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) if "outputField" in material and material["outputField"] not in render.output_fields: - print(f"Adding material field: {material['outputField']}") - render.output_fields.append(material["outputField"]) - - print(f"Render output fields are: {render.output_fields}") - - print(f"Materials are: {materials}") + render.output_fields.append(material["outputField"]) translated_output = { + "name": render.name, "animationFrequency": render.frequency, "animationFrequencyOffset": render.frequency_offset, - "surfaces": translate_setting_and_apply_to_all_entities( - [render], - RenderOutput, - translation_func=translate_output_fields, - to_list=False, - entity_type_to_include=Surface, - entity_list_attribute_name="surfaces" - ), - "slices": translate_setting_and_apply_to_all_entities( - [render], - RenderOutput, - translation_func=translate_output_fields, - to_list=False, - entity_injection_func=slice_injection_function, - entity_type_to_include=Slice, - entity_list_attribute_name="slices" - ), - "isoSurfaces": translate_setting_and_apply_to_all_entities( - [render], - RenderOutput, - translation_func=translate_output_fields, - to_list=False, - entity_injection_func=isosurface_injection_function, - entity_type_to_include=Isosurface, - entity_list_attribute_name="isosurfaces", - entity_injection_input_params=input_params, - ), + "groups": [], "camera": remove_units_in_dict(camera), "lighting": remove_units_in_dict(lighting), "environment": remove_units_in_dict(environment), - "materials": remove_units_in_dict(materials), + "outputFields": translate_output_fields(render)["outputFields"] } + for render_group in render.groups: + material = render_group.material.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) + translated_output["groups"].append({ + "surfaces": translate_setting_and_apply_to_all_entities( + [render_group], + RenderOutputGroup, + to_list=False, + entity_type_to_include=Surface, + entity_list_attribute_name="surfaces" + ), + "slices": translate_setting_and_apply_to_all_entities( + [render_group], + RenderOutputGroup, + to_list=False, + entity_injection_func=slice_injection_function, + entity_type_to_include=Slice, + entity_list_attribute_name="slices" + ), + "isoSurfaces": translate_setting_and_apply_to_all_entities( + [render_group], + RenderOutputGroup, + to_list=False, + entity_injection_func=isosurface_injection_function, + entity_type_to_include=Isosurface, + entity_list_attribute_name="isosurfaces", + entity_injection_input_params=input_params, + ), + "material": remove_units_in_dict(material) + }) if render.transform: transform = render.transform.model_dump( exclude_none=True, exclude_unset=True, by_alias=True diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index 529fba481..7e1f66796 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -278,7 +278,7 @@ def _get_key_name(entity: EntityBase): def translate_setting_and_apply_to_all_entities( obj_list: list, class_type, - translation_func, + translation_func=lambda x, **kwargs: {}, to_list: bool = False, entity_injection_func=lambda x, **kwargs: {}, pass_translated_setting_to_entity_injection=False,