chore: initialize insar management system v2
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Bundled ISCE2 production pipeline scripts."""
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert LT-1 text precise orbit files to the XML structure expected by ISCE2 LUTAN1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateVector:
|
||||
time: datetime
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
vx: float
|
||||
vy: float
|
||||
vz: float
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert LT-1 GpsData .txt orbit files to ISCE2 LUTAN1 orbit XML."
|
||||
)
|
||||
parser.add_argument("input_txt", type=Path, help="Input LT-1 GpsData text file")
|
||||
parser.add_argument("output_xml", type=Path, help="Output orbit XML file")
|
||||
parser.add_argument(
|
||||
"--annotation-xml",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional LT-1 annotation/meta XML used to clip the orbit to scene time +/- margin",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Optional explicit UTC start time, e.g. 2025-01-12T09:13:24.000000",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stop",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Optional explicit UTC stop time, e.g. 2025-01-12T09:13:32.000000",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--margin-sec",
|
||||
type=float,
|
||||
default=60.0,
|
||||
help="Seconds to expand around annotation/start-stop window",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_flexible_datetime(value: str) -> datetime:
|
||||
value = value.strip().replace("Z", "")
|
||||
try:
|
||||
return datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# LT-1 annotation files may store single-digit hours like T9:13:24.042863.
|
||||
match = re.match(r"^(\d{4}-\d{2}-\d{2}T)(\d{1})(:.*)$", value)
|
||||
if match:
|
||||
value = f"{match.group(1)}0{match.group(2)}{match.group(3)}"
|
||||
|
||||
for fmt in ("%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"):
|
||||
try:
|
||||
return datetime.strptime(value, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
raise ValueError(f"Unsupported datetime format: {value}")
|
||||
|
||||
|
||||
def parse_annotation_window(annotation_xml: Path, margin_sec: float) -> tuple[datetime, datetime]:
|
||||
root = ET.parse(annotation_xml).getroot()
|
||||
|
||||
start_text = find_text(root, "productInfo/sceneInfo/start/timeUTC")
|
||||
stop_text = find_text(root, "productInfo/sceneInfo/stop/timeUTC")
|
||||
|
||||
start_time = parse_flexible_datetime(start_text) - timedelta(seconds=margin_sec)
|
||||
stop_time = parse_flexible_datetime(stop_text) + timedelta(seconds=margin_sec)
|
||||
return start_time, stop_time
|
||||
|
||||
|
||||
def find_text(root: ET.Element, path: str) -> str:
|
||||
node = root.find(path)
|
||||
if node is None or node.text is None:
|
||||
raise ValueError(f"Missing XML path: {path}")
|
||||
return node.text.strip()
|
||||
|
||||
|
||||
def parse_orbit_file(input_txt: Path) -> list[StateVector]:
|
||||
vectors: list[StateVector] = []
|
||||
|
||||
with input_txt.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
if not line.strip() or line.startswith("#"):
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 12:
|
||||
continue
|
||||
|
||||
sec_float = float(parts[5])
|
||||
sec_int = int(sec_float)
|
||||
microsecond = int(round((sec_float - sec_int) * 1_000_000))
|
||||
|
||||
timestamp = datetime(
|
||||
int(parts[0]),
|
||||
int(parts[1]),
|
||||
int(parts[2]),
|
||||
int(parts[3]),
|
||||
int(parts[4]),
|
||||
sec_int,
|
||||
microsecond,
|
||||
)
|
||||
|
||||
vectors.append(
|
||||
StateVector(
|
||||
time=timestamp,
|
||||
x=float(parts[6]),
|
||||
y=float(parts[7]),
|
||||
z=float(parts[8]),
|
||||
vx=float(parts[9]),
|
||||
vy=float(parts[10]),
|
||||
vz=float(parts[11]),
|
||||
)
|
||||
)
|
||||
|
||||
if not vectors:
|
||||
raise ValueError(f"No orbit records parsed from {input_txt}")
|
||||
|
||||
return vectors
|
||||
|
||||
|
||||
def clip_vectors(
|
||||
vectors: Iterable[StateVector],
|
||||
start_time: Optional[datetime],
|
||||
stop_time: Optional[datetime],
|
||||
) -> list[StateVector]:
|
||||
if start_time is None and stop_time is None:
|
||||
return list(vectors)
|
||||
|
||||
clipped = [
|
||||
vector
|
||||
for vector in vectors
|
||||
if (start_time is None or vector.time >= start_time)
|
||||
and (stop_time is None or vector.time <= stop_time)
|
||||
]
|
||||
if not clipped:
|
||||
raise ValueError("No orbit records left after time clipping")
|
||||
return clipped
|
||||
|
||||
|
||||
def build_xml(vectors: Iterable[StateVector]) -> ET.ElementTree:
|
||||
root = ET.Element("Earth_Explorer_File")
|
||||
data_block = ET.SubElement(root, "Data_Block")
|
||||
vectors = list(vectors)
|
||||
list_of_osvs = ET.SubElement(data_block, "List_of_OSVs", count=str(len(vectors)))
|
||||
|
||||
for vector in vectors:
|
||||
osv = ET.SubElement(list_of_osvs, "OSV")
|
||||
ET.SubElement(osv, "UTC").text = vector.time.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
ET.SubElement(osv, "X").text = format_float(vector.x)
|
||||
ET.SubElement(osv, "Y").text = format_float(vector.y)
|
||||
ET.SubElement(osv, "Z").text = format_float(vector.z)
|
||||
ET.SubElement(osv, "VX").text = format_float(vector.vx)
|
||||
ET.SubElement(osv, "VY").text = format_float(vector.vy)
|
||||
ET.SubElement(osv, "VZ").text = format_float(vector.vz)
|
||||
|
||||
ET.indent(root, space=" ")
|
||||
return ET.ElementTree(root)
|
||||
|
||||
|
||||
def format_float(value: float) -> str:
|
||||
return f"{value:.10f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
vectors = parse_orbit_file(args.input_txt)
|
||||
|
||||
start_time: Optional[datetime] = None
|
||||
stop_time: Optional[datetime] = None
|
||||
|
||||
if args.annotation_xml is not None:
|
||||
start_time, stop_time = parse_annotation_window(args.annotation_xml, args.margin_sec)
|
||||
|
||||
if args.start:
|
||||
start_time = parse_flexible_datetime(args.start)
|
||||
if args.stop:
|
||||
stop_time = parse_flexible_datetime(args.stop)
|
||||
|
||||
clipped = clip_vectors(vectors, start_time, stop_time)
|
||||
tree = build_xml(clipped)
|
||||
|
||||
args.output_xml.parent.mkdir(parents=True, exist_ok=True)
|
||||
tree.write(args.output_xml, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
print(f"Input orbit: {args.input_txt}")
|
||||
print(f"Output xml: {args.output_xml}")
|
||||
print(f"Records read: {len(vectors)}")
|
||||
print(f"Records kept: {len(clipped)}")
|
||||
if start_time and stop_time:
|
||||
print(
|
||||
"Window: "
|
||||
f"{start_time.strftime('%Y-%m-%dT%H:%M:%S.%f')} -> "
|
||||
f"{stop_time.strftime('%Y-%m-%dT%H:%M:%S.%f')}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from osgeo import gdal
|
||||
|
||||
gdal.UseExceptions()
|
||||
|
||||
DEFAULT_WAVELENGTH = 0.23793052222222222
|
||||
DEFAULT_NODATA = -9999.0
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Export ISCE2 geocoded displacement/coherence products to GeoTIFF."
|
||||
)
|
||||
parser.add_argument(
|
||||
"work_dir",
|
||||
type=Path,
|
||||
help="ISCE2 work directory containing interferogram/*.geo outputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Output directory for GeoTIFF files. Default: work_dir",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefix",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Output filename prefix. Default: work_dir basename",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wavelength",
|
||||
type=float,
|
||||
default=DEFAULT_WAVELENGTH,
|
||||
help="Radar wavelength in meters",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--coh-threshold",
|
||||
type=float,
|
||||
default=0.05,
|
||||
help="Mask pixels with coherence below this threshold in *_disp.tif",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-disp-full",
|
||||
action="store_true",
|
||||
help="Also export the unmasked displacement GeoTIFF for debugging",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def write_geotiff(array: np.ndarray, ref_ds: gdal.Dataset, out_path: Path, nodata: float) -> None:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if out_path.exists():
|
||||
out_path.unlink()
|
||||
|
||||
driver = gdal.GetDriverByName("GTiff")
|
||||
ds = driver.Create(
|
||||
str(out_path),
|
||||
ref_ds.RasterXSize,
|
||||
ref_ds.RasterYSize,
|
||||
1,
|
||||
gdal.GDT_Float32,
|
||||
options=["COMPRESS=LZW", "TILED=YES"],
|
||||
)
|
||||
ds.SetGeoTransform(ref_ds.GetGeoTransform())
|
||||
ds.SetProjection(ref_ds.GetProjection())
|
||||
band = ds.GetRasterBand(1)
|
||||
band.SetNoDataValue(nodata)
|
||||
band.WriteArray(array.astype(np.float32))
|
||||
ds.FlushCache()
|
||||
ds = None
|
||||
|
||||
|
||||
def export_products(
|
||||
work_dir: Path,
|
||||
output_dir: Path,
|
||||
prefix: str,
|
||||
wavelength: float,
|
||||
coh_threshold: float,
|
||||
include_disp_full: bool = False,
|
||||
nodata: float = DEFAULT_NODATA,
|
||||
) -> dict[str, Path]:
|
||||
unw_path = work_dir / "interferogram" / "filt_topophase.unw.geo.vrt"
|
||||
cor_path = work_dir / "interferogram" / "topophase.cor.geo.vrt"
|
||||
|
||||
if not unw_path.exists():
|
||||
raise FileNotFoundError(f"Missing unwrapped product: {unw_path}")
|
||||
if not cor_path.exists():
|
||||
raise FileNotFoundError(f"Missing coherence product: {cor_path}")
|
||||
|
||||
unw_ds = gdal.Open(str(unw_path))
|
||||
cor_ds = gdal.Open(str(cor_path))
|
||||
if unw_ds is None or cor_ds is None:
|
||||
raise RuntimeError("Failed to open ISCE2 geo products with GDAL.")
|
||||
|
||||
amp = unw_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
|
||||
phase = unw_ds.GetRasterBand(2).ReadAsArray().astype(np.float32)
|
||||
|
||||
coh_band = 2 if cor_ds.RasterCount >= 2 else 1
|
||||
coh = cor_ds.GetRasterBand(coh_band).ReadAsArray().astype(np.float32)
|
||||
coh_valid = coh > 0
|
||||
|
||||
disp_m = phase * wavelength / (4.0 * np.pi)
|
||||
disp_m_full = disp_m.copy()
|
||||
|
||||
mask = (coh < coh_threshold) | (amp == 0)
|
||||
disp_m[mask] = nodata
|
||||
disp_m_full[amp == 0] = nodata
|
||||
|
||||
coh_out = coh.copy()
|
||||
coh_out[~coh_valid] = nodata
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_disp = output_dir / f"{prefix}_disp.tif"
|
||||
out_coh = output_dir / f"{prefix}_coh.tif"
|
||||
|
||||
write_geotiff(disp_m, unw_ds, out_disp, nodata)
|
||||
write_geotiff(coh_out, cor_ds, out_coh, nodata)
|
||||
out_disp_full = None
|
||||
if include_disp_full:
|
||||
out_disp_full = output_dir / f"{prefix}_disp_full.tif"
|
||||
write_geotiff(disp_m_full, unw_ds, out_disp_full, nodata)
|
||||
|
||||
valid_disp = disp_m[disp_m != nodata]
|
||||
valid_coh = coh_out[coh_out != nodata]
|
||||
valid_full = disp_m_full[disp_m_full != nodata] if include_disp_full else np.array([], dtype=np.float32)
|
||||
|
||||
print(f"Work dir: {work_dir}")
|
||||
print(f"Output prefix: {prefix}")
|
||||
print(f"Coherence threshold: {coh_threshold}")
|
||||
print(f"Unwrap support ratio: {(amp != 0).mean()*100:.2f}%")
|
||||
print(f"Coherence support ratio: {coh_valid.mean()*100:.2f}%")
|
||||
print(f"Masked disp ratio: {(disp_m != nodata).mean()*100:.2f}%")
|
||||
if valid_disp.size:
|
||||
print(f"Masked disp range: [{valid_disp.min():.4f}, {valid_disp.max():.4f}] m")
|
||||
if include_disp_full and valid_full.size:
|
||||
print(f"Full disp range: [{valid_full.min():.4f}, {valid_full.max():.4f}] m")
|
||||
if valid_coh.size:
|
||||
print(f"Coherence range: [{valid_coh.min():.4f}, {valid_coh.max():.4f}]")
|
||||
print(f"Wrote: {out_disp}")
|
||||
if out_disp_full is not None:
|
||||
print(f"Wrote: {out_disp_full}")
|
||||
print(f"Wrote: {out_coh}")
|
||||
|
||||
unw_ds = None
|
||||
cor_ds = None
|
||||
outputs: dict[str, Path] = {
|
||||
"disp": out_disp,
|
||||
"coh": out_coh,
|
||||
}
|
||||
if out_disp_full is not None:
|
||||
outputs["disp_full"] = out_disp_full
|
||||
return outputs
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
work_dir = args.work_dir.resolve()
|
||||
output_dir = args.output_dir.resolve() if args.output_dir else work_dir
|
||||
prefix = args.prefix or work_dir.name
|
||||
|
||||
export_products(
|
||||
work_dir=work_dir,
|
||||
output_dir=output_dir,
|
||||
prefix=prefix,
|
||||
wavelength=args.wavelength,
|
||||
coh_threshold=args.coh_threshold,
|
||||
include_disp_full=args.include_disp_full,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, Mapping, Optional
|
||||
|
||||
try:
|
||||
from .convert_lt1_orbit_to_isce_xml import (
|
||||
build_xml,
|
||||
clip_vectors,
|
||||
parse_annotation_window,
|
||||
parse_orbit_file,
|
||||
)
|
||||
except ImportError:
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from convert_lt1_orbit_to_isce_xml import ( # type: ignore
|
||||
build_xml,
|
||||
clip_vectors,
|
||||
parse_annotation_window,
|
||||
parse_orbit_file,
|
||||
)
|
||||
|
||||
|
||||
PathTransform = Callable[[str | Path], Path]
|
||||
|
||||
|
||||
DEFAULT_WINDOWS_DEM_CANDIDATES = (
|
||||
r"D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84",
|
||||
r"D:\SRTM30m\SRTMDEM_RSP_SARscape",
|
||||
)
|
||||
DEFAULT_WSL_DEM_CANDIDATES = (
|
||||
"/mnt/d/SRTM30m/SRTMDEM_RSP_SARscape.wgs84",
|
||||
"/mnt/d/SRTM30m/SRTMDEM_RSP_SARscape",
|
||||
)
|
||||
DEFAULT_WINDOWS_ORBIT_POOL_CANDIDATES = (r"D:\orbit_pools\isce2",)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OrbitXmlResolution:
|
||||
path: Path
|
||||
source: str
|
||||
source_txt: Optional[Path] = None
|
||||
|
||||
|
||||
def identity_path_transform(value: str | Path) -> Path:
|
||||
return value if isinstance(value, Path) else Path(str(value))
|
||||
|
||||
|
||||
def load_env_file(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
if not path.exists():
|
||||
return values
|
||||
|
||||
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||
continue
|
||||
key, value = stripped.split("=", 1)
|
||||
values[key.strip()] = value.strip()
|
||||
return values
|
||||
|
||||
|
||||
def parse_scene_window(meta_path: Path, margin_sec: float = 0.0) -> tuple:
|
||||
return parse_annotation_window(meta_path, margin_sec)
|
||||
|
||||
|
||||
def resolve_orbit_pool_path(
|
||||
explicit_path: str | Path | None = None,
|
||||
env_values: Mapping[str, str] | None = None,
|
||||
extra_candidates: Iterable[str | Path] | None = None,
|
||||
default_candidates: Iterable[str | Path] | None = None,
|
||||
path_transform: PathTransform = identity_path_transform,
|
||||
) -> Optional[Path]:
|
||||
candidates: list[str | Path] = []
|
||||
if explicit_path:
|
||||
candidates.append(explicit_path)
|
||||
if extra_candidates:
|
||||
candidates.extend(extra_candidates)
|
||||
|
||||
if env_values:
|
||||
for key in ("ORBIT_POOL_ISCE2", "ISCE2_ORBIT_DIR"):
|
||||
value = env_values.get(key)
|
||||
if value:
|
||||
candidates.append(value)
|
||||
|
||||
if default_candidates:
|
||||
candidates.extend(default_candidates)
|
||||
|
||||
return resolve_existing_directory(candidates, path_transform=path_transform)
|
||||
|
||||
|
||||
def resolve_prepared_dem_path(
|
||||
explicit_path: str | Path | None = None,
|
||||
env_values: Mapping[str, str] | None = None,
|
||||
extra_candidates: Iterable[str | Path] | None = None,
|
||||
default_candidates: Iterable[str | Path] | None = None,
|
||||
path_transform: PathTransform = identity_path_transform,
|
||||
) -> Optional[Path]:
|
||||
candidates: list[str | Path] = []
|
||||
if explicit_path:
|
||||
candidates.extend(_prepared_dem_variants(explicit_path))
|
||||
if extra_candidates:
|
||||
for candidate in extra_candidates:
|
||||
candidates.extend(_prepared_dem_variants(candidate))
|
||||
|
||||
if env_values:
|
||||
env_dem = env_values.get("ISCE2_DEM_PATH")
|
||||
if env_dem:
|
||||
candidates.extend(_prepared_dem_variants(env_dem))
|
||||
|
||||
env_base = env_values.get("IDL_DINSAR_DEM_BASE_FILE")
|
||||
if env_base:
|
||||
if str(env_base).lower().endswith(".wgs84"):
|
||||
candidates.extend(_prepared_dem_variants(env_base))
|
||||
else:
|
||||
candidates.extend((f"{env_base}.wgs84", env_base))
|
||||
|
||||
if default_candidates:
|
||||
for candidate in default_candidates:
|
||||
candidates.extend(_prepared_dem_variants(candidate))
|
||||
|
||||
return resolve_existing_prepared_file(candidates, path_transform=path_transform)
|
||||
|
||||
|
||||
def resolve_existing_directory(
|
||||
candidates: Iterable[str | Path],
|
||||
path_transform: PathTransform = identity_path_transform,
|
||||
) -> Optional[Path]:
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
path = path_transform(candidate)
|
||||
key = str(path)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if path.exists() and path.is_dir():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def resolve_existing_prepared_file(
|
||||
candidates: Iterable[str | Path],
|
||||
path_transform: PathTransform = identity_path_transform,
|
||||
) -> Optional[Path]:
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
path = path_transform(candidate)
|
||||
key = str(path)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if path.exists() and Path(str(path) + ".xml").exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def ensure_lt1_orbit_xml(
|
||||
date_yyyymmdd: str,
|
||||
satellite: str,
|
||||
annotation_xml: Path,
|
||||
orbit_root: Path,
|
||||
orbit_output_dir: Path,
|
||||
margin_sec: float,
|
||||
) -> OrbitXmlResolution:
|
||||
stem = build_lt1_orbit_stem(satellite=satellite, date_yyyymmdd=date_yyyymmdd)
|
||||
existing_xml = find_existing_lt1_orbit_xml(
|
||||
date_yyyymmdd=date_yyyymmdd,
|
||||
satellite=satellite,
|
||||
orbit_root=orbit_root,
|
||||
orbit_output_dir=orbit_output_dir,
|
||||
)
|
||||
if existing_xml is not None:
|
||||
return OrbitXmlResolution(path=existing_xml, source="existing_xml")
|
||||
|
||||
txt_path = find_existing_lt1_orbit_text(
|
||||
date_yyyymmdd=date_yyyymmdd,
|
||||
satellite=satellite,
|
||||
orbit_root=orbit_root,
|
||||
)
|
||||
if txt_path is None:
|
||||
raise FileNotFoundError(
|
||||
"Missing LT-1 precise orbit source. "
|
||||
f"Searched under: {orbit_root} for {stem}.xml/.txt"
|
||||
)
|
||||
|
||||
orbit_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
xml_path = orbit_output_dir / f"{stem}.xml"
|
||||
vectors = parse_orbit_file(txt_path)
|
||||
start_time, stop_time = parse_annotation_window(annotation_xml, margin_sec)
|
||||
clipped = clip_vectors(vectors, start_time, stop_time)
|
||||
tree = build_xml(clipped)
|
||||
tree.write(xml_path, encoding="utf-8", xml_declaration=True)
|
||||
return OrbitXmlResolution(path=xml_path, source="generated_from_txt", source_txt=txt_path)
|
||||
|
||||
|
||||
def find_existing_lt1_orbit_xml(
|
||||
date_yyyymmdd: str,
|
||||
satellite: str,
|
||||
orbit_root: Path,
|
||||
orbit_output_dir: Path | None = None,
|
||||
) -> Optional[Path]:
|
||||
stem = build_lt1_orbit_stem(satellite=satellite, date_yyyymmdd=date_yyyymmdd)
|
||||
candidates = []
|
||||
if orbit_output_dir is not None:
|
||||
candidates.append(orbit_output_dir / f"{stem}.xml")
|
||||
candidates.extend(
|
||||
[
|
||||
orbit_root / f"{stem}.xml",
|
||||
orbit_root / satellite.upper() / f"{stem}.xml",
|
||||
orbit_root / "converted" / "isce2" / f"{stem}.xml",
|
||||
]
|
||||
)
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def find_existing_lt1_orbit_text(
|
||||
date_yyyymmdd: str,
|
||||
satellite: str,
|
||||
orbit_root: Path,
|
||||
) -> Optional[Path]:
|
||||
stem = build_lt1_orbit_stem(satellite=satellite, date_yyyymmdd=date_yyyymmdd)
|
||||
candidates = [
|
||||
orbit_root / satellite.upper() / f"{stem}.txt",
|
||||
orbit_root / f"{stem}.txt",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def build_lt1_orbit_stem(satellite: str, date_yyyymmdd: str) -> str:
|
||||
return f"{satellite.upper()}_GpsData_GAS_C_{date_yyyymmdd}"
|
||||
|
||||
|
||||
def _prepared_dem_variants(value: str | Path) -> tuple[str | Path, ...]:
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return ()
|
||||
if text.lower().endswith(".wgs84"):
|
||||
return (value,)
|
||||
return (value, f"{text}.wgs84")
|
||||
@@ -0,0 +1,582 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from export_isce_geotiff import DEFAULT_WAVELENGTH, export_products
|
||||
from lt1_input_resolver import (
|
||||
DEFAULT_WSL_DEM_CANDIDATES,
|
||||
ensure_lt1_orbit_xml,
|
||||
resolve_prepared_dem_path,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_TARGET_GRID_SIZE_M = 10
|
||||
METERS_PER_DEGREE = 111320.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Scene:
|
||||
role: str
|
||||
tiff_path: Path
|
||||
meta_path: Path
|
||||
date_yyyymmdd: str
|
||||
orbit_xml_path: Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineConfig:
|
||||
task_name: str
|
||||
output_prefix: str
|
||||
dem_path: Path
|
||||
reference: Scene
|
||||
secondary: Scene
|
||||
bbox: list[float] | None
|
||||
target_grid_size_m: int
|
||||
geo_posting_deg: float
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
repo_root = script_dir.parent
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run an LT-1 ISCE2 DInSAR production pipeline with SNAPHU."
|
||||
)
|
||||
parser.add_argument("task_dir", help="Task directory, for example Task_20250112_20250309")
|
||||
parser.add_argument(
|
||||
"--task-name",
|
||||
default=None,
|
||||
help="Override the task name used for work directory and default outputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--work-root",
|
||||
default=str(script_dir / "jobs"),
|
||||
help="Root directory for ISCE2 work folders",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--work-dir",
|
||||
default=None,
|
||||
help="Explicit work directory. Overrides --work-root/<task_name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=None,
|
||||
help="Directory for final GeoTIFFs. Default: work_dir",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-prefix",
|
||||
default=None,
|
||||
help="Prefix for final output filenames. Default: task name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--orbit-root",
|
||||
default=str(repo_root / "orbit"),
|
||||
help="Directory containing LT1A_GpsData_GAS_C_YYYYMMDD.txt",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--orbit-output-dir",
|
||||
default=None,
|
||||
help="Directory to place generated orbit XML files. Default: work_dir/orbits",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dem",
|
||||
default=None,
|
||||
help="DEM base path. Default: auto-detect the prepared WGS84 DEM",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bbox",
|
||||
default=None,
|
||||
help="Optional geocode bounding box: south,north,west,east",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bbox-margin",
|
||||
type=float,
|
||||
default=0.05,
|
||||
help="Auto-expand topo estimated bbox by this many degrees on each side",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--orbit-margin-sec",
|
||||
type=float,
|
||||
default=60.0,
|
||||
help="Seconds to expand around scene time when clipping precise orbit, must be between 60 and 120",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--master-dir-name",
|
||||
default="master",
|
||||
help="Subdirectory name for the reference scene inside the task directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slave-dir-name",
|
||||
default="slave",
|
||||
help="Subdirectory name for the secondary scene inside the task directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scene-glob",
|
||||
default="*.tiff",
|
||||
help="Glob pattern used to find scene files inside master/slave directories",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefer-scene-keyword",
|
||||
default="_SLC_",
|
||||
help="Prefer matching files containing this keyword when multiple scene files are present",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--coh-threshold",
|
||||
type=float,
|
||||
default=0.05,
|
||||
help="Coherence threshold for *_disp.tif export",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-grid-size-m",
|
||||
type=int,
|
||||
default=DEFAULT_TARGET_GRID_SIZE_M,
|
||||
help="Target grid size in meters used to control multilook scale and geocoding spacing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-disp-full",
|
||||
action="store_true",
|
||||
help="Also export the unmasked displacement GeoTIFF for debugging",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wavelength",
|
||||
type=float,
|
||||
default=DEFAULT_WAVELENGTH,
|
||||
help="Radar wavelength in meters",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Delete an existing work directory before rerunning",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Resolve inputs and print the planned configuration without running ISCE2",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.orbit_margin_sec < 60 or args.orbit_margin_sec > 120:
|
||||
raise ValueError("--orbit-margin-sec must be between 60 and 120 seconds")
|
||||
if args.target_grid_size_m <= 0:
|
||||
raise ValueError("--target-grid-size-m must be greater than 0")
|
||||
return args
|
||||
|
||||
|
||||
def locate_stripmap_app() -> Path:
|
||||
import isce
|
||||
|
||||
app_path = Path(isce.__file__).resolve().parent / "applications" / "stripmapApp.py"
|
||||
if not app_path.exists():
|
||||
raise FileNotFoundError(f"stripmapApp.py not found: {app_path}")
|
||||
return app_path
|
||||
|
||||
|
||||
def normalize_linux_path(value: str | Path) -> Path:
|
||||
text = str(value).strip()
|
||||
if text.startswith("\\\\"):
|
||||
raise ValueError("UNC paths are not supported directly. Mount them in WSL first.")
|
||||
|
||||
match = re.match(r"^([A-Za-z]):[\\/](.*)$", text)
|
||||
if match:
|
||||
drive = match.group(1).lower()
|
||||
rest = match.group(2).replace("\\", "/")
|
||||
return Path(f"/mnt/{drive}/{rest}")
|
||||
|
||||
return Path(text)
|
||||
|
||||
|
||||
def choose_scene_tiff(scene_dir: Path, scene_glob: str, prefer_scene_keyword: str) -> Path:
|
||||
candidates = sorted(scene_dir.glob(scene_glob))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"No scene file matching {scene_glob} found in {scene_dir}")
|
||||
|
||||
slc_candidates = [path for path in candidates if prefer_scene_keyword in path.name]
|
||||
if len(slc_candidates) == 1:
|
||||
return slc_candidates[0]
|
||||
if len(candidates) == 1:
|
||||
return candidates[0]
|
||||
raise RuntimeError(
|
||||
f"Expected one scene file in {scene_dir}; found {len(candidates)} matches for {scene_glob}"
|
||||
)
|
||||
|
||||
|
||||
def scene_meta_from_tiff(tiff_path: Path) -> Path:
|
||||
meta_path = Path(str(tiff_path).replace(".tiff", ".meta.xml"))
|
||||
if not meta_path.exists():
|
||||
raise FileNotFoundError(f"Missing meta XML for {tiff_path}: {meta_path}")
|
||||
return meta_path
|
||||
|
||||
|
||||
def extract_scene_date(name: str) -> str:
|
||||
match = re.search(r"_(\d{8})_SLC_", name)
|
||||
if not match:
|
||||
match = re.search(r"(\d{8})", name)
|
||||
if not match:
|
||||
raise ValueError(f"Unable to extract scene date from filename: {name}")
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def ensure_orbit_xml(
|
||||
date_yyyymmdd: str,
|
||||
annotation_xml: Path,
|
||||
orbit_root: Path,
|
||||
orbit_out_dir: Path,
|
||||
margin_sec: float,
|
||||
) -> Path:
|
||||
resolution = ensure_lt1_orbit_xml(
|
||||
date_yyyymmdd=date_yyyymmdd,
|
||||
satellite="LT1A",
|
||||
annotation_xml=annotation_xml,
|
||||
orbit_root=orbit_root,
|
||||
orbit_output_dir=orbit_out_dir,
|
||||
margin_sec=margin_sec,
|
||||
)
|
||||
return resolution.path
|
||||
|
||||
|
||||
def resolve_dem(dem_value: str | None) -> Path:
|
||||
dem_path = resolve_prepared_dem_path(
|
||||
explicit_path=dem_value,
|
||||
env_values=None,
|
||||
default_candidates=DEFAULT_WSL_DEM_CANDIDATES,
|
||||
path_transform=normalize_linux_path,
|
||||
)
|
||||
if dem_path is not None:
|
||||
return dem_path
|
||||
|
||||
searched = ", ".join(str(path) for path in DEFAULT_WSL_DEM_CANDIDATES)
|
||||
raise FileNotFoundError(
|
||||
"Unable to resolve a prepared DEM with ISCE wrappers. "
|
||||
f"Searched: {searched}"
|
||||
)
|
||||
|
||||
|
||||
def resolve_task(
|
||||
task_dir: Path,
|
||||
orbit_root: Path,
|
||||
orbit_out_dir: Path,
|
||||
margin_sec: float,
|
||||
master_dir_name: str,
|
||||
slave_dir_name: str,
|
||||
scene_glob: str,
|
||||
prefer_scene_keyword: str,
|
||||
) -> tuple[Scene, Scene]:
|
||||
scenes: list[Scene] = []
|
||||
for role, subdir in (("master", master_dir_name), ("slave", slave_dir_name)):
|
||||
scene_dir = task_dir / subdir
|
||||
if not scene_dir.exists():
|
||||
raise FileNotFoundError(f"Missing task subdirectory: {scene_dir}")
|
||||
|
||||
tiff_path = choose_scene_tiff(scene_dir, scene_glob, prefer_scene_keyword)
|
||||
meta_path = scene_meta_from_tiff(tiff_path)
|
||||
date_yyyymmdd = extract_scene_date(tiff_path.name)
|
||||
orbit_xml_path = ensure_orbit_xml(
|
||||
date_yyyymmdd=date_yyyymmdd,
|
||||
annotation_xml=meta_path,
|
||||
orbit_root=orbit_root,
|
||||
orbit_out_dir=orbit_out_dir,
|
||||
margin_sec=margin_sec,
|
||||
)
|
||||
scenes.append(
|
||||
Scene(
|
||||
role=role,
|
||||
tiff_path=tiff_path,
|
||||
meta_path=meta_path,
|
||||
date_yyyymmdd=date_yyyymmdd,
|
||||
orbit_xml_path=orbit_xml_path,
|
||||
)
|
||||
)
|
||||
|
||||
return scenes[0], scenes[1]
|
||||
|
||||
|
||||
def render_bbox(bbox: list[float] | None) -> str:
|
||||
if bbox is None:
|
||||
return ""
|
||||
values = ", ".join(f"{value:.10f}".rstrip("0").rstrip(".") for value in bbox)
|
||||
return f' <property name="geocode bounding box">[{values}]</property>\n'
|
||||
|
||||
|
||||
def meters_to_geoposting_degrees(target_grid_size_m: int) -> float:
|
||||
return float(target_grid_size_m) / METERS_PER_DEGREE
|
||||
|
||||
|
||||
def write_stripmap_xml(xml_path: Path, config: PipelineConfig) -> None:
|
||||
bbox_xml = render_bbox(config.bbox)
|
||||
text = (
|
||||
"<stripmapApp>\n"
|
||||
" <component name=\"stripmapApp\">\n"
|
||||
" <property name=\"sensor name\">LUTAN1</property>\n"
|
||||
" <property name=\"reference sensor name\">LUTAN1</property>\n"
|
||||
" <property name=\"secondary sensor name\">LUTAN1</property>\n"
|
||||
" <property name=\"renderer\">xml</property>\n"
|
||||
" <property name=\"do unwrap\">True</property>\n"
|
||||
" <property name=\"unwrapper name\">snaphu</property>\n"
|
||||
f" <property name=\"posting\">{config.target_grid_size_m}</property>\n"
|
||||
f" <property name=\"geoPosting\">{config.geo_posting_deg:.12f}</property>\n"
|
||||
f"{bbox_xml}"
|
||||
f" <property name=\"demFilename\">{config.dem_path.as_posix()}</property>\n"
|
||||
"\n"
|
||||
" <component name=\"Reference\">\n"
|
||||
f" <property name=\"tiff\">{config.reference.tiff_path.as_posix()}</property>\n"
|
||||
f" <property name=\"orbitFile\">{config.reference.orbit_xml_path.as_posix()}</property>\n"
|
||||
" <property name=\"OUTPUT\">reference</property>\n"
|
||||
" </component>\n"
|
||||
"\n"
|
||||
" <component name=\"Secondary\">\n"
|
||||
f" <property name=\"tiff\">{config.secondary.tiff_path.as_posix()}</property>\n"
|
||||
f" <property name=\"orbitFile\">{config.secondary.orbit_xml_path.as_posix()}</property>\n"
|
||||
" <property name=\"OUTPUT\">secondary</property>\n"
|
||||
" </component>\n"
|
||||
" </component>\n"
|
||||
"</stripmapApp>\n"
|
||||
)
|
||||
xml_path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def run_logged(cmd: list[str], cwd: Path, log_path: Path) -> None:
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
print("Running:")
|
||||
print(" " + " ".join(cmd))
|
||||
print(f"Log: {log_path}")
|
||||
|
||||
with log_path.open("w", encoding="utf-8") as handle:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
assert proc.stdout is not None
|
||||
for line in proc.stdout:
|
||||
sys.stdout.write(line)
|
||||
handle.write(line)
|
||||
|
||||
status = proc.wait()
|
||||
|
||||
if status != 0:
|
||||
raise RuntimeError(f"Command failed with exit code {status}: {' '.join(cmd)}")
|
||||
|
||||
|
||||
def parse_bbox_arg(value: str | None) -> list[float] | None:
|
||||
if value is None:
|
||||
return None
|
||||
parts = [item.strip() for item in value.split(",")]
|
||||
if len(parts) != 4:
|
||||
raise ValueError("Bounding box must be south,north,west,east")
|
||||
return [float(item) for item in parts]
|
||||
|
||||
|
||||
def load_estimated_bbox(topo_xml: Path) -> list[float]:
|
||||
root = ET.fromstring(topo_xml.read_text(encoding="utf-8"))
|
||||
for prop in root.findall("property"):
|
||||
if prop.attrib.get("name") == "estimatedboundingbox":
|
||||
value = prop.findtext("value")
|
||||
if not value:
|
||||
break
|
||||
bbox = ast.literal_eval(value)
|
||||
return [float(item) for item in bbox]
|
||||
raise ValueError(f"estimatedboundingbox not found in {topo_xml}")
|
||||
|
||||
|
||||
def expand_bbox(bbox: list[float], margin: float) -> list[float]:
|
||||
south, north, west, east = bbox
|
||||
return [
|
||||
max(-90.0, south - margin),
|
||||
min(90.0, north + margin),
|
||||
max(-180.0, west - margin),
|
||||
min(180.0, east + margin),
|
||||
]
|
||||
|
||||
|
||||
def prepare_snaphu_resume(work_dir: Path, bbox: list[float] | None) -> None:
|
||||
pickle_dir = work_dir / "PICKLE"
|
||||
src = pickle_dir / "filter"
|
||||
src_xml = pickle_dir / "filter.xml"
|
||||
dst = pickle_dir / "filter_high_band"
|
||||
dst_xml = pickle_dir / "filter_high_band.xml"
|
||||
|
||||
if not src.exists() or not src_xml.exists():
|
||||
raise FileNotFoundError("filter step output is missing; cannot prepare SNAPHU resume state.")
|
||||
|
||||
shutil.copy2(src, dst)
|
||||
shutil.copy2(src_xml, dst_xml)
|
||||
|
||||
root = ET.fromstring(dst_xml.read_text(encoding="utf-8"))
|
||||
props = {prop.attrib.get("name"): prop for prop in root.findall("property")}
|
||||
|
||||
required = {
|
||||
"referenceslccroppedproduct": "reference_slc.xml",
|
||||
"secondaryslccroppedproduct": "secondary_slc.xml",
|
||||
"referenceslcproduct": "reference_slc.xml",
|
||||
"secondaryslcproduct": "secondary_slc.xml",
|
||||
"referencegeometrysystem": "Zero Doppler",
|
||||
"secondarygeometrysystem": "Zero Doppler",
|
||||
}
|
||||
if bbox is not None:
|
||||
required["estimatedboundingbox"] = str(bbox)
|
||||
|
||||
for name, value in required.items():
|
||||
if name in props:
|
||||
node = props[name].find("value")
|
||||
if node is None:
|
||||
node = ET.SubElement(props[name], "value")
|
||||
node.text = value
|
||||
continue
|
||||
|
||||
prop = ET.SubElement(root, "property", {"name": name})
|
||||
ET.SubElement(prop, "value").text = value
|
||||
|
||||
dst_xml.write_text(ET.tostring(root, encoding="unicode"), encoding="utf-8")
|
||||
|
||||
|
||||
def prepare_geocode_resume(work_dir: Path) -> None:
|
||||
pickle_dir = work_dir / "PICKLE"
|
||||
unwrap = pickle_dir / "unwrap"
|
||||
unwrap_xml = pickle_dir / "unwrap.xml"
|
||||
ionosphere = pickle_dir / "ionosphere"
|
||||
ionosphere_xml = pickle_dir / "ionosphere.xml"
|
||||
|
||||
if not unwrap.exists() or not unwrap_xml.exists():
|
||||
raise FileNotFoundError("unwrap step output is missing; cannot prepare geocode resume state.")
|
||||
|
||||
shutil.copy2(unwrap, ionosphere)
|
||||
shutil.copy2(unwrap_xml, ionosphere_xml)
|
||||
|
||||
|
||||
def print_summary(
|
||||
task_dir: Path,
|
||||
work_dir: Path,
|
||||
output_dir: Path,
|
||||
config: PipelineConfig,
|
||||
) -> None:
|
||||
print(f"Task dir: {task_dir}")
|
||||
print(f"Task name: {config.task_name}")
|
||||
print(f"Output prefix:{config.output_prefix}")
|
||||
print(f"Work dir: {work_dir}")
|
||||
print(f"Output dir: {output_dir}")
|
||||
print(f"DEM: {config.dem_path}")
|
||||
print(f"Reference: {config.reference.tiff_path}")
|
||||
print(f"Secondary: {config.secondary.tiff_path}")
|
||||
print(f"Ref orbit: {config.reference.orbit_xml_path}")
|
||||
print(f"Sec orbit: {config.secondary.orbit_xml_path}")
|
||||
print(f"BBox: {config.bbox if config.bbox is not None else 'auto'}")
|
||||
print(f"Target grid: {config.target_grid_size_m} m")
|
||||
print(f"Geo posting: {config.geo_posting_deg:.12f} deg")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
task_dir = normalize_linux_path(args.task_dir).resolve()
|
||||
if not task_dir.exists():
|
||||
raise FileNotFoundError(f"Task directory not found: {task_dir}")
|
||||
|
||||
task_name = args.task_name or task_dir.name
|
||||
output_prefix = args.output_prefix or task_name
|
||||
work_root = normalize_linux_path(args.work_root).resolve()
|
||||
work_dir = normalize_linux_path(args.work_dir).resolve() if args.work_dir else work_root / task_name
|
||||
output_dir = normalize_linux_path(args.output_dir).resolve() if args.output_dir else work_dir
|
||||
orbit_root = normalize_linux_path(args.orbit_root).resolve()
|
||||
orbit_out_dir = (
|
||||
normalize_linux_path(args.orbit_output_dir).resolve()
|
||||
if args.orbit_output_dir
|
||||
else work_dir / "orbits"
|
||||
)
|
||||
|
||||
if work_dir.exists():
|
||||
if not args.force:
|
||||
raise FileExistsError(f"Work directory already exists: {work_dir}. Use --force to recreate it.")
|
||||
shutil.rmtree(work_dir)
|
||||
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
reference, secondary = resolve_task(
|
||||
task_dir=task_dir,
|
||||
orbit_root=orbit_root,
|
||||
orbit_out_dir=orbit_out_dir,
|
||||
margin_sec=args.orbit_margin_sec,
|
||||
master_dir_name=args.master_dir_name,
|
||||
slave_dir_name=args.slave_dir_name,
|
||||
scene_glob=args.scene_glob,
|
||||
prefer_scene_keyword=args.prefer_scene_keyword,
|
||||
)
|
||||
dem_path = resolve_dem(args.dem)
|
||||
bbox = parse_bbox_arg(args.bbox)
|
||||
geo_posting_deg = meters_to_geoposting_degrees(args.target_grid_size_m)
|
||||
|
||||
config = PipelineConfig(
|
||||
task_name=task_name,
|
||||
output_prefix=output_prefix,
|
||||
dem_path=dem_path,
|
||||
reference=reference,
|
||||
secondary=secondary,
|
||||
bbox=bbox,
|
||||
target_grid_size_m=args.target_grid_size_m,
|
||||
geo_posting_deg=geo_posting_deg,
|
||||
)
|
||||
print_summary(task_dir=task_dir, work_dir=work_dir, output_dir=output_dir, config=config)
|
||||
|
||||
xml_path = work_dir / f"{task_name}_stripmap.xml"
|
||||
write_stripmap_xml(xml_path, config)
|
||||
|
||||
if args.dry_run:
|
||||
print(f"Generated XML: {xml_path}")
|
||||
return 0
|
||||
|
||||
app_py = locate_stripmap_app()
|
||||
|
||||
run_logged(
|
||||
[sys.executable, app_py.as_posix(), xml_path.as_posix(), "--steps", "--end=filter"],
|
||||
cwd=work_dir,
|
||||
log_path=work_dir / "01_to_filter.log",
|
||||
)
|
||||
|
||||
if config.bbox is None:
|
||||
estimated_bbox = load_estimated_bbox(work_dir / "PICKLE" / "topo.xml")
|
||||
config.bbox = expand_bbox(estimated_bbox, args.bbox_margin)
|
||||
write_stripmap_xml(xml_path, config)
|
||||
print(f"Auto bbox from topo: {estimated_bbox}")
|
||||
print(f"Expanded bbox used for geocode: {config.bbox}")
|
||||
|
||||
prepare_snaphu_resume(work_dir, config.bbox)
|
||||
run_logged(
|
||||
[sys.executable, app_py.as_posix(), xml_path.as_posix(), "--steps", "--start=unwrap", "--end=unwrap"],
|
||||
cwd=work_dir,
|
||||
log_path=work_dir / "02_unwrap_snaphu.log",
|
||||
)
|
||||
|
||||
prepare_geocode_resume(work_dir)
|
||||
run_logged(
|
||||
[sys.executable, app_py.as_posix(), xml_path.as_posix(), "--steps", "--start=geocode", "--end=geocode"],
|
||||
cwd=work_dir,
|
||||
log_path=work_dir / "03_geocode.log",
|
||||
)
|
||||
|
||||
outputs = export_products(
|
||||
work_dir=work_dir,
|
||||
output_dir=output_dir,
|
||||
prefix=output_prefix,
|
||||
wavelength=args.wavelength,
|
||||
coh_threshold=args.coh_threshold,
|
||||
include_disp_full=args.include_disp_full,
|
||||
)
|
||||
|
||||
print("Pipeline finished.")
|
||||
for key, path in outputs.items():
|
||||
print(f"{key}: {path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user