Support bounded shared transverse motion and steady pinball demo
This commit is contained in:
1 parent
d0968e6e91
commit
33dcb422c6
8 files changed
+546
No files matched your search
@@ -0,0 +1,60 @@
|
||||
"""Small serial GPU gates for shared transverse circles; no physical benchmark."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab import Simulation
|
||||
from CelerisLab.body.motion import MovingTables, Translation
|
||||
|
||||
CENTRES = ((70., 79.5), (96., 94.5), (96., 64.5))
|
||||
|
||||
|
||||
def simulate(tmp_path, *, moving, offsets, reverse=False):
|
||||
config = json.loads((Path(__file__).resolve().parents[2]/'research/steady_pinball/config.json').read_text())
|
||||
config['grid'].update(nx=192, ny=160)
|
||||
p = tmp_path/'config.json'
|
||||
p.write_text(json.dumps(config))
|
||||
with Simulation(str(p)) as sim:
|
||||
envelope = Translation((0., 0., -20., 20.), .5, shared=True)
|
||||
centres = list(reversed(CENTRES)) if reverse else list(CENTRES)
|
||||
ids = [sim.add_body('circle', c, 10., translation=envelope if moving else None) for c in centres]
|
||||
sim.initialize()
|
||||
previous = 0.
|
||||
crossings = 0.
|
||||
for y in offsets:
|
||||
if moving:
|
||||
for bid in ids:
|
||||
sim.set_body(bid, offset=(0., float(y)), vy=float(y-previous))
|
||||
sim.run(1)
|
||||
if moving:
|
||||
diag = sim.moving_diagnostics()
|
||||
assert np.isfinite(diag).all()
|
||||
assert np.all(diag[:, MovingTables.DIAG.index('no_donor')] == 0)
|
||||
crossings += diag[:, MovingTables.DIAG.index('crossings')].sum()
|
||||
previous = y
|
||||
forces = np.array([sim.read_force(bid) for bid in ids])
|
||||
if reverse:
|
||||
forces = forces[::-1]
|
||||
return sim.get_ddf(), sim.get_flags(), forces, crossings
|
||||
|
||||
|
||||
def test_fixed_vs_shared_hold(tmp_path):
|
||||
fixed = simulate(tmp_path, moving=False, offsets=[0.]*32)
|
||||
held = simulate(tmp_path, moving=True, offsets=[0.]*32)
|
||||
assert np.array_equal(fixed[0], held[0], equal_nan=True)
|
||||
assert np.array_equal(fixed[1], held[1])
|
||||
# FP32 atomic force reduction order only; populations remain exactly equal.
|
||||
np.testing.assert_allclose(fixed[2], held[2], rtol=0., atol=1e-4)
|
||||
assert held[3] == 0
|
||||
|
||||
|
||||
def test_common_motion_member_order_has_no_population_effect(tmp_path):
|
||||
offsets = np.concatenate((np.arange(.5, 20.5, .5), np.arange(19.5, -20.5, -.5),
|
||||
np.arange(-19.5, .5, .5)))
|
||||
forward = simulate(tmp_path, moving=True, offsets=offsets)
|
||||
reverse = simulate(tmp_path, moving=True, offsets=offsets, reverse=True)
|
||||
assert forward[3] > 0 and forward[3] == reverse[3]
|
||||
assert np.array_equal(forward[0], reverse[0], equal_nan=True)
|
||||
assert np.array_equal(forward[1], reverse[1])
|
||||
np.testing.assert_allclose(forward[2], reverse[2], rtol=0., atol=1e-4)
|
||||
@@ -0,0 +1,122 @@
|
||||
"""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
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Real config loading and pure structural update for the frozen development case."""
|
||||
import importlib.util
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
path = Path(__file__).resolve().parents[2] / 'research/steady_pinball/run.py'
|
||||
spec = importlib.util.spec_from_file_location('steady_pinball_run', path)
|
||||
runner = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(runner)
|
||||
|
||||
|
||||
def test_actual_loader_and_paper_geometry(tmp_path):
|
||||
import json
|
||||
from CelerisLab.config import load_lbm_config
|
||||
p = tmp_path/'config.json'
|
||||
p.write_text(json.dumps(runner.config()))
|
||||
cfg = load_lbm_config(str(p))
|
||||
cfg.validate()
|
||||
assert (cfg.nx, cfg.ny, cfg.viscosity, cfg.velocity) == (1280, 512, .004, .01)
|
||||
assert cfg.collision == 'MRT' and cfg.y_wall_bc == 'bounce_back'
|
||||
assert runner.U * runner.D / runner.NU == 50.
|
||||
assert runner.CENTRES == ((600., 255.5), (626., 270.5), (626., 240.5))
|
||||
|
||||
|
||||
def test_structure_rest_equilibrium_and_energy_decay():
|
||||
s = runner.Structure()
|
||||
assert s.candidate(0.) == (0., 0.)
|
||||
assert s.mass == pytest.approx(10*3*math.pi*100)
|
||||
s.y, s.v = .2, 0.
|
||||
# One damped period: independent continuous solution at total elapsed time.
|
||||
for _ in range(runner.PERIOD):
|
||||
s.y, s.v = s.candidate(0.)
|
||||
t = runner.PERIOD
|
||||
a = s.zeta*s.omega
|
||||
w = s.omega*math.sqrt(1-s.zeta**2)
|
||||
expected = .2*math.exp(-a*t)*(math.cos(w*t)+a/w*math.sin(w*t))
|
||||
assert s.y == pytest.approx(expected, abs=2e-12)
|
||||
assert .5*s.mass*s.v*s.v+.5*s.stiffness*s.y*s.y < .5*s.stiffness*.2**2
|
||||
s.y, s.v = .3, 0.
|
||||
assert s.candidate(s.stiffness*.3) == pytest.approx((.3, 0.), abs=1e-15)
|
||||
|
||||
|
||||
def test_report_uses_completed_rows_and_net_force():
|
||||
rows = np.zeros((4, 15))
|
||||
rows[:, 0] = np.arange(1, 5)
|
||||
rows[:, 1] = 2.
|
||||
rows[:, 3:9] = 1.
|
||||
got = runner.summarize(rows)
|
||||
assert got[0]['end_step'] == 4
|
||||
assert got[0]['y_rms_D'] == .1
|
||||
assert got[0]['fy_rms'] == 6.
|
||||
Reference in new issue
Block a user