Skip to content

Commit 23f5c9b

Browse files
authored
Add StrangMarchuk second-order symmetric operator splitting (#73)
* Add generic Strang-Marchuk second-order symmetric operator splitting * Add convergence order tests * Remove stringent errors on subintegration failure
1 parent 2f5d031 commit 23f5c9b

9 files changed

Lines changed: 295 additions & 25 deletions

File tree

docs/src/api-reference/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ OperatorSplittingProblem
1212
GenericSplitFunction
1313
```
1414

15-
## Solver
15+
## Solvers
1616

1717
```@docs
1818
LieTrotterGodunov
19+
StrangMarchuk
1920
```

docs/src/assets/references.bib

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,23 @@ @article{God:1959:dmn
3232
year={1959},
3333
publisher={Russian Academy of Sciences, Steklov Mathematical Institute}
3434
}
35+
36+
@article{Str:1968:ccd,
37+
title={On the construction and comparison of difference schemes},
38+
author={Strang, Gilbert},
39+
journal={SIAM Journal on Numerical Analysis},
40+
volume={5},
41+
number={3},
42+
pages={506--517},
43+
year={1968},
44+
publisher={SIAM}
45+
}
46+
47+
@incollection{Mar:1971:tsm,
48+
title={On the theory of the splitting-up method},
49+
author={Marchuk, Guri Ivanovich},
50+
booktitle={Numerical Solution of Partial Differential Equations-{II}},
51+
pages={469--500},
52+
year={1971},
53+
publisher={Academic Press}
54+
}

docs/src/topics/time-integration.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,55 @@ $n \in \mathbb{N}$ the following bound
116116

117117
which implies stability of the scheme.
118118

119+
### Strang-Marchuk Splitting
120+
121+
A natural way to improve the accuracy of operator splitting is to symmetrize the
122+
scheme. The Strang-Marchuk splitting [Str:1968:ccd,Mar:1971:tsm](@cite) achieves
123+
second-order accuracy for $N$ operators by performing a palindromic sweep
124+
125+
```math
126+
F_1(\Delta t/2) \to \cdots \to F_{N-1}(\Delta t/2) \to F_N(\Delta t) \to F_{N-1}(\Delta t/2) \to \cdots \to F_1(\Delta t/2)
127+
```
128+
129+
More formally, for the simplest case of two operators $F_1$ and $F_2$
130+
131+
```math
132+
\begin{aligned}
133+
\text{Solve} \quad d_t u^1(t) &= F_1(u^1(t), p, t) & & \quad \text{on} \; [t_0, t_0 + \Delta t/2] \; \text{with} \; u^1(t_0) = u_0 \\
134+
\text{Solve} \quad d_t u^2(t) &= F_2(u^2(t), p, t) & & \quad \text{on} \; [t_0, t_0 + \Delta t] \; \text{with} \; u^2(t_0) = u^1(t_0 + \Delta t/2) \\
135+
\text{Solve} \quad d_t u^3(t) &= F_1(u^3(t), p, t) & & \quad \text{on} \; [t_0 + \Delta t/2, t_0 + \Delta t] \; \text{with} \; u^3(t_0 + \Delta t/2) = u^2(t_0 + \Delta t)
136+
\end{aligned}
137+
```
138+
139+
yielding $u(t_0 + \Delta t) \approx u^3(t_0 + \Delta t)$.
140+
141+
### Analysis of Strang-Marchuk
142+
143+
We show the second-order accuracy for two bounded linear operators $L_1$ and
144+
$L_2$. The Strang-Marchuk approximation reads
145+
146+
```math
147+
\tilde{u}(t) = e^{L_1 t/2} \, e^{L_2 t} \, e^{L_1 t/2} \, u_0 \, .
148+
```
149+
150+
Expanding the exponentials:
151+
152+
```math
153+
\begin{aligned}
154+
e^{L_1 t/2} \, e^{L_2 t} \, e^{L_1 t/2}
155+
&= \bigl(I + \tfrac{t}{2}L_1 + \tfrac{t^2}{8}L_1^2 + \cdots\bigr)
156+
\bigl(I + t L_2 + \tfrac{t^2}{2}L_2^2 + \cdots\bigr)
157+
\bigl(I + \tfrac{t}{2}L_1 + \tfrac{t^2}{8}L_1^2 + \cdots\bigr) \\
158+
&= I + t(L_1 + L_2) + \tfrac{t^2}{2}(L_1 + L_2)^2 + O(t^3)
159+
\end{aligned}
160+
```
161+
162+
which matches the Taylor expansion of $e^{(L_1+L_2)t}$ through the $t^2$ term.
163+
The symmetry of the scheme causes the first-order commutator term
164+
$[L_1, L_2] = L_1 L_2 - L_2 L_1$ to cancel, leaving a local truncation error
165+
of $O(t^3)$ and hence second-order global accuracy. The same argument extends to
166+
the general $N$-operator palindromic scheme.
167+
119168
## References
120169

121170
```@bibliography

docs/src/usage/index.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ for (u, t) in TimeChoiceIterator(integrator, 0.0:0.5:1.0)
4949
@show t, u
5050
end
5151
```
52+
53+
For second-order accuracy, use the `StrangMarchuk` algorithm instead.
54+
It performs the symmetric palindromic splitting
55+
A₁(Δt/2) → … → Aₙ(Δt) → … → A₁(Δt/2):
56+
57+
```julia
58+
alg = StrangMarchuk(
59+
(Euler(), Euler())
60+
)
61+
62+
integrator = init(prob, alg, dt = 0.1)
63+
for (u, t) in TimeChoiceIterator(integrator, 0.0:0.5:1.0)
64+
@show t, u
65+
end
66+
```

src/OrdinaryDiffEqOperatorSplitting.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ include("integrator.jl")
3535
include("solver.jl")
3636
include("utils.jl")
3737

38-
export GenericSplitFunction, OperatorSplittingProblem, LieTrotterGodunov
38+
export GenericSplitFunction, OperatorSplittingProblem, LieTrotterGodunov, StrangMarchuk
3939

4040
include("precompilation.jl")
4141

src/integrator.jl

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,13 @@ function step_footer!(integrator::AnySplitIntegrator)
605605
integrator.last_step_failed = false
606606
integrator.tprev = integrator.t
607607
integrator.t = fixed_t_for_floatingpoint_error!(integrator, ttmp)
608+
# Children that step with subdivided dt (e.g. StrangMarchuk's `dt/2`
609+
# halves) accumulate ulp-level drift from the parent's exact `t`.
610+
# Re-anchor children to the parent's canonical time here so the
611+
# drift cannot accumulate across outer steps.
612+
try_snap_children_to_tstop!.(integrator.child_subintegrators, integrator.t)
608613
step_accept_controller!(integrator)
614+
validate_time_point(integrator)
609615
elseif integrator.force_stepfail
610616
if isadaptive(integrator)
611617
step_reject_controller!(integrator)
@@ -617,7 +623,6 @@ function step_footer!(integrator::AnySplitIntegrator)
617623
end
618624
integrator.last_step_failed = true
619625
end
620-
validate_time_point(integrator)
621626
return nothing
622627
end
623628

@@ -907,26 +912,12 @@ function advance_solution_by!(
907912
dt
908913
)
909914
SciMLBase.step!(sub, dt, true)
910-
911-
# Unrecoverable failure: error immediately regardless of adaptive/non-adaptive
912-
if !SciMLBase.successful_retcode(sub.status.retcode) &&
913-
sub.status.retcode != ReturnCode.Default
914-
error("Inner integrator failed unrecoverably with retcode \
915-
$(sub.status.retcode) at t=$(child.t). Aborting.")
916-
end
917915
return nothing
918916
end
919917

920918
# Leaf disptach
921919
function advance_solution_by!(outer::AnySplitIntegrator, child::DEIntegrator, dt)
922920
SciMLBase.step!(child, dt, true)
923-
924-
# Unrecoverable failure: error immediately regardless of adaptive/non-adaptive
925-
if !SciMLBase.successful_retcode(child.sol.retcode) &&
926-
child.sol.retcode != ReturnCode.Default
927-
error("Inner integrator failed unrecoverably with retcode \
928-
$(child.sol.retcode) at t=$(child.t). Aborting.")
929-
end
930921
return nothing
931922
end
932923

src/precompilation.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,18 @@ end
3434
fsplit = GenericSplitFunction((f1, fsplitinner), (f1dofs, [1, 2, 3]))
3535

3636
prob = OperatorSplittingProblem(fsplit, u0, tspan)
37-
tstepper = LieTrotterGodunov((Euler(), LieTrotterGodunov((Euler(), Euler()))))
3837

39-
# Precompile init and a few steps
40-
integrator = DiffEqBase.init(prob, tstepper, dt = 0.01, verbose = false)
38+
# Precompile LieTrotterGodunov
39+
tstepper_ltg = LieTrotterGodunov((Euler(), LieTrotterGodunov((Euler(), Euler()))))
40+
integrator = DiffEqBase.init(prob, tstepper_ltg, dt = 0.01, verbose = false)
4141
step!(integrator)
4242
solve!(integrator)
43+
44+
# Precompile StrangMarchuk
45+
fsplit_sm = GenericSplitFunction((f1, f2), (f1dofs, f2dofs))
46+
prob_sm = OperatorSplittingProblem(fsplit_sm, u0, tspan)
47+
tstepper_sm = StrangMarchuk((Euler(), Euler()))
48+
integrator_sm = DiffEqBase.init(prob_sm, tstepper_sm, dt = 0.01, verbose = false)
49+
step!(integrator_sm)
50+
solve!(integrator_sm)
4351
end

src/solver.jl

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,104 @@ end
5656
backward_sync_subintegrator!(parent, child, idxs, sync)
5757
end
5858
end
59+
60+
# ---------------------------------------------------------------------------
61+
# Strang-Marchuk operator splitting
62+
# ---------------------------------------------------------------------------
63+
"""
64+
StrangMarchuk <: AbstractOperatorSplittingAlgorithm
65+
66+
Second-order symmetric (palindromic) operator splitting algorithm attributed to
67+
[Str:1968:ccd,Mar:1971:tsm](@cite).
68+
69+
For ``N`` operators the scheme performs
70+
71+
``A_1(\\Delta t/2) \\to \\cdots \\to A_{N-1}(\\Delta t/2) \\to A_N(\\Delta t) \\to A_{N-1}(\\Delta t/2) \\to \\cdots \\to A_1(\\Delta t/2)``
72+
73+
achieving second-order accuracy through symmetry.
74+
"""
75+
struct StrangMarchuk{AlgTupleType} <: AbstractOperatorSplittingAlgorithm
76+
inner_algs::AlgTupleType # Tuple of timesteppers for inner problems
77+
end
78+
79+
function Base.show(io::IO, alg::StrangMarchuk)
80+
print(io, "SM (")
81+
for inner_alg in alg.inner_algs[1:(end - 1)]
82+
Base.show(io, inner_alg)
83+
print(io, " -> ")
84+
end
85+
length(alg.inner_algs) > 0 && Base.show(io, alg.inner_algs[end])
86+
return print(io, ")")
87+
end
88+
89+
struct StrangMarchukCache{uType, uprevType} <: AbstractOperatorSplittingCache
90+
u::uType
91+
uprev::uprevType
92+
end
93+
94+
function init_cache(
95+
f::GenericSplitFunction, alg::StrangMarchuk;
96+
uprev::AbstractArray, u::AbstractVector,
97+
)
98+
return StrangMarchukCache(u, uprev)
99+
end
100+
101+
# Forward pass: A₁(dt/2) → … → Aₙ₋₁(dt/2) → Aₙ(dt)
102+
@unroll function _sm_forward_pass!(parent, children::Tuple, half_dt, dt)
103+
N = length(children)
104+
i = 0
105+
@unroll for child in children
106+
i += 1
107+
step_dt = i < N ? half_dt : dt
108+
109+
idxs = parent.child_solution_indices[i]
110+
sync = parent.child_synchronizers[i]
111+
112+
@timeit_debug "sync ->" forward_sync_subintegrator!(parent, child, idxs, sync)
113+
@timeit_debug "time solve" advance_solution_by!(parent, child, step_dt)
114+
if _child_failed(child)
115+
parent.force_stepfail = true
116+
return
117+
end
118+
119+
backward_sync_subintegrator!(parent, child, idxs, sync)
120+
end
121+
end
122+
123+
# Reverse pass: Aₙ₋₁(dt/2) → … → A₁(dt/2)
124+
@unroll function _sm_reverse_pass!(parent, rev_front::Tuple, half_dt, N)
125+
j = 0
126+
@unroll for child in rev_front
127+
j += 1
128+
i = N - j
129+
130+
idxs = parent.child_solution_indices[i]
131+
sync = parent.child_synchronizers[i]
132+
133+
@timeit_debug "sync ->" forward_sync_subintegrator!(parent, child, idxs, sync)
134+
@timeit_debug "time solve" advance_solution_by!(parent, child, half_dt)
135+
if _child_failed(child)
136+
parent.force_stepfail = true
137+
return
138+
end
139+
140+
backward_sync_subintegrator!(parent, child, idxs, sync)
141+
end
142+
end
143+
144+
function _perform_step!(
145+
parent,
146+
children::Tuple,
147+
cache::StrangMarchukCache,
148+
dt
149+
)
150+
half_dt = dt / 2
151+
152+
_sm_forward_pass!(parent, children, half_dt, dt)
153+
parent.force_stepfail && return
154+
155+
_sm_reverse_pass!(parent, reverse(children[1:(end - 1)]), half_dt, length(children))
156+
parent.force_stepfail && return
157+
158+
return
159+
end

0 commit comments

Comments
 (0)