Skip to content

fix(function): rbf derivative path uses the requested component, not component 0 (BF-01)#312

Open
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/track0-rbf-derivative
Open

fix(function): rbf derivative path uses the requested component, not component 0 (BF-01)#312
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/track0-rbf-derivative

Conversation

@lmoresi

@lmoresi lmoresi commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Fixes BF-01 (finding LE-01) from the 2026-07 quality audit
(docs/reviews/2026-07/REMEDIATION-WORKLIST.md,
docs/reviews/2026-07/LOOSE-ENDS-AUDIT.md).

The quick/Clement derivative-evaluation path
(_clement_to_work_variable in src/underworld3/function/_function.pyx,
reached via uw.function.evaluate(expr, pts, rbf=True)) had a dead ternary,

comp = 0 if source_var.num_components == 1 else 0  # TODO: handle multi-component

which evaluates to 0 on both branches, so every derivative expression was
served the component-0 gradient: v[1].diff(x) with rbf=True silently
returned d(v[0])/dx. Per-component Clement gradients were already computed
and stored correctly, keyed (source_var, component), and every derivative
expression carries its own flat data-column index
(diffcls.component = data_loc, set at UnderworldFunction registration) —
only the retrieval discarded it. The accurate L2 path (rbf=False) was
unaffected throughout.

Reproduction (pristine source)

v = (x, 2y) on a P1 vector variable, interior points, rbf=True:

dv0/dx: expected 1.0, got 1.0        (component 0: correct)
dv1/dx: expected 0.0, got 1.0        (BUG: returns dv0/dx)
dv1/dy: expected 2.0, got ~0.0       (BUG: returns dv0/dy)

Reproducing with a P2 vector variable (the typical velocity case) exposed
two adjacent defects on the same evaluation chain, fixed here as well:

  1. The higher-degree nodal-sampling step indexed var.sym[comp, 0] with
    the flat data-column index, but sym is a (1, cdim) row matrix for
    vectors, so any multi-component P2+ variable on the mesh raised
    IndexError: Index out of range: a[1] before the ternary was reached.
  2. _evaluate_field_at_vertices (gradient_evaluation.py) assumed a
    component-blocked DOF layout for P2+ multi-component fields, but the
    PETSc Plex local vector is point-major with components interleaved per
    point — the extracted vertex values, and hence the Clement gradients,
    were garbage for every component of a P2 vector (verified: sample error
    2.0 on the unit box; de-interleaved extraction is exact, error 0.0).

Fix

  • Retrieval loop keys the gradient lookup by each expression's own
    component: gradient_at_nodes[(source_var, deriv_expr.component)].
    Correct for scalar (data_loc 0), vector (data_loc = component), and
    tensor (data_loc = flat _data_layout index) variables, matching the
    keys under which the gradients are stored.
  • Higher-degree nodal sampling evaluates the component's own applied
    function (varfn) directly instead of mis-indexing var.sym.
  • _evaluate_field_at_vertices de-interleaves the local vector before
    taking the vertex block for multi-component P2+ fields, and its
    evaluate-fallback selects the applied function by matching component
    rather than positional sym indexing.

No solver numerics are touched; the changes are confined to the rbf/Clement
evaluation path.

Tests

  • New tests/test_0507_rbf_derivative_component.py (level_1, tier_a),
    10 tests: P2 vector per-component derivatives (4 parametrized cases),
    P1 vector per-component derivatives isolating the ternary bug (4 cases),
    scalar-variable control, and a mixed-component divergence expression.
    All fail on the previous code (P2 cases by IndexError, P1 cases by wrong
    component) and pass with the fix: 10 passed in 3.29s.
  • .pyx rebuilt from a clean build/ (stale-C guard) and the behaviour
    change verified against the installed extension before trusting results.
  • Serial gate pytest -m "level_1 and tier_a" -x -q (run in three
    file-range batches), PRE fix on pristine build:
    • tests/test_0[0-4]*: 76 passed
    • tests/test_0[5-9]* (excl. new test): 151 passed, 1 skipped, 4 xfailed, 1 xpassed
    • remainder (test_1* etc. + tests/parallel): 68 passed, 6 skipped
  • POST fix (same batches, new test included): 76 passed / 161 passed
    (151 + 10 new), 1 skipped, 4 xfailed, 1 xpassed / 68 passed, 6 skipped.
    Identical outcomes to PRE apart from the 10 new passing tests.
  • MPI: not run — this item touches only the serial rbf evaluation path
    (no swarm/migration/partition-sensitive code).

Underworld development team with AI support from Claude Code

…component 0 (BF-01)

The quick/Clement derivative-evaluation path (_clement_to_work_variable,
reached via uw.function.evaluate(expr, pts, rbf=True)) contained a dead
ternary, `comp = 0 if source_var.num_components == 1 else 0`, which
evaluates to 0 on both branches. Every derivative expression was therefore
served the component-0 gradient: v[1].diff(x) with rbf=True silently
returned d(v[0])/dx. Per-component Clement gradients were already computed
and stored keyed (source_var, component), and each derivative expression
carries its own flat data-column index (diffcls.component, set at
UnderworldFunction registration); only the retrieval discarded it. The
retrieval loop now keys the gradient lookup by deriv_expr.component, which
is correct for scalar, vector, and tensor variables alike.

Reproducing the bug with a P2 vector variable exposed two adjacent defects
on the same path, fixed here as well. First, the higher-degree nodal
sampling step indexed var.sym[comp, 0] with the flat data-column index,
but sym is a (1, cdim) row matrix for vectors, so any multi-component P2+
variable on the mesh raised IndexError before the ternary was reached; the
sampling now evaluates the component's own applied function directly.
Second, _evaluate_field_at_vertices assumed a component-blocked DOF layout
for P2+ multi-component fields, but the PETSc Plex local vector is
point-major with components interleaved per point, so the extracted vertex
values (and hence the Clement gradients) were garbage for every component;
the extraction now de-interleaves before taking the vertex block, verified
exact against analytic fields.

The accurate L2 projection path (rbf=False) was unaffected throughout, and
no solver numerics are touched.

Regression test tests/test_0507_rbf_derivative_component.py (level_1,
tier_a) evaluates each component derivative of v = (x, 2y) on P2 and P1
vector variables, a scalar control, and a mixed-component divergence
expression; all ten tests fail on the previous code and pass with the fix.

Audit reference: docs/reviews/2026-07/REMEDIATION-WORKLIST.md (BF-01),
LOOSE-ENDS-AUDIT.md (LE-01).

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings July 3, 2026 23:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a long-standing correctness issue in the rbf=True (Clement / “quick”) derivative-evaluation path where derivative expressions for multi-component variables could incorrectly use component 0’s gradient (and, for P2+ vector fields, could error or produce garbage due to incorrect DOF/component handling). The changes align derivative-gradient retrieval with each derivative expression’s registered flat component index and correct vertex sampling for multi-component higher-degree fields.

Changes:

  • Use each derivative expression’s own component (flat data-column index) when retrieving per-component Clement gradients in _clement_to_work_variable.
  • Fix higher-degree nodal sampling by evaluating the component’s applied function directly instead of mis-indexing var.sym.
  • Correct _evaluate_field_at_vertices for multi-component P2+ fields by de-interleaving the PETSc local vector and improving fallback component selection; add a regression test suite covering P1/P2 vector derivatives and mixed-component expressions.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/underworld3/function/_function.pyx Fixes derivative-gradient lookup to use deriv_expr.component and avoids incorrect var.sym indexing for higher-degree nodal sampling.
src/underworld3/function/gradient_evaluation.py Corrects P2+ multi-component vertex extraction to match Plex point-major interleaved layout and improves fallback component selection.
tests/test_0507_rbf_derivative_component.py Adds regression coverage for per-component derivatives (P1 + P2), scalar control, and mixed-component expressions under rbf=True.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants