makewfs 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
makewfs/cli.py ADDED
@@ -0,0 +1,99 @@
1
+ """Command-line interface for config validation and phase rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from numpy.typing import NDArray
12
+
13
+ from .__about__ import __version__
14
+ from .api import WavefrontSensor
15
+ from .config import load_config
16
+
17
+
18
+ def _load_array(path: str | Path) -> NDArray[Any]:
19
+ source = Path(path)
20
+ if source.suffix.lower() == ".npy":
21
+ return np.asarray(np.load(source))
22
+ if source.suffix.lower() == ".npz":
23
+ archive = np.load(source)
24
+ if not archive.files:
25
+ raise ValueError(f"phase archive {source} contains no arrays")
26
+ return np.asarray(archive[archive.files[0]])
27
+ if source.suffix.lower() in {".fits", ".fit"}:
28
+ try:
29
+ from astropy.io import fits # type: ignore[import-untyped]
30
+ except ImportError as exc: # pragma: no cover
31
+ raise ImportError("FITS input requires astropy") from exc
32
+ return np.asarray(fits.getdata(source))
33
+ raise ValueError(f"unsupported array format {source.suffix!r}")
34
+
35
+
36
+ def _write_array(path: str | Path, array: NDArray[Any]) -> None:
37
+ destination = Path(path)
38
+ if destination.suffix.lower() == ".npy":
39
+ np.save(destination, array)
40
+ elif destination.suffix.lower() == ".npz":
41
+ np.savez_compressed(destination, data=array)
42
+ elif destination.suffix.lower() in {".fits", ".fit"}:
43
+ try:
44
+ from astropy.io import fits
45
+ except ImportError as exc: # pragma: no cover
46
+ raise ImportError("FITS output requires astropy") from exc
47
+ fits.writeto(destination, array, overwrite=True)
48
+ else:
49
+ raise ValueError(f"unsupported output format {destination.suffix!r}")
50
+
51
+
52
+ def build_parser() -> argparse.ArgumentParser:
53
+ parser = argparse.ArgumentParser(
54
+ prog="makewfs", description="Render configured AO WFS images from phase/OPD arrays."
55
+ )
56
+ parser.add_argument("--version", action="version", version=f"makewfs {__version__}")
57
+ commands = parser.add_subparsers(dest="command", required=True)
58
+ validate = commands.add_parser("validate-config", help="validate a TOML configuration")
59
+ validate.add_argument("config")
60
+ render = commands.add_parser("render", help="render one detector frame")
61
+ render.add_argument("config")
62
+ render.add_argument("wavefront")
63
+ render.add_argument("--output", required=True)
64
+ render.add_argument("--seed", type=int, default=None)
65
+ ideal = commands.add_parser("ideal", help="write an ideal photon-rate map")
66
+ ideal.add_argument("config")
67
+ ideal.add_argument("wavefront")
68
+ ideal.add_argument("--output", required=True)
69
+ return parser
70
+
71
+
72
+ def main(argv: list[str] | None = None) -> int:
73
+ parser = build_parser()
74
+ args = parser.parse_args(argv)
75
+ try:
76
+ if args.command == "validate-config":
77
+ config = load_config(args.config)
78
+ print(
79
+ json.dumps({"valid": True, "digest": config.digest, "sensor": config.sensor.kind})
80
+ )
81
+ return 0
82
+ sensor = WavefrontSensor.from_toml(args.config)
83
+ wavefront = _load_array(args.wavefront)
84
+ if args.command == "ideal":
85
+ _write_array(args.output, sensor.photon_rate(wavefront))
86
+ else:
87
+ frame = sensor.expose(wavefront, seed=args.seed)
88
+ frame.to_fits(args.output, overwrite=True) if Path(args.output).suffix.lower() in {
89
+ ".fits",
90
+ ".fit",
91
+ } else _write_array(args.output, np.asarray(frame))
92
+ return 0
93
+ except (ValueError, KeyError, FileNotFoundError, ImportError) as exc:
94
+ parser.error(str(exc))
95
+ return 2
96
+
97
+
98
+ if __name__ == "__main__": # pragma: no cover
99
+ raise SystemExit(main())