/ proof-of-concept / cymsim
Sign in
proof-of-concept / cymsim
cymsim / src / cymsim / cli.py
178 lines · 7.4 KBBlameHistoryRaw
1"""Command line interface: ``cymsim simulate`` (alias ``run``), ``cymsim check``, ``cymsim info``,
2``cymsim cache-key`` and ``cymsim example-config``."""
3from __future__ import annotations
5import argparse
6import logging
7import os
8import sys
9import time
11from .config import SimulationConfig, default_config
14def _setup_logging(quiet: bool) -> None:
15 logging.basicConfig(level=logging.WARNING if quiet else logging.INFO,
16 format="%(message)s", stream=sys.stdout)
19def _load(path: str, args) -> SimulationConfig:
20 cfg = SimulationConfig.from_yaml(path)
21 if getattr(args, "duration", None) is not None:
22 cfg.numerics.duration = args.duration
23 if getattr(args, "out_prefix", None) is not None:
24 cfg.output.prefix = args.out_prefix
25 if getattr(args, "linear", False):
26 cfg.numerics.nonlinear = False
27 if getattr(args, "record_acceleration", False):
28 cfg.output.record_acceleration = True
29 return cfg
32def cmd_simulate(args) -> int:
33 from .diagnostics import analyze_cascade, format_cascade_report, plot_cascade
34 from .simulate import Simulator
36 t0 = time.time()
37 cfg = _load(args.config, args)
38 cache = None if args.no_cache or args.resume is not None else args.cache or os.environ.get("CYMSIM_CACHE_URL")
39 if args.cache and args.resume is not None:
40 print("give either --cache or --resume, not both", file=sys.stderr)
41 return 2
42 if cache:
43 from .cache import cached_run
44 every = {} if args.upload_every is None else {"upload_every": args.upload_every}
45 result = cached_run(cfg, cache, save_path=args.output, save_every=args.save_every, **every)
46 else:
47 sim = Simulator(cfg)
48 prev = None
49 if args.resume is not None:
50 from .cymsim_file import load_cymsim
51 prev = load_cymsim(args.resume)
52 try:
53 sim.check_resumable(prev)
54 except ValueError as e:
55 print(f"cannot continue {args.resume}: {e}", file=sys.stderr)
56 return 2
57 result = sim.run(save_path=args.output, save_every=args.save_every, resume=prev)
58 prefix = cfg.output.prefix if cfg.numerics.nonlinear else cfg.output.prefix + "_linear"
59 if cfg.output.wav:
60 print("wrote " + ", ".join(result.write_wav(prefix)))
61 if cfg.output.npz:
62 print("wrote " + result.write_npz(prefix))
63 analysis = analyze_cascade(result.pressure_unbaffled, result.fs)
64 print(format_cascade_report(analysis, f"{prefix}_unbaffled"))
65 if cfg.output.cascade_plot:
66 try:
67 print("wrote " + plot_cascade(analysis, f"{prefix}_cascade.png", f"{prefix}_unbaffled", result.fs))
68 except ImportError:
69 print("matplotlib not available; skipping the cascade plot")
70 print(f"total wall time {time.time() - t0:.0f} s")
71 return 0
74def cmd_check(args) -> int:
75 """Build the model and report mesh, time step and element checks without time stepping."""
76 from .simulate import Simulator
78 cfg = _load(args.config, args)
79 sim = Simulator(cfg)
80 c = sim.shell.element_checks()
81 print(f"DKT checks: rigid-tilt residual {c.rigid_tilt_residual:.1e}, "
82 f"constant-curvature energy error {c.constant_curvature_error:.1e}")
83 print(f"zero-energy modes per element: {c.zero_energy_modes_min} to {c.zero_energy_modes_max} (expect 6)")
84 print(f"rigid modes: max |K r| / |K random| = {c.rigid_mode_ratio:.1e} (should be ~1e-6 or smaller)")
85 n = cfg.numerics
86 print(f"a {n.duration:.2f} s run needs {sim.n_steps} sub-steps of {sim.mesh.n_nodes * 5} DOFs")
87 return 0
90def cmd_info(args) -> int:
91 from .cymsim_file import describe
93 for path in args.files:
94 print(describe(path))
95 return 0
98def cmd_cache_key(args) -> int:
99 import json
101 from .cache import cache_key, key_fields
103 cfg = _load(args.config, args)
104 print(cache_key(cfg))
105 if args.show:
106 print(json.dumps(key_fields(cfg), indent=2, sort_keys=True))
107 return 0
110def cmd_example_config(args) -> int:
111 cfg = default_config(args.exciter)
112 if args.path:
113 cfg.to_yaml(args.path)
114 print(f"wrote {args.path}")
115 else:
116 import yaml
117 sys.stdout.write(yaml.safe_dump(cfg.to_dict(), sort_keys=False))
118 return 0
121def main(argv=None) -> int:
122 ap = argparse.ArgumentParser(prog="cymsim", description="Simulate a struck cymbal.")
123 sub = ap.add_subparsers(dest="command", required=True)
125 p = sub.add_parser("simulate", aliases=["run"], help="run a simulation from a YAML config")
126 p.add_argument("config")
127 p.add_argument("-o", "--output", metavar="FILE.cymsim", help="write the run to a .cymsim file (docs/format.md)")
128 p.add_argument("--save-every", type=float, metavar="SECONDS",
129 help="with -o, also save the partial run every SECONDS of wall time")
130 p.add_argument("--record-acceleration", action="store_true",
131 help="keep the normal acceleration of every node at the audio rate (output.record_acceleration)")
132 p.add_argument("--resume", metavar="FILE.cymsim",
133 help="continue the run saved in FILE.cymsim (same configuration up to the duration and the "
134 "output naming) instead of starting from rest")
135 p.add_argument("--cache", metavar="URL",
136 help="take the run from, or continue it from, the run cache at URL (an artifact-cache app, "
137 "e.g. https://artifact-cache.tempory.net/api/apps/cymsim); default $CYMSIM_CACHE_URL")
138 p.add_argument("--no-cache", action="store_true", help="ignore $CYMSIM_CACHE_URL")
139 p.add_argument("--upload-every", type=float, metavar="SECONDS",
140 help="with the cache's passcode in $CYMSIM_CACHE_KEY, also upload the partial run every SECONDS "
141 "of wall time, so that a killed job continues from there (default 600; 0: only at the end)")
142 p.add_argument("--duration", type=float, help="override numerics.duration [s]")
143 p.add_argument("--out-prefix", help="override output.prefix")
144 p.add_argument("--linear", action="store_true", help="drop the quadratic strain terms (reference run)")
145 p.add_argument("--quiet", action="store_true")
146 p.set_defaults(func=cmd_simulate)
148 p = sub.add_parser("check", help="build the model and print mesh, time-step and element checks")
149 p.add_argument("config")
150 p.add_argument("--quiet", action="store_true")
151 p.set_defaults(func=cmd_check)
153 p = sub.add_parser("info", help="summarise .cymsim files")
154 p.add_argument("files", nargs="+")
155 p.add_argument("--quiet", action="store_true")
156 p.set_defaults(func=cmd_info)
158 p = sub.add_parser("cache-key", help="print the cache key of a config (independent of the duration)")
159 p.add_argument("config")
160 p.add_argument("--show", action="store_true", help="also print the fields that the key hashes")
161 p.add_argument("--linear", action="store_true", help="the key of the linear reference run")
162 p.add_argument("--record-acceleration", action="store_true")
163 p.add_argument("--quiet", action="store_true")
164 p.set_defaults(func=cmd_cache_key)
166 p = sub.add_parser("example-config", help="print or write a default config")
167 p.add_argument("path", nargs="?")
168 p.add_argument("--exciter", choices=("mallet", "stick"), default="mallet")
169 p.add_argument("--quiet", action="store_true")
170 p.set_defaults(func=cmd_example_config)
172 args = ap.parse_args(argv)
173 _setup_logging(args.quiet)
174 return args.func(args)
177if __name__ == "__main__":
178 sys.exit(main())
↑↓ move↵openescclose