Skip to content

swarm.advection() leaves proxy mesh variables stale (frozen material) — auto-update hook orphaned in deprecated access path #289

Description

@lmoresi

Summary

swarm.advection() moves the particles but does not mark the swarm-variable proxy
mesh variables stale
, so the lazy .sym accessor never rebuilds them. The Stokes/
energy solver on the next step therefore reads a frozen proxy (composition,
density, viscosity) projected from the previous particle positions. The material is
dynamically stationary even though the particles are advecting — silently producing
wrong physics (e.g. a buoyant plume that never rises; a Rayleigh–Taylor interface that
never evolves).

This affects the standard advect-then-use-.sym pattern used by the shipped
examples docs/examples/fluid_mechanics/intermediate/Ex_Stokes_Swarm_RT_Cartesian.py
and docs/examples/convection/intermediate/Ex_Convection_Cartesian_ThermoChem.py,
which call swarm.advection(...) and rely on material.sym being current on the next
solve, with no explicit _update().

Minimal reproducer

import underworld3 as uw, numpy as np, sympy
mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(0,0), maxCoords=(1,0.25), cellSize=1/48, qdegree=3)
v = uw.discretisation.MeshVariable("V", mesh, vtype=uw.VarType.VECTOR, degree=2)
swarm = uw.swarm.Swarm(mesh=mesh)
mat = uw.swarm.IndexSwarmVariable("M", swarm, indices=2, proxy_degree=1, proxy_continuous=True)
swarm.populate(fill_param=4)
mat.data[...] = 0
pc = swarm.data
mat.data[(pc[:,0]-0.5)**2 + (pc[:,1]-0.107)**2 < 0.0179**2, 0] = 1
v.data[:,1] = 0.01                                   # uniform upward velocity

def particle_cy(): m = mat.data[:,0] > 0.5; return swarm.data[m,1].mean()
def proxy_cy():
    pv = np.asarray(uw.function.evaluate(mat.sym[1], v.coords)).reshape(-1)
    w = np.clip(pv,0,None); return (w*v.coords[:,1]).sum()/w.sum()

print("before:", particle_cy(), proxy_cy(), "stale=", mat._proxy_stale)
swarm.advection(v.sym, 2.0, order=2, corrector=True)
print("after :", particle_cy(), "_proxy_stale=", mat._proxy_stale)
print("proxy_cy (lazy):", proxy_cy())
mat._update()
print("proxy_cy after _update():", proxy_cy())

Output:

before: 0.1071 0.1070 stale= False
after : 0.1271 _proxy_stale= False     # particles moved 0.02 up; proxy NOT marked stale
proxy_cy (lazy): 0.1070                 # FROZEN at the initial position
proxy_cy after _update(): 0.1271        # explicit _update() fixes it

The particle centroid moves (0.107 → 0.127); the proxy stays at 0.107; _proxy_stale
is False after advection, so .sym returns the stale projection.

Root cause

The proxy auto-refresh contract is wired into the deprecated swarm.access()
context manager: access.__exit__ calls var._update() for all vars when the particle
coordinates were writeable —
src/underworld3/swarms/pic_swarm.py:994–1001 ("if swarm migrated, update all").
But after the c52201b8 remesh field-transfer redesign, _proxy_stale=True is set
only by the remesh/adapt path (_remesh_reinit_callback,
src/underworld3/swarm.py:1057), not by ordinary advection. The lazy .sym gate
(if self._proxy_stale: _update_proxy_variables(), swarm.py:2197) is therefore never
triggered by advection(). Net: the staleness hook that advection depends on lives in
the now-deprecated access machinery and is effectively orphaned for the advection code
path (proven above: _proxy_stale stays False).

Impact

  • Silent correctness failure in any swarm-advection model whose buoyancy/viscosity/
    composition is read from a proxy (material.sym[i]) — the dominant UW3 idiom.
  • The looks-plausible failure mode is a model that reaches a spurious "steady state"
    (the material is frozen) — easy to misread as physics.
  • Affects shipped examples (RT, ThermoChem) and the documented pattern.

Workaround

Call material._update() (the SwarmVariable/IndexSwarmVariable, not the swarm)
immediately after every swarm.advection(...):

swarm.advection(v.sym, dt, order=2, corrector=True)
material._update()        # refresh proxy so the next solve sees the moved material

Suggested fix

Mark all proxies stale (or eagerly _update() them) inside swarm.advection()
after the particles are migrated, independent of the deprecated access context
manager — e.g. at the end of advection():

for var in self.vars.values():
    var._update()      # or: if var._proxy: var._proxy_stale = True

This re-establishes the auto-update contract the examples rely on, and decouples it from
the deprecated access().__exit__ path. A regression test that advects a uniform
velocity and asserts the proxy centroid tracks the particle centroid would guard it.


Found while reproducing the Crameri (2012) free-surface plume benchmark; UW3 build on
feature/integrate-surface-submesh (commit ~a01e79f3).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions