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.
- lunar_toolkit/__init__.py +6 -0
- lunar_toolkit/asp.py +432 -0
- lunar_toolkit/cli/__init__.py +88 -0
- lunar_toolkit/cli/_jobs.py +437 -0
- lunar_toolkit/cli/_status.py +368 -0
- lunar_toolkit/cli/_utils.py +128 -0
- lunar_toolkit/cli/app.py +205 -0
- lunar_toolkit/cli/build_db.py +130 -0
- lunar_toolkit/cli/download.py +111 -0
- lunar_toolkit/cli/kaguya.py +253 -0
- lunar_toolkit/cli/ode_search.py +144 -0
- lunar_toolkit/cli/pairs.py +236 -0
- lunar_toolkit/cli/process.py +138 -0
- lunar_toolkit/cli/query.py +70 -0
- lunar_toolkit/cli/reference.py +233 -0
- lunar_toolkit/cli/search.py +90 -0
- lunar_toolkit/cli/serve.py +706 -0
- lunar_toolkit/cli/sfs.py +344 -0
- lunar_toolkit/cli/sfs_report.py +148 -0
- lunar_toolkit/cli/shadowcam.py +246 -0
- lunar_toolkit/cli/stereo.py +85 -0
- lunar_toolkit/cli/workspace.py +84 -0
- lunar_toolkit/config.py +345 -0
- lunar_toolkit/constants.py +217 -0
- lunar_toolkit/csm.py +340 -0
- lunar_toolkit/downloader.py +447 -0
- lunar_toolkit/geometry.py +556 -0
- lunar_toolkit/isis.py +222 -0
- lunar_toolkit/kaguya/__init__.py +45 -0
- lunar_toolkit/kaguya/mi.py +274 -0
- lunar_toolkit/kaguya/tc.py +370 -0
- lunar_toolkit/lroc/__init__.py +6 -0
- lunar_toolkit/lroc/cumindex.py +1197 -0
- lunar_toolkit/lroc/nac.py +702 -0
- lunar_toolkit/observation.py +127 -0
- lunar_toolkit/ode.py +485 -0
- lunar_toolkit/provenance.py +312 -0
- lunar_toolkit/reference.py +1111 -0
- lunar_toolkit/selection.py +714 -0
- lunar_toolkit/sfs.py +1534 -0
- lunar_toolkit/sfs_products.py +676 -0
- lunar_toolkit/sfs_report.py +1328 -0
- lunar_toolkit/shadowcam/__init__.py +29 -0
- lunar_toolkit/shadowcam/sc.py +392 -0
- lunar_toolkit/static/index.html +3259 -0
- lunar_toolkit/stereo.py +519 -0
- lunar_toolkit/stereo_dem.py +145 -0
- lunar_toolkit/tools.py +264 -0
- lunar_toolkit/workspace.py +1173 -0
- lunar_toolkit-0.1.0.dev2.dist-info/METADATA +177 -0
- lunar_toolkit-0.1.0.dev2.dist-info/RECORD +55 -0
- lunar_toolkit-0.1.0.dev2.dist-info/WHEEL +5 -0
- lunar_toolkit-0.1.0.dev2.dist-info/entry_points.txt +2 -0
- lunar_toolkit-0.1.0.dev2.dist-info/licenses/LICENSE +28 -0
- 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
|