Skip to content

Commit fabd0c8

Browse files
Andrzej Krupkaandrzej-krupka
authored andcommitted
Added basic data models for render output
1 parent 8bc45aa commit fabd0c8

File tree

6 files changed

+200
-13
lines changed

6 files changed

+200
-13
lines changed

flow360/component/simulation/outputs/output_entities.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from flow360.component.simulation.framework.base_model import Flow360BaseModel
99
from flow360.component.simulation.framework.entity_base import EntityBase, generate_uuid
1010
from flow360.component.simulation.outputs.output_fields import IsoSurfaceFieldNames
11-
from flow360.component.simulation.unit_system import LengthType
11+
from flow360.component.simulation.unit_system import AngleType, LengthType
1212
from flow360.component.simulation.user_code.core.types import (
1313
Expression,
1414
UnytQuantity,
@@ -21,7 +21,7 @@
2121
solver_variable_to_user_variable,
2222
)
2323
from flow360.component.simulation.user_code.core.utils import is_runtime_expression
24-
from flow360.component.types import Axis
24+
from flow360.component.types import Axis, Color, Vector
2525

2626

2727
class _OutputItemBase(Flow360BaseModel):
@@ -289,3 +289,63 @@ class PointArray2D(_PointEntityBase):
289289
v_axis_vector: LengthType.Axis = pd.Field(description="The scaled v-axis of the parallelogram.")
290290
u_number_of_points: int = pd.Field(ge=2, description="The number of points along the u axis.")
291291
v_number_of_points: int = pd.Field(ge=2, description="The number of points along the v axis.")
292+
293+
294+
# Linear interpolation between keyframes
295+
class KeyframeCamera(Flow360BaseModel):
296+
pass
297+
298+
299+
# Follow a cubic spline curve
300+
class SplineCamera(Flow360BaseModel):
301+
pass
302+
303+
304+
# Orbit a point (start and end points specified in spherical coordinates)
305+
class OrbitCamera(Flow360BaseModel):
306+
pass
307+
308+
309+
# Static camera setup in Cartesian coordinates
310+
class StaticCamera(Flow360BaseModel):
311+
position: LengthType.Point = pd.Field(description="Position of the camera in the scene")
312+
target: LengthType.Point = pd.Field(description="Target point of the camera")
313+
up: Optional[Vector] = pd.Field((0, 1, 0), description="Up vector, if not specified assume Y+")
314+
315+
316+
# Ortho projection, parallel lines stay parallel
317+
class OrthographicProjection(Flow360BaseModel):
318+
width: LengthType = pd.Field()
319+
near: LengthType = pd.Field()
320+
far: LengthType = pd.Field()
321+
322+
323+
# Perspective projection
324+
class PerspectiveProjection(Flow360BaseModel):
325+
fov: AngleType = pd.Field()
326+
near: LengthType = pd.Field()
327+
far: LengthType = pd.Field()
328+
329+
330+
# Only basic static camera with ortho projection supported currently
331+
class RenderCameraConfig(Flow360BaseModel):
332+
view: StaticCamera = pd.Field()
333+
projection: OrthographicProjection = pd.Field()
334+
335+
336+
# Ambient light, added by default to all pixels in the scene, simulates global illumination
337+
class AmbientLight(Flow360BaseModel):
338+
intensity: float = pd.Field()
339+
color: Color = pd.Field()
340+
341+
342+
# Ambient light, added by default to all pixels in the scene, simulates global illumination
343+
class DirectionalLight(Flow360BaseModel):
344+
intensity: float = pd.Field()
345+
color: Color = pd.Field()
346+
direction: Vector = pd.Field()
347+
348+
349+
class RenderLightingConfig(Flow360BaseModel):
350+
ambient: AmbientLight = pd.Field()
351+
directional: DirectionalLight = pd.Field()

flow360/component/simulation/outputs/outputs.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
Point,
2222
PointArray,
2323
PointArray2D,
24+
RenderCameraConfig,
25+
RenderLightingConfig,
2426
Slice,
2527
)
2628
from flow360.component.simulation.outputs.output_fields import (
@@ -665,6 +667,49 @@ def allow_only_simulation_surfaces_or_imported_surfaces(cls, value):
665667
return value
666668

667669

670+
class RenderOutput(_AnimationSettings):
671+
"""
672+
673+
:class:`RenderOutput` class for backend rendered output settings.
674+
675+
Example
676+
-------
677+
678+
Define the :class:`RenderOutput` of :code:`qcriterion` on two isosurfaces:
679+
680+
>>> fl.RenderOutput(
681+
... isosurfaces=[
682+
... fl.Isosurface(
683+
... name="Isosurface_T_0.1",
684+
... iso_value=0.1,
685+
... field="T",
686+
... ),
687+
... fl.Isosurface(
688+
... name="Isosurface_p_0.5",
689+
... iso_value=0.5,
690+
... field="p",
691+
... ),
692+
... ],
693+
... output_field="qcriterion",
694+
... )
695+
696+
====
697+
"""
698+
699+
name: Optional[str] = pd.Field("Render output", description="Name of the `IsosurfaceOutput`.")
700+
entities: UniqueItemList[Isosurface] = pd.Field(
701+
alias="isosurfaces",
702+
description="List of :class:`~flow360.Isosurface` entities.",
703+
)
704+
output_fields: UniqueItemList[Union[CommonFieldNames, str]] = pd.Field(
705+
description="List of output variables. Including "
706+
":ref:`universal output variables<UniversalVariablesV2>` and :class:`UserDefinedField`."
707+
)
708+
camera: RenderCameraConfig = pd.Field(description="Camera settings")
709+
lighting: RenderLightingConfig = pd.Field(description="Lighting settings")
710+
output_type: Literal["RenderOutput"] = pd.Field("RenderOutput", frozen=True)
711+
712+
668713
class ProbeOutput(_OutputBase):
669714
"""
670715
:class:`ProbeOutput` class for setting output data probed at monitor points.
@@ -1293,6 +1338,7 @@ class TimeAverageStreamlineOutput(StreamlineOutput):
12931338
AeroAcousticOutput,
12941339
StreamlineOutput,
12951340
TimeAverageStreamlineOutput,
1341+
RenderOutput,
12961342
],
12971343
pd.Field(discriminator="output_type"),
12981344
]

flow360/component/simulation/translator/solver_translator.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
IsosurfaceOutput,
6767
MonitorOutputType,
6868
ProbeOutput,
69+
RenderOutput,
6970
Slice,
7071
SliceOutput,
7172
StreamlineOutput,
@@ -102,6 +103,7 @@
102103
from flow360.component.simulation.translator.utils import (
103104
_get_key_name,
104105
convert_tuples_to_lists,
106+
get_all_entries_of_type,
105107
get_global_setting_from_first_instance,
106108
has_instance_in_list,
107109
inline_expressions_in_dict,
@@ -110,6 +112,7 @@
110112
replace_dict_key,
111113
translate_setting_and_apply_to_all_entities,
112114
translate_value_or_expression_object,
115+
update_dict_recursively,
113116
)
114117
from flow360.component.simulation.unit_system import LengthType
115118
from flow360.component.simulation.user_code.core.types import (
@@ -190,10 +193,11 @@ def init_output_base(obj_list, class_type: Type, is_average: bool):
190193
class_type,
191194
"output_format",
192195
)
193-
assert output_format is not None
194-
if output_format == "both":
195-
output_format = "paraview,tecplot"
196-
base["outputFormat"] = output_format
196+
if not no_format:
197+
assert output_format is not None
198+
if output_format == "both":
199+
output_format = "paraview,tecplot"
200+
base["outputFormat"] = output_format
197201

198202
if is_average:
199203
base = init_average_output(base, obj_list, class_type)
@@ -382,6 +386,14 @@ def get_monitor_locations(entities: EntityList):
382386
return monitor_locations
383387

384388

389+
def inject_render_info(entity: Isosurface):
390+
"""inject entity info"""
391+
return {
392+
"surfaceField": entity.field,
393+
"surfaceFieldMagnitude": entity.iso_value,
394+
}
395+
396+
385397
def inject_probe_info(entity: EntityList):
386398
"""inject entity info"""
387399

@@ -565,6 +577,35 @@ def translate_isosurface_output(
565577
return translated_output
566578

567579

580+
def translate_render_output(output_params: list, injection_function):
581+
"""Translate render output settings."""
582+
583+
renders = get_all_entries_of_type(output_params, RenderOutput)
584+
585+
translated_outputs = []
586+
587+
for render in renders:
588+
camera = render.camera.model_dump(exclude_none=True, exclude_unset=True, by_alias=True)
589+
lighting = render.lighting.model_dump(exclude_none=True, exclude_unset=True, by_alias=True)
590+
591+
translated_output = {
592+
"animationFrequency": render.frequency,
593+
"animationFrequencyOffset": render.frequency_offset,
594+
"isoSurfaces": translate_setting_and_apply_to_all_entities(
595+
[render],
596+
RenderOutput,
597+
translation_func=translate_output_fields,
598+
to_list=False,
599+
entity_injection_func=injection_function,
600+
),
601+
"camera": remove_units_in_dict(camera),
602+
"lighting": remove_units_in_dict(lighting),
603+
}
604+
translated_outputs.append(translated_output)
605+
606+
return translated_outputs
607+
608+
568609
def translate_surface_slice_output(
569610
output_params: list,
570611
output_class: Union[SurfaceSliceOutput],
@@ -924,7 +965,11 @@ def translate_output(input_params: SimulationParams, translated: dict):
924965
if imported_surface_output_configs["surfaces"]:
925966
translated["timeAverageImportedSurfaceOutput"] = imported_surface_output_configs
926967

927-
##:: Step6: Get translated["monitorOutput"]
968+
##:: Step6: Get translated["renderOutput"]
969+
if has_instance_in_list(outputs, RenderOutput):
970+
translated["renderOutput"] = translate_render_output(outputs, inject_isosurface_info)
971+
972+
##:: Step7: Get translated["monitorOutput"]
928973
probe_output = {}
929974
probe_output_average = {}
930975
integral_output = {}
@@ -946,7 +991,7 @@ def translate_output(input_params: SimulationParams, translated: dict):
946991
if probe_output or integral_output:
947992
translated["monitorOutput"] = merge_monitor_output(probe_output, integral_output)
948993

949-
##:: Step6.1: Get translated["surfaceMonitorOutput"]
994+
##:: Step7.1: Get translated["surfaceMonitorOutput"]
950995
surface_monitor_output = {}
951996
surface_monitor_output_average = {}
952997
if has_instance_in_list(outputs, SurfaceProbeOutput):
@@ -968,17 +1013,17 @@ def translate_output(input_params: SimulationParams, translated: dict):
9681013
surface_monitor_output, surface_monitor_output_average
9691014
)
9701015

971-
##:: Step6: Get translated["surfaceSliceOutput"]
1016+
##:: Step8: Get translated["surfaceSliceOutput"]
9721017
surface_slice_output = {}
9731018
if has_instance_in_list(outputs, SurfaceSliceOutput):
9741019
surface_slice_output = translate_surface_slice_output(outputs, SurfaceSliceOutput)
9751020
translated["surfaceSliceOutput"] = surface_slice_output
9761021

977-
##:: Step7: Get translated["aeroacousticOutput"]
1022+
##:: Step9: Get translated["aeroacousticOutput"]
9781023
if has_instance_in_list(outputs, AeroAcousticOutput):
9791024
translated["aeroacousticOutput"] = translate_acoustic_output(outputs)
9801025

981-
##:: Step8: Get translated["streamlineOutput"]
1026+
##:: Step10: Get translated["streamlineOutput"]
9821027
if has_instance_in_list(outputs, StreamlineOutput):
9831028
translated["streamlineOutput"] = translate_streamline_output(outputs, StreamlineOutput)
9841029

@@ -987,15 +1032,15 @@ def translate_output(input_params: SimulationParams, translated: dict):
9871032
outputs, TimeAverageStreamlineOutput
9881033
)
9891034

990-
##:: Step9: Get translated["importedSurfaceIntegralOutput"]
1035+
##:: Step11: Get translated["importedSurfaceIntegralOutput"]
9911036
if has_instance_in_list(outputs, SurfaceIntegralOutput):
9921037
imported_surface_integral_output_configs = translate_imported_surface_integral_output(
9931038
outputs,
9941039
)
9951040
if imported_surface_integral_output_configs["surfaces"]:
9961041
translated["importedSurfaceIntegralOutput"] = imported_surface_integral_output_configs
9971042

998-
##:: Step10: Sort all "output_fields" everywhere
1043+
##:: Step12: Sort all "output_fields" everywhere
9991044
# Recursively sort all "outputFields" lists in the translated dict
10001045
def _sort_output_fields_in_dict(d):
10011046
if isinstance(d, dict):

flow360/component/simulation/translator/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Union
99

1010
import numpy as np
11+
import pydantic as pd
1112
import unyt as u
1213

1314
from flow360.component.simulation.framework.base_model import snake_to_camel
@@ -220,6 +221,17 @@ def getattr_by_path(obj, path: Union[str, list], *args):
220221
return obj
221222

222223

224+
def get_all_entries_of_type(obj_list: list, class_type: type[pd.BaseModel]):
225+
entries = []
226+
227+
if obj_list is not None:
228+
for obj in obj_list:
229+
if is_exact_instance(obj, class_type):
230+
entries.append(obj)
231+
232+
return entries
233+
234+
223235
def get_global_setting_from_first_instance(
224236
obj_list: list,
225237
class_type,

flow360/component/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
List2D = List[List[float]]
1515
# we use tuple for fixed length lists, beacause List is a mutable, variable length structure
1616
Coordinate = Tuple[float, float, float]
17+
Color = Tuple[int, int, int]
1718

1819

1920
class Vector(Coordinate):

tests/simulation/translator/test_solver_translator.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,24 @@
5858
ThermalState,
5959
)
6060
from flow360.component.simulation.outputs.output_entities import (
61+
AmbientLight,
62+
DirectionalLight,
63+
Isosurface,
64+
OrthographicProjection,
6165
Point,
6266
PointArray,
6367
PointArray2D,
68+
RenderCameraConfig,
69+
RenderLightingConfig,
6470
Slice,
71+
StaticCamera,
6572
)
6673
from flow360.component.simulation.outputs.outputs import (
6774
Isosurface,
6875
IsosurfaceOutput,
6976
MovingStatistic,
7077
ProbeOutput,
78+
RenderOutput,
7179
SliceOutput,
7280
StreamlineOutput,
7381
SurfaceIntegralOutput,
@@ -181,6 +189,7 @@ def get_om6Wing_tutorial_param():
181189
my_wall = Surface(name="1")
182190
my_symmetry_plane = Surface(name="2")
183191
my_freestream = Surface(name="3")
192+
my_isosurface = Isosurface(name="iso", field="Mach", iso_value=0.5)
184193
with SI_unit_system:
185194
param = SimulationParams(
186195
reference_geometry=ReferenceGeometry(
@@ -244,6 +253,20 @@ def get_om6Wing_tutorial_param():
244253
output_format="paraview",
245254
output_fields=["Cp"],
246255
),
256+
RenderOutput(
257+
entities=[my_isosurface],
258+
output_fields=["qcriterion"],
259+
camera=RenderCameraConfig(
260+
view=StaticCamera(position=(20, 20, 20), target=(0, 0, 0)),
261+
projection=OrthographicProjection(width=30, near=0.01, far=100),
262+
),
263+
lighting=RenderLightingConfig(
264+
ambient=AmbientLight(intensity=0.4, color=(255, 255, 255)),
265+
directional=DirectionalLight(
266+
intensity=1.5, color=(255, 255, 255), direction=(-1.0, -1.0, -1.0)
267+
),
268+
),
269+
),
247270
],
248271
)
249272
return param

0 commit comments

Comments
 (0)