Generalize synchronized remap to multiple pinned body rectangles
This commit is contained in:
1 parent
864a220dbe
commit
60ad6b8b0d
6 files changed
+375
-32
No files matched your search
@@ -113,22 +113,24 @@ Per-population conservation is not an advancement target under collision.
|
|||||||
### Bounded pinned-body extension
|
### Bounded pinned-body extension
|
||||||
|
|
||||||
`replacement = current.remap_child(rectangles, child_objects=original_objects)`
|
`replacement = current.remap_child(rectangles, child_objects=original_objects)`
|
||||||
now also accepts exactly one stationary Circle in one of exactly two L2
|
also accepts stationary Circles in separated L2 rectangles. Every body’s
|
||||||
rectangles. Its **complete containing rectangle must remain identical**;
|
**complete containing rectangle must remain identical**. Several bodies may
|
||||||
only the separated fluid rectangle may change. Existing nesting, four-cell
|
share one pinned rectangle; there need not be any fluid-only rectangles.
|
||||||
gap, full-circle containment, reverse cut-link coverage and two-hop BFL
|
Separated fluid-only rectangles may be added, removed or moved. 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
|
transfer-footprint checks still apply. This is manual remapping, not adaptive
|
||||||
body tracking or runtime body editing.
|
body tracking or runtime body editing.
|
||||||
|
|
||||||
Supply the original root edge-frame definition used at construction. Do not
|
Supply all original root edge-frame definitions in their construction order.
|
||||||
pass `current.child_boundary.objects`: those are projected fine-local copies.
|
Do not pass `current.child_boundary.objects`: those are projected fine-local copies.
|
||||||
There is no retained mutable original-object registry. Before reading device
|
There is no retained mutable original-object registry. Before reading device
|
||||||
state, remapping reprojects the supplied definition onto the old topology and
|
state, remapping reprojects the supplied definition onto the old topology and
|
||||||
compares flags, physical cut links, body IDs, q, lever arms, hits, BFL support,
|
compares exact ordered circle centers/radii, flags, physical cut links, body
|
||||||
|
IDs, q, lever arms, hits, BFL support,
|
||||||
legacy SurfaceActuation specification and current per-link velocity against
|
legacy SurfaceActuation specification and current per-link velocity against
|
||||||
the existing boundary. Omission, changed geometry/actuation and changed body
|
the existing boundary. Omission, changed geometry/actuation and changed body
|
||||||
rectangle fail without device reads or allocations. Root object IDs are still
|
rectangle fail without device reads or allocations; reordered bodies fail
|
||||||
assigned by list position, as in the constructor.
|
too. 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
|
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
|
`set_jet_amplitude(..., level=2)`. There is no replacement-Jets argument or
|
||||||
@@ -139,12 +141,20 @@ boundary and velocities must match exactly.
|
|||||||
typed `FineBoundary2D`; it derives L2 fluid leaves as ACTIVE minus SOLID. No
|
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
|
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
|
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.
|
old fluid children. The helper checks the old/new body pins and boundary too.
|
||||||
|
Its immutable `pinned_rectangles` is a canonical, deduplicated tuple, empty for body-free layouts; rectangle input
|
||||||
|
order does not affect it. Scalar-parent window offsets remain part of the
|
||||||
|
coordinate conversion.
|
||||||
|
|
||||||
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`. GPU declarations:
|
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`. GPU declarations:
|
||||||
`tests/validation/multigrid/test_pinned_body_remap_gpu.py`, covering no-op
|
`tests/validation/multigrid/test_pinned_body_remap_gpu.py`, covering no-op
|
||||||
next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation,
|
next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation,
|
||||||
physical boundary/velocity equality, source independence and absolute counts.
|
physical boundary/velocity equality, source independence and absolute counts.
|
||||||
These GPU cases have **not been run here**. They do not establish evolution
|
Multiple-body CPU coverage: `tests/unit/test_multiple_pinned_remap.py`.
|
||||||
|
`tests/validation/multigrid/test_multiple_pinned_remap_gpu.py` adds two pins,
|
||||||
|
two independently updated Jets, variable fluid-region counts and add/move/remove
|
||||||
|
immediate comparisons with CPU transfer. All nine solid-excluded area-weighted
|
||||||
|
population integrals, exact overlap and nonleaf NaNs are checked.
|
||||||
|
These new 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
|
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.
|
BFL reference; the body-free replay gate above does not establish that claim.
|
||||||
@@ -499,9 +499,9 @@ class FixedPatchStepper:
|
|||||||
|
|
||||||
Periodic three-level set children only; L0/L1 and block sizes stay fixed.
|
Periodic three-level set children only; L0/L1 and block sizes stay fixed.
|
||||||
A body run requires its original root-edge-frame ``child_objects``:
|
A body run requires its original root-edge-frame ``child_objects``:
|
||||||
one stationary circle pinned in one of exactly two L2 rectangles.
|
stationary circles pinned in unchanged complete L2 rectangles.
|
||||||
The complete body rectangle stays identical; only the other fluid
|
Separated fluid-only rectangles may be added, moved or removed.
|
||||||
rectangle may move. Current L2 Jets (including updated amplitudes)
|
Current L2 Jets (including updated amplitudes)
|
||||||
are preserved. Local projected boundary objects are not valid inputs.
|
are preserved. Local projected boundary objects are not valid inputs.
|
||||||
The lock and stream synchronization certify
|
The lock and stream synchronization certify
|
||||||
current postcollision G and the completed 1:2:4 counts internally.
|
current postcollision G and the completed 1:2:4 counts internally.
|
||||||
@@ -538,10 +538,10 @@ class FixedPatchStepper:
|
|||||||
raise ValueError("child_objects changed boundary ownership or surface velocity")
|
raise ValueError("child_objects changed boundary ownership or surface velocity")
|
||||||
new = RemapTopology.from_config(cfg, self.patch, rectangles,
|
new = RemapTopology.from_config(cfg, self.patch, rectangles,
|
||||||
child_objects=child_objects, child_jets=jets)
|
child_objects=child_objects, child_jets=jets)
|
||||||
if (old.pinned_rectangle != new.pinned_rectangle
|
if (old.pinned_rectangles != new.pinned_rectangles
|
||||||
or not same_physical_boundary(old.boundary, new.boundary)
|
or not same_physical_boundary(old.boundary, new.boundary)
|
||||||
or not np.array_equal(old.surface_velocity, new.surface_velocity)):
|
or not np.array_equal(old.surface_velocity, new.surface_velocity)):
|
||||||
raise ValueError("remap requires the identical pinned body rectangle and boundary")
|
raise ValueError("remap requires the identical pinned body rectangles and boundary")
|
||||||
base_block, fine_block, child_block = self._remap_blocks
|
base_block, fine_block, child_block = self._remap_blocks
|
||||||
# Check sparse block alignment/launch size before any device read/allocation.
|
# Check sparse block alignment/launch size before any device read/allocation.
|
||||||
layout = new.child.fine_layout(child_block)
|
layout = new.child.fine_layout(child_block)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""CPU-only whole-volume G transfer, not a runtime adaptive solver.
|
"""CPU-only whole-volume G transfer, not a runtime adaptive solver.
|
||||||
|
|
||||||
Periodic three-level grids with fixed L0/L1 topology are supported, including
|
Periodic three-level grids with fixed L0/L1 topology are supported, including
|
||||||
one stationary circle pinned inside an unchanged L2 rectangle. The caller
|
stationary circles pinned inside unchanged L2 rectangles. The caller
|
||||||
certifies that current owned leaves are quiescent,
|
certifies that current owned leaves are quiescent,
|
||||||
unshifted D2Q9 postcollision G at the supplied completed base interval.
|
unshifted D2Q9 postcollision G at the supplied completed base interval.
|
||||||
No device buffers, device boundary state, ghosts, collision or time advance
|
No device buffers, device boundary state, ghosts, collision or time advance
|
||||||
@@ -25,12 +25,14 @@ class RemapTopology:
|
|||||||
Child rectangles are in the parent's allocated L1 coordinates, including
|
Child rectangles are in the parent's allocated L1 coordinates, including
|
||||||
its window offset/halo when the parent is a scalar patch. Dense masks are
|
its window offset/halo when the parent is a scalar patch. Dense masks are
|
||||||
rebuilt from geometry, never inferred from finite values in input arrays.
|
rebuilt from geometry, never inferred from finite values in input arrays.
|
||||||
|
``pinned_rectangles`` canonically lists each body-containing rectangle once;
|
||||||
|
fluid-only layouts have no pins.
|
||||||
"""
|
"""
|
||||||
parent: object
|
parent: object
|
||||||
child: FixedPatchSet2D
|
child: FixedPatchSet2D
|
||||||
leaf_masks: tuple
|
leaf_masks: tuple
|
||||||
boundary: FineBoundary2D | None = None
|
boundary: FineBoundary2D | None = None
|
||||||
pinned_rectangle: tuple | None = None
|
pinned_rectangles: tuple = ()
|
||||||
surface_velocity: np.ndarray | None = None
|
surface_velocity: np.ndarray | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -43,24 +45,24 @@ class RemapTopology:
|
|||||||
parent = type(parent).from_config(cfg, parent.bounds)
|
parent = type(parent).from_config(cfg, parent.bounds)
|
||||||
child, middle_kind = nest_patch_set(cfg, parent, child_rectangles)
|
child, middle_kind = nest_patch_set(cfg, parent, child_rectangles)
|
||||||
child_objects, child_jets = tuple(child_objects), tuple(child_jets)
|
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],
|
fine_cfg = replace(cfg, nx=parent.fine_shape[1], ny=parent.fine_shape[0],
|
||||||
viscosity=2 * cfg.viscosity)
|
viscosity=2 * cfg.viscosity)
|
||||||
boundary = prepare_fine_boundary(
|
boundary = prepare_fine_boundary(
|
||||||
fine_cfg, child, child_objects, jets=child_jets,
|
fine_cfg, child, child_objects, jets=child_jets,
|
||||||
parent_origin=parent.fine_origin, parent_spacing=.5)
|
parent_origin=parent.fine_origin, parent_spacing=.5)
|
||||||
pinned, velocity = None, None
|
pinned, velocity = (), None
|
||||||
child_leaf = child.fine_kind == ACTIVE
|
child_leaf = child.fine_kind == ACTIVE
|
||||||
if boundary is not None:
|
if boundary is not None:
|
||||||
child_leaf &= (boundary.flags.reshape(child.fine_shape) & SOLID) == 0
|
child_leaf &= (boundary.flags.reshape(child.fine_shape) & SOLID) == 0
|
||||||
# Whole circle containment, not merely the resolved solid cells.
|
# Whole circle containment, not merely the resolved solid cells.
|
||||||
obj = child_objects[0]
|
containing = set()
|
||||||
cx, cy = (np.asarray(obj.geometry.center) - parent.fine_origin) * 2
|
for obj in child_objects:
|
||||||
radius = obj.geometry._radius * 2
|
cx, cy = (np.asarray(obj.geometry.center) - parent.fine_origin) * 2
|
||||||
pinned = next(r for r in child.bounds if
|
radius = obj.geometry._radius * 2
|
||||||
r[0] <= cx-radius and cx+radius <= r[2] and
|
containing.add(next(r for r in child.bounds if
|
||||||
r[1] <= cy-radius and cy+radius <= r[3])
|
r[0] <= cx-radius and cx+radius <= r[2] and
|
||||||
|
r[1] <= cy-radius and cy+radius <= r[3]))
|
||||||
|
pinned = tuple(sorted(containing))
|
||||||
velocity = boundary_velocity(cfg, child, boundary, child_jets)
|
velocity = boundary_velocity(cfg, child, boundary, child_jets)
|
||||||
if velocity is not None:
|
if velocity is not None:
|
||||||
velocity.setflags(write=False)
|
velocity.setflags(write=False)
|
||||||
@@ -88,8 +90,11 @@ def same_physical_boundary(a, b):
|
|||||||
return a is b
|
return a is b
|
||||||
if not isinstance(a, FineBoundary2D) or not isinstance(b, FineBoundary2D):
|
if not isinstance(a, FineBoundary2D) or not isinstance(b, FineBoundary2D):
|
||||||
return False
|
return False
|
||||||
if (tuple(o.surface_actuation for o in a.objects)
|
# Exact ordered geometry also catches changes hidden by cut-table rounding.
|
||||||
!= tuple(o.surface_actuation for o in b.objects)):
|
def signatures(boundary):
|
||||||
|
return tuple((o.geometry.center, o.geometry._radius, o.surface_actuation)
|
||||||
|
for o in boundary.objects)
|
||||||
|
if signatures(a) != signatures(b):
|
||||||
return False
|
return False
|
||||||
return all(np.array_equal(x, y) for x, y in zip(
|
return all(np.array_equal(x, y) for x, y in zip(
|
||||||
(a.flags, a.boundary_cells, a.curved_hits, *a.curved_host),
|
(a.flags, a.boundary_cells, a.curved_hits, *a.curved_host),
|
||||||
@@ -162,10 +167,10 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
|||||||
or not _same_parent(old.parent, new.parent)
|
or not _same_parent(old.parent, new.parent)
|
||||||
or old.child.tau_fine != new.child.tau_fine):
|
or old.child.tau_fine != new.child.tau_fine):
|
||||||
raise ValueError("remap requires unchanged L0/L1 geometry and relaxation")
|
raise ValueError("remap requires unchanged L0/L1 geometry and relaxation")
|
||||||
if (old.pinned_rectangle != new.pinned_rectangle
|
if (old.pinned_rectangles != new.pinned_rectangles
|
||||||
or not same_physical_boundary(old.boundary, new.boundary)
|
or not same_physical_boundary(old.boundary, new.boundary)
|
||||||
or not np.array_equal(old.surface_velocity, new.surface_velocity)):
|
or not np.array_equal(old.surface_velocity, new.surface_velocity)):
|
||||||
raise ValueError("remap requires the identical pinned body rectangle and physical boundary")
|
raise ValueError("remap requires the identical pinned body rectangles and physical boundary")
|
||||||
if not isinstance(level_steps, (tuple, list)):
|
if not isinstance(level_steps, (tuple, list)):
|
||||||
raise ValueError("level_steps must be three completed integer counts")
|
raise ValueError("level_steps must be three completed integer counts")
|
||||||
counts = (base_count, *level_steps)
|
counts = (base_count, *level_steps)
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
"""Multiple body pins and variable fluid sets; mocked runtime is host-only."""
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from CelerisLab.config import LBMConfig
|
||||||
|
from CelerisLab.body.jets import Jet, Tube, Parallel
|
||||||
|
from CelerisLab.body.objects import SurfaceActuation
|
||||||
|
from CelerisLab.lbm.refinement import FixedPatch2D
|
||||||
|
from CelerisLab.lbm.synchronized_remap import RemapTopology
|
||||||
|
from tests.unit.test_pinned_body_remap import body, mesh
|
||||||
|
from tests.unit.test_refinement_three_level_multipatch import mock_backend
|
||||||
|
from tests.unit.test_synchronized_remap import fields, integrals, remap
|
||||||
|
|
||||||
|
PINS = ((16, 16, 32, 24), (56, 16, 72, 24))
|
||||||
|
FLUID = ((16, 32, 32, 40), (56, 32, 72, 40))
|
||||||
|
ADDED = ((16, 44, 32, 48),)
|
||||||
|
MOVED = ((20, 32, 36, 40), (60, 32, 76, 40))
|
||||||
|
|
||||||
|
|
||||||
|
def objects():
|
||||||
|
return (body(center=(12., 10.)), body(center=(32., 10.)))
|
||||||
|
|
||||||
|
|
||||||
|
def ready():
|
||||||
|
bodies = objects()
|
||||||
|
jets = tuple(Jet(Tube((x-1.5, 10.), 0., 3., .3), Parallel(0.),
|
||||||
|
'parabolic', .003) for x in (12., 32.))
|
||||||
|
with patch('tests.unit.test_refinement_three_level_multipatch.bodies_and_jets',
|
||||||
|
return_value=(bodies, jets)):
|
||||||
|
run, module, allocations = mock_backend(children=PINS+FLUID, 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(PINS+FLUID, bodies, jets), np.float32))
|
||||||
|
run._step_count = 7
|
||||||
|
return run, module, allocations, bodies
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplePinnedRemap(unittest.TestCase):
|
||||||
|
def check_transfer(self, old, new, arrays, tolerance):
|
||||||
|
out = remap(old, new, arrays)
|
||||||
|
before = integrals(old, arrays)
|
||||||
|
np.testing.assert_allclose(integrals(new, out.g), before, rtol=0,
|
||||||
|
atol=tolerance*np.abs(before[:9]).sum())
|
||||||
|
for a, b, om, nm in zip(arrays, out.g, old.leaf_masks, new.leaf_masks):
|
||||||
|
self.assertEqual(a[:, om & nm].tobytes(), b[:, om & nm].tobytes())
|
||||||
|
self.assertTrue(np.isnan(b[:, ~nm]).all())
|
||||||
|
return out.g
|
||||||
|
|
||||||
|
def test_add_move_remove_and_body_only_all_populations(self):
|
||||||
|
for dtype, tolerance in ((np.float64, 2e-13), (np.float32, 2e-7)):
|
||||||
|
old = mesh(PINS+FLUID, objects())
|
||||||
|
arrays = fields(old, dtype)
|
||||||
|
for rects in (PINS+FLUID+ADDED, PINS+MOVED+ADDED, PINS, PINS+FLUID):
|
||||||
|
new = mesh(rects, objects())
|
||||||
|
self.assertEqual(new.pinned_rectangles, PINS)
|
||||||
|
arrays = self.check_transfer(old, new, arrays, tolerance)
|
||||||
|
old = new
|
||||||
|
|
||||||
|
def test_shared_pin_order_invariance_and_body_free(self):
|
||||||
|
bodies = (body(center=(11., 14.)), body(center=(14., 14.)))
|
||||||
|
rects = ((16, 16, 32, 40), (56, 16, 72, 40))
|
||||||
|
old, new = mesh(rects, bodies), mesh(rects[::-1], bodies)
|
||||||
|
self.assertEqual(old.pinned_rectangles, (rects[0],))
|
||||||
|
self.assertEqual(new.pinned_rectangles, old.pinned_rectangles)
|
||||||
|
self.check_transfer(old, new, fields(old), 2e-13)
|
||||||
|
only = mesh((rects[0],), bodies)
|
||||||
|
self.check_transfer(old, only, fields(old), 2e-13)
|
||||||
|
self.assertEqual(mesh(rects, ()).pinned_rectangles, ())
|
||||||
|
|
||||||
|
def test_shifted_scalar_parent_coordinates(self):
|
||||||
|
cfg = LBMConfig(nx=48, ny=32, viscosity=.1)
|
||||||
|
parent = FixedPatch2D.from_config(cfg, (4, 4, 44, 28))
|
||||||
|
self.assertEqual(parent.fine_origin, (2., 2.))
|
||||||
|
bodies = (body(center=(14., 12.)), body(center=(34., 12.)))
|
||||||
|
def topology(rects):
|
||||||
|
return RemapTopology.from_config(cfg, parent, rects, child_objects=bodies)
|
||||||
|
old, new = topology(PINS+FLUID), topology(PINS+MOVED)
|
||||||
|
self.assertEqual(old.pinned_rectangles, PINS)
|
||||||
|
self.check_transfer(old, new, fields(old), 2e-13)
|
||||||
|
|
||||||
|
def test_direct_helper_rejects_each_changed_pin_and_body_order(self):
|
||||||
|
old = mesh(PINS+FLUID, objects())
|
||||||
|
candidates = [mesh(PINS+FLUID, objects()[::-1])]
|
||||||
|
for index in range(2):
|
||||||
|
changed = list(PINS+FLUID)
|
||||||
|
x0, y0, x1, y1 = changed[index]
|
||||||
|
changed[index] = (x0-4, y0, x1, y1)
|
||||||
|
candidates.append(mesh(changed, objects()))
|
||||||
|
for new in candidates:
|
||||||
|
# Invalid donors ensure boundary validation precedes array reads.
|
||||||
|
with self.assertRaisesRegex(ValueError, 'pinned'):
|
||||||
|
remap(old, new, None)
|
||||||
|
|
||||||
|
def test_live_independent_jets_and_source_ownership(self):
|
||||||
|
run, module, allocations, bodies = ready()
|
||||||
|
run.set_jet_amplitude(0, .017, level=2)
|
||||||
|
run.set_jet_amplitude(1, -.011, level=2)
|
||||||
|
before = run.download()
|
||||||
|
clone = run.remap_child((PINS+MOVED+ADDED)[::-1], child_objects=bodies)
|
||||||
|
try:
|
||||||
|
self.assertEqual(tuple(j.amplitude for j in clone._boundaries[2].jets), (.017, -.011))
|
||||||
|
np.testing.assert_array_equal(clone._boundaries[2].tables['surface_velocity'],
|
||||||
|
run._boundaries[2].tables['surface_velocity'])
|
||||||
|
clone.set_jet_amplitude(1, .005, level=2)
|
||||||
|
self.assertEqual(run._boundaries[2].jets[1].amplitude, -.011)
|
||||||
|
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)
|
||||||
|
finally:
|
||||||
|
clone.close()
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_sub_ulp_cut_table_geometry_change_rejected(self):
|
||||||
|
run, module, allocations, bodies = ready()
|
||||||
|
changed = objects()
|
||||||
|
changed[1].geometry._radius = np.nextafter(.8, np.inf)
|
||||||
|
start = len(allocations)
|
||||||
|
module.cuda.memcpy_dtoh.reset_mock()
|
||||||
|
with self.assertRaisesRegex(ValueError, 'boundary'):
|
||||||
|
run.remap_child(PINS+FLUID, child_objects=changed)
|
||||||
|
self.assertEqual(len(allocations), start)
|
||||||
|
module.cuda.memcpy_dtoh.assert_not_called()
|
||||||
|
with self.assertRaisesRegex(ValueError, 'pinned'):
|
||||||
|
remap(mesh(PINS+FLUID, bodies), mesh(PINS+FLUID, changed), None)
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_each_pin_and_body_identity_rejected_before_device_access(self):
|
||||||
|
run, module, allocations, bodies = ready()
|
||||||
|
cases = [(PINS+FLUID, bodies[:1]), (PINS+FLUID, bodies[::-1]),
|
||||||
|
(PINS+FLUID, (bodies[0], body(center=(32.1, 10.)))),
|
||||||
|
(PINS+FLUID, (bodies[0], body(SurfaceActuation(0., .7, 0.), (32., 10.)))),
|
||||||
|
(PINS+FLUID, run.child_boundary.objects)]
|
||||||
|
for index in range(2):
|
||||||
|
changed = list(PINS+FLUID)
|
||||||
|
x0, y0, x1, y1 = changed[index]
|
||||||
|
changed[index] = (x0-4, y0, x1, y1)
|
||||||
|
cases.append((changed, bodies))
|
||||||
|
cases.append((tuple(r for i, r in enumerate(PINS+FLUID) if i != index), bodies))
|
||||||
|
start = len(allocations)
|
||||||
|
module.cuda.memcpy_dtoh.reset_mock()
|
||||||
|
run._stream.synchronize.reset_mock()
|
||||||
|
for rects, supplied in cases:
|
||||||
|
with self.subTest(rects=rects), self.assertRaises(ValueError):
|
||||||
|
run.remap_child(rects, child_objects=supplied)
|
||||||
|
self.assertEqual(len(allocations), start)
|
||||||
|
module.cuda.memcpy_dtoh.assert_not_called()
|
||||||
|
run._stream.synchronize.assert_not_called()
|
||||||
|
self.assertEqual((run._status, run.step_count), ('ready', 7))
|
||||||
|
run.close()
|
||||||
@@ -55,7 +55,7 @@ class PinnedBodyRemap(unittest.TestCase):
|
|||||||
self.assertTrue(same_physical_boundary(old.boundary,new.boundary))
|
self.assertTrue(same_physical_boundary(old.boundary,new.boundary))
|
||||||
back=remap(new,old,out.g)
|
back=remap(new,old,out.g)
|
||||||
self.assertTrue(np.isnan(back.g[2][:,~old.leaf_masks[2]]).all())
|
self.assertTrue(np.isnan(back.g[2][:,~old.leaf_masks[2]]).all())
|
||||||
self.assertEqual(old.pinned_rectangle,A[0])
|
self.assertEqual(old.pinned_rectangles,(A[0],))
|
||||||
|
|
||||||
def test_current_jets_and_source_independence(self):
|
def test_current_jets_and_source_independence(self):
|
||||||
run,module,allocations,storage,objects=ready()
|
run,module,allocations,storage,objects=ready()
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""Manual GPU gates for two pinned bodies and several fluid-only L2 regions.
|
||||||
|
|
||||||
|
Run explicitly on an approved idle GPU. The CPU helper checks immediate
|
||||||
|
whole-population transfer at the same physical time, not independent evolution
|
||||||
|
accuracy. Continuation checks cover finite leaves and no-op bitwise identity;
|
||||||
|
they do not establish an independent BFL replay or a numerical error bound.
|
||||||
|
"""
|
||||||
|
from contextlib import ExitStack
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||||
|
from CelerisLab.body.jets import Jet, Parallel, Tube
|
||||||
|
from CelerisLab.body.objects import SimObject
|
||||||
|
from CelerisLab.cuda.context import CudaContext
|
||||||
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||||
|
from CelerisLab.lbm.synchronized_remap import (
|
||||||
|
RemapTopology, remap_synchronized, same_physical_boundary,
|
||||||
|
)
|
||||||
|
from test_refinement_blocked_base import exact
|
||||||
|
from test_refinement_three_level_multipatch_gpu import CFG, PARENTS
|
||||||
|
from test_runtime_child_remap_gpu import RuntimeChildRemapGPU, initial
|
||||||
|
|
||||||
|
|
||||||
|
# All bounds use the allocated L1 frame, with support aligned to 4x4 blocks.
|
||||||
|
PINS = ((16, 16, 32, 24), (56, 16, 72, 24))
|
||||||
|
FLUID = ((16, 32, 32, 40), (56, 32, 72, 40))
|
||||||
|
EXTRA = ((16, 44, 32, 48),)
|
||||||
|
MOVED = ((20, 32, 36, 40), (60, 32, 76, 40))
|
||||||
|
START = PINS + FLUID
|
||||||
|
CHANGES = (
|
||||||
|
('add', START + EXTRA, (.017, -.011)),
|
||||||
|
('move', PINS + MOVED + EXTRA, (-.009, .005)),
|
||||||
|
('remove', PINS + MOVED, (.013, -.007)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make(context):
|
||||||
|
centers = ((12., 10.), (32., 10.))
|
||||||
|
objects = tuple(
|
||||||
|
SimObject(71 + index, geometry=CircleGeometry(*center, .8),
|
||||||
|
center=center, radius=.8)
|
||||||
|
for index, center in enumerate(centers)
|
||||||
|
)
|
||||||
|
jets = tuple(
|
||||||
|
Jet(Tube((x - 1.5, y), 0., 3., .3), Parallel(0.), 'parabolic', amplitude)
|
||||||
|
for (x, y), amplitude in zip(centers, (.003, -.002))
|
||||||
|
)
|
||||||
|
run = FixedPatchStepper(
|
||||||
|
CFG, PARENTS, context=context, base_block=(4, 4), fine_block=(4, 4),
|
||||||
|
child_block=(4, 4), child_rectangles_in_parent=START,
|
||||||
|
child_objects=objects, child_jets=jets,
|
||||||
|
)
|
||||||
|
return run, objects
|
||||||
|
|
||||||
|
|
||||||
|
def topology(run, rectangles, objects):
|
||||||
|
return RemapTopology.from_config(
|
||||||
|
CFG, run.patch, rectangles, child_objects=objects,
|
||||||
|
child_jets=run._boundaries[2].jets,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def population_integrals(mesh, arrays):
|
||||||
|
"""Nine separate area integrals in the root frame, accumulated in FP64."""
|
||||||
|
return sum(array[:, leaf].sum(axis=1, dtype=np.float64) / 4**level
|
||||||
|
for level, (array, leaf) in enumerate(zip(arrays, mesh.leaf_masks)))
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplePinnedRemapGPU(unittest.TestCase):
|
||||||
|
check_public = RuntimeChildRemapGPU.check_public
|
||||||
|
check_reset = RuntimeChildRemapGPU.check_reset
|
||||||
|
|
||||||
|
def change_amplitudes(self, run, amplitudes):
|
||||||
|
# Each jet selects a different body. Updating one must preserve the
|
||||||
|
# other body's current per-link velocities, including its driven links.
|
||||||
|
boundary = run._boundaries[2]
|
||||||
|
body_ids = run.child_boundary.curved_host[3]
|
||||||
|
for index, amplitude in enumerate(amplitudes):
|
||||||
|
before = boundary.tables['surface_velocity'].copy()
|
||||||
|
run.set_jet_amplitude(index, amplitude, level=2)
|
||||||
|
after = boundary.tables['surface_velocity']
|
||||||
|
exact(self, after[body_ids != index], before[body_ids != index],
|
||||||
|
'other body velocity unchanged')
|
||||||
|
self.assertTrue(np.any(after[body_ids == index] != before[body_ids == index]))
|
||||||
|
self.assertEqual(tuple(jet.amplitude for jet in boundary.jets), amplitudes)
|
||||||
|
|
||||||
|
def check_boundary(self, old, new, mesh, amplitudes):
|
||||||
|
self.assertEqual(mesh.pinned_rectangles, PINS)
|
||||||
|
self.assertTrue(same_physical_boundary(old.child_boundary, new.child_boundary))
|
||||||
|
self.assertTrue(same_physical_boundary(new.child_boundary, mesh.boundary))
|
||||||
|
# Compare physical cut-link rows, never layout-dependent blocked addresses.
|
||||||
|
for run in (old, new):
|
||||||
|
boundary = run._boundaries[2]
|
||||||
|
self.assertEqual(tuple(jet.amplitude for jet in boundary.jets), amplitudes)
|
||||||
|
exact(self, boundary.tables['surface_velocity'], mesh.surface_velocity,
|
||||||
|
'latest physical per-link velocities')
|
||||||
|
|
||||||
|
def test_noop_next_step_bitwise(self):
|
||||||
|
with CudaContext(0) as context, ExitStack() as owned:
|
||||||
|
old, objects = make(context)
|
||||||
|
owned.enter_context(old)
|
||||||
|
old.initialize(*initial(old, False))
|
||||||
|
old.step(2)
|
||||||
|
amplitudes = (.017, -.011)
|
||||||
|
self.change_amplitudes(old, amplitudes)
|
||||||
|
before, count = old.download(), old.step_count
|
||||||
|
mesh = topology(old, START, objects)
|
||||||
|
# Input order is irrelevant even with several pins and fluid regions.
|
||||||
|
new = owned.enter_context(old.remap_child(START[::-1], child_objects=objects))
|
||||||
|
self.assertIsNot(old, new)
|
||||||
|
self.assertEqual((old.step_count, new.step_count), (count, count))
|
||||||
|
self.check_boundary(old, new, mesh, amplitudes)
|
||||||
|
self.check_reset(new, before)
|
||||||
|
for got, want in zip(old.download(), before):
|
||||||
|
exact(self, got, want, 'no-op source unchanged')
|
||||||
|
old.step()
|
||||||
|
new.step()
|
||||||
|
self.assertEqual((old.step_count, new.step_count), (count + 1, count + 1))
|
||||||
|
for got, want in zip(new.download(), old.download()):
|
||||||
|
exact(self, got, want, 'no-op next step bitwise')
|
||||||
|
self.check_public(old)
|
||||||
|
self.check_public(new)
|
||||||
|
|
||||||
|
def test_add_move_remove_immediate_transfer_and_source_independence(self):
|
||||||
|
with CudaContext(0) as context, ExitStack() as owned:
|
||||||
|
current, objects = make(context)
|
||||||
|
owned.enter_context(current)
|
||||||
|
current.initialize(*initial(current, False))
|
||||||
|
current.step(2)
|
||||||
|
for action, rectangles, amplitudes in CHANGES:
|
||||||
|
with self.subTest(action=action):
|
||||||
|
previous = current
|
||||||
|
self.change_amplitudes(previous, amplitudes)
|
||||||
|
before, count = previous.download(), previous.step_count
|
||||||
|
old_mesh = topology(previous, previous.child_patch.bounds, objects)
|
||||||
|
new_mesh = topology(previous, rectangles, objects)
|
||||||
|
expected = remap_synchronized(
|
||||||
|
old_mesh, new_mesh, before, base_count=count,
|
||||||
|
level_steps=(count, 2 * count, 4 * count),
|
||||||
|
phase='postcollision_g',
|
||||||
|
).g
|
||||||
|
current = owned.enter_context(
|
||||||
|
previous.remap_child(rectangles, child_objects=objects))
|
||||||
|
self.assertIsNot(previous, current)
|
||||||
|
self.assertEqual((previous.step_count, current.step_count), (count, count))
|
||||||
|
self.check_boundary(previous, current, new_mesh, amplitudes)
|
||||||
|
self.check_reset(current, expected)
|
||||||
|
actual = current.download()
|
||||||
|
for got, want, source, old_leaf, new_leaf in zip(
|
||||||
|
actual, expected, before, old_mesh.leaf_masks, new_mesh.leaf_masks):
|
||||||
|
exact(self, got, want, 'immediate CPU transfer parity')
|
||||||
|
exact(self, got[:, old_leaf & new_leaf], source[:, old_leaf & new_leaf],
|
||||||
|
'same-level physical fluid overlap')
|
||||||
|
self.assertTrue(np.isfinite(got[:, new_leaf]).all())
|
||||||
|
self.assertTrue(np.isnan(got[:, ~new_leaf]).all())
|
||||||
|
# Conservation is asserted only across remap, not collision.
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
population_integrals(new_mesh, actual),
|
||||||
|
population_integrals(old_mesh, before),
|
||||||
|
rtol=2 * np.finfo(np.float32).eps, atol=0,
|
||||||
|
err_msg='all nine area-weighted population integrals',
|
||||||
|
)
|
||||||
|
self.check_public(current)
|
||||||
|
current.step()
|
||||||
|
self.assertEqual((previous.step_count, current.step_count), (count, count + 1))
|
||||||
|
for got, want in zip(previous.download(), before):
|
||||||
|
exact(self, got, want, 'source unchanged after candidate advances')
|
||||||
|
self.check_boundary(previous, current, new_mesh, amplitudes)
|
||||||
|
self.check_public(current)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user