"""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) == []