lunar-toolkit 0.1.0.dev2__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.
Files changed (55) hide show
  1. lunar_toolkit/__init__.py +6 -0
  2. lunar_toolkit/asp.py +432 -0
  3. lunar_toolkit/cli/__init__.py +88 -0
  4. lunar_toolkit/cli/_jobs.py +437 -0
  5. lunar_toolkit/cli/_status.py +368 -0
  6. lunar_toolkit/cli/_utils.py +128 -0
  7. lunar_toolkit/cli/app.py +205 -0
  8. lunar_toolkit/cli/build_db.py +130 -0
  9. lunar_toolkit/cli/download.py +111 -0
  10. lunar_toolkit/cli/kaguya.py +253 -0
  11. lunar_toolkit/cli/ode_search.py +144 -0
  12. lunar_toolkit/cli/pairs.py +236 -0
  13. lunar_toolkit/cli/process.py +138 -0
  14. lunar_toolkit/cli/query.py +70 -0
  15. lunar_toolkit/cli/reference.py +233 -0
  16. lunar_toolkit/cli/search.py +90 -0
  17. lunar_toolkit/cli/serve.py +706 -0
  18. lunar_toolkit/cli/sfs.py +344 -0
  19. lunar_toolkit/cli/sfs_report.py +148 -0
  20. lunar_toolkit/cli/shadowcam.py +246 -0
  21. lunar_toolkit/cli/stereo.py +85 -0
  22. lunar_toolkit/cli/workspace.py +84 -0
  23. lunar_toolkit/config.py +345 -0
  24. lunar_toolkit/constants.py +217 -0
  25. lunar_toolkit/csm.py +340 -0
  26. lunar_toolkit/downloader.py +447 -0
  27. lunar_toolkit/geometry.py +556 -0
  28. lunar_toolkit/isis.py +222 -0
  29. lunar_toolkit/kaguya/__init__.py +45 -0
  30. lunar_toolkit/kaguya/mi.py +274 -0
  31. lunar_toolkit/kaguya/tc.py +370 -0
  32. lunar_toolkit/lroc/__init__.py +6 -0
  33. lunar_toolkit/lroc/cumindex.py +1197 -0
  34. lunar_toolkit/lroc/nac.py +702 -0
  35. lunar_toolkit/observation.py +127 -0
  36. lunar_toolkit/ode.py +485 -0
  37. lunar_toolkit/provenance.py +312 -0
  38. lunar_toolkit/reference.py +1111 -0
  39. lunar_toolkit/selection.py +714 -0
  40. lunar_toolkit/sfs.py +1534 -0
  41. lunar_toolkit/sfs_products.py +676 -0
  42. lunar_toolkit/sfs_report.py +1328 -0
  43. lunar_toolkit/shadowcam/__init__.py +29 -0
  44. lunar_toolkit/shadowcam/sc.py +392 -0
  45. lunar_toolkit/static/index.html +3259 -0
  46. lunar_toolkit/stereo.py +519 -0
  47. lunar_toolkit/stereo_dem.py +145 -0
  48. lunar_toolkit/tools.py +264 -0
  49. lunar_toolkit/workspace.py +1173 -0
  50. lunar_toolkit-0.1.0.dev2.dist-info/METADATA +177 -0
  51. lunar_toolkit-0.1.0.dev2.dist-info/RECORD +55 -0
  52. lunar_toolkit-0.1.0.dev2.dist-info/WHEEL +5 -0
  53. lunar_toolkit-0.1.0.dev2.dist-info/entry_points.txt +2 -0
  54. lunar_toolkit-0.1.0.dev2.dist-info/licenses/LICENSE +28 -0
  55. lunar_toolkit-0.1.0.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,246 @@
1
+ """shadowcam - download KPLO ShadowCam cubes and prepare them for ASP (SPICE + CSM camera)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ COMMAND = "shadowcam"
10
+
11
+
12
+ def add_parser(sub: argparse._SubParsersAction) -> None:
13
+ p = sub.add_parser(
14
+ COMMAND,
15
+ help="Download ShadowCam cubes, spiceinit them and write CSM cameras for ASP.",
16
+ formatter_class=argparse.RawDescriptionHelpFormatter,
17
+ epilog="""
18
+ Examples
19
+ --------
20
+ lunar shadowcam M074289249S M074296291S -o ./sc
21
+ lunar shadowcam M013178466SC --validate -o ./sc
22
+ lunar shadowcam --point -89.67 129.8 --radius-km 5 --inc 80 90 --limit 4 -o ./sc
23
+ lunar shadowcam --aoi malapert.geojson --product cal --download-only -o ./sc
24
+ lunar shadowcam ./sc/M013178466SE.cub --mapproject --dem ref.tif --resolution 1.7
25
+ lunar shadowcam M013178466S --dry-run
26
+
27
+ Follows the ASP 3.7 ShadowCam example, which needs ASP's custom ISIS / ALE
28
+ build and the KPLO kernels in $ISISDATA/kplo/kernels (rsync.im-ldi.com::kplo).
29
+ Observation IDs (M...S) map to the raw cube M...SE (used in ASP's example) or,
30
+ with --product cal, the calibrated M...SC. Per product, <output-dir> gets
31
+ <id>.cub (spiceinit'ed in place), <id>.json (isd_generate), with --validate
32
+ <id>.cam_test.txt and with --mapproject <id>.tif (ASP mapproject with --dem,
33
+ else ISIS cam2map). Steps whose output exists are skipped (--force redoes them).
34
+ --bbox is WEST SOUTH EAST NORTH, longitudes positive east.
35
+ """,
36
+ )
37
+ p.add_argument(
38
+ "sources",
39
+ nargs="*",
40
+ metavar="SOURCE",
41
+ help="Observation / product IDs (M013178466S, M013178466SE, M013178466SC) or local .cub files.",
42
+ )
43
+ p.add_argument(
44
+ "--from-file", metavar="FILE", help="Text file of sources, one per line (#comments ignored)."
45
+ )
46
+ p.add_argument(
47
+ "--output-dir",
48
+ "-o",
49
+ default=None,
50
+ metavar="DIR",
51
+ help="Output directory (default: current working directory).",
52
+ )
53
+ p.add_argument(
54
+ "--product",
55
+ "-t",
56
+ choices=("raw", "cal"),
57
+ default="raw",
58
+ help="raw (M...SE, ASP's example, default) or cal (M...SC) for IDs and searches.",
59
+ )
60
+
61
+ search = p.add_argument_group("ODE search (instead of / in addition to SOURCE)")
62
+ region = search.add_mutually_exclusive_group()
63
+ region.add_argument(
64
+ "--bbox",
65
+ nargs=4,
66
+ type=float,
67
+ metavar=("WEST", "SOUTH", "EAST", "NORTH"),
68
+ help="Bounding box in degrees.",
69
+ )
70
+ region.add_argument(
71
+ "--point",
72
+ nargs=2,
73
+ type=float,
74
+ metavar=("LAT", "LON"),
75
+ help="Search around a point (use with --radius-km).",
76
+ )
77
+ region.add_argument(
78
+ "--aoi", metavar="FILE_OR_WKT", help="Area of interest: GeoJSON / WKT file or WKT polygon string."
79
+ )
80
+ search.add_argument(
81
+ "--radius-km", type=float, default=1.0, metavar="KM", help="Radius for --point (default: 1 km)."
82
+ )
83
+ search.add_argument("--inc", nargs=2, type=float, metavar=("MIN", "MAX"), help="Incidence angle range.")
84
+ search.add_argument("--emi", nargs=2, type=float, metavar=("MIN", "MAX"), help="Emission angle range.")
85
+ search.add_argument("--phase", nargs=2, type=float, metavar=("MIN", "MAX"), help="Phase angle range.")
86
+ search.add_argument("--time", nargs=2, metavar=("START", "END"), help="Observation time range (UTC).")
87
+ search.add_argument(
88
+ "--limit",
89
+ type=int,
90
+ default=10,
91
+ metavar="N",
92
+ help="Maximum number of products from the search (default: 10).",
93
+ )
94
+
95
+ proc = p.add_argument_group("Processing")
96
+ proc.add_argument(
97
+ "--web",
98
+ action="store_true",
99
+ help="spiceinit with the USGS SPICE web service (isd_generate still needs local kernels).",
100
+ )
101
+ proc.add_argument(
102
+ "--shape",
103
+ default="system",
104
+ metavar="SHAPE",
105
+ help="spiceinit shape: system (LOLA DEM in $ISISDATA/base/dems, default) or ellipsoid.",
106
+ )
107
+ proc.add_argument(
108
+ "--no-csm",
109
+ dest="csm",
110
+ action="store_false",
111
+ help="Do not write a CSM camera (.json) with isd_generate.",
112
+ )
113
+ proc.add_argument(
114
+ "--validate",
115
+ action="store_true",
116
+ help="Compare ISIS and CSM cameras with cam_test (<id>.cam_test.txt).",
117
+ )
118
+ proc.add_argument("--mapproject", action="store_true", help="Also write a map-projected <id>.tif.")
119
+ proc.add_argument("--dem", metavar="DEM", help="DEM for ASP mapproject (default: ISIS cam2map).")
120
+ proc.add_argument(
121
+ "--resolution",
122
+ type=float,
123
+ default=None,
124
+ metavar="M",
125
+ help="Map resolution in m/pixel for --mapproject.",
126
+ )
127
+ proc.add_argument(
128
+ "--t-srs", default=None, metavar="PROJ", help="PROJ string for ASP mapproject with --dem."
129
+ )
130
+ proc.add_argument(
131
+ "--projection",
132
+ default="equirectangular",
133
+ metavar="PROJ",
134
+ help="ISIS projection for --mapproject without --dem (default: equirectangular).",
135
+ )
136
+ proc.add_argument("--keep", action="store_true", help="Keep the ISIS map-projected cube.")
137
+ proc.add_argument("--force", action="store_true", help="Redo processing steps whose output exists.")
138
+ proc.add_argument("--download-only", action="store_true", help="Download only; no processing.")
139
+ proc.add_argument(
140
+ "--dry-run",
141
+ action="store_true",
142
+ help="Print the ISIS / ASP commands without downloading or running anything.",
143
+ )
144
+ proc.add_argument(
145
+ "--workers", type=int, default=1, metavar="N", help="Products processed in parallel (default: 1)."
146
+ )
147
+
148
+
149
+ def run(args: argparse.Namespace) -> int:
150
+ from ..ode import OdeClient, OdeError
151
+ from ..shadowcam.sc import (
152
+ ScProcessor,
153
+ ScProduct,
154
+ download_product,
155
+ lookup_products,
156
+ product_id_for,
157
+ sc_products,
158
+ )
159
+ from ._utils import load_sources, ode_filters, print_dry_run, region_args
160
+
161
+ sources = load_sources(args)
162
+ region = region_args(args)
163
+ if not sources and not region:
164
+ print(
165
+ "No sources. Pass IDs, .cub paths, --from-file, --bbox, --point or --aoi.",
166
+ file=sys.stderr,
167
+ )
168
+ return 1
169
+
170
+ cubes = [Path(s) for s in sources if s.lower().endswith(".cub")]
171
+ ids = [s for s in sources if not s.lower().endswith(".cub")]
172
+ client = OdeClient()
173
+ try:
174
+ wanted = {product_id_for(i, args.product) for i in ids}
175
+ products: list[ScProduct] = sc_products(lookup_products(ids, client, args.product)) if ids else []
176
+ if region:
177
+ products += sc_products(
178
+ client.search(
179
+ "shadowcam",
180
+ args.product,
181
+ max_results=args.limit,
182
+ **region,
183
+ **ode_filters(args),
184
+ )
185
+ )
186
+ except (OdeError, ValueError) as exc:
187
+ print(f"Error: {exc}", file=sys.stderr)
188
+ return 1
189
+ products = list({p.product_id: p for p in products}.values())
190
+ for missing in sorted(wanted - {p.product_id for p in products}):
191
+ print(f" {missing} - not found on ODE", file=sys.stderr)
192
+
193
+ output_dir = Path(args.output_dir) if args.output_dir else Path.cwd()
194
+ print(f"{len(products) + len(cubes)} product(s) -> {output_dir}")
195
+ for p in products:
196
+ size = f" ({p.cube_kbytes / 1024:.0f} MB)" if p.cube_kbytes else ""
197
+ print(f" {p.product_id}{size}")
198
+
199
+ if args.download_only:
200
+ failed = 0
201
+ for p in products:
202
+ try:
203
+ print(f" {p.product_id} -> {download_product(p, output_dir)}")
204
+ except Exception as exc:
205
+ print(f" {p.product_id} - FAILED: {exc}", file=sys.stderr)
206
+ failed += 1
207
+ return 1 if failed else 0
208
+
209
+ proc = ScProcessor(
210
+ output_dir,
211
+ product=args.product,
212
+ web=args.web,
213
+ csm=args.csm,
214
+ validate=args.validate,
215
+ mapproject=args.mapproject,
216
+ dem=Path(args.dem) if args.dem else None,
217
+ resolution=args.resolution,
218
+ t_srs=args.t_srs,
219
+ projection=args.projection,
220
+ keep=args.keep,
221
+ force=args.force,
222
+ spiceinit_params={"shape": args.shape},
223
+ )
224
+ if args.dry_run:
225
+
226
+ def process_all() -> None:
227
+ for p in products:
228
+ proc.process_cube(proc.cube_path(p))
229
+ for cub in cubes:
230
+ proc.process_cube(cub)
231
+
232
+ print_dry_run(process_all)
233
+ return 0
234
+
235
+ sources_all: list = [*products, *cubes]
236
+ results = proc.process_batch(sources_all, max_workers=args.workers)
237
+ ok = 0
238
+ for src, out in zip(sources_all, results):
239
+ name = src.product_id if isinstance(src, ScProduct) else src.name
240
+ if out is None:
241
+ print(f" {name} - FAILED", file=sys.stderr)
242
+ continue
243
+ ok += 1
244
+ files = [f for f in (out.cube, out.camera, out.cam_test, out.geotiff) if f]
245
+ print(f" {name} -> {', '.join(str(f) for f in files)}")
246
+ return 0 if ok == len(sources_all) else 1
@@ -0,0 +1,85 @@
1
+ """stereo - DEM from one LROC NAC stereo pair (ISIS + ASP)."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ COMMAND = "stereo"
8
+
9
+
10
+ def add_parser(sub: argparse._SubParsersAction) -> None:
11
+ p = sub.add_parser(
12
+ COMMAND,
13
+ help="Build a DEM from a NAC stereo pair (bundle_adjust, parallel_stereo, point2dem).",
14
+ formatter_class=argparse.RawDescriptionHelpFormatter,
15
+ epilog="""
16
+ Examples
17
+ --------
18
+ lunar stereo M181058717LE M181073012LE --res 2 --work-dir pair1
19
+ lunar stereo L.IMG R.IMG --dem ldem.tif --res 2 # mapproject first, align to the DEM
20
+ lunar stereo L.cub R.cub --dry-run
21
+
22
+ Needs ISIS and ASP on PATH (or ASP_ROOT / ISISROOT).
23
+ """,
24
+ )
25
+ p.add_argument("left", help="Left image: NAC EDR product ID, .IMG or .cub path.")
26
+ p.add_argument("right", help="Right image: NAC EDR product ID, .IMG or .cub path.")
27
+ p.add_argument("--work-dir", default="stereo_work", metavar="DIR")
28
+ p.add_argument("--res", type=float, default=2.0, metavar="M", help="DEM m/px (default: 2).")
29
+ p.add_argument("--dem", default=None, metavar="TIF", help="Prior DEM to mapproject onto and align to.")
30
+ p.add_argument(
31
+ "--image-res",
32
+ type=float,
33
+ default=1.0,
34
+ metavar="M",
35
+ help="Mapprojected image m/px when --dem is set (default: 1).",
36
+ )
37
+ p.add_argument("--no-align", action="store_true", help="Skip pc_align to --dem.")
38
+ p.add_argument("--max-displacement", type=float, default=200.0, metavar="M")
39
+ p.add_argument("--no-csm", action="store_true", help="Use ISIS cameras instead of CSM.")
40
+ p.add_argument("--local-spice", action="store_true")
41
+ p.add_argument("--processes", type=int, default=None, metavar="N")
42
+ p.add_argument("--threads", type=int, default=None, metavar="N")
43
+ p.add_argument(
44
+ "--stereo-opt",
45
+ action="append",
46
+ metavar="KEY=VALUE",
47
+ help="Extra / overriding parallel_stereo option (repeatable).",
48
+ )
49
+ p.add_argument("--force", action="store_true")
50
+ p.add_argument("--dry-run", action="store_true", help="Print the commands without running them.")
51
+ p.add_argument("--data-dir", type=Path, default=None, metavar="DIR")
52
+
53
+
54
+ def run(args: argparse.Namespace) -> int:
55
+ from ..stereo_dem import stereo_dem
56
+ from ..tools import ToolError
57
+ from ._utils import parse_options, print_dry_run, resolve_images
58
+
59
+ work_dir = Path(args.work_dir)
60
+ left, right = resolve_images(
61
+ [args.left, args.right], work_dir / "raw", args.data_dir, download=not args.dry_run
62
+ )
63
+ kwargs = dict(
64
+ resolution=args.res,
65
+ prior_dem=Path(args.dem) if args.dem else None,
66
+ image_resolution=args.image_res,
67
+ align=not args.no_align,
68
+ max_displacement=args.max_displacement,
69
+ csm=not args.no_csm,
70
+ web_spice=not args.local_spice,
71
+ processes=args.processes,
72
+ threads=args.threads,
73
+ stereo_options=parse_options(args.stereo_opt),
74
+ force=args.force,
75
+ )
76
+ try:
77
+ if args.dry_run:
78
+ print_dry_run(lambda: stereo_dem(left, right, work_dir, **kwargs))
79
+ return 0
80
+ dem = stereo_dem(left, right, work_dir, **kwargs)
81
+ except ToolError as exc:
82
+ print(exc, file=sys.stderr)
83
+ return 1
84
+ print(f"Stereo DEM -> {dem}")
85
+ return 0
@@ -0,0 +1,84 @@
1
+ """workspace - ad-hoc SQL on a work dir's DuckDB workspace (lunar.duckdb)."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ COMMAND = "workspace"
8
+
9
+
10
+ def add_parser(sub: argparse._SubParsersAction) -> None:
11
+ p = sub.add_parser(
12
+ COMMAND,
13
+ help="Query a work dir's DuckDB workspace (images, pairs, bundle-adjustment tables and views).",
14
+ formatter_class=argparse.RawDescriptionHelpFormatter,
15
+ epilog="""
16
+ Examples
17
+ --------
18
+ lunar workspace work/malapert # tables and views with row counts
19
+ lunar workspace work/malapert --sql "SELECT * FROM ba_run_summary"
20
+ lunar workspace work/malapert --sql "SELECT run, rule, count(*) FROM image_rejections GROUP BY ALL"
21
+ lunar workspace work/malapert --sql "SELECT * FROM image_rejections_at(1.5, 0.8, 1.0, 1)"
22
+ lunar workspace work/malapert --export ba_image_stats ba_stats.parquet
23
+
24
+ WORK_DIR is a lunar sfs work directory (its lunar.duckdb) or a .duckdb file; it is
25
+ opened read-only. Tables: images, pairs, ba_runs, ba_residual_stats,
26
+ ba_pixel_residuals, ba_match_offsets, ba_points, ba_convergence, ba_camera_offsets,
27
+ rejection_params. Views: ba_image_stats, ba_run_summary, image_rejections.
28
+ --export writes Parquet for .parquet paths, else CSV (geometries as WKT).
29
+ """,
30
+ )
31
+ p.add_argument("work_dir", type=Path, metavar="WORK_DIR", help="Work directory or .duckdb file.")
32
+ p.add_argument("--sql", action="append", metavar="SQL", help="Run SQL and print the result (repeatable).")
33
+ p.add_argument("--tables", action="store_true", help="List tables and views with row counts.")
34
+ p.add_argument(
35
+ "--export", nargs=2, metavar=("TABLE", "PATH"), help="Write a table or view to Parquet / CSV."
36
+ )
37
+ p.add_argument("--csv", metavar="FILE", help="Write the last --sql result to CSV.")
38
+ p.add_argument("--max-rows", type=int, default=50, metavar="N", help="Rows to print (default: 50).")
39
+ p.add_argument(
40
+ "--memory-limit", default="1GB", metavar="SIZE", help="DuckDB memory_limit (default: 1GB)."
41
+ )
42
+ p.add_argument("--threads", type=int, metavar="N", help="DuckDB threads (default: all cores).")
43
+
44
+
45
+ def run(args: argparse.Namespace) -> int:
46
+ import duckdb
47
+
48
+ from ..workspace import Workspace, WorkspaceError, workspace_path
49
+
50
+ path = workspace_path(args.work_dir)
51
+ if not path.exists():
52
+ print(f"Error: no workspace at {path}", file=sys.stderr)
53
+ return 1
54
+ try:
55
+ ws = Workspace(path, read_only=True, memory_limit=args.memory_limit, threads=args.threads)
56
+ except duckdb.Error as exc:
57
+ print(f"Error: {exc}", file=sys.stderr)
58
+ return 1
59
+ with ws:
60
+ try:
61
+ if args.tables or not (args.sql or args.export):
62
+ print(ws.tables().to_string(index=False))
63
+ res = None
64
+ for sql in args.sql or []:
65
+ res = ws.query(sql)
66
+ geom = [
67
+ c for c in res.columns if res[c].map(lambda v: isinstance(v, bytes | bytearray)).any()
68
+ ]
69
+ shown = res.drop(columns=geom)
70
+ if len(shown.columns):
71
+ with_more = len(res) > args.max_rows
72
+ print(shown.head(args.max_rows).to_string(index=False))
73
+ if with_more:
74
+ print(f"... {len(res)} rows")
75
+ if args.csv and res is not None:
76
+ res.to_csv(args.csv, index=False)
77
+ print(f"Wrote {args.csv}")
78
+ if args.export:
79
+ out = ws.export(args.export[0], args.export[1])
80
+ print(f"Wrote {out}")
81
+ except (duckdb.Error, WorkspaceError, ValueError) as exc:
82
+ print(f"Error: {exc}", file=sys.stderr)
83
+ return 1
84
+ return 0