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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions