Skip to content

Pack and index auxiliary fields by DM field, not by position in mesh.vars - #676

Open
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/aux-field-layout-orphaned-variables
Open

Pack and index auxiliary fields by DM field, not by position in mesh.vars#676
lmoresi wants to merge 1 commit into
developmentfrom
bugfix/aux-field-layout-orphaned-variables

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 3, 2026

Copy link
Copy Markdown
Member

Extracted from #673 so it can land ahead of the solver work. Nothing here depends on that PR. The defect surfaced while testing the Eulerian SUPG solver that grew out of @NengLu's contribution in issue #657; the P0 cell-size field that solver reads is what made the mis-packing visible.

The defect

mesh.vars holds variables weakly. The only strong reference to a MeshVariable is the default Model's registry, and the mesh outlives the model it was created under, so uw.reset_default_model() (the test suite does it between tests) releases every variable a script no longer names; the variable-statistics helpers also delete temporaries from the registry on purpose. A released variable is garbage-collected, but a DMPlex cannot shed a field, so its PETSc field stays in the DM.

Two places assumed the registry and the DM's field list line up by position:

  • Mesh.update_lvec zipped mesh.vars.values() against dm.createFieldDecomposition(), so every variable after the orphan was packed into the wrong field and its own slot kept whatever it held;
  • the JIT's petsc_a[] / petsc_a_x[] offsets were a running count over the live variables, skipping the orphan's components.

Measured: a DM with seven fields and a registry with three; a P0 cell-size field landing in a P2 slot as garbage (values of order 1.7); a solver giving DIVERGED_FUNCTION_NANORINF at iteration zero in one run and a subtly wrong answer (1e-4) in the next, depending on when the collector ran. It surfaced as a flaky test that only failed after a test which dropped variables.

The fix

update_lvec packs by field name and zeroes an orphaned field so nothing stale reaches a kernel. _jitextension._aux_component_offsets(mesh) reads component offsets from the DM's own field list and ccode_patch_fns patches each variable from its field_id.

Test

tests/test_1058_dropped_meshvariable_aux_layout.py (level_1, tier_a, in the test_105* CI batch): the premise (a dropped variable leaves an orphaned field), a Poisson solve with a field coefficient after two dropped variables matching a clean mesh to 1e-10, and the packed aux vector landing in the named fields. Two of the three checks fail with the fix reverted. Core batches test_00xx to test_02xx pass with the fix (398 tests).

Not changed: the DM still grows by one field per dropped variable for the life of the mesh. That is the pre-existing memory behaviour, now merely correct.

Underworld development team with AI support from Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL

…vars

A MeshVariable that is dropped and garbage-collected (the default Model
holds the only strong reference; uw.reset_default_model() releases it, and
the statistics helpers delete temporaries deliberately) leaves its PETSc
field in the DM. Mesh.update_lvec zipped mesh.vars.values() against the
field decomposition by position, and the JIT's petsc_a[] offsets were a
running count over the live variables, so every later variable was packed
into, and read from, the wrong slots. Measured: a P0 cell-size field landing
in a P2 slot as garbage, NaN residuals in one run and a subtly wrong answer
in the next, depending on when the collector ran.

update_lvec now packs by field name and zeroes an orphaned field; the JIT
reads component offsets from the DM's own field list and patches each
variable from its field_id. Regression test: 2 of its 3 checks fail without
the fix.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL
(cherry picked from commit e84dea9)

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.

🔵 Needs a closer look

It changes core mesh/JIT packing logic used broadly by solvers, so it warrants final human review despite strong test coverage.

Pull request overview

This PR fixes a correctness defect in how auxiliary mesh fields are packed/indexed for PETSc DMs when MeshVariable instances are dropped (garbage-collected) but their PETSc fields remain in the DM. The change makes auxiliary layout robust to “orphaned” DM fields, preventing stale/wrong data from being fed into assembled kernels and subsequent solver instability or silent wrong answers.

Changes:

  • Pack Mesh.update_lvec() by DM field name (and zero orphaned fields) instead of zipping by mesh.vars position.
  • Derive JIT auxiliary-component offsets from the DM’s field list (via field_id) rather than counting only live mesh.vars.
  • Add a level_1 / tier_a regression test that reproduces the orphan-field scenario and validates solve equivalence + packing correctness.
File summaries
File Description
src/underworld3/discretisation/discretisation_mesh.py Updates update_lvec() to pack aux data by DM field name and zero orphan fields to avoid stale data propagation.
src/underworld3/utilities/_jitextension.py Computes aux component offsets from the DM’s fields and patches each variable using its field_id-based offset.
tests/test_1058_dropped_meshvariable_aux_layout.py Adds regression coverage for dropped-variable orphan fields affecting aux layout and downstream solves.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 863 to 867
varlist: list
The variables to patch. Note that *all* the variables in the
corresponding `PetscDM` must be included. They must also be
ordered according to their `field_id`.
prefix_str: str
Comment on lines +26 to +32
import gc

import numpy as np
import pytest
import sympy

import underworld3 as uw
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