From 6a7e9226dba2842680558782eddeb50eead848b8 Mon Sep 17 00:00:00 2001 From: TRON Date: Fri, 16 Oct 2026 10:41:56 -0700 Subject: [PATCH] Add bounded pinned-circle manual L2 remapping --- docs/synchronized_leaf_remap.md | 39 ++++++ src/CelerisLab/lbm/refinement_stepper.py | 48 +++++-- src/CelerisLab/lbm/synchronized_remap.py | 79 +++++++++-- tests/unit/test_pinned_body_remap.py | 130 ++++++++++++++++++ tests/unit/test_synchronized_remap.py | 2 +- .../multigrid/test_pinned_body_remap_gpu.py | 84 +++++++++++ 6 files changed, 361 insertions(+), 21 deletions(-) create mode 100644 tests/unit/test_pinned_body_remap.py create mode 100644 tests/validation/multigrid/test_pinned_body_remap_gpu.py diff --git a/docs/synchronized_leaf_remap.md b/docs/synchronized_leaf_remap.md index dede90e..f4f4523 100644 --- a/docs/synchronized_leaf_remap.md +++ b/docs/synchronized_leaf_remap.md @@ -109,3 +109,42 @@ finite leaves, exact masks and absolute counts. They were not run on the CPU host. The separate independent FP64 capture/replay gate is max population error **3e-6**, with exact masks; remap mass/momentum allow FP32 roundoff. Per-population conservation is not an advancement target under collision. + +### Bounded pinned-body extension + +`replacement = current.remap_child(rectangles, child_objects=original_objects)` +now also accepts exactly one stationary Circle in one of exactly two L2 +rectangles. Its **complete containing rectangle must remain identical**; +only the separated fluid rectangle may change. Existing nesting, four-cell +gap, full-circle containment, reverse cut-link coverage and two-hop BFL +transfer-footprint checks still apply. This is manual remapping, not adaptive +body tracking or runtime body editing. + +Supply the original root edge-frame definition used at construction. Do not +pass `current.child_boundary.objects`: those are projected fine-local copies. +There is no retained mutable original-object registry. Before reading device +state, remapping reprojects the supplied definition onto the old topology and +compares flags, physical cut links, body IDs, q, lever arms, hits, BFL support, +legacy SurfaceActuation specification and current per-link velocity against +the existing boundary. Omission, changed geometry/actuation and changed body +rectangle fail without device reads or allocations. Root object IDs are still +assigned by list position, as in the constructor. + +Jets are taken from the live L2 boundary, including the most recent successful +`set_jet_amplitude(..., level=2)`. There is no replacement-Jets argument or +constructor-time Jet snapshot. New blocked addresses may differ; the physical +boundary and velocities must match exactly. + +`RemapTopology.from_config(..., child_objects=..., child_jets=...)` prepares a +typed `FineBoundary2D`; it derives L2 fluid leaves as ACTIVE minus SOLID. No +caller mask is accepted. Solids, ghosts and holes are never donors and remain +NaN, physical-fluid overlap is exact, and exposed L1 still restricts all four +old fluid children. The helper checks the old/new body pin and boundary too. + +Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`. GPU declarations: +`tests/validation/multigrid/test_pinned_body_remap_gpu.py`, covering no-op +next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation, +physical boundary/velocity equality, source independence and absolute counts. +These GPU cases have **not been run here**. They do not establish evolution +accuracy. A 3e-6 FP32-vs-FP64 evolution gate requires a separate independent +BFL reference; the body-free replay gate above does not establish that claim. diff --git a/src/CelerisLab/lbm/refinement_stepper.py b/src/CelerisLab/lbm/refinement_stepper.py index 55b8260..846b5be 100644 --- a/src/CelerisLab/lbm/refinement_stepper.py +++ b/src/CelerisLab/lbm/refinement_stepper.py @@ -3,7 +3,7 @@ The caller owns the CudaContext and must close this backend before that context. Only synchronized postcollision_g leaf populations are public. No checkpoint, -forcing or runtime parameter mutation is offered. Manual body-free periodic +forcing or runtime parameter mutation is offered. Bounded manual periodic L2 set replacement is available at completed base intervals via remap_child. """ from dataclasses import dataclass, replace @@ -494,29 +494,54 @@ class FixedPatchStepper: self._status = "failed" raise - def remap_child(self, rectangles): + def remap_child(self, rectangles, *, child_objects=()): """Return a NEW ready stepper with an L2 set replaced at this base time. - Only body-free periodic three-level set children are supported; L0/L1 - and block sizes stay fixed. The lock and stream synchronization certify + Periodic three-level set children only; L0/L1 and block sizes stay fixed. + A body run requires its original root-edge-frame ``child_objects``: + one stationary circle pinned in one of exactly two L2 rectangles. + The complete body rectangle stays identical; only the other fluid + rectangle may move. Current L2 Jets (including updated amplitudes) + are preserved. Local projected boundary objects are not valid inputs. + The lock and stream synchronization certify current postcollision G and the completed 1:2:4 counts internally. Both objects remain independently owned: the caller must close each, and chooses when to stop using the unchanged old instance. Candidate construction/upload errors never invalidate the old instance; failure to synchronize/read its device state does, as with step/download. """ - from .synchronized_remap import RemapTopology, remap_synchronized + from .synchronized_remap import (RemapTopology, remap_synchronized, + same_physical_boundary, boundary_velocity) with self._lock: self._require("ready") if (self._wall_aware or len(self._levels) != 3 or not isinstance(self.child_patch, FixedPatchSet2D) - or self.fine_boundary is not None or self.child_boundary is not None - or any(level.boundary is not None for level in self._levels)): - raise ValueError("remap_child requires body-free periodic three-level L2 sets") + or self.fine_boundary is not None + or any(level.boundary is not None for level in self._levels[:2])): + raise ValueError("remap_child requires periodic three-level L2 sets with no L0/L1 bodies") cfg = self._remap_cfg - old = RemapTopology.from_config(cfg, self.patch, self.child_patch.bounds) - new = RemapTopology.from_config(cfg, self.patch, rectangles) + child_objects = tuple(child_objects) + actual = self._boundaries[2] + jets = () if actual is None else actual.jets + old = RemapTopology.from_config(cfg, self.patch, self.child_patch.bounds, + child_objects=child_objects, child_jets=jets) + if (not same_physical_boundary(old.boundary, self.child_boundary) + or (self.child_boundary is not None and actual is None)): + raise ValueError("child_objects must reproduce the original root-edge-frame boundary") + if actual is not None: + velocity = actual.tables.get("surface_velocity") + if velocity is None: + velocity = boundary_velocity(cfg, self.child_patch, actual.host, jets) + if (not same_physical_boundary(old.boundary, actual.host) + or not np.array_equal(old.surface_velocity, velocity)): + raise ValueError("child_objects changed boundary ownership or surface velocity") + new = RemapTopology.from_config(cfg, self.patch, rectangles, + child_objects=child_objects, child_jets=jets) + if (old.pinned_rectangle != new.pinned_rectangle + or not same_physical_boundary(old.boundary, new.boundary) + or not np.array_equal(old.surface_velocity, new.surface_velocity)): + raise ValueError("remap requires the identical pinned body rectangle and boundary") base_block, fine_block, child_block = self._remap_blocks # Check sparse block alignment/launch size before any device read/allocation. layout = new.child.fine_layout(child_block) @@ -536,7 +561,8 @@ class FixedPatchStepper: candidate = type(self)( cfg, self.patch.bounds, context=self.ctx, base_block=base_block, fine_block=fine_block, child_block=child_block, - child_rectangles_in_parent=new.child.bounds) + child_rectangles_in_parent=new.child.bounds, + child_objects=child_objects, child_jets=jets) try: candidate.initialize(*transferred.g) candidate._step_count = count diff --git a/src/CelerisLab/lbm/synchronized_remap.py b/src/CelerisLab/lbm/synchronized_remap.py index 91dbf4b..204a697 100644 --- a/src/CelerisLab/lbm/synchronized_remap.py +++ b/src/CelerisLab/lbm/synchronized_remap.py @@ -1,16 +1,21 @@ """CPU-only whole-volume G transfer, not a runtime adaptive solver. -Only body-free periodic three-level grids with a fixed L0/L1 topology are -supported. The caller certifies that current owned leaves are quiescent, +Periodic three-level grids with fixed L0/L1 topology are supported, including +one stationary circle pinned inside an unchanged L2 rectangle. The caller +certifies that current owned leaves are quiescent, unshifted D2Q9 postcollision G at the supplied completed base interval. -No device buffers, boundary state, ghosts, collision or time advance occur. +No device buffers, device boundary state, ghosts, collision or time advance +occur; boundary preparation is host-only. """ -from dataclasses import dataclass +from dataclasses import dataclass, replace from numbers import Integral import numpy as np -from .refinement import ACTIVE, FixedPatch2D, FixedPatchSet2D, nest_patch_set +from .refinement import (ACTIVE, FixedPatch2D, FixedPatchSet2D, FineBoundary2D, + nest_patch_set, prepare_fine_boundary) +from .descriptors import SOLID +from ..body.jets import surface_link_velocity @dataclass(frozen=True, eq=False) @@ -24,19 +29,71 @@ class RemapTopology: parent: object child: FixedPatchSet2D leaf_masks: tuple + boundary: FineBoundary2D | None = None + pinned_rectangle: tuple | None = None + surface_velocity: np.ndarray | None = None @classmethod - def from_config(cls, cfg, parent, child_rectangles, *, coarse_boundary="periodic", objects=()): + def from_config(cls, cfg, parent, child_rectangles, *, coarse_boundary="periodic", objects=(), + child_objects=(), child_jets=()): if coarse_boundary != "periodic" or tuple(objects) or cfg.nz != 1: - raise ValueError("remap supports only body-free periodic 2D state") + raise ValueError("remap requires periodic 2D state; use child_objects for a pinned L2 body") if type(parent) not in (FixedPatch2D, FixedPatchSet2D): raise ValueError("parent must be a fixed patch or fixed patch set") parent = type(parent).from_config(cfg, parent.bounds) child, middle_kind = nest_patch_set(cfg, parent, child_rectangles) - masks = (~parent.covered, middle_kind == ACTIVE, child.fine_kind == ACTIVE) + child_objects, child_jets = tuple(child_objects), tuple(child_jets) + if child_objects and (len(child_objects) != 1 or len(child.bounds) != 2): + raise ValueError("body remap requires one stationary circle and two L2 rectangles") + fine_cfg = replace(cfg, nx=parent.fine_shape[1], ny=parent.fine_shape[0], + viscosity=2 * cfg.viscosity) + boundary = prepare_fine_boundary( + fine_cfg, child, child_objects, jets=child_jets, + parent_origin=parent.fine_origin, parent_spacing=.5) + pinned, velocity = None, None + child_leaf = child.fine_kind == ACTIVE + if boundary is not None: + child_leaf &= (boundary.flags.reshape(child.fine_shape) & SOLID) == 0 + # Whole circle containment, not merely the resolved solid cells. + obj = child_objects[0] + cx, cy = (np.asarray(obj.geometry.center) - parent.fine_origin) * 2 + radius = obj.geometry._radius * 2 + pinned = next(r for r in child.bounds if + r[0] <= cx-radius and cx+radius <= r[2] and + r[1] <= cy-radius and cy+radius <= r[3]) + velocity = boundary_velocity(cfg, child, boundary, child_jets) + if velocity is not None: + velocity.setflags(write=False) + masks = (~parent.covered, middle_kind == ACTIVE, child_leaf) for mask in masks: mask.setflags(write=False) - return cls(parent, child, masks) + return cls(parent, child, masks, boundary, pinned, velocity) + + +def boundary_velocity(cfg, child, boundary, jets=()): + """Physical cut-link order; never blocked addresses or stale Jet snapshots.""" + local_cfg = replace(cfg, nx=child.fine_shape[1], ny=child.fine_shape[0], + viscosity=4 * cfg.viscosity) + local_cfg.validate() + fluid, dirs, _, ids, rx, ry, _, _ = boundary.curved_host + velocity, _ = surface_link_velocity( + boundary.objects, local_cfg, boundary.flags, fluid, dirs, ids, rx, ry, + jets=jets, hits=boundary.curved_hits) + return velocity + + +def same_physical_boundary(a, b): + """Compare geometry, ownership, hits and the entire BFL support stencil.""" + if a is None or b is None: + return a is b + if not isinstance(a, FineBoundary2D) or not isinstance(b, FineBoundary2D): + return False + if (tuple(o.surface_actuation for o in a.objects) + != tuple(o.surface_actuation for o in b.objects)): + return False + return all(np.array_equal(x, y) for x, y in zip( + (a.flags, a.boundary_cells, a.curved_hits, *a.curved_host), + (b.flags, b.boundary_cells, b.curved_hits, *b.curved_host))) @dataclass(frozen=True, eq=False) @@ -105,6 +162,10 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase, or not _same_parent(old.parent, new.parent) or old.child.tau_fine != new.child.tau_fine): raise ValueError("remap requires unchanged L0/L1 geometry and relaxation") + if (old.pinned_rectangle != new.pinned_rectangle + or not same_physical_boundary(old.boundary, new.boundary) + or not np.array_equal(old.surface_velocity, new.surface_velocity)): + raise ValueError("remap requires the identical pinned body rectangle and physical boundary") if not isinstance(level_steps, (tuple, list)): raise ValueError("level_steps must be three completed integer counts") counts = (base_count, *level_steps) diff --git a/tests/unit/test_pinned_body_remap.py b/tests/unit/test_pinned_body_remap.py new file mode 100644 index 0000000..9d8df56 --- /dev/null +++ b/tests/unit/test_pinned_body_remap.py @@ -0,0 +1,130 @@ +"""Bounded pinned-body transfer and host lifecycle; no CUDA numerical claim.""" +import unittest +from unittest.mock import patch +import numpy as np + +from CelerisLab.body.geometry.circle import CircleGeometry +from CelerisLab.body.objects import SimObject, SurfaceActuation +from CelerisLab.body.jets import Jet, Tube, Parallel +from CelerisLab.lbm.synchronized_remap import RemapTopology, same_physical_boundary +from tests.unit.test_synchronized_remap import CFG, PARENTS, fields, remap +from tests.unit.test_refinement_three_level_multipatch import mock_backend +from CelerisLab.lbm.refinement import FixedPatchSet2D + +A = ((16,16,32,40), (56,16,72,40)) +B = ((16,16,32,40), (60,16,76,40)) + + +def body(spec=None, center=(12.,14.)): + return SimObject(71, geometry=CircleGeometry(*center,.8), center=center, + radius=.8, surface_actuation=spec) + + +def jet(): + return Jet(Tube((10.5,14.),0.,3.,.3), Parallel(0.), 'parabolic', .003) + + +def mesh(rects, objects, jets=()): + return RemapTopology.from_config(CFG, FixedPatchSet2D.from_config(CFG,PARENTS), + rects, child_objects=objects, child_jets=jets) + + +def ready(objects=None, jets=None): + objects = (body(),) if objects is None else objects + jets = (jet(),) if jets is None else jets + with patch('tests.unit.test_refinement_three_level_multipatch.bodies_and_jets', + return_value=(objects,jets)): + run, module, allocations = mock_backend(children=A,bodies=True) + storage = {} + module.cuda.memcpy_htod.side_effect = lambda p,a: storage.__setitem__(id(p),np.array(a,copy=True)) + module.cuda.memcpy_dtoh.side_effect = lambda a,p: np.copyto(a,storage[id(p)]) + run.initialize(*fields(mesh(A,objects,jets),np.float32)) + run._step_count = 7 + return run,module,allocations,storage,objects + + +class PinnedBodyRemap(unittest.TestCase): + def test_fluid_only_transfer_poison_and_round_trip(self): + old,new = mesh(A,(body(),)),mesh(B,(body(),)) + arrays=fields(old,np.float32) + for a,leaf in zip(arrays,old.leaf_masks): a[:,~leaf]=12345 + out=remap(old,new,arrays) + for a,b,before,after in zip(arrays,out.g,old.leaf_masks,new.leaf_masks): + np.testing.assert_array_equal(a[:,before & after],b[:,before & after]) + self.assertTrue(np.isnan(b[:,~after]).all()) + self.assertTrue(same_physical_boundary(old.boundary,new.boundary)) + back=remap(new,old,out.g) + self.assertTrue(np.isnan(back.g[2][:,~old.leaf_masks[2]]).all()) + self.assertEqual(old.pinned_rectangle,A[0]) + + def test_current_jets_and_source_independence(self): + run,module,allocations,storage,objects=ready() + run.set_jet_amplitude(0,.017,level=2) + before=run.download() + clone=run.remap_child(B,child_objects=objects) + self.assertEqual(clone._boundaries[2].jets[0].amplitude,.017) + np.testing.assert_array_equal(clone._boundaries[2].tables['surface_velocity'], + run._boundaries[2].tables['surface_velocity']) + self.assertTrue(same_physical_boundary(run.child_boundary,clone.child_boundary)) + clone.step() + self.assertEqual((run.step_count,clone.step_count),(7,8)) + for got,want in zip(run.download(),before): np.testing.assert_array_equal(got,want) + clone.close();run.close() + + def test_static_body_without_actuation(self): + run,module,allocations,storage,objects=ready(jets=()) + clone=run.remap_child(A,child_objects=objects) + self.assertFalse(clone._boundaries[2].actuated) + self.assertNotIn('surface_velocity',clone._boundaries[2].tables) + for a,b in zip(run.download(),clone.download()): np.testing.assert_array_equal(a,b) + clone.close();run.close() + + def test_reject_before_device_reads_or_allocation(self): + run,module,allocations,storage,objects=ready() + start=len(allocations) + cases=[(B,()),(B,run.child_boundary.objects),(B,(body(center=(12.1,14.)),)), + (((20,16,36,40),B[1]),objects),(((16,20,32,40),B[1]),objects), + ((B[1],),objects)] + module.cuda.memcpy_dtoh.reset_mock() + for rects,objs in cases: + with self.subTest(rects=rects),self.assertRaises(ValueError): + run.remap_child(rects,child_objects=objs) + self.assertEqual(len(allocations),start) + module.cuda.memcpy_dtoh.assert_not_called() + self.assertEqual((run._status,run.step_count),('ready',7)) + run.close() + + def test_legacy_velocity_and_zero_velocity_ownership_rejected(self): + for speed in (.002,0.): + run,module,allocations,storage,objects=ready( + (body(SurfaceActuation(0.,.7,speed)),),()) + for spec in (SurfaceActuation(0.,.7,.003),SurfaceActuation(2.,.7,speed)): + module.cuda.memcpy_dtoh.reset_mock() + with self.assertRaises(ValueError): run.remap_child(B,child_objects=(body(spec),)) + module.cuda.memcpy_dtoh.assert_not_called() + clone=run.remap_child(B,child_objects=objects) + clone.close();run.close() + + def test_allocation_failure_keeps_old_ready_and_count(self): + run,module,allocations,storage,objects=ready() + before=run.download(); saved=list(allocations) + allocate=module.cuda.mem_alloc.side_effect + calls=0 + def fail(size): + nonlocal calls + calls+=1 + if calls == 3: raise RuntimeError('candidate allocation') + return allocate(size) + with patch.object(module.cuda,'mem_alloc',side_effect=fail): + with self.assertRaisesRegex(RuntimeError,'allocation'): + run.remap_child(B,child_objects=objects) + self.assertEqual((run._status,run.step_count),('ready',7)) + for got,want in zip(run.download(),before): np.testing.assert_array_equal(got,want) + for a in saved: a.free.assert_not_called() + for a in allocations[len(saved):]: a.free.assert_called_once() + run.close() + + def test_direct_helper_rejects_changed_pinned_rectangle(self): + old=mesh(A,(body(),)) + new=mesh(((12,16,32,40),A[1]),(body(),)) + with self.assertRaisesRegex(ValueError,'pinned'): remap(old,new,fields(old)) diff --git a/tests/unit/test_synchronized_remap.py b/tests/unit/test_synchronized_remap.py index 0554a66..30a4ca5 100644 --- a/tests/unit/test_synchronized_remap.py +++ b/tests/unit/test_synchronized_remap.py @@ -185,7 +185,7 @@ class SynchronizedRemapTests(unittest.TestCase): with self.assertRaises(ValueError): RemapTopology.from_config(replace(CFG, **changes), parent, A) for kwargs in (dict(coarse_boundary='slot_reservoir'), dict(objects=(object(),))): - with self.assertRaisesRegex(ValueError, 'body-free periodic'): + with self.assertRaisesRegex(ValueError, 'periodic 2D'): RemapTopology.from_config(CFG, parent, A, **kwargs) different = topology(A, cfg=replace(CFG, viscosity=.12)) with self.assertRaisesRegex(ValueError, 'unchanged'): diff --git a/tests/validation/multigrid/test_pinned_body_remap_gpu.py b/tests/validation/multigrid/test_pinned_body_remap_gpu.py new file mode 100644 index 0000000..8f1e1d0 --- /dev/null +++ b/tests/validation/multigrid/test_pinned_body_remap_gpu.py @@ -0,0 +1,84 @@ +"""Pinned-body GPU lifecycle gates. NOT executed by CPU test collection. + +No evolution accuracy tolerance is asserted here: an independent BFL replay +reference is required before a 3e-6 FP32-vs-FP64 gate can be claimed. These +checks require exact physical boundary/velocity, remap overlap and no-op next +step identity; they never compare blocked boundary addresses across layouts. +""" +from contextlib import ExitStack +import unittest +import numpy as np +from CelerisLab.cuda.context import CudaContext +from CelerisLab.lbm.refinement_stepper import FixedPatchStepper +from CelerisLab.lbm.synchronized_remap import RemapTopology, remap_synchronized +from CelerisLab.body.geometry.circle import CircleGeometry +from CelerisLab.body.objects import SimObject +from CelerisLab.body.jets import Jet, Tube, Parallel +from test_runtime_child_remap_gpu import RuntimeChildRemapGPU, initial +from test_refinement_three_level_multipatch_gpu import CFG, PARENTS +from test_refinement_blocked_base import exact + +A = ((16,16,32,40), (56,16,72,40)) +B = ((16,16,32,40), (60,16,76,40)) + + +def make(ctx): + body = SimObject(71, geometry=CircleGeometry(12.,14.,.8),center=(12.,14.),radius=.8) + jet = Jet(Tube((10.5,14.),0.,3.,.3),Parallel(0.),'parabolic',.003) + run = FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4),fine_block=(4,4), + child_block=(4,4),child_rectangles_in_parent=A, + child_objects=(body,),child_jets=(jet,)) + return run,(body,) + + +class PinnedBodyRemapGPU(unittest.TestCase): + check_public = RuntimeChildRemapGPU.check_public + check_reset = RuntimeChildRemapGPU.check_reset + + def check_boundary(self, before, after): + for name in ('flags','boundary_cells','curved_hits'): + exact(self,getattr(after.child_boundary,name),getattr(before.child_boundary,name),name) + for a,b in zip(after.child_boundary.curved_host,before.child_boundary.curved_host): + exact(self,a,b,'physical cut links') + exact(self,after._boundaries[2].tables['surface_velocity'], + before._boundaries[2].tables['surface_velocity'],'current per-link velocities') + self.assertEqual(after._boundaries[2].jets[0].amplitude,.017) + + def test_noop_next_step_bitwise(self): + with CudaContext(0) as ctx, ExitStack() as owned: + old,objects=make(ctx);owned.enter_context(old) + old.initialize(*initial(old,False));old.step(2) + old.set_jet_amplitude(0,.017,level=2) + before=old.download() + new=owned.enter_context(old.remap_child(A,child_objects=objects)) + self.check_boundary(old,new);self.check_reset(new,before) + old.step();new.step() + for a,b in zip(new.download(),old.download()): exact(self,a,b,'no-op next step bitwise') + self.assertEqual((old.step_count,new.step_count),(3,3)) + self.check_public(new) + + def test_moved_fluid_return_and_source_independence(self): + with CudaContext(0) as ctx, ExitStack() as owned: + current,objects=make(ctx);owned.enter_context(current) + current.initialize(*initial(current,False));current.step(2) + current.set_jet_amplitude(0,.017,level=2) + for rects in (B,A): + previous=current;before=previous.download();count=previous.step_count + jets=previous._boundaries[2].jets + old=RemapTopology.from_config(CFG,previous.patch,previous.child_patch.bounds, + child_objects=objects,child_jets=jets) + new=RemapTopology.from_config(CFG,previous.patch,rects, + child_objects=objects,child_jets=jets) + expected=remap_synchronized(old,new,before,base_count=count, + level_steps=(count,2*count,4*count),phase='postcollision_g').g + current=owned.enter_context(previous.remap_child(rects,child_objects=objects)) + self.check_boundary(previous,current);self.check_reset(current,expected) + for a,b,om,nm in zip(current.download(),before,old.leaf_masks,new.leaf_masks): + exact(self,a[:,om & nm],b[:,om & nm],'physical-fluid overlap') + current.step(2) + self.assertEqual((previous.step_count,current.step_count),(count,count+2)) + for a,b in zip(previous.download(),before): exact(self,a,b,'source independent') + self.check_public(current) + + +if __name__ == '__main__': unittest.main()