halo_width migration guide (v0.2 → v0.3.0)#
Changed in version v0.2: Per-axis halo widths via
SimulationNode.halo_width(),
returning a dict[int, int] ({axis: width}). Pointwise nodes
return {}; stencil nodes return a non-empty dict. The legacy
boolean requires_halo property was retained as a derived
fallback (bool(self.halo_width())) and any subclass overriding
requires_halo directly emitted a
FutureWarning at class-definition time.
Changed in version v0.3.0: The requires_halo property and the __init_subclass__ compat
shim were removed. Subclasses that override requires_halo
instead of halo_width now raise
MigrationError at class-definition time.
Why this changed#
Pencil decomposition (the Cartesian multi-GPU sharding path,
ShardedStencilNode,
added in v0.2.1) needs to know each axis’s halo width, not just
“do you need a halo at all”: it pads every sharded axis by that
axis’s width on each side, [halo | interior | halo], before
update_padded runs. The boolean was load-bearing
once — when stencils were 1-D and one axis per node — but it
silently lost information the moment we shipped 3-D LBM.
ShardedUnstructuredNode
(added in v0.3.0) takes no halo width from the node. A node’s
neighbours there are the partition layout’s ghost cells, handed to
update_padded as [the shard's own cells | ghost cells], so a node
written for it declares halo_width() == {}. Since v0.4.0 that
wrapper refuses a node that declares a non-empty halo_width(): that
is the Cartesian contract, and such a node would read the partition
layout as the wrong cells. Shard a stencil node with
ShardedStencilNode. (Until v0.4.0 this guide said the unstructured
wrapper needed each axis’s halo width too.) The
sharding topology guide sets the two layouts
side by side.
The v0.2 release kept the boolean as a derived fallback for
back-compat and emitted a FutureWarning. v0.3.0 closes
the deprecation cycle.
What to do#
If you override halo_width already#
Nothing. You already write to the new API; the v0.3.0 hard-removal doesn’t affect you.
If you override requires_halo only#
Replace the property override with a halo_width() method:
class MyStencilNode(SimulationNode):
@property
def requires_halo(self) -> bool:
return True
def update_padded(self, state_padded, boundary_inputs, dt, **kwargs):
...
class MyStencilNode(SimulationNode):
def halo_width(self) -> dict[int, int]:
# axis → cells per side. Two-cell halo on axis 0 (a 5-point
# stencil), one-cell halo on axis 1 (a 3-point stencil).
return {0: 2, 1: 1}
def update_padded(self, state_padded, boundary_inputs, dt, **kwargs):
...
Pointwise nodes#
Pointwise nodes (no spatial neighbour access) return the empty dict:
class MyPointwiseNode(SimulationNode):
def halo_width(self) -> dict[int, int]:
return {}
If your node never overrode halo_width or requires_halo, the
base class already returns {} and you don’t need to do anything.
A node written for ShardedUnstructuredNode returns {} too, even
though its update_padded reads neighbours: they arrive as the
partition layout’s ghost cells, not as a per-axis halo (see above).
Auto-migration tooling#
The MigrationError raised at class-
definition time carries structured detail rather than just a string
message — err.api_name, err.affected_class, err.replacement,
err.migration_guide. Tools can introspect these without grepping
the message:
import importlib, traceback
from maddening.warnings import MigrationError
try:
importlib.import_module("my_pkg.legacy_node")
except MigrationError as err:
print(f"Migrate {err.affected_class.__qualname__}: "
f"replace requires_halo with {err.replacement}")
Detecting affected subclasses#
A grep over downstream packages catches most cases. For each match,
either replace with halo_width or delete the override entirely if
the parent class already returns the right value:
$ grep -rn "def requires_halo\|requires_halo =\|@property" --include="*.py" | grep -A1 "requires_halo"
MIME’s CI runs a grep gate on this.