"""Cache keys: independent of the duration and the output naming, of whether a default is written out, and consistent with which runs can continue one another.""" import copy import hashlib import json import re import yaml from cymsim.cache import MODEL_VERSION, cache_key, key_fields from cymsim.cli import main from cymsim.config import ForceExciter, Hit, MalletExciter, SimulationConfig, StickExciter, default_config # sha256 of the full default configuration. When this test fails, a default has changed or a field # was added. If the results of some existing configuration change, bump MODEL_VERSION in cache.py; # either way, update the fingerprint. DEFAULTS_FINGERPRINT = "4b9d969c01b47ddabc6ee40ac7315d0c1ed1ac998f4a95b120ed4e668268672c" def test_key_format(): assert re.fullmatch(rf"v{MODEL_VERSION}/[0-9a-f]{{64}}", cache_key(SimulationConfig())) def test_continuation_fields_do_not_change_the_key(): a = default_config("stick") b = copy.deepcopy(a) b.numerics.duration = 7.5 b.numerics.n_progress = 3 b.output.prefix, b.output.wav, b.output.npz, b.output.cascade_plot = "other", False, False, False assert cache_key(a) == cache_key(b) def test_written_out_defaults_do_not_change_the_key(tmp_path): full = tmp_path / "full.yaml" default_config("stick").to_yaml(full) # every field written out short = tmp_path / "short.yaml" h = default_config("stick").hits[0] short.write_text(yaml.safe_dump({"hits": [{"r": h.r, "phi": h.phi, "velocity": 7, "exciter": {"kind": "stick"}}]})) assert cache_key(SimulationConfig.from_yaml(full)) == cache_key(SimulationConfig.from_yaml(short)) assert cache_key(SimulationConfig.from_yaml(full)) == cache_key(default_config("stick")) def test_physical_and_numerical_changes_change_the_key(): base = default_config("mallet") variants = [] for change in (lambda c: setattr(c.hits[0], "velocity", 8.0001), lambda c: setattr(c.hits[0], "exciter", StickExciter()), lambda c: setattr(c.hits[0], "exciter", ForceExciter()), lambda c: c.hits.append(Hit(time=0.5)), lambda c: setattr(c.material, "E", 1.2e11), lambda c: setattr(c.imperfections, "seed", 2), lambda c: setattr(c.numerics, "dtype", "float64"), lambda c: setattr(c.numerics, "steps_per_block", 10000), lambda c: setattr(c.output, "probes", [(0.1, 0.0)]), lambda c: setattr(c.output, "record_acceleration", True), lambda c: setattr(c.listener, "position", (0.4, 0.3, 1.0))): c = copy.deepcopy(base) change(c) variants.append(cache_key(c)) assert len(set(variants + [cache_key(base)])) == len(variants) + 1 def test_exciters_at_their_defaults_are_told_apart(): keys = {cache_key(SimulationConfig(hits=[Hit(exciter=e)])) for e in (MalletExciter(), StickExciter(), ForceExciter())} assert len(keys) == 3 assert key_fields(SimulationConfig(hits=[Hit(exciter=StickExciter())])) == {"hits": [{"exciter": {"kind": "stick"}}]} def test_equal_keys_mean_continuable_runs(): """The key and Simulator.check_resumable compare the same thing.""" base = default_config("stick") cfgs = [base] for change in (lambda c: setattr(c.numerics, "duration", 1.0), lambda c: setattr(c.output, "prefix", "z"), lambda c: setattr(c.hits[0], "phi", 1.0), lambda c: setattr(c.output, "displacement_rate", 0.0)): c = copy.deepcopy(base) change(c) cfgs.append(c) for a in cfgs: for b in cfgs: assert (cache_key(a) == cache_key(b)) == (a.identity_dict() == b.identity_dict()) def test_defaults_fingerprint(): text = json.dumps(SimulationConfig().to_dict(), sort_keys=True, separators=(",", ":")) assert hashlib.sha256(text.encode()).hexdigest() == DEFAULTS_FINGERPRINT, ( "the default configuration changed: bump MODEL_VERSION in cache.py if results of existing " "configurations change, then update DEFAULTS_FINGERPRINT") def test_cli_cache_key(tmp_path, capsys): path = tmp_path / "c.yaml" default_config("stick").to_yaml(path) assert main(["cache-key", str(path), "--show"]) == 0 out = capsys.readouterr().out.splitlines() assert out[0] == cache_key(default_config("stick")) assert '"hits": [' in "\n".join(out)