chore: initialize insar management system v2
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a publish-style manifest and preview bundle from MintPy outputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _decode_date(value):
|
||||
return value.decode() if isinstance(value, (bytes, np.bytes_)) else str(value)
|
||||
|
||||
|
||||
def _read_h5_summary(h5_path: Path) -> dict:
|
||||
with h5py.File(h5_path, "r") as f:
|
||||
datasets = sorted(f.keys())
|
||||
attrs = {k: (v.item() if hasattr(v, "item") else v) for k, v in f.attrs.items()}
|
||||
serializable_attrs = {}
|
||||
for key, value in attrs.items():
|
||||
if isinstance(value, bytes):
|
||||
serializable_attrs[key] = value.decode()
|
||||
elif isinstance(value, np.ndarray):
|
||||
serializable_attrs[key] = value.tolist()
|
||||
else:
|
||||
serializable_attrs[key] = value
|
||||
|
||||
summary = {
|
||||
"path": h5_path.name,
|
||||
"datasets": datasets,
|
||||
"attrs": serializable_attrs,
|
||||
}
|
||||
|
||||
if "date" in f:
|
||||
summary["dates"] = [_decode_date(x) for x in f["date"][:]]
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def _write_velocity_preview(geo_velocity_h5: Path, output_png: Path) -> dict:
|
||||
with h5py.File(geo_velocity_h5, "r") as f:
|
||||
velocity = f["velocity"][:]
|
||||
|
||||
finite = np.isfinite(velocity)
|
||||
valid = velocity[finite]
|
||||
|
||||
if valid.size == 0:
|
||||
raise RuntimeError(f"No finite velocity values found in {geo_velocity_h5}")
|
||||
|
||||
vmax = float(np.nanpercentile(np.abs(valid), 98))
|
||||
vmax = max(vmax, 1e-6)
|
||||
vmin = -vmax
|
||||
|
||||
fig = plt.figure(figsize=(10, 7), dpi=150)
|
||||
ax = fig.add_subplot(111)
|
||||
im = ax.imshow(velocity, cmap="RdBu_r", vmin=vmin, vmax=vmax)
|
||||
ax.set_title("Velocity Preview (m/year)")
|
||||
ax.set_xticks([])
|
||||
ax.set_yticks([])
|
||||
cbar = fig.colorbar(im, ax=ax, shrink=0.82)
|
||||
cbar.set_label("m/year")
|
||||
fig.tight_layout()
|
||||
output_png.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(output_png, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
return {
|
||||
"vmin": vmin,
|
||||
"vmax": vmax,
|
||||
"valid_pixels": int(valid.size),
|
||||
}
|
||||
|
||||
|
||||
def _count_mask_pixels(mask_h5: Path) -> dict:
|
||||
with h5py.File(mask_h5, "r") as f:
|
||||
dataset_name = "mask" if "mask" in f else "waterMask"
|
||||
data = f[dataset_name][:]
|
||||
|
||||
total = int(data.size)
|
||||
valid = int(np.count_nonzero(data))
|
||||
return {
|
||||
"dataset": dataset_name,
|
||||
"valid_pixels": valid,
|
||||
"total_pixels": total,
|
||||
"valid_ratio": valid / total if total else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def build_bundle(mintpy_work_dir: Path, publish_dir: Path, group_key: str | None) -> None:
|
||||
assets_dir = publish_dir / "assets"
|
||||
preview_dir = publish_dir / "preview"
|
||||
metadata_dir = publish_dir / "metadata"
|
||||
|
||||
geo_velocity_h5 = assets_dir / "geo_velocity.h5"
|
||||
geo_timeseries_h5 = assets_dir / "geo_timeseries.h5"
|
||||
geo_temporal_coh_h5 = assets_dir / "geo_temporalCoherence.h5"
|
||||
geo_mask_temp_coh_h5 = assets_dir / "geo_maskTempCoh.h5"
|
||||
|
||||
preview_stats = _write_velocity_preview(
|
||||
geo_velocity_h5=geo_velocity_h5,
|
||||
output_png=preview_dir / "velocity_preview.png",
|
||||
)
|
||||
|
||||
with h5py.File(mintpy_work_dir / "timeseries.h5", "r") as ts_file:
|
||||
ref_date = ts_file.attrs.get("REF_DATE")
|
||||
ref_x = ts_file.attrs.get("REF_X")
|
||||
ref_y = ts_file.attrs.get("REF_Y")
|
||||
stack_dates = [_decode_date(x) for x in ts_file["date"][:]]
|
||||
|
||||
manifest = {
|
||||
"schema_version": "psinsar.publish.v1",
|
||||
"catalog_name": "psinsar",
|
||||
"mode": "sbas",
|
||||
"engine_code": "isce2",
|
||||
"processor_code": "isce2_stack_mintpy",
|
||||
"group_key": group_key,
|
||||
"mintpy_work_dir": str(mintpy_work_dir),
|
||||
"publish_dir": str(publish_dir),
|
||||
"reference_date": _decode_date(ref_date) if ref_date is not None else None,
|
||||
"reference_point": {
|
||||
"x": int(ref_x) if ref_x is not None else None,
|
||||
"y": int(ref_y) if ref_y is not None else None,
|
||||
},
|
||||
"stack_dates": stack_dates,
|
||||
"artifacts": [
|
||||
{"product_type": "timeseries_cube", "path": "assets/geo_timeseries.h5"},
|
||||
{"product_type": "velocity_map", "path": "assets/geo_velocity.h5"},
|
||||
{"product_type": "velocity_geotiff", "path": "assets/velocity.tif"},
|
||||
{"product_type": "temporal_coherence", "path": "assets/geo_temporalCoherence.h5"},
|
||||
{"product_type": "temporal_coherence_geotiff", "path": "assets/temporalCoherence.tif"},
|
||||
{"product_type": "quality_mask", "path": "assets/geo_maskTempCoh.h5"},
|
||||
{"product_type": "quality_mask_geotiff", "path": "assets/maskTempCoh.tif"},
|
||||
{"product_type": "preview_png", "path": "preview/velocity_preview.png"},
|
||||
{"product_type": "diagnostic_png", "path": "preview/numTriNonzeroIntAmbiguity.png"},
|
||||
],
|
||||
"quality": {
|
||||
"mask_all_valid": _count_mask_pixels(mintpy_work_dir / "maskAllValid.h5"),
|
||||
"mask_temp_coh": _count_mask_pixels(mintpy_work_dir / "maskTempCoh.h5"),
|
||||
"velocity_preview": preview_stats,
|
||||
},
|
||||
"summaries": {
|
||||
"geo_velocity": _read_h5_summary(geo_velocity_h5),
|
||||
"geo_timeseries": _read_h5_summary(geo_timeseries_h5),
|
||||
"geo_temporal_coherence": _read_h5_summary(geo_temporal_coh_h5),
|
||||
"geo_mask_temp_coh": _read_h5_summary(geo_mask_temp_coh_h5),
|
||||
},
|
||||
"metadata_files": [
|
||||
"metadata/smallbaselineApp.cfg",
|
||||
"metadata/source_quality_summary.json",
|
||||
],
|
||||
}
|
||||
|
||||
summary_json = {
|
||||
"maskAllValid": manifest["quality"]["mask_all_valid"],
|
||||
"maskTempCoh": manifest["quality"]["mask_temp_coh"],
|
||||
"preview": manifest["quality"]["velocity_preview"],
|
||||
}
|
||||
|
||||
publish_dir.mkdir(parents=True, exist_ok=True)
|
||||
metadata_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
(publish_dir / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(metadata_dir / "source_quality_summary.json").write_text(
|
||||
json.dumps(summary_json, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print(f"Wrote manifest: {publish_dir / 'manifest.json'}")
|
||||
print(f"Wrote quality summary: {metadata_dir / 'source_quality_summary.json'}")
|
||||
print(f"Wrote preview: {preview_dir / 'velocity_preview.png'}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Build publish-style artifacts for MintPy SBAS outputs.")
|
||||
parser.add_argument("--mintpy-work-dir", required=True, help="MintPy work directory containing timeseries.h5, velocity.h5, etc.")
|
||||
parser.add_argument("--publish-dir", required=True, help="Publish output directory.")
|
||||
parser.add_argument("--group-key", default=None, help="Optional stack group key to embed in manifest.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
build_bundle(
|
||||
mintpy_work_dir=Path(args.mintpy_work_dir),
|
||||
publish_dir=Path(args.publish_dir),
|
||||
group_key=args.group_key,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
REPO_ROOT="${REPO_ROOT:-/mnt/z/Code/Insar_management_system_v2}"
|
||||
EXP_ROOT="${EXP_ROOT:-$REPO_ROOT/experiments/isce2_sbas_timeseries}"
|
||||
|
||||
echo "== repo =="
|
||||
echo "$REPO_ROOT"
|
||||
test -d "$REPO_ROOT"
|
||||
|
||||
echo "== experiment root =="
|
||||
echo "$EXP_ROOT"
|
||||
test -d "$EXP_ROOT"
|
||||
|
||||
echo "== python3 =="
|
||||
python3 --version
|
||||
|
||||
echo "== conda env list =="
|
||||
"$CONDA_BIN" env list
|
||||
|
||||
echo "== isce2 runtime =="
|
||||
"$CONDA_BIN" run -n isce2 python -c "import sys; import isce; print(sys.executable); print(isce.__file__)"
|
||||
|
||||
echo "== Lutan1 sensor module =="
|
||||
"$CONDA_BIN" run -n isce2 python -c "from isce.components.isceobj.Sensor import Lutan1; print(Lutan1.__file__)"
|
||||
|
||||
echo "== mintpy import check =="
|
||||
"$CONDA_BIN" run -n isce2 python -c "import importlib.util; print('mintpy:present' if importlib.util.find_spec('mintpy') else 'mintpy:missing')"
|
||||
|
||||
echo "== candidate ISCE stack directories =="
|
||||
find /home/administrator/miniconda3/envs/isce2 -maxdepth 6 \
|
||||
\( -iname 'stripmapStack' -o -iname 'topsStack' -o -iname 'stack' \) 2>/dev/null || true
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a strict MintPy mask containing only pixels valid in all interferograms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
|
||||
|
||||
def build_mask(ifgram_stack: Path, output_path: Path, block_rows: int) -> None:
|
||||
with h5py.File(ifgram_stack, "r") as src:
|
||||
unwrap = src["unwrapPhase"]
|
||||
conn = src.get("connectComponent")
|
||||
|
||||
num_ifg, length, width = unwrap.shape
|
||||
mask = np.ones((length, width), dtype=np.bool_)
|
||||
|
||||
print(f"Input stack: {ifgram_stack}")
|
||||
print(f"Interferograms: {num_ifg}")
|
||||
print(f"Shape: {length} x {width}")
|
||||
print(f"Block rows: {block_rows}")
|
||||
|
||||
for row0 in range(0, length, block_rows):
|
||||
row1 = min(row0 + block_rows, length)
|
||||
block = unwrap[:, row0:row1, :]
|
||||
block_mask = np.all(np.isfinite(block) & (block != 0.0), axis=0)
|
||||
|
||||
if conn is not None:
|
||||
conn_block = conn[:, row0:row1, :]
|
||||
block_mask &= np.all(conn_block != 0, axis=0)
|
||||
|
||||
mask[row0:row1, :] = block_mask
|
||||
print(f"Processed rows {row0}:{row1}")
|
||||
|
||||
attrs = dict(src.attrs)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with h5py.File(output_path, "w") as dst:
|
||||
dst.create_dataset("mask", data=mask, dtype=np.bool_)
|
||||
for key, value in attrs.items():
|
||||
dst.attrs[key] = value
|
||||
dst.attrs["FILE_TYPE"] = "mask"
|
||||
dst.attrs["DATASET_NAME"] = "mask"
|
||||
dst.attrs["SOURCE_FILE"] = str(ifgram_stack)
|
||||
dst.attrs["MASK_RULE"] = "all_ifgrams_finite_nonzero_and_conncomp_nonzero"
|
||||
|
||||
valid_pixels = int(mask.sum())
|
||||
total_pixels = int(mask.size)
|
||||
print(f"Output mask: {output_path}")
|
||||
print(f"Valid pixels: {valid_pixels}/{total_pixels} ({valid_pixels / total_pixels * 100:.2f}%)")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a strict mask of pixels valid in all MintPy interferograms."
|
||||
)
|
||||
parser.add_argument("--ifgram-stack", required=True, help="Path to MintPy inputs/ifgramStack.h5")
|
||||
parser.add_argument("--output", required=True, help="Output HDF5 path, e.g. maskAllValid.h5")
|
||||
parser.add_argument(
|
||||
"--block-rows",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Number of image rows processed per block.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
build_mask(
|
||||
ifgram_stack=Path(args.ifgram_stack),
|
||||
output_path=Path(args.output),
|
||||
block_rows=args.block_rows,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Create a synthetic stripmapStack water mask in radar coordinates. "
|
||||
"The default fill value 1 means all-land, which preserves downstream pixels."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--like-image",
|
||||
required=True,
|
||||
help="Existing ISCE image base path or .xml path used only for shape/metadata, for example shadowMask.rdr",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
required=True,
|
||||
help="Output water-mask base path, for example .../geom_reference/waterMask.rdr",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fill-value",
|
||||
type=int,
|
||||
default=1,
|
||||
choices=(0, 1),
|
||||
help="Pixel value to write. 1 keeps all pixels, 0 masks all pixels.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite an existing output mask.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
default=None,
|
||||
help="Optional JSON report path.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resolve_like_paths(value: str) -> tuple[Path, Path]:
|
||||
candidate = Path(value)
|
||||
if candidate.suffix == ".xml":
|
||||
xml_path = candidate
|
||||
image_path = Path(str(candidate)[:-4])
|
||||
else:
|
||||
image_path = candidate
|
||||
xml_path = Path(str(candidate) + ".xml")
|
||||
|
||||
if not xml_path.exists():
|
||||
raise FileNotFoundError(f"Template image XML not found: {xml_path}")
|
||||
return image_path, xml_path
|
||||
|
||||
|
||||
def maybe_unlink(path: Path) -> None:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
|
||||
|
||||
def require_xml_value(root: ET.Element, property_name: str) -> str:
|
||||
value_node = root.find(f"./property[@name='{property_name}']/value")
|
||||
if value_node is None or value_node.text is None:
|
||||
raise ValueError(f"Missing XML property '{property_name}'")
|
||||
return value_node.text.strip()
|
||||
|
||||
|
||||
def write_template_metadata(template_image: Path, template_xml: Path, output: Path) -> tuple[int, int]:
|
||||
root = ET.parse(template_xml).getroot()
|
||||
width = int(require_xml_value(root, "width"))
|
||||
length = int(require_xml_value(root, "length"))
|
||||
|
||||
file_name_node = root.find("./property[@name='file_name']/value")
|
||||
if file_name_node is None:
|
||||
raise ValueError(f"Missing XML file_name entry: {template_xml}")
|
||||
file_name_node.text = str(output)
|
||||
|
||||
xml_output = Path(str(output) + ".xml")
|
||||
ET.indent(root, space=" ")
|
||||
ET.ElementTree(root).write(xml_output, encoding="utf-8")
|
||||
|
||||
hdr_template = template_image.with_suffix(".hdr")
|
||||
hdr_output = output.with_suffix(".hdr")
|
||||
if hdr_template.exists():
|
||||
hdr_text = hdr_template.read_text(encoding="utf-8", errors="ignore")
|
||||
hdr_output.write_text(hdr_text.replace(str(template_image), str(output)), encoding="utf-8")
|
||||
|
||||
vrt_template = Path(str(template_image) + ".vrt")
|
||||
vrt_output = Path(str(output) + ".vrt")
|
||||
if vrt_template.exists():
|
||||
vrt_text = vrt_template.read_text(encoding="utf-8", errors="ignore")
|
||||
vrt_text = vrt_text.replace(template_image.name, output.name)
|
||||
vrt_output.write_text(vrt_text, encoding="utf-8")
|
||||
|
||||
return width, length
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
template_image, template_xml = resolve_like_paths(args.like_image)
|
||||
output = Path(args.output)
|
||||
|
||||
if output.exists() and not args.force:
|
||||
raise FileExistsError(f"Output already exists, use --force to overwrite: {output}")
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
width, length = write_template_metadata(template_image=template_image, template_xml=template_xml, output=output)
|
||||
|
||||
mask = np.full((length, width), args.fill_value, dtype=np.uint8)
|
||||
mask.tofile(output)
|
||||
|
||||
maybe_unlink(output.with_suffix(".rdr.aux.xml"))
|
||||
|
||||
report = {
|
||||
"generated_at_utc": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
||||
"template_xml": str(template_xml),
|
||||
"output": str(output),
|
||||
"width": width,
|
||||
"length": length,
|
||||
"fill_value": args.fill_value,
|
||||
"data_type": "BYTE",
|
||||
"note": "Synthetic all-land water mask for local stripmapStack experiments without Earthdata SWBD access.",
|
||||
}
|
||||
|
||||
report_path = Path(args.report) if args.report else output.parent / "synthetic_watermask_report.json"
|
||||
report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
print(f"Template: {template_xml}")
|
||||
print(f"Output: {output}")
|
||||
print(f"Shape: {length} x {width}")
|
||||
print(f"Value: {args.fill_value}")
|
||||
print(f"Report: {report_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <env-name> <output-dir-wsl>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENV_NAME="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
SAFE_NAME="${ENV_NAME//[^A-Za-z0-9._-]/_}"
|
||||
YAML_PATH="$OUTPUT_DIR/${SAFE_NAME}.no_builds.yml"
|
||||
EXPLICIT_PATH="$OUTPUT_DIR/${SAFE_NAME}.explicit.txt"
|
||||
LIST_PATH="$OUTPUT_DIR/${SAFE_NAME}.conda_list.txt"
|
||||
RUNTIME_PATH="$OUTPUT_DIR/${SAFE_NAME}.runtime_versions.txt"
|
||||
|
||||
echo "Exporting conda environment snapshot"
|
||||
echo "Env: $ENV_NAME"
|
||||
echo "Output dir: $OUTPUT_DIR"
|
||||
|
||||
"$CONDA_BIN" env export -n "$ENV_NAME" --no-builds > "$YAML_PATH"
|
||||
"$CONDA_BIN" list -n "$ENV_NAME" --explicit > "$EXPLICIT_PATH"
|
||||
"$CONDA_BIN" list -n "$ENV_NAME" > "$LIST_PATH"
|
||||
|
||||
"$CONDA_BIN" run -n "$ENV_NAME" python -c "
|
||||
import importlib.util
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
|
||||
def version_of(name):
|
||||
try:
|
||||
mod = __import__(name)
|
||||
return getattr(mod, '__version__', '<missing>')
|
||||
except Exception as exc:
|
||||
return f'<import failed: {exc}>'
|
||||
|
||||
for line in [
|
||||
f'python_executable={sys.executable}',
|
||||
f'python_version={platform.python_version()}',
|
||||
f'isce_present={importlib.util.find_spec(\"isce\") is not None}',
|
||||
f'mintpy_present={importlib.util.find_spec(\"mintpy\") is not None}',
|
||||
f'h5py_present={importlib.util.find_spec(\"h5py\") is not None}',
|
||||
]:
|
||||
print(line)
|
||||
|
||||
if importlib.util.find_spec('isce') is not None:
|
||||
import isce
|
||||
print(f'isce_file={isce.__file__}')
|
||||
print(f'isce_version={getattr(isce, \"__version__\", \"<missing>\")}')
|
||||
|
||||
if importlib.util.find_spec('mintpy') is not None:
|
||||
import mintpy
|
||||
print(f'mintpy_file={mintpy.__file__}')
|
||||
print(f'mintpy_version={getattr(mintpy, \"__version__\", \"<missing>\")}')
|
||||
|
||||
if importlib.util.find_spec('h5py') is not None:
|
||||
import h5py
|
||||
print(f'h5py_version={h5py.__version__}')
|
||||
" > "$RUNTIME_PATH"
|
||||
|
||||
echo "Wrote: $YAML_PATH"
|
||||
echo "Wrote: $EXPLICIT_PATH"
|
||||
echo "Wrote: $LIST_PATH"
|
||||
echo "Wrote: $RUNTIME_PATH"
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 2 || $# -gt 3 ]]; then
|
||||
echo "Usage: $0 <mintpy-work-dir-wsl> <publish-dir-wsl> [group-key]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MINTPY_WORK_DIR="$1"
|
||||
PUBLISH_DIR="$2"
|
||||
GROUP_KEY="${3:-}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MINTPY_RUNNER="${MINTPY_RUNNER:-$SCRIPT_DIR/run_mintpy_with_isce_ubuntu2404.sh}"
|
||||
PUBLISH_BUILDER="$SCRIPT_DIR/build_mintpy_publish_bundle.py"
|
||||
|
||||
GEO_LAT_STEP="${GEO_LAT_STEP:--0.000185185}"
|
||||
GEO_LON_STEP="${GEO_LON_STEP:-0.000185185}"
|
||||
GEO_INTERP_METHOD="${GEO_INTERP_METHOD:-nearest}"
|
||||
|
||||
ASSETS_DIR="$PUBLISH_DIR/assets"
|
||||
PREVIEW_DIR="$PUBLISH_DIR/preview"
|
||||
METADATA_DIR="$PUBLISH_DIR/metadata"
|
||||
|
||||
mkdir -p "$ASSETS_DIR" "$PREVIEW_DIR" "$METADATA_DIR"
|
||||
|
||||
LOOKUP_FILE="$MINTPY_WORK_DIR/inputs/geometryRadar.h5"
|
||||
|
||||
echo "MintPy publish export"
|
||||
echo "Work dir: $MINTPY_WORK_DIR"
|
||||
echo "Publish dir: $PUBLISH_DIR"
|
||||
echo "Lookup file: $LOOKUP_FILE"
|
||||
echo "Runner: $MINTPY_RUNNER"
|
||||
echo "Geo step: $GEO_LAT_STEP, $GEO_LON_STEP"
|
||||
echo "Interp: $GEO_INTERP_METHOD"
|
||||
|
||||
for src in velocity.h5 temporalCoherence.h5 maskTempCoh.h5 timeseries.h5; do
|
||||
bash "$MINTPY_RUNNER" geocode.py \
|
||||
"$MINTPY_WORK_DIR/$src" \
|
||||
-l "$LOOKUP_FILE" \
|
||||
--lalo "$GEO_LAT_STEP" "$GEO_LON_STEP" \
|
||||
-i "$GEO_INTERP_METHOD" \
|
||||
--outdir "$ASSETS_DIR" \
|
||||
--update
|
||||
done
|
||||
|
||||
bash "$MINTPY_RUNNER" save_gdal.py "$ASSETS_DIR/geo_velocity.h5" -d velocity -o "$ASSETS_DIR/velocity.tif"
|
||||
bash "$MINTPY_RUNNER" save_gdal.py "$ASSETS_DIR/geo_temporalCoherence.h5" -d temporalCoherence -o "$ASSETS_DIR/temporalCoherence.tif"
|
||||
bash "$MINTPY_RUNNER" save_gdal.py "$ASSETS_DIR/geo_maskTempCoh.h5" -d mask -o "$ASSETS_DIR/maskTempCoh.tif"
|
||||
|
||||
cp "$MINTPY_WORK_DIR/smallbaselineApp.cfg" "$METADATA_DIR/smallbaselineApp.cfg"
|
||||
cp "$MINTPY_WORK_DIR/numTriNonzeroIntAmbiguity.png" "$PREVIEW_DIR/numTriNonzeroIntAmbiguity.png"
|
||||
|
||||
if [[ -n "$GROUP_KEY" ]]; then
|
||||
bash "$MINTPY_RUNNER" python "$PUBLISH_BUILDER" \
|
||||
--mintpy-work-dir "$MINTPY_WORK_DIR" \
|
||||
--publish-dir "$PUBLISH_DIR" \
|
||||
--group-key "$GROUP_KEY"
|
||||
else
|
||||
bash "$MINTPY_RUNNER" python "$PUBLISH_BUILDER" \
|
||||
--mintpy-work-dir "$MINTPY_WORK_DIR" \
|
||||
--publish-dir "$PUBLISH_DIR"
|
||||
fi
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GENERIC_EXPORTER="$SCRIPT_DIR/export_mintpy_publish_products_ubuntu2404.sh"
|
||||
|
||||
export MINTPY_RUNNER="${MINTPY_RUNNER:-$SCRIPT_DIR/run_mintpy_unified_env_ubuntu2404.sh}"
|
||||
|
||||
bash "$GENERIC_EXPORTER" "$@"
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <output-dir-wsl>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTPUT_DIR="$1"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EXPORTER="$SCRIPT_DIR/export_conda_env_snapshot_ubuntu2404.sh"
|
||||
|
||||
bash "$EXPORTER" isce2 "$OUTPUT_DIR"
|
||||
bash "$EXPORTER" isce2_mintpy_v1 "$OUTPUT_DIR"
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
CONDA_ENV="${CONDA_ENV:-isce2}"
|
||||
PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}"
|
||||
PACKAGES=(
|
||||
matplotlib
|
||||
)
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ISCE2 stack runtime bootstrap"
|
||||
echo "Conda: $CONDA_BIN"
|
||||
echo "Env: $CONDA_ENV"
|
||||
echo "Index: $PIP_INDEX_URL"
|
||||
|
||||
for pkg in "${PACKAGES[@]}"; do
|
||||
echo "Installing $pkg into $CONDA_ENV"
|
||||
"$CONDA_BIN" run -n "$CONDA_ENV" python -m pip install -i "$PIP_INDEX_URL" "$pkg"
|
||||
done
|
||||
|
||||
echo "Runtime bootstrap complete"
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
SOURCE_ENV="${SOURCE_ENV:-isce2}"
|
||||
TARGET_ENV="${TARGET_ENV:-isce2_mintpy}"
|
||||
PYTHON_VERSION="${PYTHON_VERSION:-3.11}"
|
||||
BOOTSTRAP_MODE="${BOOTSTRAP_MODE:-clone}"
|
||||
USE_TUNA_MIRROR="${USE_TUNA_MIRROR:-1}"
|
||||
CHANNEL_CONDA_FORGE="${CHANNEL_CONDA_FORGE:-https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge}"
|
||||
CHANNEL_MAIN="${CHANNEL_MAIN:-https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main}"
|
||||
CHANNEL_R="${CHANNEL_R:-https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r}"
|
||||
MINTPY_SPEC="${MINTPY_SPEC:-mintpy}"
|
||||
CLONE_OFFLINE="${CLONE_OFFLINE:-1}"
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
channel_args=()
|
||||
if [[ "$USE_TUNA_MIRROR" == "1" ]]; then
|
||||
channel_args=(
|
||||
--override-channels
|
||||
-c "$CHANNEL_CONDA_FORGE"
|
||||
-c "$CHANNEL_MAIN"
|
||||
-c "$CHANNEL_R"
|
||||
)
|
||||
fi
|
||||
|
||||
env_exists() {
|
||||
"$CONDA_BIN" env list | awk '{print $1}' | grep -Fxq "$1"
|
||||
}
|
||||
|
||||
echo "Unified ISCE2 + MintPy runtime bootstrap"
|
||||
echo "Conda: $CONDA_BIN"
|
||||
echo "Source env: $SOURCE_ENV"
|
||||
echo "Target env: $TARGET_ENV"
|
||||
echo "Python: $PYTHON_VERSION"
|
||||
echo "Mode: $BOOTSTRAP_MODE"
|
||||
echo "MintPy spec: $MINTPY_SPEC"
|
||||
echo "Use mirror: $USE_TUNA_MIRROR"
|
||||
echo "Clone offline:$CLONE_OFFLINE"
|
||||
|
||||
if ! env_exists "$SOURCE_ENV"; then
|
||||
echo "Missing source environment: $SOURCE_ENV" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$BOOTSTRAP_MODE" != "clone" && "$BOOTSTRAP_MODE" != "recreate" ]]; then
|
||||
echo "Unsupported BOOTSTRAP_MODE: $BOOTSTRAP_MODE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if env_exists "$TARGET_ENV"; then
|
||||
echo "Environment $TARGET_ENV already exists. Reusing it."
|
||||
else
|
||||
if [[ "$BOOTSTRAP_MODE" == "clone" ]]; then
|
||||
echo "Cloning $SOURCE_ENV into $TARGET_ENV"
|
||||
clone_args=("${channel_args[@]}" -y -n "$TARGET_ENV" --clone "$SOURCE_ENV")
|
||||
if [[ "$CLONE_OFFLINE" == "1" ]]; then
|
||||
clone_args+=(--offline)
|
||||
fi
|
||||
"$CONDA_BIN" create "${clone_args[@]}"
|
||||
else
|
||||
tmp_export="$(mktemp)"
|
||||
tmp_conda_specs="$(mktemp)"
|
||||
tmp_pip_specs="$(mktemp)"
|
||||
trap 'rm -f "$tmp_export" "$tmp_conda_specs" "$tmp_pip_specs"' EXIT
|
||||
|
||||
echo "Exporting $SOURCE_ENV into a recreate spec"
|
||||
"$CONDA_BIN" env export -n "$SOURCE_ENV" --no-builds > "$tmp_export"
|
||||
|
||||
awk \
|
||||
'
|
||||
/^dependencies:/ {
|
||||
in_dependencies = 1
|
||||
next
|
||||
}
|
||||
/^prefix:/ {
|
||||
exit
|
||||
}
|
||||
in_dependencies == 1 && /^ - pip:$/ {
|
||||
exit
|
||||
}
|
||||
in_dependencies == 1 && /^ - / {
|
||||
print substr($0, 5)
|
||||
}
|
||||
' "$tmp_export" > "$tmp_conda_specs"
|
||||
|
||||
awk \
|
||||
'
|
||||
/^ - pip:$/ {
|
||||
in_pip = 1
|
||||
next
|
||||
}
|
||||
/^prefix:/ {
|
||||
exit
|
||||
}
|
||||
in_pip == 1 && /^ - / {
|
||||
print substr($0, 7)
|
||||
}
|
||||
' "$tmp_export" > "$tmp_pip_specs"
|
||||
|
||||
mapfile -t conda_specs < "$tmp_conda_specs"
|
||||
if [[ ${#conda_specs[@]} -eq 0 ]]; then
|
||||
echo "Failed to extract conda dependency specs from $SOURCE_ENV export" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Recreating $TARGET_ENV from exported dependency list"
|
||||
"$CONDA_BIN" create -y -n "$TARGET_ENV" "${channel_args[@]}" "${conda_specs[@]}"
|
||||
|
||||
if [[ -s "$tmp_pip_specs" ]]; then
|
||||
mapfile -t pip_specs < "$tmp_pip_specs"
|
||||
echo "Reinstalling exported pip packages into $TARGET_ENV"
|
||||
"$CONDA_BIN" run -n "$TARGET_ENV" python -m pip install "${pip_specs[@]}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Installing MintPy into $TARGET_ENV"
|
||||
"$CONDA_BIN" install -y -n "$TARGET_ENV" "${channel_args[@]}" "$MINTPY_SPEC"
|
||||
|
||||
echo "Verifying unified runtime imports"
|
||||
"$CONDA_BIN" run -n "$TARGET_ENV" python -c "import sys; import isce; import mintpy; import h5py; print(sys.executable); print(isce.__file__); print(mintpy.__file__); print('h5py=' + h5py.__version__)"
|
||||
|
||||
echo "Unified runtime bootstrap complete"
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
TARGET_ENV="${TARGET_ENV:-mintpy}"
|
||||
PYTHON_VERSION="${PYTHON_VERSION:-3.11}"
|
||||
USE_TUNA_MIRROR="${USE_TUNA_MIRROR:-1}"
|
||||
CHANNEL_CONDA_FORGE="${CHANNEL_CONDA_FORGE:-https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge}"
|
||||
CHANNEL_MAIN="${CHANNEL_MAIN:-https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main}"
|
||||
CHANNEL_R="${CHANNEL_R:-https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r}"
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
channel_args=()
|
||||
if [[ "$USE_TUNA_MIRROR" == "1" ]]; then
|
||||
channel_args=(
|
||||
--override-channels
|
||||
-c "$CHANNEL_CONDA_FORGE"
|
||||
-c "$CHANNEL_MAIN"
|
||||
-c "$CHANNEL_R"
|
||||
)
|
||||
fi
|
||||
|
||||
env_exists() {
|
||||
"$CONDA_BIN" env list | awk '{print $1}' | grep -Fxq "$TARGET_ENV"
|
||||
}
|
||||
|
||||
echo "MintPy runtime bootstrap"
|
||||
echo "Conda: $CONDA_BIN"
|
||||
echo "Target env: $TARGET_ENV"
|
||||
echo "Python: $PYTHON_VERSION"
|
||||
echo "Use mirror: $USE_TUNA_MIRROR"
|
||||
|
||||
if env_exists; then
|
||||
echo "Environment $TARGET_ENV already exists. Installing or updating MintPy."
|
||||
"$CONDA_BIN" install -y -n "$TARGET_ENV" "${channel_args[@]}" mintpy
|
||||
else
|
||||
echo "Creating environment $TARGET_ENV with MintPy."
|
||||
"$CONDA_BIN" create -y -n "$TARGET_ENV" "${channel_args[@]}" "python=$PYTHON_VERSION" mintpy
|
||||
fi
|
||||
|
||||
echo "Verifying MintPy import"
|
||||
"$CONDA_BIN" run -n "$TARGET_ENV" python -c "import mintpy; print(mintpy.__file__)"
|
||||
echo "MintPy runtime bootstrap complete"
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shelve
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def choose_path(scene: Dict[str, Any], primary_key: str, fallback_key: str) -> str:
|
||||
primary = scene.get(primary_key)
|
||||
fallback = scene.get(fallback_key)
|
||||
for candidate in (primary, fallback):
|
||||
if candidate and Path(candidate).exists():
|
||||
return str(Path(candidate))
|
||||
raise FileNotFoundError(
|
||||
f"Neither {primary_key} nor {fallback_key} exists for scene {scene.get('date') or scene.get('imaging_date')}"
|
||||
)
|
||||
|
||||
|
||||
def load_manifest(path: Path) -> Dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def remove_existing_shelve(stem: Path) -> None:
|
||||
for suffix in ("", ".db", ".dat", ".dir", ".bak"):
|
||||
candidate = Path(str(stem) + suffix)
|
||||
if candidate.exists():
|
||||
if candidate.is_dir():
|
||||
shutil.rmtree(candidate)
|
||||
else:
|
||||
candidate.unlink()
|
||||
|
||||
|
||||
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 SceneResult:
|
||||
date: str
|
||||
output_dir: str
|
||||
slc_path: str
|
||||
data_shelve: str
|
||||
status: str
|
||||
bytes_written: Optional[int]
|
||||
started_at_utc: str
|
||||
ended_at_utc: str
|
||||
|
||||
|
||||
def materialize_one_scene(
|
||||
scene: Dict[str, Any],
|
||||
force: bool,
|
||||
) -> SceneResult:
|
||||
import isce
|
||||
from isceobj.Sensor import createSensor
|
||||
|
||||
date = str(scene["date"])
|
||||
output_dir = Path(scene["target_dir_wsl"])
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
slc_path = Path(scene["expected_slc_wsl"])
|
||||
slc_xml_path = Path(scene["expected_slc_xml_wsl"])
|
||||
data_shelve = Path(scene["expected_data_shelve_wsl"])
|
||||
|
||||
tiff_path = choose_path(scene, "source_tiff_wsl", "source_tiff_windows")
|
||||
orbit_xml = choose_path(scene, "orbit_xml_wsl", "orbit_xml_windows")
|
||||
|
||||
if force:
|
||||
for path in (slc_path, slc_xml_path, Path(str(slc_path) + ".vrt")):
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
remove_existing_shelve(data_shelve)
|
||||
|
||||
if slc_path.exists() and slc_xml_path.exists() and shelve_stem_exists(data_shelve):
|
||||
now = datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
|
||||
return SceneResult(
|
||||
date=date,
|
||||
output_dir=str(output_dir),
|
||||
slc_path=str(slc_path),
|
||||
data_shelve=str(data_shelve),
|
||||
status="skipped_existing",
|
||||
bytes_written=slc_path.stat().st_size,
|
||||
started_at_utc=now,
|
||||
ended_at_utc=now,
|
||||
)
|
||||
|
||||
started_at = datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
|
||||
|
||||
sensor = createSensor("LUTAN1")
|
||||
sensor.configure()
|
||||
sensor.tiff = tiff_path
|
||||
sensor.orbitFile = orbit_xml
|
||||
sensor.output = str(slc_path)
|
||||
sensor.extractImage()
|
||||
sensor.extractDoppler()
|
||||
sensor.frame.getImage().renderHdr()
|
||||
|
||||
remove_existing_shelve(data_shelve)
|
||||
with shelve.open(str(data_shelve)) as db:
|
||||
db["frame"] = sensor.frame
|
||||
|
||||
ended_at = datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
|
||||
report = {
|
||||
"date": date,
|
||||
"source_tiff": tiff_path,
|
||||
"orbit_xml": orbit_xml,
|
||||
"output_slc": str(slc_path),
|
||||
"output_slc_xml": str(slc_xml_path),
|
||||
"data_shelve": str(data_shelve),
|
||||
"frame_lines": sensor.frame.getNumberOfLines(),
|
||||
"frame_samples": sensor.frame.getNumberOfSamples(),
|
||||
"started_at_utc": started_at,
|
||||
"ended_at_utc": ended_at,
|
||||
}
|
||||
(output_dir / "materialization_report.json").write_text(
|
||||
json.dumps(report, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return SceneResult(
|
||||
date=date,
|
||||
output_dir=str(output_dir),
|
||||
slc_path=str(slc_path),
|
||||
data_shelve=str(data_shelve),
|
||||
status="materialized",
|
||||
bytes_written=slc_path.stat().st_size if slc_path.exists() else None,
|
||||
started_at_utc=started_at,
|
||||
ended_at_utc=ended_at,
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Materialize LT-1 stack acquisitions into stripmapStack-ready SLC/date directories."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stack-manifest",
|
||||
required=True,
|
||||
help="Path to stack_input_manifest.json generated by build_lt1_stack_prep.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dates",
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="Optional subset of acquisition dates to materialize, for example 20250510 20250705",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite existing .slc/.xml/data outputs for the selected dates.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
manifest_path = Path(args.stack_manifest)
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"Stack manifest not found: {manifest_path}")
|
||||
|
||||
manifest = load_manifest(manifest_path)
|
||||
scenes = list(manifest.get("scenes", []))
|
||||
if not scenes:
|
||||
raise ValueError(f"No scenes found in stack manifest: {manifest_path}")
|
||||
|
||||
selected_dates = set(args.dates or [])
|
||||
if selected_dates:
|
||||
scenes = [scene for scene in scenes if str(scene["date"]) in selected_dates]
|
||||
if not scenes:
|
||||
raise ValueError(f"No matching dates found in manifest for selection: {sorted(selected_dates)}")
|
||||
|
||||
results: List[SceneResult] = []
|
||||
for scene in scenes:
|
||||
print(f"Materializing {scene['date']} -> {scene['target_dir_wsl']}")
|
||||
result = materialize_one_scene(scene, force=args.force)
|
||||
results.append(result)
|
||||
print(f" status={result.status} slc={result.slc_path}")
|
||||
|
||||
report = {
|
||||
"generated_at_utc": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
||||
"stack_manifest": str(manifest_path),
|
||||
"results": [result.__dict__ for result in results],
|
||||
}
|
||||
|
||||
report_path = manifest_path.parent / "materialization_summary.json"
|
||||
report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"Summary: {report_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,59 @@
|
||||
# Phase 0 Practical TODO
|
||||
|
||||
## Immediate
|
||||
|
||||
- [x] Run `scripts/check_env_ubuntu2404.sh` inside `Ubuntu-24.04`.
|
||||
- [x] Use `scripts/scan_lt1_stack_candidates.py` to keep one baseline sample stack manifest current.
|
||||
- [x] Treat `E123.3_N46.1` as the first tile-level smoke-test sample unless a better sample appears.
|
||||
- [x] Confirm ISCE2 stack-processing scripts are present.
|
||||
- [x] Confirm the official helper scripts do not advertise LT-1/LUTAN1 stack prep.
|
||||
- [x] Record required orbit, DEM, and metadata adaptations.
|
||||
- [x] Run `scripts/build_lt1_stack_prep.py` to keep the dry-run stack workspace current.
|
||||
|
||||
## Before first end-to-end run
|
||||
|
||||
- [x] Implement an LT-1 scene materializer that creates `YYYYMMDD.slc`, `YYYYMMDD.slc.xml`, and `data`.
|
||||
- [x] Materialize the remaining acquisitions for `E123.3_N46.1` under `scratch/.../SLC/`.
|
||||
- [x] Smoke-test the materializer on the reference date `20250510`.
|
||||
- [x] Run the generated `run_stripmap_stack_dryrun.sh` preflight and then `stackStripMap.py --nofocus`.
|
||||
- [x] Inspect the produced `baseline/`, `configs/`, and `run_files/` outputs.
|
||||
- [x] Prepare a stack-local DEM to avoid global-DEM bbox behavior during `createWaterMask`.
|
||||
- [x] Add a reproducible synthetic `waterMask` fallback for `run_01_reference` when Earthdata credentials are unavailable.
|
||||
Working rule: DEM is already local and sufficient; do not download `SWBD` during this experiment stage.
|
||||
- [x] Extract shared LT-1 input preparation helper for DEM/orbit resolution.
|
||||
Compatibility rule: original D-InSAR entry logic remains in place; only the duplicated input-prep internals were consolidated.
|
||||
- [x] Decide MintPy installation strategy after stack generation is stable.
|
||||
Decision: default to a dedicated WSL conda env named `mintpy` so the working `isce2` processing env stays unchanged on the development machine.
|
||||
- [x] Freeze the first smoke-test command chain.
|
||||
Frozen chain: `run_01_reference -> run_02_focus_split -> run_03_geo2rdr_coarseResamp -> run_04_refineSecondaryTiming -> run_05_invertMisreg -> run_06_fineResamp -> run_07_grid_baseline`
|
||||
- [x] Execute `run_01_reference` through the WSL wrapper and verify the fallback-recovered geometry outputs.
|
||||
- [x] Execute `run_02` to `run_07` and record LT-1-specific failures if they appear.
|
||||
Result: all stages exited `0` in `Ubuntu-24.04`. `run_04_refineSecondaryTiming` logs still contain `Bad match at level 1` and `correlation error`, but pair-level `misreg`, date-level `misreg`, merged SLC, and merged baseline products were all generated successfully.
|
||||
|
||||
## Next Focus
|
||||
|
||||
- [x] Run `scripts/install_mintpy_runtime_ubuntu2404.sh` in `Ubuntu-24.04` and verify the new env.
|
||||
Result: dedicated WSL env `mintpy` was created successfully and `smallbaselineApp.py` / `prep_isce.py` are available.
|
||||
- [x] Validate MintPy ingestion against the current `stack_work/merged/` outputs.
|
||||
Result: `build_lt1_stack_prep.py --workflow interferogram` plus `run_08_igram` produced `Igrams/*/filt_*_snaphu.unw`, and `prep_isce.py` completed successfully after bridging the working `isce2` Python package into the `mintpy` env.
|
||||
- [x] Draft the first `smallbaselineApp.cfg` for the LT-1 sample stack.
|
||||
Result: `configs/sample_smallbaseline_lt1_e123p3_n46p1.cfg` now records the first runnable LT-1 stripmapStack -> MintPy SBAS contract.
|
||||
- [x] Execute the first MintPy workflow steps after `prep_isce.py`.
|
||||
Result: the repo-local smoke-test chain now reaches radar-coordinate `timeseries.h5` and `velocity.h5` in `stack_work/mintpy_sbas_v5/`.
|
||||
Current helper chain:
|
||||
- `scripts/run_mintpy_with_isce_ubuntu2404.sh`
|
||||
- `scripts/create_mintpy_all_ifgram_mask.py`
|
||||
- `scripts/run_smallbaselineApp_patched.py`
|
||||
- `scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh`
|
||||
- [x] Draft the first production-side SBAS artifact manifest and publish contract.
|
||||
Result:
|
||||
- `configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json`
|
||||
- `docs/ISCE2_SBAS_TIMESERIES_DESIGN.md`
|
||||
|
||||
## New Follow-up
|
||||
|
||||
- [ ] Decide whether production should keep the repo-local patched MintPy launcher or pin an upstream-fixed MintPy version.
|
||||
- [x] Add the geocode/export stage needed for publishable SBAS rasters and previews.
|
||||
Result: experiment-layer publish export now succeeds into `publish/mintpy_sbas_v5/` with geocoded HDF5, GeoTIFF, preview PNG, and `manifest.json`.
|
||||
- [ ] Wire the validated SBAS runtime chain into backend workflow submission and artifact publishing.
|
||||
- [ ] Run a separate unified-environment experiment by cloning the current WSL `isce2` env and installing MintPy directly inside it.
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def load_manifest(path: Path) -> Dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def resolve_dem_source(arg_value: str | None, manifest: Dict[str, Any]) -> Path:
|
||||
candidates: List[str] = []
|
||||
if arg_value:
|
||||
candidates.append(arg_value)
|
||||
resolved = manifest.get("resolved_dependencies", {})
|
||||
for key in ("dem_path_wsl", "dem_path_windows"):
|
||||
value = resolved.get(key)
|
||||
if value:
|
||||
candidates.append(value)
|
||||
|
||||
for candidate in candidates:
|
||||
path = Path(candidate)
|
||||
if path.exists() and Path(str(path) + ".xml").exists():
|
||||
return path
|
||||
raise FileNotFoundError("Unable to resolve source DEM from arguments or stack manifest")
|
||||
|
||||
|
||||
def compute_bbox(manifest: Dict[str, Any], margin_deg: float) -> List[float]:
|
||||
lons: List[float] = []
|
||||
lats: List[float] = []
|
||||
for scene in manifest["scenes"]:
|
||||
lon = scene.get("scene_center_lon")
|
||||
lat = scene.get("scene_center_lat")
|
||||
if lon is None or lat is None:
|
||||
source_scene_json = scene.get("source_scene_json_wsl") or scene.get("source_scene_json_windows")
|
||||
if source_scene_json and Path(source_scene_json).exists():
|
||||
source_payload = json.loads(Path(source_scene_json).read_text(encoding="utf-8"))
|
||||
lon = source_payload.get("scene_center_lon")
|
||||
lat = source_payload.get("scene_center_lat")
|
||||
if lon is not None and lat is not None:
|
||||
lons.append(float(lon))
|
||||
lats.append(float(lat))
|
||||
if not lons or not lats:
|
||||
raise ValueError("Stack manifest does not include usable scene center coordinates")
|
||||
west = min(lons) - margin_deg
|
||||
east = max(lons) + margin_deg
|
||||
south = min(lats) - margin_deg
|
||||
north = max(lats) + margin_deg
|
||||
return [south, north, west, east]
|
||||
|
||||
|
||||
def prepare_dem(source_dem: Path, output_dem: Path, bbox: List[float]) -> None:
|
||||
from osgeo import gdal
|
||||
from isce.applications.gdal2isce_xml import gdal2isce_xml
|
||||
|
||||
south, north, west, east = bbox
|
||||
src_open_path = Path(str(source_dem) + ".vrt")
|
||||
if not src_open_path.exists():
|
||||
src_open_path = source_dem
|
||||
|
||||
output_dem.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_vrt = Path(str(output_dem) + ".vrt")
|
||||
output_xml = Path(str(output_dem) + ".xml")
|
||||
output_hdr = Path(str(output_dem) + ".hdr")
|
||||
fallback_hdr = output_dem.with_suffix(".hdr")
|
||||
|
||||
src_ds = gdal.Open(str(src_open_path), gdal.GA_ReadOnly)
|
||||
if src_ds is None:
|
||||
raise RuntimeError(f"Unable to open DEM source: {src_open_path}")
|
||||
|
||||
translate_options = gdal.TranslateOptions(
|
||||
format="ENVI",
|
||||
projWin=[west, north, east, south],
|
||||
)
|
||||
out_ds = gdal.Translate(str(output_dem), src_ds, options=translate_options)
|
||||
if out_ds is None:
|
||||
raise RuntimeError("gdal.Translate failed while clipping the DEM")
|
||||
out_ds = None
|
||||
src_ds = None
|
||||
|
||||
vrt_ds = gdal.Open(str(output_dem), gdal.GA_ReadOnly)
|
||||
if vrt_ds is None:
|
||||
raise RuntimeError(f"Unable to reopen clipped DEM: {output_dem}")
|
||||
gdal.Translate(str(output_vrt), vrt_ds, options=gdal.TranslateOptions(format="VRT"))
|
||||
vrt_ds = None
|
||||
|
||||
gdal2isce_xml(str(output_vrt))
|
||||
if not output_xml.exists():
|
||||
raise RuntimeError(f"Expected ISCE XML was not created: {output_xml}")
|
||||
if not output_hdr.exists() and not fallback_hdr.exists():
|
||||
raise RuntimeError(f"Expected ENVI header was not created: {output_hdr} or {fallback_hdr}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Clip a local DEM window for one LT-1 stack workspace."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stack-manifest",
|
||||
required=True,
|
||||
help="Path to stack_input_manifest.json generated by build_lt1_stack_prep.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-dem",
|
||||
default=None,
|
||||
help="Override source DEM base path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--margin-deg",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Margin around stack scene-center extents in degrees.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
manifest_path = Path(args.stack_manifest)
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"Stack manifest not found: {manifest_path}")
|
||||
|
||||
manifest = load_manifest(manifest_path)
|
||||
source_dem = resolve_dem_source(args.source_dem, manifest)
|
||||
bbox = compute_bbox(manifest, margin_deg=args.margin_deg)
|
||||
|
||||
workspace = manifest["workspace"]
|
||||
dem_dir = Path(workspace["inputs_dir_wsl"]) / "dem"
|
||||
output_dem = dem_dir / "stack_dem_window.wgs84"
|
||||
prepare_dem(source_dem=source_dem, output_dem=output_dem, bbox=bbox)
|
||||
|
||||
report = {
|
||||
"stack_manifest": str(manifest_path),
|
||||
"source_dem": str(source_dem),
|
||||
"output_dem": str(output_dem),
|
||||
"bbox_south_north_west_east": bbox,
|
||||
}
|
||||
report_path = dem_dir / "stack_dem_window_report.json"
|
||||
report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
print(f"Source DEM: {source_dem}")
|
||||
print(f"Output DEM: {output_dem}")
|
||||
print(f"BBox: {bbox}")
|
||||
print(f"Report: {report_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 <scratch_root_wsl> <run_file_name>" >&2
|
||||
echo "Example: $0 /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_01_reference" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRATCH_ROOT="$1"
|
||||
RUN_FILE_NAME="$2"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
CONDA_ENV="${CONDA_ENV:-isce2}"
|
||||
ISCE2_SHARE="${ISCE2_SHARE:-/home/administrator/miniconda3/envs/isce2/share/isce2}"
|
||||
STRIPMAP_STACK_DIR="${STRIPMAP_STACK_DIR:-$ISCE2_SHARE/stripmapStack}"
|
||||
SYNTHETIC_WATERMASK_SCRIPT="${SYNTHETIC_WATERMASK_SCRIPT:-$SCRIPT_DIR/create_synthetic_watermask.py}"
|
||||
ALLOW_SYNTHETIC_WATERMASK="${ALLOW_SYNTHETIC_WATERMASK:-1}"
|
||||
STACK_WORK="$SCRATCH_ROOT/stack_work"
|
||||
RUN_FILE="$STACK_WORK/run_files/$RUN_FILE_NAME"
|
||||
LOG_DIR="$STACK_WORK/logs"
|
||||
LOG_FILE="$LOG_DIR/$RUN_FILE_NAME.log"
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$RUN_FILE" ]]; then
|
||||
echo "Run file not found: $RUN_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
export PYTHONPATH="$STRIPMAP_STACK_DIR:$ISCE2_SHARE${PYTHONPATH:+:$PYTHONPATH}"
|
||||
export PATH="$STRIPMAP_STACK_DIR:$PATH"
|
||||
|
||||
recover_reference_watermask() {
|
||||
local like_image="$STACK_WORK/geom_reference/shadowMask.rdr"
|
||||
local output_mask="$STACK_WORK/geom_reference/waterMask.rdr"
|
||||
local report_path="$LOG_DIR/$RUN_FILE_NAME.synthetic_watermask.json"
|
||||
local watermask_failure_pattern='Please create a \.netrc file|Running: createWaterMask|DataRetriever - ERROR|There was a problem in retrieving the file|SRTMSWBD\.003|SWBD'
|
||||
|
||||
if [[ "$RUN_FILE_NAME" != "run_01_reference" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$ALLOW_SYNTHETIC_WATERMASK" != "1" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$LOG_FILE" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Recover only the known offline water-mask failure modes observed in this
|
||||
# experiment: missing Earthdata credentials or SWBD retrieval failure.
|
||||
if ! grep -Eq "$watermask_failure_pattern" "$LOG_FILE"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$like_image" || ! -f "$like_image.xml" ]]; then
|
||||
echo "Synthetic water-mask fallback could not find template image: $like_image" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SYNTHETIC_WATERMASK_SCRIPT" ]]; then
|
||||
echo "Synthetic water-mask helper script not found: $SYNTHETIC_WATERMASK_SCRIPT" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Earthdata credentials are unavailable. Creating a synthetic all-land water mask."
|
||||
"$CONDA_BIN" run -n "$CONDA_ENV" python "$SYNTHETIC_WATERMASK_SCRIPT" \
|
||||
--like-image "$like_image" \
|
||||
--output "$output_mask" \
|
||||
--fill-value 1 \
|
||||
--force \
|
||||
--report "$report_path"
|
||||
}
|
||||
|
||||
echo "Executing stripmap stack run file"
|
||||
echo "Scratch root: $SCRATCH_ROOT"
|
||||
echo "Run file: $RUN_FILE"
|
||||
echo "Log file: $LOG_FILE"
|
||||
echo "Conda env: $CONDA_ENV"
|
||||
echo "PYTHONPATH: $PYTHONPATH"
|
||||
echo "PATH prefix: $STRIPMAP_STACK_DIR"
|
||||
|
||||
set -o pipefail
|
||||
"$CONDA_BIN" run -n "$CONDA_ENV" bash "$RUN_FILE" 2>&1 | tee "$LOG_FILE"
|
||||
RUN_STATUS=${PIPESTATUS[0]}
|
||||
|
||||
if [[ "$RUN_STATUS" -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if recover_reference_watermask; then
|
||||
echo "Recovered $RUN_FILE_NAME with a synthetic all-land water mask."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit "$RUN_STATUS"
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <smallbaseline-config-wsl> <mintpy-work-dir-wsl>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CFG_PATH="$1"
|
||||
WORK_DIR="$2"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BRIDGE_RUNNER="$SCRIPT_DIR/run_mintpy_with_isce_ubuntu2404.sh"
|
||||
PATCHED_APP="$SCRIPT_DIR/run_smallbaselineApp_patched.py"
|
||||
STRICT_MASK_BUILDER="$SCRIPT_DIR/create_mintpy_all_ifgram_mask.py"
|
||||
|
||||
echo "MintPy SBAS smoketest"
|
||||
echo "Config: $CFG_PATH"
|
||||
echo "Work dir: $WORK_DIR"
|
||||
|
||||
bash "$BRIDGE_RUNNER" python "$PATCHED_APP" "$CFG_PATH" --dir "$WORK_DIR" --dostep load_data
|
||||
|
||||
bash "$BRIDGE_RUNNER" python "$STRICT_MASK_BUILDER" \
|
||||
--ifgram-stack "$WORK_DIR/inputs/ifgramStack.h5" \
|
||||
--output "$WORK_DIR/maskAllValid.h5"
|
||||
|
||||
bash "$BRIDGE_RUNNER" python "$PATCHED_APP" "$CFG_PATH" --dir "$WORK_DIR" --start modify_network --end velocity
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <smallbaseline-config-wsl> <mintpy-work-dir-wsl>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CFG_PATH="$1"
|
||||
WORK_DIR="$2"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
UNIFIED_RUNNER="$SCRIPT_DIR/run_mintpy_unified_env_ubuntu2404.sh"
|
||||
PATCHED_APP="$SCRIPT_DIR/run_smallbaselineApp_patched.py"
|
||||
STRICT_MASK_BUILDER="$SCRIPT_DIR/create_mintpy_all_ifgram_mask.py"
|
||||
|
||||
echo "MintPy SBAS unified-env smoketest"
|
||||
echo "Config: $CFG_PATH"
|
||||
echo "Work dir: $WORK_DIR"
|
||||
|
||||
bash "$UNIFIED_RUNNER" python "$PATCHED_APP" "$CFG_PATH" --dir "$WORK_DIR" --dostep load_data
|
||||
|
||||
bash "$UNIFIED_RUNNER" python "$STRICT_MASK_BUILDER" \
|
||||
--ifgram-stack "$WORK_DIR/inputs/ifgramStack.h5" \
|
||||
--output "$WORK_DIR/maskAllValid.h5"
|
||||
|
||||
bash "$UNIFIED_RUNNER" python "$PATCHED_APP" "$CFG_PATH" --dir "$WORK_DIR" --start modify_network --end velocity
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <mintpy-command> [args...]" >&2
|
||||
echo "Example: $0 prep_isce.py -h" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
MINTPY_ENV="${MINTPY_ENV:-isce2_mintpy}"
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "MintPy command in unified env"
|
||||
echo "Conda: $CONDA_BIN"
|
||||
echo "Target env: $MINTPY_ENV"
|
||||
echo "Command: $*"
|
||||
|
||||
"$CONDA_BIN" run -n "$MINTPY_ENV" "$@"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <mintpy-command> [args...]" >&2
|
||||
echo "Example: $0 prep_isce.py -h" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONDA_BIN="${CONDA_BIN:-/home/administrator/miniconda3/bin/conda}"
|
||||
MINTPY_ENV="${MINTPY_ENV:-mintpy}"
|
||||
ISCE_SITE_PACKAGES="${ISCE_SITE_PACKAGES:-/home/administrator/miniconda3/envs/isce2/lib/python3.11/site-packages}"
|
||||
ISCE_PACKAGE_DIR="${ISCE_PACKAGE_DIR:-$ISCE_SITE_PACKAGES/isce}"
|
||||
ISCE_BRIDGE_DIR="${ISCE_BRIDGE_DIR:-$HOME/.cache/mintpy_isce_bridge}"
|
||||
|
||||
if [[ ! -x "$CONDA_BIN" ]]; then
|
||||
echo "Missing conda binary: $CONDA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ISCE_SITE_PACKAGES" ]]; then
|
||||
echo "Missing ISCE site-packages directory: $ISCE_SITE_PACKAGES" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ISCE_PACKAGE_DIR" ]]; then
|
||||
echo "Missing ISCE package directory: $ISCE_PACKAGE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ISCE_BRIDGE_DIR"
|
||||
ln -sfn "$ISCE_PACKAGE_DIR" "$ISCE_BRIDGE_DIR/isce"
|
||||
|
||||
# Bridge only the top-level ISCE package into the MintPy env.
|
||||
# The package itself extends sys.path to its internal components on import,
|
||||
# which avoids shadowing MintPy's own numpy/h5py stack with the isce2 env.
|
||||
export PYTHONPATH="$ISCE_BRIDGE_DIR${PYTHONPATH:+:$PYTHONPATH}"
|
||||
|
||||
echo "MintPy command bridge"
|
||||
echo "Conda: $CONDA_BIN"
|
||||
echo "MintPy env: $MINTPY_ENV"
|
||||
echo "ISCE bridge: $ISCE_BRIDGE_DIR -> $ISCE_PACKAGE_DIR"
|
||||
echo "Command: $*"
|
||||
|
||||
"$CONDA_BIN" run -n "$MINTPY_ENV" "$@"
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run MintPy smallbaselineApp with a local workaround for a single-pixel inversion bug."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
import mintpy.ifgram_inversion as ifgram_inversion
|
||||
from mintpy.cli.smallbaselineApp import main as mintpy_smallbaseline_main
|
||||
|
||||
|
||||
_ORIGINAL_ESTIMATE_TIMESERIES = ifgram_inversion.estimate_timeseries
|
||||
|
||||
|
||||
def _patched_estimate_timeseries(*args, **kwargs):
|
||||
ts, inv_quality, num_inv_obs = _ORIGINAL_ESTIMATE_TIMESERIES(*args, **kwargs)
|
||||
|
||||
# MintPy 1.6.2 may return a shape-(1,) inversion quality array for the
|
||||
# single-pixel partial-network branch, while the caller expects a scalar.
|
||||
if isinstance(inv_quality, np.ndarray) and inv_quality.size == 1:
|
||||
inv_quality = np.asarray(inv_quality).reshape(-1)[0].item()
|
||||
|
||||
if isinstance(num_inv_obs, np.ndarray) and num_inv_obs.size == 1:
|
||||
num_inv_obs = int(np.asarray(num_inv_obs).reshape(-1)[0])
|
||||
|
||||
return ts, inv_quality, num_inv_obs
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ifgram_inversion.estimate_timeseries = _patched_estimate_timeseries
|
||||
print("Applied local MintPy estimate_timeseries single-pixel fix.")
|
||||
return mintpy_smallbaseline_main(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,373 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user