"""Command line interface: ``cymsim simulate`` (alias ``run``), ``cymsim check``, ``cymsim info``, ``cymsim cache-key`` and ``cymsim example-config``.""" from __future__ import annotations import argparse import logging import os import sys import time from .config import SimulationConfig, default_config def _setup_logging(quiet: bool) -> None: logging.basicConfig(level=logging.WARNING if quiet else logging.INFO, format="%(message)s", stream=sys.stdout) def _load(path: str, args) -> SimulationConfig: cfg = SimulationConfig.from_yaml(path) if getattr(args, "duration", None) is not None: cfg.numerics.duration = args.duration if getattr(args, "out_prefix", None) is not None: cfg.output.prefix = args.out_prefix if getattr(args, "linear", False): cfg.numerics.nonlinear = False if getattr(args, "record_acceleration", False): cfg.output.record_acceleration = True return cfg def cmd_simulate(args) -> int: from .diagnostics import analyze_cascade, format_cascade_report, plot_cascade from .simulate import Simulator t0 = time.time() cfg = _load(args.config, args) cache = None if args.no_cache or args.resume is not None else args.cache or os.environ.get("CYMSIM_CACHE_URL") if args.cache and args.resume is not None: print("give either --cache or --resume, not both", file=sys.stderr) return 2 if cache: from .cache import cached_run every = {} if args.upload_every is None else {"upload_every": args.upload_every} result = cached_run(cfg, cache, save_path=args.output, save_every=args.save_every, **every) else: sim = Simulator(cfg) prev = None if args.resume is not None: from .cymsim_file import load_cymsim prev = load_cymsim(args.resume) try: sim.check_resumable(prev) except ValueError as e: print(f"cannot continue {args.resume}: {e}", file=sys.stderr) return 2 result = sim.run(save_path=args.output, save_every=args.save_every, resume=prev) prefix = cfg.output.prefix if cfg.numerics.nonlinear else cfg.output.prefix + "_linear" if cfg.output.wav: print("wrote " + ", ".join(result.write_wav(prefix))) if cfg.output.npz: print("wrote " + result.write_npz(prefix)) analysis = analyze_cascade(result.pressure_unbaffled, result.fs) print(format_cascade_report(analysis, f"{prefix}_unbaffled")) if cfg.output.cascade_plot: try: print("wrote " + plot_cascade(analysis, f"{prefix}_cascade.png", f"{prefix}_unbaffled", result.fs)) except ImportError: print("matplotlib not available; skipping the cascade plot") print(f"total wall time {time.time() - t0:.0f} s") return 0 def cmd_check(args) -> int: """Build the model and report mesh, time step and element checks without time stepping.""" from .simulate import Simulator cfg = _load(args.config, args) sim = Simulator(cfg) c = sim.shell.element_checks() print(f"DKT checks: rigid-tilt residual {c.rigid_tilt_residual:.1e}, " f"constant-curvature energy error {c.constant_curvature_error:.1e}") print(f"zero-energy modes per element: {c.zero_energy_modes_min} to {c.zero_energy_modes_max} (expect 6)") print(f"rigid modes: max |K r| / |K random| = {c.rigid_mode_ratio:.1e} (should be ~1e-6 or smaller)") n = cfg.numerics print(f"a {n.duration:.2f} s run needs {sim.n_steps} sub-steps of {sim.mesh.n_nodes * 5} DOFs") return 0 def cmd_info(args) -> int: from .cymsim_file import describe for path in args.files: print(describe(path)) return 0 def cmd_cache_key(args) -> int: import json from .cache import cache_key, key_fields cfg = _load(args.config, args) print(cache_key(cfg)) if args.show: print(json.dumps(key_fields(cfg), indent=2, sort_keys=True)) return 0 def cmd_example_config(args) -> int: cfg = default_config(args.exciter) if args.path: cfg.to_yaml(args.path) print(f"wrote {args.path}") else: import yaml sys.stdout.write(yaml.safe_dump(cfg.to_dict(), sort_keys=False)) return 0 def main(argv=None) -> int: ap = argparse.ArgumentParser(prog="cymsim", description="Simulate a struck cymbal.") sub = ap.add_subparsers(dest="command", required=True) p = sub.add_parser("simulate", aliases=["run"], help="run a simulation from a YAML config") p.add_argument("config") p.add_argument("-o", "--output", metavar="FILE.cymsim", help="write the run to a .cymsim file (docs/format.md)") p.add_argument("--save-every", type=float, metavar="SECONDS", help="with -o, also save the partial run every SECONDS of wall time") p.add_argument("--record-acceleration", action="store_true", help="keep the normal acceleration of every node at the audio rate (output.record_acceleration)") p.add_argument("--resume", metavar="FILE.cymsim", help="continue the run saved in FILE.cymsim (same configuration up to the duration and the " "output naming) instead of starting from rest") p.add_argument("--cache", metavar="URL", help="take the run from, or continue it from, the run cache at URL (an artifact-cache app, " "e.g. https://artifact-cache.tempory.net/api/apps/cymsim); default $CYMSIM_CACHE_URL") p.add_argument("--no-cache", action="store_true", help="ignore $CYMSIM_CACHE_URL") p.add_argument("--upload-every", type=float, metavar="SECONDS", help="with the cache's passcode in $CYMSIM_CACHE_KEY, also upload the partial run every SECONDS " "of wall time, so that a killed job continues from there (default 600; 0: only at the end)") p.add_argument("--duration", type=float, help="override numerics.duration [s]") p.add_argument("--out-prefix", help="override output.prefix") p.add_argument("--linear", action="store_true", help="drop the quadratic strain terms (reference run)") p.add_argument("--quiet", action="store_true") p.set_defaults(func=cmd_simulate) p = sub.add_parser("check", help="build the model and print mesh, time-step and element checks") p.add_argument("config") p.add_argument("--quiet", action="store_true") p.set_defaults(func=cmd_check) p = sub.add_parser("info", help="summarise .cymsim files") p.add_argument("files", nargs="+") p.add_argument("--quiet", action="store_true") p.set_defaults(func=cmd_info) p = sub.add_parser("cache-key", help="print the cache key of a config (independent of the duration)") p.add_argument("config") p.add_argument("--show", action="store_true", help="also print the fields that the key hashes") p.add_argument("--linear", action="store_true", help="the key of the linear reference run") p.add_argument("--record-acceleration", action="store_true") p.add_argument("--quiet", action="store_true") p.set_defaults(func=cmd_cache_key) p = sub.add_parser("example-config", help="print or write a default config") p.add_argument("path", nargs="?") p.add_argument("--exciter", choices=("mallet", "stick"), default="mallet") p.add_argument("--quiet", action="store_true") p.set_defaults(func=cmd_example_config) args = ap.parse_args(argv) _setup_logging(args.quiet) return args.func(args) if __name__ == "__main__": sys.exit(main())