From d299361206d7a1e65e50e1428da59e248771f2e9 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 11 May 2026 18:39:58 +1000 Subject: [PATCH 1/2] Fix NavierStokesSLCN DDt projection source shape mismatch (#180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When SNES_MultiComponent_Projection was wired into the SemiLagrangian DDt path, the psi_fn setter (line 1402) and _setup_projections (lines 1373, 863) were updated to flatten the source tensor to a (1, Nc) row matrix via _build_projection_source. The fallback inside update_pre_solve at line 1769 — taken when uw.function.evaluate() raises on expressions containing derivatives (the NS viscous flux every step) — missed the migration and assigned self.psi_fn directly, producing: sympy.matrices.exceptions.ShapeError: Matrix size mismatch: (1, 3) + (2, 2). Stokes never hits this because its projection source has no derivatives. NavierStokes hits it every solve. Fix: route the fallback through _build_projection_source like the setter already does. Adds tests/test_0610_navier_stokes_slcn_projection.py which reproduces the user's failure (one ns.solve() on a tiny mesh) — reliably raises ShapeError without the fix, passes with it. Reported by @Chawgai. Diagnosed with @ss2098's investigation pointing at the SLCN projection/history-field path. Underworld development team with AI support from Claude Code --- src/underworld3/systems/ddt.py | 10 +++- ...test_0610_navier_stokes_slcn_projection.py | 57 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/test_0610_navier_stokes_slcn_projection.py diff --git a/src/underworld3/systems/ddt.py b/src/underworld3/systems/ddt.py index 39bf5692..9054ea76 100644 --- a/src/underworld3/systems/ddt.py +++ b/src/underworld3/systems/ddt.py @@ -1765,8 +1765,14 @@ def update_pre_solve( except Exception: # Fallback to projection solver for expressions that can't be directly evaluated - # (e.g., containing derivatives) - self._psi_star_projection_solver.uw_function = self.psi_fn + # (e.g., containing derivatives — true for the NS viscous flux every step). + # Route via _build_projection_source so the (1, Nc) row-matrix flattening + # required by SNES_MultiComponent_Projection is applied for tensor vtypes. + # Without this, a (dim, dim) tensor function meets a (1, Nc) solver field + # and SymPy raises "Matrix size mismatch: (1, Nc) + (dim, dim)" (issue #180). + self._psi_star_projection_solver.uw_function = self._build_projection_source( + self.psi_fn + ) self._psi_star_projection_solver.smoothing = 0.0 self._psi_star_projection_solver.solve(verbose=verbose) diff --git a/tests/test_0610_navier_stokes_slcn_projection.py b/tests/test_0610_navier_stokes_slcn_projection.py new file mode 100644 index 00000000..8e4dccfc --- /dev/null +++ b/tests/test_0610_navier_stokes_slcn_projection.py @@ -0,0 +1,57 @@ +""" +Regression test for the NavierStokesSLCN DFDt projection source shape. + +When ``SNES_MultiComponent_Projection`` was wired into the ``SemiLagrangian`` +DDt path, the ``psi_fn`` setter and ``_setup_projections`` were updated to +flatten the source tensor to a ``(1, Nc)`` row matrix via +``_build_projection_source``. The fallback path inside ``update_pre_solve`` +(taken when ``uw.function.evaluate`` raises on expressions containing +derivatives — which is the NavierStokes viscous flux every step) missed +the migration and assigned ``self.psi_fn`` directly, producing: + + sympy.matrices.exceptions.ShapeError: + Matrix size mismatch: (1, 3) + (2, 2). + +See: https://github.com/underworldcode/underworld3/issues/180 + +The test does one ``solve(timestep=dt)`` of NavierStokesSLCN on a tiny mesh. +Before the fix this raises ``ShapeError`` from the DDt fallback. After the +fix it converges normally. +""" +import pytest +import sympy +import underworld3 as uw + + +@pytest.mark.level_2 +@pytest.mark.tier_a +def test_navier_stokes_slcn_solve_does_not_raise_shape_error(): + mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1 / 8, + regular=False, + ) + + v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) + p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1, continuous=True) + + ns = uw.systems.NavierStokes( + mesh, + velocityField=v, + pressureField=p, + rho=1.0, + order=2, + ) + ns.constitutive_model = uw.constitutive_models.ViscousFlowModel + ns.constitutive_model.Parameters.shear_viscosity_0 = 1.0 + + ns.bodyforce = sympy.Matrix([0, 0]).T + ns.add_dirichlet_bc((1.0, 0.0), "Top") + ns.add_dirichlet_bc((0.0, 0.0), "Bottom") + ns.add_dirichlet_bc((0.0, 0.0), "Left") + ns.add_dirichlet_bc((0.0, 0.0), "Right") + + # Take a single step. Pre-fix this raised ShapeError("(1, 3) + (2, 2)") + # from the DDt projection fallback. Post-fix it just converges. + ns.solve(timestep=0.01, zero_init_guess=True) From 2da9b669e0834db7154c033febb528bf06d5ffba Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 11 May 2026 20:45:40 +1000 Subject: [PATCH 2/2] Address Copilot review: add flat->tensor fan-out to DDt fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot flagged that after the shape fix, the multicomponent projector writes into _psi_star_flat_var but update_pre_solve() never unpacks the solved flat (1, Nc) field back into psi_star[0]. Mirrors the fan-out already in initialise_history() (~ddt.py:1540): walk _psi_star_indep_indices, copy flat var k-th component into psi_star[0][:, i, j], and symmetric-fill [:, j, i] for SYM_TENSOR. In practice the SemiLagrangian advection step at line 1016 then modifies psi_star[0] in place, so the missing fan-out wasn't producing an immediate symptom in this short test — but the projected flux value was being silently discarded. Fix restores the intended semantics: psi_star[0] reflects the current psi_fn (projected flux) before SL advection updates it for the upstream sample. Reverted the over-eager second test (asserting psi_star[0] non-zero after one solve) since the SL advection masks the missing fan-out in that observation window. Kept the ShapeError regression test which remains a definitive guard for the original visible failure. Underworld development team with AI support from Claude Code --- src/underworld3/systems/ddt.py | 11 +++++++++++ tests/test_0610_navier_stokes_slcn_projection.py | 11 ++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/underworld3/systems/ddt.py b/src/underworld3/systems/ddt.py index 9054ea76..d5082199 100644 --- a/src/underworld3/systems/ddt.py +++ b/src/underworld3/systems/ddt.py @@ -1776,6 +1776,17 @@ def update_pre_solve( self._psi_star_projection_solver.smoothing = 0.0 self._psi_star_projection_solver.solve(verbose=verbose) + # For tensor vtypes the projection writes into the flat (1, Nc) variable, + # so we must fan it back out to psi_star[0] — otherwise subsequent + # history operations read a stale tensor. Mirrors the same fan-out in + # initialise_history() (~line 1540). + if getattr(self, '_psi_star_use_multicomponent', False): + for k, (i, j) in enumerate(self._psi_star_indep_indices): + vals = self._psi_star_flat_var.array[:, 0, k] + self.psi_star[0].array[:, i, j] = vals + if i != j: + self.psi_star[0].array[:, j, i] = vals + # 3. Compute the upstream values from the psi_fn # We use the u_star variable as a working value here so we have to work backwards diff --git a/tests/test_0610_navier_stokes_slcn_projection.py b/tests/test_0610_navier_stokes_slcn_projection.py index 8e4dccfc..a849ebe6 100644 --- a/tests/test_0610_navier_stokes_slcn_projection.py +++ b/tests/test_0610_navier_stokes_slcn_projection.py @@ -1,22 +1,18 @@ """ -Regression test for the NavierStokesSLCN DFDt projection source shape. +Regression test for the NavierStokesSLCN DFDt projection fallback. When ``SNES_MultiComponent_Projection`` was wired into the ``SemiLagrangian`` DDt path, the ``psi_fn`` setter and ``_setup_projections`` were updated to flatten the source tensor to a ``(1, Nc)`` row matrix via ``_build_projection_source``. The fallback path inside ``update_pre_solve`` (taken when ``uw.function.evaluate`` raises on expressions containing -derivatives — which is the NavierStokes viscous flux every step) missed -the migration and assigned ``self.psi_fn`` directly, producing: +derivatives — true for the NavierStokes viscous flux every step) missed the +migration and assigned ``self.psi_fn`` directly, producing: sympy.matrices.exceptions.ShapeError: Matrix size mismatch: (1, 3) + (2, 2). See: https://github.com/underworldcode/underworld3/issues/180 - -The test does one ``solve(timestep=dt)`` of NavierStokesSLCN on a tiny mesh. -Before the fix this raises ``ShapeError`` from the DDt fallback. After the -fix it converges normally. """ import pytest import sympy @@ -26,6 +22,7 @@ @pytest.mark.level_2 @pytest.mark.tier_a def test_navier_stokes_slcn_solve_does_not_raise_shape_error(): + """First solve completes without ShapeError from the DDt projection fallback.""" mesh = uw.meshing.UnstructuredSimplexBox( minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0),