Skip to content
Closed
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/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
KOmegaSST,
KOmegaSSTModelConstants,
LinearSolver,
LineSearch,
NavierStokesSolver,
NoneSolver,
SpalartAllmaras,
Expand Down Expand Up @@ -302,6 +303,7 @@
"SpalartAllmarasModelConstants",
"DetachedEddySimulation",
"KOmegaSSTModelConstants",
"LineSearch",
"LinearSolver",
"Folder",
"ForcePerArea",
Expand Down
78 changes: 78 additions & 0 deletions flow360/component/simulation/models/solver_numerics.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,41 @@
)
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.primitives import Box, CustomVolume, GenericVolume
from flow360.log import log

# from .time_stepping import UnsteadyTimeStepping

HEAT_EQUATION_EVAL_MAX_PER_PSEUDOSTEP_UNSTEADY = 40
HEAT_EQUATION_EVALUATION_FREQUENCY_STEADY = 10


class LineSearch(Flow360BaseModel):
""":class:`LineSearch` class for configuring line search parameters used with
the Krylov (GMRES) solver.

Example
-------
>>> fl.LineSearch(
... residual_growth_threshold=0.85,
... max_residual_growth=1.1,
... activation_step=100,
... )
"""

residual_growth_threshold: float = pd.Field(
0.85,
description="Pseudo-step convergence ratio above which no residual increase (RHS > 1.0) is allowed.",
)
max_residual_growth: float = pd.Field(
1.1,
description="Hard cap on RHS ratio — never allow residual to grow beyond this factor.",
)
activation_step: PositiveInt = pd.Field(
100,
description="Pseudo step threshold before the max_residual_growth limit is activated.",
)


class LinearSolver(Flow360BaseModel):
""":class:`LinearSolver` class for setting up the linear solver.

Expand All @@ -54,6 +82,15 @@ class LinearSolver(Flow360BaseModel):
description="The linear solver converges when the ratio of the final residual and the initial "
+ "residual of the pseudo step is below this value.",
)
max_preconditioner_iterations: Optional[PositiveInt] = pd.Field(
None,
description="Number of Jacobi preconditioner sweeps when using the Krylov solver. "
+ "When set, max_iterations is interpreted as the Krylov subspace size.",
)
krylov_relative_tolerance: Optional[PositiveFloat] = pd.Field(
None,
description="Relative tolerance for the Krylov (GMRES) linear solver convergence.",
)

model_config = pd.ConfigDict(
conflicting_fields=[Conflicts(field1="absolute_tolerance", field2="relative_tolerance")]
Expand Down Expand Up @@ -143,6 +180,17 @@ class NavierStokesSolver(GenericSolverSettings):
+ "Mach number.",
)

use_krylov_solver: bool = pd.Field(
False,
description="Enable the Krylov (GMRES) solver for the Navier-Stokes equations. "
+ "When enabled, appropriate defaults are set for the linear solver and line search.",
)
line_search: Optional[LineSearch] = pd.Field(
None,
description="Line search parameters for the Newton-Krylov solver. "
+ "Automatically created with defaults when use_krylov_solver is True.",
)

update_jacobian_frequency: PositiveInt = pd.Field(
4, description="Frequency at which the jacobian is updated."
)
Expand All @@ -152,6 +200,36 @@ class NavierStokesSolver(GenericSolverSettings):
+ "updated every pseudo step.",
)

@pd.model_validator(mode="after")
def _populate_krylov_defaults(self) -> Self:
"""When use_krylov_solver is True, populate sensible defaults for the Krylov solver."""
if not self.use_krylov_solver:
if self.linear_solver.max_preconditioner_iterations is not None: # pylint: disable=no-member
raise ValueError(
"max_preconditioner_iterations can only be set when use_krylov_solver=True."
)
if self.linear_solver.krylov_relative_tolerance is not None: # pylint: disable=no-member
log.warning(
"krylov_relative_tolerance is set but use_krylov_solver is False. "
"This value will be ignored."
)
if self.line_search is not None:
log.warning(
"line_search is set but use_krylov_solver is False. "
"This value will be ignored."
)
return self
ls = self.linear_solver
if ls.max_preconditioner_iterations is None: # pylint: disable=no-member
ls.max_preconditioner_iterations = 25
if ls.krylov_relative_tolerance is None: # pylint: disable=no-member
ls.krylov_relative_tolerance = 0.05
if "max_iterations" not in ls.model_fields_set: # pylint: disable=no-member
ls.max_iterations = 15
if self.line_search is None:
self.line_search = LineSearch()
return self


class SpalartAllmarasModelConstants(Flow360BaseModel):
"""
Expand Down
6 changes: 6 additions & 0 deletions flow360/component/simulation/simulation_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
_check_duplicate_isosurface_names,
_check_duplicate_surface_usage,
_check_hybrid_model_to_use_zonal_enforcement,
_check_krylov_solver_restrictions,
_check_low_mach_preconditioner_output,
_check_numerical_dissipation_factor_output,
_check_parent_volume_is_rotating,
Expand Down Expand Up @@ -611,6 +612,11 @@ def check_tpg_not_with_isentropic_solver(self):
"""
return _check_tpg_not_with_isentropic_solver(self)

@pd.model_validator(mode="after")
def check_krylov_solver_restrictions(self):
"""Krylov solver is not compatible with limiters or unsteady time stepping."""
return _check_krylov_solver_restrictions(self)

@contextual_model_validator(mode="after")
@context_validator(context=CASE)
def check_complete_boundary_condition_and_unknown_surface(
Expand Down
14 changes: 14 additions & 0 deletions flow360/component/simulation/translator/solver_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2242,6 +2242,20 @@ def get_solver_json(
)
translated["navierStokesSolver"] = dump_dict(model.navier_stokes_solver)

ns_dict = translated["navierStokesSolver"]
ls_dict = ns_dict.setdefault("linearSolver", {})
if not model.navier_stokes_solver.use_krylov_solver:
ls_dict.pop("maxPreconditionerIterations", None)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translator doesn't strip Krylov fields when solver disabled

Medium Severity

When use_krylov_solver is False, the translator only pops maxPreconditionerIterations from ls_dict but doesn't strip krylovRelativeTolerance from ls_dict or lineSearch from ns_dict. The model validator in _populate_krylov_defaults warns that these values "will be ignored" (without raising an error), so a user can set them and they survive through dump_dict into the translated output — contradicting the warning and potentially causing unexpected solver behavior.

Additional Locations (1)

Fix in Cursor Fix in Web

elif "maxPreconditionerIterations" not in ls_dict:
ls_dict.setdefault("maxPreconditionerIterations", 25)
ls_dict.setdefault("krylovRelativeTolerance", 0.05)
if "lineSearch" not in ns_dict:
ns_dict["lineSearch"] = {
"residualGrowthThreshold": 0.85,
"maxResidualGrowth": 1.1,
"activationStep": 100,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated Krylov defaults in validator and translator

Low Severity

The elif branch in the translator duplicates the same default values (25, 0.05, 0.85, 1.1, 100) already set by the _populate_krylov_defaults model validator. Since the validator always runs and populates these fields when use_krylov_solver=True, the elif condition ("maxPreconditionerIterations" not in ls_dict) is effectively unreachable — making this dead code with a maintenance risk of the two sets of defaults diverging.

Additional Locations (1)

Fix in Cursor Fix in Web


replace_dict_key(translated["navierStokesSolver"], "typeName", "modelType")
if isinstance(op, LiquidOperatingCondition) and not (
model.navier_stokes_solver.private_attribute_dict is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,43 @@ def _material_has_temperature_dependent_gas(material):
return False


def _check_krylov_solver_restrictions(params):
"""Validate that the Krylov (GMRES) solver is not used with incompatible settings."""
models = params.models
if not models:
return params

for model in models:
if not isinstance(model, Fluid):
continue
ns = model.navier_stokes_solver
if not ns.use_krylov_solver:
continue

if ns.limit_velocity:
raise ValueError(
"The Krylov solver (use_krylov_solver=True) is not compatible with "
"limit_velocity=True. Please disable the velocity limiter when using "
"the Krylov solver."
)
if ns.limit_pressure_density:
raise ValueError(
"The Krylov solver (use_krylov_solver=True) is not compatible with "
"limit_pressure_density=True. Please disable the pressure-density limiter "
"when using the Krylov solver."
)

if params.time_stepping is not None and isinstance(params.time_stepping, Unsteady):
for model in models:
if isinstance(model, Fluid) and model.navier_stokes_solver.use_krylov_solver:
raise ValueError(
"The Krylov solver (use_krylov_solver=True) is not supported with "
"Unsteady time stepping. Please use Steady time stepping."
)

return params


def _check_tpg_not_with_isentropic_solver(params):
"""
Validate that temperature-dependent ThermallyPerfectGas is not used with CompressibleIsentropic solver.
Expand Down
Loading