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,29 @@
|
|
|
1
|
+
"""KPLO ShadowCam download, SPICE and CSM cameras for ASP."""
|
|
2
|
+
|
|
3
|
+
from .sc import (
|
|
4
|
+
ScOutputs,
|
|
5
|
+
ScProcessor,
|
|
6
|
+
ScProduct,
|
|
7
|
+
download_product,
|
|
8
|
+
has_spice,
|
|
9
|
+
import_sc,
|
|
10
|
+
is_sc_product_id,
|
|
11
|
+
lookup_products,
|
|
12
|
+
product_id_for,
|
|
13
|
+
sc_products,
|
|
14
|
+
select_sc_files,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ScOutputs",
|
|
19
|
+
"ScProcessor",
|
|
20
|
+
"ScProduct",
|
|
21
|
+
"download_product",
|
|
22
|
+
"has_spice",
|
|
23
|
+
"import_sc",
|
|
24
|
+
"is_sc_product_id",
|
|
25
|
+
"lookup_products",
|
|
26
|
+
"product_id_for",
|
|
27
|
+
"sc_products",
|
|
28
|
+
"select_sc_files",
|
|
29
|
+
]
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""KPLO ShadowCam: ODE file selection, download, SPICE and CSM cameras for ASP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Iterable, Sequence
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from .. import asp
|
|
14
|
+
from ..constants import DEFAULT_PROJECTION
|
|
15
|
+
from ..downloader import download_file, url_filename
|
|
16
|
+
from ..isis import isd_generate, map_project, spiceinit
|
|
17
|
+
from ..tools import is_dry_run, reuse_existing, run
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
from ..ode import OdeClient
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Observation M013178466S ships M013178466SE (raw, 8-bit, 3144 samples incl.
|
|
27
|
+
# bias columns; ODE "RAW") and M013178466SC (calibrated, float, 3072 samples;
|
|
28
|
+
# ODE "CAL"). Both are ISIS cubes with the Instrument group attached.
|
|
29
|
+
SC_ID_RE = re.compile(r"^M\d{9}S([CE]?)$", re.IGNORECASE)
|
|
30
|
+
|
|
31
|
+
PRODUCT_SUFFIX = {"raw": "E", "cal": "C"}
|
|
32
|
+
|
|
33
|
+
# ASP 3.7 example (docs/examples/shadowcam.rst): plain ``spiceinit from=<cube>``.
|
|
34
|
+
SPICEINIT_PARAMS: dict[str, Any] = {"shape": "system"}
|
|
35
|
+
|
|
36
|
+
# ``cam_test`` ISIS-vs-CSM comparison from the ASP example.
|
|
37
|
+
CAM_TEST_SAMPLE_RATE = 50
|
|
38
|
+
|
|
39
|
+
_POINTING_RE = re.compile(rb"^\s*InstrumentPointing\s*=", re.MULTILINE)
|
|
40
|
+
_HISTORY_RE = re.compile(
|
|
41
|
+
rb"^Object = History\s+Name\s*=\s*IsisCube\s+StartByte\s*=\s*(\d+)\s+Bytes\s*=\s*(\d+)", re.MULTILINE
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ScProduct:
|
|
47
|
+
"""Cube and PDS4 label URLs of one ShadowCam product."""
|
|
48
|
+
|
|
49
|
+
product_id: str
|
|
50
|
+
cube_url: str
|
|
51
|
+
label_url: str | None = None
|
|
52
|
+
cube_kbytes: float | None = None
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def observation_id(self) -> str:
|
|
56
|
+
"""``M013178466S`` for ``M013178466SE`` / ``M013178466SC``."""
|
|
57
|
+
return self.product_id[:-1]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_calibrated(self) -> bool:
|
|
61
|
+
return self.product_id.endswith("C")
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def cube_name(self) -> str:
|
|
65
|
+
return f"{self.product_id}.cub"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ScOutputs:
|
|
70
|
+
"""Files written for one product (None for skipped steps)."""
|
|
71
|
+
|
|
72
|
+
product_id: str
|
|
73
|
+
cube: Path
|
|
74
|
+
camera: Path | None = None
|
|
75
|
+
cam_test: Path | None = None
|
|
76
|
+
geotiff: Path | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_sc_product_id(product_id: str) -> bool:
|
|
80
|
+
"""True for a ShadowCam observation (``M…S``) or product (``M…SE`` / ``M…SC``) ID."""
|
|
81
|
+
return bool(SC_ID_RE.match(product_id.strip()))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def product_id_for(source: str, product: str = "raw") -> str:
|
|
85
|
+
"""Product ID for an observation or product ID.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
source : str
|
|
90
|
+
``M013178466S`` (observation), ``M013178466SE`` or ``M013178466SC``.
|
|
91
|
+
product : str
|
|
92
|
+
``"raw"`` (``…SE``, the cube in ASP's example) or ``"cal"`` (``…SC``);
|
|
93
|
+
only used for observation IDs.
|
|
94
|
+
"""
|
|
95
|
+
m = SC_ID_RE.match(source.strip())
|
|
96
|
+
if not m:
|
|
97
|
+
raise ValueError(f"Not a ShadowCam ID: {source!r}")
|
|
98
|
+
pid = source.strip().upper()
|
|
99
|
+
if m.group(1):
|
|
100
|
+
return pid
|
|
101
|
+
try:
|
|
102
|
+
return pid + PRODUCT_SUFFIX[product.lower()]
|
|
103
|
+
except KeyError:
|
|
104
|
+
raise ValueError(f"Unknown ShadowCam product {product!r}: {sorted(PRODUCT_SUFFIX)}") from None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def select_sc_files(files: Iterable[dict], product_id: str | None = None) -> ScProduct | None:
|
|
108
|
+
"""Pick the cube and label of a ShadowCam product from its ODE ``files`` list.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
files : iterable of dict
|
|
113
|
+
ODE product-file dicts (``FileName``, ``Type``, ``URL``, ``KBytes``).
|
|
114
|
+
product_id : str, optional
|
|
115
|
+
Product ID; derived from the cube file name when omitted.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
ScProduct or None
|
|
120
|
+
None when the list has no ``Product`` ``.cub`` (mosaics, DTMs). The
|
|
121
|
+
map-projected COGs and smear maps (``Referenced``) are never picked.
|
|
122
|
+
"""
|
|
123
|
+
cube: dict | None = None
|
|
124
|
+
label: dict | None = None
|
|
125
|
+
for f in files or []:
|
|
126
|
+
if str(f.get("Type", "")).lower() != "product" or not f.get("URL"):
|
|
127
|
+
continue
|
|
128
|
+
name = str(f.get("FileName") or url_filename(f["URL"])).upper()
|
|
129
|
+
if name.endswith(".CUB") and cube is None:
|
|
130
|
+
cube = f
|
|
131
|
+
elif name.endswith(".XML") and label is None:
|
|
132
|
+
label = f
|
|
133
|
+
if cube is None:
|
|
134
|
+
return None
|
|
135
|
+
pid = (product_id or url_filename(cube["URL"]).split(".")[0]).upper()
|
|
136
|
+
try:
|
|
137
|
+
kbytes = float(cube["KBytes"]) if cube.get("KBytes") is not None else None
|
|
138
|
+
except (TypeError, ValueError):
|
|
139
|
+
kbytes = None
|
|
140
|
+
return ScProduct(pid, cube["URL"], label["URL"] if label else None, kbytes)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def sc_products(df: pd.DataFrame) -> list[ScProduct]:
|
|
144
|
+
"""ShadowCam products with a downloadable cube from an ODE search.
|
|
145
|
+
|
|
146
|
+
Rows that are not ``…SE`` / ``…SC`` products (mosaics, DTMs) or lack a
|
|
147
|
+
cube are skipped with a warning.
|
|
148
|
+
"""
|
|
149
|
+
out: list[ScProduct] = []
|
|
150
|
+
pids = df["product_id"] if "product_id" in df else [None] * len(df)
|
|
151
|
+
for pid, files in zip(pids, df.get("files", [])):
|
|
152
|
+
if pid and not is_sc_product_id(pid):
|
|
153
|
+
log.warning("Skipping %s: not a ShadowCam RAW / CAL product", pid)
|
|
154
|
+
continue
|
|
155
|
+
product = select_sc_files(files, pid)
|
|
156
|
+
if product is None:
|
|
157
|
+
log.warning("Skipping %s: no .cub product file", pid)
|
|
158
|
+
continue
|
|
159
|
+
out.append(product)
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def lookup_products(
|
|
164
|
+
product_ids: Iterable[str],
|
|
165
|
+
client: OdeClient | None = None,
|
|
166
|
+
product: str = "raw",
|
|
167
|
+
) -> pd.DataFrame:
|
|
168
|
+
"""Query ODE by observation / product ID (see :func:`product_id_for`, :meth:`OdeClient.lookup`)."""
|
|
169
|
+
from ..ode import OdeClient
|
|
170
|
+
|
|
171
|
+
ids = [product_id_for(p, product) for p in product_ids]
|
|
172
|
+
return (client or OdeClient()).lookup(ids, instrument_label="ShadowCam")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def download_product(
|
|
176
|
+
product: ScProduct,
|
|
177
|
+
output_dir: Path,
|
|
178
|
+
force: bool = False,
|
|
179
|
+
show_progress: bool = True,
|
|
180
|
+
) -> Path:
|
|
181
|
+
"""Download one product's cube (and PDS4 label) into *output_dir*.
|
|
182
|
+
|
|
183
|
+
Existing files are reused unless *force*; interrupted downloads resume.
|
|
184
|
+
Returns the cube path.
|
|
185
|
+
"""
|
|
186
|
+
output_dir = Path(output_dir)
|
|
187
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
if product.label_url:
|
|
189
|
+
label = output_dir / url_filename(product.label_url)
|
|
190
|
+
if not reuse_existing(label, force):
|
|
191
|
+
download_file(product.label_url, label, show_progress=show_progress)
|
|
192
|
+
cube = output_dir / product.cube_name
|
|
193
|
+
if not reuse_existing(cube, force):
|
|
194
|
+
download_file(product.cube_url, cube, show_progress=show_progress)
|
|
195
|
+
return cube
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def has_spice(cube: Path) -> bool:
|
|
199
|
+
"""True when ``spiceinit`` completed on *cube*.
|
|
200
|
+
|
|
201
|
+
A failed ``spiceinit`` can leave SPICE tables in the label, so the cube's
|
|
202
|
+
History must also record a ``spiceinit`` run.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
with open(cube, "rb") as f:
|
|
206
|
+
head = f.read(1 << 20)
|
|
207
|
+
end = head.find(b"\nEnd\n")
|
|
208
|
+
label = head if end < 0 else head[:end]
|
|
209
|
+
m = _HISTORY_RE.search(label)
|
|
210
|
+
if not (m and _POINTING_RE.search(label)):
|
|
211
|
+
return False
|
|
212
|
+
f.seek(int(m.group(1)) - 1)
|
|
213
|
+
return b"Object = spiceinit" in f.read(int(m.group(2)))
|
|
214
|
+
except OSError:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class ScProcessor:
|
|
219
|
+
"""Download ShadowCam cubes and prepare them for ASP (SPICE + CSM camera).
|
|
220
|
+
|
|
221
|
+
Follows the ASP 3.7 ShadowCam example: ``spiceinit`` on the delivered cube
|
|
222
|
+
(in place), ``isd_generate -k <cube> <cube>`` for the CSM camera and,
|
|
223
|
+
optionally, ``cam_test`` and ``mapproject``. Needs ASP 3.7's custom ISIS /
|
|
224
|
+
ALE build and the KPLO kernels under ``$ISISDATA/kplo/kernels`` (rsync
|
|
225
|
+
from ``rsync.im-ldi.com::kplo``) plus ``$ISISDATA/base/kernels``.
|
|
226
|
+
|
|
227
|
+
Per product *output_dir* gets ``<id>.cub``, ``<id>.json`` (when *csm*),
|
|
228
|
+
``<id>.cam_test.txt`` (when *validate*) and ``<id>.tif`` (when
|
|
229
|
+
*mapproject*). Steps whose output exists (for ``spiceinit``: a cube
|
|
230
|
+
whose History records it, see :func:`has_spice`) are skipped unless
|
|
231
|
+
*force*.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
output_dir : Path
|
|
236
|
+
Where cubes, labels and cameras are written.
|
|
237
|
+
product : str
|
|
238
|
+
``"raw"`` (``…SE``, the cube in ASP's example) or ``"cal"`` (``…SC``,
|
|
239
|
+
radiometrically calibrated, bias columns removed) for observation IDs.
|
|
240
|
+
web : bool
|
|
241
|
+
Use the USGS SPICE web service (it serves the KPLO kernels) in
|
|
242
|
+
``spiceinit``. ALE's ShadowCam driver only reads NAIF kernel files,
|
|
243
|
+
so ``isd_generate`` needs the local kernels either way.
|
|
244
|
+
csm : bool
|
|
245
|
+
Write a CSM camera with ``isd_generate``.
|
|
246
|
+
validate : bool
|
|
247
|
+
Run ``cam_test`` (ISIS vs CSM camera) and save its report.
|
|
248
|
+
mapproject : bool
|
|
249
|
+
Write a GeoTIFF: ASP ``mapproject`` onto *dem* when given, else
|
|
250
|
+
ISIS ``cam2map`` onto the ``spiceinit`` shape.
|
|
251
|
+
dem : Path, optional
|
|
252
|
+
DEM for ASP ``mapproject``.
|
|
253
|
+
resolution : float, optional
|
|
254
|
+
Map resolution in m/pixel (ASP ``--tr`` / ``cam2map`` resolution).
|
|
255
|
+
t_srs : str, optional
|
|
256
|
+
PROJ string for ASP ``mapproject`` (default: the DEM's projection).
|
|
257
|
+
projection : str
|
|
258
|
+
ISIS projection for ``mosrange`` without *dem*.
|
|
259
|
+
keep : bool
|
|
260
|
+
Keep the ISIS map-projected cube.
|
|
261
|
+
force : bool
|
|
262
|
+
Redo every processing step (downloads are always reused).
|
|
263
|
+
spiceinit_params : dict, optional
|
|
264
|
+
Overrides merged into :data:`SPICEINIT_PARAMS`.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
output_dir: Path = Path("shadowcam"),
|
|
270
|
+
*,
|
|
271
|
+
product: str = "raw",
|
|
272
|
+
web: bool = False,
|
|
273
|
+
csm: bool = True,
|
|
274
|
+
validate: bool = False,
|
|
275
|
+
mapproject: bool = False,
|
|
276
|
+
dem: Path | None = None,
|
|
277
|
+
resolution: float | None = None,
|
|
278
|
+
t_srs: str | None = None,
|
|
279
|
+
projection: str = DEFAULT_PROJECTION,
|
|
280
|
+
keep: bool = False,
|
|
281
|
+
force: bool = False,
|
|
282
|
+
spiceinit_params: dict[str, Any] | None = None,
|
|
283
|
+
) -> None:
|
|
284
|
+
if product.lower() not in PRODUCT_SUFFIX:
|
|
285
|
+
raise ValueError(f"Unknown ShadowCam product {product!r}: {sorted(PRODUCT_SUFFIX)}")
|
|
286
|
+
self.output_dir = Path(output_dir)
|
|
287
|
+
self.product = product.lower()
|
|
288
|
+
self.web = web
|
|
289
|
+
self.csm = csm
|
|
290
|
+
self.validate = validate
|
|
291
|
+
self.mapproject = mapproject
|
|
292
|
+
self.dem = Path(dem) if dem else None
|
|
293
|
+
self.resolution = resolution
|
|
294
|
+
self.t_srs = t_srs
|
|
295
|
+
self.projection = projection
|
|
296
|
+
self.keep = keep
|
|
297
|
+
self.force = force
|
|
298
|
+
self.spiceinit_params = {**SPICEINIT_PARAMS, **(spiceinit_params or {})}
|
|
299
|
+
|
|
300
|
+
def process(self, source: ScProduct | str | Path) -> ScOutputs:
|
|
301
|
+
"""Download (if needed) and process one product.
|
|
302
|
+
|
|
303
|
+
*source* is a :class:`ScProduct`, a local ``.cub`` path or an
|
|
304
|
+
observation / product ID (looked up on ODE).
|
|
305
|
+
"""
|
|
306
|
+
if isinstance(source, ScProduct):
|
|
307
|
+
cube = download_product(source, self.output_dir)
|
|
308
|
+
elif Path(source).suffix.lower() == ".cub":
|
|
309
|
+
cube = Path(source)
|
|
310
|
+
else:
|
|
311
|
+
products = sc_products(lookup_products([str(source)], product=self.product))
|
|
312
|
+
if not products:
|
|
313
|
+
raise ValueError(f"No ShadowCam product {source!r} on ODE")
|
|
314
|
+
cube = download_product(products[0], self.output_dir)
|
|
315
|
+
return self.process_cube(cube)
|
|
316
|
+
|
|
317
|
+
def process_batch(
|
|
318
|
+
self,
|
|
319
|
+
sources: Sequence[ScProduct | str | Path],
|
|
320
|
+
max_workers: int = 2,
|
|
321
|
+
) -> list[ScOutputs | None]:
|
|
322
|
+
"""Process several products in parallel (None for failures)."""
|
|
323
|
+
results: list[ScOutputs | None] = [None] * len(sources)
|
|
324
|
+
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
325
|
+
futures = {pool.submit(self.process, s): i for i, s in enumerate(sources)}
|
|
326
|
+
for future in as_completed(futures):
|
|
327
|
+
i = futures[future]
|
|
328
|
+
try:
|
|
329
|
+
results[i] = future.result()
|
|
330
|
+
except Exception as exc:
|
|
331
|
+
log.error("Failed %s: %s", _source_name(sources[i]), exc)
|
|
332
|
+
return results
|
|
333
|
+
|
|
334
|
+
def cube_path(self, product: ScProduct) -> Path:
|
|
335
|
+
"""Where :meth:`process` downloads *product*'s cube."""
|
|
336
|
+
return self.output_dir / product.cube_name
|
|
337
|
+
|
|
338
|
+
def process_cube(self, cube: Path) -> ScOutputs:
|
|
339
|
+
"""Run the SPICE / CSM chain on a downloaded ShadowCam cube."""
|
|
340
|
+
cube = Path(cube)
|
|
341
|
+
pid = cube.name.split(".")[0].upper()
|
|
342
|
+
if not is_dry_run():
|
|
343
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
344
|
+
out = ScOutputs(pid, cube)
|
|
345
|
+
|
|
346
|
+
if self.force or not has_spice(cube):
|
|
347
|
+
spiceinit(cube, web=self.web, **self.spiceinit_params)
|
|
348
|
+
else:
|
|
349
|
+
log.info("Reusing SPICE in %s", cube)
|
|
350
|
+
|
|
351
|
+
if self.csm:
|
|
352
|
+
out.camera = self.output_dir / f"{pid}.json"
|
|
353
|
+
if not reuse_existing(out.camera, self.force):
|
|
354
|
+
isd_generate(cube, out.camera)
|
|
355
|
+
|
|
356
|
+
if self.validate and out.camera:
|
|
357
|
+
out.cam_test = self.output_dir / f"{pid}.cam_test.txt"
|
|
358
|
+
if not reuse_existing(out.cam_test, self.force):
|
|
359
|
+
self._cam_test(cube, out.camera, out.cam_test)
|
|
360
|
+
|
|
361
|
+
if self.mapproject:
|
|
362
|
+
out.geotiff = self.output_dir / f"{pid}.tif"
|
|
363
|
+
if not reuse_existing(out.geotiff, self.force):
|
|
364
|
+
self._mapproject(cube, out.camera, out.geotiff)
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
def _cam_test(self, cube: Path, camera: Path, report: Path) -> None:
|
|
368
|
+
args = ["--image", cube, "--cam1", cube, "--cam2", camera]
|
|
369
|
+
args += ["--session1", "isis", "--session2", "csm", "--sample-rate", CAM_TEST_SAMPLE_RATE]
|
|
370
|
+
res = run("cam_test", args, capture=True)
|
|
371
|
+
if not is_dry_run():
|
|
372
|
+
report.write_text(res.stdout or "")
|
|
373
|
+
|
|
374
|
+
def _mapproject(self, cube: Path, camera: Path | None, tif: Path) -> None:
|
|
375
|
+
if self.dem:
|
|
376
|
+
asp.mapproject(self.dem, cube, tif, camera, tr=self.resolution, t_srs=self.t_srs)
|
|
377
|
+
return
|
|
378
|
+
map_cube = tif.with_suffix(".map.cub")
|
|
379
|
+
map_project(cube, map_cube, self.projection, self.resolution, keep=self.keep)
|
|
380
|
+
run("gdal_translate", ["-of", "GTiff", "-co", "COMPRESS=DEFLATE", "-co", "TILED=YES", map_cube, tif])
|
|
381
|
+
if not self.keep and not is_dry_run():
|
|
382
|
+
map_cube.unlink(missing_ok=True)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def import_sc(cube: Path, output_dir: Path | None = None, **kwargs: Any) -> ScOutputs:
|
|
386
|
+
"""Process one downloaded ShadowCam cube; see :class:`ScProcessor` for *kwargs*."""
|
|
387
|
+
cube = Path(cube)
|
|
388
|
+
return ScProcessor(output_dir or cube.parent, **kwargs).process_cube(cube)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _source_name(source: Any) -> str:
|
|
392
|
+
return source.product_id if isinstance(source, ScProduct) else str(source)
|