Files
CelerisLab/tests/unit/test_steady_pinball.py
T

129 lines
5.3 KiB
Python

"""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(zeta=.01)
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.
@pytest.mark.parametrize('case', ['Z0', 'M2', 'R2'])
def test_zero_damping_cases_conserve_unforced_energy_and_match_forced_solution(case):
s = runner.Structure(*runner.STRUCTURES[case])
assert s.zeta == s.damping == 0.
s.y, s.v = .2, 1.e-5
y0, v0 = s.y, s.v
energy = .5*s.mass*v0**2 + .5*s.stiffness*y0**2
for _ in range(12345):
s.y, s.v = s.candidate(0.)
assert .5*s.mass*s.v**2 + .5*s.stiffness*s.y**2 == pytest.approx(energy, rel=2e-11)
force, elapsed = 1.e-4, 3210
s.y, s.v = y0, v0
for _ in range(elapsed):
s.y, s.v = s.candidate(force)
phase = s.omega*elapsed
equilibrium = force/s.stiffness
expected_y = equilibrium+(y0-equilibrium)*math.cos(phase)+v0*math.sin(phase)/s.omega
expected_v = -(y0-equilibrium)*s.omega*math.sin(phase)+v0*math.cos(phase)
assert (s.y, s.v) == pytest.approx((expected_y, expected_v), abs=2e-11)
def test_predeclared_comparisons_change_structure_only():
z, m, r = [runner.Structure(*runner.STRUCTURES[k]) for k in ['Z0', 'M2', 'R2']]
assert m.mass/z.mass == pytest.approx(.2)
assert m.stiffness/z.stiffness == pytest.approx(.2)
assert r.mass == m.mass
assert r.stiffness/m.stiffness == pytest.approx(.36)
assert [runner.STEPS*s.frequency for s in [z, m, r]] == pytest.approx([60., 60., 36.])
@pytest.mark.parametrize('diameter', [20, 40, 60])
def test_diffusive_scaling_preserves_physical_geometry_and_duration(tmp_path, diameter):
import json
from CelerisLab.config import load_lbm_config
scale = diameter/20
raw = runner.config(diameter)
p = tmp_path/'scaled.json'
p.write_text(json.dumps(raw))
cfg = load_lbm_config(str(p))
cfg.validate()
assert cfg.velocity*diameter/cfg.viscosity == pytest.approx(50.)
assert (cfg.nx-1)/diameter == 1279/20
assert (cfg.ny-2)/diameter == 510/20
for original, mapped in zip(runner.CENTRES+runner.PROBES,
runner.scaled_points(runner.CENTRES+runner.PROBES, scale)):
assert mapped[0]/diameter == pytest.approx(original[0]/20)
assert (mapped[1]-.5)/diameter == pytest.approx((original[1]-.5)/20)
old = runner.Structure(*runner.STRUCTURES['M2'])
new = runner.Structure(2., old.frequency/scale**2, diameter=diameter)
assert new.mass/old.mass == pytest.approx(scale**2)
assert new.stiffness/old.stiffness == pytest.approx(1/scale**2)
assert new.damping == 0.
assert cfg.velocity/(new.frequency*diameter) == pytest.approx(5.)
assert runner.STEPS*scale**2*cfg.velocity/diameter == pytest.approx(300.)
assert runner.WARMUP*scale**2*cfg.velocity/diameter == pytest.approx(256.)
# A physical held force maps as F_fine=F_coarse/scale in two dimensions.
old.y, old.v = .3, .0002
new.y, new.v = scale*old.y, old.v/scale
expected = old.candidate(.0001)
for _ in range(round(scale**2)):
new.y, new.v = new.candidate(.0001/scale)
assert (new.y/scale, new.v*scale) == pytest.approx(expected, abs=2e-12)
def test_scaled_summary_uses_requested_units_and_completed_rows():
rows = np.zeros((5, 15))
rows[:, 0] = np.arange(1, 6)
rows[:, 1] = 4.
got = runner.summarize(rows, diameter=40, block_steps=3)
assert [(x['start_step'], x['end_step']) for x in got] == [(1, 3), (4, 5)]
assert all(x['y_rms_D'] == .1 for x in got)
assert runner.summarize(rows[:0], diameter=40) == []