Time Integration

The dream.time module provides the time-stepping infrastructure that is shared across all dream solvers. It separates two concerns:

Stationary and transient routines

For steady-state problems, StationaryRoutine solves the discrete problem directly, without time-stepping. For time-dependent problems, TransientRoutine marches the solution forward over a fixed time interval \((t_0, t_{\mathrm{end}})\) held by a Timer, advancing \(t_n \to t_{n+1} = t_n + \Delta t\) until \(t_{\mathrm{end}}\) is reached. PseudoTimeSteppingRoutine instead marches a stationary problem towards steady state using pseudo-time continuation, in which an artificial time derivative is added and the (pseudo) time step is progressively increased, improving the robustness of the nonlinear solver when starting far from the solution.

Geometry-split implicit-explicit (IMEX) time integration

In many flow problems, only a small part of the computational mesh is responsible for the geometry-induced stiffness that limits the stable time step of an explicit scheme, for example highly refined regions near walls or small geometric features. The IMEX routines derived from IMEXTimeRoutine exploit this by coupling two independently configured SolverConfigurations, held by an IMEXTimeRoutine as cfg_implicit and cfg_explicit. The routine itself does not partition a mesh into implicit/explicit regions; rather, it assumes that the two complementary meshes,

\[\begin{align*} \mesh = \mesh^{im} \cup \mesh^{ex}, \qquad \Gamma_i = \mesh^{im} \cap \mesh^{ex}, \end{align*}\]

with interface \(\Gamma_i\), have already been constructed and assigned to cfg_implicit and cfg_explicit, respectively, e.g. with the stiff (typically small) region assigned to cfg_implicit and the remaining, non-stiff region to cfg_explicit. Stiff regions are typically discretized with an implicit Hybridised Discontinuous Galerkin (HDG) scheme, while non-stiff regions are discretized with a (standard) discontinuous Galerkin (DG) scheme; however, the routine itself only assumes that cfg_implicit is solved implicitly and cfg_explicit explicitly in time, not any specific spatial discretization. In particular, both regions may equally well use a DG discretization, with only the time treatment (implicit vs. explicit) differing between them.

The two solutions are coupled weakly and conservatively across \(\Gamma_i\) by appropriate interface conditions, while the temporal synchronization between the implicit and explicit schemes is achieved through additive Runge-Kutta (ARK) methods: the implicit part is advanced with a singly diagonally implicit Runge-Kutta (SDIRK) method, and the explicit part with a standard explicit Runge-Kutta (ERK) method. At every stage, the explicit solution is updated first, using the implicit solution of the previous stage, and the implicit solution is then updated using the just-computed explicit solution.

Two synchronization strategies are available:

  • SynchronizedIMEXTimeRoutine: the implicit and explicit schemes share the same (global) time step \(\Delta t\) and their stage times coincide, \(\overline{c}_{i+1} = c_i\), following the classical structure of an ARK method. This is the strategy used for the ARS-type IMEX schemes for compressible flows.

  • PCIMEXTimeRoutine and LinearPCIMEXTimeRoutine: a predictor-corrector strategy in which the global (implicit) time step may be a larger integer multiple of the local (explicit) time step, allowing the explicit scheme to sub-cycle within an implicit stage. During the sub-cycling, the interface value provided to the explicit scheme is either held frozen at \(\vec{U}_n^{im}\) (PCIMEXTimeRoutine) or linearly interpolated in time between the implicit solution at the start and end of the stage (LinearPCIMEXTimeRoutine), before the implicit (corrector) solution is recomputed using the updated explicit state.

By restricting the (more expensive) implicit solve to the stiff region \(\mesh^{im}\), while advancing the remainder of the domain \(\mesh^{ex}\) with the cheaper explicit scheme, the IMEX approach increases the overall stable time step compared to a fully explicit discretization, at the cost of the additional implicit solve. The net benefit therefore depends both on an effective mesh partitioning and an efficient implicit solver.

Timer([mesh, root])

Keeps track of the simulation time and time step used by a TimeRoutine.

Scheme(mesh[, root])

Base interface for a spatial/temporal discretization scheme.

TimeSchemes(mesh[, root])

Base interface for a concrete (multi-step or multi-stage) time-stepping scheme.

TimeRoutine(mesh[, root])

Base interface for a top-level solution routine driving a SolverConfiguration.

StationaryRoutine(mesh[, root])

Solves a stationary (steady-state) problem.

TransientRoutine(mesh[, root])

Marches a time-dependent problem forward over a fixed time interval.

PseudoTimeSteppingRoutine(mesh[, root])

Marches a stationary problem to steady state using pseudo-time continuation.

IMEXTimeRoutine(cfg_implicit, cfg_explicit, ...)

Base interface for a geometry-split implicit-explicit (IMEX) time routine.

SynchronizedIMEXTimeRoutine(cfg_implicit, ...)

IMEX routine in which the implicit and explicit schemes share the same time step and stage times.

PCIMEXTimeRoutine(cfg_implicit, ...)

IMEX predictor-corrector routine with frozen interface values during the predictor stage.

LinearPCIMEXTimeRoutine(cfg_implicit, ...)

IMEX predictor-corrector routine with linear interpolation of interface values.

class Timer(mesh=None, root=None, **default)

Keeps track of the simulation time and time step used by a TimeRoutine.

The timer holds the time interval \((t_0, t_{\mathrm{end}})\) to be simulated, the current simulation time \(t\) and the time step \(\Delta t\). Calling the timer as a generator, see __call__(), advances \(t\) from \(t_0\) to \(t_{\mathrm{end}}\) in increments of \(\Delta t\), yielding the iteration number together with the time levels \(t_n\) and \(t_{n+1}\) bracketing each step.

property interval: tuple[float, float]

Sets the time interval \((t_0, t_{\mathrm{end}})\) to be simulated.

Getter:

Returns the time interval

Setter:

Sets the time interval, defaults to (0.0, 1.0)

property step: Parameter

Sets the time step \(\Delta t\) used to advance the simulation.

Getter:

Returns the time step

Setter:

Sets the time step, defaults to 1e-4

property t: Parameter

Sets the current simulation time \(t\).

Getter:

Returns the current simulation time

Setter:

Sets the current simulation time, defaults to 0.0

class Scheme(mesh, root=None, **default)

Base interface for a spatial/temporal discretization scheme.

A Scheme is responsible for assembling the bilinear and linear forms of the discretized problem, see assemble(), and for solving either a stationary problem, see solve_stationary(), or a single time level/stage, see TimeSchemes.

property compile: dict[str, bool]

Sets the NGSolve compile options used when assembling the symbolic forms.

Getter:

Returns the compile options

Setter:

Sets the compile options, defaults to False

parse_sum_of_integrals(integrals: dict[str, dict[str, SumOfIntegrals]], include_spaces: tuple[str, ...] = None, exclude_spaces: tuple[str, ...] = None, include_terms: tuple[str, ...] = None, exclude_terms: tuple[str, ...] = None) dict[str, dict[str, SumOfIntegrals]]

Parse the sum of integrals dictionary to include or exclude specific spaces and terms.

By default, it includes all spaces and terms in the integrals. You can specify which spaces to include or exclude, and which terms to include or exclude. If a space in the include container is not found in the integrals dictionary, it will raise an error.

class TimeSchemes(mesh, root=None, **default)

Base interface for a concrete (multi-step or multi-stage) time-stepping scheme.

A TimeSchemes discretizes the time derivative of the semi-discrete problem and advances the solution either over a single time level, see solve_current_time_level(), or over a single Runge-Kutta stage, see solve_stage(). Multi-step schemes (e.g. BDF) keep track of the solutions of the previous number_of_steps time levels via get_step_gridfunctions(), while multi-stage schemes (e.g. Runge-Kutta) evaluate the right-hand side at the intermediate stage times given by time_of_stages.

Concrete implementations are provided per solver, e.g. in dream.scalar_transport.time and dream.compressible_flow.conservative.time.

update_final_stage_solution() None

Updates the final stage solution

This method is needed in case the time scheme is not stiffly accurate.

class TimeRoutine(mesh: Mesh, root: Configuration = None, **default)

Base interface for a top-level solution routine driving a SolverConfiguration.

While a TimeSchemes discretizes a single time level or stage, a TimeRoutine orchestrates the full solution process, i.e. it drives the main solution loop, see start_solution_routine(), manages I/O, and reports solver convergence, see parse_routine_log(). Available routines are StationaryRoutine, TransientRoutine, PseudoTimeSteppingRoutine, and the IMEX routines derived from IMEXTimeRoutine.

parse_routine_log(it: int | None = None, error: float | None = None, t: float | None = None, stage: int | None = None, cfg: SolverConfiguration = None, **kwargs)

Parse the routine log and return a formatted string.

class StationaryRoutine(mesh: Mesh, root: Configuration = None, **default)

Solves a stationary (steady-state) problem.

No time integration is performed: the routine simply assembles the discrete problem and solves it via the nonlinear solver of the underlying scheme, see Scheme.solve_stationary().

class TransientRoutine(mesh, root=None, **default)

Marches a time-dependent problem forward over a fixed time interval.

The routine repeatedly advances the solution from \(t_n\) to \(t_{n+1} = t_n + \Delta t\) over the interval held by its timer, see start_solution_routine(). Each time level is solved by the scheme of the underlying SolverConfiguration, see TimeSchemes.solve_current_time_level(). In addition to the standard solution routine, this class provides start_timing_solution_routine() for performance profiling, and find_stable_time_step() to determine, by bisection, the largest stable time step for an explicit scheme.

property timer: Timer

Sets the timer holding the time interval, time step and current simulation time.

Getter:

Returns the timer

Setter:

Sets the timer, defaults to Timer

class PseudoTimeSteppingRoutine(mesh, root=None, **default)

Marches a stationary problem to steady state using pseudo-time continuation.

Rather than solving the nonlinear stationary problem directly, an artificial (pseudo) time derivative is added and advanced with a time step that is progressively increased, every increment_at iterations, by increment_factor, up to max_time_step. This continuation strategy improves the robustness of the nonlinear solver, especially when starting far from the steady-state solution, see solver_iteration_update().

property timer: Timer

Sets the timer holding the pseudo-time step and current pseudo-time.

Getter:

Returns the timer

Setter:

Sets the timer, defaults to Timer

property max_time_step: float

Sets the upper bound the pseudo-time step is allowed to grow to.

Getter:

Returns the maximum pseudo-time step

Setter:

Sets the maximum pseudo-time step, defaults to 1.0

property increment_at: int

Sets the number of iterations after which the pseudo-time step is increased.

Getter:

Returns the increment interval

Setter:

Sets the increment interval, defaults to 10

property increment_factor: int

Sets the multiplicative factor applied to the pseudo-time step at each increment.

Getter:

Returns the increment factor

Setter:

Sets the increment factor, defaults to 10

class IMEXTimeRoutine(cfg_implicit: SolverConfiguration, cfg_explicit: SolverConfiguration, **default)

Base interface for a geometry-split implicit-explicit (IMEX) time routine.

An IMEXTimeRoutine couples two independently configured solvers, cfg_implicit and cfg_explicit, each holding its own SolverConfiguration and mesh. The routine itself does not partition a mesh into implicit/explicit regions; rather, it assumes that the two complementary meshes \(\mesh^{im}\) and \(\mesh^{ex}\), with interface \(\Gamma_i = \mesh^{im} \cap \mesh^{ex}\), have already been constructed and assigned to cfg_implicit and cfg_explicit, respectively, e.g. with the stiff (typically small) region assigned to cfg_implicit and the remaining, non-stiff region to cfg_explicit.

Stiff regions are typically treated implicitly using a hybridizable discontinuous Galerkin (HDG) scheme, while non-stiff regions are treated explicitly using a (standard) discontinuous Galerkin (DG) scheme; however, the routine itself only assumes that cfg_implicit is solved implicitly and cfg_explicit explicitly in time, and not any specific spatial discretization. In particular, both regions may equally well use a DG discretization, with only the time treatment (implicit vs. explicit) differing between them. The two solutions are weakly and conservatively coupled across \(\Gamma_i\) by appropriate interface conditions, while temporal synchronization between the implicit and explicit schemes is achieved through additive Runge-Kutta (ARK) methods, in which the implicit part is a singly diagonally implicit Runge-Kutta (SDIRK) method and the explicit part a standard explicit Runge-Kutta (ERK) method.

At each global time step, every stage is solved by first advancing the explicit region, see solve_explicit_stage(), and then the implicit region, see solve_implicit_stage(), such that the explicit stage uses the implicit solution at the previous stage, and the implicit stage uses the just-updated explicit solution, see solve_stages().

Restricting the (typically more expensive) implicit scheme to a small region, while advancing the bulk of the domain with the cheaper explicit scheme, allows the explicit scheme’s stable time step to increase compared to a fully explicit discretization of the whole domain; this increase yields the overall speedup of the IMEX approach, at the cost of the additional implicit solve. Two synchronization strategies are provided: SynchronizedIMEXTimeRoutine, in which the implicit and explicit schemes share the same time step and stage times, and the predictor-corrector routines PCIMEXTimeRoutine/LinearPCIMEXTimeRoutine, which allow the explicit scheme to sub-cycle with a smaller, locally defined time step.

property gtimer: Timer

Global timer for the implicit scheme.

property ltimer: Timer

Local timer for the explicit scheme.

property gscheme: TimeSchemes

Global implicit scheme.

property lscheme: TimeSchemes

Local explicit scheme.

update_final_stage_solution() None

Updates the final stage solution

This method is needed in case the time scheme is not stiffly accurate.

class SynchronizedIMEXTimeRoutine(cfg_implicit: SolverConfiguration, cfg_explicit: SolverConfiguration, **default)

IMEX routine in which the implicit and explicit schemes share the same time step and stage times.

Both the implicit and explicit schemes advance with the same (global) time step \(\Delta t\) and the additive Runge-Kutta tableaux are synchronized via padding, i.e. the stage times coincide, \(\overline{c}_{i+1} = c_i\). This is the classical structure of an ARK method and is the synchronization strategy used for the geometry-split IMEX schemes (ARS-type) developed for compressible flows in IMEXTimeRoutine.

At every stage \(i\), the explicit stage solution is computed first, see solve_explicit_stage(), followed by the implicit stage solution, see solve_implicit_stage(), both using the previously computed stage solution of the other scheme. The final solution update accounts for the first-same-as-last (FSAL) property of the explicit scheme and the stiff accuracy of the implicit scheme, see update_final_stage_solution().

update_final_stage_solution()

Updates the final stage solution

This method is needed in case the time scheme is not stiffly accurate.

class PCIMEXTimeRoutine(cfg_implicit: SolverConfiguration, cfg_explicit: SolverConfiguration, **default)

IMEX predictor-corrector routine with frozen interface values during the predictor stage.

Unlike SynchronizedIMEXTimeRoutine, the global (implicit) time step \(\Delta t\) need not equal the local (explicit) time step, but must be an integer multiple of it, allowing the explicit scheme to sub-cycle with several smaller steps per implicit stage.

For each implicit stage, a predictor step first advances the implicit solution, see solve_predictor_stage(), and the explicit scheme is then sub-cycled across the stage interval using the predicted interface value frozen at \(\vec{U}_n^{im}\) for all explicit sub-steps, see set_predictor_solution(). The implicit (corrector) solution is finally recomputed using the now up-to-date explicit interface state, see solve_implicit_stage().

update_final_stage_solution()

Updates the final stage solution

This method is needed in case the time scheme is not stiffly accurate.

class LinearPCIMEXTimeRoutine(cfg_implicit: SolverConfiguration, cfg_explicit: SolverConfiguration, **default)

IMEX predictor-corrector routine with linear interpolation of interface values.

Extends PCIMEXTimeRoutine by replacing the frozen interface value during the predictor stage with a linear interpolation in time. The predictor stage first solves the implicit scheme to obtain a prediction of the interface state at the end of the stage, see solve_predictor_stage(), and the explicit sub-steps then use values linearly interpolated, based on the local sub-step time, between the implicit solution at the start (\(\vec{y}_1\)) and end (\(\vec{y}_2\)) of the stage, see set_predictor_solution(). The corrector step, see solve_implicit_stage(), resets the implicit solution to \(\vec{y}_1\) and resolves the implicit stage using the now updated explicit solution, improving accuracy compared to the frozen-interface predictor of PCIMEXTimeRoutine.