Source code for flow360.component.simulation.outputs.outputs

"""Mostly the same as Flow360Param counterparts.
Caveats:
1. Check if we support non-average and average output specified at the same time in solver.
(Yes but they share the same output_fields)
2. We do not support multiple output frequencies/file format for the same type of output.
"""

# pylint: disable=too-many-lines
from typing import Annotated, List, Literal, Optional, Union, get_args

import pydantic as pd

from flow360.component.simulation.framework.base_model import (
    Flow360BaseModel,
    RegistryLookup,
)
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.framework.expressions import StringExpression
from flow360.component.simulation.framework.unique_list import UniqueItemList
from flow360.component.simulation.models.surface_models import EntityListAllowingGhost
from flow360.component.simulation.outputs.output_entities import (
    Isosurface,
    Point,
    PointArray,
    PointArray2D,
    Slice,
)
from flow360.component.simulation.outputs.output_fields import (
    AllFieldNames,
    CommonFieldNames,
    InvalidOutputFieldsForLiquid,
    SliceFieldNames,
    SurfaceFieldNames,
    VolumeFieldNames,
    get_field_values,
)
from flow360.component.simulation.primitives import (
    GhostCircularPlane,
    GhostSphere,
    GhostSurface,
    Surface,
)
from flow360.component.simulation.unit_system import LengthType
from flow360.component.simulation.user_code.core.types import (
    Expression,
    UserVariable,
    solver_variable_to_user_variable,
)
from flow360.component.simulation.validation.validation_context import (
    ALL,
    CASE,
    get_validation_info,
    get_validation_levels,
)
from flow360.component.simulation.validation.validation_utils import (
    check_deleted_surface_in_entity_list,
)


[docs] class UserDefinedField(Flow360BaseModel): """ Defines additional fields that can be used as output variables. Example ------- - Compute :code:`Mach` using :class:`UserDefinedField` (Showcase use, already supported in :ref:`Output Fields <UniversalVariablesV2>`): >>> fl.UserDefinedField( ... name="Mach_UDF", ... expression="double Mach = sqrt(primitiveVars[1] * primitiveVars[1] + " ... + "primitiveVars[2] * primitiveVars[2] + primitiveVars[3] * primitiveVars[3])" ... + " / sqrt(gamma * primitiveVars[4] / primitiveVars[0]);", ... ) - Compute :code:`PressureForce` using :class:`UserDefinedField`: >>> fl.UserDefinedField( ... name="PressureForce", ... expression="double prel = primitiveVars[4] - pressureFreestream; " ... + "PressureForce[0] = prel * nodeNormals[0]; " ... + "PressureForce[1] = prel * nodeNormals[1]; " ... + "PressureForce[2] = prel * nodeNormals[2];", ... ) ==== """ type_name: Literal["UserDefinedField"] = pd.Field("UserDefinedField", frozen=True) name: str = pd.Field(description="The name of the output field.") expression: StringExpression = pd.Field( description="The mathematical expression for the field." ) @pd.field_validator("name", mode="after") @classmethod def _check_redefined_user_defined_fields(cls, value): current_levels = get_validation_levels() if get_validation_levels() else [] if all(level not in current_levels for level in (ALL, CASE)): return value defined_field_names = get_field_values(AllFieldNames) if value in defined_field_names: raise ValueError( f"User defined field variable name: {value} conflicts with pre-defined field names." " Please consider renaming this user defined field variable." ) return value
class _OutputBase(Flow360BaseModel): output_fields: UniqueItemList[str] = pd.Field() @pd.field_validator("output_fields", mode="after") @classmethod def _validate_improper_surface_field_usage(cls, value: UniqueItemList): if any( output_type in cls.__name__ for output_type in [ "SurfaceProbeOutput", "SurfaceOutput", "SurfaceSliceOutput", "SurfaceIntegralOutput", ] ): return value for output_item in value.items: if not isinstance(output_item, UserVariable) or not isinstance( output_item.value, Expression ): continue surface_solver_variable_names = output_item.value.solver_variable_names( variable_type="Surface" ) if len(surface_solver_variable_names) > 0: raise ValueError( f"Variable `{output_item}` cannot be used in `{cls.__name__}` " + "since it contains Surface solver variable(s): " + f"{', '.join(sorted(surface_solver_variable_names))}.", ) return value @pd.field_validator("output_fields", mode="after") @classmethod def _validate_non_liquid_output_fields(cls, value: UniqueItemList): validation_info = get_validation_info() if validation_info is None or validation_info.using_liquid_as_material is False: return value for output_item in value.items: if output_item in get_args(InvalidOutputFieldsForLiquid): raise ValueError( f"Output field {output_item} cannot be selected when using liquid as simulation material." ) return value @pd.field_validator("output_fields", mode="before") @classmethod def _convert_solver_variables_as_user_variables(cls, value): # Handle both dict/list (deserialization) and UniqueItemList (python object) # If input is a dict (from deserialization so no SolverVariable expected) if isinstance(value, dict): return value # If input is a list (from Python mode) if isinstance(value, list): return [solver_variable_to_user_variable(item) for item in value] # If input is a UniqueItemList (python object) if hasattr(value, "items") and isinstance(value.items, list): value.items = [solver_variable_to_user_variable(item) for item in value.items] return value return value class _AnimationSettings(_OutputBase): """ Controls how frequently the output files are generated. """ frequency: int = pd.Field( default=-1, ge=-1, description="Frequency (in number of physical time steps) at which output is saved. " + "-1 is at end of simulation.", ) frequency_offset: int = pd.Field( default=0, ge=0, description="Offset (in number of physical time steps) at which output animation is started." + " 0 is at beginning of simulation.", ) class _AnimationAndFileFormatSettings(_AnimationSettings): """ Controls how frequently the output files are generated and the file format. """ output_format: Literal["paraview", "tecplot", "both"] = pd.Field( default="paraview", description=":code:`paraview`, :code:`tecplot` or :code:`both`." )
[docs] class SurfaceOutput(_AnimationAndFileFormatSettings): """ :class:`SurfaceOutput` class for surface output settings. Example ------- - Define :class:`SurfaceOutput` on all surfaces of the geometry using naming pattern :code:`"*"`. >>> fl.SurfaceOutput( ... entities=[geometry['*']],, ... output_format="paraview", ... output_fields=["vorticity", "T"], ... ) - Define :class:`SurfaceOutput` on the selected surfaces of the volume_mesh using name pattern :code:`"fluid/inflow*"`. >>> fl.SurfaceOutput( ... entities=[volume_mesh["fluid/inflow*"]],, ... output_format="paraview", ... output_fields=["vorticity", "T"], ... ) ==== """ # pylint: disable=fixme # TODO: entities is None --> use all surfaces. This is not implemented yet. name: Optional[str] = pd.Field("Surface output", description="Name of the `SurfaceOutput`.") entities: EntityListAllowingGhost[Surface, GhostSurface, GhostCircularPlane, GhostSphere] = ( pd.Field( alias="surfaces", description="List of boundaries where output is generated.", ) ) write_single_file: bool = pd.Field( default=False, description="Enable writing all surface outputs into a single file instead of one file per surface." + "This option currently only supports Tecplot output format." + "Will choose the value of the last instance of this option of the same output type " + "(:class:`SurfaceOutput` or :class:`TimeAverageSurfaceOutput`) in the output list.", ) output_fields: UniqueItemList[Union[SurfaceFieldNames, str, UserVariable]] = pd.Field( description="List of output variables. Including :ref:`universal output variables<UniversalVariablesV2>`," + " :ref:`variables specific to SurfaceOutput<SurfaceSpecificVariablesV2>` and :class:`UserDefinedField`." ) output_type: Literal["SurfaceOutput"] = pd.Field("SurfaceOutput", frozen=True) @pd.field_validator("entities", mode="after") @classmethod def ensure_surface_existence(cls, value): """Ensure all boundaries will be present after mesher""" return check_deleted_surface_in_entity_list(value)
[docs] class TimeAverageSurfaceOutput(SurfaceOutput): """ :class:`TimeAverageSurfaceOutput` class for time average surface output settings. Example ------- Calculate the average value starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). >>> fl.TimeAverageSurfaceOutput( ... output_format="paraview", ... output_fields=["primitiveVars"], ... entities=[ ... volume_mesh["VOLUME/LEFT"], ... volume_mesh["VOLUME/RIGHT"], ... ], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) ==== """ name: Optional[str] = pd.Field( "Time average surface output", description="Name of the `TimeAverageSurfaceOutput`." ) start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( default=-1, description="Physical time step to start calculating averaging." ) output_type: Literal["TimeAverageSurfaceOutput"] = pd.Field( "TimeAverageSurfaceOutput", frozen=True )
[docs] class VolumeOutput(_AnimationAndFileFormatSettings): """ :class:`VolumeOutput` class for volume output settings. Example ------- >>> fl.VolumeOutput( ... output_format="paraview", ... output_fields=["Mach", "vorticity", "T"], ... ) ==== """ name: Optional[str] = pd.Field("Volume output", description="Name of the `VolumeOutput`.") output_fields: UniqueItemList[Union[VolumeFieldNames, str, UserVariable]] = pd.Field( description="List of output variables. Including :ref:`universal output variables<UniversalVariablesV2>`," " :ref:`variables specific to VolumeOutput<VolumeAndSliceSpecificVariablesV2>`" " and :class:`UserDefinedField`." ) output_type: Literal["VolumeOutput"] = pd.Field("VolumeOutput", frozen=True)
[docs] class TimeAverageVolumeOutput(VolumeOutput): """ :class:`TimeAverageVolumeOutput` class for time average volume output settings. Example ------- Calculate the average value starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). >>> fl.TimeAverageVolumeOutput( ... output_format="paraview", ... output_fields=["primitiveVars"], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) ==== """ name: Optional[str] = pd.Field( "Time average volume output", description="Name of the `TimeAverageVolumeOutput`." ) start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( default=-1, description="Physical time step to start calculating averaging." ) output_type: Literal["TimeAverageVolumeOutput"] = pd.Field( "TimeAverageVolumeOutput", frozen=True )
[docs] class SliceOutput(_AnimationAndFileFormatSettings): """ :class:`SliceOutput` class for slice output settings. Example ------- >>> fl.SliceOutput( ... slices=[ ... fl.Slice( ... name="Slice_1", ... normal=(0, 1, 0), ... origin=(0, 0.56, 0)*fl.u.m ... ), ... ], ... output_format="paraview", ... output_fields=["vorticity", "T"], ... ) ==== """ name: Optional[str] = pd.Field("Slice output", description="Name of the `SliceOutput`.") entities: EntityList[Slice] = pd.Field( alias="slices", description="List of output :class:`~flow360.Slice` entities.", ) output_fields: UniqueItemList[Union[SliceFieldNames, str, UserVariable]] = pd.Field( description="List of output variables. Including :ref:`universal output variables<UniversalVariablesV2>`," " :ref:`variables specific to SliceOutput<VolumeAndSliceSpecificVariablesV2>`" " and :class:`UserDefinedField`." ) output_type: Literal["SliceOutput"] = pd.Field("SliceOutput", frozen=True)
[docs] class TimeAverageSliceOutput(SliceOutput): """ :class:`TimeAverageSliceOutput` class for time average slice output settings. Example ------- Calculate the average value starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). >>> fl.TimeAverageSliceOutput( ... entities=[ ... fl.Slice(name="Slice_1", ... origin=(0, 0, 0) * fl.u.m, ... normal=(0, 0, 1), ... ) ... ], ... output_fields=["s", "T"], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) ==== """ name: Optional[str] = pd.Field( "Time average slice output", description="Name of the `TimeAverageSliceOutput`." ) start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( default=-1, description="Physical time step to start calculating averaging." ) output_type: Literal["TimeAverageSliceOutput"] = pd.Field("TimeAverageSliceOutput", frozen=True)
[docs] class IsosurfaceOutput(_AnimationAndFileFormatSettings): """ :class:`IsosurfaceOutput` class for isosurface output settings. Example ------- Define the :class:`IsosurfaceOutput` of :code:`qcriterion` on two isosurfaces: - :code:`Isosurface_T_0.1` is the :class:`Isosurface` with its temperature equals to 1.5 non-dimensional temperature; - :code:`Isosurface_p_0.5` is the :class:`Isosurface` with its pressure equals to 0.5 non-dimensional pressure. >>> fl.IsosurfaceOutput( ... 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_fields=["qcriterion"], ... ) ==== """ name: Optional[str] = pd.Field( "Isosurface 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, UserVariable]] = pd.Field( description="List of output variables. Including " ":ref:`universal output variables<UniversalVariablesV2>` and :class:`UserDefinedField`." ) output_type: Literal["IsosurfaceOutput"] = pd.Field("IsosurfaceOutput", frozen=True) def preprocess( self, *, params=None, exclude: List[str] = None, required_by: List[str] = None, registry_lookup: RegistryLookup = None, ) -> Flow360BaseModel: exclude_isosurface_output = exclude + ["iso_value"] return super().preprocess( params=params, exclude=exclude_isosurface_output, required_by=required_by, registry_lookup=registry_lookup, )
class TimeAverageIsosurfaceOutput(IsosurfaceOutput): """ :class:`TimeAverageIsosurfaceOutput` class for isosurface output settings. Example ------- Define the :class:`TimeAverageIsosurfaceOutput` of :code:`qcriterion` on two isosurfaces: - :code:`TimeAverageIsosurface_T_0.1` is the :class:`Isosurface` with its temperature equals to 1.5 non-dimensional temperature; - :code:`TimeAverageIsosurface_p_0.5` is the :class:`Isosurface` with its pressure equals to 0.5 non-dimensional pressure. >>> fl.TimeAverageIsosurfaceOutput( ... isosurfaces=[ ... fl.Isosurface( ... name="TimeAverageIsosurface_T_0.1", ... iso_value=0.1, ... field="T", ... ), ... fl.Isosurface( ... name="TimeAverageIsosurface_p_0.5", ... iso_value=0.5, ... field="p", ... ), ... ], ... output_fields=["qcriterion"], ... ) ==== """ name: Optional[str] = pd.Field( "Time Average Isosurface output", description="Name of `TimeAverageIsosurfaceOutput`." ) start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( default=-1, description="Physical time step to start calculating averaging." ) output_type: Literal["TimeAverageIsosurfaceOutput"] = pd.Field( "TimeAverageIsosurfaceOutput", frozen=True )
[docs] class SurfaceIntegralOutput(_OutputBase): """ :class:`SurfaceIntegralOutput` class for surface integral output settings. Note ---- :class:`SurfaceIntegralOutput` can only be used with :class:`UserDefinedField`. See :ref:`User Defined Postprocessing Tutorial <UserDefinedPostprocessing>` for more details about how to set up :class:`UserDefinedField`. Example ------- Define :class:`SurfaceIntegralOutput` of :code:`PressureForce` as set up in this :ref:`User Defined Postprocessing Tutorial Case <UDFSurfIntegral>`. >>> fl.SurfaceIntegralOutput( ... name="surface_integral", ... output_fields=["PressureForce"], ... entities=[volume_mesh["wing1"], volume_mesh["wing2"]], ... ) ==== """ name: str = pd.Field("Surface integral output", description="Name of integral.") entities: EntityListAllowingGhost[Surface, GhostSurface, GhostCircularPlane, GhostSphere] = ( pd.Field( alias="surfaces", description="List of boundaries where the surface integral will be calculated.", ) ) output_fields: UniqueItemList[Union[str, UserVariable]] = pd.Field( description="List of output variables, only the :class:`UserDefinedField` is allowed." ) output_type: Literal["SurfaceIntegralOutput"] = pd.Field("SurfaceIntegralOutput", frozen=True) @pd.field_validator("entities", mode="after") @classmethod def ensure_surface_existence(cls, value): """Ensure all boundaries will be present after mesher""" return check_deleted_surface_in_entity_list(value)
[docs] class ProbeOutput(_OutputBase): """ :class:`ProbeOutput` class for setting output data probed at monitor points. Example ------- Define :class:`ProbeOutput` on multiple specific monitor points and monitor points along the line. - :code:`Point_1` and :code:`Point_2` are two specific points we want to monitor in this probe output group. - :code:`Line_1` is from (1,0,0) * fl.u,m to (1.5,0,0) * fl.u,m and has 6 monitor points. - :code:`Line_2` is from (-1,0,0) * fl.u,m to (-1.5,0,0) * fl.u,m and has 3 monitor points, namely, (-1,0,0) * fl.u,m, (-1.25,0,0) * fl.u,m and (-1.5,0,0) * fl.u,m. >>> fl.ProbeOutput( ... name="probe_group_points_and_lines", ... entities=[ ... fl.Point( ... name="Point_1", ... location=(0.0, 1.5, 0.0) * fl.u.m, ... ), ... fl.Point( ... name="Point_2", ... location=(0.0, -1.5, 0.0) * fl.u.m, ... ), ... fl.PointArray( ... name="Line_1", ... start=(1.0, 0.0, 0.0) * fl.u.m, ... end=(1.5, 0.0, 0.0) * fl.u.m, ... number_of_points=6, ... ), ... fl.PointArray( ... name="Line_2", ... start=(-1.0, 0.0, 0.0) * fl.u.m, ... end=(-1.5, 0.0, 0.0) * fl.u.m, ... number_of_points=3, ... ), ... ], ... output_fields=["primitiveVars"], ... ) ==== """ name: str = pd.Field("Probe output", description="Name of the monitor group.") entities: EntityList[Point, PointArray] = pd.Field( alias="probe_points", description="List of monitored :class:`~flow360.Point`/" + ":class:`~flow360.PointArray` entities belonging to this " + "monitor group. :class:`~flow360.PointArray` is used to " + "define monitored points along a line.", ) output_fields: UniqueItemList[Union[CommonFieldNames, str, UserVariable]] = pd.Field( description="List of output fields. Including :ref:`universal output variables<UniversalVariablesV2>`" " and :class:`UserDefinedField`." ) output_type: Literal["ProbeOutput"] = pd.Field("ProbeOutput", frozen=True)
[docs] class SurfaceProbeOutput(_OutputBase): """ :class:`SurfaceProbeOutput` class for setting surface output data probed at monitor points. The specified monitor point will be projected to the :py:attr:`~SurfaceProbeOutput.target_surfaces` closest to the point. The probed results on the projected point will be dumped. Example ------- Define :class:`SurfaceProbeOutput` on the :code:`geometry["wall"]` surface with multiple specific monitor points and monitor points along the line. - :code:`Point_1` and :code:`Point_2` are two specific points we want to monitor in this probe output group. - :code:`Line_surface` is from (1,0,0) * fl.u.m to (1,0,-10) * fl.u.m and has 11 monitor points, including both starting and end points. >>> fl.SurfaceProbeOutput( ... name="surface_probe_group_points", ... entities=[ ... fl.Point( ... name="Point_1", ... location=(0.0, 1.5, 0.0) * fl.u.m, ... ), ... fl.Point( ... name="Point_2", ... location=(0.0, -1.5, 0.0) * fl.u.m, ... ), ... fl.PointArray( ... name="Line_surface", ... start=(1.0, 0.0, 0.0) * fl.u.m, ... end=(1.0, 0.0, -10.0) * fl.u.m, ... number_of_points=11, ... ), ... ], ... target_surfaces=[ ... geometry["wall"], ... ], ... output_fields=["heatFlux", "T"], ... ) ==== """ name: str = pd.Field("Surface probe output", description="Name of the surface monitor group.") entities: EntityList[Point, PointArray] = pd.Field( alias="probe_points", description="List of monitored :class:`~flow360.Point`/" + ":class:`~flow360.PointArray` entities belonging to this " + "surface monitor group. :class:`~flow360.PointArray` " + "is used to define monitored points along a line.", ) # Maybe add preprocess for this and by default add all Surfaces? target_surfaces: EntityList[Surface] = pd.Field( description="List of :class:`~flow360.component.simulation.primitives.Surface` " + "entities belonging to this monitor group." ) output_fields: UniqueItemList[Union[SurfaceFieldNames, str, UserVariable]] = pd.Field( description="List of output variables. Including :ref:`universal output variables<UniversalVariablesV2>`," " :ref:`variables specific to SurfaceOutput<SurfaceSpecificVariablesV2>` and :class:`UserDefinedField`." ) output_type: Literal["SurfaceProbeOutput"] = pd.Field("SurfaceProbeOutput", frozen=True) @pd.field_validator("target_surfaces", mode="after") @classmethod def ensure_surface_existence(cls, value): """Ensure all boundaries will be present after mesher""" return check_deleted_surface_in_entity_list(value)
[docs] class SurfaceSliceOutput(_AnimationAndFileFormatSettings): """ Surface slice settings. """ name: str = pd.Field("Surface slice output", description="Name of the `SurfaceSliceOutput`.") entities: EntityList[Slice] = pd.Field( alias="slices", description="List of :class:`Slice` entities." ) # Maybe add preprocess for this and by default add all Surfaces? target_surfaces: EntityList[Surface] = pd.Field( description="List of :class:`Surface` entities on which the slice will cut through." ) output_format: Literal["paraview"] = pd.Field(default="paraview") output_fields: UniqueItemList[Union[SurfaceFieldNames, str, UserVariable]] = pd.Field( description="List of output variables. Including :ref:`universal output variables<UniversalVariablesV2>`," " :ref:`variables specific to SurfaceOutput<SurfaceSpecificVariablesV2>` and :class:`UserDefinedField`." ) output_type: Literal["SurfaceSliceOutput"] = pd.Field("SurfaceSliceOutput", frozen=True) @pd.field_validator("target_surfaces", mode="after") @classmethod def ensure_surface_existence(cls, value): """Ensure all boundaries will be present after mesher""" return check_deleted_surface_in_entity_list(value)
[docs] class TimeAverageProbeOutput(ProbeOutput): """ :class:`TimeAverageProbeOutput` class for time average probe monitor output settings. Example ------- - Calculate the average value on multiple monitor points starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). >>> fl.TimeAverageProbeOutput( ... name="time_average_probe_group_points", ... entities=[ ... fl.Point( ... name="Point_1", ... location=(0.0, 1.5, 0.0) * fl.u.m, ... ), ... fl.Point( ... name="Point_2", ... location=(0.0, -1.5, 0.0) * fl.u.m, ... ), ... ], ... output_fields=["primitiveVars", "Mach"], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) - Calculate the average value on multiple monitor points starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). - :code:`Line_1` is from (1,0,0) * fl.u,m to (1.5,0,0) * fl.u,m and has 6 monitor points. - :code:`Line_2` is from (-1,0,0) * fl.u,m to (-1.5,0,0) * fl.u,m and has 3 monitor points, namely, (-1,0,0) * fl.u,m, (-1.25,0,0) * fl.u,m and (-1.5,0,0) * fl.u,m. >>> fl.TimeAverageProbeOutput( ... name="time_average_probe_group_points", ... entities=[ ... fl.PointArray( ... name="Line_1", ... start=(1.0, 0.0, 0.0) * fl.u.m, ... end=(1.5, 0.0, 0.0) * fl.u.m, ... number_of_points=6, ... ), ... fl.PointArray( ... name="Line_2", ... start=(-1.0, 0.0, 0.0) * fl.u.m, ... end=(-1.5, 0.0, 0.0) * fl.u.m, ... number_of_points=3, ... ), ... ], ... output_fields=["primitiveVars", "Mach"], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) ==== """ name: Optional[str] = pd.Field( "Time average probe output", description="Name of the `TimeAverageProbeOutput`." ) # pylint: disable=abstract-method frequency: int = pd.Field( default=1, ge=-1, description="Frequency (in number of physical time steps) at which output is saved. " + "-1 is at end of simulation.", ) frequency_offset: int = pd.Field( default=0, ge=0, description="Offset (in number of physical time steps) at which output animation is started." + " 0 is at beginning of simulation.", ) start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( default=-1, description="Physical time step to start calculating averaging" ) output_type: Literal["TimeAverageProbeOutput"] = pd.Field("TimeAverageProbeOutput", frozen=True)
[docs] class TimeAverageSurfaceProbeOutput(SurfaceProbeOutput): """ :class:`TimeAverageSurfaceProbeOutput` class for time average surface probe monitor output settings. The specified monitor point will be projected to the :py:attr:`~TimeAverageSurfaceProbeOutput.target_surfaces` closest to the point. The probed results on the projected point will be dumped. Example ------- - Calculate the average value on the :code:`geometry["surface1"]` and :code:`geometry["surface2"]` surfaces with multiple monitor points. The average is computed starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). >>> TimeAverageSurfaceProbeOutput( ... name="time_average_surface_probe_group_points", ... entities=[ ... Point(name="Point_1", location=[1, 1.02, 0.03] * fl.u.cm), ... Point(name="Point_2", location=[2, 1.01, 0.03] * fl.u.m), ... Point(name="Point_3", location=[3, 1.02, 0.03] * fl.u.m), ... ], ... target_surfaces=[ ... Surface(name="Surface_1", geometry["surface1"]), ... Surface(name="Surface_2", geometry["surface2"]), ... ], ... output_fields=["Mach", "primitiveVars", "yPlus"], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) - Calculate the average value on the :code:`geometry["surface1"]` and :code:`geometry["surface2"]` surfaces with multiple monitor lines. The average is computed starting from the :math:`4^{th}` physical step. The results are output every 10 physical step starting from the :math:`14^{th}` physical step (14, 24, 34 etc.). - :code:`Line_1` is from (1,0,0) * fl.u,m to (1.5,0,0) * fl.u,m and has 6 monitor points. - :code:`Line_2` is from (-1,0,0) * fl.u,m to (-1.5,0,0) * fl.u,m and has 3 monitor points, namely, (-1,0,0) * fl.u,m, (-1.25,0,0) * fl.u,m and (-1.5,0,0) * fl.u,m. >>> TimeAverageSurfaceProbeOutput( ... name="time_average_surface_probe_group_points", ... entities=[ ... fl.PointArray( ... name="Line_1", ... start=(1.0, 0.0, 0.0) * fl.u.m, ... end=(1.5, 0.0, 0.0) * fl.u.m, ... number_of_points=6, ... ), ... fl.PointArray( ... name="Line_2", ... start=(-1.0, 0.0, 0.0) * fl.u.m, ... end=(-1.5, 0.0, 0.0) * fl.u.m, ... number_of_points=3, ... ), ... ], ... target_surfaces=[ ... Surface(name="Surface_1", geometry["surface1"]), ... Surface(name="Surface_2", geometry["surface2"]), ... ], ... output_fields=["Mach", "primitiveVars", "yPlus"], ... start_step=4, ... frequency=10, ... frequency_offset=14, ... ) ==== """ name: Optional[str] = pd.Field( "Time average surface probe output", description="Name of the `TimeAverageSurfaceProbeOutput`.", ) # pylint: disable=abstract-method frequency: int = pd.Field( default=1, ge=-1, description="Frequency (in number of physical time steps) at which output is saved. " + "-1 is at end of simulation.", ) frequency_offset: int = pd.Field( default=0, ge=0, description="Offset (in number of physical time steps) at which output animation is started." + " 0 is at beginning of simulation.", ) start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( default=-1, description="Physical time step to start calculating averaging" ) output_type: Literal["TimeAverageSurfaceProbeOutput"] = pd.Field( "TimeAverageSurfaceProbeOutput", frozen=True )
[docs] class Observer(Flow360BaseModel): """ :class:`Observer` class for setting up the :py:attr:`AeroAcousticOutput.observers`. Example ------- >>> fl.Observer(position=[1, 2, 3] * fl.u.m, group_name="1") ==== """ # pylint: disable=no-member position: LengthType.Point = pd.Field( description="Position at which time history of acoustic pressure signal " + "is stored in aeroacoustic output file. The observer position can be outside the simulation domain, " + "but cannot be on or inside the solid surfaces of the simulation domain." ) group_name: str = pd.Field( description="Name of the group to which the observer will be assigned " + "for postprocessing purposes in Flow360 web client." ) private_attribute_expand: Optional[bool] = pd.Field(None)
[docs] class AeroAcousticOutput(Flow360BaseModel): """ :class:`AeroAcousticOutput` class for aeroacoustic output settings. Example ------- >>> fl.AeroAcousticOutput( ... observers=[ ... fl.Observer(position=[1.0, 0.0, 1.75] * fl.u.m, group_name="1"), ... fl.Observer(position=[0.2, 0.3, 1.725] * fl.u.m, group_name="1"), ... ], ... ) If using permeable surfaces: >>> fl.AeroAcousticOutput( ... observers=[ ... fl.Observer(position=[1.0, 0.0, 1.75] * fl.u.m, group_name="1"), ... fl.Observer(position=[0.2, 0.3, 1.725] * fl.u.m, group_name="1"), ... ], ... patch_type="permeable", ... permeable_surfaces=[volume_mesh["inner/interface*"]] ... ) ==== """ name: Optional[str] = pd.Field( "Aeroacoustic output", description="Name of the `AeroAcousticOutput`." ) patch_type: Literal["solid", "permeable"] = pd.Field( default="solid", description="Type of aeroacoustic simulation to " + "perform. `solid` uses solid walls to compute the " + "aeroacoustic solution. `permeable` uses surfaces " + "embedded in the volumetric domain as aeroacoustic solver " + "input.", ) permeable_surfaces: Optional[ EntityListAllowingGhost[Surface, GhostSurface, GhostCircularPlane, GhostSphere] ] = pd.Field( None, description="List of permeable surfaces. Left empty if `patch_type` is solid" ) # pylint: disable=no-member observers: List[Observer] = pd.Field( description="A List of :class:`Observer` objects specifying each observer's position and group name." ) write_per_surface_output: bool = pd.Field( False, description="Enable writing of aeroacoustic results on a per-surface basis, " + "in addition to results for all wall surfaces combined.", ) output_type: Literal["AeroAcousticOutput"] = pd.Field("AeroAcousticOutput", frozen=True) @pd.field_validator("observers", mode="after") @classmethod def validate_observer_has_same_unit(cls, input_value): """ All observer location should have the same length unit. This is because UI has single toggle for all coordinates. """ unit_set = {} for observer in input_value: unit_set[observer.position.units] = None if len(unit_set.keys()) > 1: raise ValueError( "All observer locations should have the same unit." f" But now it has both `{list(unit_set.keys())[0]}` and `{list(unit_set.keys())[1]}`." ) return input_value @pd.model_validator(mode="after") def check_consistent_patch_type_and_permeable_surfaces(self): """Check if permeable_surfaces is None when patch_type is solid.""" if self.patch_type == "solid" and self.permeable_surfaces is not None: raise ValueError("`permeable_surfaces` cannot be specified when `patch_type` is solid.") if self.patch_type == "permeable" and self.permeable_surfaces is None: raise ValueError("`permeable_surfaces` cannot be empty when `patch_type` is permeable.") return self @pd.field_validator("permeable_surfaces", mode="after") @classmethod def ensure_surface_existence(cls, value): """Ensure all boundaries will be present after mesher""" if value is None: return value return check_deleted_surface_in_entity_list(value)
[docs] class StreamlineOutput(Flow360BaseModel): """ :class:`StreamlineOutput` class for calculating streamlines. Stramtraces are computed upwind and downwind, and may originate from a single point, from a line, or from points evenly distributed across a parallelogram. Example ------- Define a :class:`StreamlineOutput` with streaptraces originating from points, lines (:class:`~flow360.PointArray`), and parallelograms (:class:`~flow360.PointArray2D`). - :code:`Point_1` and :code:`Point_2` are two specific points we want to track the streamlines. - :code:`Line_streamline` is from (1,0,0) * fl.u.m to (1,0,-10) * fl.u.m and has 11 points, including both starting and end points. - :code:`Parallelogram_streamline` is a parallelogram in 3D space with an origin at (1.0, 0.0, 0.0), a u-axis orientation of (0, 2.0, 2.0) with 11 points in the u direction, and a v-axis orientation of (0, 1.0, 0) with 20 points along the v direction. >>> fl.StreamlineOutput( ... entities=[ ... fl.Point( ... name="Point_1", ... location=(0.0, 1.5, 0.0) * fl.u.m, ... ), ... fl.Point( ... name="Point_2", ... location=(0.0, -1.5, 0.0) * fl.u.m, ... ), ... fl.PointArray( ... name="Line_streamline", ... start=(1.0, 0.0, 0.0) * fl.u.m, ... end=(1.0, 0.0, -10.0) * fl.u.m, ... number_of_points=11, ... ), ... fl.PointArray2D( ... name="Parallelogram_streamline", ... origin=(1.0, 0.0, 0.0) * fl.u.m, ... u_axis_vector=(0, 2.0, 2.0) * fl.u.m, ... v_axis_vector=(0, 1.0, 0) * fl.u.m, ... u_number_of_points=11, ... v_number_of_points=20 ... ) ... ] ... ) ==== """ name: Optional[str] = pd.Field( "Streamline output", description="Name of the `StreamlineOutput`." ) entities: EntityList[Point, PointArray, PointArray2D] = pd.Field( alias="streamline_points", description="List of monitored :class:`~flow360.Point`/" + ":class:`~flow360.PointArray`/:class:`~flow360.PointArray2D` " + "entities belonging to this " + "streamline group. :class:`~flow360.PointArray` " + "is used to define streamline originating along a line. " + ":class:`~flow360.PointArray2D` " + "is used to define streamline originating from a parallelogram.", ) output_type: Literal["StreamlineOutput"] = pd.Field("StreamlineOutput", frozen=True)
OutputTypes = Annotated[ Union[ SurfaceOutput, TimeAverageSurfaceOutput, VolumeOutput, TimeAverageVolumeOutput, SliceOutput, TimeAverageSliceOutput, IsosurfaceOutput, TimeAverageIsosurfaceOutput, SurfaceIntegralOutput, ProbeOutput, SurfaceProbeOutput, SurfaceSliceOutput, TimeAverageProbeOutput, TimeAverageSurfaceProbeOutput, AeroAcousticOutput, StreamlineOutput, ], pd.Field(discriminator="output_type"), ] TimeAverageOutputTypes = ( TimeAverageSurfaceOutput, TimeAverageVolumeOutput, TimeAverageSliceOutput, TimeAverageIsosurfaceOutput, TimeAverageProbeOutput, TimeAverageSurfaceProbeOutput, )