Support bounded shared transverse motion and steady pinball demo

This commit is contained in:
Frank14f committed 2026-10-31 13:58:19 +08:00
1 parent d0968e6e91
commit 33dcb422c6
8 files changed
+546

No files matched your search

+122
View File
@@ -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
+54
View File
@@ -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.