Implement tensor evaluate path in _project_to_work_variable (#180 followup)#185
Implement tensor evaluate path in _project_to_work_variable (#180 followup)#185lmoresi wants to merge 1 commit into
Conversation
_project_to_work_variable's matrix-expression branch was dead code with
two bugs marked by a TODO:
1. MeshVariable(num_components=rows*cols) (flat int) couldn't infer
vtype, raising ValueError on every call.
2. SNES_Projection(scalar_component=c) — that kwarg doesn't exist on
SNES_Projection.__init__; the loop would have raised TypeError if
it ever reached.
So evaluate(strain_tensor, pts) — or any tensor expression containing
derivatives — bounced out via the ValueError, and callers like
SemiLagrangian.update_pre_solve hit their outer ``except Exception``
and fell back to their own projection solver. After PR #183 the DDt
fallback works correctly, but the wasted try-fail-fallback was running
every NS step and the spurious ValueError polluted stack traces.
Replace the dead block with a real multi-component projection:
- Cache a flat (1, Nc) MATRIX work var as the projector target (mirrors
the existing pattern in DDt._setup_projections for SYM_TENSOR/TENSOR).
- Use SNES_MultiComponent_Projection (one solve, shared DM) instead of
a per-component cycling loop.
- Cache a tensor-shaped (rows, cols) work var so the caller's
``work_var.sym`` preserves the input expression's shape.
- After solve, fan flat var values into the tensor work var.
Adds tests/test_0506_tensor_evaluate.py covering:
- evaluate(strain_tensor, pts) returns (n_pts, dim, dim) shape
- A known shear field produces the expected non-zero strain entries
- A NavierStokes solve completes without entering the DDt fallback
Related: issue #180, PR #183 (which fixes the fallback path so it works
correctly when other expressions still hit it).
Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
This PR fixes the tensor/matrix projection path used by uw.function.evaluate() when evaluating expressions containing derivatives, by replacing previously unreachable/broken code in _project_to_work_variable with a working multi-component projection approach. It also adds regression tests for tensor evaluation (strain tensor) and a NavierStokes solve scenario related to issue #180.
Changes:
- Replace dead/broken matrix-projection logic in
_project_to_work_variablewith aSNES_MultiComponent_Projection-based implementation using a flat(1, Nc)work variable and a tensor-shaped result variable. - Cache tensor-shaped, flat, and projector objects on the mesh keyed by tensor shape.
- Add new tests covering tensor-valued derivative evaluation and a NavierStokes solve regression.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/underworld3/function/_function.pyx |
Implements working tensor/matrix projection via a cached flat (1, Nc) multicomponent projector plus fan-out to a tensor-shaped work variable. |
tests/test_0506_tensor_evaluate.py |
Adds regression tests for tensor evaluate shape/values and a NavierStokes solve scenario tied to the derivative-evaluate projection path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Build flat row-matrix source: [[expr[0,0], expr[0,1], ..., expr[r-1,c-1]]] | ||
| flat_source = sympy.Matrix( | ||
| [[expr[i, j] for i in range(rows) for j in range(cols)]] | ||
| ) | ||
| projector.uw_function = flat_source |
| def test_navier_stokes_solve_does_not_trigger_ddt_fallback(): | ||
| """A NavierStokes solve completes without entering the DDt projection fallback. | ||
|
|
||
| The DDt fallback in ``update_pre_solve`` exists for expressions where | ||
| ``uw.function.evaluate`` raises. Once tensor evaluate works, the | ||
| fallback should not be needed for the NS viscous flux. |
Review pass — ready to mergeScope: Verification:
Copilot's two notes:
Together with #183, this closes out #180. #183 hardens the fallback path; this PR removes the reason the fallback gets entered. Belt + braces. LGTM modulo standard maintainer eyeball. Underworld development team with AI support from Claude Code |
Summary
Replaces dead matrix-projection code in
_project_to_work_variable(_function.pyx) souw.function.evaluateworks directly on tensor expressions containing derivatives (e.g. strain rate, NS viscous flux). Follow-on to #183 — together they fix issue #180 from both sides:The bugs (both marked by a pre-existing
TODO(BUG))MeshVariable(num_components=rows*cols)(flat int) — base class can't infer vtype, raisesValueErroron every call. This is what Mohammed sees in his trace.SNES_Projection(scalar_component=c)— that keyword doesn't exist onSNES_Projection.__init__; the loop would haveTypeErrorif it ever reached.So the entire matrix branch was unreachable. Every tensor-evaluate call (e.g.
evaluate(NS.flux, coords)) bounced out via bug 1 and callers likeSemiLagrangian.update_pre_solvefell back to their own projector.Fix
Replaces the dead branch with a real multi-component projection that mirrors the pattern already used in
DDt._setup_projections:(1, Nc) MATRIXwork var as the projector target.SNES_MultiComponent_Projection(one solve, shared DM) instead of per-component cycling.(rows, cols)work var so the caller'swork_var.symkeeps the original shape.Tests
tests/test_0506_tensor_evaluate.py(3 tests):evaluate(strain_tensor, pts)returns shape(n_pts, dim, dim)— rotation field gives zero strain (correct).Test plan
pytest tests/test_0506_tensor_evaluate.py— 3 passedpytest tests/test_0503_evaluate.py tests/test_0503_evaluate2.py tests/test_0504_projections.py— 18 passed (no regression on existing evaluate paths)_project_to_work_variableis now reached and succeeds with(2, 2)shaped tensors on a NS solvemesh._eval_work_<rows>x<cols>etc.) is OK for long-running modelsUnderworld development team with AI support from Claude Code