Apply current workspace changes

This commit is contained in:
2026-04-22 01:52:21 +08:00
parent cf34b9a8ed
commit cb518b431f
449 changed files with 92252 additions and 321 deletions
@@ -0,0 +1,847 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import shutil
import stat
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterable, List
LT1_INPUT_GLOBS = ("LT1*.tar.gz", "LT1*.tiff")
PAIR_META_FILENAME = ".dinsar_pair.json"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Materialize a PyINT LT-1 workspace from an existing Task_xxx pair directory."
)
parser.add_argument("task_dir", help="Task directory containing master/ and slave/ subdirectories.")
parser.add_argument("--project-dir", required=True, help="Workspace directory for the generated PyINT project.")
parser.add_argument("--template-root", required=True, help="Directory where the generated template will be written.")
parser.add_argument("--output-dir", required=True, help="Directory where normalized native outputs will be copied.")
parser.add_argument("--pyint-home", required=True, help="PyINT repository root inside WSL.")
parser.add_argument("--pyint-app-script", required=True, help="pyintApp.py path inside WSL.")
parser.add_argument("--python", required=True, help="Python interpreter used to run PyINT inside WSL.")
parser.add_argument("--dem-root", required=True, help="DEMDIR root used by PyINT.")
parser.add_argument("--dem-mode", default="local_fabdem", help="DEM strategy used for this run.")
parser.add_argument("--fabdem-root", default="", help="Optional FABDEM tile root inside WSL.")
parser.add_argument("--prepared-dem-path", default="", help="Optional existing DEM path inside WSL.")
parser.add_argument("--opentopo-dem-type", default="SRTMGL1", help="DEM type when using OpenTopography.")
parser.add_argument("--opentopo-api-key", default="", help="Optional OpenTopography API key.")
parser.add_argument("--project-name", required=True, help="Unique PyINT project name for this run.")
parser.add_argument("--gamma-env-script", default="", help="Optional shell script used to expose GAMMA commands.")
parser.add_argument("--pair-key", default="", help="Pair key recorded into the run summary.")
parser.add_argument("--task-alias", default="", help="Task alias recorded into the run summary.")
parser.add_argument("--orbit-policy", default="require_txt", help="Orbit governance policy recorded into the run summary.")
parser.add_argument("--input-assets-dir", default="", help="Optional input_assets directory for this run.")
parser.add_argument("--input-assets-json", default="", help="Optional task_manifest.json path for this run.")
parser.add_argument("--master-date", default="", help="Master date in YYYYMMDD format.")
parser.add_argument("--slave-date", default="", help="Slave date in YYYYMMDD format.")
parser.add_argument("--time-baseline-days", type=int, default=0, help="Time baseline to record in ifgram_list.txt.")
parser.add_argument("--range-looks", type=int, default=2)
parser.add_argument("--azimuth-looks", type=int, default=2)
parser.add_argument("--parallel-workers", type=int, default=1)
parser.add_argument("--lt1-precise-orbit-enabled", default="true", help="Enable LT-1 precise orbit bridge.")
parser.add_argument("--lt1-precise-orbit-mode", default="replace", help="LT-1 precise orbit bridge mode.")
parser.add_argument("--lt1-precise-orbit-strict", default="true", help="Fail the run if precise orbit bridge fails.")
parser.add_argument(
"--lt1-precise-orbit-validate-with-orb-filt",
default="false",
help="Run ORB_filt_spline.py on a validation copy after rewriting state vectors.",
)
parser.add_argument("--lt1-precise-orbit-backup", default="true", help="Backup original .slc.par before rewrite.")
parser.add_argument("--lt1-precise-orbit-orb-filt-degree", type=int, default=5)
parser.add_argument("--unwrap", dest="unwrap", action="store_true")
parser.add_argument("--no-unwrap", dest="unwrap", action="store_false")
parser.add_argument("--geocode", dest="geocode", action="store_true")
parser.add_argument("--no-geocode", dest="geocode", action="store_false")
parser.add_argument("--force", action="store_true", help="Delete an existing run root before rebuilding it.")
parser.set_defaults(unwrap=True, geocode=True)
return parser.parse_args()
def normalize_date_text(value: Any) -> str:
text = str(value or "").strip()
digits = "".join(ch for ch in text if ch.isdigit())
if len(digits) >= 8 and digits.startswith("20"):
return digits[:8]
return ""
def normalize_bool_text(value: Any, default: bool = False) -> bool:
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return bool(value)
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def ensure_directory(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path
def safe_rmtree(path: Path) -> None:
if not path.exists():
return
resolved = path.resolve()
if len(resolved.parts) < 4:
raise RuntimeError(f"Refusing to remove an unsafe path: {resolved}")
shutil.rmtree(resolved)
def load_pair_meta(task_dir: Path) -> Dict[str, Any]:
path = task_dir / PAIR_META_FILENAME
if not path.is_file():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
def load_json_file(path: Path | None) -> Dict[str, Any]:
if path is None or not path.is_file():
return {}
try:
payload = json.loads(path.read_text(encoding="utf-8-sig"))
except Exception:
return {}
return payload if isinstance(payload, dict) else {}
def discover_lt1_archives(scene_dir: Path) -> List[Path]:
if not scene_dir.is_dir():
return []
items: List[Path] = []
for pattern in LT1_INPUT_GLOBS:
items.extend(path.resolve() for path in scene_dir.rglob(pattern) if path.is_file())
return sorted(set(items))
def infer_scene_date(paths: Iterable[Path]) -> str:
dates = {
normalize_date_text(path.name)
for path in paths
if normalize_date_text(path.name)
}
if len(dates) == 1:
return next(iter(dates))
return ""
def hardlink_or_copy(src: Path, dst: Path) -> str:
ensure_directory(dst.parent)
if dst.exists():
return "skipped"
try:
os.link(src, dst)
return "hardlinked"
except OSError:
pass
try:
dst.symlink_to(src)
return "symlinked"
except OSError:
pass
shutil.copy2(src, dst)
return "copied"
def collect_related_lt1_input_files(path: Path) -> List[Path]:
resolved = path.resolve()
if resolved.suffix.lower() != ".tiff":
return [resolved]
stem = resolved.stem
files = [
candidate.resolve()
for candidate in resolved.parent.iterdir()
if candidate.is_file() and (candidate.name == resolved.name or candidate.name.startswith(stem))
]
return sorted(set(files))
def write_text(path: Path, content: str) -> Path:
ensure_directory(path.parent)
path.write_text(content, encoding="utf-8")
return path
def inspect_prepared_dem_path(path_text: str) -> Dict[str, str]:
text = str(path_text or "").strip()
if not text:
return {
"path": "",
"kind": "",
"direct_dem_path": "",
"source_dem_path": "",
"source_dem_open_path": "",
}
path = Path(text)
try:
resolved_path = path.resolve()
except Exception:
resolved_path = path
gamma_par_path = Path(str(resolved_path) + ".par")
vrt_path = Path(str(resolved_path) + ".vrt")
xml_path = Path(str(resolved_path) + ".xml")
hdr_path = Path(str(resolved_path) + ".hdr")
if resolved_path.is_file() and gamma_par_path.is_file():
return {
"path": str(resolved_path),
"kind": "gamma_ready",
"direct_dem_path": str(resolved_path),
"source_dem_path": "",
"source_dem_open_path": "",
}
if resolved_path.is_file() and (vrt_path.is_file() or xml_path.is_file() or hdr_path.is_file()):
return {
"path": str(resolved_path),
"kind": "source_dem",
"direct_dem_path": "",
"source_dem_path": str(resolved_path),
"source_dem_open_path": str(vrt_path if vrt_path.is_file() else resolved_path),
}
if resolved_path.suffix.lower() == ".vrt" and resolved_path.is_file():
return {
"path": str(resolved_path),
"kind": "source_dem",
"direct_dem_path": "",
"source_dem_path": str(resolved_path),
"source_dem_open_path": str(resolved_path),
}
return {
"path": str(resolved_path),
"kind": "",
"direct_dem_path": "",
"source_dem_path": "",
"source_dem_open_path": "",
}
def build_template_text(
*,
project_name: str,
master_date: str,
range_looks: int,
azimuth_looks: int,
parallel_workers: int,
unwrap: bool,
geocode: bool,
dem_mode: str,
fabdem_root: str,
prepared_dem_path: str,
opentopo_dem_type: str,
opentopo_api_key: str,
) -> str:
prepared_dem = inspect_prepared_dem_path(prepared_dem_path) if dem_mode == "prepared_file" else {}
lines = [
f"# Auto-generated for {project_name}",
"satelite=LT",
f"masterDate={master_date}",
f"range_looks={int(range_looks)}",
f"azimuth_looks={int(azimuth_looks)}",
"download_data=0",
"raw2slc_all=1",
f"raw2slc_all_parallel={int(parallel_workers)}",
"extract_burst_all=0",
f"extract_all_parallel={int(parallel_workers)}",
"coreg_all=1",
f"coreg_all_parallel={int(parallel_workers)}",
"select_pairs=0",
"diff_all=1",
f"diff_all_parallel={int(parallel_workers)}",
"pot_all=0",
f"pot_all_parallel={int(parallel_workers)}",
f"unwrap_all={1 if unwrap else 0}",
f"unwrap_all_parallel={int(parallel_workers)}",
"atmcor_all=0",
f"atmcor_all_parallel={int(parallel_workers)}",
f"geocode_all={1 if geocode else 0}",
f"geocode_all_parallel={int(parallel_workers)}",
"gacos_correction=0",
"load_data=0",
"geocode_products=hyp3,licsbas",
]
if dem_mode == "local_fabdem" and fabdem_root:
lines.append(f"fabdem_dir={fabdem_root}")
else:
lines.append("fabdem_dir=-")
if dem_mode == "prepared_file" and prepared_dem.get("kind") == "gamma_ready":
lines.append(f"DEM={prepared_dem['direct_dem_path']}")
if dem_mode == "prepared_file" and prepared_dem.get("kind") == "source_dem":
lines.append(f"prepared_dem_source={prepared_dem['source_dem_path']}")
else:
lines.append("prepared_dem_source=-")
if dem_mode == "opentopo":
lines.append(f"opentopo_dem_type={opentopo_dem_type or 'SRTMGL1'}")
lines.append(f"opentopo_api_key={opentopo_api_key or '-'}")
else:
lines.append("opentopo_dem_type=-")
lines.append("opentopo_api_key=-")
return "\n".join(lines) + "\n"
def write_ifgram_list(path: Path, master_date: str, slave_date: str, time_baseline_days: int) -> Path:
content = f"{master_date}-{slave_date} {int(time_baseline_days)} 0.0\n"
return write_text(path, content)
def write_wrapper_scripts(
*,
wrappers_dir: Path,
pyint_home: Path,
python_cmd: str,
gamma_env_script: str,
) -> List[Path]:
scripts_dir = pyint_home / "pyint"
if not scripts_dir.is_dir():
raise FileNotFoundError(f"PyINT scripts directory not found: {scripts_dir}")
ensure_directory(wrappers_dir)
created: List[Path] = []
pyint_scripts = sorted(path for path in scripts_dir.glob("*.py") if path.is_file())
for script_path in pyint_scripts:
wrapper_path = wrappers_dir / script_path.name
lines = [
"#!/usr/bin/env bash",
"set -e",
]
if gamma_env_script:
lines.append(f". '{gamma_env_script}' >/dev/null 2>&1")
lines.extend(
[
f"export PATH='{wrappers_dir}':'{scripts_dir}':\"$PATH\"",
f"export PYTHONPATH='{pyint_home}':\"${{PYTHONPATH:-}}\"",
f"exec '{python_cmd}' '{script_path}' \"$@\"",
"",
]
)
write_text(wrapper_path, "\n".join(lines))
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC)
created.append(wrapper_path)
return created
def run_logged(command: List[str], *, env: Dict[str, str], cwd: Path, stdout_path: Path, stderr_path: Path) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
command,
cwd=str(cwd),
env=env,
text=True,
capture_output=True,
check=False,
)
write_text(stdout_path, result.stdout or "")
write_text(stderr_path, result.stderr or "")
return result
def require_task_layout(task_dir: Path) -> None:
missing = [name for name in ("master", "slave") if not (task_dir / name).is_dir()]
if missing:
raise FileNotFoundError(f"Task directory is missing required subdirectories: {', '.join(missing)}")
def collect_expected_outputs(project_dir: Path, pair_name: str, range_looks: int) -> Dict[str, str]:
pair_dir = project_dir / "ifgrams" / pair_name
look_text = f"{int(range_looks)}rlks"
return {
"pair_dir": str(pair_dir),
"diff_filt": str(pair_dir / f"{pair_name}_{look_text}.diff_filt"),
"coh": str(pair_dir / f"{pair_name}_{look_text}.diff_filt.cor"),
"unw": str(pair_dir / f"{pair_name}_{look_text}.diff_filt.unw"),
"geo_unw": str(pair_dir / f"geo_{pair_name}_{look_text}.diff_filt.unw"),
"geo_los": str(pair_dir / f"geo_{pair_name}_{look_text}.los_disp"),
}
def assert_required_outputs(outputs: Dict[str, str], *, unwrap: bool, geocode: bool) -> None:
required = ["pair_dir", "diff_filt", "coh"]
if unwrap:
required.append("unw")
if geocode:
required.append("geo_unw")
missing = [name for name in required if not Path(outputs[name]).exists()]
if missing:
raise RuntimeError(f"PyINT run finished but required outputs are missing: {', '.join(missing)}")
def is_binary_all_zero(path: Path, *, chunk_size: int = 1024 * 1024) -> bool:
if not path.is_file():
return False
with path.open("rb") as handle:
while True:
chunk = handle.read(chunk_size)
if not chunk:
return True
if any(chunk):
return False
def collect_output_sanity_checks(
outputs: Dict[str, str],
*,
unwrap: bool,
geocode: bool,
) -> List[Dict[str, Any]]:
targets = [
("diff_filt", "wrapped differential interferogram"),
("coh", "coherence"),
]
if unwrap:
targets.append(("unw", "unwrapped interferogram"))
if geocode:
targets.extend(
[
("geo_unw", "geocoded unwrapped interferogram"),
("geo_los", "geocoded LOS displacement"),
]
)
checks: List[Dict[str, Any]] = []
for name, label in targets:
path = Path(outputs[name])
exists = path.exists()
size_bytes = path.stat().st_size if exists else 0
all_zero = exists and is_binary_all_zero(path)
checks.append(
{
"name": name,
"label": label,
"path": str(path),
"exists": exists,
"size_bytes": int(size_bytes),
"all_zero": bool(all_zero),
"ok": bool(exists and size_bytes > 0 and not all_zero),
}
)
return checks
def assert_output_sanity(checks: List[Dict[str, Any]]) -> None:
failed = [item for item in checks if not item.get("ok")]
if not failed:
return
detail = ", ".join(f"{item['name']}={item['path']}" for item in failed)
raise RuntimeError(f"PyINT run produced invalid all-zero binary outputs: {detail}")
def collect_stage_error_logs(project_dir: Path) -> Dict[str, str]:
logs: Dict[str, str] = {}
for filename in (
"coreg_gamma_all.err",
"diff_gamma_all.err",
"unwrap_gamma_all.err",
"geocode_gamma_all.err",
):
path = project_dir / filename
if path.is_file():
logs[filename] = str(path)
return logs
def copy_native_outputs(
*,
project_dir: Path,
output_dir: Path,
pair_name: str,
template_path: Path,
ifgram_list_path: Path,
stdout_path: Path,
stderr_path: Path,
) -> Dict[str, str]:
ensure_directory(output_dir)
native_pair_dir = project_dir / "ifgrams" / pair_name
target_pair_dir = output_dir / "ifgrams" / pair_name
if native_pair_dir.is_dir():
shutil.copytree(native_pair_dir, target_pair_dir, dirs_exist_ok=True)
target_template = output_dir / template_path.name
shutil.copy2(template_path, target_template)
target_ifgram_list = output_dir / ifgram_list_path.name
shutil.copy2(ifgram_list_path, target_ifgram_list)
target_stdout = output_dir / stdout_path.name
target_stderr = output_dir / stderr_path.name
shutil.copy2(stdout_path, target_stdout)
shutil.copy2(stderr_path, target_stderr)
return {
"pair_dir": str(target_pair_dir),
"template_path": str(target_template),
"ifgram_list_path": str(target_ifgram_list),
"stdout_path": str(target_stdout),
"stderr_path": str(target_stderr),
}
def collect_orbit_bridge_summaries(project_dir: Path) -> List[Dict[str, Any]]:
summaries: List[Dict[str, Any]] = []
slc_root = project_dir / "SLC"
if not slc_root.is_dir():
return summaries
for summary_path in sorted(slc_root.glob("*/orbit_bridge_summary.json")):
payload = load_json_file(summary_path)
operations = payload.get("operations") if isinstance(payload.get("operations"), list) else []
failed_operations = [item for item in operations if not item.get("ok")]
summaries.append(
{
"path": str(summary_path),
"date_dir": summary_path.parent.name,
"ok": bool(payload.get("ok", not failed_operations)),
"operation_count": len(operations),
"failed_operation_count": len(failed_operations),
"operations": operations,
"payload": payload,
}
)
return summaries
def copy_orbit_bridge_summaries(summaries: List[Dict[str, Any]], output_dir: Path) -> Dict[str, str]:
if not summaries:
return {}
target_dir = ensure_directory(output_dir / "orbit_bridge")
copied: Dict[str, str] = {}
for item in summaries:
source_path = Path(item["path"])
target_path = target_dir / f"{item['date_dir']}_orbit_bridge_summary.json"
shutil.copy2(source_path, target_path)
copied[item["date_dir"]] = str(target_path)
return copied
def assert_orbit_bridge_ok(
*,
enabled: bool,
strict: bool,
summaries: List[Dict[str, Any]],
expected_dates: Iterable[str],
) -> None:
if not enabled:
return
if not strict:
return
expected = {str(item).strip() for item in expected_dates if str(item).strip()}
found = {str(item.get("date_dir") or "").strip() for item in summaries if str(item.get("date_dir") or "").strip()}
missing = sorted(expected - found)
if missing:
raise RuntimeError(f"LT-1 precise orbit bridge summary is missing for: {', '.join(missing)}")
failed = [item for item in summaries if not item.get("ok")]
if failed:
failed_dates = ", ".join(sorted(str(item.get("date_dir") or "") for item in failed))
raise RuntimeError(f"LT-1 precise orbit bridge reported failures for: {failed_dates}")
def main() -> int:
args = parse_args()
task_dir = Path(args.task_dir).resolve()
project_dir = Path(args.project_dir).resolve()
run_root = project_dir.parent
template_root = Path(args.template_root).resolve()
output_dir = Path(args.output_dir).resolve()
pyint_home = Path(args.pyint_home).resolve()
pyint_app_script = Path(args.pyint_app_script).resolve()
dem_root = Path(args.dem_root).resolve()
input_assets_dir = Path(args.input_assets_dir).resolve() if args.input_assets_dir else None
input_assets_json = Path(args.input_assets_json).resolve() if args.input_assets_json else None
input_assets_payload = load_json_file(input_assets_json)
precise_orbit_enabled = normalize_bool_text(args.lt1_precise_orbit_enabled, True)
precise_orbit_strict = normalize_bool_text(args.lt1_precise_orbit_strict, True)
precise_orbit_validate_with_orb_filt = normalize_bool_text(
args.lt1_precise_orbit_validate_with_orb_filt,
False,
)
precise_orbit_backup = normalize_bool_text(args.lt1_precise_orbit_backup, True)
precise_orbit_mode = str(args.lt1_precise_orbit_mode or "replace").strip().lower() or "replace"
precise_orbit_helper = (Path(__file__).resolve().parent / "apply_lt1_precise_orbit.py").resolve()
dem_mode = str(args.dem_mode or "local_fabdem").strip().lower() or "local_fabdem"
prepared_dem_info = inspect_prepared_dem_path(args.prepared_dem_path) if dem_mode == "prepared_file" else {}
require_task_layout(task_dir)
if not pyint_app_script.is_file():
raise FileNotFoundError(f"pyintApp.py not found: {pyint_app_script}")
if precise_orbit_enabled and not precise_orbit_helper.is_file():
raise FileNotFoundError(f"Precise orbit bridge helper not found: {precise_orbit_helper}")
if precise_orbit_enabled and input_assets_json is None:
raise RuntimeError("LT-1 precise orbit bridge requires --input-assets-json.")
if dem_mode == "prepared_file" and not prepared_dem_info.get("kind"):
raise RuntimeError(
"Prepared DEM mode requires either a Gamma DEM with .par, "
"or a source DEM with .xml/.hdr/.vrt sidecars."
)
if args.force:
safe_rmtree(run_root)
safe_rmtree(template_root)
safe_rmtree(output_dir)
if run_root.exists():
raise RuntimeError(f"PyINT run root already exists, rerun with --force: {run_root}")
pair_meta = load_pair_meta(task_dir)
master_archives = discover_lt1_archives(task_dir / "master")
slave_archives = discover_lt1_archives(task_dir / "slave")
if not master_archives:
raise FileNotFoundError(f"No LT1 archives found under: {task_dir / 'master'}")
if not slave_archives:
raise FileNotFoundError(f"No LT1 archives found under: {task_dir / 'slave'}")
master_date = normalize_date_text(args.master_date) or normalize_date_text(pair_meta.get("master_imaging_date")) or infer_scene_date(master_archives)
slave_date = normalize_date_text(args.slave_date) or normalize_date_text(pair_meta.get("slave_imaging_date")) or infer_scene_date(slave_archives)
if not master_date or not slave_date:
raise RuntimeError("Unable to determine master/slave dates from pair metadata or archive names.")
pair_name = f"{master_date}-{slave_date}"
task_alias = str(args.task_alias or pair_meta.get("task_alias") or task_dir.name).strip() or task_dir.name
pair_key = str(args.pair_key or pair_meta.get("pair_key") or "").strip()
time_baseline_days = int(args.time_baseline_days or pair_meta.get("time_baseline_days") or 0)
ensure_directory(run_root)
ensure_directory(template_root)
ensure_directory(output_dir)
ensure_directory(dem_root)
pyint_scripts_dir = pyint_home / "pyint"
wrappers_dir = ensure_directory(run_root / "wrappers")
write_wrapper_scripts(
wrappers_dir=wrappers_dir,
pyint_home=pyint_home,
python_cmd=args.python,
gamma_env_script=args.gamma_env_script,
)
template_path = write_text(
template_root / f"{args.project_name}.template",
build_template_text(
project_name=args.project_name,
master_date=master_date,
range_looks=args.range_looks,
azimuth_looks=args.azimuth_looks,
parallel_workers=args.parallel_workers,
unwrap=bool(args.unwrap),
geocode=bool(args.geocode),
dem_mode=dem_mode,
fabdem_root=str(args.fabdem_root or "").strip(),
prepared_dem_path=str(args.prepared_dem_path or "").strip(),
opentopo_dem_type=str(args.opentopo_dem_type or "SRTMGL1").strip(),
opentopo_api_key=str(args.opentopo_api_key or "").strip(),
),
)
scratch_root = ensure_directory(project_dir.parent)
archive_materialization: List[Dict[str, str]] = []
env = os.environ.copy()
env.update(
{
"SCRATCHDIR": str(scratch_root),
"TEMPLATEDIR": str(template_root),
"DEMDIR": str(dem_root),
"PATH": f"{wrappers_dir}:{pyint_scripts_dir}:{env.get('PATH', '')}",
"PYTHONPATH": f"{pyint_home}:{env.get('PYTHONPATH', '')}",
"PYINT_LT1_PRECISE_ORBIT_ENABLED": "true" if precise_orbit_enabled else "false",
"PYINT_LT1_PRECISE_ORBIT_MODE": precise_orbit_mode,
"PYINT_LT1_PRECISE_ORBIT_STRICT": "true" if precise_orbit_strict else "false",
"PYINT_LT1_PRECISE_ORBIT_VALIDATE_WITH_ORB_FILT": "true" if precise_orbit_validate_with_orb_filt else "false",
"PYINT_LT1_PRECISE_ORBIT_BACKUP": "true" if precise_orbit_backup else "false",
"PYINT_LT1_PRECISE_ORBIT_ORB_FILT_DEGREE": str(int(args.lt1_precise_orbit_orb_filt_degree)),
"PYINT_LT1_PRECISE_ORBIT_HELPER": str(precise_orbit_helper),
"PYINT_LT1_PRECISE_ORBIT_MANIFEST": str(input_assets_json) if input_assets_json else "",
}
)
generate_stdout = run_root / "pyint_generate.stdout.log"
generate_stderr = run_root / "pyint_generate.stderr.log"
generate_result = run_logged(
[str(wrappers_dir / "pyintApp.py"), "-g", args.project_name],
env=env,
cwd=scratch_root,
stdout_path=generate_stdout,
stderr_path=generate_stderr,
)
if generate_result.returncode != 0:
raise RuntimeError(
f"pyintApp.py -g failed with rc={generate_result.returncode}: "
f"{(generate_result.stderr or generate_result.stdout or '').strip()}"
)
pyint_project_dir = project_dir
download_dir = ensure_directory(pyint_project_dir / "DOWNLOAD")
ifgram_list_path = write_ifgram_list(pyint_project_dir / "ifgram_list.txt", master_date, slave_date, time_baseline_days)
for role, archives in (("master", master_archives), ("slave", slave_archives)):
for src_path in archives:
related_files = collect_related_lt1_input_files(src_path)
for related_path in related_files:
target_path = download_dir / related_path.name
op = hardlink_or_copy(related_path, target_path)
archive_materialization.append(
{
"role": role,
"source": str(related_path),
"group_source": str(src_path),
"target": str(target_path),
"operation": op,
}
)
run_stdout = run_root / "pyint.stdout.log"
run_stderr = run_root / "pyint.stderr.log"
run_started_at = datetime.utcnow().isoformat(timespec="seconds") + "Z"
run_result = run_logged(
[str(wrappers_dir / "pyintApp.py"), args.project_name],
env=env,
cwd=scratch_root,
stdout_path=run_stdout,
stderr_path=run_stderr,
)
if run_result.returncode != 0:
stage_error_logs = collect_stage_error_logs(pyint_project_dir)
detail_text = (run_result.stderr or run_result.stdout or "").strip()
if stage_error_logs:
log_text = ", ".join(f"{name}={path}" for name, path in stage_error_logs.items())
detail_text = f"{detail_text}\nStage logs: {log_text}" if detail_text else f"Stage logs: {log_text}"
raise RuntimeError(
f"pyintApp.py failed with rc={run_result.returncode}: "
f"{detail_text}"
)
orbit_bridge_summaries = collect_orbit_bridge_summaries(pyint_project_dir)
assert_orbit_bridge_ok(
enabled=precise_orbit_enabled,
strict=precise_orbit_strict,
summaries=orbit_bridge_summaries,
expected_dates=(master_date, slave_date),
)
outputs = collect_expected_outputs(pyint_project_dir, pair_name, args.range_looks)
assert_required_outputs(outputs, unwrap=bool(args.unwrap), geocode=bool(args.geocode))
output_sanity_checks = collect_output_sanity_checks(
outputs,
unwrap=bool(args.unwrap),
geocode=bool(args.geocode),
)
assert_output_sanity(output_sanity_checks)
stage_error_logs = collect_stage_error_logs(pyint_project_dir)
copied_paths = copy_native_outputs(
project_dir=pyint_project_dir,
output_dir=output_dir,
pair_name=pair_name,
template_path=template_path,
ifgram_list_path=ifgram_list_path,
stdout_path=run_stdout,
stderr_path=run_stderr,
)
copied_orbit_bridge_paths = copy_orbit_bridge_summaries(orbit_bridge_summaries, output_dir)
summary = {
"ok": True,
"task_dir": str(task_dir),
"task_alias": task_alias,
"pair_key": pair_key,
"project_name": args.project_name,
"project_dir": str(pyint_project_dir),
"run_root": str(run_root),
"template_root": str(template_root),
"output_dir": str(output_dir),
"pyint_home": str(pyint_home),
"pyint_app_script": str(pyint_app_script),
"gamma_env_script": args.gamma_env_script,
"dem": {
"mode": dem_mode,
"dem_root": str(dem_root),
"fabdem_root": str(args.fabdem_root or "").strip(),
"prepared_dem_path": str(args.prepared_dem_path or "").strip(),
"prepared_dem_kind": str(prepared_dem_info.get("kind") or ""),
"prepared_dem_direct_path": str(prepared_dem_info.get("direct_dem_path") or ""),
"prepared_dem_source_path": str(prepared_dem_info.get("source_dem_path") or ""),
"prepared_dem_open_path": str(prepared_dem_info.get("source_dem_open_path") or ""),
"opentopo_dem_type": str(args.opentopo_dem_type or "SRTMGL1").strip(),
"opentopo_api_key_configured": bool(str(args.opentopo_api_key or "").strip()),
},
"orbit_policy": str(args.orbit_policy or "require_txt").strip().lower(),
"precise_orbit_bridge": {
"enabled": precise_orbit_enabled,
"mode": precise_orbit_mode,
"strict": precise_orbit_strict,
"validate_with_orb_filt": precise_orbit_validate_with_orb_filt,
"backup": precise_orbit_backup,
"orb_filt_degree": int(args.lt1_precise_orbit_orb_filt_degree),
"helper_path": str(precise_orbit_helper),
"manifest_json": str(input_assets_json) if input_assets_json else "",
"summaries": [
{
"path": item["path"],
"date_dir": item["date_dir"],
"ok": item["ok"],
"operation_count": item["operation_count"],
"failed_operation_count": item["failed_operation_count"],
"copied_summary_path": copied_orbit_bridge_paths.get(item["date_dir"], ""),
}
for item in orbit_bridge_summaries
],
},
"input_assets_dir": str(input_assets_dir) if input_assets_dir else "",
"input_assets_json": str(input_assets_json) if input_assets_json else "",
"input_assets": input_assets_payload,
"master_date": master_date,
"slave_date": slave_date,
"pair_name": pair_name,
"time_baseline_days": time_baseline_days,
"range_looks": int(args.range_looks),
"azimuth_looks": int(args.azimuth_looks),
"parallel_workers": int(args.parallel_workers),
"unwrap": bool(args.unwrap),
"geocode": bool(args.geocode),
"archives": {
"master": [str(path) for path in master_archives],
"slave": [str(path) for path in slave_archives],
},
"archive_materialization": archive_materialization,
"workspace_outputs": outputs,
"output_sanity_checks": output_sanity_checks,
"copied_outputs": copied_paths,
"copied_orbit_bridge_paths": copied_orbit_bridge_paths,
"logs": {
"generate_stdout": str(generate_stdout),
"generate_stderr": str(generate_stderr),
"run_stdout": str(run_stdout),
"run_stderr": str(run_stderr),
"stage_error_logs": stage_error_logs,
},
"started_at": run_started_at,
"finished_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
}
summary_path = output_dir / "pyint_run_summary.json"
write_text(summary_path, json.dumps(summary, ensure_ascii=False, indent=2) + "\n")
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(str(exc), file=sys.stderr)
raise