Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flow360/component/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,8 @@ def _run(
tags=all_tags,
).submit()

params.pre_submit_summary()

draft.update_simulation_params(params)

try:
Expand Down
2 changes: 1 addition & 1 deletion flow360/component/simulation/outputs/output_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
ValueOrExpression,
get_input_value_dimensions,
get_input_value_length,
is_runtime_expression,
solver_variable_to_user_variable,
)
from flow360.component.simulation.user_code.core.utils import is_runtime_expression
from flow360.component.types import Axis


Expand Down
55 changes: 55 additions & 0 deletions flow360/component/simulation/simulation_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
unit_system_manager,
unyt_quantity,
)
from flow360.component.simulation.user_code.core.types import (
batch_get_user_variable_units,
get_post_processing_variables,
)
from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import (
UserDefinedDynamic,
)
Expand Down Expand Up @@ -101,6 +105,7 @@
use_unit_system_for_simulation_msg,
)
from flow360.exceptions import Flow360ConfigurationError, Flow360RuntimeError
from flow360.log import log
from flow360.version import __version__

from .validation.validation_context import (
Expand Down Expand Up @@ -713,3 +718,53 @@ def has_user_defined_dynamics(self):
returns True when SimulationParams has user defined dynamics
"""
return self.user_defined_dynamics is not None and len(self.user_defined_dynamics) > 0

def display_output_units(self) -> None:
"""
Display all the output units for UserVariables used in `outputs`.
"""
if not self.outputs:
return

post_processing_variables = get_post_processing_variables(self)

# Sort for consistent behavior
post_processing_variables = sorted(post_processing_variables)
name_units_pair = batch_get_user_variable_units(post_processing_variables, self)

if not name_units_pair:
return

# Calculate column widths dynamically
name_column_width = max(len("Variable Name"), max(len(name) for name in name_units_pair))
unit_column_width = max(
len("Unit"), max(len(str(unit)) for unit in name_units_pair.values())
)

# Ensure minimum column widths
name_column_width = max(name_column_width, 15)
unit_column_width = max(unit_column_width, 10)

# Create the table header
header = f"{'Variable Name':<{name_column_width}} | {'Unit':<{unit_column_width}}"
separator = "-" * len(header)

# Print the table
log.info("")
log.info("Units of output `UserVariables`:")
log.info(separator)
log.info(header)
log.info(separator)

# Print each row
for name, unit in name_units_pair.items():
log.info(f"{name:<{name_column_width}} | {str(unit):<{unit_column_width}}")

log.info(separator)
log.info("")

def pre_submit_summary(self):
"""
Display a summary of the simulation params before submission.
"""
self.display_output_units()
119 changes: 68 additions & 51 deletions flow360/component/simulation/user_code/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from flow360.component.simulation.user_code.core.utils import (
handle_syntax_error,
is_number_string,
is_runtime_expression,
split_keep_delimiters,
)

Expand All @@ -54,30 +55,6 @@ class VariableContextInfo(Flow360BaseModel):
model_config = pd.ConfigDict(extra="allow")


def save_user_variables(params):
"""
Save user variables to the project variables.
Declared here since I do not want to import default_context everywhere.
"""
# Get all output variables:
post_processing_variables = set()
for item in params.outputs if params.outputs else []:
if not "output_fields" in item.__class__.model_fields:
continue
for item in item.output_fields.items:
if isinstance(item, UserVariable):
post_processing_variables.add(item.name)

params.private_attribute_asset_cache.project_variables = [
VariableContextInfo(
name=name, value=value, postProcessing=name in post_processing_variables
)
for name, value in default_context._values.items() # pylint: disable=protected-access
if "." not in name # Skipping scoped variables (non-user variables)
]
return params


def update_global_context(value: List[VariableContextInfo]):
"""Once the project variables are validated, update the global context."""

Expand Down Expand Up @@ -569,6 +546,22 @@ def in_units(
return new_variable


def get_input_value_length(
value: Union[Number, list[float], unyt_array, unyt_quantity, Expression, Variable],
):
"""Get the length of the input value."""
if isinstance(value, Expression):
value = value.evaluate(raise_on_non_evaluable=False, force_evaluate=True)
assert isinstance(
value, (unyt_array, unyt_quantity, list, Number, np.ndarray)
), f"Unexpected evaluated result type: {type(value)}"
if isinstance(value, list):
return len(value)
if isinstance(value, np.ndarray):
return 0 if value.shape == () else value.shape[0]
return 0 if isinstance(value, (unyt_quantity, Number)) else value.shape[0]


class Expression(Flow360BaseModel, Evaluable):
"""
A symbolic, validated representation of a mathematical expression.
Expand Down Expand Up @@ -1124,17 +1117,57 @@ def _discriminator(v: Any) -> str:
return union_type


def is_runtime_expression(value):
"""Check if the input value is a runtime expression."""
if isinstance(value, unyt_quantity) and np.isnan(value.value):
return True
if isinstance(value, unyt_array) and np.isnan(value.value).any():
return True
if isinstance(value, Number) and np.isnan(value):
return True
if isinstance(value, list) and any(np.isnan(item) for item in value):
return any(np.isnan(item) for item in value)
return False
def get_post_processing_variables(params) -> set[str]:
"""
Get all the post processing related variables from the simulation params.
"""
post_processing_variables = set()
for item in params.outputs if params.outputs else []:
if item.output_type in ("IsosurfaceOutput", "TimeAverageIsosurfaceOutput"):
for isosurface in item.entities.items:
post_processing_variables.add(isosurface.field.name)
if not "output_fields" in item.__class__.model_fields:
continue
for item in item.output_fields.items:
if isinstance(item, UserVariable):
post_processing_variables.add(item.name)
return post_processing_variables


def save_user_variables(params):
"""
Save user variables to the project variables.
Declared here since I do not want to import default_context everywhere.
"""
# Get all output variables which will be tagged with postProcessing=True:
post_processing_variables = get_post_processing_variables(params)

params.private_attribute_asset_cache.project_variables = [
VariableContextInfo(
name=name, value=value, postProcessing=name in post_processing_variables
)
for name, value in default_context._values.items() # pylint: disable=protected-access
if "." not in name # Skipping scoped variables (non-user variables)
]
return params


def batch_get_user_variable_units(variable_names: list[str], params):
"""
Get the units of a list of user variables.
"""
result = {}
for name in variable_names:
value = default_context.get(name)
if isinstance(value, Expression):
result[name] = value.get_output_units(params)
elif isinstance(value, unyt_array):
result[name] = value.units
elif isinstance(value, Number):
result[name] = "dimensionless"
else:
raise ValueError(f"Unknown variable type: {value}")
return result


def get_input_value_dimensions(
Expand All @@ -1158,22 +1191,6 @@ def get_input_value_dimensions(
)


def get_input_value_length(
value: Union[Number, list[float], unyt_array, unyt_quantity, Expression, Variable],
):
"""Get the length of the input value."""
if isinstance(value, Expression):
value = value.evaluate(raise_on_non_evaluable=False, force_evaluate=True)
assert isinstance(
value, (unyt_array, unyt_quantity, list, Number, np.ndarray)
), f"Unexpected evaluated result type: {type(value)}"
if isinstance(value, list):
return len(value)
if isinstance(value, np.ndarray):
return 0 if value.shape == () else value.shape[0]
return 0 if isinstance(value, (unyt_quantity, Number)) else value.shape[0]


def solver_variable_to_user_variable(item):
"""Convert the solver variable to a user variable using the current unit system."""
if isinstance(item, SolverVariable):
Expand Down
16 changes: 16 additions & 0 deletions flow360/component/simulation/user_code/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Utility functions for the user code module"""

import re
from numbers import Number

import numpy as np
import pydantic as pd
from pydantic_core import InitErrorDetails
from unyt import unyt_array, unyt_quantity


def is_number_string(s: str) -> bool:
Expand Down Expand Up @@ -45,3 +48,16 @@ def handle_syntax_error(se: SyntaxError, source: str):
)
],
)


def is_runtime_expression(value):
"""Check if the input value is a runtime expression."""
if isinstance(value, unyt_quantity) and np.isnan(value.value):
return True
if isinstance(value, unyt_array) and np.isnan(value.value).any():
return True
if isinstance(value, Number) and np.isnan(value):
return True
if isinstance(value, list) and any(np.isnan(item) for item in value):
return any(np.isnan(item) for item in value)
return False
3 changes: 2 additions & 1 deletion tests/simulation/test_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
math,
u,
)
from flow360.component.project_utils import save_user_variables
from flow360.component.simulation.blueprint.core.dependency_graph import DependencyGraph
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.param_utils import AssetCache
Expand Down Expand Up @@ -74,6 +73,7 @@
SolverVariable,
UserVariable,
ValueOrExpression,
save_user_variables,
)
from flow360.component.simulation.user_code.variables import control, solution
from tests.utils import to_file_from_file_test
Expand Down Expand Up @@ -868,6 +868,7 @@ def test_to_file_from_file_expression(
)

to_file_from_file_test(params)
params.display_output_units() # Just to make sure not exception.


def assert_ignore_space(expected: str, actual: str):
Expand Down