Files
CelerisLab/tests/unit/test_shared_translation.py
T

123 lines
5.2 KiB
Python

"""Host admission and independent lattice-stencil checks; no CUDA context."""
from types import SimpleNamespace
import numpy as np
import pytest
from CelerisLab.body.motion import MovingCircle, MovingTables, Translation, validate_shared_translations
from CelerisLab.lbm.descriptors import FLUID
from tests.unit.test_moving_bodies import STREAM, body, moving_simulation, routed_simulation
def scene(monkeypatch, envelope=None, gap=10.):
sim = routed_simulation(monkeypatch)
envelope = envelope or Translation((0., 0., -6., 6.), .5, shared=True)
for y in (40. - (20. + gap) / 2., 40. + (20. + gap) / 2.):
sim.bodies.add(body(60., y, 10., envelope))
sim.field = SimpleNamespace(nx=120, ny=80, build_channel_flags=lambda: np.full((80, 120), FLUID))
return sim
def test_shared_bands_are_admitted_but_other_groups_are_not(monkeypatch):
sim = scene(monkeypatch)
sim._validate_moving_bodies()
a, b = [o.motion for o in sim.bodies.objects]
assert a.band()[3] >= b.band()[2] # whole envelopes genuinely overlap
band, geom = MovingTables.layout([(a, 0), (b, 1)])
assert band[10] == band[2] * band[3] * 8
assert len(geom) == 14
b.translation = Translation((0., 0., -6., 6.), .5, shared=True)
with pytest.raises(ValueError, match='overlaps body'):
sim._validate_moving_bodies() # equal values do not link independent motion
b.translation = a.translation
sim.bodies.add(body(60., 40., 2.))
with pytest.raises(ValueError, match='overlaps body'):
sim._validate_moving_bodies()
@pytest.mark.parametrize('gap', (.3, 4.))
def test_small_surface_gaps_rejected(monkeypatch, gap):
with pytest.raises(ValueError, match='clearance'):
scene(monkeypatch, gap=gap)._validate_moving_bodies()
def test_shared_envelope_is_transverse_only():
with pytest.raises(ValueError, match='transverse'):
Translation((-.1, .1, -1., 1.), .5, shared=True)
envelope = Translation((0., 0., -1., 1.), .5, shared=True)
with pytest.raises(ValueError, match="zero x velocity"):
MovingCircle((0., 0.), 10., envelope).check_target((0., .1), (.1, .1))
def test_group_rejection_is_before_writes_and_can_be_corrected(monkeypatch):
sim, writes = moving_simulation(monkeypatch)
env = Translation((0., 0., -2., 2.), .5, shared=True)
sim.bodies.objects = [body(40., y, 10., env) for y in (20., 50.)]
a, b = [o.motion for o in sim.bodies.objects]
sim.stepper.moving = object()
a.set_target((0., .2), (0., .2))
with pytest.raises(ValueError, match='identical'):
sim.run(1, stream=STREAM)
assert writes == [] and a.applied == b.applied == (0., 0.)
b.set_target((0., .2), (0., .1))
with pytest.raises(ValueError, match='identical'):
sim.run(1, stream=STREAM)
assert writes == []
b.set_target((0., .2), (0., .2))
validate_shared_translations([a, b])
b.applied = (0., -.1)
with pytest.raises(ValueError, match='identical'):
validate_shared_translations([a, b])
C = np.array([(x, y) for x in (-1, 0, 1) for y in (-1, 0, 1) if (x, y) != (0, 0)])
def footprint_conflicts(centres, radius, dy):
"""Enumerate geometric write/read supports, not refill/BFL formulas.
Tests every candidate donor, conservatively including M2's second link.
Body-indexed slot numbers are separate from physical (cell,direction) writes.
"""
centres = np.asarray(centres, float)
lo = np.floor(centres.min(0) - radius - 4).astype(int)
hi = np.ceil(centres.max(0) + radius + 4).astype(int)
yy, xx = np.mgrid[lo[1]:hi[1]+1, lo[0]:hi[0]+1]
points = np.column_stack((xx.ravel(), yy.ravel()))
def inside(p, c):
return ((p - c)**2).sum(axis=-1) < radius**2
old = np.array([inside(points, c) for c in centres])
new_centres = centres + (0., dy)
new = np.array([inside(points, c) for c in new_centres])
changed = old ^ new
conflicts = int(np.count_nonzero(changed.sum(0) > 1))
writes = set()
for i, (oc, nc) in enumerate(zip(centres, new_centres)):
others = [j for j in range(len(centres)) if j != i]
fresh = points[old[i] & ~new[i]]
for e in C:
donor = fresh + e
valid = ~inside(donor, oc) & ~inside(donor, nc)
for j in others:
conflicts += int(np.count_nonzero(valid & (inside(donor, centres[j]) | inside(donor, new_centres[j]))))
link = ~new[i] & inside(points + e, nc)
fluid = points[link]
solid = fluid + e
for p in solid:
slot = (int(p[0]), int(p[1]), int(e[0]), int(e[1]))
conflicts += int(slot in writes)
writes.add(slot)
for read in (fluid, fluid - e):
for j in others:
conflicts += int(np.count_nonzero(inside(read, centres[j]) | inside(read, new_centres[j])))
return conflicts
def test_counterexample_and_pinball_local_ownership():
# Close but disjoint circles are insufficient: parent/Grok independent example.
assert footprint_conflicts(((.25, .25), (.25, 8.55)), 4., .5) > 0
for phase in (0., .25, .5, .75):
for dy in (-.5, -.1, .1, .5):
centres = np.array(((0., 0.), (26., 15.), (26., -15.))) + phase
assert footprint_conflicts(centres, 10., dy) == 0