Skip to content

[SCFD-4997][SCFD-4998] Customized Stopping criteria and Monitoring value #1285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
BETDiskSectionalPolar,
BETDiskTwist,
C81File,
Criterion,
DFDCFile,
Fluid,
ForcePerArea,
Expand Down Expand Up @@ -111,6 +112,7 @@
from flow360.component.simulation.outputs.outputs import (
AeroAcousticOutput,
IsosurfaceOutput,
MovingStatistic,
Observer,
ProbeOutput,
SliceOutput,
Expand Down Expand Up @@ -291,4 +293,6 @@
"get_user_variable",
"show_user_variables",
"remove_user_variable",
"Criterion",
"MovingStatistic",
]
34 changes: 33 additions & 1 deletion flow360/component/project_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

import datetime
from typing import List, Literal, Optional
from typing import List, Literal, Optional, get_args

import pydantic as pd

Expand All @@ -12,12 +12,14 @@
from flow360.component.simulation import services
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.models.volume_models import Fluid
from flow360.component.simulation.outputs.output_entities import (
Point,
PointArray,
PointArray2D,
Slice,
)
from flow360.component.simulation.outputs.outputs import MonitorOutputType
from flow360.component.simulation.primitives import Box, Cylinder, GhostSurface
from flow360.component.simulation.simulation_params import SimulationParams
from flow360.component.simulation.unit_system import LengthType
Expand Down Expand Up @@ -254,6 +256,35 @@ def _set_up_default_reference_geometry(params: SimulationParams, length_unit: Le
return params


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:
return params
stopping_criterion = None
for model in params.models:
if not isinstance(model, Fluid):
continue
stopping_criterion = model.stopping_criterion
if not stopping_criterion:
return params
monitor_output_ids = []
if params.outputs is not None:
for output in params.outputs:
if not isinstance(output, get_args(get_args(MonitorOutputType)[0])):
continue
monitor_output_ids.append(output.output_id)
for criterion in stopping_criterion:
monitor_output = criterion.monitor_output
if isinstance(monitor_output, str):
continue
if monitor_output.output_id not in monitor_output_ids:
params.outputs.append(monitor_output)
monitor_output_ids.append(monitor_output.output_id)
return params


def set_up_params_for_uploading(
root_asset,
length_unit: LengthType,
Expand Down Expand Up @@ -295,6 +326,7 @@ def set_up_params_for_uploading(
params = _set_up_default_geometry_accuracy(root_asset, params, use_geometry_AI)

params = _set_up_default_reference_geometry(params, length_unit)
params = _set_up_monitor_output_from_stopping_criterion(params=params)

# Convert all reference of UserVariables to VariableToken
params = save_user_variables(params)
Expand Down
14 changes: 14 additions & 0 deletions flow360/component/simulation/migration/BETDisk.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import os
from typing import Union

from numpy import sqrt
from pydantic import validate_call
Expand All @@ -15,6 +16,17 @@
from flow360.log import log


def _remove_comments(obj: Union[dict, list]) -> Union[dict, list]:
"""
Recursively return a copy of `obj` with all 'comments' entries removed.
"""
if isinstance(obj, dict):
return {key: _remove_comments(value) for key, value in obj.items() if key != "comments"}
if isinstance(obj, list):
return [_remove_comments(item) for item in obj]
return obj


# pylint: disable=too-many-arguments
def _parse_flow360_bet_disk_dict(
*,
Expand Down Expand Up @@ -42,6 +54,8 @@ def _parse_flow360_bet_disk_dict(
if len(flow360_bet_disk_dict["BETDisks"]) == 0:
raise ValueError("Input file does not contain BETDisk setting.")
flow360_bet_disk_dict = flow360_bet_disk_dict["BETDisks"][0]
# Recursively remove "comments" from the flow360_bet_disk_dict
flow360_bet_disk_dict = _remove_comments(flow360_bet_disk_dict)

specific_heat_ratio = 1.4
gas_constant = 287.0529 * u.m**2 / u.s**2 / u.K # pylint: disable=no-member
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@

from pydantic import ValidationInfo

from flow360.component.simulation.validation.validation_context import (
TimeSteppingType,
get_validation_info,
)


def _check_bet_disk_initial_blade_direction_and_blade_line_chord(bet_disk):
if bet_disk.blade_line_chord > 0 and bet_disk.initial_blade_direction is None:
Expand Down Expand Up @@ -57,18 +52,6 @@ def _check_bet_disk_duplicate_twists(value, info: ValidationInfo):
return value


def _check_bet_disk_initial_blade_direction(value, info: ValidationInfo):
validation_info = get_validation_info()
if validation_info is None:
return value

if validation_info.time_stepping == TimeSteppingType.UNSTEADY and value is None:
raise ValueError(
"The initial_blade_direction must be specified if performing an unsteady BET Line simulation."
)
return value


def _check_bet_disk_sectional_radius_and_polars(bet_disk):
radiuses = bet_disk.sectional_radiuses
polars = bet_disk.sectional_polars
Expand Down
168 changes: 158 additions & 10 deletions flow360/component/simulation/models/volume_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
import os
import re
from abc import ABCMeta
from typing import Annotated, Dict, List, Literal, Optional, Union
from typing import Annotated, Dict, List, Literal, Optional, Union, get_args

import pydantic as pd

import flow360.component.simulation.units as u
from flow360.component.simulation.framework.base_model import Flow360BaseModel
from flow360.component.simulation.framework.base_model import (
Flow360BaseModel,
RegistryLookup,
)
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.framework.expressions import (
StringExpression,
Expand Down Expand Up @@ -48,10 +51,16 @@
_check_bet_disk_alphas_in_order,
_check_bet_disk_duplicate_chords,
_check_bet_disk_duplicate_twists,
_check_bet_disk_initial_blade_direction,
_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.outputs import (
MonitorOutputType,
ProbeOutput,
SurfaceIntegralOutput,
SurfaceProbeOutput,
)
from flow360.component.simulation.primitives import Box, Cylinder, GenericVolume
from flow360.component.simulation.unit_system import (
AngleType,
Expand All @@ -63,7 +72,13 @@
PressureType,
u,
)
from flow360.component.simulation.user_code.core.types import ValueOrExpression
from flow360.component.simulation.user_code.core.types import (
UnytQuantity,
UserVariable,
ValueOrExpression,
get_input_value_dimensions,
get_input_value_length,
)
from flow360.component.simulation.validation.validation_context import (
get_validation_info,
)
Expand All @@ -76,6 +91,141 @@
from flow360.component.types import Axis


class Criterion(Flow360BaseModel):
"""

:class:`Criterion` 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.Criterion(
... 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("Criterion", description="Name of this criterion.")
monitor_field: Union[UserVariable, str] = pd.Field(description="The field to be monitored.")
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["Criterion"] = pd.Field("Criterion", frozen=True)

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

@pd.field_serializer("monitor_output")
def serialize_monitor_output(self, v):
"""Serialize only the output_id of the related object."""
if isinstance(v, get_args(get_args(MonitorOutputType)[0])):
return v.output_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:
raise ValueError("The stopping criterion can only be defined on a scalar field.")
return v

@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 steup, 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="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


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

stopping_criterion: Optional[List[Criterion]] = pd.Field(
None, description="The stopping criterion setting of the Fluid solver."
)

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

Expand Down Expand Up @@ -783,12 +937,6 @@ def check_bet_disk_duplicate_twists(cls, value, info: pd.ValidationInfo):
"""validate duplicates in twists in BET disks"""
return _check_bet_disk_duplicate_twists(value, info)

@pd.field_validator("initial_blade_direction", mode="after")
@classmethod
def invalid_growth_rate(cls, value, info: pd.ValidationInfo):
"""Ensure initial_blade_direction is specified in an unsteady simulation"""
return _check_bet_disk_initial_blade_direction(value, info)

@pd.model_validator(mode="after")
@_validator_append_instance_name
def check_bet_disk_sectional_radius_and_polars(self):
Expand Down
Loading
Loading