Files
insar-management-system-v2/experiments/isce2_sbas_timeseries/scripts/scan_lt1_stack_candidates.py
T

374 lines
12 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import re
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
def _repo_root() -> Path:
return Path(__file__).resolve().parents[3]
def _load_utils_module():
utils_path = _repo_root() / "backend" / "app" / "utils.py"
spec = importlib.util.spec_from_file_location("repo_utils", utils_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Unable to load repo utils module: {utils_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
UTILS = _load_utils_module()
LT1_NAME_RE = re.compile(
r"^(?P<satellite>LT1[AB])_"
r"(?P<satellite_mode>[^_]+)_"
r"(?P<receiving_station>[^_]+)_"
r"(?P<imaging_mode>[^_]+)_"
r"(?P<abs_orbit>\d+)_"
r"(?P<lon>E\d+\.\d+)_"
r"(?P<lat>N\d+\.\d+)_"
r"(?P<date>\d{8})_"
r"(?P<product_type>[^_]+)_"
r"(?P<polarization>[^_]+)_"
r"(?P<product_level>[^_]+)_"
r"(?P<product_unique_id>\d+)$"
)
@dataclass
class SceneRecord:
folder_name: str
folder_path: str
folder_path_wsl: str
tiff_path: str
tiff_path_wsl: str
meta_path: str
meta_path_wsl: str
file_size_bytes: int
satellite: str
imaging_date: str
imaging_mode: Optional[str]
polarization: Optional[str]
orbit_direction: Optional[str]
satellite_mode: Optional[str]
receiving_station: Optional[str]
orbit_circle: Optional[str]
scene_center_lon: Optional[float]
scene_center_lat: Optional[float]
acquisition_time_utc: Optional[str]
product_type: Optional[str]
product_level: Optional[str]
product_unique_id: Optional[str]
tile_key: str
group_key: str
orbit_txt_expected_name: str
def windows_to_wsl(path: str | Path) -> str:
text = str(path)
match = re.match(r"^([A-Za-z]):[\\/](.*)$", os.path.normpath(text))
if not match:
return text.replace("\\", "/")
drive = match.group(1).lower()
normalized_tail = match.group(2).replace("\\", "/").lstrip("/")
return f"/mnt/{drive}/{normalized_tail}"
def choose_tiff(folder: Path) -> Optional[Path]:
candidates = sorted(folder.glob("*.tiff"))
if not candidates:
return None
slc_candidates = [path for path in candidates if "_SLC_" in path.name]
if len(slc_candidates) == 1:
return slc_candidates[0]
if len(candidates) == 1:
return candidates[0]
return candidates[0]
def merge_metadata(name_meta: Dict[str, Any], xml_meta: Dict[str, Any]) -> Dict[str, Any]:
merged = dict(name_meta or {})
prefer_name_keys = {"product_unique_id"}
for key, value in (xml_meta or {}).items():
if value in (None, ""):
continue
if key in prefer_name_keys and merged.get(key):
continue
merged[key] = value
return merged
def parse_scene(folder: Path) -> Optional[SceneRecord]:
match = LT1_NAME_RE.match(folder.name)
if not match:
return None
name_meta = UTILS.get_parser(folder.name, UTILS.RADAR_PARSERS)
if not name_meta:
return None
xml_file_path = UTILS.find_xml_file(str(folder))
if not xml_file_path:
return None
coverage_polygon, xml_meta = UTILS.parse_xml_metadata(xml_file_path)
if not coverage_polygon:
return None
tiff_path = choose_tiff(folder)
if tiff_path is None:
return None
merged = merge_metadata(name_meta, xml_meta or {})
tile_key = f"{match.group('lon')}_{match.group('lat')}"
orbit_direction = str(merged.get("orbit_direction") or "").upper() or None
group_key = "|".join(
[
str(merged.get("satellite") or ""),
str(merged.get("imaging_mode") or ""),
str(merged.get("polarization") or ""),
str(orbit_direction or ""),
tile_key,
]
)
satellite = str(merged.get("satellite") or "")
imaging_date = str(merged.get("imaging_date") or "")
return SceneRecord(
folder_name=folder.name,
folder_path=str(folder),
folder_path_wsl=windows_to_wsl(folder),
tiff_path=str(tiff_path),
tiff_path_wsl=windows_to_wsl(tiff_path),
meta_path=str(xml_file_path),
meta_path_wsl=windows_to_wsl(xml_file_path),
file_size_bytes=tiff_path.stat().st_size,
satellite=satellite,
imaging_date=imaging_date,
imaging_mode=merged.get("imaging_mode"),
polarization=merged.get("polarization"),
orbit_direction=orbit_direction,
satellite_mode=merged.get("satellite_mode"),
receiving_station=merged.get("receiving_station"),
orbit_circle=merged.get("orbit_circle"),
scene_center_lon=merged.get("scene_center_lon"),
scene_center_lat=merged.get("scene_center_lat"),
acquisition_time_utc=merged.get("acquisition_time_utc"),
product_type=merged.get("product_type"),
product_level=merged.get("product_level"),
product_unique_id=merged.get("product_unique_id"),
tile_key=tile_key,
group_key=group_key,
orbit_txt_expected_name=f"{satellite}_GpsData_GAS_C_{imaging_date}.txt",
)
def scan_scenes(root_dir: Path) -> List[SceneRecord]:
scenes: List[SceneRecord] = []
for entry in sorted(root_dir.iterdir()):
if not entry.is_dir():
continue
scene = parse_scene(entry)
if scene:
scenes.append(scene)
return scenes
def build_group_summary(scenes: List[SceneRecord]) -> List[Dict[str, Any]]:
groups: Dict[str, List[SceneRecord]] = {}
for scene in scenes:
groups.setdefault(scene.group_key, []).append(scene)
summary: List[Dict[str, Any]] = []
for key, items in groups.items():
items.sort(key=lambda item: item.imaging_date)
first = items[0]
summary.append(
{
"group_key": key,
"count": len(items),
"satellite": first.satellite,
"imaging_mode": first.imaging_mode,
"polarization": first.polarization,
"orbit_direction": first.orbit_direction,
"tile_key": first.tile_key,
"dates": [item.imaging_date for item in items],
"receiving_stations": sorted({item.receiving_station for item in items if item.receiving_station}),
}
)
summary.sort(key=lambda item: (-item["count"], item["group_key"]))
return summary
def select_group(
summary: List[Dict[str, Any]],
tile_key: Optional[str],
group_key: Optional[str],
min_scenes: int,
) -> Optional[str]:
if group_key:
return group_key
if tile_key:
for item in summary:
if item["tile_key"] == tile_key and item["count"] >= min_scenes:
return item["group_key"]
return None
for item in summary:
if item["count"] >= min_scenes:
return item["group_key"]
return None
def build_manifest(root_dir: Path, group_key: str, scenes: List[SceneRecord]) -> Dict[str, Any]:
group_scenes = [scene for scene in scenes if scene.group_key == group_key]
if not group_scenes:
raise ValueError(f"Group not found: {group_key}")
group_scenes.sort(key=lambda item: item.imaging_date)
first = group_scenes[0]
reference_index = len(group_scenes) // 2
reference_scene = group_scenes[reference_index]
slug = (
f"{first.satellite.lower()}_"
f"{(first.imaging_mode or 'unknown').lower()}_"
f"{(first.polarization or 'unknown').lower()}_"
f"{(first.orbit_direction or 'unknown').lower()}_"
f"{first.tile_key.lower().replace('.', 'p')}"
)
scratch_root = _repo_root() / "experiments" / "isce2_sbas_timeseries" / "scratch" / slug
scratch_root_wsl = windows_to_wsl(scratch_root)
return {
"source_root_windows": str(root_dir),
"source_root_wsl": windows_to_wsl(root_dir),
"group_key": group_key,
"tile_key": first.tile_key,
"scene_count": len(group_scenes),
"reference_strategy": "middle_by_date",
"reference_date": reference_scene.imaging_date,
"stack_group": {
"satellite": first.satellite,
"imaging_mode": first.imaging_mode,
"polarization": first.polarization,
"orbit_direction": first.orbit_direction,
"receiving_stations": sorted({item.receiving_station for item in group_scenes if item.receiving_station}),
},
"proposed_scratch_windows": str(scratch_root),
"proposed_scratch_wsl": scratch_root_wsl,
"proposed_layout": {
"stack_input_manifest": f"{scratch_root_wsl}/stack_input_manifest.json",
"slc_dir": f"{scratch_root_wsl}/SLC",
"orbits_dir": f"{scratch_root_wsl}/orbits",
"logs_dir": f"{scratch_root_wsl}/logs",
},
"stack_prep_assessment": {
"current_scene_layout": "per_scene_folder_with_tiff_meta_rpc",
"official_stripmapStack_expected_layout": "SLC/YYYYMMDD/YYYYMMDD.raw or YYYYMMDD.slc",
"direct_compatibility": "unproven",
"lt1_adapter_required_likely": True,
"notes": [
"Current repo can read these scene folders as RadarData assets.",
"Official stripmapStack helper scripts do not advertise LT-1/LUTAN1 preparation hooks.",
"A custom LT-1 stack preparation layer is likely needed before official stack execution.",
],
},
"scenes": [asdict(scene) for scene in group_scenes],
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Scan LT-1 scene folders and build a dry-run SBAS stack-prep manifest."
)
parser.add_argument(
"--root-dir",
default=r"F:\Insar_data_pool_1",
help="Windows root directory containing LT-1 scene folders.",
)
parser.add_argument(
"--min-scenes",
type=int,
default=4,
help="Minimum scenes required for candidate groups.",
)
parser.add_argument(
"--top-n",
type=int,
default=20,
help="How many candidate groups to print.",
)
parser.add_argument(
"--tile-key",
default=None,
help="Pick one candidate by tile key, for example E123.3_N46.1.",
)
parser.add_argument(
"--group-key",
default=None,
help="Pick one candidate by full group key.",
)
parser.add_argument(
"--manifest-path",
default=None,
help="Optional JSON output path for the selected group's dry-run manifest.",
)
return parser
def main() -> int:
args = build_parser().parse_args()
root_dir = Path(args.root_dir)
if not root_dir.exists():
raise FileNotFoundError(f"Root directory does not exist: {root_dir}")
scenes = scan_scenes(root_dir)
summary = build_group_summary(scenes)
print(f"scanned_scenes={len(scenes)}")
print(f"candidate_groups={len(summary)}")
print("top_candidates:")
for item in summary[: args.top_n]:
print(
json.dumps(
{
"count": item["count"],
"tile_key": item["tile_key"],
"group_key": item["group_key"],
"dates": item["dates"],
"receiving_stations": item["receiving_stations"],
},
ensure_ascii=False,
)
)
selected_group = select_group(summary, args.tile_key, args.group_key, args.min_scenes)
if not selected_group:
print("selected_group=None")
return 0
manifest = build_manifest(root_dir, selected_group, scenes)
print(f"selected_group={selected_group}")
print(f"reference_date={manifest['reference_date']}")
if args.manifest_path:
manifest_path = Path(args.manifest_path)
manifest_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"manifest_written={manifest_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())