From b5b2197984b96f612853217b4cfb3cb3e734d9cc Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Fri, 25 Apr 2025 10:46:30 +0200 Subject: [PATCH 1/4] Reorganized solver variables into target namespaces --- flow360/__init__.py | 52 +++------------ .../simulation/blueprint/flow360/symbols.py | 63 ++++++++++++++++++- flow360/component/simulation/services.py | 4 +- .../component/simulation/solver_builtins.py | 48 -------------- .../simulation/variables/__init__.py | 0 .../simulation/variables/control_variables.py | 21 +++++++ .../variables/solution_variables.py | 32 ++++++++++ tests/simulation/test_expressions.py | 2 +- 8 files changed, 124 insertions(+), 98 deletions(-) delete mode 100644 flow360/component/simulation/solver_builtins.py create mode 100644 flow360/component/simulation/variables/__init__.py create mode 100644 flow360/component/simulation/variables/control_variables.py create mode 100644 flow360/component/simulation/variables/solution_variables.py diff --git a/flow360/__init__.py b/flow360/__init__.py index 6c0970895..0664f493e 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -9,6 +9,8 @@ from flow360.component.project import Project from flow360.component.simulation import migration, services from flow360.component.simulation import units as u +from flow360.component.simulation.variables import control_variables as control +from flow360.component.simulation.variables import solution_variables as solution from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.meshing_param.edge_params import ( AngleBasedRefinement, @@ -148,51 +150,8 @@ from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import ( UserDefinedDynamic, ) -from flow360.component.simulation.solver_builtins import ( - mut, - mu, - solutionNavierStokes, - residualNavierStokes, - solutionTurbulence, - residualTurbulence, - kOmega, - nuHat, - solutionTransition, - residualTransition, - solutionHeatSolver, - residualHeatSolver, - coordinate, - physicalStep, - pseudoStep, - timeStepSize, - alphaAngle, - betaAngle, - pressureFreestream, - momentLengthX, - momentLengthY, - momentLengthZ, - momentCenterX, - momentCenterY, - momentCenterZ, - bet_thrust, - bet_torque, - bet_omega, - CD, - CL, - forceX, - forceY, - forceZ, - momentX, - momentY, - momentZ, - nodeNormals, - theta, - omega, - omegaDot, - wallFunctionMetric, - wallShearStress, - yPlus, -) +from flow360.component.simulation.variables.control_variables import * +from flow360.component.simulation.variables.solution_variables import * from flow360.component.surface_mesh_v2 import SurfaceMeshV2 as SurfaceMesh from flow360.component.volume_mesh import VolumeMeshV2 as VolumeMesh from flow360.environment import Env @@ -317,6 +276,9 @@ "PointArray2D", "StreamlineOutput", "Transformation", + "MachRef", + "Tref", + "t", "mut", "mu", "solutionNavierStokes", diff --git a/flow360/component/simulation/blueprint/flow360/symbols.py b/flow360/component/simulation/blueprint/flow360/symbols.py index 033032ed1..05795673a 100644 --- a/flow360/component/simulation/blueprint/flow360/symbols.py +++ b/flow360/component/simulation/blueprint/flow360/symbols.py @@ -29,6 +29,16 @@ def _import_flow360(name: str) -> Any: return u + if name == "control": + from flow360 import control + + return control + + if name == "solution": + from flow360 import solution + + return solution + WHITELISTED_CALLABLES = { "flow360.units": { @@ -36,8 +46,57 @@ def _import_flow360(name: str) -> Any: "callables": _unit_list(), "evaluate": True }, - "flow360.solver_builtins": { - "prefix": "fl.", + "flow360.control": { + "prefix": "control.", + "callables": [ + "mut", + "mu", + "solutionNavierStokes", + "residualNavierStokes", + "solutionTurbulence", + "residualTurbulence", + "kOmega", + "nuHat", + "solutionTransition", + "residualTransition", + "solutionHeatSolver", + "residualHeatSolver", + "coordinate", + "physicalStep", + "pseudoStep", + "timeStepSize", + "alphaAngle", + "betaAngle", + "pressureFreestream", + "momentLengthX", + "momentLengthY", + "momentLengthZ", + "momentCenterX", + "momentCenterY", + "momentCenterZ", + "bet_thrust", + "bet_torque", + "bet_omega", + "CD", + "CL", + "forceX", + "forceY", + "forceZ", + "momentX", + "momentY", + "momentZ", + "nodeNormals", + "theta", + "omega", + "omegaDot", + "wallFunctionMetric", + "wallShearStress", + "yPlus", + ], + "evaluate": False + }, + "flow360.solution": { + "prefix": "solution.", "callables": [ "mut", "mu", diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 57347322d..7b75569f8 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -8,7 +8,6 @@ import pydantic as pd # Required for correct global scope initialization -from flow360.component.simulation.solver_builtins import * from flow360.component.simulation.exposed_units import supported_units_by_front_end from flow360.component.simulation.framework.multi_constructor_model_base import ( @@ -22,7 +21,8 @@ from flow360.component.simulation.models.volume_models import ( # pylint: disable=unused-import BETDisk, ) -from flow360.component.simulation.operating_condition.operating_condition import ( # pylint: disable=unused-import +# pylint: disable=unused-import +from flow360.component.simulation.operating_condition.operating_condition import ( AerospaceCondition, GenericReferenceCondition, ThermalState, diff --git a/flow360/component/simulation/solver_builtins.py b/flow360/component/simulation/solver_builtins.py deleted file mode 100644 index 916758531..000000000 --- a/flow360/component/simulation/solver_builtins.py +++ /dev/null @@ -1,48 +0,0 @@ -from flow360.component.simulation.user_code import SolverVariable - -mut = SolverVariable(name="fl.mut", value=float("NaN")) # Turbulent viscosity -mu = SolverVariable(name="fl.mu", value=float("NaN")) # Laminar viscosity -solutionNavierStokes = SolverVariable(name="fl.solutionNavierStokes", value=float("NaN")) # Solution for N-S equation in conservative form -residualNavierStokes = SolverVariable(name="fl.residualNavierStokes", value=float("NaN")) # Residual for N-S equation in conservative form -solutionTurbulence = SolverVariable(name="fl.solutionTurbulence", value=float("NaN")) # Solution for turbulence model -residualTurbulence = SolverVariable(name="fl.residualTurbulence", value=float("NaN")) # Residual for turbulence model -kOmega = SolverVariable(name="fl.kOmega", value=float("NaN")) # Effectively solutionTurbulence when using SST model -nuHat = SolverVariable(name="fl.nuHat", value=float("NaN")) # Effectively solutionTurbulence when using SA model -solutionTransition = SolverVariable(name="fl.solutionTransition", value=float("NaN")) # Solution for transition model -residualTransition = SolverVariable(name="fl.residualTransition", value=float("NaN")) # Residual for transition model -solutionHeatSolver = SolverVariable(name="fl.solutionHeatSolver", value=float("NaN")) # Solution for heat equation -residualHeatSolver = SolverVariable(name="fl.residualHeatSolver", value=float("NaN")) # Residual for heat equation -coordinate = SolverVariable(name="fl.coordinate", value=float("NaN")) # Grid coordinates - -physicalStep = SolverVariable(name="fl.physicalStep", value=float("NaN")) # Physical time step, starting from 0 -pseudoStep = SolverVariable(name="fl.pseudoStep", value=float("NaN")) # Pseudo time step within physical time step -timeStepSize = SolverVariable(name="fl.timeStepSize", value=float("NaN")) # Physical time step size -alphaAngle = SolverVariable(name="fl.alphaAngle", value=float("NaN")) # Alpha angle specified in freestream -betaAngle = SolverVariable(name="fl.betaAngle", value=float("NaN")) # Beta angle specified in freestream -pressureFreestream = SolverVariable(name="fl.pressureFreestream", value=float("NaN")) # Freestream reference pressure (1.0/1.4) -momentLengthX = SolverVariable(name="fl.momentLengthX", value=float("NaN")) # X component of momentLength -momentLengthY = SolverVariable(name="fl.momentLengthY", value=float("NaN")) # Y component of momentLength -momentLengthZ = SolverVariable(name="fl.momentLengthZ", value=float("NaN")) # Z component of momentLength -momentCenterX = SolverVariable(name="fl.momentCenterX", value=float("NaN")) # X component of momentCenter -momentCenterY = SolverVariable(name="fl.momentCenterY", value=float("NaN")) # Y component of momentCenter -momentCenterZ = SolverVariable(name="fl.momentCenterZ", value=float("NaN")) # Z component of momentCenter - -bet_thrust = SolverVariable(name="fl.bet_thrust", value=float("NaN")) # Thrust force for BET disk -bet_torque = SolverVariable(name="fl.bet_torque", value=float("NaN")) # Torque for BET disk -bet_omega = SolverVariable(name="fl.bet_omega", value=float("NaN")) # Rotation speed for BET disk -CD = SolverVariable(name="fl.CD", value=float("NaN")) # Drag coefficient on patch -CL = SolverVariable(name="fl.CL", value=float("NaN")) # Lift coefficient on patch -forceX = SolverVariable(name="fl.forceX", value=float("NaN")) # Total force in X direction -forceY = SolverVariable(name="fl.forceY", value=float("NaN")) # Total force in Y direction -forceZ = SolverVariable(name="fl.forceZ", value=float("NaN")) # Total force in Z direction -momentX = SolverVariable(name="fl.momentX", value=float("NaN")) # Total moment in X direction -momentY = SolverVariable(name="fl.momentY", value=float("NaN")) # Total moment in Y direction -momentZ = SolverVariable(name="fl.momentZ", value=float("NaN")) # Total moment in Z direction -nodeNormals = SolverVariable(name="fl.nodeNormals", value=float("NaN")) # Normal vector of patch -theta = SolverVariable(name="fl.theta", value=float("NaN")) # Rotation angle of volume zone -omega = SolverVariable(name="fl.omega", value=float("NaN")) # Rotation speed of volume zone -omegaDot = SolverVariable(name="fl.omegaDot", value=float("NaN")) # Rotation acceleration of volume zone -wallFunctionMetric = SolverVariable(name="fl.wallFunctionMetric", value=float("NaN")) # Wall model quality indicator -wallShearStress = SolverVariable(name="fl.wallShearStress", value=float("NaN")) # Wall viscous shear stress -yPlus = SolverVariable(name="fl.yPlus", value=float("NaN")) # Non-dimensional wall distance - diff --git a/flow360/component/simulation/variables/__init__.py b/flow360/component/simulation/variables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flow360/component/simulation/variables/control_variables.py b/flow360/component/simulation/variables/control_variables.py new file mode 100644 index 000000000..b01f4f231 --- /dev/null +++ b/flow360/component/simulation/variables/control_variables.py @@ -0,0 +1,21 @@ +from flow360.component.simulation.user_code import SolverVariable + +MachRef = SolverVariable(name="control.MachRef", value=float("NaN")) # Reference mach specified by the user +Tref = SolverVariable(name="control.Tref", value=float("NaN")) # Temperature specified by the user +t = SolverVariable(name="control.t", value=float("NaN")) # Physical time +physicalStep = SolverVariable(name="control.physicalStep", value=float("NaN")) # Physical time step, starting from 0 +pseudoStep = SolverVariable(name="control.pseudoStep", value=float("NaN")) # Pseudo time step within physical time step +timeStepSize = SolverVariable(name="control.timeStepSize", value=float("NaN")) # Physical time step size +alphaAngle = SolverVariable(name="control.alphaAngle", value=float("NaN")) # Alpha angle specified in freestream +betaAngle = SolverVariable(name="control.betaAngle", value=float("NaN")) # Beta angle specified in freestream +pressureFreestream = SolverVariable(name="control.pressureFreestream", value=float("NaN")) # Freestream reference pressure (1.0/1.4) +momentLengthX = SolverVariable(name="control.momentLengthX", value=float("NaN")) # X component of momentLength +momentLengthY = SolverVariable(name="control.momentLengthY", value=float("NaN")) # Y component of momentLength +momentLengthZ = SolverVariable(name="control.momentLengthZ", value=float("NaN")) # Z component of momentLength +momentCenterX = SolverVariable(name="control.momentCenterX", value=float("NaN")) # X component of momentCenter +momentCenterY = SolverVariable(name="control.momentCenterY", value=float("NaN")) # Y component of momentCenter +momentCenterZ = SolverVariable(name="control.momentCenterZ", value=float("NaN")) # Z component of momentCenter +theta = SolverVariable(name="control.theta", value=float("NaN")) # Rotation angle of volume zone +omega = SolverVariable(name="control.omega", value=float("NaN")) # Rotation speed of volume zone +omegaDot = SolverVariable(name="control.omegaDot", value=float("NaN")) # Rotation acceleration of volume zone + diff --git a/flow360/component/simulation/variables/solution_variables.py b/flow360/component/simulation/variables/solution_variables.py new file mode 100644 index 000000000..e45cffe29 --- /dev/null +++ b/flow360/component/simulation/variables/solution_variables.py @@ -0,0 +1,32 @@ +from flow360.component.simulation.user_code import SolverVariable + +mut = SolverVariable(name="solution.mut", value=float("NaN")) # Turbulent viscosity +mu = SolverVariable(name="solution.mu", value=float("NaN")) # Laminar viscosity +solutionNavierStokes = SolverVariable(name="solution.solutionNavierStokes", value=float("NaN")) # Solution for N-S equation in conservative form +residualNavierStokes = SolverVariable(name="solution.residualNavierStokes", value=float("NaN")) # Residual for N-S equation in conservative form +solutionTurbulence = SolverVariable(name="solution.solutionTurbulence", value=float("NaN")) # Solution for turbulence model +residualTurbulence = SolverVariable(name="solution.residualTurbulence", value=float("NaN")) # Residual for turbulence model +kOmega = SolverVariable(name="solution.kOmega", value=float("NaN")) # Effectively solutionTurbulence when using SST model +nuHat = SolverVariable(name="solution.nuHat", value=float("NaN")) # Effectively solutionTurbulence when using SA model +solutionTransition = SolverVariable(name="solution.solutionTransition", value=float("NaN")) # Solution for transition model +residualTransition = SolverVariable(name="solution.residualTransition", value=float("NaN")) # Residual for transition model +solutionHeatSolver = SolverVariable(name="solution.solutionHeatSolver", value=float("NaN")) # Solution for heat equation +residualHeatSolver = SolverVariable(name="solution.residualHeatSolver", value=float("NaN")) # Residual for heat equation +coordinate = SolverVariable(name="solution.coordinate", value=float("NaN")) # Grid coordinates + +bet_thrust = SolverVariable(name="solution.bet_thrust", value=float("NaN")) # Thrust force for BET disk +bet_torque = SolverVariable(name="solution.bet_torque", value=float("NaN")) # Torque for BET disk +bet_omega = SolverVariable(name="solution.bet_omega", value=float("NaN")) # Rotation speed for BET disk +CD = SolverVariable(name="solution.CD", value=float("NaN")) # Drag coefficient on patch +CL = SolverVariable(name="solution.CL", value=float("NaN")) # Lift coefficient on patch +forceX = SolverVariable(name="solution.forceX", value=float("NaN")) # Total force in X direction +forceY = SolverVariable(name="solution.forceY", value=float("NaN")) # Total force in Y direction +forceZ = SolverVariable(name="solution.forceZ", value=float("NaN")) # Total force in Z direction +momentX = SolverVariable(name="solution.momentX", value=float("NaN")) # Total moment in X direction +momentY = SolverVariable(name="solution.momentY", value=float("NaN")) # Total moment in Y direction +momentZ = SolverVariable(name="solution.momentZ", value=float("NaN")) # Total moment in Z direction +nodeNormals = SolverVariable(name="solution.nodeNormals", value=float("NaN")) # Normal vector of patch +wallFunctionMetric = SolverVariable(name="solution.wallFunctionMetric", value=float("NaN")) # Wall model quality indicator +wallShearStress = SolverVariable(name="solution.wallShearStress", value=float("NaN")) # Wall viscous shear stress +yPlus = SolverVariable(name="solution.yPlus", value=float("NaN")) # Non-dimensional wall distance + diff --git a/tests/simulation/test_expressions.py b/tests/simulation/test_expressions.py index 54823b888..dd0652cbe 100644 --- a/tests/simulation/test_expressions.py +++ b/tests/simulation/test_expressions.py @@ -355,7 +355,7 @@ class TestModel(Flow360BaseModel): model = TestModel(field=x * u.m + fl.kOmega * u.cm) - assert str(model.field) == "x * u.m + (fl.kOmega * u.cm)" + assert str(model.field) == "x * u.m + (solution.kOmega * u.cm)" # Raises when trying to evaluate with a message about this variable being blacklisted with pytest.raises(ValueError): From 991697bb159782bb941913e84535ef3c62a39dba Mon Sep 17 00:00:00 2001 From: Andrzej Krupka Date: Wed, 7 May 2025 14:58:59 +0200 Subject: [PATCH 2/4] Added ability to convert expressions to C++ syntax --- .../simulation/blueprint/__init__.py | 4 +- .../simulation/blueprint/codegen/generator.py | 297 ++++++++++++------ .../simulation/blueprint/codegen/parser.py | 2 +- .../simulation/blueprint/utils/operators.py | 1 + .../simulation/blueprint/utils/types.py | 7 + flow360/component/simulation/user_code.py | 25 +- tests/simulation/test_expressions.py | 18 +- 7 files changed, 252 insertions(+), 102 deletions(-) create mode 100644 flow360/component/simulation/blueprint/utils/types.py diff --git a/flow360/component/simulation/blueprint/__init__.py b/flow360/component/simulation/blueprint/__init__.py index 7b9bb46a1..187e52626 100644 --- a/flow360/component/simulation/blueprint/__init__.py +++ b/flow360/component/simulation/blueprint/__init__.py @@ -1,7 +1,7 @@ """Blueprint: Safe function serialization and visual programming integration.""" from .codegen.generator import model_to_function -from .codegen.parser import function_to_model, expression_to_model +from .codegen.parser import function_to_model, expr_to_model from .core.function import Function -__all__ = ["Function", "function_to_model", "model_to_function", "expression_to_model"] +__all__ = ["Function", "function_to_model", "model_to_function", "expr_to_model"] diff --git a/flow360/component/simulation/blueprint/codegen/generator.py b/flow360/component/simulation/blueprint/codegen/generator.py index a324a694b..c232c83fc 100644 --- a/flow360/component/simulation/blueprint/codegen/generator.py +++ b/flow360/component/simulation/blueprint/codegen/generator.py @@ -1,3 +1,4 @@ +import functools from typing import Any from ..core.expressions import ( @@ -9,6 +10,7 @@ Name, RangeCall, Tuple, + UnaryOp, ) from ..core.function import Function from ..core.statements import ( @@ -19,7 +21,8 @@ Return, TupleUnpack, ) -from ..utils.operators import BINARY_OPERATORS +from ..utils.operators import BINARY_OPERATORS, UNARY_OPERATORS +from ..utils.types import TargetSyntax def _indent(code: str, level: int = 1) -> str: @@ -28,33 +31,74 @@ def _indent(code: str, level: int = 1) -> str: return "\n".join(spaces + line if line else line for line in code.split("\n")) -def expr_to_code(expr: Any) -> str: - """Convert an expression model back to Python code.""" - if expr is None: +def check_syntax_type(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if result is None: + raise ValueError(f"Unsupported syntax type, available {[syntax.name for syntax in TargetSyntax]}") + return result + return wrapper + + +@check_syntax_type +def _empty(syntax): + if syntax == TargetSyntax.PYTHON: return "None" + elif syntax == TargetSyntax.CPP: + return "null" - if isinstance(expr, Name): - return expr.id - elif isinstance(expr, Constant): - if isinstance(expr.value, str): - return f"'{expr.value}'" - return str(expr.value) +@check_syntax_type +def _name(expr, remap): + return expr.id if expr.id not in remap else remap[expr.id] - elif isinstance(expr, BinOp): - op_info = BINARY_OPERATORS[expr.op] - return f"({expr_to_code(expr.left)} {op_info.symbol} {expr_to_code(expr.right)})" - elif isinstance(expr, RangeCall): - return f"range({expr_to_code(expr.arg)})" +@check_syntax_type +def _constant(expr): + if isinstance(expr.value, str): + return f"'{expr.value}'" + return str(expr.value) - elif isinstance(expr, CallModel): - args_str = ", ".join(expr_to_code(arg) for arg in expr.args) + +@check_syntax_type +def _unary_op(expr, syntax, remap): + op_info = UNARY_OPERATORS[expr.op] + return f"{op_info.symbol}{expr_to_code(expr.operand, syntax, remap)}" + + +@check_syntax_type +def _binary_op(expr, syntax, remap): + if syntax == TargetSyntax.CPP: + # Special case handling for operators not directly supported in CPP syntax, requires #include + if expr.op == "FloorDiv": + return f"floor({expr_to_code(expr.left, syntax, remap)} / {expr_to_code(expr.right, syntax, remap)})" + if expr.op == "Pow": + return f"pow({expr_to_code(expr.left, syntax, remap)}, {expr_to_code(expr.right, syntax, remap)})" + if expr.op == "Is": + return f"&{expr_to_code(expr.left, syntax, remap)} == &{expr_to_code(expr.right, syntax, remap)}" + + op_info = BINARY_OPERATORS[expr.op] + return f"({expr_to_code(expr.left, syntax, remap)} {op_info.symbol} {expr_to_code(expr.right, syntax, remap)})" + + +@check_syntax_type +def _range_call(expr, syntax, remap): + if syntax == TargetSyntax.PYTHON: + return f"range({expr_to_code(expr.arg, syntax, remap)})" + + raise ValueError("Range calls are only supported for Python target syntax") + + +@check_syntax_type +def _call_model(expr, syntax, remap): + if syntax == TargetSyntax.PYTHON: + args_str = ", ".join(expr_to_code(arg, syntax, remap) for arg in expr.args) kwargs_parts = [] for k, v in expr.kwargs.items(): if v is None: continue - val_str = expr_to_code(v) + val_str = expr_to_code(v, syntax, remap) if not val_str or val_str.isspace(): continue kwargs_parts.append(f"{k}={val_str}") @@ -62,96 +106,171 @@ def expr_to_code(expr: Any) -> str: kwargs_str = ", ".join(kwargs_parts) all_args = ", ".join(x for x in [args_str, kwargs_str] if x) return f"{expr.func_qualname}({all_args})" + elif syntax == TargetSyntax.CPP: + args_str = ", ".join(expr_to_code(arg, syntax, remap) for arg in expr.args) + if expr.kwargs: + raise ValueError("Named arguments are not supported in C++ syntax") + return f"{expr.func_qualname}({args_str})" - elif isinstance(expr, Tuple): + +@check_syntax_type +def _tuple(expr, syntax, remap): + if syntax == TargetSyntax.PYTHON: if len(expr.elements) == 0: return "()" elif len(expr.elements) == 1: - return f"({expr_to_code(expr.elements[0])},)" - return f"({', '.join(expr_to_code(e) for e in expr.elements)})" + return f"({expr_to_code(expr.elements[0], syntax, remap)},)" + return f"({', '.join(expr_to_code(e, syntax, remap) for e in expr.elements)})" + elif syntax == TargetSyntax.CPP: + if len(expr.elements) == 0: + return "{}" + elif len(expr.elements) == 1: + return f"{{{expr_to_code(expr.elements[0], syntax, remap)}}}" + return f"{{{', '.join(expr_to_code(e, syntax, remap) for e in expr.elements)}}}" - elif isinstance(expr, List): + +@check_syntax_type +def _list(expr, syntax, remap): + if syntax == TargetSyntax.PYTHON: if not expr.elements: return "[]" - elements = [expr_to_code(e) for e in expr.elements] + elements = [expr_to_code(e, syntax, remap) for e in expr.elements] elements_str = ", ".join(elements) return f"[{elements_str}]" + elif syntax == TargetSyntax.CPP: + if len(expr.elements) == 0: + return "{}" + return f"{{{', '.join(expr_to_code(e, syntax, remap) for e in expr.elements)}}}" + + +def _list_comp(expr, syntax, remap): + if syntax == TargetSyntax.PYTHON: + return f"[{expr_to_code(expr.element, syntax, remap)} for {expr.target, syntax, remap} in {expr_to_code(expr.iter, syntax, remap)}]" + + raise ValueError("List comprehensions are only supported for Python target syntax") + +def expr_to_code( + expr: Any, + syntax: TargetSyntax = TargetSyntax.PYTHON, + remap: dict[str, str] = None +) -> str: + """Convert an expression model back to source code.""" + if expr is None: + return _empty(syntax) + + # Names and constants are language-agnostic (apart from symbol remaps) + if isinstance(expr, Name): + return _name(expr, remap) + + elif isinstance(expr, Constant): + return _constant(expr) + + elif isinstance(expr, UnaryOp): + return _unary_op(expr, syntax, remap) + + elif isinstance(expr, BinOp): + return _binary_op(expr, syntax, remap) + + elif isinstance(expr, RangeCall): + return _range_call(expr, syntax, remap) + + elif isinstance(expr, CallModel): + return _call_model(expr, syntax, remap) + + elif isinstance(expr, Tuple): + return _tuple(expr, syntax, remap) + + elif isinstance(expr, List): + return _list(expr, syntax, remap) elif isinstance(expr, ListComp): - return f"[{expr_to_code(expr.element)} for {expr.target} in {expr_to_code(expr.iter)}]" + return _list_comp(expr, syntax, remap) else: raise ValueError(f"Unsupported expression type: {type(expr)}") -def stmt_to_code(stmt: Any) -> str: - """Convert a statement model back to Python code.""" - if isinstance(stmt, Assign): - if stmt.target == "_": # Expression statement - return expr_to_code(stmt.value) - return f"{stmt.target} = {expr_to_code(stmt.value)}" - - elif isinstance(stmt, AugAssign): - op_map = { - "Add": "+=", - "Sub": "-=", - "Mult": "*=", - "Div": "/=", - } - op_str = op_map.get(stmt.op, f"{stmt.op}=") - return f"{stmt.target} {op_str} {expr_to_code(stmt.value)}" - - elif isinstance(stmt, IfElse): - code = [f"if {expr_to_code(stmt.condition)}:"] - code.append(_indent("\n".join(stmt_to_code(s) for s in stmt.body))) - if stmt.orelse: - code.append("else:") - code.append(_indent("\n".join(stmt_to_code(s) for s in stmt.orelse))) - return "\n".join(code) - - elif isinstance(stmt, ForLoop): - code = [f"for {stmt.target} in {expr_to_code(stmt.iter)}:"] - code.append(_indent("\n".join(stmt_to_code(s) for s in stmt.body))) - return "\n".join(code) - - elif isinstance(stmt, Return): - return f"return {expr_to_code(stmt.value)}" - - elif isinstance(stmt, TupleUnpack): - targets = ", ".join(stmt.targets) - if len(stmt.values) == 1: - # Single expression that evaluates to a tuple - return f"{targets} = {expr_to_code(stmt.values[0])}" - else: - # Multiple expressions - values = ", ".join(expr_to_code(v) for v in stmt.values) - return f"{targets} = {values}" +def stmt_to_code( + stmt: Any, + syntax: TargetSyntax = TargetSyntax.PYTHON, + remap: dict[str, str] = None +) -> str: + if syntax == TargetSyntax.PYTHON: + """Convert a statement model back to source code.""" + if isinstance(stmt, Assign): + if stmt.target == "_": # Expression statement + return expr_to_code(stmt.value) + return f"{stmt.target} = {expr_to_code(stmt.value, syntax, remap)}" - else: - raise ValueError(f"Unsupported statement type: {type(stmt)}") - - -def model_to_function(func: Function) -> str: - """Convert a Function model back to Python code.""" - # Build the function signature - args_with_defaults = [] - for arg in func.args: - if arg in func.defaults: - default_val = func.defaults[arg] - if isinstance(default_val, int | float | bool | str): - args_with_defaults.append(f"{arg}={default_val}") + elif isinstance(stmt, AugAssign): + op_map = { + "Add": "+=", + "Sub": "-=", + "Mult": "*=", + "Div": "/=", + } + op_str = op_map.get(stmt.op, f"{stmt.op}=") + return f"{stmt.target} {op_str} {expr_to_code(stmt.value, syntax, remap)}" + + elif isinstance(stmt, IfElse): + code = [f"if {expr_to_code(stmt.condition)}:"] + code.append(_indent("\n".join(stmt_to_code(s, syntax, remap) for s in stmt.body))) + if stmt.orelse: + code.append("else:") + code.append(_indent("\n".join(stmt_to_code(s, syntax, remap) for s in stmt.orelse))) + return "\n".join(code) + + elif isinstance(stmt, ForLoop): + code = [f"for {stmt.target} in {expr_to_code(stmt.iter)}:"] + code.append(_indent("\n".join(stmt_to_code(s, syntax, remap) for s in stmt.body))) + return "\n".join(code) + + elif isinstance(stmt, Return): + return f"return {expr_to_code(stmt.value, syntax, remap)}" + + elif isinstance(stmt, TupleUnpack): + targets = ", ".join(stmt.targets) + if len(stmt.values) == 1: + # Single expression that evaluates to a tuple + return f"{targets} = {expr_to_code(stmt.values[0], syntax, remap)}" else: - args_with_defaults.append(f"{arg}={expr_to_code(default_val)}") + # Multiple expressions + values = ", ".join(expr_to_code(v, syntax, remap) for v in stmt.values) + return f"{targets} = {values}" else: - args_with_defaults.append(arg) + raise ValueError(f"Unsupported statement type: {type(stmt)}") + + raise NotImplementedError("Statement translation is not available for other syntax types yet") + + +def model_to_function( + func: Function, + syntax: TargetSyntax = TargetSyntax.PYTHON, + remap: dict[str, str] = None +) -> str: + if syntax == TargetSyntax.PYTHON: + """Convert a Function model back to source code.""" + # Build the function signature + args_with_defaults = [] + for arg in func.args: + if arg in func.defaults: + default_val = func.defaults[arg] + if isinstance(default_val, (int, float, str, bool)): + args_with_defaults.append(f"{arg}={default_val}") + else: + args_with_defaults.append(f"{arg}={expr_to_code(default_val, syntax, remap)}") + else: + args_with_defaults.append(arg) + + signature = f"def {func.name}({', '.join(args_with_defaults)}):" - signature = f"def {func.name}({', '.join(args_with_defaults)}):" + # Convert the function body + body_lines = [] + for stmt in func.body: + line = stmt_to_code(stmt) + body_lines.append(line) - # Convert the function body - body_lines = [] - for stmt in func.body: - line = stmt_to_code(stmt) - body_lines.append(line) + body = "\n".join(body_lines) if body_lines else "pass" + return f"{signature}\n{_indent(body)}" - body = "\n".join(body_lines) if body_lines else "pass" - return f"{signature}\n{_indent(body)}" + raise NotImplementedError("Function translation is not available for other syntax types yet") diff --git a/flow360/component/simulation/blueprint/codegen/parser.py b/flow360/component/simulation/blueprint/codegen/parser.py index 2af09f57d..6732251c6 100644 --- a/flow360/component/simulation/blueprint/codegen/parser.py +++ b/flow360/component/simulation/blueprint/codegen/parser.py @@ -242,7 +242,7 @@ def function_to_model( return Function(name=name, args=args, body=body, defaults=defaults) -def expression_to_model( +def expr_to_model( source: str, ctx: EvaluationContext, ) -> Expression: diff --git a/flow360/component/simulation/blueprint/utils/operators.py b/flow360/component/simulation/blueprint/utils/operators.py index 249f15546..0ddc1cd7e 100644 --- a/flow360/component/simulation/blueprint/utils/operators.py +++ b/flow360/component/simulation/blueprint/utils/operators.py @@ -1,6 +1,7 @@ import operator from collections.abc import Callable from typing import Any, Union +from .types import TargetSyntax class OpInfo: diff --git a/flow360/component/simulation/blueprint/utils/types.py b/flow360/component/simulation/blueprint/utils/types.py new file mode 100644 index 000000000..05843a198 --- /dev/null +++ b/flow360/component/simulation/blueprint/utils/types.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class TargetSyntax(Enum): + PYTHON = "python", + CPP = "cpp", + # Possibly other languages in the future if needed... \ No newline at end of file diff --git a/flow360/component/simulation/user_code.py b/flow360/component/simulation/user_code.py index 1a6c7accf..50e10c4c9 100644 --- a/flow360/component/simulation/user_code.py +++ b/flow360/component/simulation/user_code.py @@ -5,11 +5,13 @@ from typing_extensions import Self import re +from flow360.component.simulation.blueprint.codegen import expr_to_code from flow360.component.simulation.blueprint.flow360 import resolver +from flow360.component.simulation.blueprint.utils.types import TargetSyntax from flow360.component.simulation.unit_system import * from flow360.component.simulation.blueprint.core import EvaluationContext from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.blueprint import expression_to_model +from flow360.component.simulation.blueprint import expr_to_model import pydantic as pd from numbers import Number @@ -18,6 +20,7 @@ _global_ctx: EvaluationContext = EvaluationContext(resolver) _user_variables: set[str] = set() +_solver_variables: dict[str, str] = dict() def _is_number_string(s: str) -> bool: @@ -190,10 +193,13 @@ def update_context(cls, value): class SolverVariable(Variable): + solver_name: Optional[str] = pd.Field(None, alias="solverName") + @pd.model_validator(mode="after") @classmethod def update_context(cls, value): _global_ctx.set(value.name, value.value) + _solver_variables[value.name] = value.solver_name if value.solver_name is not None else value.name def _handle_syntax_error(se: SyntaxError, source: str): @@ -223,7 +229,6 @@ def _handle_syntax_error(se: SyntaxError, source: str): ) - class Expression(Flow360BaseModel): expression: str = pd.Field("") @@ -241,13 +246,11 @@ def _validate_expression(cls, value) -> Self: elif isinstance(value, Variable): expression = str(value) else: - details = InitErrorDetails( - type="value_error", ctx={"error": f"Invalid type {type(value)}"} - ) + details = InitErrorDetails(type="value_error", ctx={"error": f"Invalid type {type(value)}"}) raise pd.ValidationError.from_exception_data("expression type error", [details]) try: - _ = expression_to_model(expression, _global_ctx) + expr_to_model(expression, _global_ctx) except SyntaxError as s_err: raise _handle_syntax_error(s_err, expression) except ValueError as v_err: @@ -257,18 +260,24 @@ def _validate_expression(cls, value) -> Self: return {"expression": expression} def evaluate(self, strict=True) -> float: - expr = expression_to_model(self.expression, _global_ctx) + expr = expr_to_model(self.expression, _global_ctx) result = expr.evaluate(_global_ctx, strict) return result def user_variables(self): - expr = expression_to_model(self.expression, _global_ctx) + expr = expr_to_model(self.expression, _global_ctx) names = expr.used_names() names = [name for name in names if name in _user_variables] return [UserVariable(name=name, value=_global_ctx.get(name)) for name in names] + def to_solver_code(self): + expr = expr_to_model(self.expression, _global_ctx) + source = expr_to_code(expr, TargetSyntax.CPP, _solver_variables) + # TODO: What do we do with dimensioned expressions? We need to replace all units by their conversion factors. + return source + def __hash__(self): return hash(self.expression) diff --git a/tests/simulation/test_expressions.py b/tests/simulation/test_expressions.py index dd0652cbe..083ec12b6 100644 --- a/tests/simulation/test_expressions.py +++ b/tests/simulation/test_expressions.py @@ -418,8 +418,6 @@ class TestModel(Flow360BaseModel): assert str(deserialized.field) == '4.0 m/s' - - def test_error_message(): class TestModel(Flow360BaseModel): field: ValueOrExpression[VelocityType] = pd.Field() @@ -484,3 +482,19 @@ class TestModel(Flow360BaseModel): assert "column" in validation_errors[0]["ctx"] assert validation_errors[0]["ctx"]["column"] == 11 + +def test_solver_translation(): + class TestModel(Flow360BaseModel): + field: ValueOrExpression[float] = pd.Field() + + x = UserVariable(name="x", value=4) + + model = TestModel(field=(x // 3) ** 2) + + assert isinstance(model.field, Expression) + assert model.field.evaluate() == 1 + assert str(model.field) == "(x // 3) ** 2" + + solver_code = model.field.to_solver_code() + + assert solver_code == "pow(floor(x / 3), 2)" From 4433a5baef062759a2c77e9b25ddd37d185a1af9 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Wed, 7 May 2025 18:18:08 +0000 Subject: [PATCH 3/4] Ran isort and black formatter --- flow360/__init__.py | 4 +- .../simulation/blueprint/__init__.py | 2 +- .../simulation/blueprint/codegen/generator.py | 27 ++---- .../simulation/blueprint/codegen/parser.py | 28 ++---- .../simulation/blueprint/core/expressions.py | 8 +- .../simulation/blueprint/core/resolver.py | 3 +- .../simulation/blueprint/flow360/symbols.py | 19 ++-- .../simulation/blueprint/tidy3d/symbols.py | 27 ++---- .../simulation/blueprint/utils/operators.py | 1 + .../simulation/blueprint/utils/types.py | 6 +- .../simulation/framework/base_model.py | 9 +- .../simulation/framework/param_utils.py | 6 +- flow360/component/simulation/services.py | 6 +- flow360/component/simulation/user_code.py | 43 ++++----- flow360/component/simulation/utils.py | 2 +- .../simulation/variables/control_variables.py | 65 ++++++++++---- .../variables/solution_variables.py | 87 ++++++++++++------- tests/simulation/test_expressions.py | 78 ++++++++--------- 18 files changed, 217 insertions(+), 204 deletions(-) diff --git a/flow360/__init__.py b/flow360/__init__.py index 0664f493e..789907792 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -9,8 +9,6 @@ from flow360.component.project import Project from flow360.component.simulation import migration, services from flow360.component.simulation import units as u -from flow360.component.simulation.variables import control_variables as control -from flow360.component.simulation.variables import solution_variables as solution from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.meshing_param.edge_params import ( AngleBasedRefinement, @@ -150,6 +148,8 @@ from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import ( UserDefinedDynamic, ) +from flow360.component.simulation.variables import control_variables as control +from flow360.component.simulation.variables import solution_variables as solution from flow360.component.simulation.variables.control_variables import * from flow360.component.simulation.variables.solution_variables import * from flow360.component.surface_mesh_v2 import SurfaceMeshV2 as SurfaceMesh diff --git a/flow360/component/simulation/blueprint/__init__.py b/flow360/component/simulation/blueprint/__init__.py index 187e52626..b560db134 100644 --- a/flow360/component/simulation/blueprint/__init__.py +++ b/flow360/component/simulation/blueprint/__init__.py @@ -1,7 +1,7 @@ """Blueprint: Safe function serialization and visual programming integration.""" from .codegen.generator import model_to_function -from .codegen.parser import function_to_model, expr_to_model +from .codegen.parser import expr_to_model, function_to_model from .core.function import Function __all__ = ["Function", "function_to_model", "model_to_function", "expr_to_model"] diff --git a/flow360/component/simulation/blueprint/codegen/generator.py b/flow360/component/simulation/blueprint/codegen/generator.py index c232c83fc..f6bdf4d84 100644 --- a/flow360/component/simulation/blueprint/codegen/generator.py +++ b/flow360/component/simulation/blueprint/codegen/generator.py @@ -13,14 +13,7 @@ UnaryOp, ) from ..core.function import Function -from ..core.statements import ( - Assign, - AugAssign, - ForLoop, - IfElse, - Return, - TupleUnpack, -) +from ..core.statements import Assign, AugAssign, ForLoop, IfElse, Return, TupleUnpack from ..utils.operators import BINARY_OPERATORS, UNARY_OPERATORS from ..utils.types import TargetSyntax @@ -36,8 +29,11 @@ def check_syntax_type(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) if result is None: - raise ValueError(f"Unsupported syntax type, available {[syntax.name for syntax in TargetSyntax]}") + raise ValueError( + f"Unsupported syntax type, available {[syntax.name for syntax in TargetSyntax]}" + ) return result + return wrapper @@ -149,10 +145,9 @@ def _list_comp(expr, syntax, remap): raise ValueError("List comprehensions are only supported for Python target syntax") + def expr_to_code( - expr: Any, - syntax: TargetSyntax = TargetSyntax.PYTHON, - remap: dict[str, str] = None + expr: Any, syntax: TargetSyntax = TargetSyntax.PYTHON, remap: dict[str, str] = None ) -> str: """Convert an expression model back to source code.""" if expr is None: @@ -191,9 +186,7 @@ def expr_to_code( def stmt_to_code( - stmt: Any, - syntax: TargetSyntax = TargetSyntax.PYTHON, - remap: dict[str, str] = None + stmt: Any, syntax: TargetSyntax = TargetSyntax.PYTHON, remap: dict[str, str] = None ) -> str: if syntax == TargetSyntax.PYTHON: """Convert a statement model back to source code.""" @@ -244,9 +237,7 @@ def stmt_to_code( def model_to_function( - func: Function, - syntax: TargetSyntax = TargetSyntax.PYTHON, - remap: dict[str, str] = None + func: Function, syntax: TargetSyntax = TargetSyntax.PYTHON, remap: dict[str, str] = None ) -> str: if syntax == TargetSyntax.PYTHON: """Convert a Function model back to source code.""" diff --git a/flow360/component/simulation/blueprint/codegen/parser.py b/flow360/component/simulation/blueprint/codegen/parser.py index 6732251c6..56ad1662f 100644 --- a/flow360/component/simulation/blueprint/codegen/parser.py +++ b/flow360/component/simulation/blueprint/codegen/parser.py @@ -1,32 +1,14 @@ import ast import inspect from collections.abc import Callable -from typing import Any, Union, Optional +from typing import Any, Optional, Union from ..core.context import EvaluationContext -from ..core.expressions import ( - BinOp, - UnaryOp, - CallModel, - Constant, - ListComp, - Name, - RangeCall, - Tuple, - Expression, -) -from ..core.expressions import ( - List as ListExpr, -) +from ..core.expressions import BinOp, CallModel, Constant, Expression +from ..core.expressions import List as ListExpr +from ..core.expressions import ListComp, Name, RangeCall, Tuple, UnaryOp from ..core.function import Function -from ..core.statements import ( - Assign, - AugAssign, - ForLoop, - IfElse, - Return, - TupleUnpack, -) +from ..core.statements import Assign, AugAssign, ForLoop, IfElse, Return, TupleUnpack def parse_expr(node: ast.AST, ctx: EvaluationContext) -> Any: diff --git a/flow360/component/simulation/blueprint/core/expressions.py b/flow360/component/simulation/blueprint/core/expressions.py index 70327c234..5db519172 100644 --- a/flow360/component/simulation/blueprint/core/expressions.py +++ b/flow360/component/simulation/blueprint/core/expressions.py @@ -185,9 +185,9 @@ def used_names(self) -> set[str]: for arg in self.args: names = names.union(arg.used_names()) - for (keyword, arg) in self.kwargs.items(): + for keyword, arg in self.kwargs.items(): names = names.union(arg.used_names()) - + return names @@ -212,13 +212,13 @@ class List(Expression): def evaluate(self, context: EvaluationContext, strict: bool) -> list: return [elem.evaluate(context, strict) for elem in self.elements] - + def used_names(self) -> set[str]: names = set() for arg in self.elements: names = names.union(arg.used_names()) - + return names diff --git a/flow360/component/simulation/blueprint/core/resolver.py b/flow360/component/simulation/blueprint/core/resolver.py index 99416954a..7ab1467e1 100644 --- a/flow360/component/simulation/blueprint/core/resolver.py +++ b/flow360/component/simulation/blueprint/core/resolver.py @@ -99,7 +99,8 @@ def get_allowed_callable(self, qualname: str) -> Callable[..., Any]: or qualname in self._module_builtins or any( qualname.startswith(f"{group['prefix']}{name}") - for group in self._callable_builtins.values() if group is not None + for group in self._callable_builtins.values() + if group is not None for name in group["callables"] ) ): diff --git a/flow360/component/simulation/blueprint/flow360/symbols.py b/flow360/component/simulation/blueprint/flow360/symbols.py index 05795673a..4532eb9f6 100644 --- a/flow360/component/simulation/blueprint/flow360/symbols.py +++ b/flow360/component/simulation/blueprint/flow360/symbols.py @@ -41,11 +41,7 @@ def _import_flow360(name: str) -> Any: WHITELISTED_CALLABLES = { - "flow360.units": { - "prefix": "u.", - "callables": _unit_list(), - "evaluate": True - }, + "flow360.units": {"prefix": "u.", "callables": _unit_list(), "evaluate": True}, "flow360.control": { "prefix": "control.", "callables": [ @@ -93,7 +89,7 @@ def _import_flow360(name: str) -> Any: "wallShearStress", "yPlus", ], - "evaluate": False + "evaluate": False, }, "flow360.solution": { "prefix": "solution.", @@ -142,8 +138,8 @@ def _import_flow360(name: str) -> Any: "wallShearStress", "yPlus", ], - "evaluate": False - } + "evaluate": False, + }, } # Define allowed modules @@ -162,7 +158,8 @@ def _import_flow360(name: str) -> Any: **{ f"{group['prefix']}{name}": None for group in WHITELISTED_CALLABLES.values() - for name in group["callables"] if not group["evaluate"] + for name in group["callables"] + if not group["evaluate"] }, } @@ -170,4 +167,6 @@ def _import_flow360(name: str) -> Any: ("fl", "u"): _import_flow360, } -resolver = CallableResolver(ALLOWED_CALLABLES, ALLOWED_MODULES, IMPORT_FUNCTIONS, EVALUATION_BLACKLIST) +resolver = CallableResolver( + ALLOWED_CALLABLES, ALLOWED_MODULES, IMPORT_FUNCTIONS, EVALUATION_BLACKLIST +) diff --git a/flow360/component/simulation/blueprint/tidy3d/symbols.py b/flow360/component/simulation/blueprint/tidy3d/symbols.py index 7a0aca1d3..e7e64839c 100644 --- a/flow360/component/simulation/blueprint/tidy3d/symbols.py +++ b/flow360/component/simulation/blueprint/tidy3d/symbols.py @@ -55,23 +55,11 @@ def _import_tidy3d(name: str) -> Any: "ModeSpec", "inf", ], - "evaluate": True - }, - "tidy3d.plugins": { - "prefix": "", - "callables": ["ModeSolver"], - "evaluate": True - }, - "tidy3d.constants": { - "prefix": "", - "callables": ["C_0"], - "evaluate": True - }, - "utilities": { - "prefix": "", - "callables": ["print"], - "evaluate": True + "evaluate": True, }, + "tidy3d.plugins": {"prefix": "", "callables": ["ModeSolver"], "evaluate": True}, + "tidy3d.constants": {"prefix": "", "callables": ["C_0"], "evaluate": True}, + "utilities": {"prefix": "", "callables": ["print"], "evaluate": True}, } # Define allowed modules @@ -94,7 +82,8 @@ def _import_tidy3d(name: str) -> Any: **{ f"{group['prefix']}{name}": None for group in WHITELISTED_CALLABLES.values() - for name in group["callables"] if not group["evaluate"] + for name in group["callables"] + if not group["evaluate"] }, } @@ -104,4 +93,6 @@ def _import_tidy3d(name: str) -> Any: ("print",): _import_utilities, } -resolver = CallableResolver(ALLOWED_CALLABLES, ALLOWED_MODULES, IMPORT_FUNCTIONS, EVALUATION_BLACKLIST) +resolver = CallableResolver( + ALLOWED_CALLABLES, ALLOWED_MODULES, IMPORT_FUNCTIONS, EVALUATION_BLACKLIST +) diff --git a/flow360/component/simulation/blueprint/utils/operators.py b/flow360/component/simulation/blueprint/utils/operators.py index 0ddc1cd7e..b72a7317f 100644 --- a/flow360/component/simulation/blueprint/utils/operators.py +++ b/flow360/component/simulation/blueprint/utils/operators.py @@ -1,6 +1,7 @@ import operator from collections.abc import Callable from typing import Any, Union + from .types import TargetSyntax diff --git a/flow360/component/simulation/blueprint/utils/types.py b/flow360/component/simulation/blueprint/utils/types.py index 05843a198..bbeaf35e7 100644 --- a/flow360/component/simulation/blueprint/utils/types.py +++ b/flow360/component/simulation/blueprint/utils/types.py @@ -2,6 +2,6 @@ class TargetSyntax(Enum): - PYTHON = "python", - CPP = "cpp", - # Possibly other languages in the future if needed... \ No newline at end of file + PYTHON = ("python",) + CPP = ("cpp",) + # Possibly other languages in the future if needed... diff --git a/flow360/component/simulation/framework/base_model.py b/flow360/component/simulation/framework/base_model.py index 82a5b224f..06988b85d 100644 --- a/flow360/component/simulation/framework/base_model.py +++ b/flow360/component/simulation/framework/base_model.py @@ -3,9 +3,9 @@ from __future__ import annotations import copy -import re import hashlib import json +import re from itertools import chain from typing import Any, List, Literal, Set, get_origin @@ -35,7 +35,6 @@ _FUNCTION_SEGMENT = re.compile(r"^function-") - def _preprocess_nested_list(value, required_by, params, exclude, registry_lookup): new_list = [] for i, item in enumerate(value): @@ -342,7 +341,8 @@ def populate_ctx_to_error_messages(cls, values, handler, info) -> Any: for error in raw_errors: new_loc = tuple( - seg for seg in error["loc"] + seg + for seg in error["loc"] if not (isinstance(seg, str) and _FUNCTION_SEGMENT.match(seg)) ) @@ -363,13 +363,10 @@ def populate_ctx_to_error_messages(cls, values, handler, info) -> Any: ) ) - raise pd.ValidationError.from_exception_data( title=cls.__class__.__name__, line_errors=cleaned_errors ) from None - - # Note: to_solver architecture will be reworked in favor of splitting the models between # the user-side and solver-side models (see models.py and models_avl.py for reference # in the design360 repo) diff --git a/flow360/component/simulation/framework/param_utils.py b/flow360/component/simulation/framework/param_utils.py index dc07fe4ee..4ae031209 100644 --- a/flow360/component/simulation/framework/param_utils.py +++ b/flow360/component/simulation/framework/param_utils.py @@ -1,6 +1,6 @@ """pre processing and post processing utilities for simulation parameters.""" -from typing import Optional, Union, List +from typing import List, Optional, Union import pydantic as pd @@ -74,10 +74,10 @@ def find_instances(obj, target_type): elif isinstance(current, (list, tuple, set, frozenset)): stack.extend(current) - elif hasattr(current, '__dict__'): + elif hasattr(current, "__dict__"): stack.extend(vars(current).values()) - elif hasattr(current, '__iter__') and not isinstance(current, (str, bytes)): + elif hasattr(current, "__iter__") and not isinstance(current, (str, bytes)): try: stack.extend(iter(current)) except Exception: diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 7b75569f8..71c543c60 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -7,8 +7,6 @@ import pydantic as pd -# Required for correct global scope initialization - from flow360.component.simulation.exposed_units import supported_units_by_front_end from flow360.component.simulation.framework.multi_constructor_model_base import ( parse_model_dict, @@ -21,6 +19,7 @@ from flow360.component.simulation.models.volume_models import ( # pylint: disable=unused-import BETDisk, ) + # pylint: disable=unused-import from flow360.component.simulation.operating_condition.operating_condition import ( AerospaceCondition, @@ -60,6 +59,9 @@ from flow360.exceptions import Flow360RuntimeError, Flow360TranslationError from flow360.version import __version__ +# Required for correct global scope initialization + + unit_system_map = { "SI": SI_unit_system, "CGS": CGS_unit_system, diff --git a/flow360/component/simulation/user_code.py b/flow360/component/simulation/user_code.py index 50e10c4c9..c7063832a 100644 --- a/flow360/component/simulation/user_code.py +++ b/flow360/component/simulation/user_code.py @@ -1,22 +1,21 @@ from __future__ import annotations -from typing import get_origin, Generic, TypeVar, Optional, Iterable +import re +from numbers import Number +from typing import Generic, Iterable, Optional, TypeVar, get_origin + +import pydantic as pd from pydantic import BeforeValidator from typing_extensions import Self -import re +from unyt import Unit, unyt_array, unyt_quantity +from flow360.component.simulation.blueprint import expr_to_model from flow360.component.simulation.blueprint.codegen import expr_to_code +from flow360.component.simulation.blueprint.core import EvaluationContext from flow360.component.simulation.blueprint.flow360 import resolver from flow360.component.simulation.blueprint.utils.types import TargetSyntax -from flow360.component.simulation.unit_system import * -from flow360.component.simulation.blueprint.core import EvaluationContext from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.blueprint import expr_to_model - -import pydantic as pd -from numbers import Number -from unyt import Unit, unyt_quantity, unyt_array - +from flow360.component.simulation.unit_system import * _global_ctx: EvaluationContext = EvaluationContext(resolver) _user_variables: set[str] = set() @@ -88,7 +87,9 @@ class SerializedValueOrExpression(Flow360BaseModel): value: Optional[Union[Number, Iterable[Number]]] = pd.Field(None) units: Optional[str] = pd.Field(None) expression: Optional[str] = pd.Field(None) - evaluated_value: Optional[Union[Number, Iterable[Number]]] = pd.Field(None, alias="evaluatedValue") + evaluated_value: Optional[Union[Number, Iterable[Number]]] = pd.Field( + None, alias="evaluatedValue" + ) evaluated_units: Optional[str] = pd.Field(None, alias="evaluatedUnits") @@ -96,7 +97,7 @@ class Variable(Flow360BaseModel): name: str = pd.Field() value: Union[list[float], float, unyt_quantity, unyt_array] = pd.Field() - model_config = pd.ConfigDict(validate_assignment=True, extra='allow') + model_config = pd.ConfigDict(validate_assignment=True, extra="allow") def __add__(self, other): (arg, parenthesize) = _convert_argument(other) @@ -199,15 +200,13 @@ class SolverVariable(Variable): @classmethod def update_context(cls, value): _global_ctx.set(value.name, value.value) - _solver_variables[value.name] = value.solver_name if value.solver_name is not None else value.name + _solver_variables[value.name] = ( + value.solver_name if value.solver_name is not None else value.name + ) def _handle_syntax_error(se: SyntaxError, source: str): - caret = ( - " " * (se.offset - 1) + "^" - if se.text and se.offset - else None - ) + caret = " " * (se.offset - 1) + "^" if se.text and se.offset else None msg = f"{se.msg} at line {se.lineno}, column {se.offset}" if caret: msg += f"\n{se.text.rstrip()}\n{caret}" @@ -246,7 +245,9 @@ def _validate_expression(cls, value) -> Self: elif isinstance(value, Variable): expression = str(value) else: - details = InitErrorDetails(type="value_error", ctx={"error": f"Invalid type {type(value)}"}) + details = InitErrorDetails( + type="value_error", ctx={"error": f"Invalid type {type(value)}"} + ) raise pd.ValidationError.from_exception_data("expression type error", [details]) try: @@ -376,7 +377,7 @@ def _internal_validator(value: Expression): try: result = value.evaluate(strict=False) except Exception as err: - raise ValueError(f'expression evaluation failed: {err}') from err + raise ValueError(f"expression evaluation failed: {err}") from err pd.TypeAdapter(internal_type).validate_python(result, strict=True) @@ -431,7 +432,7 @@ def _serializer(value, info) -> dict: serialized.units = str(value.units.expr) return serialized.model_dump(**info.__dict__) - + union_type = Union[expr_type, internal_type] union_type = Annotated[union_type, PlainSerializer(_serializer)] union_type = Annotated[union_type, BeforeValidator(_deserialize)] diff --git a/flow360/component/simulation/utils.py b/flow360/component/simulation/utils.py index 4b6682240..e2bf9ceb2 100644 --- a/flow360/component/simulation/utils.py +++ b/flow360/component/simulation/utils.py @@ -61,7 +61,7 @@ def is_instance_of_type_in_union(obj, typ) -> bool: class UnknownFloat(float): def __new__(cls): - return super().__new__(cls, float('nan')) + return super().__new__(cls, float("nan")) def __repr__(self): return "UnknownFloat()" diff --git a/flow360/component/simulation/variables/control_variables.py b/flow360/component/simulation/variables/control_variables.py index b01f4f231..e9e30c7e3 100644 --- a/flow360/component/simulation/variables/control_variables.py +++ b/flow360/component/simulation/variables/control_variables.py @@ -1,21 +1,48 @@ from flow360.component.simulation.user_code import SolverVariable -MachRef = SolverVariable(name="control.MachRef", value=float("NaN")) # Reference mach specified by the user -Tref = SolverVariable(name="control.Tref", value=float("NaN")) # Temperature specified by the user -t = SolverVariable(name="control.t", value=float("NaN")) # Physical time -physicalStep = SolverVariable(name="control.physicalStep", value=float("NaN")) # Physical time step, starting from 0 -pseudoStep = SolverVariable(name="control.pseudoStep", value=float("NaN")) # Pseudo time step within physical time step -timeStepSize = SolverVariable(name="control.timeStepSize", value=float("NaN")) # Physical time step size -alphaAngle = SolverVariable(name="control.alphaAngle", value=float("NaN")) # Alpha angle specified in freestream -betaAngle = SolverVariable(name="control.betaAngle", value=float("NaN")) # Beta angle specified in freestream -pressureFreestream = SolverVariable(name="control.pressureFreestream", value=float("NaN")) # Freestream reference pressure (1.0/1.4) -momentLengthX = SolverVariable(name="control.momentLengthX", value=float("NaN")) # X component of momentLength -momentLengthY = SolverVariable(name="control.momentLengthY", value=float("NaN")) # Y component of momentLength -momentLengthZ = SolverVariable(name="control.momentLengthZ", value=float("NaN")) # Z component of momentLength -momentCenterX = SolverVariable(name="control.momentCenterX", value=float("NaN")) # X component of momentCenter -momentCenterY = SolverVariable(name="control.momentCenterY", value=float("NaN")) # Y component of momentCenter -momentCenterZ = SolverVariable(name="control.momentCenterZ", value=float("NaN")) # Z component of momentCenter -theta = SolverVariable(name="control.theta", value=float("NaN")) # Rotation angle of volume zone -omega = SolverVariable(name="control.omega", value=float("NaN")) # Rotation speed of volume zone -omegaDot = SolverVariable(name="control.omegaDot", value=float("NaN")) # Rotation acceleration of volume zone - +MachRef = SolverVariable( + name="control.MachRef", value=float("NaN") +) # Reference mach specified by the user +Tref = SolverVariable(name="control.Tref", value=float("NaN")) # Temperature specified by the user +t = SolverVariable(name="control.t", value=float("NaN")) # Physical time +physicalStep = SolverVariable( + name="control.physicalStep", value=float("NaN") +) # Physical time step, starting from 0 +pseudoStep = SolverVariable( + name="control.pseudoStep", value=float("NaN") +) # Pseudo time step within physical time step +timeStepSize = SolverVariable( + name="control.timeStepSize", value=float("NaN") +) # Physical time step size +alphaAngle = SolverVariable( + name="control.alphaAngle", value=float("NaN") +) # Alpha angle specified in freestream +betaAngle = SolverVariable( + name="control.betaAngle", value=float("NaN") +) # Beta angle specified in freestream +pressureFreestream = SolverVariable( + name="control.pressureFreestream", value=float("NaN") +) # Freestream reference pressure (1.0/1.4) +momentLengthX = SolverVariable( + name="control.momentLengthX", value=float("NaN") +) # X component of momentLength +momentLengthY = SolverVariable( + name="control.momentLengthY", value=float("NaN") +) # Y component of momentLength +momentLengthZ = SolverVariable( + name="control.momentLengthZ", value=float("NaN") +) # Z component of momentLength +momentCenterX = SolverVariable( + name="control.momentCenterX", value=float("NaN") +) # X component of momentCenter +momentCenterY = SolverVariable( + name="control.momentCenterY", value=float("NaN") +) # Y component of momentCenter +momentCenterZ = SolverVariable( + name="control.momentCenterZ", value=float("NaN") +) # Z component of momentCenter +theta = SolverVariable(name="control.theta", value=float("NaN")) # Rotation angle of volume zone +omega = SolverVariable(name="control.omega", value=float("NaN")) # Rotation speed of volume zone +omegaDot = SolverVariable( + name="control.omegaDot", value=float("NaN") +) # Rotation acceleration of volume zone diff --git a/flow360/component/simulation/variables/solution_variables.py b/flow360/component/simulation/variables/solution_variables.py index e45cffe29..07237b7c8 100644 --- a/flow360/component/simulation/variables/solution_variables.py +++ b/flow360/component/simulation/variables/solution_variables.py @@ -1,32 +1,61 @@ from flow360.component.simulation.user_code import SolverVariable -mut = SolverVariable(name="solution.mut", value=float("NaN")) # Turbulent viscosity -mu = SolverVariable(name="solution.mu", value=float("NaN")) # Laminar viscosity -solutionNavierStokes = SolverVariable(name="solution.solutionNavierStokes", value=float("NaN")) # Solution for N-S equation in conservative form -residualNavierStokes = SolverVariable(name="solution.residualNavierStokes", value=float("NaN")) # Residual for N-S equation in conservative form -solutionTurbulence = SolverVariable(name="solution.solutionTurbulence", value=float("NaN")) # Solution for turbulence model -residualTurbulence = SolverVariable(name="solution.residualTurbulence", value=float("NaN")) # Residual for turbulence model -kOmega = SolverVariable(name="solution.kOmega", value=float("NaN")) # Effectively solutionTurbulence when using SST model -nuHat = SolverVariable(name="solution.nuHat", value=float("NaN")) # Effectively solutionTurbulence when using SA model -solutionTransition = SolverVariable(name="solution.solutionTransition", value=float("NaN")) # Solution for transition model -residualTransition = SolverVariable(name="solution.residualTransition", value=float("NaN")) # Residual for transition model -solutionHeatSolver = SolverVariable(name="solution.solutionHeatSolver", value=float("NaN")) # Solution for heat equation -residualHeatSolver = SolverVariable(name="solution.residualHeatSolver", value=float("NaN")) # Residual for heat equation -coordinate = SolverVariable(name="solution.coordinate", value=float("NaN")) # Grid coordinates - -bet_thrust = SolverVariable(name="solution.bet_thrust", value=float("NaN")) # Thrust force for BET disk -bet_torque = SolverVariable(name="solution.bet_torque", value=float("NaN")) # Torque for BET disk -bet_omega = SolverVariable(name="solution.bet_omega", value=float("NaN")) # Rotation speed for BET disk -CD = SolverVariable(name="solution.CD", value=float("NaN")) # Drag coefficient on patch -CL = SolverVariable(name="solution.CL", value=float("NaN")) # Lift coefficient on patch -forceX = SolverVariable(name="solution.forceX", value=float("NaN")) # Total force in X direction -forceY = SolverVariable(name="solution.forceY", value=float("NaN")) # Total force in Y direction -forceZ = SolverVariable(name="solution.forceZ", value=float("NaN")) # Total force in Z direction -momentX = SolverVariable(name="solution.momentX", value=float("NaN")) # Total moment in X direction -momentY = SolverVariable(name="solution.momentY", value=float("NaN")) # Total moment in Y direction -momentZ = SolverVariable(name="solution.momentZ", value=float("NaN")) # Total moment in Z direction -nodeNormals = SolverVariable(name="solution.nodeNormals", value=float("NaN")) # Normal vector of patch -wallFunctionMetric = SolverVariable(name="solution.wallFunctionMetric", value=float("NaN")) # Wall model quality indicator -wallShearStress = SolverVariable(name="solution.wallShearStress", value=float("NaN")) # Wall viscous shear stress -yPlus = SolverVariable(name="solution.yPlus", value=float("NaN")) # Non-dimensional wall distance +mut = SolverVariable(name="solution.mut", value=float("NaN")) # Turbulent viscosity +mu = SolverVariable(name="solution.mu", value=float("NaN")) # Laminar viscosity +solutionNavierStokes = SolverVariable( + name="solution.solutionNavierStokes", value=float("NaN") +) # Solution for N-S equation in conservative form +residualNavierStokes = SolverVariable( + name="solution.residualNavierStokes", value=float("NaN") +) # Residual for N-S equation in conservative form +solutionTurbulence = SolverVariable( + name="solution.solutionTurbulence", value=float("NaN") +) # Solution for turbulence model +residualTurbulence = SolverVariable( + name="solution.residualTurbulence", value=float("NaN") +) # Residual for turbulence model +kOmega = SolverVariable( + name="solution.kOmega", value=float("NaN") +) # Effectively solutionTurbulence when using SST model +nuHat = SolverVariable( + name="solution.nuHat", value=float("NaN") +) # Effectively solutionTurbulence when using SA model +solutionTransition = SolverVariable( + name="solution.solutionTransition", value=float("NaN") +) # Solution for transition model +residualTransition = SolverVariable( + name="solution.residualTransition", value=float("NaN") +) # Residual for transition model +solutionHeatSolver = SolverVariable( + name="solution.solutionHeatSolver", value=float("NaN") +) # Solution for heat equation +residualHeatSolver = SolverVariable( + name="solution.residualHeatSolver", value=float("NaN") +) # Residual for heat equation +coordinate = SolverVariable(name="solution.coordinate", value=float("NaN")) # Grid coordinates +bet_thrust = SolverVariable( + name="solution.bet_thrust", value=float("NaN") +) # Thrust force for BET disk +bet_torque = SolverVariable(name="solution.bet_torque", value=float("NaN")) # Torque for BET disk +bet_omega = SolverVariable( + name="solution.bet_omega", value=float("NaN") +) # Rotation speed for BET disk +CD = SolverVariable(name="solution.CD", value=float("NaN")) # Drag coefficient on patch +CL = SolverVariable(name="solution.CL", value=float("NaN")) # Lift coefficient on patch +forceX = SolverVariable(name="solution.forceX", value=float("NaN")) # Total force in X direction +forceY = SolverVariable(name="solution.forceY", value=float("NaN")) # Total force in Y direction +forceZ = SolverVariable(name="solution.forceZ", value=float("NaN")) # Total force in Z direction +momentX = SolverVariable(name="solution.momentX", value=float("NaN")) # Total moment in X direction +momentY = SolverVariable(name="solution.momentY", value=float("NaN")) # Total moment in Y direction +momentZ = SolverVariable(name="solution.momentZ", value=float("NaN")) # Total moment in Z direction +nodeNormals = SolverVariable( + name="solution.nodeNormals", value=float("NaN") +) # Normal vector of patch +wallFunctionMetric = SolverVariable( + name="solution.wallFunctionMetric", value=float("NaN") +) # Wall model quality indicator +wallShearStress = SolverVariable( + name="solution.wallShearStress", value=float("NaN") +) # Wall viscous shear stress +yPlus = SolverVariable(name="solution.yPlus", value=float("NaN")) # Non-dimensional wall distance diff --git a/tests/simulation/test_expressions.py b/tests/simulation/test_expressions.py index 083ec12b6..7835c2fb4 100644 --- a/tests/simulation/test_expressions.py +++ b/tests/simulation/test_expressions.py @@ -2,44 +2,41 @@ from pprint import pprint from typing import List -import pytest - -from flow360.component.simulation.user_code import ( - ValueOrExpression, - UserVariable, - Expression, -) -from flow360.component.simulation.framework.base_model import Flow360BaseModel - import pydantic as pd -from flow360 import u +import pytest import flow360 as fl - +from flow360 import u +from flow360.component.simulation.framework.base_model import Flow360BaseModel from flow360.component.simulation.unit_system import ( - LengthType, - AngleType, - MassType, - TimeType, AbsoluteTemperatureType, - VelocityType, + AngleType, + AngularVelocityType, AreaType, - ForceType, - PressureType, DensityType, - ViscosityType, - PowerType, - MomentType, - AngularVelocityType, + ForceType, + FrequencyType, HeatFluxType, HeatSourceType, - SpecificHeatCapacityType, InverseAreaType, + InverseLengthType, + LengthType, MassFlowRateType, + MassType, + MomentType, + PowerType, + PressureType, SpecificEnergyType, - FrequencyType, + SpecificHeatCapacityType, ThermalConductivityType, - InverseLengthType, + TimeType, + VelocityType, + ViscosityType, +) +from flow360.component.simulation.user_code import ( + Expression, + UserVariable, + ValueOrExpression, ) @@ -368,16 +365,16 @@ class TestModel(Flow360BaseModel): x = UserVariable(name="x", value=4) - model = TestModel(field=x * u.m / u.s + 4 * x ** 2 * u.m / u.s) + model = TestModel(field=x * u.m / u.s + 4 * x**2 * u.m / u.s) - assert str(model.field) == '(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)' + assert str(model.field) == "(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)" serialized = model.model_dump(exclude_none=True) print(model.model_dump_json(indent=2, exclude_none=True)) assert serialized["field"]["type_name"] == "expression" - assert serialized["field"]["expression"] == '(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)' + assert serialized["field"]["expression"] == "(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)" model = TestModel(field=4 * u.m / u.s) @@ -400,22 +397,18 @@ class TestModel(Flow360BaseModel): "type_name": "expression", "expression": "(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)", "evaluated_value": 68.0, - "evaluated_units": "m/s" + "evaluated_units": "m/s", } deserialized = TestModel(field=model) - assert str(deserialized.field) == '(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)' + assert str(deserialized.field) == "(x * u.m) / u.s + (((4 * (x ** 2)) * u.m) / u.s)" - model = { - "type_name": "number", - "value": 4.0, - "units": "m/s" - } + model = {"type_name": "number", "value": 4.0, "units": "m/s"} deserialized = TestModel(field=model) - assert str(deserialized.field) == '4.0 m/s' + assert str(deserialized.field) == "4.0 m/s" def test_error_message(): @@ -428,26 +421,25 @@ class TestModel(Flow360BaseModel): model = TestModel(field="1 + nonexisting * 1") except pd.ValidationError as err: validation_errors = err.errors() - + assert len(validation_errors) >= 1 assert validation_errors[0]["type"] == "value_error" assert "Name 'nonexisting' is not defined" in validation_errors[0]["msg"] - + try: model = TestModel(field="1 + x * 1") except pd.ValidationError as err: validation_errors = err.errors() - + assert len(validation_errors) >= 1 assert validation_errors[0]["type"] == "value_error" assert "does not match (length)/(time) dimension" in validation_errors[0]["msg"] - try: model = TestModel(field="1 * 1 +") except pd.ValidationError as err: validation_errors = err.errors() - + assert len(validation_errors) >= 1 assert validation_errors[0]["type"] == "value_error" assert "invalid syntax" in validation_errors[0]["msg"] @@ -460,7 +452,7 @@ class TestModel(Flow360BaseModel): model = TestModel(field="1 * 1 +* 2") except pd.ValidationError as err: validation_errors = err.errors() - + assert len(validation_errors) >= 1 assert validation_errors[0]["type"] == "value_error" assert "invalid syntax" in validation_errors[0]["msg"] @@ -473,7 +465,7 @@ class TestModel(Flow360BaseModel): model = TestModel(field="1 * 1 + (2") except pd.ValidationError as err: validation_errors = err.errors() - + assert len(validation_errors) >= 1 assert validation_errors[0]["type"] == "value_error" assert "unexpected EOF" in validation_errors[0]["msg"] From 4d1d239e1a1e73af83cc1ce1b7c797149f257c54 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Wed, 30 Apr 2025 19:09:19 +0000 Subject: [PATCH 4/4] [Rebased] [POC] entire workflow passed --- flow360/__init__.py | 46 ++++++++++++ flow360/component/simulation/conversion.py | 15 ++++ .../component/simulation/outputs/outputs.py | 3 +- flow360/component/simulation/services.py | 5 +- .../translator/solver_translator.py | 72 ++++++++++++++----- .../component/simulation/translator/utils.py | 3 +- flow360/component/simulation/unit_system.py | 60 ++++++++++++++++ .../validation/validation_output.py | 4 +- 8 files changed, 186 insertions(+), 22 deletions(-) diff --git a/flow360/__init__.py b/flow360/__init__.py index 789907792..c9c120aa1 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -134,6 +134,52 @@ Transformation, ) from flow360.component.simulation.simulation_params import SimulationParams + +# from flow360.component.simulation.solver_builtins import ( +# CD, +# CL, +# alphaAngle, +# bet_omega, +# bet_thrust, +# bet_torque, +# betaAngle, +# coordinate, +# forceX, +# forceY, +# forceZ, +# kOmega, +# momentCenterX, +# momentCenterY, +# momentCenterZ, +# momentLengthX, +# momentLengthY, +# momentLengthZ, +# momentX, +# momentY, +# momentZ, +# mu, +# mut, +# nodeNormals, +# nuHat, +# omega, +# omegaDot, +# physicalStep, +# pressureFreestream, +# pseudoStep, +# residualHeatSolver, +# residualNavierStokes, +# residualTransition, +# residualTurbulence, +# solutionHeatSolver, +# solutionNavierStokes, +# solutionTransition, +# solutionTurbulence, +# theta, +# timeStepSize, +# wallFunctionMetric, +# wallShearStress, +# yPlus, +# ) from flow360.component.simulation.time_stepping.time_stepping import ( AdaptiveCFL, RampCFL, diff --git a/flow360/component/simulation/conversion.py b/flow360/component/simulation/conversion.py index e61d769aa..3c4c1c597 100644 --- a/flow360/component/simulation/conversion.py +++ b/flow360/component/simulation/conversion.py @@ -342,6 +342,21 @@ def get_base_thermal_conductivity(): base_density * base_length**3 / base_time ) + elif dimension == u.dimensions.mass_flux: + base_density = get_base_density() + base_length = get_base_length() + base_time = get_base_time() + # TODO: Unit test for non-dimensionalize this new unit + flow360_conversion_unit_system.base_mass_flux = base_density * base_length / base_time + + elif dimension == u.dimensions.energy_density: + base_density = get_base_density() + base_velocity = get_base_velocity() + # TODO: Unit test for non-dimensionalize this new unit + flow360_conversion_unit_system.base_energy_density = ( + base_density * base_velocity * base_velocity + ) + elif dimension == u.dimensions.specific_energy: base_velocity = get_base_velocity() diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 558f30daa..71157fe0c 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -37,6 +37,7 @@ Surface, ) from flow360.component.simulation.unit_system import LengthType +from flow360.component.simulation.user_code import SolverVariable from flow360.component.simulation.validation.validation_context import ( ALL, CASE, @@ -260,7 +261,7 @@ class VolumeOutput(_AnimationAndFileFormatSettings): """ name: Optional[str] = pd.Field("Volume output", description="Name of the `VolumeOutput`.") - output_fields: UniqueItemList[Union[VolumeFieldNames, str]] = pd.Field( + output_fields: UniqueItemList[Union[VolumeFieldNames, str, SolverVariable]] = pd.Field( description="List of output variables. Including :ref:`universal output variables`," " :ref:`variables specific to VolumeOutput`" " and :class:`UserDefinedField`." diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 71c543c60..3165b36e0 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -33,6 +33,9 @@ ReferenceGeometry, SimulationParams, ) + +# Required for correct global scope initialization +from flow360.component.simulation.solver_builtins import * from flow360.component.simulation.translator.solver_translator import get_solver_json from flow360.component.simulation.translator.surface_meshing_translator import ( get_surface_meshing_json, @@ -526,7 +529,7 @@ def _translate_simulation_json( translated_dict = translation_func(input_params, mesh_unit) except Flow360TranslationError as err: raise ValueError(str(err)) from err - except Exception as err: # tranlsation itself is not supposed to raise any other exception + except Exception as err: # translation itself is not supposed to raise any other exception raise ValueError( f"Unexpected error translating to {target_name} json: " + str(err) ) from err diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index 2555fb8d4..718c550b1 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -3,6 +3,8 @@ # pylint: disable=too-many-lines from typing import Type, Union +import unyt as u # TEST + from flow360.component.simulation.conversion import LIQUID_IMAGINARY_FREESTREAM_MACH from flow360.component.simulation.framework.entity_base import EntityList from flow360.component.simulation.models.material import Sutherland @@ -80,7 +82,8 @@ translate_setting_and_apply_to_all_entities, update_dict_recursively, ) -from flow360.component.simulation.unit_system import LengthType +from flow360.component.simulation.unit_system import LengthType, unit_system_manager +from flow360.component.simulation.user_code import SolverVariable from flow360.component.simulation.utils import ( is_exact_instance, is_instance_of_type_in_union, @@ -218,6 +221,20 @@ def rotation_translator(model: Rotation): return volume_zone +def _convert_output_field(output_field: Union[SolverVariable, str]): + """Convert output field to string""" + if isinstance(output_field, str): + return output_field + elif isinstance(output_field, SolverVariable): + return output_field._get_udf_name(unit_system=unit_system_manager.current) + else: + raise Flow360TranslationError( + f"Unsupported output field type: {type(output_field)}", + input_value=output_field, + location=["outputs"], + ) + + def translate_output_fields( output_model: Union[ SurfaceOutput, @@ -232,11 +249,15 @@ def translate_output_fields( ], ): """Get output fields""" - return {"outputFields": output_model.output_fields.items} + output_fields = [] + for output_field in output_model.output_fields.items: + output_fields.append(_convert_output_field(output_field)) + + return {"outputFields": output_fields} def surface_probe_setting_translation_func(entity: SurfaceProbeOutput): - """Translate non-entitties part of SurfaceProbeOutput""" + """Translate non-entities part of SurfaceProbeOutput""" dict_with_merged_output_fields = monitor_translator(entity) dict_with_merged_output_fields["surfacePatches"] = [ surface.full_name for surface in entity.target_surfaces.stored_entities @@ -350,11 +371,16 @@ def translate_volume_output( is_average=volume_output_class is TimeAverageVolumeOutput, ) # Get outputFields + output_fields = [] + + for output_field in get_global_setting_from_first_instance( + output_params, volume_output_class, "output_fields" + ).items: + output_fields.append(_convert_output_field(output_field)) + volume_output.update( { - "outputFields": get_global_setting_from_first_instance( - output_params, volume_output_class, "output_fields" - ).model_dump()["items"], + "outputFields": output_fields, } ) return volume_output @@ -514,25 +540,36 @@ def process_output_fields_for_udf(input_params): input_params: SimulationParams object containing outputs configuration Returns: - tuple: (all_field_names, generated_udfs) where: - - all_field_names is a set of all output field names + tuple: (all_fields, generated_udfs) where: + - all_fields is a set of all output field names - generated_udfs is a list of UserDefinedField objects for dimensioned fields """ # Collect all output field names from all output types - all_field_names = set() + all_fields = set() if input_params.outputs: for output in input_params.outputs: if hasattr(output, "output_fields") and output.output_fields: - all_field_names.update(output.output_fields.items) + all_fields.update(output.output_fields.items) # Generate UDFs for dimensioned fields generated_udfs = [] - for field_name in all_field_names: - udf_expression = generate_predefined_udf(field_name, input_params) - if udf_expression: - generated_udfs.append(UserDefinedField(name=field_name, expression=udf_expression)) + for field in all_fields: + if isinstance(field, str): + udf_expression = generate_predefined_udf(field, input_params) + if udf_expression: + generated_udfs.append(UserDefinedField(name=field, expression=udf_expression)) + + elif isinstance(field, SolverVariable): + generated_udfs.append( + UserDefinedField( + name=field._get_udf_name(unit_system=unit_system_manager.current), + expression=field._get_udf_source_code( + unit_system=unit_system_manager.current, params=input_params + ), + ) + ) return generated_udfs @@ -1283,13 +1320,14 @@ def get_solver_json( ) ##:: Step 4: Get outputs (has to be run after the boundaries are translated) - - translated = translate_output(input_params, translated) + with input_params.unit_system: + translated = translate_output(input_params, translated) ##:: Step 5: Get user defined fields and auto-generated fields for dimensioned output translated["userDefinedFields"] = [] # Add auto-generated UDFs for dimensioned fields - generated_udfs = process_output_fields_for_udf(input_params) + with input_params.unit_system: + generated_udfs = process_output_fields_for_udf(input_params) # Add user-specified UDFs and auto-generated UDFs for dimensioned fields for udf in [*input_params.user_defined_fields, *generated_udfs]: diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index 89c557612..79a69b2ce 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -117,9 +117,10 @@ def convert_tuples_to_lists(input_dict): def remove_units_in_dict(input_dict): """Remove units from a dimensioned value.""" unit_keys = {"value", "units"} + const_value_expression_keys = {"value", "units", "typeName"} if isinstance(input_dict, dict): new_dict = {} - if input_dict.keys() == unit_keys: + if input_dict.keys() == unit_keys or input_dict.keys() == const_value_expression_keys: new_dict = input_dict["value"] if input_dict["units"].startswith("flow360_") is False: raise ValueError( diff --git a/flow360/component/simulation/unit_system.py b/flow360/component/simulation/unit_system.py index f7c223504..b421c2fdb 100644 --- a/flow360/component/simulation/unit_system.py +++ b/flow360/component/simulation/unit_system.py @@ -39,6 +39,8 @@ udim.inverse_area = 1 / udim.area udim.inverse_length = 1 / udim.length udim.mass_flow_rate = udim.mass / udim.time +udim.mass_flux = udim.density * udim.velocity +udim.energy_density = udim.energy / udim.volume udim.specific_energy = udim.length**2 * udim.time ** (-2) udim.frequency = udim.time ** (-1) udim.delta_temperature = Symbol("(delta temperature)", positive=True) @@ -1017,6 +1019,29 @@ class _FrequencyType(_DimensionedType): FrequencyType = Annotated[_FrequencyType, PlainSerializer(_dimensioned_type_serializer)] +# Following are dimensioned types specific for output fields (for now) + + +class _MassFluxType(_DimensionedType): + """:class: MassFluxType""" + + dim = udim.mass_flux + dim_name = "mass_flux" + + +MassFluxType = Annotated[_MassFluxType, PlainSerializer(_dimensioned_type_serializer)] + + +class _EnergyDensityType(_DimensionedType): + """:class: EnergyDensityType""" + + dim = udim.energy_density + dim_name = "energy_density" + + +EnergyDensityType = Annotated[_EnergyDensityType, PlainSerializer(_dimensioned_type_serializer)] + + DimensionedTypes = Union[ LengthType, AngleType, @@ -1042,6 +1067,9 @@ class _FrequencyType(_DimensionedType): MassFlowRateType, SpecificEnergyType, FrequencyType, + # Outputs + MassFluxType, + EnergyDensityType, ] @@ -1417,6 +1445,20 @@ class Flow360FrequencyUnit(_Flow360BaseUnit): unit_name = "flow360_frequency_unit" +class Flow360MassFluxUnit(_Flow360BaseUnit): + """:class: Flow360MassFluxUnit""" + + dimension_type = MassFluxType + unit_name = "flow360_mass_flux_unit" + + +class Flow360EnergyDensityUnit(_Flow360BaseUnit): + """:class: Flow360EnergyDensityUnit""" + + dimension_type = EnergyDensityType + unit_name = "flow360_energy_density_unit" + + def is_flow360_unit(value): """ Check if the provided value represents a dimensioned quantity with units @@ -1478,6 +1520,8 @@ class BaseSystemType(Enum): "mass_flow_rate", "specific_energy", "frequency", + "mass_flux", + "energy_density", "delta_temperature", ] @@ -1512,6 +1556,8 @@ class UnitSystem(pd.BaseModel): specific_energy: SpecificEnergyType = pd.Field() frequency: FrequencyType = pd.Field() delta_temperature: DeltaTemperatureType = pd.Field() + mass_flux: MassFluxType = pd.Field() + energy_density: EnergyDensityType = pd.Field() name: Literal["Custom"] = pd.Field("Custom") @@ -1648,6 +1694,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): flow360_specific_energy_unit = Flow360SpecificEnergyUnit() flow360_delta_temperature_unit = Flow360DeltaTemperatureUnit() flow360_frequency_unit = Flow360FrequencyUnit() +flow360_mass_flux_unit = Flow360MassFluxUnit() +flow360_energy_density_unit = Flow360EnergyDensityUnit() dimensions = [ flow360_length_unit, @@ -1675,6 +1723,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): flow360_delta_temperature_unit, flow360_frequency_unit, flow360_heat_source_unit, + flow360_mass_flux_unit, + flow360_energy_density_unit, ] _flow360_system = {u.dimension_type.dim_name: u for u in dimensions} @@ -1737,6 +1787,12 @@ class Flow360ConversionUnitSystem(pd.BaseModel): base_mass_flow_rate: float = pd.Field( np.inf, json_schema_extra={"target_dimension": Flow360MassFlowRateUnit} ) + base_mass_flux: float = pd.Field( + np.inf, json_schema_extra={"target_dimension": Flow360MassFluxUnit} + ) + base_energy_density: float = pd.Field( + np.inf, json_schema_extra={"target_dimension": Flow360EnergyDensityUnit} + ) base_specific_energy: float = pd.Field( np.inf, json_schema_extra={"target_dimension": Flow360SpecificEnergyUnit} ) @@ -1792,6 +1848,8 @@ def __init__(self): conversion_system["inverse_area"] = "flow360_inverse_area_unit" conversion_system["inverse_length"] = "flow360_inverse_length_unit" conversion_system["mass_flow_rate"] = "flow360_mass_flow_rate_unit" + conversion_system["mass_flux"] = "flow360_mass_flux_unit" + conversion_system["energy_density"] = "flow360_energy_density_unit" conversion_system["specific_energy"] = "flow360_specific_energy_unit" conversion_system["delta_temperature"] = "flow360_delta_temperature_unit" conversion_system["frequency"] = "flow360_frequency_unit" @@ -1844,6 +1902,8 @@ class _PredefinedUnitSystem(UnitSystem): specific_energy: SpecificEnergyType = pd.Field(exclude=True) delta_temperature: DeltaTemperatureType = pd.Field(exclude=True) frequency: FrequencyType = pd.Field(exclude=True) + mass_flux: MassFluxType = pd.Field(exclude=True) + energy_density: EnergyDensityType = pd.Field(exclude=True) # pylint: disable=missing-function-docstring def system_repr(self): diff --git a/flow360/component/simulation/validation/validation_output.py b/flow360/component/simulation/validation/validation_output.py index 248606973..9b871c7a4 100644 --- a/flow360/component/simulation/validation/validation_output.py +++ b/flow360/component/simulation/validation/validation_output.py @@ -52,7 +52,7 @@ def extract_literal_values(annotation): allowed_items = natively_supported + additional_fields for item in output.output_fields.items: - if item not in allowed_items: + if item not in allowed_items and isinstance(item, str): raise ValueError( f"In `outputs`[{output_index}] {output.output_type}:, {item} is not a" f" valid output field name. Allowed fields are {allowed_items}." @@ -96,7 +96,7 @@ def _check_output_fields_valid_given_turbulence_model(params): if output.output_type in ("AeroAcousticOutput", "StreamlineOutput"): continue for item in output.output_fields.items: - if item in invalid_output_fields[turbulence_model]: + if isinstance(item, str) and item in invalid_output_fields[turbulence_model]: raise ValueError( f"In `outputs`[{output_index}] {output.output_type}:, {item} is not a valid" f" output field when using turbulence model: {turbulence_model}."