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

BDF_COEFFS

Classes

CFLController

Switched-evolution-relaxation CFL law with the safeguards the p = 0 RANS

StepRecord

StepSizeController

Physical time step: ramp from dt_start by growth per step up to dt,

TimeIntegrator

Drives a CompressibleEulerModel (or a model hosted on one) through

TimeTerm

The physical and pseudo-time terms of R* as UFL forms over Constants

Functions

bdf_coefficients(order[, ratio])

(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)
_unsteady_residual_norm(b)

|R*| with the pseudo term off (at U = U_old it vanishes anyway).

_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