dragonfly_sim.core.time_integration
Time integration: pseudo-transient continuation (PTC) for steady solves and BDF1/BDF2 for unsteady ones, on one residual.
- R*(U) = R(U) + inv_dt (a0 U + a1 U^n + a2 U^{n-1}) physical BDF term
(w_K / CFL) (U - U_old) pseudo-time term
R is the model’s steady residual (model.F, unchanged, which is what the adjoint differentiates). w_K = lambda_K / h_K (+ the model’s time_scale_terms, e.g. viscous 2 nu / h^2) is the cell’s inverse local time step at CFL 1, evaluated at U_old. Both terms are lumped DG0 mass terms and both live only in TimeTerm.augment(), so they never enter model.F.
Modes (all one step loop, TimeIntegrator._newton_step):
- steady PTC physical term off (inv_dt = 0); one Newton step per pseudo
step, U_old <- U before each, the CFL law (CFLController) adapts the step from the steady |R|. At the start of a pseudo step U = U_old, so the residual assembled there is the steady one.
- unsteady physical term on, pseudo term off (1/CFL = 0): Newton per
physical step to the residual criterion; Delta t ramp and halving on failure (StepSizeController).
- dual time physical term on, and the pseudo term on inside every
physical step, with the CFL law restarted each step: PTC on R* instead of R.
Switching a term off sets its Constant to exactly zero, so the compiled forms stay valid across modes; a model with neither term enabled never builds them at all (the plain Newton solve is unchanged).
Attributes
Classes
Switched-evolution-relaxation CFL law with the safeguards the p = 0 RANS |
|
Physical time step: ramp from dt_start by growth per step up to dt, |
|
Drives a CompressibleEulerModel (or a model hosted on one) through |
|
The physical and pseudo-time terms of R* as UFL forms over Constants |
Functions
|
(a0, a1, a2) for (a0 U^{n+1} + a1 U^n + a2 U^{n-1}) / dt_n. Variable-step |
Module Contents
- class dragonfly_sim.core.time_integration.CFLController
Switched-evolution-relaxation CFL law with the safeguards the p = 0 RANS runs needed. Pure Python: update() takes the step’s outcome and returns the decision, so the law is unit-tested without a solver.
After a step from residual r_prev to r, with a converged linear solve (ksp_ok) and positivity step length theta:
- reject r non-finite or r > reject_factor r_prev: undo the step,
CFL *= cut (stop when CFL < 1e-6 cfl0)
- limited theta < theta_cut: CFL *= cut, down to cfl_min_limited.
Without it a limited step leaves |R| flat, the band below holds CFL, and the limiter goes on to zero every step (p=0 MUSCL from freestream froze at M 0.9 and 0.95; one leading-edge cell at p = 1e-4 blocked the step at CFL 1 but not at CFL 0.1, hence a floor below cfl_min).
no Krylov an inexact direction never grows CFL progress r < (1 - 1e-3) r_prev: grow by max(r_prev/r, min_growth),
capped at max_growth
- rise r > 1.05 r_prev: CFL *= r_prev/r, clipped to
[rise_factor_min, rise_factor_max]; the defaults, 0.1 and 1/max_growth = 0.5, cut it by at least half
- band otherwise (|R| flat within -0.1%..+5%): grow by
band_growth on a clean step (theta = 1), else hold. Holding kept the STW L3 wing at CFL 3.15 for 118 of 152 steps.
CFL stays within [floor, cfl_max]; the floor is cfl_min_limited after a limited step and min(cfl_min, CFL) otherwise, so a CFL below cfl_min regrows by the usual rule instead of jumping back to cfl_min.
exhausted: CFL fell below 1e-6 cfl0 after rejections, or the last max_frozen_steps steps all had theta = 0. A step with theta = 0 leaves the state unchanged, so once a cell sits at the positivity floor every later step repeats it; without this stop PTC spent its whole step budget frozen (OAT15A, Cadence grid level 1: ~950 identical steps at CFL cfl_min_limited).
- __post_init__()
- reset()
- update(r_prev, r, ksp_ok=True, theta=1.0)
Returns (accept, limited). Updates self.cfl.
- band_growth: float = 1.2
- cfl: float
- cfl0: float = 5.0
- cfl_max: float = 1000000000000.0
- cfl_min: float = 1.0
- cfl_min_limited: float = 0.001
- cut: float = 0.1
- property exhausted
- frozen_steps: int = 0
- max_frozen_steps: int = 5
- max_growth: float = 2.0
- min_growth: float = 1.5
- reject_factor: float = 10.0
- rise_factor_max: float = 0.5
- rise_factor_min: float = 0.1
- theta_cut: float = 0.1
- class dragonfly_sim.core.time_integration.StepRecord
- accepted: bool
- dt: float
- ksp_its: int = 0
- newton_its: int = 0
- order: int = 0
- res_final: float
- res_initial: float
- retries: int = 0
- class dragonfly_sim.core.time_integration.StepSizeController
Physical time step: ramp from dt_start by growth per step up to dt, and halve on a failed step, up to max_retries times.
- first(dt_prev)
- dt: float
- dt_start: float = None
- growth: float = 1.5
- max_retries: int = 8
- class dragonfly_sim.core.time_integration.TimeIntegrator(model, term)
Drives a CompressibleEulerModel (or a model hosted on one) through steady PTC, unsteady BDF or dual-time steps. Built by the model (set_up_solver / enable_time_stepping); term is its TimeTerm.
Time-dependent boundary conditions (core/farfield.py:TransverseFarfield) register through boundary_updaters: objects with prepare(order) before a physical step’s Newton solve and commit() after it is accepted.
- _copy(dst, src)
- _dual_time_solve(ctrl, rtol, atol, max_it)
PTC on R* inside one physical step; converged when |R*| < max(atol, rtol |R*_0|). Each pseudo step is one Newton step.
- _newton_solve(rtol, atol, max_it)
- _print(*args)
- _warm_start_cfl()
The final CFL of the last converged steady solve if the state now in u_vec is that solve’s end state (the warm start of an optimization step), else None. Collective.
- advance(step_control, order=2, newton_rtol=1e-08, newton_atol=1e-10, newton_max_it=20, dual_time=None)
One physical time step from U^n. The step size comes from step_control (StepSizeController); a failed Newton solve restores U^n and halves dt, up to step_control.max_retries times. dual_time (a CFLController) switches the inner solve to PTC on R*. Returns a StepRecord; record.accepted is False if every retry failed.
- solve_steady(controller=None, max_steps=None, tol=None)
Pseudo-transient continuation to the steady state from the current u_vec. Returns the model’s physical residual Vec.
With model.ptc_warm_start (default) and no explicit controller, a start from the end state of the previous converged steady solve continues at that solve’s final CFL: the design (or alpha) has moved a little, so the flow is near its new steady state, and ramping the CFL up from cfl0 again would only repeat the early steps. A step that is too bold is rejected and the CFL cut, as always.
- start(time=0.0, step_index=0, dt_prev=None)
Begin (or restart) time stepping from the current u_vec, which is taken as U^n (and U^{n-1} unless restored from a checkpoint).
- _steady_end = None
- boundary_updaters = []
- dt_prev = None
- model
- step_index = 0
- term
- time = 0.0
- verbose = True
- class dragonfly_sim.core.time_integration.TimeTerm(model, physical=False, pseudo=False)
The physical and pseudo-time terms of R* as UFL forms over Constants and Functions owned here (see the module docstring).
- augment(F)
- local_inverse_time_scale(U)
w_K = lambda_max / h_min plus the model’s time_scale_terms.
- physical_off()
- set_bdf(order, dt, dt_prev=None)
- set_cfl(cfl)
- U_n
- U_nm1
- U_old
- a
- inv_cfl
- inv_dt
- model
- dragonfly_sim.core.time_integration.bdf_coefficients(order, ratio=None)
(a0, a1, a2) for (a0 U^{n+1} + a1 U^n + a2 U^{n-1}) / dt_n. Variable-step BDF2 with ratio w = dt_n / dt_{n-1}:
a0 = (1 + 2w)/(1 + w), a1 = -(1 + w), a2 = w^2/(1 + w),
which is BDF_COEFFS[2] at w = 1. BDF1 ignores the ratio.
- dragonfly_sim.core.time_integration.BDF_COEFFS