605 lines
23 KiB
Python
605 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
DEFAULT_MANIFEST_PATH = (
|
|
REPO_ROOT
|
|
/ "experiments"
|
|
/ "isce2_sbas_timeseries"
|
|
/ "configs"
|
|
/ "sample_stack_e123p3_n46p1.json"
|
|
)
|
|
DEFAULT_STACK_SCRIPT_WSL = (
|
|
"/home/administrator/miniconda3/envs/isce2/share/isce2/stripmapStack/stackStripMap.py"
|
|
)
|
|
DEFAULT_CONDA_WSL = "/home/administrator/miniconda3/bin/conda"
|
|
DEFAULT_ISCE2_SHARE_WSL = "/home/administrator/miniconda3/envs/isce2/share/isce2"
|
|
DEFAULT_STRIPMAP_STACK_DIR_WSL = f"{DEFAULT_ISCE2_SHARE_WSL}/stripmapStack"
|
|
SUPPORTED_STACK_WORKFLOWS = ("slc", "interferogram", "ionosphere")
|
|
DEFAULT_STACK_TEXT_CMD = (
|
|
f"export PATH={DEFAULT_STRIPMAP_STACK_DIR_WSL}:$PATH; "
|
|
f"export PYTHONPATH={DEFAULT_STRIPMAP_STACK_DIR_WSL}:{DEFAULT_ISCE2_SHARE_WSL}${{PYTHONPATH:+:$PYTHONPATH}}; "
|
|
)
|
|
|
|
|
|
def windows_to_wsl(path: str | Path) -> str:
|
|
text = str(path)
|
|
match = re.match(r"^([A-Za-z]):[\\/](.*)$", text)
|
|
if not match:
|
|
return text.replace("\\", "/")
|
|
drive = match.group(1).lower()
|
|
tail = match.group(2).replace("\\", "/").lstrip("/")
|
|
return f"/mnt/{drive}/{tail}"
|
|
|
|
|
|
def load_isce2_input_helper_module():
|
|
helper_path = REPO_ROOT / "backend" / "app" / "isce2_pipeline" / "lt1_input_resolver.py"
|
|
spec = importlib.util.spec_from_file_location("lt1_orbit_helper", helper_path)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError(f"Unable to load orbit helper module: {helper_path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
ISCE2_INPUT_HELPER = load_isce2_input_helper_module()
|
|
|
|
|
|
def require_file(path: Path, label: str) -> None:
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"Missing {label}: {path}")
|
|
|
|
|
|
def shelve_stem_exists(stem: Path) -> bool:
|
|
for suffix in ("", ".db", ".dat", ".dir", ".bak"):
|
|
if Path(str(stem) + suffix).exists():
|
|
return True
|
|
return False
|
|
|
|
|
|
@dataclass
|
|
class ScenePlan:
|
|
date: str
|
|
target_dir_windows: str
|
|
target_dir_wsl: str
|
|
source_scene_json_windows: str
|
|
source_scene_json_wsl: str
|
|
source_tiff_windows: str
|
|
source_tiff_wsl: str
|
|
source_meta_windows: str
|
|
source_meta_wsl: str
|
|
orbit_xml_windows: Optional[str]
|
|
orbit_xml_wsl: Optional[str]
|
|
orbit_xml_exists: bool
|
|
orbit_resolution_mode: Optional[str]
|
|
orbit_resolution_error: Optional[str]
|
|
source_exists: bool
|
|
scene_start_utc: str
|
|
scene_stop_utc: str
|
|
orbit_window_start_utc: str
|
|
orbit_window_stop_utc: str
|
|
expected_slc_windows: str
|
|
expected_slc_wsl: str
|
|
expected_slc_xml_windows: str
|
|
expected_slc_xml_wsl: str
|
|
expected_data_shelve_windows: str
|
|
expected_data_shelve_wsl: str
|
|
materialized_slc_exists: bool
|
|
materialized_data_exists: bool
|
|
stack_ready: bool
|
|
status: str
|
|
|
|
|
|
def build_scene_plan(
|
|
scene: Dict[str, Any],
|
|
slc_root: Path,
|
|
orbit_pool: Optional[Path],
|
|
orbit_stage_dir: Path,
|
|
margin_sec: float,
|
|
) -> ScenePlan:
|
|
date = str(scene["imaging_date"])
|
|
satellite = str(scene["satellite"])
|
|
source_tiff = Path(scene["tiff_path"])
|
|
source_meta = Path(scene["meta_path"])
|
|
require_file(source_tiff, f"scene TIFF for {date}")
|
|
require_file(source_meta, f"scene meta XML for {date}")
|
|
|
|
scene_start_dt, scene_stop_dt = ISCE2_INPUT_HELPER.parse_scene_window(source_meta, margin_sec=0.0)
|
|
orbit_window_start_dt, orbit_window_stop_dt = ISCE2_INPUT_HELPER.parse_scene_window(
|
|
source_meta,
|
|
margin_sec=margin_sec,
|
|
)
|
|
scene_start_utc = scene_start_dt.isoformat()
|
|
scene_stop_utc = scene_stop_dt.isoformat()
|
|
orbit_window_start_utc = orbit_window_start_dt.isoformat()
|
|
orbit_window_stop_utc = orbit_window_stop_dt.isoformat()
|
|
|
|
target_dir = slc_root / date
|
|
expected_slc = target_dir / f"{date}.slc"
|
|
expected_slc_xml = target_dir / f"{date}.slc.xml"
|
|
expected_data = target_dir / "data"
|
|
source_scene_json = target_dir / "source_scene.json"
|
|
|
|
orbit_xml: Optional[Path] = None
|
|
orbit_resolution_mode: Optional[str] = None
|
|
orbit_resolution_error: Optional[str] = None
|
|
if orbit_pool is not None:
|
|
try:
|
|
orbit_resolution = ISCE2_INPUT_HELPER.ensure_lt1_orbit_xml(
|
|
date_yyyymmdd=date,
|
|
satellite=satellite,
|
|
annotation_xml=source_meta,
|
|
orbit_root=orbit_pool,
|
|
orbit_output_dir=orbit_stage_dir,
|
|
margin_sec=margin_sec,
|
|
)
|
|
orbit_xml = orbit_resolution.path
|
|
orbit_resolution_mode = orbit_resolution.source
|
|
except Exception as exc:
|
|
orbit_resolution_error = str(exc)
|
|
|
|
materialized_slc_exists = expected_slc.exists() and expected_slc_xml.exists()
|
|
materialized_data_exists = shelve_stem_exists(expected_data)
|
|
stack_ready = bool(orbit_xml and materialized_slc_exists and materialized_data_exists)
|
|
|
|
if not orbit_xml:
|
|
status = "missing_orbit_xml"
|
|
elif not materialized_slc_exists and not materialized_data_exists:
|
|
status = "waiting_for_scene_materializer"
|
|
elif not materialized_slc_exists:
|
|
status = "missing_slc"
|
|
elif not materialized_data_exists:
|
|
status = "missing_data_shelve"
|
|
else:
|
|
status = "ready"
|
|
|
|
return ScenePlan(
|
|
date=date,
|
|
target_dir_windows=str(target_dir),
|
|
target_dir_wsl=windows_to_wsl(target_dir),
|
|
source_scene_json_windows=str(source_scene_json),
|
|
source_scene_json_wsl=windows_to_wsl(source_scene_json),
|
|
source_tiff_windows=str(source_tiff),
|
|
source_tiff_wsl=windows_to_wsl(source_tiff),
|
|
source_meta_windows=str(source_meta),
|
|
source_meta_wsl=windows_to_wsl(source_meta),
|
|
orbit_xml_windows=str(orbit_xml) if orbit_xml else None,
|
|
orbit_xml_wsl=windows_to_wsl(orbit_xml) if orbit_xml else None,
|
|
orbit_xml_exists=bool(orbit_xml),
|
|
orbit_resolution_mode=orbit_resolution_mode,
|
|
orbit_resolution_error=orbit_resolution_error,
|
|
source_exists=True,
|
|
scene_start_utc=scene_start_utc,
|
|
scene_stop_utc=scene_stop_utc,
|
|
orbit_window_start_utc=orbit_window_start_utc,
|
|
orbit_window_stop_utc=orbit_window_stop_utc,
|
|
expected_slc_windows=str(expected_slc),
|
|
expected_slc_wsl=windows_to_wsl(expected_slc),
|
|
expected_slc_xml_windows=str(expected_slc_xml),
|
|
expected_slc_xml_wsl=windows_to_wsl(expected_slc_xml),
|
|
expected_data_shelve_windows=str(expected_data),
|
|
expected_data_shelve_wsl=windows_to_wsl(expected_data),
|
|
materialized_slc_exists=materialized_slc_exists,
|
|
materialized_data_exists=materialized_data_exists,
|
|
stack_ready=stack_ready,
|
|
status=status,
|
|
)
|
|
|
|
|
|
def render_stack_command(
|
|
slc_dir_wsl: str,
|
|
dem_wsl: str,
|
|
work_dir_wsl: str,
|
|
reference_date: str,
|
|
workflow: str,
|
|
) -> List[str]:
|
|
return [
|
|
DEFAULT_CONDA_WSL,
|
|
"run",
|
|
"-n",
|
|
"isce2",
|
|
"python",
|
|
DEFAULT_STACK_SCRIPT_WSL,
|
|
"-s",
|
|
slc_dir_wsl,
|
|
"-d",
|
|
dem_wsl,
|
|
"-w",
|
|
work_dir_wsl,
|
|
"-m",
|
|
reference_date,
|
|
"--nofocus",
|
|
"-W",
|
|
workflow,
|
|
"-u",
|
|
"snaphu",
|
|
"-c",
|
|
DEFAULT_STACK_TEXT_CMD,
|
|
]
|
|
|
|
|
|
def shell_quote(value: str) -> str:
|
|
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
|
|
|
|
def render_shell_command(argv: List[str]) -> str:
|
|
return " ".join(shell_quote(item) for item in argv)
|
|
|
|
|
|
def build_blockers(scene_plans: List[ScenePlan], orbit_pool: Optional[Path], dem_path: Optional[Path]) -> List[str]:
|
|
blockers: List[str] = []
|
|
if orbit_pool is None:
|
|
blockers.append("ORBIT_POOL_ISCE2 was not resolved.")
|
|
if dem_path is None:
|
|
blockers.append("Prepared DEM with .xml sidecar was not resolved.")
|
|
|
|
missing_orbit = [item.date for item in scene_plans if not item.orbit_xml_exists]
|
|
if missing_orbit:
|
|
blockers.append("Missing orbit XML for dates: " + ", ".join(missing_orbit))
|
|
orbit_errors = [f"{item.date}: {item.orbit_resolution_error}" for item in scene_plans if item.orbit_resolution_error]
|
|
if orbit_errors:
|
|
blockers.append("Orbit resolution errors: " + "; ".join(orbit_errors))
|
|
|
|
missing_slc = [item.date for item in scene_plans if not item.materialized_slc_exists]
|
|
if missing_slc:
|
|
blockers.append("Materialized .slc/.slc.xml are missing for dates: " + ", ".join(missing_slc))
|
|
|
|
missing_data = [item.date for item in scene_plans if not item.materialized_data_exists]
|
|
if missing_data:
|
|
blockers.append("ISCE data shelve is missing for dates: " + ", ".join(missing_data))
|
|
|
|
return blockers
|
|
|
|
|
|
def render_contract_markdown(report: Dict[str, Any]) -> str:
|
|
lines: List[str] = []
|
|
ready = bool(report["readiness"]["ready_for_stackStripMap_nofocus"])
|
|
lines.append("# LT-1 Stack Prep Contract")
|
|
lines.append("")
|
|
lines.append(f"Generated: {report['generated_at_utc']}")
|
|
lines.append("")
|
|
lines.append("## Selected Stack")
|
|
lines.append("")
|
|
lines.append(f"- Group key: `{report['group_key']}`")
|
|
lines.append(f"- Reference date: `{report['reference_date']}`")
|
|
lines.append(f"- Workflow: `{report['processing_workflow']}`")
|
|
lines.append(f"- Scene count: `{report['scene_count']}`")
|
|
lines.append("")
|
|
lines.append("## Resolved Runtime Inputs")
|
|
lines.append("")
|
|
lines.append(f"- Orbit pool (Windows): `{report['resolved_dependencies']['orbit_pool_windows'] or 'UNRESOLVED'}`")
|
|
lines.append(f"- Orbit pool (WSL): `{report['resolved_dependencies']['orbit_pool_wsl'] or 'UNRESOLVED'}`")
|
|
lines.append(f"- DEM (Windows): `{report['resolved_dependencies']['dem_path_windows'] or 'UNRESOLVED'}`")
|
|
lines.append(f"- DEM (WSL): `{report['resolved_dependencies']['dem_path_wsl'] or 'UNRESOLVED'}`")
|
|
lines.append("")
|
|
lines.append("## Confirmed stripmapStack Contract")
|
|
lines.append("")
|
|
lines.append("- `stackStripMap.py --nofocus` discovers dates from `SLC/YYYYMMDD/YYYYMMDD.slc`.")
|
|
lines.append("- `topo.py` opens `SLC/YYYYMMDD/data` for the reference acquisition.")
|
|
lines.append("- `geo2rdr.py` opens `SLC/YYYYMMDD/data` for each secondary acquisition.")
|
|
lines.append("- Therefore each acquisition directory must contain at least:")
|
|
lines.append(" - `YYYYMMDD.slc`")
|
|
lines.append(" - `YYYYMMDD.slc.xml`")
|
|
lines.append(" - `data` shelve with `frame` and optional `doppler`")
|
|
lines.append("")
|
|
lines.append("## Scene Status")
|
|
lines.append("")
|
|
lines.append("| Date | Orbit XML | SLC | Data | Status |")
|
|
lines.append("| --- | --- | --- | --- | --- |")
|
|
for scene in report["scenes"]:
|
|
orbit_ok = "yes" if scene["orbit_xml_exists"] else "no"
|
|
slc_ok = "yes" if scene["materialized_slc_exists"] else "no"
|
|
data_ok = "yes" if scene["materialized_data_exists"] else "no"
|
|
lines.append(f"| {scene['date']} | {orbit_ok} | {slc_ok} | {data_ok} | `{scene['status']}` |")
|
|
lines.append("")
|
|
lines.append("## Draft stackStripMap Command")
|
|
lines.append("")
|
|
lines.append("```bash")
|
|
lines.append(report["stack_command"]["shell"])
|
|
lines.append("```")
|
|
lines.append("")
|
|
lines.append("## Current Blockers")
|
|
lines.append("")
|
|
blockers = report["readiness"]["blocking_reasons"]
|
|
if blockers:
|
|
for blocker in blockers:
|
|
lines.append(f"- {blocker}")
|
|
else:
|
|
lines.append("- none")
|
|
lines.append("")
|
|
lines.append("## Next Tasks")
|
|
lines.append("")
|
|
if ready:
|
|
lines.append("- Execute `run_01_reference` and confirm geometry generation succeeds.")
|
|
lines.append("- Execute `run_02` to `run_07` step by step and record any LT-1-specific failures.")
|
|
lines.append("- Inspect `baselines/`, `configs/`, and the first coarse coregistration outputs.")
|
|
lines.append("- Install MintPy only after the stack run outputs are stable.")
|
|
else:
|
|
lines.append("- Use the LT-1 scene materializer to finish the remaining acquisitions under the generated `SLC/` root.")
|
|
lines.append("- Re-run the generated preflight script, then execute `stackStripMap.py --nofocus`.")
|
|
lines.append("- Install MintPy only after the stack materializer contract is working end to end.")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_run_script(report: Dict[str, Any]) -> str:
|
|
slc_dir = report["workspace"]["slc_dir_wsl"]
|
|
work_dir = report["workspace"]["stack_work_dir_wsl"]
|
|
dem_path = report["resolved_dependencies"]["dem_path_wsl"] or "__MISSING_DEM__"
|
|
reference_date = report["reference_date"]
|
|
dates = " ".join(scene["date"] for scene in report["scenes"])
|
|
command = report["stack_command"]["shell"]
|
|
|
|
return f"""#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SLC_DIR={shell_quote(slc_dir)}
|
|
WORK_DIR={shell_quote(work_dir)}
|
|
DEM={shell_quote(dem_path)}
|
|
REFERENCE_DATE={shell_quote(reference_date)}
|
|
ISCE2_SHARE={shell_quote(DEFAULT_ISCE2_SHARE_WSL)}
|
|
STRIPMAP_STACK_DIR={shell_quote(DEFAULT_STRIPMAP_STACK_DIR_WSL)}
|
|
DATES=({dates})
|
|
|
|
export PYTHONPATH="$STRIPMAP_STACK_DIR:$ISCE2_SHARE${{PYTHONPATH:+:$PYTHONPATH}}"
|
|
export PATH="$STRIPMAP_STACK_DIR:$PATH"
|
|
|
|
echo "LT-1 stripmap stack dry-run preflight"
|
|
echo "SLC root: $SLC_DIR"
|
|
echo "Work dir: $WORK_DIR"
|
|
echo "DEM: $DEM"
|
|
echo "Reference date: $REFERENCE_DATE"
|
|
echo "PYTHONPATH: $PYTHONPATH"
|
|
echo "PATH prefix: $STRIPMAP_STACK_DIR"
|
|
|
|
missing=0
|
|
for d in "${{DATES[@]}}"; do
|
|
if [[ ! -f "$SLC_DIR/$d/$d.slc" ]]; then
|
|
echo "MISSING: $SLC_DIR/$d/$d.slc"
|
|
missing=1
|
|
fi
|
|
if [[ ! -f "$SLC_DIR/$d/$d.slc.xml" ]]; then
|
|
echo "MISSING: $SLC_DIR/$d/$d.slc.xml"
|
|
missing=1
|
|
fi
|
|
if [[ ! -e "$SLC_DIR/$d/data" && ! -e "$SLC_DIR/$d/data.db" && ! -e "$SLC_DIR/$d/data.dat" && ! -e "$SLC_DIR/$d/data.dir" && ! -e "$SLC_DIR/$d/data.bak" ]]; then
|
|
echo "MISSING: $SLC_DIR/$d/data"
|
|
missing=1
|
|
fi
|
|
done
|
|
|
|
if [[ "$missing" -ne 0 ]]; then
|
|
echo "Dry-run only. LT-1 scene materialization is still missing."
|
|
exit 2
|
|
fi
|
|
|
|
echo "Preflight passed. Running stackStripMap."
|
|
{command}
|
|
"""
|
|
|
|
|
|
def write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Build a dry-run LT-1 SBAS stack-prep workspace for ISCE2 stripmapStack."
|
|
)
|
|
parser.add_argument(
|
|
"--manifest-path",
|
|
default=str(DEFAULT_MANIFEST_PATH),
|
|
help="Path to the selected stack manifest JSON.",
|
|
)
|
|
parser.add_argument(
|
|
"--scratch-root",
|
|
default=None,
|
|
help="Override the stack scratch root directory. Defaults to proposed_scratch_windows in the manifest.",
|
|
)
|
|
parser.add_argument(
|
|
"--orbit-pool",
|
|
default=None,
|
|
help="Override ORBIT_POOL_ISCE2 (Windows path containing LT1A_GpsData_GAS_C_YYYYMMDD.xml).",
|
|
)
|
|
parser.add_argument(
|
|
"--dem-path",
|
|
default=None,
|
|
help="Override the prepared DEM base path (must have a .xml sidecar).",
|
|
)
|
|
parser.add_argument(
|
|
"--orbit-margin-sec",
|
|
type=float,
|
|
default=60.0,
|
|
help="Margin used when reporting the recommended orbit clip window.",
|
|
)
|
|
parser.add_argument(
|
|
"--workflow",
|
|
default="slc",
|
|
choices=SUPPORTED_STACK_WORKFLOWS,
|
|
help="stripmapStack workflow to generate: slc, interferogram, or ionosphere.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
env_values = ISCE2_INPUT_HELPER.load_env_file(REPO_ROOT / ".env")
|
|
|
|
manifest_path = Path(args.manifest_path).resolve()
|
|
require_file(manifest_path, "stack manifest")
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
|
|
scratch_root = Path(args.scratch_root or manifest["proposed_scratch_windows"]).resolve()
|
|
slc_root = scratch_root / "SLC"
|
|
orbits_dir = scratch_root / "orbits"
|
|
logs_dir = scratch_root / "logs"
|
|
notes_dir = scratch_root / "notes"
|
|
inputs_dir = scratch_root / "inputs"
|
|
stack_work_dir = scratch_root / "stack_work"
|
|
|
|
for path in (scratch_root, slc_root, orbits_dir, logs_dir, notes_dir, inputs_dir, stack_work_dir):
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
orbit_pool = ISCE2_INPUT_HELPER.resolve_orbit_pool_path(
|
|
explicit_path=args.orbit_pool,
|
|
env_values=env_values,
|
|
default_candidates=ISCE2_INPUT_HELPER.DEFAULT_WINDOWS_ORBIT_POOL_CANDIDATES,
|
|
)
|
|
local_dem_candidate = inputs_dir / "dem" / "stack_dem_window.wgs84"
|
|
dem_path = ISCE2_INPUT_HELPER.resolve_prepared_dem_path(
|
|
explicit_path=args.dem_path,
|
|
env_values=env_values,
|
|
extra_candidates=[local_dem_candidate],
|
|
default_candidates=ISCE2_INPUT_HELPER.DEFAULT_WINDOWS_DEM_CANDIDATES,
|
|
)
|
|
|
|
scene_plans = [
|
|
build_scene_plan(
|
|
scene,
|
|
slc_root=slc_root,
|
|
orbit_pool=orbit_pool,
|
|
orbit_stage_dir=orbits_dir,
|
|
margin_sec=args.orbit_margin_sec,
|
|
)
|
|
for scene in manifest["scenes"]
|
|
]
|
|
scene_plans.sort(key=lambda item: item.date)
|
|
|
|
for plan, source_scene in zip(scene_plans, sorted(manifest["scenes"], key=lambda item: item["imaging_date"])):
|
|
target_dir = Path(plan.target_dir_windows)
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
scene_payload = dict(source_scene)
|
|
scene_payload["stack_prep"] = {
|
|
"date": plan.date,
|
|
"target_dir_windows": plan.target_dir_windows,
|
|
"target_dir_wsl": plan.target_dir_wsl,
|
|
"orbit_xml_windows": plan.orbit_xml_windows,
|
|
"orbit_xml_wsl": plan.orbit_xml_wsl,
|
|
"orbit_resolution_mode": plan.orbit_resolution_mode,
|
|
"orbit_resolution_error": plan.orbit_resolution_error,
|
|
"scene_start_utc": plan.scene_start_utc,
|
|
"scene_stop_utc": plan.scene_stop_utc,
|
|
"orbit_window_start_utc": plan.orbit_window_start_utc,
|
|
"orbit_window_stop_utc": plan.orbit_window_stop_utc,
|
|
"expected_slc_windows": plan.expected_slc_windows,
|
|
"expected_slc_xml_windows": plan.expected_slc_xml_windows,
|
|
"expected_data_shelve_windows": plan.expected_data_shelve_windows,
|
|
"status": plan.status,
|
|
}
|
|
write_json(target_dir / "source_scene.json", scene_payload)
|
|
|
|
stack_command_argv = render_stack_command(
|
|
slc_dir_wsl=windows_to_wsl(slc_root),
|
|
dem_wsl=windows_to_wsl(dem_path) if dem_path else "__MISSING_DEM__",
|
|
work_dir_wsl=windows_to_wsl(stack_work_dir),
|
|
reference_date=manifest["reference_date"],
|
|
workflow=args.workflow,
|
|
)
|
|
|
|
blockers = build_blockers(scene_plans, orbit_pool=orbit_pool, dem_path=dem_path)
|
|
readiness = {
|
|
"all_orbits_resolved": all(item.orbit_xml_exists for item in scene_plans),
|
|
"all_materialized_slc_present": all(item.materialized_slc_exists for item in scene_plans),
|
|
"all_data_shelves_present": all(item.materialized_data_exists for item in scene_plans),
|
|
"ready_for_stackStripMap_nofocus": not blockers,
|
|
"blocking_reasons": blockers,
|
|
}
|
|
|
|
report: Dict[str, Any] = {
|
|
"manifest_version": 1,
|
|
"generated_at_utc": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
|
"source_manifest_windows": str(manifest_path),
|
|
"source_manifest_wsl": windows_to_wsl(manifest_path),
|
|
"group_key": manifest["group_key"],
|
|
"tile_key": manifest["tile_key"],
|
|
"scene_count": manifest["scene_count"],
|
|
"reference_date": manifest["reference_date"],
|
|
"reference_strategy": manifest["reference_strategy"],
|
|
"processing_workflow": args.workflow,
|
|
"sensor_name": "LUTAN1",
|
|
"stack_driver": "isce2.stripmapStack.stackStripMap",
|
|
"workspace": {
|
|
"root_windows": str(scratch_root),
|
|
"root_wsl": windows_to_wsl(scratch_root),
|
|
"slc_dir_windows": str(slc_root),
|
|
"slc_dir_wsl": windows_to_wsl(slc_root),
|
|
"orbits_dir_windows": str(orbits_dir),
|
|
"orbits_dir_wsl": windows_to_wsl(orbits_dir),
|
|
"logs_dir_windows": str(logs_dir),
|
|
"logs_dir_wsl": windows_to_wsl(logs_dir),
|
|
"notes_dir_windows": str(notes_dir),
|
|
"notes_dir_wsl": windows_to_wsl(notes_dir),
|
|
"inputs_dir_windows": str(inputs_dir),
|
|
"inputs_dir_wsl": windows_to_wsl(inputs_dir),
|
|
"stack_work_dir_windows": str(stack_work_dir),
|
|
"stack_work_dir_wsl": windows_to_wsl(stack_work_dir),
|
|
},
|
|
"resolved_dependencies": {
|
|
"orbit_pool_windows": str(orbit_pool) if orbit_pool else None,
|
|
"orbit_pool_wsl": windows_to_wsl(orbit_pool) if orbit_pool else None,
|
|
"dem_path_windows": str(dem_path) if dem_path else None,
|
|
"dem_path_wsl": windows_to_wsl(dem_path) if dem_path else None,
|
|
},
|
|
"stack_contract": {
|
|
"mode": "nofocus",
|
|
"workflow": args.workflow,
|
|
"required_per_acquisition_files": [
|
|
"YYYYMMDD.slc",
|
|
"YYYYMMDD.slc.xml",
|
|
"data shelve",
|
|
],
|
|
"current_source_layout": "per_scene_folder_with_tiff_meta_rpc",
|
|
"adapter_needed": True,
|
|
"adapter_goal": "materialize a stripmapStack-ready date directory from LT-1 TIFF/meta/orbit inputs",
|
|
},
|
|
"stack_command": {
|
|
"argv": stack_command_argv,
|
|
"shell": render_shell_command(stack_command_argv),
|
|
},
|
|
"readiness": readiness,
|
|
"scenes": [plan.__dict__ for plan in scene_plans],
|
|
"next_tasks": [
|
|
"Use the LT-1 scene materializer to build YYYYMMDD.slc and data shelve for the remaining acquisitions.",
|
|
"Keep raw scene data external and store only lightweight source manifests plus generated ISCE products under scratch/SLC/YYYYMMDD.",
|
|
f"Run stripmapStack in --nofocus mode with workflow={args.workflow} once every date directory is materialized.",
|
|
"Install MintPy only after stackStripMap produces stable interferogram outputs.",
|
|
],
|
|
}
|
|
|
|
report_path = scratch_root / "stack_input_manifest.json"
|
|
contract_path = scratch_root / "stack_prep_contract.md"
|
|
run_script_path = scratch_root / "run_stripmap_stack_dryrun.sh"
|
|
|
|
write_json(report_path, report)
|
|
contract_path.write_text(render_contract_markdown(report), encoding="utf-8")
|
|
run_script_path.write_text(render_run_script(report), encoding="utf-8", newline="\n")
|
|
|
|
print(f"Manifest: {report_path}")
|
|
print(f"Contract: {contract_path}")
|
|
print(f"Run script: {run_script_path}")
|
|
print(f"Scratch root: {scratch_root}")
|
|
print(f"Orbit pool: {orbit_pool if orbit_pool else 'UNRESOLVED'}")
|
|
print(f"DEM: {dem_path if dem_path else 'UNRESOLVED'}")
|
|
print(f"Ready: {readiness['ready_for_stackStripMap_nofocus']}")
|
|
if blockers:
|
|
print("Blockers:")
|
|
for blocker in blockers:
|
|
print(f" - {blocker}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|