|
141 | 141 |
|
142 | 142 | stokes_poiseuille_1d_fd_lift_full |
143 | 143 | 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. |
144 | 154 | """ |
145 | 155 |
|
146 | 156 | import numpy as np |
@@ -420,6 +430,116 @@ def _schur_solve_full_implicit(self, rhs_eff, v_approx, factor, constraint_rhs): |
420 | 430 | U = w + factor * G_prime * v0 |
421 | 431 | return U, float(G_prime) |
422 | 432 |
|
| 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 | + |
423 | 543 | def _schur_solve_diffconstr(self, rhs_eff, factor, q_prime_val): |
424 | 544 | r""" |
425 | 545 | Schur-complement solve using the **differentiated constraint** |
@@ -1193,3 +1313,152 @@ def solve_system(self, impl_sys, u_approx, factor, u0, t): |
1193 | 1313 | me.diff[:] = U |
1194 | 1314 | me.alg[0] = G_new |
1195 | 1315 | 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 |
0 commit comments