123 lines
5.2 KiB
Python
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
|