Skip to content

Commit 9dc1cfc

Browse files
Copilotpancetta
andcommitted
Add index-1 semi-explicit DAE playground with temporal convergence study
Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> Agent-Logs-Url: https://github.com/Parallel-in-Time/pySDC/sessions/1909c185-754a-4f77-bed0-722371529d95
1 parent e9e6b0c commit 9dc1cfc

2 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
r"""
2+
Index-1 semi-explicit DAE problem class
3+
========================================
4+
5+
Implements the index-1 semi-explicit DAE
6+
7+
.. math::
8+
y'(t) = -\lambda y(t) + z(t) + (\lambda - 1)\sin(t),
9+
10+
.. math::
11+
0 = z(t) - (y(t) + \cos(t)),
12+
13+
which has the analytical solution
14+
15+
.. math::
16+
y_{\mathrm{ex}}(t) = \sin(t), \quad z_{\mathrm{ex}}(t) = \sin(t) + \cos(t).
17+
18+
The system is index-1 because the algebraic constraint uniquely determines
19+
:math:`z` as a function of :math:`y` and :math:`t`. Differentiating the
20+
constraint once yields :math:`z' = y' - \sin(t)`, which can be substituted
21+
back to eliminate :math:`z` and produce a pure ODE.
22+
"""
23+
24+
import numpy as np
25+
26+
from pySDC.projects.DAE.misc.problemDAE import ProblemDAE
27+
28+
29+
class index1_semiexplicit_dae(ProblemDAE):
30+
r"""
31+
Index-1 semi-explicit DAE with known analytical solution.
32+
33+
The system is
34+
35+
.. math::
36+
y'(t) = -\lambda y(t) + z(t) + (\lambda - 1)\sin(t),
37+
38+
.. math::
39+
0 = z(t) - (y(t) + \cos(t)).
40+
41+
The exact solution is
42+
43+
.. math::
44+
y_{\mathrm{ex}}(t) = \sin(t), \quad z_{\mathrm{ex}}(t) = \sin(t) + \cos(t).
45+
46+
Parameters
47+
----------
48+
lam : float, optional
49+
Stiffness parameter :math:`\lambda > 0`. Default 1.
50+
newton_tol : float, optional
51+
Tolerance for the nonlinear solver. Default 1e-12.
52+
"""
53+
54+
def __init__(self, lam=1.0, newton_tol=1e-12):
55+
"""Initialization routine."""
56+
# 1 differential variable (y) + 1 algebraic variable (z)
57+
super().__init__(nvars=1, newton_tol=newton_tol)
58+
self._makeAttributeAndRegister('lam', localVars=locals(), readOnly=True)
59+
60+
def eval_f(self, u, du, t):
61+
r"""
62+
Evaluate the implicit residual :math:`F(u, u', t)` of the semi-explicit DAE.
63+
64+
For the SemiImplicitDAE sweeper the unknowns at each collocation node
65+
are :math:`(y', z)`. The residual is
66+
67+
.. math::
68+
F_{\mathrm{diff}} &= y'(t) - \bigl(-\lambda y + z + (\lambda-1)\sin t\bigr) = 0, \\
69+
F_{\mathrm{alg}} &= z - (y + \cos t) = 0.
70+
71+
Parameters
72+
----------
73+
u : dtype_u
74+
Current solution; ``u.diff[0]`` = :math:`y`, ``u.alg[0]`` = :math:`z`.
75+
du : dtype_u
76+
Derivative estimate; ``du.diff[0]`` = :math:`y'`, ``du.alg[0]`` = :math:`z`.
77+
t : float
78+
Current time.
79+
80+
Returns
81+
-------
82+
f : dtype_f
83+
Residual with components ``.diff`` and ``.alg``.
84+
"""
85+
f = self.dtype_f(self.init)
86+
lam = self.lam
87+
88+
# Differential equation residual: y' - f(y, z, t) = 0
89+
f.diff[0] = du.diff[0] - (-lam * u.diff[0] + u.alg[0] + (lam - 1) * np.sin(t))
90+
# Algebraic constraint residual: z - (y + cos t) = 0
91+
f.alg[0] = u.alg[0] - (u.diff[0] + np.cos(t))
92+
93+
self.work_counters['rhs']()
94+
return f
95+
96+
def u_exact(self, t):
97+
r"""
98+
Exact solution at time :math:`t`.
99+
100+
Parameters
101+
----------
102+
t : float
103+
Evaluation time.
104+
105+
Returns
106+
-------
107+
me : dtype_u
108+
Exact solution: ``me.diff[0]`` = :math:`\sin t`,
109+
``me.alg[0]`` = :math:`\sin t + \cos t`.
110+
"""
111+
me = self.dtype_u(self.init)
112+
me.diff[0] = np.sin(t)
113+
me.alg[0] = np.sin(t) + np.cos(t)
114+
return me
115+
116+
def du_exact(self, t):
117+
r"""
118+
Exact derivative of the solution at time :math:`t`.
119+
120+
Parameters
121+
----------
122+
t : float
123+
Evaluation time.
124+
125+
Returns
126+
-------
127+
me : dtype_u
128+
Exact derivative: ``me.diff[0]`` = :math:`\cos t`,
129+
``me.alg[0]`` = :math:`\cos t - \sin t`.
130+
"""
131+
me = self.dtype_u(self.init)
132+
me.diff[0] = np.cos(t)
133+
me.alg[0] = np.cos(t) - np.sin(t)
134+
return me
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
r"""
2+
Temporal order of convergence — index-1 semi-explicit DAE
3+
==========================================================
4+
5+
This script tests the temporal order of convergence of semi-implicit SDC
6+
applied to the index-1 semi-explicit DAE
7+
8+
.. math::
9+
y'(t) = -\lambda y(t) + z(t) + (\lambda - 1)\sin(t),
10+
11+
.. math::
12+
0 = z(t) - (y(t) + \cos(t)),
13+
14+
whose analytical solution is
15+
16+
.. math::
17+
y_{\mathrm{ex}}(t) = \sin(t), \quad z_{\mathrm{ex}}(t) = \sin(t) + \cos(t).
18+
19+
**Goal**: with :math:`M = 3` RADAU-RIGHT quadrature nodes and fully-converged
20+
SDC (``restol = 1e-13``, ``maxiter = 50``), confirm that:
21+
22+
* The **differential variable** :math:`y` achieves the full collocation order
23+
:math:`2M - 1 = 5`.
24+
* Whether the **algebraic variable** :math:`z` shows **order reduction** or
25+
also achieves the full collocation order.
26+
27+
The SemiImplicitDAE sweeper is used, which treats :math:`y'` and :math:`z`
28+
as unknowns at each collocation node and only integrates the differential
29+
components.
30+
31+
Usage::
32+
33+
python run_convergence.py
34+
"""
35+
36+
import numpy as np
37+
38+
from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI
39+
from pySDC.projects.DAE.sweepers.semiImplicitDAE import SemiImplicitDAE
40+
from pySDC.playgrounds.DAE_index1.index1_dae import index1_semiexplicit_dae
41+
42+
# ---------------------------------------------------------------------------
43+
# Fixed parameters
44+
# ---------------------------------------------------------------------------
45+
46+
_LAM = 1.0 # stiffness parameter λ
47+
_T0 = 0.0
48+
_TEND = 1.0
49+
_NUM_NODES = 3 # RADAU-RIGHT quadrature nodes
50+
_RESTOL = 1e-13 # tight tolerance → SDC has converged
51+
52+
_SWEEPER_PARAMS = {
53+
'quad_type': 'RADAU-RIGHT',
54+
'num_nodes': _NUM_NODES,
55+
'QI': 'LU',
56+
'initial_guess': 'spread',
57+
}
58+
59+
60+
# ---------------------------------------------------------------------------
61+
# Helpers
62+
# ---------------------------------------------------------------------------
63+
64+
def _run(dt):
65+
"""
66+
Run one simulation and return ``(uend, problem_instance)``.
67+
68+
Parameters
69+
----------
70+
dt : float
71+
Time-step size.
72+
73+
Returns
74+
-------
75+
uend : MeshDAE
76+
Solution at the final time; ``.diff[0]`` = y, ``.alg[0]`` = z.
77+
P : index1_semiexplicit_dae
78+
Problem instance (used to evaluate the exact solution).
79+
"""
80+
desc = {
81+
'problem_class': index1_semiexplicit_dae,
82+
'problem_params': {'lam': _LAM, 'newton_tol': 1e-12},
83+
'sweeper_class': SemiImplicitDAE,
84+
'sweeper_params': _SWEEPER_PARAMS,
85+
'level_params': {'restol': _RESTOL, 'dt': dt},
86+
'step_params': {'maxiter': 50},
87+
}
88+
ctrl = controller_nonMPI(num_procs=1, controller_params={'logger_level': 40}, description=desc)
89+
P = ctrl.MS[0].levels[0].prob
90+
uend, _ = ctrl.run(u0=P.u_exact(_T0), t0=_T0, Tend=_TEND)
91+
return uend, P
92+
93+
94+
def _print_table(dts, errs, expected_order, var_name):
95+
"""Print a convergence table for one variable."""
96+
print(f' {var_name}:')
97+
print(f' {"dt":>10} {"error (inf)":>14} {"order":>8} {"expected":>10}')
98+
for i, (dt, err) in enumerate(zip(dts, errs)):
99+
if i > 0 and errs[i - 1] > 0.0 and err > 0.0:
100+
order = np.log(errs[i - 1] / err) / np.log(dts[i - 1] / dt)
101+
print(f' {dt:>10.5f} {err:>14.4e} {order:>8.2f} {expected_order:>10d}')
102+
else:
103+
print(f' {dt:>10.5f} {err:>14.4e} {"---":>8} {expected_order:>10d}')
104+
105+
106+
# ---------------------------------------------------------------------------
107+
# Main study
108+
# ---------------------------------------------------------------------------
109+
110+
def main():
111+
r"""
112+
Compare convergence orders for the differential variable :math:`y` and
113+
the algebraic variable :math:`z` under fully-converged semi-implicit SDC.
114+
115+
Parameters (fixed):
116+
117+
* ``restol = 1e-13``, ``maxiter = 50``, :math:`M = 3` RADAU-RIGHT nodes
118+
* :math:`\lambda = 1`, :math:`T_{\mathrm{end}} = 1`
119+
* Error measured vs. analytical solution at :math:`T_{\mathrm{end}}`.
120+
121+
Full collocation order for RADAU-RIGHT: :math:`2M - 1 = 5`.
122+
"""
123+
coll_order = 2 * _NUM_NODES - 1 # 5
124+
125+
# dt range: coarse to fine in factors of 2
126+
dts = [_TEND / (2**k) for k in range(1, 7)] # 0.5, 0.25, 0.125, ...
127+
128+
errs_y = []
129+
errs_z = []
130+
131+
for dt in dts:
132+
uend, P = _run(dt)
133+
uex = P.u_exact(_TEND)
134+
errs_y.append(abs(float(uend.diff[0]) - float(uex.diff[0])))
135+
errs_z.append(abs(float(uend.alg[0]) - float(uex.alg[0])))
136+
137+
print(f'\nFully-converged Semi-Implicit-SDC (restol={_RESTOL:.0e}, λ={_LAM}, M={_NUM_NODES})')
138+
print(f'Expected collocation order = {coll_order} (= 2M − 1 for RADAU-RIGHT)')
139+
print(f't ∈ [{_T0}, {_TEND}], error vs. analytical solution at T_end\n')
140+
141+
print('=' * 66)
142+
_print_table(dts, errs_y, coll_order, 'y (differential variable)')
143+
print('=' * 66)
144+
print()
145+
print('=' * 66)
146+
_print_table(dts, errs_z, coll_order, 'z (algebraic variable) ')
147+
print('=' * 66)
148+
149+
# ---- summary ----
150+
# Compute observed order for last two refinements
151+
def _obs_order(errs):
152+
if errs[-1] > 0.0 and errs[-2] > 0.0:
153+
return np.log(errs[-2] / errs[-1]) / np.log(dts[-2] / dts[-1])
154+
return float('nan')
155+
156+
oy = _obs_order(errs_y)
157+
oz = _obs_order(errs_z)
158+
159+
print()
160+
print('=' * 66)
161+
print(' Summary')
162+
print('=' * 66)
163+
print(f' y (differential): observed order ≈ {oy:.2f} (expected {coll_order})')
164+
print(f' z (algebraic): observed order ≈ {oz:.2f} (expected {coll_order})')
165+
if abs(oy - coll_order) < 0.5:
166+
print(f' → y achieves full collocation order {coll_order}. ✓')
167+
else:
168+
print(f' → y does NOT reach full collocation order {coll_order}.')
169+
if abs(oz - coll_order) < 0.5:
170+
print(f' → z also achieves full collocation order {coll_order}. No order reduction.')
171+
print(f' (z is directly recovered from the constraint z = y + cos(t) at each')
172+
print(f' collocation node, so it inherits the full order of y.)')
173+
else:
174+
print(f' → z shows order reduction (≈ {oz:.2f} < {coll_order}).')
175+
176+
177+
if __name__ == '__main__':
178+
main()

0 commit comments

Comments
 (0)