1"""Command line interface: ``cymsim simulate`` (alias ``run``), ``cymsim check``, ``cymsim info`` and
2``cymsim example-config``."""
3from __future__ import annotations
5import argparse
6import logging
7import sys
9from .config import SimulationConfig, default_config
12def _setup_logging(quiet: bool) -> None:
13 logging.basicConfig(level=logging.WARNING if quiet else logging.INFO,
14 format="%(message)s", stream=sys.stdout)
17def _load(path: str, args) -> SimulationConfig:
18 cfg = SimulationConfig.from_yaml(path)
19 if getattr(args, "duration", None) is not None:
20 cfg.numerics.duration = args.duration
21 if getattr(args, "out_prefix", None) is not None:
22 cfg.output.prefix = args.out_prefix
23 if getattr(args, "linear", False):
24 cfg.numerics.nonlinear = False
25 if getattr(args, "record_acceleration", False):
26 cfg.output.record_acceleration = True
27 return cfg
30def cmd_simulate(args) -> int:
31 from .diagnostics import analyze_cascade, format_cascade_report, plot_cascade
32 from .simulate import Simulator
34 cfg = _load(args.config, args)
35 sim = Simulator(cfg)
36 result = sim.run(save_path=args.output, save_every=args.save_every)
37 prefix = cfg.output.prefix if cfg.numerics.nonlinear else cfg.output.prefix + "_linear"
38 if cfg.output.wav:
39 print("wrote " + ", ".join(result.write_wav(prefix)))
40 if cfg.output.npz:
41 print("wrote " + result.write_npz(prefix))
42 analysis = analyze_cascade(result.pressure_unbaffled, result.fs)
43 print(format_cascade_report(analysis, f"{prefix}_unbaffled"))
44 if cfg.output.cascade_plot:
45 try:
46 print("wrote " + plot_cascade(analysis, f"{prefix}_cascade.png", f"{prefix}_unbaffled", result.fs))
47 except ImportError:
48 print("matplotlib not available; skipping the cascade plot")
49 print(f"total wall time {sim.setup_time + result.wall_time:.0f} s")
50 return 0
53def cmd_check(args) -> int:
54 """Build the model and report mesh, time step and element checks without time stepping."""
55 from .simulate import Simulator
57 cfg = _load(args.config, args)
58 sim = Simulator(cfg)
59 c = sim.shell.element_checks()
60 print(f"DKT checks: rigid-tilt residual {c.rigid_tilt_residual:.1e}, "
61 f"constant-curvature energy error {c.constant_curvature_error:.1e}")
62 print(f"zero-energy modes per element: {c.zero_energy_modes_min} to {c.zero_energy_modes_max} (expect 6)")
63 print(f"rigid modes: max |K r| / |K random| = {c.rigid_mode_ratio:.1e} (should be ~1e-6 or smaller)")
64 n = cfg.numerics
65 print(f"a {n.duration:.2f} s run needs {sim.n_steps} sub-steps of {sim.mesh.n_nodes * 5} DOFs")
66 return 0
69def cmd_info(args) -> int:
70 from .cymsim_file import describe
72 for path in args.files:
73 print(describe(path))
74 return 0
77def cmd_example_config(args) -> int:
78 cfg = default_config(args.exciter)
79 if args.path:
80 cfg.to_yaml(args.path)
81 print(f"wrote {args.path}")
82 else:
83 import yaml
84 sys.stdout.write(yaml.safe_dump(cfg.to_dict(), sort_keys=False))
85 return 0
88def main(argv=None) -> int:
89 ap = argparse.ArgumentParser(prog="cymsim", description="Simulate a struck cymbal.")
90 sub = ap.add_subparsers(dest="command", required=True)
92 p = sub.add_parser("simulate", aliases=["run"], help="run a simulation from a YAML config")
93 p.add_argument("config")
94 p.add_argument("-o", "--output", metavar="FILE.cymsim", help="write the run to a .cymsim file (docs/format.md)")
95 p.add_argument("--save-every", type=float, metavar="SECONDS",
96 help="with -o, also save the partial run every SECONDS of wall time")
97 p.add_argument("--record-acceleration", action="store_true",
98 help="keep the normal acceleration of every node at the audio rate (output.record_acceleration)")
99 p.add_argument("--duration", type=float, help="override numerics.duration [s]")
100 p.add_argument("--out-prefix", help="override output.prefix")
101 p.add_argument("--linear", action="store_true", help="drop the quadratic strain terms (reference run)")
102 p.add_argument("--quiet", action="store_true")
103 p.set_defaults(func=cmd_simulate)
105 p = sub.add_parser("check", help="build the model and print mesh, time-step and element checks")
106 p.add_argument("config")
107 p.add_argument("--quiet", action="store_true")
108 p.set_defaults(func=cmd_check)
110 p = sub.add_parser("info", help="summarise .cymsim files")
111 p.add_argument("files", nargs="+")
112 p.add_argument("--quiet", action="store_true")
113 p.set_defaults(func=cmd_info)
115 p = sub.add_parser("example-config", help="print or write a default config")
116 p.add_argument("path", nargs="?")
117 p.add_argument("--exciter", choices=("mallet", "stick"), default="mallet")
118 p.add_argument("--quiet", action="store_true")
119 p.set_defaults(func=cmd_example_config)
121 args = ap.parse_args(argv)
122 _setup_logging(args.quiet)
123 return args.func(args)
126if __name__ == "__main__":
127 sys.exit(main())