6c740b5Continue a run from a .cymsim file (cymsim run --resume, simulate(cfg, resume=...)), identical to the uninterrupted run; keep the outputs past the duration in the state and store probe signals in the fileJeremy Magland 1"""Continuing a run from a .cymsim file reproduces the uninterrupted run exactly."""
2import copy
4import h5py
5import numpy as np
6import pytest
8from cymsim.cli import main
9from cymsim.cymsim_file import CymsimWriter, load_cymsim
10from cymsim.simulate import Simulator
13def _config(base, duration):
14 cfg = copy.deepcopy(base)
15 cfg.numerics.duration = duration
16 cfg.output.record_acceleration = True
17 cfg.output.displacement_rate = 200.0
18 cfg.output.probes = [(0.1, 0.05), (0.05, -0.12)]
19 return cfg
22def _datasets(path) -> dict:
23 """Every dataset of a .cymsim file except the energy log, whose report times depend on how
24 the run was split."""
25 out = {}
26 with h5py.File(path) as f:
27 f.visititems(lambda name, obj: out.__setitem__(name, obj[()]) if isinstance(obj, h5py.Dataset) else None)
28 out.pop("energy/table")
29 return out
32def _assert_same_files(a, b):
33 da, db = _datasets(a), _datasets(b)
34 assert sorted(da) == sorted(db)
35 for name in da:
36 assert np.array_equal(da[name], db[name]), name
37 with h5py.File(a) as fa, h5py.File(b) as fb:
38 for key in ("n_steps_done", "complete", "duration", "n_samples"):
39 assert fa["run"].attrs[key] == fb["run"].attrs[key], key
43def straight(coarse_config, tmp_path_factory):
44 path = tmp_path_factory.mktemp("straight") / "straight.cymsim"
45 Simulator(_config(coarse_config, 0.05)).run(save_path=str(path))
46 return path
49def test_continued_run_equals_the_uninterrupted_one(coarse_config, straight, tmp_path):
50 prev = None
51 for k, duration in enumerate((0.02, 0.035, 0.05)):
52 path = tmp_path / f"seg{k}.cymsim"
53 Simulator(_config(coarse_config, duration)).run(save_path=str(path), resume=prev)
54 prev = load_cymsim(path)
55 assert prev.complete and len(prev.pressure_rayleigh) == round(44100 * duration)
56 assert len(prev.state.overhang_front) > 0 # the overshoot past the duration is kept
57 _assert_same_files(path, straight)
58 assert len(prev.energy_log) > len(load_cymsim(straight).energy_log)
61def test_continue_from_a_killed_run(coarse_config, straight, tmp_path, monkeypatch):
62 """A run killed right after a partial save continues from the partial file."""
63 path = tmp_path / "killed.cymsim"
64 update, calls = CymsimWriter.update, []
66 def update_then_die(self, result):
67 update(self, result)
68 calls.append(1)
69 if len(calls) == 3:
70 self.close()
71 raise KeyboardInterrupt
73 monkeypatch.setattr(CymsimWriter, "update", update_then_die)
74 with pytest.raises(KeyboardInterrupt):
75 Simulator(_config(coarse_config, 0.05)).run(save_path=str(path), save_every=0)
76 monkeypatch.undo()
77 part = load_cymsim(path)
78 assert not part.complete and 0 < part.n_steps_done
79 out = tmp_path / "continued.cymsim"
80 Simulator(_config(coarse_config, 0.05)).run(save_path=str(out), resume=part)
81 _assert_same_files(out, straight)
84def test_shorter_duration_cuts_the_records(coarse_config, straight, tmp_path):
85 short = Simulator(_config(coarse_config, 0.03)).run()
86 cut = Simulator(_config(coarse_config, 0.03)).run(resume=load_cymsim(straight))
87 n = round(44100 * 0.03)
88 assert len(cut.pressure_rayleigh) == n and cut.complete
89 assert np.array_equal(cut.pressure_rayleigh.astype(np.float32), short.pressure_rayleigh.astype(np.float32))
90 assert np.array_equal(cut.acceleration.data, short.acceleration.data)
91 assert np.array_equal(cut.displacement.w, short.displacement.w)
92 assert np.array_equal(cut.probe_acceleration.astype(np.float32), short.probe_acceleration.astype(np.float32))
95def test_a_different_configuration_is_refused(coarse_config, straight):
96 cfg = _config(coarse_config, 0.08)
97 cfg.hits[0].velocity *= 1.001
98 with pytest.raises(ValueError, match=r"hits"):
99 Simulator(cfg).run(resume=load_cymsim(straight))
100 cfg = _config(coarse_config, 0.08)
101 cfg.output.prefix = "elsewhere" # output naming may change
102 Simulator(cfg).check_resumable(load_cymsim(straight))
105def test_cli_resume(coarse_config, straight, tmp_path, capsys):
106 cfg = _config(coarse_config, 0.05)
107 cfg.output.wav = cfg.output.npz = cfg.output.cascade_plot = False
108 cfg.output.prefix = str(tmp_path / "x")
109 ypath = tmp_path / "c.yaml"
110 cfg.to_yaml(ypath)
111 out = tmp_path / "longer.cymsim"
112 assert main(["run", str(ypath), "--resume", str(straight), "--duration", "0.06", "-o", str(out), "--quiet"]) == 0
113 assert load_cymsim(out).complete and len(load_cymsim(out).pressure_rayleigh) == round(44100 * 0.06)
114 cfg.hits[0].r += 0.01
115 cfg.to_yaml(ypath)
116 assert main(["run", str(ypath), "--resume", str(straight), "-o", str(out), "--quiet"]) == 2
117 assert "hits" in capsys.readouterr().err
483cb51Store signals (audio, probes, displacement frames) in the run's precision, float64 for a float64 run; run the tests on the CPU, where runs are reproducible bit for bitJeremy Magland 118
120def test_float64_runs_store_float64_signals(coarse_config, tmp_path):
121 """A float64 run keeps its signals in float64, and continues exactly."""
122 def cfg(duration):
123 c = _config(coarse_config, duration)
124 c.numerics.dtype = "float64"
125 return c
127 straight = tmp_path / "straight.cymsim"
128 Simulator(cfg(0.035)).run(save_path=str(straight))
129 with h5py.File(straight) as f:
130 for name in ("audio/baffled", "audio/unbaffled", "probes/normal_acceleration", "displacement/w"):
131 assert f[name].dtype == np.float64, name
132 res = load_cymsim(straight)
133 assert res.displacement.w.dtype == np.float64
134 first = tmp_path / "first.cymsim"
135 Simulator(cfg(0.02)).run(save_path=str(first))
136 out = tmp_path / "continued.cymsim"
137 Simulator(cfg(0.035)).run(save_path=str(out), resume=load_cymsim(first))
138 _assert_same_files(out, straight)