Source code for flow360_schema.models.simulation.user_defined_dynamics.user_defined_dynamics

"""User defined dynamic model for SimulationParams"""

import pydantic as pd

from flow360_schema.framework.base_model import Flow360BaseModel
from flow360_schema.framework.entity.entity_list import EntityList
from flow360_schema.framework.expression import StringExpression
from flow360_schema.models.entities.surface_entities import (
    MirroredSurface,
    Surface,
)
from flow360_schema.models.entities.volume_entities import (
    CustomVolume,
    Cylinder,
    GenericVolume,
    SeedpointVolume,
)
from flow360_schema.models.simulation.validation.validation_context import (
    ParamsValidationInfo,
    contextual_field_validator,
)
from flow360_schema.models.simulation.validation.validation_utils import (
    validate_entity_list_surface_existence,
)


[docs] class UserDefinedDynamic(Flow360BaseModel): """ :class:`UserDefinedDynamic` class for defining the user defined dynamics inputs. Please refer to :doc:`this example </python_api/example_library/notebooks/udd_alpha_controller>` for an implementation example. Example ------- >>> fl.UserDefinedDynamic( ... name="dynamicTheta", ... input_vars=["momentY"], ... constants={ ... "I": 0.443768309310345, ... "zeta": zeta, ... "K": K, ... "omegaN": omegaN, ... "theta0": theta0, ... }, ... output_vars={ ... "omegaDot": "state[0];", ... "omega": "state[1];", ... "theta": "state[2];", ... }, ... state_vars_initial_value=[str(initOmegaDot), "0.0", str(initTheta)], ... update_law=[ ... "if (pseudoStep == 0) (momentY - K * ( state[2] - theta0 ) " ... + "- 2 * zeta * omegaN * I *state[1] ) / I; else state[0];", ... "if (pseudoStep == 0) state[1] + state[0] * timeStepSize; else state[1];", ... "if (pseudoStep == 0) state[2] + state[1] * timeStepSize; else state[2];", ... ], ... input_boundary_patches=volume_mesh["plateBlock/noSlipWall"], ... output_target=volume_mesh["plateBlock"], ... ) ==== """ name: str = pd.Field("User defined dynamics", description="Name of the dynamics defined by the user.") input_vars: list[str] = pd.Field( description="List of the inputs to define the user defined dynamics. Inputs can be: :code:`CL`, :code:`CD`, " + ":code:`bet_NUM_torque`, :code:`bet_NUM_thrust`, (NUM is the index of the BET disk starting from 0), " + ":code:`momentX`, :code:`momentY`, :code:`momentZ` (X/Y/Z moments with respect to " + ":py:attr:`~ReferenceGeometry.moment_center`), :code:`forceX`, :code:`forceY`, :code:`forceZ`. " ) constants: dict[str, float] | None = pd.Field( None, description="A list of constants that can be used in the expressions." ) output_vars: dict[str, StringExpression] | None = pd.Field( None, description="Name of the output variables and the expression for the output variables using state " + "variables. Outputs can be: :code:`alphaAngle`, :code:`betaAngle`, " + ":code:`bet_NUM_omega` (NUM is the index of the BET disk starting from 0), " + ":code:`theta`, :code:`omega` and :code:`omegaDot` (rotation angle/velocity/acceleration " + "in radians for sliding interfaces), :code:`actuatorDisk_<DISK_ENTITY_NAME>_thrustMultiplier` " + "and :code:`actuatorDisk_<DISK_ENTITY_NAME>_torqueMultiplier` (where <DISK_ENTITY_NAME> is " + "the name of the actuator disk entity). Please exercise caution when choosing output " + "variables, as any modifications to their values will be directly mirrored in the solver.", ) state_vars_initial_value: list[StringExpression] = pd.Field( description="The initial value of state variables are specified here. The entries could be either values " + "(in the form of strings, e.g., :code:`0.0`) or expression with constants defined earlier or any input " + "and output variable. (e.g., :code:`2.0 * alphaAngle + someConstant`). The list entries correspond to " + "the initial values for :code:`state[0]`, :code:`state[1]`, ..., respectively." ) update_law: list[StringExpression] = pd.Field( "List of expressions for updating state variables. The list entries correspond to the update laws for " + ":code:`state[0]`, :code:`state[1]`, ..., respectively. These expressions follows similar guidelines as " + ":ref:`user Defined Expressions<UserDefinedExpressions>`." ) input_boundary_patches: EntityList[Surface, MirroredSurface] | None = pd.Field( None, description="The list of :class:`~flow360.Surface` entities to which the input variables belongs. " + "If multiple boundaries are specified then the summation over the boundaries are used as the input. " + "For input variables that already specified the source in the name (like bet_NUM_torque) " + "this entry does not have any effect.", ) output_target: Cylinder | GenericVolume | Surface | CustomVolume | None = pd.Field( None, description="The target to which the output variables belong to. For example this can be the rotating " + "volume zone name. Only one output target is supported per user defined dynamics instance. Only " + ":class:`~flow360.Cylinder` entity is supported as target for now.", ) # Limited to `Cylinder` for now as we have only tested using UDD to control rotation.
[docs] @contextual_field_validator("input_boundary_patches", mode="after") @classmethod def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo): """Ensure all boundaries will be present after mesher""" return validate_entity_list_surface_existence(value, param_info)
[docs] @contextual_field_validator("output_target", mode="after") @classmethod def ensure_output_surface_existence(cls, value, param_info: ParamsValidationInfo): """Ensure that the output target surface is not a deleted surface""" # TODO: We can make this a Surface's after model validator once entity info is separated from params. # TODO: And therefore no need for duplicate-code override. if isinstance(value, Surface) and value._will_be_deleted_by_mesher( entity_transformation_detected=param_info.entity_transformation_detected, farfield_method=param_info.farfield_method, global_bounding_box=param_info.global_bounding_box, planar_face_tolerance=param_info.planar_face_tolerance, half_model_symmetry_plane_center_y=param_info.half_model_symmetry_plane_center_y, quasi_3d_symmetry_planes_center_y=param_info.quasi_3d_symmetry_planes_center_y, farfield_domain_type=param_info.farfield_domain_type, ): raise ValueError( f"Boundary `{value.name}` will likely be deleted after mesh generation. Therefore it cannot be used." ) return value
@contextual_field_validator("output_target", mode="after") @classmethod def _ensure_custom_volume_is_valid( cls, value: GenericVolume | Cylinder | CustomVolume | SeedpointVolume | None, param_info: ParamsValidationInfo, ): """Ensure parent volume is a custom volume.""" if value is None: return value if not isinstance(value, (CustomVolume, SeedpointVolume)): return value if value.name not in param_info.to_be_generated_custom_volumes: raise ValueError( f"Parent {type(value).__name__} {value.name} is not listed under meshing->volume_zones(or zones)" + "->CustomZones." ) return value