Skip to content

Commit 0aad670

Browse files
Copilotpancetta
andauthored
Add stokes_poiseuille_1d_fd_coupled: (N+1)x(N+1) block solve with optional projection
Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> Agent-Logs-Url: https://github.com/Parallel-in-Time/pySDC/sessions/a4d9fa91-6674-4917-9903-3f9123628b48
1 parent 6e79a2c commit 0aad670

2 files changed

Lines changed: 301 additions & 9 deletions

File tree

pySDC/playgrounds/Stokes_Poiseuille_1D_FD/Stokes_Poiseuille_1D_FD.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,16 @@
141141
142142
stokes_poiseuille_1d_fd_lift_full
143143
Lifting, FullyImplicitDAE (same fixed point as lifted).
144+
145+
stokes_poiseuille_1d_fd_coupled
146+
Explicit :math:`(N+1)\times(N+1)` block solve for the differentiated
147+
constraint (mathematically equivalent to diffconstr Schur).
148+
Optional ``project=True`` also enforces :math:`B\mathbf{u}_m = q(\tau_m)`
149+
via a post-solve projection, but this **degrades** convergence because
150+
it creates an inconsistency between ``solve_system`` (which after
151+
projection enforces the algebraic constraint) and ``eval_f`` (which
152+
checks the differentiated constraint). Provided for pedagogical
153+
comparison only.
144154
"""
145155

146156
import numpy as np
@@ -420,6 +430,116 @@ def _schur_solve_full_implicit(self, rhs_eff, v_approx, factor, constraint_rhs):
420430
U = w + factor * G_prime * v0
421431
return U, float(G_prime)
422432

433+
def _coupled_block_solve(self, rhs_eff, v_approx, factor, q_prime_val,
434+
project=False, q_val=None):
435+
r"""
436+
Fully-coupled :math:`(N+1)\times(N+1)` block solve for the
437+
differentiated constraint :math:`B\mathbf{U} = q'(t)`.
438+
439+
Assembles and solves the full block system
440+
441+
.. math::
442+
443+
\begin{pmatrix}
444+
I - \alpha\nu A & -\mathbf{1} \\
445+
h\,\mathbf{1}^T & 0
446+
\end{pmatrix}
447+
\begin{pmatrix} \mathbf{U} \\ G \end{pmatrix}
448+
=
449+
\begin{pmatrix} \mathbf{r}_\text{eff} \\ q'(t) \end{pmatrix}
450+
451+
This is **mathematically equivalent** to
452+
:meth:`_schur_solve_diffconstr` (which reduces the same system to a
453+
scalar Schur equation), but solves the full :math:`(N+1)` sparse
454+
system directly.
455+
456+
If ``project=True``, a post-solve projection step enforces the
457+
**original** algebraic constraint :math:`B(\mathbf{v}+\alpha\mathbf{U})
458+
= q(t)` in addition to the differentiated one. The minimal-norm
459+
correction
460+
461+
.. math::
462+
463+
\delta = -\frac{B(\mathbf{v}+\alpha\mathbf{U}) - q(t)}{s}
464+
\,\mathbf{1}
465+
466+
is added to :math:`\mathbf{v}+\alpha\mathbf{U}`, giving
467+
468+
.. math::
469+
470+
\mathbf{U}_\text{proj} =
471+
\mathbf{U} + \frac{\delta}{\alpha}.
472+
473+
The pressure :math:`G` is then updated by solving the Schur formula
474+
:math:`G = (q'(t) - B\mathbf{U}_\text{proj}) / (B\mathbf{v}_0)` so
475+
that :math:`B\mathbf{U}_\text{proj}` uses the corrected derivative.
476+
477+
Parameters
478+
----------
479+
rhs_eff : numpy.ndarray
480+
Effective velocity RHS
481+
:math:`\nu A\mathbf{v}_\text{approx} + \mathbf{f}(t)`.
482+
v_approx : numpy.ndarray
483+
Current velocity approximation at the node.
484+
factor : float
485+
Implicit prefactor :math:`\alpha = \Delta t\,\tilde{q}_{mm}`.
486+
q_prime_val : float
487+
Value of :math:`q'(t)` at the current stage time.
488+
project : bool, optional
489+
If ``True``, apply the projection step that also enforces
490+
:math:`B(\mathbf{v}+\alpha\mathbf{U}) = q(t)`. Default ``False``.
491+
q_val : float or None
492+
Value of :math:`q(t)` required when ``project=True``.
493+
494+
Returns
495+
-------
496+
U : numpy.ndarray
497+
Velocity derivative at the node.
498+
G_new : float
499+
Pressure gradient satisfying :math:`B\mathbf{U} = q'(t)` (before
500+
projection) or consistent with the projected velocity (after).
501+
"""
502+
n = self.nvars
503+
top_left = sp.eye(n, format='csc') - factor * self.A
504+
top_right = sp.csc_matrix(-self.ones.reshape(-1, 1))
505+
bot_left = sp.csc_matrix(self.dx * self.ones.reshape(1, -1))
506+
bot_right = sp.csc_matrix(np.zeros((1, 1)))
507+
508+
K = sp.bmat(
509+
[[top_left, top_right], [bot_left, bot_right]],
510+
format='csc',
511+
)
512+
rhs = np.concatenate([rhs_eff, [q_prime_val]])
513+
sol = spsolve(K, rhs)
514+
515+
U = sol[:n].copy()
516+
G = float(sol[n])
517+
518+
if project and q_val is not None:
519+
# Post-solve projection: enforce B*(v + factor*U) = q(t).
520+
# WARNING: this projection changes U_m in a way that is
521+
# INCONSISTENT with eval_f, which checks B*u' - q'(t) = 0.
522+
# After projection, B*U_proj = B*U - violation/factor ≠ q'(t)
523+
# in general (since B*U = q'(t) before projection but violation ≠ 0).
524+
# As a result, the SDC residual no longer converges cleanly and
525+
# the sweep converges to a DIFFERENT (worse) fixed point.
526+
# The projection is provided here for pedagogical comparison only;
527+
# it should NOT be used in practice with the differentiated-constraint
528+
# eval_f.
529+
u_m = v_approx + factor * U
530+
violation = self._B_dot(u_m) - q_val
531+
if abs(violation) > 0.0:
532+
delta = -(violation / self.s) * self.ones
533+
U = U + delta / factor
534+
# Update G from the momentum residual.
535+
# At the unperturbed solution, (I-factor*A)*U - G*ones = rhs_eff,
536+
# so G*ones = (I-factor*A)*U - rhs_eff. After projection, U changes
537+
# and all N components of (I-factor*A)*U_proj - rhs_eff theoretically
538+
# equal the same G value; we take the mean for numerical stability.
539+
G = float(np.mean(top_left.dot(U) - rhs_eff))
540+
541+
return U, G
542+
423543
def _schur_solve_diffconstr(self, rhs_eff, factor, q_prime_val):
424544
r"""
425545
Schur-complement solve using the **differentiated constraint**
@@ -1193,3 +1313,152 @@ def solve_system(self, impl_sys, u_approx, factor, u0, t):
11931313
me.diff[:] = U
11941314
me.alg[0] = G_new
11951315
return me
1316+
1317+
1318+
# ---------------------------------------------------------------------------
1319+
# Case 7: Coupled block solve + projection
1320+
# ---------------------------------------------------------------------------
1321+
1322+
class stokes_poiseuille_1d_fd_coupled(stokes_poiseuille_1d_fd):
1323+
r"""
1324+
1-D Stokes/Poiseuille DAE using an **explicit** :math:`(N+1)\times(N+1)`
1325+
block solve for the differentiated constraint, with an optional
1326+
post-solve projection that also enforces the original algebraic constraint.
1327+
1328+
Two sub-variants are provided via the ``project`` constructor parameter:
1329+
1330+
**Block solve only** (``project=False``, default)
1331+
Assembles and solves the full :math:`(N+1)\times(N+1)` sparse system
1332+
1333+
.. math::
1334+
1335+
\begin{pmatrix}
1336+
I - \alpha\nu A & -\mathbf{1} \\
1337+
h\,\mathbf{1}^T & 0
1338+
\end{pmatrix}
1339+
\begin{pmatrix} \mathbf{U} \\ G \end{pmatrix}
1340+
=
1341+
\begin{pmatrix}
1342+
\nu A\mathbf{v} + \mathbf{f}(\tau_m) \\
1343+
q'(\tau_m)
1344+
\end{pmatrix}
1345+
1346+
This is **mathematically equivalent** to
1347+
:class:`stokes_poiseuille_1d_fd_diffconstr` (which reduces the same
1348+
system to a scalar Schur equation). Convergence orders are the same:
1349+
velocity :math:`M+2`, pressure :math:`M+2` (:math:`= 2M-1` for
1350+
:math:`M = 3` by coincidence).
1351+
1352+
**Block solve + projection** (``project=True``)
1353+
After the block solve, a minimal-norm correction enforces the
1354+
**original** algebraic constraint :math:`B(\mathbf{v}+\alpha\mathbf{U})
1355+
= q(\tau_m)` as well.
1356+
1357+
.. warning::
1358+
1359+
This variant gives **worse** results than the plain block solve.
1360+
The root cause is an inconsistency between ``solve_system`` (which
1361+
after projection enforces :math:`B(\mathbf{v}+\alpha\mathbf{U})
1362+
= q(t)`) and ``eval_f`` (which checks the **differentiated**
1363+
constraint :math:`B\mathbf{u}' - q'(t) = 0`). Because the
1364+
projection changes :math:`\mathbf{U}` so that
1365+
:math:`B\mathbf{U} \neq q'(t)` any more, the SDC residual
1366+
never converges cleanly and the sweep converges to a different,
1367+
lower-accuracy fixed point. Numerically, the projection variant
1368+
achieves only velocity :math:`M+1 \approx 4`, pressure
1369+
:math:`M \approx 3` — the same as the standard algebraic formulation.
1370+
1371+
The lesson is that **self-consistency between** ``solve_system`` **and**
1372+
``eval_f`` **is essential**: both must enforce the same constraint
1373+
(either the algebraic :math:`B\mathbf{u}=q` or the differentiated
1374+
:math:`B\mathbf{u}'=q'`). Mixing the two degrades convergence.
1375+
1376+
The ``eval_f`` uses the differentiated constraint
1377+
:math:`F_\text{alg} = B\mathbf{u}' - q'(t) = 0`, matching
1378+
:class:`stokes_poiseuille_1d_fd_diffconstr`.
1379+
1380+
Parameters
1381+
----------
1382+
nvars : int
1383+
Number of interior grid points (default 127; must be ≥ 5).
1384+
nu : float
1385+
Kinematic viscosity (default 1.0).
1386+
newton_tol : float
1387+
Unused; passed to base class (default 1e-10).
1388+
project : bool
1389+
If ``True``, apply the post-solve projection step that also enforces
1390+
:math:`B(\mathbf{v}+\alpha\mathbf{U}) = q(\tau_m)`. Default ``False``.
1391+
See warning above — this is provided for pedagogical comparison only.
1392+
"""
1393+
1394+
def __init__(self, nvars=127, nu=1.0, newton_tol=1e-10, project=False):
1395+
super().__init__(nvars=nvars, nu=nu, newton_tol=newton_tol)
1396+
self._makeAttributeAndRegister('project', localVars=locals(), readOnly=True)
1397+
1398+
def eval_f(self, u, du, t):
1399+
r"""
1400+
Fully-implicit DAE residual using the **differentiated constraint**:
1401+
1402+
.. math::
1403+
1404+
F_\text{diff} = \mathbf{u}' - \nu A\,\mathbf{u}
1405+
- G\,\mathbf{1} - \mathbf{f}(t),
1406+
1407+
.. math::
1408+
1409+
F_\text{alg} = B\,\mathbf{u}' - q'(t).
1410+
1411+
Identical to :class:`stokes_poiseuille_1d_fd_diffconstr`.
1412+
"""
1413+
f = self.dtype_f(self.init, val=0.0)
1414+
u_vel = np.asarray(u.diff)
1415+
du_vel = np.asarray(du.diff)
1416+
G = float(u.alg[0])
1417+
1418+
f.diff[:] = du_vel - (self.A.dot(u_vel) + G * self.ones + self._forcing(t))
1419+
f.alg[0] = self._B_dot(du_vel) - self._q_prime(t)
1420+
1421+
self.work_counters['rhs']()
1422+
return f
1423+
1424+
def solve_system(self, impl_sys, u_approx, factor, u0, t):
1425+
r"""
1426+
Coupled :math:`(N+1)\times(N+1)` block solve with the differentiated
1427+
constraint :math:`B\mathbf{U} = q'(t)`.
1428+
1429+
Optionally applies a post-solve projection onto
1430+
:math:`B(\mathbf{v}+\alpha\mathbf{U}) = q(t)` if ``self.project``
1431+
is ``True``.
1432+
1433+
Parameters
1434+
----------
1435+
impl_sys : callable
1436+
Unused; system solved directly.
1437+
u_approx : MeshDAE
1438+
Current velocity approximation at the node.
1439+
factor : float
1440+
Implicit prefactor :math:`\alpha`.
1441+
u0 : MeshDAE
1442+
Unused (direct solver).
1443+
t : float
1444+
Current time.
1445+
1446+
Returns
1447+
-------
1448+
me : MeshDAE
1449+
``me.diff[:]`` = velocity derivative :math:`\mathbf{U}_m`,
1450+
``me.alg[0]`` = pressure gradient :math:`G_m`.
1451+
"""
1452+
me = self.dtype_u(self.init, val=0.0)
1453+
v_approx = np.asarray(u_approx.diff).copy()
1454+
1455+
rhs_eff = self.A.dot(v_approx) + self._forcing(t)
1456+
U, G_new = self._coupled_block_solve(
1457+
rhs_eff, v_approx, factor, self._q_prime(t),
1458+
project=self.project,
1459+
q_val=self._q(t) if self.project else None,
1460+
)
1461+
1462+
me.diff[:] = U
1463+
me.alg[0] = G_new
1464+
return me

pySDC/playgrounds/Stokes_Poiseuille_1D_FD/run_convergence.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
stokes_poiseuille_1d_fd_lift,
9898
stokes_poiseuille_1d_fd_diffconstr,
9999
stokes_poiseuille_1d_fd_lift_diffconstr,
100+
stokes_poiseuille_1d_fd_coupled,
100101
)
101102

102103
# ---------------------------------------------------------------------------
@@ -126,10 +127,14 @@
126127
# Helpers
127128
# ---------------------------------------------------------------------------
128129

129-
def _run(problem_class, sweeper_class, dt, restol=_RESTOL, max_iter=50, nvars=_NVARS):
130+
def _run(problem_class, sweeper_class, dt, restol=_RESTOL, max_iter=50, nvars=_NVARS,
131+
extra_problem_params=None):
132+
prob_params = {'nvars': nvars, 'nu': _NU}
133+
if extra_problem_params:
134+
prob_params.update(extra_problem_params)
130135
desc = {
131136
'problem_class': problem_class,
132-
'problem_params': {'nvars': nvars, 'nu': _NU},
137+
'problem_params': prob_params,
133138
'sweeper_class': sweeper_class,
134139
'sweeper_params': _SWEEPER_PARAMS,
135140
'level_params': {'restol': restol, 'dt': dt},
@@ -241,25 +246,35 @@ def main():
241246

242247
cases = [
243248
(stokes_poiseuille_1d_fd,
244-
'Standard (B·u = q(t), algebraic constraint)'),
249+
'Standard (B·u = q(t), algebraic constraint)',
250+
{}),
245251
(stokes_poiseuille_1d_fd_lift,
246-
'Lifted (B·ṽ = 0, homogeneous constraint)'),
252+
'Lifted (B·ṽ = 0, homogeneous constraint)',
253+
{}),
247254
(stokes_poiseuille_1d_fd_diffconstr,
248-
f'Diff.constr. (B·U = q\'(t)) order M+2={mplus2} ← remedy'),
255+
f'Diff.constr. (B·U = q\'(t)) order M+2={mplus2} ← remedy',
256+
{}),
249257
(stokes_poiseuille_1d_fd_lift_diffconstr,
250-
'Lifted+diff. (B·Ũ = 0, equiv. to lifted)'),
258+
'Lifted+diff. (B·Ũ = 0, equiv. to lifted)',
259+
{}),
260+
(stokes_poiseuille_1d_fd_coupled,
261+
f'Coupled block (N+1)×(N+1) solve, no projection [≡ diffconstr]',
262+
{'project': False}),
263+
(stokes_poiseuille_1d_fd_coupled,
264+
f'Coupled block + projection B·u_m=q(τ_m) [degrades!]',
265+
{'project': True}),
251266
]
252267

253268
results = {}
254-
for cls, label in cases:
269+
for cls, label, extra_params in cases:
255270
print()
256271
print('=' * 72)
257272
print(f' {label}')
258273
print('=' * 72)
259274

260275
vel_errs, pres_errs = [], []
261276
for dt in dts:
262-
uend, P = _run(cls, SemiImplicitDAE, dt)
277+
uend, P = _run(cls, SemiImplicitDAE, dt, extra_problem_params=extra_params)
263278
ve, pe = _errors(uend, P)
264279
vel_errs.append(ve)
265280
pres_errs.append(pe)
@@ -272,7 +287,7 @@ def main():
272287
print('=' * 72)
273288
print(' Summary')
274289
print('=' * 72)
275-
for cls, label in cases:
290+
for cls, label, extra_params in cases:
276291
vel_errs, pres_errs = results[label]
277292
vel_ord = _asymptotic_order(dts, vel_errs)
278293
pres_ord = _asymptotic_order(dts, pres_errs)
@@ -304,6 +319,14 @@ def main():
304319
f' homogeneous constraint B·Ũ=0 is equivalent to the original B·ṽ=0\n'
305320
f' at the SDC fixed point; no additional improvement).'
306321
)
322+
print(
323+
f' • Coupled block (N+1)×(N+1) solve (no proj): mathematically equivalent\n'
324+
f' to diffconstr (same Schur system, different implementation). Same orders.\n'
325+
f' • Coupled + projection B·u_m=q(τ_m): DEGRADES convergence! The projection\n'
326+
f' changes U so that B·U ≠ q\'(t) any more, creating an inconsistency with\n'
327+
f' eval_f (which checks B·u\'-q\'=0). Lesson: solve_system and eval_f must\n'
328+
f' enforce the SAME constraint — mixing algebraic and differentiated degrades.'
329+
)
307330
print(
308331
f' • Spatial floor ~1e-12 (nvars={_NVARS}) limits the finest accessible Δt.'
309332
)

0 commit comments

Comments
 (0)