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
8 changes: 6 additions & 2 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@
PorousMedium,
Rotation,
Solid,
StopCriterion,
XFOILFile,
XROTORFile,
)
Expand Down Expand Up @@ -146,6 +145,10 @@
ImportedSurface,
ReferenceGeometry,
)
from flow360.component.simulation.run_control.run_control import RunControl
from flow360.component.simulation.run_control.stopping_criterion import (
StoppingCriterion,
)
from flow360.component.simulation.simulation_params import SimulationParams
from flow360.component.simulation.time_stepping.time_stepping import (
AdaptiveCFL,
Expand Down Expand Up @@ -309,7 +312,8 @@
"get_user_variable",
"show_user_variables",
"remove_user_variable",
"StopCriterion",
"StoppingCriterion",
"MovingStatistic",
"ImportedSurface",
"RunControl",
]
9 changes: 2 additions & 7 deletions flow360/component/project_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.framework.param_utils import AssetCache
from flow360.component.simulation.models.volume_models import Fluid
from flow360.component.simulation.outputs.output_entities import (
Point,
PointArray,
Expand Down Expand Up @@ -367,13 +366,9 @@ def _set_up_monitor_output_from_stopping_criterion(params: SimulationParams):
"""
Setting up the monitor output in the stopping criterion if not provided in params.outputs.
"""
if not params.models:
if not params.run_control:
return params
stopping_criterion = None
for model in params.models:
if not isinstance(model, Fluid):
continue
stopping_criterion = model.stopping_criterion
stopping_criterion = params.run_control.stopping_criteria
if not stopping_criterion:
return params
monitor_output_ids = []
Expand Down
216 changes: 2 additions & 214 deletions flow360/component/simulation/models/volume_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import re
from abc import ABCMeta
from typing import Annotated, Dict, List, Literal, Optional, Union, get_args
from typing import Annotated, Dict, List, Literal, Optional, Union

import pydantic as pd

Expand Down Expand Up @@ -51,14 +51,6 @@
_check_bet_disk_initial_blade_direction_and_blade_line_chord,
_check_bet_disk_sectional_radius_and_polars,
)
from flow360.component.simulation.outputs.output_entities import Point
from flow360.component.simulation.outputs.output_fields import _FIELD_IS_SCALAR_MAPPING
from flow360.component.simulation.outputs.outputs import (
MonitorOutputType,
ProbeOutput,
SurfaceIntegralOutput,
SurfaceProbeOutput,
)
from flow360.component.simulation.primitives import (
Box,
CustomVolume,
Expand All @@ -76,17 +68,7 @@
PressureType,
u,
)
from flow360.component.simulation.user_code.core.types import (
SolverVariable,
UnytQuantity,
UserVariable,
ValueOrExpression,
get_input_value_dimensions,
get_input_value_length,
infer_units_by_unit_system,
is_variable_with_unit_system_as_units,
solver_variable_to_user_variable,
)
from flow360.component.simulation.user_code.core.types import ValueOrExpression
from flow360.component.simulation.validation.validation_context import (
get_validation_info,
)
Expand All @@ -99,194 +81,6 @@
from flow360.component.types import Axis


class StopCriterion(Flow360BaseModel):
"""

:class:`StopCriterion` class for :py:attr:`Fluid.stopping_criterion` settings.

Example
-------

Define a stopping criterion on a :class:`ProbeOutput` with a tolerance of 0.01.
The ProbeOutput monitors the moving deviation of Helicity in a moving window of 10 steps,
at the location of (0, 0, 0,005) * fl.u.m.

>>> monitored_variable = fl.UserVariable(
... name="Helicity_user",
... value=fl.math.dot(fl.solution.velocity, fl.solution.vorticity),
... )
>>> criterion = fl.StopCriterion(
... name="Criterion_1",
... monitor_output=fl.ProbeOutput(
... name="Helicity_probe",
... output_fields=[
... monitored_variable,
... ],
... probe_points=fl.Point(name="Point1", location=(0, 0, 0.005) * fl.u.m),
... moving_statistic = fl.MovingStatistic(method = "deviation", moving_window = 10)
... ),
... monitor_field=monitored_variable,
... tolerance=0.01,
... )

====
"""

name: Optional[str] = pd.Field("StopCriterion", description="Name of this criterion.")
monitor_field: Union[UserVariable, str] = pd.Field(
description="The field to be monitored. This field must be "
"present in the `output_fields` of `monitor_output`."
)
monitor_output: Union[MonitorOutputType, str] = pd.Field(
description="The output to be monitored."
)
tolerance: ValueOrExpression[Union[UnytQuantity, float]] = pd.Field(
description="The tolerance threshold of this criterion."
)
criterion_change_window: Optional[int] = pd.Field(
None,
description="The number of data points used to check if the deviation of "
"monitored field is below tolerance. "
"If not set, the criterion will directly compare the latest value with tolerance.",
ge=2,
)
type_name: Literal["StopCriterion"] = pd.Field("StopCriterion", frozen=True)

def preprocess(
self,
*,
params=None,
exclude: List[str] = None,
required_by: List[str] = None,
flow360_unit_system=None,
) -> Flow360BaseModel:
exclude_criterion = exclude + ["tolerance"]
return super().preprocess(
params=params,
exclude=exclude_criterion,
required_by=required_by,
flow360_unit_system=flow360_unit_system,
)

@pd.field_serializer("monitor_output")
def serialize_monitor_output(self, v):
"""Serialize only the output's id of the related object."""
if isinstance(v, get_args(get_args(MonitorOutputType)[0])):
return v.private_attribute_id
return v

@pd.field_validator("monitor_field", mode="after")
@classmethod
def _check_monitor_field_is_scalar(cls, v):
if (isinstance(v, UserVariable) and get_input_value_length(v.value) != 0) or (
isinstance(v, str) and v in _FIELD_IS_SCALAR_MAPPING and not _FIELD_IS_SCALAR_MAPPING[v]
):
raise ValueError("The stopping criterion can only be defined on a scalar field.")
return v

@pd.field_validator("monitor_output", mode="before")
@classmethod
def _preprocess_monitor_output_with_id(cls, v):
if not isinstance(v, str):
return v
validation_info = get_validation_info()
if (
validation_info is None
or validation_info.output_dict is None
or validation_info.output_dict.get(v) is None
):
raise ValueError("The monitor output does not exist in the outputs list.")
monitor_output_dict = validation_info.output_dict[v]
monitor_output = pd.TypeAdapter(MonitorOutputType).validate_python(monitor_output_dict)
return monitor_output

@pd.field_validator("monitor_output", mode="after")
@classmethod
def _check_single_point_in_probe_output(cls, v):
if not isinstance(v, (ProbeOutput, SurfaceProbeOutput)):
return v
if len(v.entities.stored_entities) == 1 and isinstance(
v.entities.stored_entities[0], Point
):
return v
raise ValueError(
"For stopping criterion setup, only one single `Point` entity is allowed "
"in `ProbeOutput`/`SurfaceProbeOutput`."
)

@pd.field_validator("monitor_output", mode="after")
@classmethod
def _check_field_exists_in_monitor_output(cls, v, info: pd.ValidationInfo):
"""Ensure the monitor field exist in the monitor output."""
if isinstance(v, str):
return v
monitor_field = info.data.get("monitor_field", None)
if monitor_field not in v.output_fields.items:
raise ValueError("The monitor field does not exist in the monitor output.")
return v

@pd.field_validator("tolerance", mode="before")
@classmethod
def _preprocess_field_with_unit_system(cls, value, info: pd.ValidationInfo):
if is_variable_with_unit_system_as_units(value):
return value
if info.data.get("monitor_field") is None:
# `field` validation failed.
raise ValueError(
"The monitor field is invalid and therefore unit inference is not possible."
)
if info.data.get("monitor_output") is None:
raise ValueError(
"The monitor output is invalid and therefore unit inference is not possible."
)
units = value["units"]
monitor_field = info.data["monitor_field"]
monitor_output = info.data.get("monitor_output")
field_dimensions = get_input_value_dimensions(value=monitor_field.value)
if isinstance(monitor_output, SurfaceIntegralOutput):
field_dimensions = field_dimensions * u.dimensions.length**2
value = infer_units_by_unit_system(
value=value, value_dimensions=field_dimensions, unit_system=units
)
return value

@pd.field_validator("tolerance", mode="after")
@classmethod
def check_tolerance_value_for_string_monitor_field(cls, v, info: pd.ValidationInfo):
"""Ensure the tolerance is float when string field is used."""

monitor_field = info.data.get("monitor_field", None)
if isinstance(monitor_field, str) and not isinstance(v, float):
raise ValueError(
f"The monitor field ({monitor_field}) specified by string "
"can only be used with a nondimensional tolerance."
)
return v

@pd.field_validator("tolerance", mode="after")
@classmethod
def _check_tolerance_and_monitor_field_match_dimensions(cls, v, info: pd.ValidationInfo):
"""Ensure the tolerance has the same dimensions as the monitor field."""
monitor_field = info.data.get("monitor_field", None)
monitor_output = info.data.get("monitor_output", None)
if not isinstance(monitor_field, UserVariable):
return v
field_dimensions = get_input_value_dimensions(value=monitor_field.value)
if isinstance(monitor_output, SurfaceIntegralOutput):
field_dimensions = field_dimensions * u.dimensions.length**2
tolerance_dimensions = get_input_value_dimensions(value=v)
if tolerance_dimensions != field_dimensions:
raise ValueError("The dimensions of monitor field and tolerance do not match.")
return v

@pd.field_validator("monitor_field", mode="before")
@classmethod
def _convert_solver_variable_as_user_variable(cls, value):
if isinstance(value, SolverVariable):
return solver_variable_to_user_variable(value)
return value


class AngleExpression(SingleAttributeModel):
"""
:class:`AngleExpression` class for define the angle expression for :py:attr:`Rotation.spec`.
Expand Down Expand Up @@ -519,12 +313,6 @@ class Fluid(PDEModelBase):
)
)

stopping_criterion: Optional[List[StopCriterion]] = pd.Field(
None,
description="The stopping criterion setting of the Fluid solver. "
"All criteria must be met at the same time to stop the solver.",
)

# pylint: disable=fixme
# fixme: Add support for other initial conditions

Expand Down
18 changes: 9 additions & 9 deletions flow360/component/simulation/outputs/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,30 +125,30 @@ class MovingStatistic(Flow360BaseModel):
10 steps, with the initial 100 steps skipped.

>>> fl.MovingStatistic(
... moving_window=10,
... moving_window_size=10,
... method="std",
... initial_skipping_steps=100,
... start_step=100,
... )

====
"""

moving_window: pd.PositiveInt = pd.Field(
moving_window_size: pd.PositiveInt = pd.Field(
10,
description="The number of pseudo/time steps to compute moving statistics. "
"For steady simulation, the moving_window should be a multiple of 10.",
description="The number of pseudo/physical steps to compute moving statistics. "
"For steady simulation, the moving_window_size should be a multiple of 10.",
)
method: Literal["mean", "min", "max", "std", "deviation"] = pd.Field(
"mean", description="The type of moving statistics used to monitor the output."
)
initial_skipping_steps: pd.NonNegativeInt = pd.Field(
start_step: pd.NonNegativeInt = pd.Field(
0,
description="The number of steps to skip before computing the moving statistics. "
"For steady simulation, the moving_window should be a multiple of 10.",
description="The number of pseudo/physical steps to skip before computing the moving statistics. "
"For steady simulation, the moving_window_size should be a multiple of 10.",
)
type_name: Literal["MovingStatistic"] = pd.Field("MovingStatistic", frozen=True)

@pd.field_validator("moving_window", "initial_skipping_steps", mode="after")
@pd.field_validator("moving_window_size", "start_step", mode="after")
@classmethod
def _check_moving_window_for_steady_simulation(cls, value):
validation_info = get_validation_info()
Expand Down
33 changes: 33 additions & 0 deletions flow360/component/simulation/run_control/run_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Module for the run control settings of simulation."""

from typing import List, Literal, Optional

import pydantic as pd

from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.run_control.stopping_criterion import (
StoppingCriterion,
)


class RunControl(Flow360BaseModel):
"""
:class:`RunControl` class for run control settings.

Example
-------

>>> criterion = fl.StoppingCriterion(...)
>>> fl.RunControl(
... stopping_criteria = [criterion],
... )

====
"""

stopping_criteria: Optional[List[StoppingCriterion]] = pd.Field(
None,
description="A list of :class:`StoppingCriterion` for the solver. "
"All criteria must be met at the same time to stop the solver.",
)
type_name: Literal["RunControl"] = pd.Field("RunControl", frozen=True)
Loading
Loading