diff --git a/.env.example b/.env.example index 0c5ff04..5c91a73 100644 --- a/.env.example +++ b/.env.example @@ -177,9 +177,26 @@ PYINT_DEM_MODE=local_fabdem PYINT_FABDEM_ROOT= # When PYINT_DEM_MODE=prepared_file, point this to the same prepared WGS84 DEM. PYINT_PREPARED_DEM_PATH= +# 0 means do not derive looks from a target output grid. +PYINT_DEFAULT_TARGET_GRID_SIZE_M=0 +# Source DEM resolution recorded in PyINT/Gamma run metadata. +PYINT_DEM_RESOLUTION_M=30.0 PYINT_OPENTOPO_DEM_TYPE=SRTMGL1 PYINT_OPENTOPO_API_KEY= PYINT_DEM_STRICT=true +# Gamma unwrap support and quality support reporting are intentionally separate. +PYINT_UNWRAP_COH_THRESHOLD=0.05 +PYINT_PRODUCT_COH_THRESHOLD=0.20 +PYINT_GAMMA_NODATA_VALUE=-9999.0 +PYINT_GEO_INTERP=1 +PYINT_ATMCOR_ENABLED=false +PYINT_ATMCOR_USE_FOR_DISP=false +PYINT_REFLATTEN_ENABLED=true +PYINT_REFLATTEN_MODEL=plane +PYINT_REFLATTEN_COH_THRESHOLD=0.70 +PYINT_REFLATTEN_FALLBACK_COH_THRESHOLD=0.20 +PYINT_REFLATTEN_RANGE_STEP=32 +PYINT_REFLATTEN_AZIMUTH_STEP=32 PYINT_ORBIT_POLICY=require_txt PYINT_ORBIT_POOL_TXT=D:\orbit_pools\envi PYINT_RECORD_INPUT_ASSETS=true diff --git a/backend/app/config.py b/backend/app/config.py index 227a90d..37d0c9f 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -265,9 +265,26 @@ class Settings(BaseSettings): PYINT_DEM_MODE: str = "local_fabdem" PYINT_FABDEM_ROOT: str = "" PYINT_PREPARED_DEM_PATH: str = "" + PYINT_DEM_RESOLUTION_M: float = 30.0 PYINT_OPENTOPO_DEM_TYPE: str = "SRTMGL1" PYINT_OPENTOPO_API_KEY: str = "" PYINT_DEM_STRICT: bool = True + PYINT_UNWRAP_COH_THRESHOLD: float = 0.05 + PYINT_PRODUCT_COH_THRESHOLD: float = 0.20 + PYINT_REFERENCE_MODE: str = "none" + PYINT_REFERENCE_COH_THRESHOLD: float = 0.30 + PYINT_DERAMP_MODE: str = "none" + PYINT_DERAMP_COH_THRESHOLD: float = 0.30 + PYINT_GAMMA_NODATA_VALUE: float = -9999.0 + PYINT_GEO_INTERP: str = "1" + PYINT_ATMCOR_ENABLED: bool = False + PYINT_ATMCOR_USE_FOR_DISP: bool = False + PYINT_REFLATTEN_ENABLED: bool = True + PYINT_REFLATTEN_MODEL: str = "plane" + PYINT_REFLATTEN_COH_THRESHOLD: float = 0.70 + PYINT_REFLATTEN_FALLBACK_COH_THRESHOLD: float = 0.20 + PYINT_REFLATTEN_RANGE_STEP: int = 32 + PYINT_REFLATTEN_AZIMUTH_STEP: int = 32 PYINT_ORBIT_POLICY: str = "require_txt" PYINT_ORBIT_POOL_TXT: str = "" PYINT_RECORD_INPUT_ASSETS: bool = True @@ -491,6 +508,70 @@ class Settings(BaseSettings): if pyint_dem_mode not in {"local_fabdem", "opentopo", "prepared_file"}: pyint_dem_mode = "local_fabdem" object.__setattr__(self, "PYINT_DEM_MODE", pyint_dem_mode) + object.__setattr__(self, "PYINT_DEM_RESOLUTION_M", max(0.1, float(self.PYINT_DEM_RESOLUTION_M or 30.0))) + object.__setattr__( + self, + "PYINT_UNWRAP_COH_THRESHOLD", + min(1.0, max(0.0, float(self.PYINT_UNWRAP_COH_THRESHOLD or 0.05))), + ) + object.__setattr__( + self, + "PYINT_PRODUCT_COH_THRESHOLD", + min(1.0, max(0.0, float(self.PYINT_PRODUCT_COH_THRESHOLD or 0.20))), + ) + pyint_reference_mode = str(self.PYINT_REFERENCE_MODE or "none").strip().lower() or "none" + if pyint_reference_mode not in {"none", "coh_median"}: + pyint_reference_mode = "none" + object.__setattr__(self, "PYINT_REFERENCE_MODE", pyint_reference_mode) + object.__setattr__( + self, + "PYINT_REFERENCE_COH_THRESHOLD", + min(1.0, max(0.0, float(self.PYINT_REFERENCE_COH_THRESHOLD or 0.30))), + ) + pyint_deramp_mode = str(self.PYINT_DERAMP_MODE or "none").strip().lower() or "none" + if pyint_deramp_mode not in {"none", "plane"}: + pyint_deramp_mode = "none" + object.__setattr__(self, "PYINT_DERAMP_MODE", pyint_deramp_mode) + object.__setattr__( + self, + "PYINT_DERAMP_COH_THRESHOLD", + min(1.0, max(0.0, float(self.PYINT_DERAMP_COH_THRESHOLD or 0.30))), + ) + object.__setattr__( + self, + "PYINT_GAMMA_NODATA_VALUE", + float(self.PYINT_GAMMA_NODATA_VALUE if self.PYINT_GAMMA_NODATA_VALUE is not None else -9999.0), + ) + pyint_geo_interp = str(self.PYINT_GEO_INTERP or "0").strip() + if pyint_geo_interp not in {"0", "1"}: + pyint_geo_interp = "1" + object.__setattr__(self, "PYINT_GEO_INTERP", pyint_geo_interp) + pyint_reflatten_model = str(self.PYINT_REFLATTEN_MODEL or "plane").strip().lower() or "plane" + if pyint_reflatten_model in {"linear"}: + pyint_reflatten_model = "plane" + if pyint_reflatten_model not in {"plane", "quadratic"}: + pyint_reflatten_model = "plane" + object.__setattr__(self, "PYINT_REFLATTEN_MODEL", pyint_reflatten_model) + object.__setattr__( + self, + "PYINT_REFLATTEN_COH_THRESHOLD", + min(1.0, max(0.0, float(self.PYINT_REFLATTEN_COH_THRESHOLD or 0.70))), + ) + object.__setattr__( + self, + "PYINT_REFLATTEN_FALLBACK_COH_THRESHOLD", + min(1.0, max(0.0, float(self.PYINT_REFLATTEN_FALLBACK_COH_THRESHOLD or 0.20))), + ) + object.__setattr__( + self, + "PYINT_REFLATTEN_RANGE_STEP", + max(1, int(self.PYINT_REFLATTEN_RANGE_STEP or 32)), + ) + object.__setattr__( + self, + "PYINT_REFLATTEN_AZIMUTH_STEP", + max(1, int(self.PYINT_REFLATTEN_AZIMUTH_STEP or 32)), + ) if not self.PYINT_OPENTOPO_DEM_TYPE: object.__setattr__(self, "PYINT_OPENTOPO_DEM_TYPE", "SRTMGL1") pyint_orbit_policy = str(self.PYINT_ORBIT_POLICY or "require_txt").strip().lower() or "require_txt" diff --git a/backend/app/dinsar_engines/pyint_engine.py b/backend/app/dinsar_engines/pyint_engine.py index 61320bc..5aea18d 100644 --- a/backend/app/dinsar_engines/pyint_engine.py +++ b/backend/app/dinsar_engines/pyint_engine.py @@ -8,7 +8,9 @@ from pathlib import Path from typing import Any, Dict, List from ..config import get_env_text, read_bool_env, settings +from ..services.dinsar_completion_files import repair_managed_completion_files from ..services.dinsar_naming import write_run_metadata +from ..services.isce2_result_validator import validate_isce2_result_files from ..services.pyint_input_assets_service import ( get_pyint_dem_summary, get_pyint_orbit_context, @@ -17,11 +19,33 @@ from ..services.pyint_input_assets_service import ( ) from ..services.pyint_service import ( DEFAULT_AZIMUTH_LOOKS, + DEFAULT_DEM_RESOLUTION_M, + DEFAULT_DERAMP_COH_THRESHOLD, + DEFAULT_DERAMP_MODE, + DEFAULT_ATMCOR_ENABLED, + DEFAULT_ATMCOR_USE_FOR_DISP, + DEFAULT_GEO_INTERP, DEFAULT_PARALLEL_WORKERS, + DEFAULT_PRODUCT_COH_THRESHOLD, DEFAULT_RANGE_LOOKS, + DEFAULT_REFLATTEN_AZIMUTH_STEP, + DEFAULT_REFLATTEN_COH_THRESHOLD, + DEFAULT_REFLATTEN_ENABLED, + DEFAULT_REFLATTEN_FALLBACK_COH_THRESHOLD, + DEFAULT_REFLATTEN_MODEL, + DEFAULT_REFLATTEN_RANGE_STEP, + DEFAULT_REFERENCE_COH_THRESHOLD, + DEFAULT_REFERENCE_MODE, + DEFAULT_TARGET_GRID_SIZE_M, + DEFAULT_UNWRAP_COH_THRESHOLD, MAX_LOOKS, MAX_PARALLEL_WORKERS, + REFLATTEN_MODEL_CHOICES, + TARGET_GRID_SIZE_MAX_M, + TARGET_GRID_SIZE_MIN_M, build_project_name, + calculate_dem_oversampling, + calculate_looks_from_task_dir, check_pyint_environment, infer_scene_date_from_archives, infer_task_identity, @@ -31,10 +55,11 @@ from ..services.pyint_service import ( to_wsl_path, validate_pyint_root_dir, ) -from ..services.wsl_service import run_wsl_command +from ..services.wsl_service import run_wsl_command_stream from .base import DinsarEngine, EngineAvailability, EngineProfile, RunRequest, RunResult RERUN_MODE_UNFINISHED_ONLY = "unfinished_only" +DEFAULT_COHERENCE_MASK_THRESHOLD = DEFAULT_PRODUCT_COH_THRESHOLD def _read_env(name: str, default: str = "") -> str: @@ -121,6 +146,85 @@ class PyintEngine(DinsarEngine): def _dem_mode(self) -> str: return str(getattr(settings, "PYINT_DEM_MODE", "local_fabdem") or "local_fabdem").strip().lower() + @property + def _dem_resolution_m(self) -> float: + return max(0.1, float(getattr(settings, "PYINT_DEM_RESOLUTION_M", DEFAULT_DEM_RESOLUTION_M) or DEFAULT_DEM_RESOLUTION_M)) + + @property + def _default_unwrap_coh_threshold(self) -> float: + return float(getattr(settings, "PYINT_UNWRAP_COH_THRESHOLD", DEFAULT_UNWRAP_COH_THRESHOLD) or DEFAULT_UNWRAP_COH_THRESHOLD) + + @property + def _default_product_coh_threshold(self) -> float: + return float(getattr(settings, "PYINT_PRODUCT_COH_THRESHOLD", DEFAULT_PRODUCT_COH_THRESHOLD) or DEFAULT_PRODUCT_COH_THRESHOLD) + + @property + def _default_reference_mode(self) -> str: + return str(getattr(settings, "PYINT_REFERENCE_MODE", DEFAULT_REFERENCE_MODE) or DEFAULT_REFERENCE_MODE).strip().lower() + + @property + def _default_reference_coh_threshold(self) -> float: + return float(getattr(settings, "PYINT_REFERENCE_COH_THRESHOLD", DEFAULT_REFERENCE_COH_THRESHOLD) or DEFAULT_REFERENCE_COH_THRESHOLD) + + @property + def _default_deramp_mode(self) -> str: + return str(getattr(settings, "PYINT_DERAMP_MODE", DEFAULT_DERAMP_MODE) or DEFAULT_DERAMP_MODE).strip().lower() + + @property + def _default_deramp_coh_threshold(self) -> float: + return float(getattr(settings, "PYINT_DERAMP_COH_THRESHOLD", DEFAULT_DERAMP_COH_THRESHOLD) or DEFAULT_DERAMP_COH_THRESHOLD) + + @property + def _gamma_nodata_value(self) -> float: + return float(getattr(settings, "PYINT_GAMMA_NODATA_VALUE", -9999.0) if getattr(settings, "PYINT_GAMMA_NODATA_VALUE", None) is not None else -9999.0) + + @property + def _geo_interp(self) -> str: + value = str(getattr(settings, "PYINT_GEO_INTERP", DEFAULT_GEO_INTERP) or DEFAULT_GEO_INTERP).strip() + return value if value in {"0", "1"} else DEFAULT_GEO_INTERP + + @property + def _atmcor_enabled(self) -> bool: + return bool(getattr(settings, "PYINT_ATMCOR_ENABLED", DEFAULT_ATMCOR_ENABLED)) + + @property + def _atmcor_use_for_disp(self) -> bool: + return bool(getattr(settings, "PYINT_ATMCOR_USE_FOR_DISP", DEFAULT_ATMCOR_USE_FOR_DISP)) + + @property + def _reflatten_enabled(self) -> bool: + return bool(getattr(settings, "PYINT_REFLATTEN_ENABLED", DEFAULT_REFLATTEN_ENABLED)) + + @property + def _reflatten_model(self) -> str: + value = str(getattr(settings, "PYINT_REFLATTEN_MODEL", DEFAULT_REFLATTEN_MODEL) or DEFAULT_REFLATTEN_MODEL).strip().lower() + if value == "linear": + value = "plane" + return value if value in REFLATTEN_MODEL_CHOICES else DEFAULT_REFLATTEN_MODEL + + @property + def _reflatten_coh_threshold(self) -> float: + return float(getattr(settings, "PYINT_REFLATTEN_COH_THRESHOLD", DEFAULT_REFLATTEN_COH_THRESHOLD) or DEFAULT_REFLATTEN_COH_THRESHOLD) + + @property + def _reflatten_fallback_coh_threshold(self) -> float: + return float( + getattr( + settings, + "PYINT_REFLATTEN_FALLBACK_COH_THRESHOLD", + DEFAULT_REFLATTEN_FALLBACK_COH_THRESHOLD, + ) + or DEFAULT_REFLATTEN_FALLBACK_COH_THRESHOLD + ) + + @property + def _reflatten_range_step(self) -> int: + return max(1, int(getattr(settings, "PYINT_REFLATTEN_RANGE_STEP", DEFAULT_REFLATTEN_RANGE_STEP) or DEFAULT_REFLATTEN_RANGE_STEP)) + + @property + def _reflatten_azimuth_step(self) -> int: + return max(1, int(getattr(settings, "PYINT_REFLATTEN_AZIMUTH_STEP", DEFAULT_REFLATTEN_AZIMUTH_STEP) or DEFAULT_REFLATTEN_AZIMUTH_STEP)) + @property def _fabdem_root(self) -> str: return _read_env("PYINT_FABDEM_ROOT", "") @@ -197,25 +301,39 @@ class PyintEngine(DinsarEngine): "label": "强制重跑", "type": "boolean", "default": False, + "section": "Execution", "description": "删除当前 run_key 对应的工作区后重跑。", }, + "target_grid_size_m": { + "label": "目标网格尺寸(米)", + "type": "number", + "default": DEFAULT_TARGET_GRID_SIZE_M, + "step": 1, + "min": TARGET_GRID_SIZE_MIN_M, + "max": TARGET_GRID_SIZE_MAX_M, + "section": "Advanced", + "description": "可选。仅在未手动填写 looks 时用于估算多视数;不会重采样 DEM 或改写 Gamma 产品。", + "recommendation": "保持 0 使用显式或默认的 Gamma/PyINT looks。", + }, "range_looks": { - "label": "距离向多视", + "label": "距离向多视(手动覆盖)", "type": "number", "default": DEFAULT_RANGE_LOOKS, "step": 1, "min": 1, "max": MAX_LOOKS, - "description": "PyINT 模板中的 range_looks。", + "section": "Execution", + "description": "PyINT/Gamma 模板中的 range_looks。", }, "azimuth_looks": { - "label": "方位向多视", + "label": "方位向多视(手动覆盖)", "type": "number", "default": DEFAULT_AZIMUTH_LOOKS, "step": 1, "min": 1, "max": MAX_LOOKS, - "description": "PyINT 模板中的 azimuth_looks。", + "section": "Execution", + "description": "PyINT/Gamma 模板中的 azimuth_looks。", }, "parallel_workers": { "label": "并行数", @@ -224,18 +342,121 @@ class PyintEngine(DinsarEngine): "step": 1, "min": 1, "max": MAX_PARALLEL_WORKERS, + "section": "Execution", "description": "同步控制 raw2slc/coreg/diff/unwrap/geocode 的并行数。", }, + "coherence_mask_threshold": { + "label": "Coherence quality", + "type": "number", + "default": self._default_product_coh_threshold, + "step": 0.05, + "min": 0.0, + "max": 1.0, + "section": "Delivery", + "description": "Only used for quality support statistics. It is not applied as a Python product mask.", + "recommendation": "Use 0.20 by default for LT-1 single-pair reporting; raise it for stricter review maps.", + }, + "unwrap_coh_threshold": { + "label": "Unwrap coherence", + "type": "number", + "default": self._default_unwrap_coh_threshold, + "step": 0.05, + "min": 0.0, + "max": 1.0, + "section": "Advanced", + "description": "Minimum coherence used by Gamma rascc_mask/mcf during unwrapping.", + "recommendation": "Use 0.05 for ENVI-like permissive LT-1 unwrapping; raise it only when low-coherence bridges cause unwrap artifacts.", + }, + "geo_interp": { + "label": "Geocode interpolation", + "type": "select", + "default": self._geo_interp, + "enum": ["0", "1"], + "section": "Advanced", + "description": "Gamma geocode_back interpolation mode: 0 nearest, 1 bicubic spline.", + }, + "atmcor": { + "label": "Gamma atmcor", + "type": "boolean", + "default": self._atmcor_enabled, + "section": "Advanced", + "description": "Run PyINT/Gamma atm_correction stage using atm_mod_2d/atm_sim_2d/sub_phase.", + }, + "atmcor_use_for_disp": { + "label": "Use atmcor for disp", + "type": "boolean", + "default": self._atmcor_use_for_disp, + "section": "Advanced", + "description": "Use the Gamma atmospheric-corrected unwrapped phase as dispmap input when available.", + }, + "reflatten": { + "label": "Gamma residual reflatten", + "type": "boolean", + "default": self._reflatten_enabled, + "section": "Gamma Refinement", + "description": "After unwrapping, fit and remove residual long-wavelength phase ramps with Gamma rascc_mask/quad_fit/quad_sub.", + "recommendation": "Keep enabled for LT-1 D-InSAR unless validating the raw PyINT/Gamma baseline.", + }, + "reflatten_model": { + "label": "Reflatten model", + "type": "select", + "default": self._reflatten_model, + "enum": ["plane", "quadratic"], + "section": "Gamma Refinement", + "description": "Gamma quad_fit model used for residual phase trend removal.", + "recommendation": "plane is safer for single-pair production; use quadratic only when a clear curved residual ramp remains.", + }, + "reflatten_coh_threshold": { + "label": "Reflatten coherence", + "type": "number", + "default": self._reflatten_coh_threshold, + "step": 0.05, + "min": 0.0, + "max": 1.0, + "section": "Gamma Refinement", + "description": "Coherence threshold used to build the fit mask.", + "recommendation": "Keep the primary fit conservative at 0.70; the backend can retry with a looser fallback.", + }, + "reflatten_fallback_coh_threshold": { + "label": "Reflatten fallback coherence", + "type": "number", + "default": self._reflatten_fallback_coh_threshold, + "step": 0.05, + "min": 0.0, + "max": 1.0, + "section": "Gamma Refinement", + "description": "Fallback coherence threshold if the primary reflatten fit does not have enough usable samples.", + }, + "reflatten_range_step": { + "label": "Reflatten range step", + "type": "number", + "default": self._reflatten_range_step, + "step": 1, + "min": 1, + "section": "Gamma Refinement", + "description": "Sampling step in range pixels for Gamma quad_fit control points.", + }, + "reflatten_azimuth_step": { + "label": "Reflatten azimuth step", + "type": "number", + "default": self._reflatten_azimuth_step, + "step": 1, + "min": 1, + "section": "Gamma Refinement", + "description": "Sampling step in azimuth lines for Gamma quad_fit control points.", + }, "unwrap": { "label": "执行解缠", "type": "boolean", "default": True, + "section": "Execution", "description": "关闭后仅做到差分干涉图,不做解缠。", }, "geocode": { "label": "执行地理编码", "type": "boolean", "default": True, + "section": "Execution", "description": "关闭后不导出地理编码结果。", }, }, @@ -257,10 +478,36 @@ class PyintEngine(DinsarEngine): return False return bool(value) - for key in ("force", "unwrap", "geocode"): + for key in ("force", "unwrap", "geocode", "atmcor", "atmcor_use_for_disp", "reflatten"): if key in normalized: normalized[key] = _coerce_bool(normalized[key]) + if "geo_interp" in normalized and normalized["geo_interp"] is not None: + value = str(normalized["geo_interp"] or "").strip() + if not value: + normalized.pop("geo_interp", None) + elif value not in {"0", "1"}: + raise ValueError("geo_interp must be 0 or 1.") + else: + normalized["geo_interp"] = value + + if "target_grid_size_m" in normalized and str(normalized["target_grid_size_m"] or "").strip() == "": + normalized.pop("target_grid_size_m", None) + + if "target_grid_size_m" in normalized and normalized["target_grid_size_m"] is not None: + try: + grid_size = float(normalized["target_grid_size_m"]) + except (TypeError, ValueError) as exc: + raise ValueError("目标网格尺寸必须为数字。") from exc + if int(grid_size) != grid_size: + raise ValueError("目标网格尺寸必须使用整数米。") + grid_size = int(grid_size) + if grid_size < TARGET_GRID_SIZE_MIN_M or grid_size > TARGET_GRID_SIZE_MAX_M: + raise ValueError( + f"目标网格尺寸必须在 {TARGET_GRID_SIZE_MIN_M} 到 {TARGET_GRID_SIZE_MAX_M} 米之间。" + ) + normalized["target_grid_size_m"] = grid_size + for key, maximum, label in ( ("range_looks", MAX_LOOKS, "距离向多视"), ("azimuth_looks", MAX_LOOKS, "方位向多视"), @@ -268,6 +515,9 @@ class PyintEngine(DinsarEngine): ): if key not in normalized or normalized[key] is None: continue + if str(normalized[key]).strip() == "": + normalized.pop(key, None) + continue try: parsed = int(normalized[key]) except (TypeError, ValueError) as exc: @@ -276,6 +526,59 @@ class PyintEngine(DinsarEngine): raise ValueError(f"{label}必须在 1 到 {maximum} 之间。") normalized[key] = parsed + for mode_key, choices in ( + ("reference_mode", {"none", "coh_median"}), + ("deramp_mode", {"none", "plane"}), + ("reflatten_model", {"plane", "linear", "quadratic"}), + ): + if mode_key not in normalized or normalized[mode_key] is None: + continue + value = str(normalized[mode_key] or "").strip().lower() + if not value: + normalized.pop(mode_key, None) + continue + if mode_key == "reflatten_model" and value == "linear": + value = "plane" + if value not in choices: + supported = ", ".join(sorted(choices)) + raise ValueError(f"{mode_key} must be one of: {supported}.") + normalized[mode_key] = value + + for threshold_key in ( + "coherence_mask_threshold", + "unwrap_coh_threshold", + "reference_coh_threshold", + "deramp_coh_threshold", + "reflatten_coh_threshold", + "reflatten_fallback_coh_threshold", + ): + if threshold_key not in normalized or normalized[threshold_key] is None: + continue + if str(normalized[threshold_key]).strip() == "": + normalized.pop(threshold_key, None) + continue + try: + parsed_threshold = float(normalized[threshold_key]) + except (TypeError, ValueError) as exc: + raise ValueError(f"{threshold_key} must be a number.") from exc + if parsed_threshold < 0.0 or parsed_threshold > 1.0: + raise ValueError(f"{threshold_key} must be between 0.0 and 1.0.") + normalized[threshold_key] = parsed_threshold + + for step_key in ("reflatten_range_step", "reflatten_azimuth_step"): + if step_key not in normalized or normalized[step_key] is None: + continue + if str(normalized[step_key]).strip() == "": + normalized.pop(step_key, None) + continue + try: + parsed_step = int(normalized[step_key]) + except (TypeError, ValueError) as exc: + raise ValueError(f"{step_key} must be an integer.") from exc + if parsed_step < 1: + raise ValueError(f"{step_key} must be greater than or equal to 1.") + normalized[step_key] = parsed_step + return normalized def _has_completed_task_result(self, task_dir: str, profile_code: str) -> bool: @@ -413,7 +716,10 @@ class PyintEngine(DinsarEngine): total_tasks = len(task_dirs) run_started_at = datetime.utcnow() run_started_at_text = run_started_at.isoformat(timespec="seconds") + "Z" - run_key = f"run_{run_started_at.strftime('%Y%m%dT%H%M%SZ')}_{self.engine_code}_{request.profile}" + managed_run_key = str(extra.get("__managed_run_key") or "").strip() + run_key = managed_run_key or f"run_{run_started_at.strftime('%Y%m%dT%H%M%SZ')}_{self.engine_code}_{request.profile}" + managed_run_dir_override = str(extra.get("__managed_run_dir") or "").strip() + managed_native_output_dir_override = str(extra.get("__managed_native_output_dir") or "").strip() progress_callback = request.progress_callback def emit_progress(event_type: str, **payload: Any) -> None: @@ -426,9 +732,44 @@ class PyintEngine(DinsarEngine): timeout = max(60, int(request.timeout_seconds or self.default_timeout_seconds)) force = bool(extra.get("force")) - range_looks = int(extra.get("range_looks", DEFAULT_RANGE_LOOKS)) - azimuth_looks = int(extra.get("azimuth_looks", DEFAULT_AZIMUTH_LOOKS)) + target_grid_size_m = int(extra.get("target_grid_size_m") or 0) + manual_range_looks = extra.get("range_looks") + manual_azimuth_looks = extra.get("azimuth_looks") parallel_workers = int(extra.get("parallel_workers", DEFAULT_PARALLEL_WORKERS)) + dem_resolution_m = self._dem_resolution_m + dem_oversampling = calculate_dem_oversampling( + dem_resolution_m=dem_resolution_m, + target_grid_size_m=target_grid_size_m, + ) + dem_lat_ovr = float(dem_oversampling["oversampling"]) + dem_lon_ovr = float(dem_oversampling["oversampling"]) + unwrap_coh_threshold = float(extra.get("unwrap_coh_threshold", self._default_unwrap_coh_threshold)) + coherence_mask_threshold = float(extra.get("coherence_mask_threshold", self._default_product_coh_threshold)) + reference_mode = "none" + reference_coh_threshold = float(self._default_reference_coh_threshold) + deramp_mode = "none" + deramp_coh_threshold = float(self._default_deramp_coh_threshold) + gamma_nodata_value = self._gamma_nodata_value + geo_interp = str(extra.get("geo_interp", self._geo_interp) or self._geo_interp).strip() + if geo_interp not in {"0", "1"}: + geo_interp = DEFAULT_GEO_INTERP + atmcor = bool(extra.get("atmcor", self._atmcor_enabled)) + atmcor_use_for_disp = bool(extra.get("atmcor_use_for_disp", self._atmcor_use_for_disp)) if atmcor else False + reflatten = bool(extra.get("reflatten", self._reflatten_enabled)) + reflatten_model = str(extra.get("reflatten_model", self._reflatten_model) or self._reflatten_model).strip().lower() + if reflatten_model == "linear": + reflatten_model = "plane" + if reflatten_model not in {"plane", "quadratic"}: + reflatten_model = DEFAULT_REFLATTEN_MODEL + reflatten_coh_threshold = float(extra.get("reflatten_coh_threshold", self._reflatten_coh_threshold)) + reflatten_fallback_coh_threshold = float( + extra.get( + "reflatten_fallback_coh_threshold", + self._reflatten_fallback_coh_threshold, + ) + ) + reflatten_range_step = int(extra.get("reflatten_range_step", self._reflatten_range_step)) + reflatten_azimuth_step = int(extra.get("reflatten_azimuth_step", self._reflatten_azimuth_step)) unwrap = bool(extra.get("unwrap", True)) geocode = bool(extra.get("geocode", True)) @@ -443,6 +784,57 @@ class PyintEngine(DinsarEngine): wsl_prepared_dem_path = to_wsl_path(prepared_dem_path) if prepared_dem_path else "" shared_orbit_context = get_pyint_orbit_context() + def resolve_pair_looks(task_dir: str) -> Dict[str, Any]: + manual_range = int(manual_range_looks) if manual_range_looks is not None else None + manual_azimuth = int(manual_azimuth_looks) if manual_azimuth_looks is not None else None + calculation: Dict[str, Any] = {} + error_text = "" + + if target_grid_size_m > 0 and (manual_range is None or manual_azimuth is None): + try: + calculation = calculate_looks_from_task_dir( + task_dir, + target_grid_size_m, + ) + except Exception as exc: + error_text = str(exc) + calculation = { + "mode": "fallback_default", + "target_resolution_m": target_grid_size_m, + "error": error_text, + } + elif manual_range is None or manual_azimuth is None: + calculation = { + "mode": "gamma_default_looks", + "target_resolution_m": None, + } + + range_looks = manual_range + if range_looks is None: + range_looks = int(calculation.get("range_looks") or DEFAULT_RANGE_LOOKS) + + azimuth_looks = manual_azimuth + if azimuth_looks is None: + azimuth_looks = int(calculation.get("azimuth_looks") or DEFAULT_AZIMUTH_LOOKS) + + if manual_range is not None or manual_azimuth is not None: + calculation = { + **calculation, + "mode": "manual_override" if calculation else "manual", + "manual_range_looks": manual_range, + "manual_azimuth_looks": manual_azimuth, + } + + calculation["resolved_range_looks"] = int(range_looks) + calculation["resolved_azimuth_looks"] = int(azimuth_looks) + calculation["target_grid_size_m"] = int(target_grid_size_m) + return { + "range_looks": int(range_looks), + "azimuth_looks": int(azimuth_looks), + "calculation": calculation, + "error": error_text, + } + task_results: List[Dict[str, Any]] = [] output_dirs: List[str] = [] pairs_processed = 0 @@ -458,11 +850,19 @@ class PyintEngine(DinsarEngine): slave_date = task_identity["slave_date"] work_run_root = os.path.normpath(os.path.join(self._work_root, pair_key, run_key)) - output_dir = os.path.normpath(os.path.join(self._output_root, pair_key, "runs", run_key, "native")) + run_dir = os.path.normpath(managed_run_dir_override) if managed_run_dir_override else os.path.normpath( + os.path.join(self._output_root, pair_key, "runs", run_key) + ) + output_dir = ( + os.path.normpath(managed_native_output_dir_override) + if managed_native_output_dir_override + else os.path.join(run_dir, "native") + ) template_root = os.path.normpath(os.path.join(self._template_root, pair_key, run_key)) project_name = build_project_name(pair_key, run_key) project_dir = os.path.join(work_run_root, project_name) - input_assets_dir = os.path.join(work_run_root, "input_assets") + # Keep input assets outside the run root because the WSL pipeline may delete run_root on --force. + input_assets_dir = os.path.join(self._work_root, pair_key, "input_assets", run_key) wsl_task_dir = to_wsl_path(task_dir) wsl_project_dir = to_wsl_path(project_dir) @@ -663,6 +1063,45 @@ class PyintEngine(DinsarEngine): else "" ) + look_resolution = resolve_pair_looks(task_dir) + range_looks = int(look_resolution["range_looks"]) + azimuth_looks = int(look_resolution["azimuth_looks"]) + look_calculation = dict(look_resolution.get("calculation") or {}) + look_message = ( + f"PyINT looks resolved for {task_alias}: " + f"range={range_looks}, azimuth={azimuth_looks}, " + f"target_grid={target_grid_size_m or 'not_set'}m, mode={look_calculation.get('mode', 'unknown')}" + ) + if look_resolution.get("error"): + look_message += f", fallback_reason={look_resolution['error']}" + emit_progress( + "log", + pair_index=pair_index, + pair_total=total_tasks, + task_name=task_name, + task_alias=task_alias, + pair_key=pair_key, + level="WARNING" if look_resolution.get("error") else "INFO", + source="looks", + message=look_message, + ) + emit_progress( + "log", + pair_index=pair_index, + pair_total=total_tasks, + task_name=task_name, + task_alias=task_alias, + pair_key=pair_key, + level="INFO", + source="dem", + message=( + f"PyINT DEM oversampling for {task_alias}: " + f"dem_resolution={dem_resolution_m:g}m, target_grid={target_grid_size_m or 'not_set'}m, " + f"dem_lat_ovr={dem_lat_ovr:g}, dem_lon_ovr={dem_lon_ovr:g}, " + f"actual_grid={float(dem_oversampling.get('actual_grid_size_m') or 0.0):g}m" + ), + ) + cmd_parts = [ f"{quote_shell(self._python)} {quote_shell(self._pipeline_script)} {quote_shell(wsl_task_dir)}", f"--project-dir {quote_shell(wsl_project_dir)}", @@ -679,10 +1118,24 @@ class PyintEngine(DinsarEngine): f"--orbit-policy {quote_shell(self._orbit_policy)}", f"--range-looks {range_looks}", f"--azimuth-looks {azimuth_looks}", + f"--dem-resolution-m {dem_resolution_m}", + f"--dem-lat-ovr {dem_lat_ovr}", + f"--dem-lon-ovr {dem_lon_ovr}", f"--parallel-workers {parallel_workers}", f"--master-date {quote_shell(master_date)}" if master_date else "", f"--slave-date {quote_shell(slave_date)}" if slave_date else "", f"--time-baseline-days {time_baseline_days}", + f"--target-grid-size-m {target_grid_size_m}", + f"--unwrap-coh-threshold {unwrap_coh_threshold}", + f"--coherence-mask-threshold {coherence_mask_threshold}", + f"--geo-interp {quote_shell(geo_interp)}", + f"--gamma-nodata-value {gamma_nodata_value}", + "--reflatten" if reflatten else "--no-reflatten", + f"--reflatten-model {quote_shell(reflatten_model)}", + f"--reflatten-coh-threshold {reflatten_coh_threshold}", + f"--reflatten-fallback-coh-threshold {reflatten_fallback_coh_threshold}", + f"--reflatten-range-step {reflatten_range_step}", + f"--reflatten-azimuth-step {reflatten_azimuth_step}", f"--input-assets-dir {quote_shell(wsl_input_assets_dir)}" if wsl_input_assets_dir else "", f"--input-assets-json {quote_shell(wsl_input_assets_json)}" if wsl_input_assets_json else "", f"--lt1-precise-orbit-enabled {'true' if self._lt1_precise_orbit_enabled else 'false'}", @@ -695,6 +1148,8 @@ class PyintEngine(DinsarEngine): f"--lt1-precise-orbit-backup {'true' if self._lt1_precise_orbit_backup else 'false'}", f"--lt1-precise-orbit-orb-filt-degree {self._lt1_precise_orbit_orb_filt_degree}", "--unwrap" if unwrap else "--no-unwrap", + "--atmcor" if atmcor else "--no-atmcor", + "--atmcor-use-for-disp" if atmcor_use_for_disp else "--no-atmcor-use-for-disp", "--geocode" if geocode else "--no-geocode", ] if self._dem_mode == "local_fabdem" and wsl_fabdem_root: @@ -712,19 +1167,61 @@ class PyintEngine(DinsarEngine): cmd_parts.append("--force") cmd = " ".join(part for part in cmd_parts if part) - rc, stdout, stderr = run_wsl_command( + def _emit_stream_log(level: str, source: str, text: str) -> None: + line = str(text or "").strip() + if not line: + return + max_len = 2000 + if len(line) > max_len: + line = line[:max_len] + "..." + emit_progress( + "log", + pair_index=pair_index, + pair_total=total_tasks, + task_name=task_name, + task_alias=task_alias, + pair_key=pair_key, + level=level, + source=source, + message=line, + ) + + rc, stdout, stderr = run_wsl_command_stream( cmd, distro=self._distro, timeout=timeout, + stdout_callback=lambda line: _emit_stream_log("INFO", "stdout", line), + stderr_callback=lambda line: _emit_stream_log("WARNING", "stderr", line), ) success = rc == 0 + error_text = stderr.strip() if stderr else "" + validation_result: Dict[str, Any] = {} + completion_files_result: Dict[str, Any] = {} + primary_file = "" + source_files: List[str] = [] if success: - pairs_processed += 1 - os.makedirs(output_dir, exist_ok=True) - write_run_metadata( - output_dir, - { + try: + os.makedirs(output_dir, exist_ok=True) + os.makedirs(run_dir, exist_ok=True) + standard_disp_path = os.path.join(run_dir, "assets", "disp", "disp.tif") + standard_coh_path = os.path.join(run_dir, "assets", "coh", "coh.tif") + if geocode: + validation_sources = [standard_disp_path] + if os.path.isfile(standard_coh_path): + validation_sources.append(standard_coh_path) + validation_result = validate_isce2_result_files( + standard_disp_path, + validation_sources, + ) + if not bool(validation_result.get("accepted")): + issues = validation_result.get("issues") or [] + issue_text = "; ".join(str(item) for item in issues[:3]) or "unknown validation error" + raise RuntimeError(f"PyINT standard GeoTIFF validation failed: {issue_text}") + primary_file = str(validation_result.get("primary_file") or standard_disp_path) + source_files = list(validation_result.get("source_files") or validation_sources) + + run_metadata = { "run_key": run_key, "pair_key": pair_key, "task_name": task_name, @@ -734,15 +1231,50 @@ class PyintEngine(DinsarEngine): "source_root": os.path.normpath(request.root_dir), "task_dir": os.path.normpath(task_dir), "work_dir": work_run_root, - "output_dir": output_dir, + "output_dir": run_dir, + "native_output_dir": output_dir, "project_dir": project_dir, + "runtime_id": settings.PYINT_RUNTIME_ID, "started_at": run_started_at_text, "finished_at": datetime.utcnow().isoformat(timespec="seconds") + "Z", + "primary_file": primary_file, + "source_files": source_files, + "acceptance": validation_result, "params": { "force": force, + "target_grid_size_m": target_grid_size_m, + "dem_resolution_m": dem_resolution_m, + "dem_oversampling": dem_oversampling, + "dem_lat_ovr": dem_lat_ovr, + "dem_lon_ovr": dem_lon_ovr, "range_looks": range_looks, "azimuth_looks": azimuth_looks, + "manual_range_looks": manual_range_looks, + "manual_azimuth_looks": manual_azimuth_looks, + "look_calculation": look_calculation, "parallel_workers": parallel_workers, + "unwrap_coh_threshold": unwrap_coh_threshold, + "coherence_quality_threshold": coherence_mask_threshold, + "reference_mode": reference_mode, + "reference_coh_threshold": reference_coh_threshold, + "deramp_mode": deramp_mode, + "deramp_coh_threshold": deramp_coh_threshold, + "gamma_nodata_value": gamma_nodata_value, + "geo_interp": geo_interp, + "atmcor": atmcor, + "atmcor_use_for_disp": atmcor_use_for_disp, + "reflatten": reflatten, + "reflatten_model": reflatten_model, + "reflatten_coh_threshold": reflatten_coh_threshold, + "reflatten_fallback_coh_threshold": reflatten_fallback_coh_threshold, + "reflatten_range_step": reflatten_range_step, + "reflatten_azimuth_step": reflatten_azimuth_step, + "gamma_native_export": { + "python_data_processing_applied": False, + "coherence_mask_applied": False, + "reference_applied": False, + "deramp_applied": False, + }, "unwrap": unwrap, "geocode": geocode, }, @@ -765,10 +1297,24 @@ class PyintEngine(DinsarEngine): "policy_version": pair_meta.get("policy_version"), "selection_strategy": pair_meta.get("selection_strategy"), "input_assets": input_assets_summary, - }, - ) - output_dirs.append(output_dir) - else: + } + write_run_metadata(run_dir, run_metadata) + write_run_metadata(output_dir, run_metadata) + if geocode and primary_file: + completion_files_result = repair_managed_completion_files( + run_dir, + primary_file=primary_file, + source_files=source_files, + run_meta=run_metadata, + ) + output_dirs.append(run_dir) + pairs_processed += 1 + except Exception as exc: + success = False + error_text = str(exc) + stderr = (stderr.rstrip() + "\n" + error_text) if stderr else error_text + + if not success: pairs_failed += 1 emit_progress( @@ -780,7 +1326,7 @@ class PyintEngine(DinsarEngine): pair_key=pair_key, success=success, returncode=rc, - error=stderr.strip() if stderr else "", + error=error_text, ) task_results.append( { @@ -791,13 +1337,45 @@ class PyintEngine(DinsarEngine): "task_dir": task_dir, "work_dir": work_run_root, "project_dir": project_dir, - "output_dir": output_dir, + "run_dir": run_dir, + "output_dir": run_dir, + "native_output_dir": output_dir, + "primary_file": primary_file, + "source_files": source_files, + "acceptance": validation_result, + "completion_files": completion_files_result, + "target_grid_size_m": target_grid_size_m, + "dem_resolution_m": dem_resolution_m, + "dem_oversampling": dem_oversampling, + "dem_lat_ovr": dem_lat_ovr, + "dem_lon_ovr": dem_lon_ovr, + "range_looks": range_looks, + "azimuth_looks": azimuth_looks, + "manual_range_looks": manual_range_looks, + "manual_azimuth_looks": manual_azimuth_looks, + "look_calculation": look_calculation, + "unwrap_coh_threshold": unwrap_coh_threshold, + "coherence_quality_threshold": coherence_mask_threshold, + "reference_mode": reference_mode, + "reference_coh_threshold": reference_coh_threshold, + "deramp_mode": deramp_mode, + "deramp_coh_threshold": deramp_coh_threshold, + "gamma_nodata_value": gamma_nodata_value, + "geo_interp": geo_interp, + "atmcor": atmcor, + "atmcor_use_for_disp": atmcor_use_for_disp, + "gamma_native_export": { + "python_data_processing_applied": False, + "coherence_mask_applied": False, + "reference_applied": False, + "deramp_applied": False, + }, "command": cmd, "success": success, "returncode": rc, "stdout_tail": stdout[-3000:] if stdout else "", "stderr_tail": stderr[-3000:] if stderr else "", - "error": stderr.strip() if stderr else "", + "error": error_text, "wsl_task_dir": wsl_task_dir, "wsl_project_dir": wsl_project_dir, "wsl_output_dir": wsl_output_dir, @@ -849,9 +1427,32 @@ class PyintEngine(DinsarEngine): "started_at": run_started_at_text, "force": force, "timeout_seconds": timeout, - "range_looks": range_looks, - "azimuth_looks": azimuth_looks, + "target_grid_size_m": target_grid_size_m, + "dem_resolution_m": dem_resolution_m, + "dem_oversampling": dem_oversampling, + "dem_lat_ovr": dem_lat_ovr, + "dem_lon_ovr": dem_lon_ovr, + "range_looks": last_task_result.get("range_looks"), + "azimuth_looks": last_task_result.get("azimuth_looks"), + "manual_range_looks": manual_range_looks, + "manual_azimuth_looks": manual_azimuth_looks, "parallel_workers": parallel_workers, + "unwrap_coh_threshold": unwrap_coh_threshold, + "coherence_quality_threshold": coherence_mask_threshold, + "reference_mode": reference_mode, + "reference_coh_threshold": reference_coh_threshold, + "deramp_mode": deramp_mode, + "deramp_coh_threshold": deramp_coh_threshold, + "gamma_nodata_value": gamma_nodata_value, + "geo_interp": geo_interp, + "atmcor": atmcor, + "atmcor_use_for_disp": atmcor_use_for_disp, + "gamma_native_export": { + "python_data_processing_applied": False, + "coherence_mask_applied": False, + "reference_applied": False, + "deramp_applied": False, + }, "unwrap": unwrap, "geocode": geocode, "command": last_task_result.get("command", ""), diff --git a/backend/app/pyint_pipeline/run_lt1_pyint_pipeline.py b/backend/app/pyint_pipeline/run_lt1_pyint_pipeline.py index 5ad00a0..a7be7bc 100644 --- a/backend/app/pyint_pipeline/run_lt1_pyint_pipeline.py +++ b/backend/app/pyint_pipeline/run_lt1_pyint_pipeline.py @@ -3,18 +3,39 @@ from __future__ import annotations import argparse import json +import math import os +import shlex import shutil import stat import subprocess import sys +import threading from datetime import datetime from pathlib import Path -from typing import Any, Dict, Iterable, List +from typing import Any, Dict, Iterable, List, Tuple LT1_INPUT_GLOBS = ("LT1*.tar.gz", "LT1*.tiff") PAIR_META_FILENAME = ".dinsar_pair.json" +DEFAULT_DEM_RESOLUTION_M = 30.0 +DEFAULT_DEM_OVERSAMPLING = 1.0 +DEM_OVERSAMPLING_MIN = 0.25 +DEM_OVERSAMPLING_MAX = 16.0 +MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS = 1 +DEFAULT_UNWRAP_COH_THRESHOLD = 0.05 +DEFAULT_COHERENCE_MASK_THRESHOLD = 0.20 +DEFAULT_REFERENCE_MODE = "none" +DEFAULT_REFERENCE_COH_THRESHOLD = 0.30 +DEFAULT_DERAMP_MODE = "none" +DEFAULT_DERAMP_COH_THRESHOLD = 0.30 +DEFAULT_REFLATTEN_MODEL = "plane" +DEFAULT_REFLATTEN_COH_THRESHOLD = 0.70 +DEFAULT_REFLATTEN_FALLBACK_COH_THRESHOLD = 0.20 +DEFAULT_REFLATTEN_RANGE_STEP = 32 +DEFAULT_REFLATTEN_AZIMUTH_STEP = 32 +QUALITY_COHERENCE_THRESHOLDS = (0.20, 0.30, 0.40, 0.50) +GRID_MISMATCH_TOLERANCE = 0.25 def parse_args() -> argparse.Namespace: @@ -44,8 +65,121 @@ def parse_args() -> argparse.Namespace: 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("--target-grid-size-m", type=int, default=0, help="Optional requested grid size recorded for reporting; it does not resample Gamma products.") parser.add_argument("--range-looks", type=int, default=2) parser.add_argument("--azimuth-looks", type=int, default=2) + parser.add_argument( + "--dem-resolution-m", + type=float, + default=DEFAULT_DEM_RESOLUTION_M, + help="Source DEM resolution in meters, used to derive Gamma DEM oversampling.", + ) + parser.add_argument( + "--dem-lat-ovr", + type=float, + default=0.0, + help="Gamma DEM latitude oversampling. Defaults to the PyINT/Gamma native setting.", + ) + parser.add_argument( + "--dem-lon-ovr", + type=float, + default=0.0, + help="Gamma DEM longitude oversampling. Defaults to the PyINT/Gamma native setting.", + ) + parser.add_argument( + "--unwrap-coh-threshold", + type=float, + default=DEFAULT_UNWRAP_COH_THRESHOLD, + help="Minimum coherence used by Gamma rascc_mask/mcf during unwrapping.", + ) + parser.add_argument( + "--coherence-mask-threshold", + type=float, + default=DEFAULT_COHERENCE_MASK_THRESHOLD, + help="Minimum coherence reported in Gamma native product quality support metrics.", + ) + parser.add_argument( + "--reference-mode", + default=DEFAULT_REFERENCE_MODE, + choices={"none", "coh_median"}, + help="Compatibility option; Python does not reference-correct Gamma displacement products.", + ) + parser.add_argument( + "--reference-coh-threshold", + type=float, + default=DEFAULT_REFERENCE_COH_THRESHOLD, + help="Minimum coherence used when selecting pixels for reference correction.", + ) + parser.add_argument( + "--deramp-mode", + default=DEFAULT_DERAMP_MODE, + choices={"none", "plane"}, + help="Compatibility option; Python does not deramp Gamma displacement products.", + ) + parser.add_argument( + "--deramp-coh-threshold", + type=float, + default=DEFAULT_DERAMP_COH_THRESHOLD, + help="Minimum coherence used when selecting pixels for deramp fitting.", + ) + parser.add_argument( + "--gamma-nodata-value", + type=float, + default=-9999.0, + help="NoData value passed to Gamma data2geotiff exports.", + ) + parser.add_argument( + "--geo-interp", + default="1", + choices={"0", "1"}, + help="Gamma geocode_back interpolation mode: 0=nearest, 1=bicubic spline.", + ) + parser.add_argument("--atmcor", dest="atmcor", action="store_true", help="Enable PyINT/Gamma atmcor_all stage.") + parser.add_argument("--no-atmcor", dest="atmcor", action="store_false") + parser.add_argument( + "--atmcor-use-for-disp", + dest="atmcor_use_for_disp", + action="store_true", + help="Use atmcor unwrapped phase as the Gamma dispmap source when atmcor output exists.", + ) + parser.add_argument("--no-atmcor-use-for-disp", dest="atmcor_use_for_disp", action="store_false") + parser.add_argument( + "--reflatten", + dest="reflatten", + action="store_true", + help="Fit and remove a residual unwrapped-phase trend after PyINT/Gamma unwrapping.", + ) + parser.add_argument("--no-reflatten", dest="reflatten", action="store_false") + parser.add_argument( + "--reflatten-model", + default=DEFAULT_REFLATTEN_MODEL, + choices={"plane", "linear", "quadratic"}, + help="Gamma quad_fit model for reflattening. linear is accepted as an alias for plane.", + ) + parser.add_argument( + "--reflatten-coh-threshold", + type=float, + default=DEFAULT_REFLATTEN_COH_THRESHOLD, + help="Coherence threshold used to build the reflatten fit mask.", + ) + parser.add_argument( + "--reflatten-fallback-coh-threshold", + type=float, + default=DEFAULT_REFLATTEN_FALLBACK_COH_THRESHOLD, + help="Fallback coherence threshold used if the primary reflatten fit fails.", + ) + parser.add_argument( + "--reflatten-range-step", + type=int, + default=DEFAULT_REFLATTEN_RANGE_STEP, + help="Range sample spacing passed to Gamma quad_fit.", + ) + parser.add_argument( + "--reflatten-azimuth-step", + type=int, + default=DEFAULT_REFLATTEN_AZIMUTH_STEP, + help="Azimuth sample spacing passed to Gamma quad_fit.", + ) 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.") @@ -63,6 +197,7 @@ def parse_args() -> argparse.Namespace: 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) + parser.set_defaults(atmcor=False, atmcor_use_for_disp=False, reflatten=True) return parser.parse_args() @@ -84,6 +219,54 @@ def normalize_bool_text(value: Any, default: bool = False) -> bool: return str(value).strip().lower() in {"1", "true", "yes", "on"} +def clamp_float(value: float, minimum: float, maximum: float) -> float: + return min(float(maximum), max(float(minimum), float(value))) + + +def validate_unit_interval(value: float, name: str) -> float: + parsed = float(value) + if parsed < 0.0 or parsed > 1.0: + raise ValueError(f"{name} must be between 0.0 and 1.0.") + return parsed + + +def format_gamma_number(value: float) -> str: + text = f"{float(value):.6f}".rstrip("0").rstrip(".") + return text or "0" + + +def calculate_dem_oversampling( + *, + dem_resolution_m: float, + target_grid_size_m: float, + dem_lat_ovr: float = 0.0, + dem_lon_ovr: float = 0.0, +) -> Dict[str, Any]: + dem_resolution = float(dem_resolution_m or DEFAULT_DEM_RESOLUTION_M) + target_grid = float(target_grid_size_m or 0.0) + if dem_resolution <= 0: + dem_resolution = DEFAULT_DEM_RESOLUTION_M + + raw_factor = dem_resolution / target_grid if target_grid > 0 else None + lat_factor = clamp_float(float(dem_lat_ovr or DEFAULT_DEM_OVERSAMPLING), DEM_OVERSAMPLING_MIN, DEM_OVERSAMPLING_MAX) + lon_factor = clamp_float(float(dem_lon_ovr or DEFAULT_DEM_OVERSAMPLING), DEM_OVERSAMPLING_MIN, DEM_OVERSAMPLING_MAX) + average_factor = (lat_factor + lon_factor) / 2.0 + actual_grid = dem_resolution / average_factor if average_factor > 0 else dem_resolution + mismatch_ratio = abs(actual_grid - target_grid) / target_grid if target_grid > 0 else None + return { + "mode": "gamma_dem_oversampling", + "dem_resolution_m": dem_resolution, + "target_grid_size_m": target_grid, + "raw_oversampling": raw_factor, + "dem_lat_ovr": lat_factor, + "dem_lon_ovr": lon_factor, + "actual_grid_size_m": actual_grid, + "mismatch_ratio": mismatch_ratio, + "min_oversampling": DEM_OVERSAMPLING_MIN, + "max_oversampling": DEM_OVERSAMPLING_MAX, + } + + def ensure_directory(path: Path) -> Path: path.mkdir(parents=True, exist_ok=True) return path @@ -240,6 +423,16 @@ def build_template_text( master_date: str, range_looks: int, azimuth_looks: int, + target_grid_size_m: int, + dem_lat_ovr: float, + dem_lon_ovr: float, + unwrap_coh_threshold: float, + geo_interp: str, + atmcor: bool, + atmcor_use_for_disp: bool, + reflatten: bool, + reflatten_model: str, + reflatten_coh_threshold: float, parallel_workers: int, unwrap: bool, geocode: bool, @@ -256,6 +449,9 @@ def build_template_text( f"masterDate={master_date}", f"range_looks={int(range_looks)}", f"azimuth_looks={int(azimuth_looks)}", + f"target_grid_size_m={int(target_grid_size_m or 0)}", + f"dem_lat_ovr={format_gamma_number(dem_lat_ovr)}", + f"dem_lon_ovr={format_gamma_number(dem_lon_ovr)}", "download_data=0", "raw2slc_all=1", f"raw2slc_all_parallel={int(parallel_workers)}", @@ -270,10 +466,20 @@ def build_template_text( 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"unwrapThreshold={format_gamma_number(unwrap_coh_threshold)}", + "make_mask=1", + "auto_unw=1", + "r_refer=-", + "a_refer=-", + f"atmcor_all={1 if atmcor else 0}", f"atmcor_all_parallel={int(parallel_workers)}", + f"atmcor_use_for_disp={1 if (atmcor and atmcor_use_for_disp) else 0}", + f"reflatten={1 if reflatten else 0}", + f"reflatten_model={str(reflatten_model or DEFAULT_REFLATTEN_MODEL).strip().lower()}", + f"reflatten_coh_threshold={format_gamma_number(reflatten_coh_threshold)}", f"geocode_all={1 if geocode else 0}", f"geocode_all_parallel={int(parallel_workers)}", + f"geo_interp={str(geo_interp or '0').strip()}", "gacos_correction=0", "load_data=0", "geocode_products=hyp3,licsbas", @@ -338,18 +544,85 @@ def write_wrapper_scripts( return created -def run_logged(command: List[str], *, env: Dict[str, str], cwd: Path, stdout_path: Path, stderr_path: Path) -> subprocess.CompletedProcess[str]: +def load_shell_environment(script_path: str, base_env: Dict[str, str]) -> Dict[str, str]: + text = str(script_path or "").strip() + if not text: + return {} + path = Path(text).resolve() + if not path.is_file(): + raise FileNotFoundError(f"Gamma environment script not found: {path}") + + command = f". {shlex.quote(str(path))} >/dev/null 2>&1; env -0" result = subprocess.run( + ["bash", "-lc", command], + env=base_env, + capture_output=True, + check=False, + ) + if result.returncode != 0: + detail = (result.stderr or result.stdout or b"").decode("utf-8", errors="replace").strip() + raise RuntimeError(f"Failed to source Gamma environment script {path}: {detail}") + + values: Dict[str, str] = {} + for chunk in result.stdout.split(b"\0"): + if not chunk or b"=" not in chunk: + continue + key, value = chunk.split(b"=", 1) + values[key.decode("utf-8", errors="replace")] = value.decode("utf-8", errors="replace") + return values + + +def run_logged( + command: List[str], + *, + env: Dict[str, str], + cwd: Path, + stdout_path: Path, + stderr_path: Path, + mirror_output: bool = True, +) -> subprocess.CompletedProcess[str]: + ensure_directory(stdout_path.parent) + ensure_directory(stderr_path.parent) + stdout_parts: List[str] = [] + stderr_parts: List[str] = [] + + proc = subprocess.Popen( command, cwd=str(cwd), env=env, text=True, - capture_output=True, - check=False, + encoding="utf-8", + errors="replace", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def _drain(stream: Any, target_path: Path, parts: List[str], mirror: Any) -> None: + with target_path.open("w", encoding="utf-8") as fp: + for line in iter(stream.readline, ""): + parts.append(line) + fp.write(line) + fp.flush() + if mirror_output: + mirror.write(line) + mirror.flush() + + threads = [ + threading.Thread(target=_drain, args=(proc.stdout, stdout_path, stdout_parts, sys.stdout), daemon=True), + threading.Thread(target=_drain, args=(proc.stderr, stderr_path, stderr_parts, sys.stderr), daemon=True), + ] + for thread in threads: + thread.start() + returncode = proc.wait() + for thread in threads: + thread.join() + + return subprocess.CompletedProcess( + command, + returncode, + stdout="".join(stdout_parts), + stderr="".join(stderr_parts), ) - write_text(stdout_path, result.stdout or "") - write_text(stderr_path, result.stderr or "") - return result def require_task_layout(task_dir: Path) -> None: @@ -361,13 +634,32 @@ def require_task_layout(task_dir: Path) -> None: 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" + master_date = pair_name.split("-", 1)[0] 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"), + "reflat_unw": str(pair_dir / f"{pair_name}_{look_text}.diff_filt.reflat.unw"), + "reflat_trend": str(pair_dir / f"{pair_name}_{look_text}.diff_filt.reflat.trend"), + "reflat_mask": str(pair_dir / f"{pair_name}_{look_text}.diff_filt.reflat.mask.bmp"), + "reflat_diff_par": str(pair_dir / f"{pair_name}_{look_text}.reflat.diff_par"), + "geo_coh": str(pair_dir / f"geo_{master_date}_{look_text}.diff_filt.cor"), "geo_unw": str(pair_dir / f"geo_{pair_name}_{look_text}.diff_filt.unw"), + "geo_reflat_unw": str(pair_dir / f"geo_{pair_name}_{look_text}.diff_filt.reflat.unw"), + "atmcor_unw": str(pair_dir / f"{pair_name}_{look_text}.diff_filt.atmcor.unw"), + "geo_atmcor_unw": str(pair_dir / f"geo_{pair_name}_{look_text}.diff_filt.atmcor.unw"), "geo_los": str(pair_dir / f"geo_{pair_name}_{look_text}.los_disp"), + "reflat_los": str(pair_dir / f"{pair_name}_{look_text}.reflat.los_disp"), + "geo_reflat_los": str(pair_dir / f"geo_{pair_name}_{look_text}.reflat.los_disp"), + "reflat_vert": str(pair_dir / f"{pair_name}_{look_text}.reflat.vert_disp"), + "geo_reflat_vert": str(pair_dir / f"geo_{pair_name}_{look_text}.reflat.vert_disp"), + "geo_atmcor_los": str(pair_dir / f"geo_{pair_name}_{look_text}.atmcor.los_disp"), + "geo_vert": str(pair_dir / f"geo_{pair_name}_{look_text}.vert_disp"), + "geo_atmcor_vert": str(pair_dir / f"geo_{pair_name}_{look_text}.atmcor.vert_disp"), + "geo_wrapped_phase": str(pair_dir / f"geo_{pair_name}_{look_text}.diff_filt.pha"), + "look_vector_theta": str(pair_dir / "lv_theta"), + "look_vector_phi": str(pair_dir / "lv_phi"), } @@ -407,12 +699,11 @@ def collect_output_sanity_checks( if unwrap: targets.append(("unw", "unwrapped interferogram")) if geocode: - targets.extend( - [ - ("geo_unw", "geocoded unwrapped interferogram"), - ("geo_los", "geocoded LOS displacement"), - ] - ) + targets.append(("geo_unw", "geocoded unwrapped interferogram")) + if outputs.get("reflat_unw") and Path(outputs["reflat_unw"]).is_file(): + targets.append(("reflat_unw", "reflattened unwrapped interferogram")) + if outputs.get("geo_reflat_unw") and Path(outputs["geo_reflat_unw"]).is_file(): + targets.append(("geo_reflat_unw", "geocoded reflattened unwrapped interferogram")) checks: List[Dict[str, Any]] = [] for name, label in targets: @@ -438,8 +729,481 @@ 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}") + details = [] + for item in failed: + if not item.get("exists"): + reason = "missing" + elif int(item.get("size_bytes") or 0) <= 0: + reason = "empty" + elif item.get("all_zero"): + reason = "all-zero" + else: + reason = "invalid" + details.append(f"{item['name']}({reason})={item['path']}") + raise RuntimeError(f"PyINT run produced invalid binary outputs: {', '.join(details)}") + + +def _gamma_reflatten_model_code(model: str) -> int: + normalized = str(model or DEFAULT_REFLATTEN_MODEL).strip().lower() + if normalized in {"plane", "linear"}: + return 3 + if normalized == "quadratic": + return 0 + raise ValueError("reflatten_model must be plane or quadratic.") + + +def _run_reflatten_command( + *, + command: List[str], + stage: str, + log_dir: Path, + env: Dict[str, str], + cwd: Path, +) -> Dict[str, Any]: + stdout_path = log_dir / f"{stage}.stdout.log" + stderr_path = log_dir / f"{stage}.stderr.log" + result = run_logged( + command, + env=env, + cwd=cwd, + stdout_path=stdout_path, + stderr_path=stderr_path, + mirror_output=False, + ) + info = { + "stage": stage, + "command": command, + "returncode": int(result.returncode), + "stdout_path": str(stdout_path), + "stderr_path": str(stderr_path), + "stdout_tail": (result.stdout or "")[-2000:], + "stderr_tail": (result.stderr or "")[-2000:], + } + if result.returncode != 0: + detail = (result.stderr or result.stdout or "").strip() + raise RuntimeError(f"Gamma reflatten stage {stage} failed with rc={result.returncode}: {detail}") + return info + + +def run_gamma_reflatten( + *, + project_dir: Path, + run_root: Path, + output_dir: Path, + outputs: Dict[str, str], + pair_name: str, + master_date: str, + range_looks: int, + env: Dict[str, str], + model: str, + coherence_threshold: float, + fallback_coherence_threshold: float, + range_step: int, + azimuth_step: int, + geo_interp: str, +) -> Dict[str, Any]: + pair_dir = Path(outputs["pair_dir"]) + look_text = f"{int(range_looks)}rlks" + unw = Path(outputs["unw"]) + atmcor_unw = Path(str(outputs.get("atmcor_unw") or "")) + if atmcor_unw.is_file(): + unw = atmcor_unw + coh = Path(outputs["coh"]) + amp = pair_dir / f"{master_date}_{look_text}.amp" + amp_par = pair_dir / f"{master_date}_{look_text}.amp.par" + source_amp = project_dir / "RSLC" / master_date / f"{master_date}_{look_text}.amp" + source_amp_par = project_dir / "RSLC" / master_date / f"{master_date}_{look_text}.amp.par" + source_diff_par = project_dir / "DEM" / f"{master_date}_{look_text}.diff_par" + diff_par = Path(outputs["reflat_diff_par"]) + off_par = pair_dir / f"{pair_name}_{look_text}.off" + utm_to_rdc = project_dir / "DEM" / f"{master_date}_{look_text}.UTM_TO_RDC" + utm_dem_par = project_dir / "DEM" / f"{master_date}_{look_text}.utm.dem.par" + rdc_dem = project_dir / "DEM" / f"{master_date}_{look_text}.rdc.dem" + slc_par = project_dir / "SLC" / master_date / f"{master_date}.slc.par" + + if not amp.is_file() and source_amp.is_file(): + shutil.copy2(source_amp, amp) + if not amp_par.is_file() and source_amp_par.is_file(): + shutil.copy2(source_amp_par, amp_par) + + required = { + "unw": unw, + "coh": coh, + "amp": amp, + "amp_par": amp_par, + "source_diff_par": source_diff_par, + "off_par": off_par, + "utm_to_rdc": utm_to_rdc, + "utm_dem_par": utm_dem_par, + "slc_par": slc_par, + } + missing = [f"{name}={path}" for name, path in required.items() if not path.is_file()] + if missing: + raise FileNotFoundError("Gamma reflatten requires missing native files: " + ", ".join(missing)) + shutil.copy2(source_diff_par, diff_par) + + values = read_gamma_par_file(amp_par) + width = _gamma_par_int(values, "range_samples") + nlines = _gamma_par_int(values, "azimuth_lines") + if width <= 0 or nlines <= 0: + raise RuntimeError(f"Cannot determine reflatten radar grid from: {amp_par}") + dem_values = read_gamma_par_file(utm_dem_par) + geo_width = _gamma_par_int(dem_values, "width") + geo_nlines = _gamma_par_int(dem_values, "nlines") + if geo_width <= 0 or geo_nlines <= 0: + raise RuntimeError(f"Cannot determine reflatten geocoded grid from: {utm_dem_par}") + + log_dir = ensure_directory(run_root / "gamma_reflatten") + primary_threshold = validate_unit_interval(coherence_threshold, "--reflatten-coh-threshold") + fallback_threshold = validate_unit_interval( + fallback_coherence_threshold, + "--reflatten-fallback-coh-threshold", + ) + thresholds = [primary_threshold] + if fallback_threshold != primary_threshold: + thresholds.append(fallback_threshold) + + model_code = _gamma_reflatten_model_code(model) + command_results: List[Dict[str, Any]] = [] + last_error = "" + selected_threshold = primary_threshold + reflat_mask = Path(outputs["reflat_mask"]) + reflat_trend = Path(outputs["reflat_trend"]) + reflat_unw = Path(outputs["reflat_unw"]) + + for attempt_index, threshold in enumerate(thresholds, start=1): + selected_threshold = threshold + suffix = "" if attempt_index == 1 else f".fallback{attempt_index}" + mask_path = reflat_mask if attempt_index == 1 else Path(str(reflat_mask) + suffix + ".bmp") + trend_path = reflat_trend if attempt_index == 1 else Path(str(reflat_trend) + suffix) + try: + command_results.append( + _run_reflatten_command( + command=[ + "rascc_mask", + str(coh), + str(amp), + str(width), + "1", + "1", + "0", + "1", + "1", + format_gamma_number(threshold), + "0.0", + "0.1", + "0.9", + "1.", + ".35", + "1", + str(mask_path), + ], + stage=f"rascc_mask_attempt{attempt_index}", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + command_results.append( + _run_reflatten_command( + command=[ + "quad_fit", + str(unw), + str(diff_par), + str(max(1, int(range_step))), + str(max(1, int(azimuth_step))), + str(mask_path), + "-", + str(model_code), + str(trend_path), + ], + stage=f"quad_fit_attempt{attempt_index}", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + if trend_path != reflat_trend: + shutil.copy2(trend_path, reflat_trend) + if mask_path != reflat_mask: + shutil.copy2(mask_path, reflat_mask) + last_error = "" + break + except Exception as exc: + last_error = str(exc) + print(f"[reflatten] attempt {attempt_index} failed with coherence threshold {threshold:g}: {last_error}") + else: + raise RuntimeError(f"Gamma reflatten failed for all thresholds: {last_error}") + + command_results.append( + _run_reflatten_command( + command=[ + "quad_sub", + str(unw), + str(diff_par), + str(reflat_unw), + "0", + "0", + ], + stage="quad_sub", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + if not reflat_unw.is_file() or reflat_unw.stat().st_size <= 0 or is_binary_all_zero(reflat_unw): + raise RuntimeError(f"Gamma reflatten produced invalid output: {reflat_unw}") + + geo_reflat_unw = Path(outputs["geo_reflat_unw"]) + command_results.append( + _run_reflatten_command( + command=[ + "geocode_back", + str(reflat_unw), + str(width), + str(utm_to_rdc), + str(geo_reflat_unw), + str(geo_width), + str(geo_nlines), + str(geo_interp or "0"), + "0", + ], + stage="geocode_reflat_unw", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + if not geo_reflat_unw.is_file() or geo_reflat_unw.stat().st_size <= 0 or is_binary_all_zero(geo_reflat_unw): + raise RuntimeError(f"Gamma reflatten produced invalid geocoded unwrapped output: {geo_reflat_unw}") + + reflat_los = Path(outputs["reflat_los"]) + reflat_vert = Path(outputs["reflat_vert"]) + hgt_arg = str(rdc_dem) if rdc_dem.is_file() else "-" + command_results.append( + _run_reflatten_command( + command=["dispmap", str(reflat_unw), hgt_arg, str(slc_par), str(off_par), str(reflat_los), "0"], + stage="dispmap_reflat_los", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + command_results.append( + _run_reflatten_command( + command=["dispmap", str(reflat_unw), hgt_arg, str(slc_par), str(off_par), str(reflat_vert), "1"], + stage="dispmap_reflat_vert", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + if not reflat_los.is_file() or reflat_los.stat().st_size <= 0 or is_binary_all_zero(reflat_los): + raise RuntimeError(f"Gamma reflatten produced invalid LOS displacement output: {reflat_los}") + if not reflat_vert.is_file() or reflat_vert.stat().st_size <= 0 or is_binary_all_zero(reflat_vert): + raise RuntimeError(f"Gamma reflatten produced invalid vertical displacement output: {reflat_vert}") + geo_reflat_los = Path(outputs["geo_reflat_los"]) + geo_reflat_vert = Path(outputs["geo_reflat_vert"]) + command_results.append( + _run_reflatten_command( + command=[ + "geocode_back", + str(reflat_los), + str(width), + str(utm_to_rdc), + str(geo_reflat_los), + str(geo_width), + str(geo_nlines), + str(geo_interp or "0"), + "0", + ], + stage="geocode_reflat_los", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + if not geo_reflat_los.is_file() or geo_reflat_los.stat().st_size <= 0 or is_binary_all_zero(geo_reflat_los): + raise RuntimeError(f"Gamma reflatten produced invalid geocoded LOS displacement output: {geo_reflat_los}") + command_results.append( + _run_reflatten_command( + command=[ + "geocode_back", + str(reflat_vert), + str(width), + str(utm_to_rdc), + str(geo_reflat_vert), + str(geo_width), + str(geo_nlines), + str(geo_interp or "0"), + "0", + ], + stage="geocode_reflat_vert", + log_dir=log_dir, + env=env, + cwd=project_dir, + ) + ) + if not geo_reflat_vert.is_file() or geo_reflat_vert.stat().st_size <= 0 or is_binary_all_zero(geo_reflat_vert): + raise RuntimeError(f"Gamma reflatten produced invalid geocoded vertical displacement output: {geo_reflat_vert}") + + native_copy_dir = ensure_directory(output_dir / "reflatten") + copied: Dict[str, str] = {} + for name in ( + "reflat_unw", + "reflat_trend", + "reflat_mask", + "geo_reflat_unw", + "reflat_los", + "geo_reflat_los", + "reflat_vert", + "geo_reflat_vert", + ): + source_path = Path(outputs[name]) + if source_path.is_file(): + target_path = native_copy_dir / source_path.name + shutil.copy2(source_path, target_path) + copied[name] = str(target_path) + + return { + "enabled": True, + "applied": True, + "model": str(model or DEFAULT_REFLATTEN_MODEL).strip().lower(), + "model_code": model_code, + "coherence_threshold": float(selected_threshold), + "primary_coherence_threshold": float(primary_threshold), + "fallback_coherence_threshold": float(fallback_threshold), + "range_step": int(range_step), + "azimuth_step": int(azimuth_step), + "input_unwrapped_role": "atmcor_unw" if atmcor_unw.is_file() else "unw", + "input_unwrapped": str(unw), + "paths": {name: outputs[name] for name in outputs if name.startswith("reflat") or name.startswith("geo_reflat")}, + "copied": copied, + "commands": command_results, + } + + +def remove_pair_derived_outputs(*, project_dir: Path, pair_name: str, master_date: str, range_looks: int) -> List[str]: + pair_dir = project_dir / "ifgrams" / pair_name + if not pair_dir.is_dir(): + return [] + + look_text = f"{int(range_looks)}rlks" + patterns = [ + f"{pair_name}_{look_text}.diff*", + f"{pair_name}_{look_text}.los_disp", + f"{pair_name}_{look_text}.atmcor.los_disp", + f"{pair_name}_{look_text}.vert_disp", + f"{pair_name}_{look_text}.atmcor.vert_disp", + f"geo_{pair_name}_{look_text}.diff*", + f"geo_{pair_name}_{look_text}.los_disp", + f"geo_{pair_name}_{look_text}.atmcor.los_disp", + f"geo_{pair_name}_{look_text}.vert_disp", + f"geo_{pair_name}_{look_text}.atmcor.vert_disp", + f"geo_{master_date}_{look_text}.amp*", + f"geo_{master_date}_{look_text}.diff_filt.cor", + f"geo_{master_date}_{look_text}.hgt", + "lv_phi", + "lv_theta", + ] + removed: List[str] = [] + seen: set[str] = set() + for pattern in patterns: + for path in pair_dir.glob(pattern): + if str(path) in seen or not path.is_file(): + continue + seen.add(str(path)) + path.unlink() + removed.append(str(path)) + return removed + + +def rerun_pair_product_stages( + *, + project_name: str, + project_dir: Path, + run_root: Path, + scratch_root: Path, + env: Dict[str, str], + pair_name: str, + master_date: str, + slave_date: str, + range_looks: int, + unwrap: bool, + atmcor: bool, + geocode: bool, +) -> Dict[str, Any]: + print( + f"[repair] PyINT output sanity failed; repair attempt 1/{MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS}; " + "deleting pair derived outputs and rerunning " + "single-pair diff/unwrap/atmcor/geocode stages." + ) + removed = remove_pair_derived_outputs( + project_dir=project_dir, + pair_name=pair_name, + master_date=master_date, + range_looks=range_looks, + ) + + commands: List[tuple[str, List[str]]] = [ + ("diff", ["diff_gamma.py", project_name, master_date, slave_date]), + ] + if unwrap: + commands.append(("unwrap", ["unwrap_gamma.py", project_name, master_date, slave_date])) + if atmcor: + commands.append(("atmcor", ["atm_correction_gamma.py", project_name, master_date, slave_date])) + if geocode: + commands.append(("geocode", ["geocode_gamma.py", project_name, pair_name])) + + log_dir = ensure_directory(run_root / "repair_pair_products") + stage_results: List[Dict[str, Any]] = [] + for stage, command in commands: + stdout_path = log_dir / f"{stage}.stdout.log" + stderr_path = log_dir / f"{stage}.stderr.log" + print( + f"[repair] attempt 1/{MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS} running {stage}: " + f"{' '.join(command)}" + ) + result = run_logged( + command, + env=env, + cwd=scratch_root, + stdout_path=stdout_path, + stderr_path=stderr_path, + mirror_output=False, + ) + stdout_tail = (result.stdout or "")[-2000:] + stderr_tail = (result.stderr or "")[-2000:] + stage_info = { + "stage": stage, + "command": command, + "returncode": int(result.returncode), + "stdout_path": str(stdout_path), + "stderr_path": str(stderr_path), + "stdout_tail": stdout_tail, + "stderr_tail": stderr_tail, + } + stage_results.append(stage_info) + if result.returncode != 0: + detail = (result.stderr or result.stdout or "").strip() + raise RuntimeError( + f"PyINT pair repair attempt 1/{MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS} stage {stage} " + f"failed with rc={result.returncode}: {detail}" + ) + print( + f"[repair] attempt 1/{MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS} {stage} completed; " + f"stdout={stdout_path}, stderr={stderr_path}" + ) + if stderr_tail.strip(): + print(f"[repair] {stage} stderr tail:\n{stderr_tail.strip()}") + + return { + "attempted": True, + "attempt_count": 1, + "max_attempts": MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS, + "removed_outputs": removed, + "stages": stage_results, + } def collect_stage_error_logs(project_dir: Path) -> Dict[str, str]: @@ -448,6 +1212,7 @@ def collect_stage_error_logs(project_dir: Path) -> Dict[str, str]: "coreg_gamma_all.err", "diff_gamma_all.err", "unwrap_gamma_all.err", + "atm_correction_gamma_all.err", "geocode_gamma_all.err", ): path = project_dir / filename @@ -490,6 +1255,850 @@ def copy_native_outputs( } +def read_gamma_par_file(path: Path) -> Dict[str, str]: + values: Dict[str, str] = {} + if not path.is_file(): + return values + for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if ":" in line: + key, value = line.split(":", 1) + else: + parts = line.split(None, 1) + if len(parts) != 2: + continue + key, value = parts + values[key.strip()] = value.strip() + return values + + +def _first_gamma_token(values: Dict[str, str], key: str) -> str: + value = str(values.get(key) or "").strip() + return value.split()[0] if value.split() else "" + + +def _gamma_par_int(values: Dict[str, str], key: str) -> int: + token = _first_gamma_token(values, key) + return int(float(token)) if token else 0 + + +def _gamma_par_float(values: Dict[str, str], key: str) -> float | None: + token = _first_gamma_token(values, key) + if not token: + return None + try: + return float(token) + except ValueError: + return None + + +def collect_gamma_grid_metadata(dem_par: Path, *, target_grid_size_m: int) -> Dict[str, Any]: + values = read_gamma_par_file(dem_par) + width = _gamma_par_int(values, "width") + nlines = _gamma_par_int(values, "nlines") + corner_lat = _gamma_par_float(values, "corner_lat") + corner_lon = _gamma_par_float(values, "corner_lon") + post_lat = _gamma_par_float(values, "post_lat") + post_lon = _gamma_par_float(values, "post_lon") + mid_lat = None + if corner_lat is not None and post_lat is not None and nlines > 0: + mid_lat = corner_lat + post_lat * (nlines - 1) / 2.0 + + lat_spacing_m = abs(post_lat) * 111_320.0 if post_lat is not None else None + lon_spacing_m = None + if post_lon is not None: + scale_lat = mid_lat if mid_lat is not None else corner_lat + cos_lat = math.cos(math.radians(scale_lat or 0.0)) + lon_spacing_m = abs(post_lon) * 111_320.0 * max(abs(cos_lat), 0.01) + + average_spacing_m = None + if lat_spacing_m is not None and lon_spacing_m is not None: + average_spacing_m = (lat_spacing_m + lon_spacing_m) / 2.0 + elif lat_spacing_m is not None: + average_spacing_m = lat_spacing_m + elif lon_spacing_m is not None: + average_spacing_m = lon_spacing_m + + mismatch_ratio = None + if target_grid_size_m > 0 and average_spacing_m is not None: + mismatch_ratio = abs(average_spacing_m - float(target_grid_size_m)) / float(target_grid_size_m) + + return { + "dem_par": str(dem_par), + "projection": _first_gamma_token(values, "DEM_projection"), + "epsg": _first_gamma_token(values, "EPSG"), + "data_format": _first_gamma_token(values, "data_format"), + "width": width, + "nlines": nlines, + "corner_lat": corner_lat, + "corner_lon": corner_lon, + "post_lat_deg": post_lat, + "post_lon_deg": post_lon, + "mid_lat": mid_lat, + "pixel_spacing_lat_m": lat_spacing_m, + "pixel_spacing_lon_m": lon_spacing_m, + "average_pixel_spacing_m": average_spacing_m, + "target_grid_size_m": int(target_grid_size_m or 0), + "target_grid_mismatch_ratio": mismatch_ratio, + } + + +def _import_numpy() -> Any: + try: + import numpy as np # type: ignore + except Exception as exc: + raise RuntimeError("numpy is required for Gamma quality statistics.") from exc + return np + + +def _select_gamma_float_array( + path: Path, + *, + expected_count: int, + kind: str, +) -> Tuple[Any, str, Dict[str, Any]]: + if expected_count <= 0: + raise RuntimeError(f"Cannot read Gamma float data without valid raster dimensions: {path}") + if not path.is_file(): + raise FileNotFoundError(f"Gamma float source not found: {path}") + expected_bytes = expected_count * 4 + actual_bytes = path.stat().st_size + if actual_bytes < expected_bytes: + raise RuntimeError( + f"Gamma float source is smaller than expected: {path} " + f"({actual_bytes} < {expected_bytes} bytes)" + ) + + np = _import_numpy() + best_array: Any = None + best_dtype = "" + best_info: Dict[str, Any] = {} + best_score = -1.0 + for dtype_text in (">f4", "= 0.0) & (array <= 1.0) + else: + plausible = finite & (np.abs(array) < 1000.0) + score = float(np.count_nonzero(plausible)) / float(expected_count) + info = { + "dtype": dtype_text, + "plausible_percent": score * 100.0, + "finite_percent": (float(np.count_nonzero(finite)) / float(expected_count)) * 100.0, + "size_bytes": int(actual_bytes), + } + if score > best_score: + best_score = score + best_array = array + best_dtype = dtype_text + best_info = info + + if best_array is None: + raise RuntimeError(f"Unable to read Gamma float source with a plausible byte order: {path}") + if kind == "coherence" and best_score < 0.80: + raise RuntimeError(f"Gamma coherence source has implausible float values: {path}") + if kind != "coherence" and best_score < 0.50: + raise RuntimeError(f"Gamma displacement source has implausible float values: {path}") + return best_array, best_dtype, best_info + + +def _float_stats(values: Any, *, total_count: int) -> Dict[str, Any]: + np = _import_numpy() + if values is None: + values = np.asarray([], dtype=np.float32) + finite_values = values[np.isfinite(values)] + count = int(finite_values.size) + result: Dict[str, Any] = { + "count": count, + "total_count": int(total_count), + "valid_percent": (float(count) / float(total_count) * 100.0) if total_count > 0 else 0.0, + } + if count == 0: + result.update( + { + "min": None, + "max": None, + "mean": None, + "std": None, + "p02": None, + "p50": None, + "p98": None, + } + ) + return result + result.update( + { + "min": float(np.min(finite_values)), + "max": float(np.max(finite_values)), + "mean": float(np.mean(finite_values)), + "std": float(np.std(finite_values)), + "p02": float(np.percentile(finite_values, 2)), + "p50": float(np.percentile(finite_values, 50)), + "p98": float(np.percentile(finite_values, 98)), + } + ) + return result + + +def create_gamma_quality_report( + *, + disp_source: Path, + coh_source: Path, + dem_par: Path, + pair_name: str, + master_date: str, + range_looks: int, + azimuth_looks: int, + target_grid_size_m: int, + coherence_threshold: float, + reference_mode: str, + reference_coh_threshold: float, + deramp_mode: str, + deramp_coh_threshold: float, + product_source_dir: Path, +) -> Dict[str, Any]: + np = _import_numpy() + grid = collect_gamma_grid_metadata(dem_par, target_grid_size_m=target_grid_size_m) + width = int(grid.get("width") or 0) + nlines = int(grid.get("nlines") or 0) + total_count = width * nlines + if total_count <= 0: + raise RuntimeError(f"Gamma DEM parameter file does not contain usable width/nlines: {dem_par}") + + disp, disp_dtype, disp_read = _select_gamma_float_array( + disp_source, + expected_count=total_count, + kind="displacement", + ) + coh, coh_dtype, coh_read = _select_gamma_float_array( + coh_source, + expected_count=total_count, + kind="coherence", + ) + disp = disp.reshape((nlines, width)) + coh = coh.reshape((nlines, width)) + + disp_valid = np.isfinite(disp) & (np.abs(disp) < 1000.0) & (disp != 0.0) + coh_valid = np.isfinite(coh) & (coh > 0.0) & (coh <= 1.0) + product_support = disp_valid & coh_valid & (coh >= float(coherence_threshold)) + coherence_valid_count = int(np.count_nonzero(coh_valid)) + threshold_support: Dict[str, Any] = {} + for threshold in QUALITY_COHERENCE_THRESHOLDS: + key = f"ge_{threshold:.2f}" + count = int(np.count_nonzero(coh_valid & (coh >= threshold))) + threshold_support[key] = { + "count": count, + "percent_of_valid_coherence": ( + (float(count) / float(coherence_valid_count)) * 100.0 + if coherence_valid_count > 0 + else 0.0 + ), + "percent_of_raster": (float(count) / float(total_count)) * 100.0, + } + + raw_disp_stats = _float_stats(disp[disp_valid], total_count=total_count) + product_support_stats = _float_stats(disp[product_support], total_count=total_count) + coherence_stats = _float_stats(coh[coh_valid], total_count=total_count) + flags: List[Dict[str, Any]] = [] + coherence_mean = coherence_stats.get("mean") + if isinstance(coherence_mean, (int, float)) and float(coherence_mean) < 0.40: + flags.append( + { + "code": "low_mean_coherence", + "level": "warning", + "value": float(coherence_mean), + "threshold": 0.40, + "message": "Mean valid coherence is below the production review threshold.", + } + ) + if float(product_support_stats.get("valid_percent") or 0.0) < 40.0: + flags.append( + { + "code": "low_coherence_support", + "level": "warning", + "value": float(product_support_stats.get("valid_percent") or 0.0), + "threshold": 40.0, + "message": "Less than 40 percent of raster pixels meet the configured coherence support threshold.", + } + ) + mismatch_ratio = grid.get("target_grid_mismatch_ratio") + if isinstance(mismatch_ratio, (int, float)) and float(mismatch_ratio) > GRID_MISMATCH_TOLERANCE: + flags.append( + { + "code": "geocoded_grid_mismatch", + "level": "info", + "value": float(mismatch_ratio), + "threshold": GRID_MISMATCH_TOLERANCE, + "message": "Actual Gamma geocoded spacing differs from the requested target grid.", + } + ) + if not str(grid.get("epsg") or "").strip(): + flags.append( + { + "code": "missing_epsg", + "level": "info", + "message": "Gamma DEM parameter file does not carry an EPSG code.", + } + ) + + return { + "pair_name": pair_name, + "master_date": master_date, + "production_mode": "gamma_native", + "python_data_processing_applied": False, + "range_looks": int(range_looks), + "azimuth_looks": int(azimuth_looks), + "target_grid_size_m": int(target_grid_size_m or 0), + "coherence_quality_threshold": float(coherence_threshold), + "coherence_support_threshold": float(coherence_threshold), + "generated_sources": {}, + "sources": { + "displacement": str(disp_source), + "coherence": str(coh_source), + "dem_par": str(dem_par), + }, + "byte_order_detection": { + "displacement": {**disp_read, "selected_dtype": disp_dtype}, + "coherence": {**coh_read, "selected_dtype": coh_dtype}, + }, + "grid": grid, + "coherence": { + "valid_stats": coherence_stats, + "threshold_support": threshold_support, + }, + "reference": { + "mode": str(reference_mode or "none").strip().lower() or "none", + "applied": False, + "reason": "not_applied_in_python_layer", + "selection_threshold": float(reference_coh_threshold), + }, + "deramp": { + "mode": str(deramp_mode or "none").strip().lower() or "none", + "applied": False, + "reason": "not_applied_in_python_layer", + "selection_threshold": float(deramp_coh_threshold), + }, + "displacement": { + "raw_valid_stats": raw_disp_stats, + "coherence_support_stats": product_support_stats, + "raw_valid_percent": float(raw_disp_stats.get("valid_percent") or 0.0), + "coherence_support_percent": float(product_support_stats.get("valid_percent") or 0.0), + }, + "quality_flags": flags, + } + + +def _run_data2geotiff( + *, + dem_par: Path, + source_file: Path, + target_file: Path, + nodata_value: float, + env: Dict[str, str], + cwd: Path, + stdout_path: Path, + stderr_path: Path, +) -> Dict[str, Any]: + if not dem_par.is_file(): + raise FileNotFoundError(f"Gamma DEM parameter file not found: {dem_par}") + if not source_file.is_file(): + raise FileNotFoundError(f"Gamma geocoded source file not found: {source_file}") + + ensure_directory(target_file.parent) + if target_file.exists(): + target_file.unlink() + + result = run_logged( + [ + "data2geotiff", + str(dem_par), + str(source_file), + "2", + str(target_file), + format_gamma_number(nodata_value), + ], + env=env, + cwd=cwd, + stdout_path=stdout_path, + stderr_path=stderr_path, + ) + if result.returncode != 0: + detail = (result.stderr or result.stdout or "").strip() + raise RuntimeError( + f"data2geotiff failed for {source_file.name} with rc={result.returncode}: {detail}" + ) + if not target_file.is_file() or target_file.stat().st_size <= 0: + raise RuntimeError(f"data2geotiff did not create a usable output: {target_file}") + + return { + "source": str(source_file), + "target": str(target_file), + "dem_par": str(dem_par), + "nodata_value": float(nodata_value), + "stdout_path": str(stdout_path), + "stderr_path": str(stderr_path), + "size_bytes": int(target_file.stat().st_size), + } + + +def _write_gamma_zero_as_nodata_source( + *, + source_file: Path, + target_file: Path, + expected_count: int, + nodata_value: float, + kind: str, +) -> Dict[str, Any]: + np = _import_numpy() + array, dtype_text, read_info = _select_gamma_float_array( + source_file, + expected_count=expected_count, + kind=kind, + ) + mask = np.isfinite(array) & (array == 0.0) + replacement_count = int(np.count_nonzero(mask)) + ensure_directory(target_file.parent) + output = np.array(array, dtype=np.dtype(dtype_text), copy=True) + output[mask] = np.array(nodata_value, dtype=np.dtype(dtype_text)) + output.tofile(str(target_file)) + return { + "enabled": True, + "source": str(source_file), + "target": str(target_file), + "dtype": dtype_text, + "read": read_info, + "zero_count": replacement_count, + "total_count": int(expected_count), + "zero_percent": (float(replacement_count) / float(expected_count)) * 100.0 if expected_count > 0 else 0.0, + "nodata_value": float(nodata_value), + } + + +def _run_gamma_native_geotiff_export( + *, + dem_par: Path, + source_file: Path, + target_file: Path, + width: int, + nlines: int, + nodata_value: float, + nodata_source_dir: Path, + log_dir: Path, + log_name: str, + env: Dict[str, str], + cwd: Path, + replace_zero_with_nodata: bool = False, +) -> Dict[str, Any]: + export_source = source_file + zero_to_nodata: Dict[str, Any] = { + "enabled": False, + "reason": "disabled_for_product", + "nodata_value": float(nodata_value), + } + if replace_zero_with_nodata: + export_source = nodata_source_dir / f"{source_file.name}.zero_as_nodata" + zero_to_nodata = _write_gamma_zero_as_nodata_source( + source_file=source_file, + target_file=export_source, + expected_count=int(width) * int(nlines), + nodata_value=nodata_value, + kind="coherence" if "coh" in log_name.lower() else "displacement", + ) + geotiff_result = _run_data2geotiff( + dem_par=dem_par, + source_file=export_source, + target_file=target_file, + nodata_value=nodata_value, + env=env, + cwd=cwd, + stdout_path=log_dir / f"data2geotiff_{log_name}.stdout.log", + stderr_path=log_dir / f"data2geotiff_{log_name}.stderr.log", + ) + return { + **geotiff_result, + "production_mode": "gamma_native", + "original_source": str(source_file), + "export_source": str(export_source), + "zero_to_nodata": zero_to_nodata, + } + + +def _run_optional_gamma_native_geotiff_export( + *, + dem_par: Path, + source_file: Path, + target_file: Path, + width: int, + nlines: int, + nodata_value: float, + nodata_source_dir: Path, + log_dir: Path, + log_name: str, + env: Dict[str, str], + cwd: Path, + replace_zero_with_nodata: bool = False, +) -> Dict[str, Any]: + if not source_file.is_file(): + return { + "enabled": False, + "reason": "source_missing", + "source": str(source_file), + "target": str(target_file), + } + try: + result = _run_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=source_file, + target_file=target_file, + width=width, + nlines=nlines, + nodata_value=nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name=log_name, + env=env, + cwd=cwd, + replace_zero_with_nodata=replace_zero_with_nodata, + ) + except Exception as exc: + return { + "enabled": False, + "reason": "export_failed", + "error": str(exc), + "source": str(source_file), + "target": str(target_file), + } + return {"enabled": True, **result} + + +def _copy_product_alias(source: Dict[str, Any], target_file: Path, *, alias_of: str) -> Dict[str, Any]: + source_target = Path(str(source.get("target") or "")) + if not source_target.is_file(): + raise FileNotFoundError(f"Cannot create product alias, source GeoTIFF is missing: {source_target}") + ensure_directory(target_file.parent) + if target_file.exists(): + target_file.unlink() + shutil.copy2(source_target, target_file) + return { + **source, + "target": str(target_file), + "size_bytes": int(target_file.stat().st_size), + "compatibility_alias_of": alias_of, + } + + +def export_standard_products( + *, + project_dir: Path, + output_dir: Path, + pair_name: str, + master_date: str, + range_looks: int, + azimuth_looks: int, + target_grid_size_m: int, + coherence_mask_threshold: float, + reference_mode: str, + reference_coh_threshold: float, + deramp_mode: str, + deramp_coh_threshold: float, + atmcor_enabled: bool, + atmcor_use_for_disp: bool, + reflatten_summary: Dict[str, Any], + gamma_nodata_value: float, + outputs: Dict[str, str], + env: Dict[str, str], + run_root: Path, +) -> Dict[str, Any]: + run_dir = output_dir.parent if output_dir.name.lower() == "native" else output_dir + assets_dir = run_dir / "assets" + disp_path = assets_dir / "disp" / "disp.tif" + disp_unmasked_path = assets_dir / "disp" / "disp_unmasked.tif" + coh_path = assets_dir / "coh" / "coh.tif" + look_text = f"{int(range_looks)}rlks" + dem_par = project_dir / "DEM" / f"{master_date}_{look_text}.utm.dem.par" + + reflatten_applied = bool((reflatten_summary or {}).get("applied")) + disp_source = Path(str(outputs.get("geo_los") or "")).resolve() + disp_source_role = "geo_los" + if reflatten_applied: + reflat_los_source = Path(str(outputs.get("geo_reflat_los") or "")).resolve() + reflat_unw_source = Path(str(outputs.get("geo_reflat_unw") or "")).resolve() + if reflat_los_source.is_file(): + disp_source = reflat_los_source + disp_source_role = "geo_reflat_los" + elif reflat_unw_source.is_file(): + disp_source = reflat_unw_source + disp_source_role = "geo_reflat_unw" + else: + raise FileNotFoundError( + "Gamma reflatten was applied but no geocoded reflattened displacement or " + "unwrapped phase source was found." + ) + elif atmcor_enabled and atmcor_use_for_disp: + atmcor_los_source = Path(str(outputs.get("geo_atmcor_los") or "")).resolve() + atmcor_unw_source = Path(str(outputs.get("geo_atmcor_unw") or "")).resolve() + if atmcor_los_source.is_file(): + disp_source = atmcor_los_source + disp_source_role = "geo_atmcor_los" + elif atmcor_unw_source.is_file(): + disp_source = atmcor_unw_source + disp_source_role = "geo_atmcor_unw" + else: + raise FileNotFoundError( + "atmcor_use_for_disp is enabled but no geocoded atmospheric-corrected " + "PyINT/Gamma displacement or unwrapped source was found." + ) + if not disp_source.is_file(): + disp_source = Path(str(outputs.get("geo_unw") or "")).resolve() + disp_source_role = "geo_unw" + coh_source = Path(str(outputs.get("geo_coh") or "")).resolve() + + if not disp_source.is_file(): + raise FileNotFoundError("No geocoded PyINT displacement source found for standard export.") + if not coh_source.is_file(): + raise FileNotFoundError("No geocoded PyINT coherence source found for standard export.") + + log_dir = ensure_directory(run_root / "standard_products") + nodata_source_dir = ensure_directory(output_dir / "ifgrams" / pair_name) + grid = collect_gamma_grid_metadata(dem_par, target_grid_size_m=target_grid_size_m) + width = int(grid.get("width") or 0) + nlines = int(grid.get("nlines") or 0) + if width <= 0 or nlines <= 0: + raise RuntimeError(f"Gamma DEM parameter file does not contain usable width/nlines: {dem_par}") + + quality_report = create_gamma_quality_report( + disp_source=disp_source, + coh_source=coh_source, + dem_par=dem_par, + pair_name=pair_name, + master_date=master_date, + range_looks=range_looks, + azimuth_looks=azimuth_looks, + target_grid_size_m=target_grid_size_m, + coherence_threshold=coherence_mask_threshold, + reference_mode=reference_mode, + reference_coh_threshold=reference_coh_threshold, + deramp_mode=deramp_mode, + deramp_coh_threshold=deramp_coh_threshold, + product_source_dir=nodata_source_dir, + ) + quality_report["gamma_nodata_value"] = float(gamma_nodata_value) + quality_report["export_policy"] = { + "mode": "gamma_native", + "python_data_processing_applied": False, + "gamma_reflatten_applied": bool(reflatten_applied), + "zero_to_nodata_tool": "", + "geotiff_tool": "data2geotiff", + "primary": ( + "gamma_reflattened_geocoded_los_displacement" + if disp_source_role == "geo_reflat_los" + else "gamma_reflattened_geocoded_unwrapped_phase" + if disp_source_role == "geo_reflat_unw" + else "gamma_geocoded_los_displacement" + ), + "coherence_threshold_usage": "quality_support_only", + "display_disp_zero_to_nodata": False, + "raw_disp_unmasked_preserved": True, + } + quality_report["reflatten"] = { + "enabled": bool((reflatten_summary or {}).get("enabled")), + "applied": bool(reflatten_applied), + "source_role": disp_source_role, + "summary": reflatten_summary or {}, + } + disp_unmasked_export = _run_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=disp_source, + target_file=disp_unmasked_path, + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="disp_unmasked", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ) + disp_export = _run_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=disp_source, + target_file=disp_path, + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="disp", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ) + coh_export = _run_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=coh_source, + target_file=coh_path, + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="coh", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ) + optional_exports = { + "disp_vertical": _run_optional_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=Path(str(outputs.get("geo_vert") or "")).resolve(), + target_file=assets_dir / "disp" / "disp_vertical.tif", + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="disp_vertical", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ), + "disp_vertical_atmcor": _run_optional_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=Path(str(outputs.get("geo_atmcor_vert") or "")).resolve(), + target_file=assets_dir / "disp" / "disp_vertical_atmcor.tif", + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="disp_vertical_atmcor", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ), + "wrapped_phase": _run_optional_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=Path(str(outputs.get("geo_wrapped_phase") or "")).resolve(), + target_file=assets_dir / "phase" / "wrapped_phase.tif", + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="wrapped_phase", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ), + "look_vector_theta": _run_optional_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=Path(str(outputs.get("look_vector_theta") or "")).resolve(), + target_file=assets_dir / "look_vector" / "theta.tif", + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="look_vector_theta", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ), + "look_vector_phi": _run_optional_gamma_native_geotiff_export( + dem_par=dem_par, + source_file=Path(str(outputs.get("look_vector_phi") or "")).resolve(), + target_file=assets_dir / "look_vector" / "phi.tif", + width=width, + nlines=nlines, + nodata_value=gamma_nodata_value, + nodata_source_dir=nodata_source_dir, + log_dir=log_dir, + log_name="look_vector_phi", + env=env, + cwd=project_dir, + replace_zero_with_nodata=False, + ), + } + primary_kind_by_role = { + "geo_reflat_los": "gamma_reflattened_geocoded_los_displacement", + "geo_reflat_unw": "gamma_reflattened_geocoded_unwrapped_phase", + "geo_los": "gamma_geocoded_los_displacement", + "geo_atmcor_los": "gamma_atmcor_geocoded_los_displacement", + "geo_atmcor_unw": "gamma_atmcor_geocoded_unwrapped_phase", + "geo_unw": "gamma_geocoded_unwrapped_phase", + } + primary_kind = primary_kind_by_role.get(disp_source_role, "gamma_geocoded_unwrapped_phase") + quality_report["export_policy"]["primary"] = primary_kind + quality_report["export_policy"]["zero_to_nodata"] = disp_export.get("zero_to_nodata", {}) + quality_report["exports"] = { + "disp": { + "target": disp_export.get("target"), + "source": disp_export.get("source"), + "original_source": disp_export.get("original_source"), + "export_source": disp_export.get("export_source"), + "zero_to_nodata": disp_export.get("zero_to_nodata"), + }, + "disp_unmasked": { + "target": disp_unmasked_export.get("target"), + "source": disp_unmasked_export.get("source"), + "original_source": disp_unmasked_export.get("original_source"), + "export_source": disp_unmasked_export.get("export_source"), + "zero_to_nodata": disp_unmasked_export.get("zero_to_nodata"), + }, + "coh": { + "target": coh_export.get("target"), + "source": coh_export.get("source"), + "original_source": coh_export.get("original_source"), + "export_source": coh_export.get("export_source"), + "zero_to_nodata": coh_export.get("zero_to_nodata"), + }, + } + quality_dir = ensure_directory(run_dir / "quality") + quality_report_path = write_text( + quality_dir / "quality_report.json", + json.dumps(quality_report, ensure_ascii=True, indent=2) + "\n", + ) + + return { + "enabled": True, + "run_dir": str(run_dir), + "assets_dir": str(assets_dir), + "production_mode": "gamma_native", + "python_data_processing_applied": False, + "gamma_reflatten_applied": bool(reflatten_applied), + "primary": "disp", + "primary_kind": primary_kind, + "reflatten": reflatten_summary or {}, + "atmcor": { + "enabled": bool(atmcor_enabled), + "use_for_disp": bool(atmcor_use_for_disp), + "source_role": disp_source_role, + }, + "gamma_nodata_value": float(gamma_nodata_value), + "grid": grid, + "coherence_quality_threshold": float(coherence_mask_threshold), + "coherence_support_threshold": float(coherence_mask_threshold), + "masking": { + "enabled": False, + "reason": "not_applied_in_python_layer", + }, + "reference": {"enabled": False, "reason": "not_applied_in_python_layer"}, + "deramp": {"enabled": False, "reason": "not_applied_in_python_layer"}, + "disp": disp_export, + "disp_unmasked": disp_unmasked_export, + "coh": coh_export, + "optional": optional_exports, + "quality_report": str(quality_report_path), + "quality": quality_report, + } + + def collect_orbit_bridge_summaries(project_dir: Path) -> List[Dict[str, Any]]: summaries: List[Dict[str, Any]] = [] slc_root = project_dir / "SLC" @@ -577,6 +2186,39 @@ def main() -> int: 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 {} + dem_oversampling = calculate_dem_oversampling( + dem_resolution_m=float(args.dem_resolution_m), + target_grid_size_m=float(args.target_grid_size_m or 0), + dem_lat_ovr=float(args.dem_lat_ovr or 0.0), + dem_lon_ovr=float(args.dem_lon_ovr or 0.0), + ) + unwrap_coh_threshold = validate_unit_interval(args.unwrap_coh_threshold, "--unwrap-coh-threshold") + coherence_mask_threshold = validate_unit_interval(args.coherence_mask_threshold, "--coherence-mask-threshold") + reference_mode = str(args.reference_mode or DEFAULT_REFERENCE_MODE).strip().lower() or DEFAULT_REFERENCE_MODE + deramp_mode = str(args.deramp_mode or DEFAULT_DERAMP_MODE).strip().lower() or DEFAULT_DERAMP_MODE + reference_coh_threshold = validate_unit_interval(args.reference_coh_threshold, "--reference-coh-threshold") + deramp_coh_threshold = validate_unit_interval(args.deramp_coh_threshold, "--deramp-coh-threshold") + geo_interp = str(args.geo_interp or "1").strip() + if geo_interp not in {"0", "1"}: + raise ValueError("--geo-interp must be 0 or 1.") + gamma_nodata_value = float(args.gamma_nodata_value) + if not math.isfinite(gamma_nodata_value): + raise ValueError("--gamma-nodata-value must be a finite number.") + reflatten_model = str(args.reflatten_model or DEFAULT_REFLATTEN_MODEL).strip().lower() + if reflatten_model == "linear": + reflatten_model = "plane" + if reflatten_model not in {"plane", "quadratic"}: + raise ValueError("--reflatten-model must be plane or quadratic.") + reflatten_coh_threshold = validate_unit_interval( + args.reflatten_coh_threshold, + "--reflatten-coh-threshold", + ) + reflatten_fallback_coh_threshold = validate_unit_interval( + args.reflatten_fallback_coh_threshold, + "--reflatten-fallback-coh-threshold", + ) + reflatten_range_step = max(1, int(args.reflatten_range_step or DEFAULT_REFLATTEN_RANGE_STEP)) + reflatten_azimuth_step = max(1, int(args.reflatten_azimuth_step or DEFAULT_REFLATTEN_AZIMUTH_STEP)) require_task_layout(task_dir) if not pyint_app_script.is_file(): @@ -638,6 +2280,16 @@ def main() -> int: master_date=master_date, range_looks=args.range_looks, azimuth_looks=args.azimuth_looks, + target_grid_size_m=int(args.target_grid_size_m or 0), + dem_lat_ovr=dem_oversampling["dem_lat_ovr"], + dem_lon_ovr=dem_oversampling["dem_lon_ovr"], + unwrap_coh_threshold=unwrap_coh_threshold, + geo_interp=geo_interp, + atmcor=bool(args.atmcor), + atmcor_use_for_disp=bool(args.atmcor_use_for_disp), + reflatten=bool(args.reflatten), + reflatten_model=reflatten_model, + reflatten_coh_threshold=reflatten_coh_threshold, parallel_workers=args.parallel_workers, unwrap=bool(args.unwrap), geocode=bool(args.geocode), @@ -652,6 +2304,8 @@ def main() -> int: scratch_root = ensure_directory(project_dir.parent) archive_materialization: List[Dict[str, str]] = [] env = os.environ.copy() + if args.gamma_env_script: + env.update(load_shell_environment(args.gamma_env_script, env)) env.update( { "SCRATCHDIR": str(scratch_root), @@ -733,16 +2387,95 @@ def main() -> int: expected_dates=(master_date, slave_date), ) + repair_summary: Dict[str, Any] = { + "attempted": False, + "attempt_count": 0, + "max_attempts": MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS, + } 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) + try: + 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) + except RuntimeError as exc: + print(f"[repair] initial output check failed: {exc}") + repair_summary = rerun_pair_product_stages( + project_name=args.project_name, + project_dir=pyint_project_dir, + run_root=run_root, + scratch_root=scratch_root, + env=env, + pair_name=pair_name, + master_date=master_date, + slave_date=slave_date, + range_looks=args.range_looks, + unwrap=bool(args.unwrap), + atmcor=bool(args.atmcor), + geocode=bool(args.geocode), + ) + 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), + ) + try: + assert_output_sanity(output_sanity_checks) + except RuntimeError as repair_exc: + raise RuntimeError( + f"{exc}; pair repair attempt 1/{MAX_PAIR_PRODUCT_REPAIR_ATTEMPTS} was attempted " + f"but outputs are still invalid, no further automatic repair will be attempted: {repair_exc}" + ) from repair_exc stage_error_logs = collect_stage_error_logs(pyint_project_dir) + reflatten_summary: Dict[str, Any] = { + "enabled": bool(args.reflatten), + "applied": False, + "reason": "", + "model": reflatten_model, + "coherence_threshold": reflatten_coh_threshold, + "fallback_coherence_threshold": reflatten_fallback_coh_threshold, + "range_step": reflatten_range_step, + "azimuth_step": reflatten_azimuth_step, + } + if bool(args.reflatten) and bool(args.unwrap): + print( + "[reflatten] running Gamma residual phase reflattening " + f"model={reflatten_model}, coh={reflatten_coh_threshold:g}" + ) + reflatten_summary = run_gamma_reflatten( + project_dir=pyint_project_dir, + run_root=run_root, + output_dir=output_dir, + outputs=outputs, + pair_name=pair_name, + master_date=master_date, + range_looks=args.range_looks, + env=env, + model=reflatten_model, + coherence_threshold=reflatten_coh_threshold, + fallback_coherence_threshold=reflatten_fallback_coh_threshold, + range_step=reflatten_range_step, + azimuth_step=reflatten_azimuth_step, + geo_interp=geo_interp, + ) + outputs = collect_expected_outputs(pyint_project_dir, pair_name, args.range_looks) + output_sanity_checks = collect_output_sanity_checks( + outputs, + unwrap=bool(args.unwrap), + geocode=bool(args.geocode), + ) + assert_output_sanity(output_sanity_checks) + elif bool(args.reflatten): + reflatten_summary["reason"] = "unwrap disabled" + else: + reflatten_summary["reason"] = "disabled" + copied_paths = copy_native_outputs( project_dir=pyint_project_dir, output_dir=output_dir, @@ -753,6 +2486,34 @@ def main() -> int: stderr_path=run_stderr, ) copied_orbit_bridge_paths = copy_orbit_bridge_summaries(orbit_bridge_summaries, output_dir) + standard_products = ( + export_standard_products( + project_dir=pyint_project_dir, + output_dir=output_dir, + pair_name=pair_name, + master_date=master_date, + range_looks=args.range_looks, + azimuth_looks=args.azimuth_looks, + target_grid_size_m=int(args.target_grid_size_m or 0), + coherence_mask_threshold=coherence_mask_threshold, + reference_mode=reference_mode, + reference_coh_threshold=reference_coh_threshold, + deramp_mode=deramp_mode, + deramp_coh_threshold=deramp_coh_threshold, + atmcor_enabled=bool(args.atmcor), + atmcor_use_for_disp=bool(args.atmcor_use_for_disp), + reflatten_summary=reflatten_summary, + gamma_nodata_value=gamma_nodata_value, + outputs=outputs, + env=env, + run_root=run_root, + ) + if bool(args.geocode) + else { + "enabled": False, + "reason": "geocode disabled", + } + ) summary = { "ok": True, @@ -776,6 +2537,8 @@ def main() -> int: "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 ""), + "configured_resolution_m": float(args.dem_resolution_m), + "oversampling": dem_oversampling, "opentopo_dem_type": str(args.opentopo_dem_type or "SRTMGL1").strip(), "opentopo_api_key_configured": bool(str(args.opentopo_api_key or "").strip()), }, @@ -808,8 +2571,34 @@ def main() -> int: "slave_date": slave_date, "pair_name": pair_name, "time_baseline_days": time_baseline_days, + "target_grid_size_m": int(args.target_grid_size_m or 0), "range_looks": int(args.range_looks), "azimuth_looks": int(args.azimuth_looks), + "dem_resolution_m": float(args.dem_resolution_m), + "dem_oversampling": dem_oversampling, + "unwrap_coh_threshold": unwrap_coh_threshold, + "coherence_quality_threshold": coherence_mask_threshold, + "reference_mode": reference_mode, + "reference_coh_threshold": reference_coh_threshold, + "deramp_mode": deramp_mode, + "deramp_coh_threshold": deramp_coh_threshold, + "gamma_nodata_value": gamma_nodata_value, + "geo_interp": geo_interp, + "atmcor": bool(args.atmcor), + "atmcor_use_for_disp": bool(args.atmcor_use_for_disp), + "reflatten": bool(args.reflatten), + "reflatten_model": reflatten_model, + "reflatten_coh_threshold": reflatten_coh_threshold, + "reflatten_fallback_coh_threshold": reflatten_fallback_coh_threshold, + "reflatten_range_step": reflatten_range_step, + "reflatten_azimuth_step": reflatten_azimuth_step, + "gamma_native_export": { + "python_data_processing_applied": False, + "coherence_mask_applied": False, + "reference_applied": False, + "deramp_applied": False, + "reflatten_applied": bool(reflatten_summary.get("applied")), + }, "parallel_workers": int(args.parallel_workers), "unwrap": bool(args.unwrap), "geocode": bool(args.geocode), @@ -820,8 +2609,11 @@ def main() -> int: "archive_materialization": archive_materialization, "workspace_outputs": outputs, "output_sanity_checks": output_sanity_checks, + "output_repair": repair_summary, + "reflatten_summary": reflatten_summary, "copied_outputs": copied_paths, "copied_orbit_bridge_paths": copied_orbit_bridge_paths, + "standard_products": standard_products, "logs": { "generate_stdout": str(generate_stdout), "generate_stderr": str(generate_stderr), @@ -834,8 +2626,8 @@ def main() -> int: } 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)) + write_text(summary_path, json.dumps(summary, ensure_ascii=True, indent=2) + "\n") + print(json.dumps(summary, ensure_ascii=True, indent=2)) return 0 diff --git a/backend/app/routers/dinsar_production.py b/backend/app/routers/dinsar_production.py index e0decc8..274b922 100644 --- a/backend/app/routers/dinsar_production.py +++ b/backend/app/routers/dinsar_production.py @@ -268,6 +268,7 @@ async def submit_run( else: job_type = JOB_TYPE_PYINT_RUN max_attempts = PYINT_PRODUCTION_JOB_MAX_ATTEMPTS + create_managed_run = True if validation_summary is not None: validated_task_count = validation_summary.get("task_count", 0) payload["extra"].update( diff --git a/backend/app/services/dinsar_production_service.py b/backend/app/services/dinsar_production_service.py index 9135201..8335123 100644 --- a/backend/app/services/dinsar_production_service.py +++ b/backend/app/services/dinsar_production_service.py @@ -27,6 +27,7 @@ from .workflow_service import workflow_service TASK_TYPE_DINSAR_PRODUCTION = "IDL_RUN_DINSAR" TASK_TYPE_ISCE2_DINSAR_PRODUCTION = "ISCE2_RUN" +TASK_TYPE_PYINT_DINSAR_PRODUCTION = "PYINT_RUN" RUN_STATUS_PENDING = "PENDING" RUN_STATUS_RUNNING = "RUNNING" RUN_STATUS_COMPLETED = "COMPLETED" @@ -75,6 +76,8 @@ def _task_type_for_engine(engine_code: str) -> str: return TASK_TYPE_DINSAR_PRODUCTION if normalized == "isce2": return TASK_TYPE_ISCE2_DINSAR_PRODUCTION + if normalized in {"pyint", "gamma"}: + return TASK_TYPE_PYINT_DINSAR_PRODUCTION raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}") @@ -84,6 +87,8 @@ def _workflow_name_for_engine(engine_code: str) -> str: return "dinsar_sarscape_production" if normalized == "isce2": return "dinsar_isce2_production" + if normalized in {"pyint", "gamma"}: + return "dinsar_pyint_gamma_production" raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}") @@ -93,6 +98,8 @@ def _workflow_step_name_for_engine(engine_code: str) -> str: return RUNS_STEP_NAME if normalized == "isce2": return "Execute ISCE2 D-InSAR items" + if normalized in {"pyint", "gamma"}: + return "Execute PyINT/Gamma D-InSAR items" raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}") @@ -329,6 +336,82 @@ def _execution_dir(item: DinsarProductionRunItemORM, run_key: str) -> str: return os.path.join(item.results_root_dir, "runs", run_key) +def _first_text(*values: Any) -> str: + for value in values: + text = str(value or "").strip() + if text: + return text + return "" + + +def _read_json_if_exists(path: str) -> Dict[str, Any]: + text = str(path or "").strip() + if not text or not os.path.isfile(text): + return {} + try: + with open(text, "r", encoding="utf-8") as fp: + payload = json.load(fp) + return payload if isinstance(payload, dict) else {} + except Exception: + return {} + + +def _maybe_join(base: str, *parts: str) -> str: + text = str(base or "").strip() + if not text: + return "" + return os.path.normpath(os.path.join(text, *parts)) + + +def _build_output_paths( + *, + engine_code: str, + item: DinsarProductionRunItemORM, + run_key: str, + output_dir: str, + manifest_path: Optional[str] = None, +) -> Dict[str, Any]: + run_dir = os.path.normpath(str(output_dir or _execution_dir(item, run_key))) + native_dir = _maybe_join(run_dir, "native") + paths: Dict[str, Any] = { + "run_dir": run_dir, + "native_dir": native_dir, + "assets_dir": _maybe_join(run_dir, "assets"), + "quality_dir": _maybe_join(run_dir, "quality"), + "manifest_path": str(manifest_path or "").strip(), + } + + if str(engine_code or "").strip().lower() in {"pyint", "gamma"}: + pair_key = _first_text(item.pair_key, os.path.basename(os.path.dirname(os.path.dirname(run_dir)))) + project_name = f"{pair_key}_{run_key}" if pair_key and run_key else "" + work_root = _maybe_join(settings.PYINT_WORK_ROOT, pair_key, run_key) + project_dir = _maybe_join(work_root, project_name) if project_name else "" + + summary_payload = _read_json_if_exists(_maybe_join(native_dir, "pyint_run_summary.json")) + summary_project_dir = _first_text(summary_payload.get("project_dir")) + project_dir = summary_project_dir or project_dir + + master_date = _first_text(summary_payload.get("master_date")) + slave_date = _first_text(summary_payload.get("slave_date")) + pair_name = f"{master_date}-{slave_date}" if master_date and slave_date else "" + ifgrams_dir = _maybe_join(project_dir, "ifgrams", pair_name) if pair_name else _maybe_join(project_dir, "ifgrams") + + paths.update( + { + "work_dir": work_root, + "project_dir": project_dir, + "ifgrams_dir": ifgrams_dir, + "reflatten_dir": _maybe_join(run_dir, "gamma_reflatten"), + "native_reflatten_dir": _maybe_join(native_dir, "reflatten"), + "pyint_summary_path": _maybe_join(native_dir, "pyint_run_summary.json"), + "stdout_log": _maybe_join(work_root, "pyint.stdout.log"), + "stderr_log": _maybe_join(work_root, "pyint.stderr.log"), + } + ) + + return paths + + def _sanitize_pointer_fragment(value: str, default: str) -> str: text = _SAFE_POINTER_RE.sub("_", str(value or "").strip()).strip("._") return text or default @@ -616,6 +699,7 @@ class DinsarProductionService: ) result = await db.execute(stmt) runs = result.scalars().all() + run_ids = [run.run_id for run in runs if run.run_id] pending_reconcile = [ run for run in runs @@ -636,6 +720,15 @@ class DinsarProductionService: ) or changed if changed: await db.commit() + items_by_run_id: Dict[str, List[DinsarProductionRunItemORM]] = {} + if run_ids: + items_result = await db.execute( + select(DinsarProductionRunItemORM) + .where(DinsarProductionRunItemORM.run_id.in_(run_ids)) + .order_by(DinsarProductionRunItemORM.order_index.asc(), DinsarProductionRunItemORM.id.asc()) + ) + for item in items_result.scalars().all(): + items_by_run_id.setdefault(item.run_id, []).append(item) return { "runs": [ { @@ -656,6 +749,29 @@ class DinsarProductionService: "completed_items": run.completed_items, "failed_items": run.failed_items, "skipped_items": run.skipped_items, + "items": [ + { + "task_name": item.task_name, + "task_alias": item.task_alias, + "pair_key": item.pair_key, + "status": item.status, + "current_step": item.current_step, + "latest_run_key": item.latest_run_key, + "latest_output_dir": item.latest_output_dir, + "latest_manifest_path": item.latest_manifest_path, + "last_error": item.last_error, + "paths": _build_output_paths( + engine_code=run.engine_code, + item=item, + run_key=str(item.latest_run_key or ""), + output_dir=str(item.latest_output_dir or _execution_dir(item, str(item.latest_run_key or ""))), + manifest_path=item.latest_manifest_path, + ) + if item.latest_run_key + else {}, + } + for item in items_by_run_id.get(run.run_id, [])[:5] + ], } for run in runs ], diff --git a/backend/app/services/job_handlers.py b/backend/app/services/job_handlers.py index 272e06f..348338e 100644 --- a/backend/app/services/job_handlers.py +++ b/backend/app/services/job_handlers.py @@ -2058,6 +2058,25 @@ async def _handle_queued_engine_run( pair_index = max(0, int(event.get("pair_index") or 0)) task_label = str(event.get("task_alias") or event.get("task_name") or "").strip() + if event_type == "log": + level = str(event.get("level") or "INFO").strip().upper() + if level not in {"DEBUG", "INFO", "WARNING", "ERROR"}: + level = "INFO" + source = str(event.get("source") or "").strip() + message = str(event.get("message") or "").strip() + if not message: + continue + label = task_label or str(progress_state.get("pair_label") or "").strip() or "pair" + prefix = f"{engine_title} {pair_index}/{pair_total} {label}" + if source: + prefix = f"{prefix} {source}" + await task_service.add_log( + job.task_id, + level, + f"{prefix}: {message}", + ) + continue + if event_type == "pair_started": progress = min( 90, @@ -2496,7 +2515,22 @@ async def _run_wsl_dinsar_production_controller( if event is None: return event_type = str(event.get("event") or "").strip().lower() - if event_type == "pair_started": + if event_type == "log": + level = str(event.get("level") or "INFO").strip().upper() + if level not in {"DEBUG", "INFO", "WARNING", "ERROR"}: + level = "INFO" + source = str(event.get("source") or "").strip() + message = str(event.get("message") or "").strip() + if message: + prefix = f"[{item_index}/{total_items}] {engine_title} {item_label}" + if source: + prefix = f"{prefix} {source}" + await task_service.add_log( + job.task_id, + level, + f"{prefix}: {message}", + ) + elif event_type == "pair_started": progress_state["message"] = ( f"[{engine_code}/{run.profile_code}] Running " f"{item_index}/{total_items}: {item_label}" @@ -2892,6 +2926,52 @@ async def _handle_isce2_run(job: SystemJobORM) -> None: async def _handle_pyint_run(job: SystemJobORM) -> None: + production_run_id = str((job.payload or {}).get("production_run_id") or "").strip() + if production_run_id: + try: + await _run_wsl_dinsar_production_controller( + job, + engine_code="pyint", + engine_title="PyINT/Gamma", + fallback_timeout_seconds=settings.PYINT_DEFAULT_TIMEOUT_SECONDS, + ) + except Exception as exc: + latest_message = f"PyINT/Gamma D-InSAR production controller failed: {exc}" + try: + async with AsyncSessionLocal() as db: + run = await dinsar_production_service.get_run(production_run_id, db) + if run is not None and str(run.status or "").strip().upper() not in {"COMPLETED", "FAILED", "CANCELLED"}: + summary_payload = dict(run.summary_json or {}) + summary_payload["controller_error"] = str(exc) + await dinsar_production_service.finalize_run( + run, + db=db, + status="FAILED", + summary_payload=summary_payload, + latest_message=latest_message, + ) + dinsar_production_service.append_run_log( + run.run_id, + f"[controller-failed] {exc}", + ) + except Exception: + pass + + try: + current_task = await task_service.get_task(job.task_id) + if current_task and current_task.status not in {"COMPLETED", "FAILED", "CANCELLED"}: + await task_service.add_log(job.task_id, "ERROR", latest_message) + await task_service.update_task( + job.task_id, + status="FAILED", + progress=100, + message=latest_message, + ) + except Exception: + pass + raise + return + await _handle_queued_engine_run( job, engine_title="PyINT", diff --git a/backend/app/services/pyint_input_assets_service.py b/backend/app/services/pyint_input_assets_service.py index 9d1c2bf..d374cb1 100644 --- a/backend/app/services/pyint_input_assets_service.py +++ b/backend/app/services/pyint_input_assets_service.py @@ -316,6 +316,7 @@ def get_pyint_dem_summary() -> Dict[str, Any]: "hdr_exists": bool(prepared_dem_info.get("hdr_exists")), "vrt_exists": bool(prepared_dem_info.get("vrt_exists")), }, + "configured_resolution_m": float(getattr(settings, "PYINT_DEM_RESOLUTION_M", 30.0) or 30.0), "opentopo_dem_type": opentopo_dem_type, "opentopo_api_key_configured": bool(opentopo_api_key), "status": status, diff --git a/backend/app/services/pyint_service.py b/backend/app/services/pyint_service.py index bf5d6d2..dc6b9fa 100644 --- a/backend/app/services/pyint_service.py +++ b/backend/app/services/pyint_service.py @@ -4,6 +4,8 @@ from __future__ import annotations import os import re import shlex +import math +import defusedxml.ElementTree as ET from dataclasses import dataclass, field from datetime import datetime from pathlib import Path @@ -11,12 +13,83 @@ from typing import Any, Dict, Iterable, List, Optional from ..config import get_env_text, read_bool_env, settings from .dinsar_naming import PAIR_META_FILENAME, build_fallback_pair_key, find_json_sidecar -from .wsl_service import run_wsl_command +from .wsl_service import run_wsl_exec LT1_INPUT_GLOBS = ("LT1*.tar.gz", "LT1*.tiff") DEFAULT_RANGE_LOOKS = 2 DEFAULT_AZIMUTH_LOOKS = 2 +DEFAULT_DEM_RESOLUTION_M = 30.0 +DEFAULT_UNWRAP_COH_THRESHOLD = 0.05 +DEFAULT_PRODUCT_COH_THRESHOLD = 0.20 +DEFAULT_REFERENCE_MODE = "none" +DEFAULT_REFERENCE_COH_THRESHOLD = 0.30 +DEFAULT_DERAMP_MODE = "none" +DEFAULT_DERAMP_COH_THRESHOLD = 0.30 +DEFAULT_GEO_INTERP = "1" +DEFAULT_ATMCOR_ENABLED = False +DEFAULT_ATMCOR_USE_FOR_DISP = False +DEFAULT_REFLATTEN_ENABLED = True +DEFAULT_REFLATTEN_MODEL = "plane" +DEFAULT_REFLATTEN_COH_THRESHOLD = 0.70 +DEFAULT_REFLATTEN_FALLBACK_COH_THRESHOLD = 0.20 +DEFAULT_REFLATTEN_RANGE_STEP = 32 +DEFAULT_REFLATTEN_AZIMUTH_STEP = 32 +DEM_OVERSAMPLING_MIN = 0.25 +DEM_OVERSAMPLING_MAX = 16.0 +REFERENCE_MODE_CHOICES = {"none", "coh_median"} +DERAMP_MODE_CHOICES = {"none", "plane"} +REFLATTEN_MODEL_CHOICES = {"plane", "linear", "quadratic"} + + +def _read_default_target_grid_size_m() -> int: + for name in ("PYINT_DEFAULT_TARGET_GRID_SIZE_M",): + text = str(get_env_text(name, "") or "").strip() + if not text: + continue + try: + value = float(text) + except (TypeError, ValueError): + continue + if value > 0: + return int(value) + return 0 + + +def _read_float_env(names: Iterable[str], default: float) -> float: + for name in names: + text = str(get_env_text(name, "") or "").strip() + if not text: + continue + try: + value = float(text) + except (TypeError, ValueError): + continue + if math.isfinite(value): + return value + return float(default) + + +DEFAULT_TARGET_GRID_SIZE_M = _read_default_target_grid_size_m() +TARGET_GRID_SIZE_MIN_M = 0 +TARGET_GRID_SIZE_MAX_M = 100 +DEFAULT_DEM_RESOLUTION_M = _read_float_env(("PYINT_DEM_RESOLUTION_M",), DEFAULT_DEM_RESOLUTION_M) +DEFAULT_UNWRAP_COH_THRESHOLD = _read_float_env( + ("PYINT_UNWRAP_COH_THRESHOLD",), + DEFAULT_UNWRAP_COH_THRESHOLD, +) +DEFAULT_PRODUCT_COH_THRESHOLD = _read_float_env( + ("PYINT_PRODUCT_COH_THRESHOLD", "PYINT_COHERENCE_MASK_THRESHOLD"), + DEFAULT_PRODUCT_COH_THRESHOLD, +) +DEFAULT_REFERENCE_COH_THRESHOLD = _read_float_env( + ("PYINT_REFERENCE_COH_THRESHOLD",), + DEFAULT_REFERENCE_COH_THRESHOLD, +) +DEFAULT_DERAMP_COH_THRESHOLD = _read_float_env( + ("PYINT_DERAMP_COH_THRESHOLD",), + DEFAULT_DERAMP_COH_THRESHOLD, +) DEFAULT_PARALLEL_WORKERS = 1 MAX_LOOKS = 32 MAX_PARALLEL_WORKERS = 16 @@ -76,6 +149,180 @@ def normalize_date_text(value: Any) -> str: return "" +def _local_xml_tag_name(tag: Any) -> str: + text = str(tag or "") + return text.split("}")[-1] if "}" in text else text + + +def _read_xml_first_parameter(xml_file: str, names: Iterable[str]) -> Optional[str]: + path = os.path.normpath(str(xml_file or "").strip()) + if not path or not os.path.isfile(path): + return None + wanted = {str(name or "").strip().lower() for name in names if str(name or "").strip()} + if not wanted: + return None + try: + tree = ET.parse(path) + root = tree.getroot() + except Exception: + return None + for elem in root.iter(): + local_name = _local_xml_tag_name(elem.tag).lower() + if local_name in wanted and elem.text and str(elem.text).strip(): + return str(elem.text).strip() + return None + + +def _read_scene_geometry_metadata(metadata_path: str) -> Dict[str, Any]: + source = os.path.normpath(str(metadata_path or "").strip()) + range_spacing = _read_xml_first_parameter( + source, + ("PixelSpacingRg", "columnSpacing", "slantRange", "range_pixel_spacing"), + ) + azimuth_spacing = _read_xml_first_parameter( + source, + ("PixelSpacingAz", "rowSpacing", "projectedSpacingAzimuth", "azimuth_pixel_spacing"), + ) + incidence_angle = _read_xml_first_parameter( + source, + ("IncidenceAngle", "incidence_angle"), + ) + if not all((range_spacing, azimuth_spacing, incidence_angle)): + raise ValueError(f"Cannot read range/azimuth spacing and incidence angle from: {source}") + return { + "source": source, + "range_pixel_spacing_m": float(range_spacing), + "azimuth_pixel_spacing_m": float(azimuth_spacing), + "incidence_angle_deg": float(incidence_angle), + } + + +def _scene_geometry_metadata_candidates(directory: str, patterns: Iterable[str]) -> List[str]: + root = os.path.normpath(str(directory or "").strip()) + if not root or not os.path.isdir(root): + return [] + candidates: List[str] = [] + for pattern in patterns: + candidates.extend(str(path) for path in Path(root).glob(pattern) if path.is_file()) + return [ + os.path.normpath(path) + for path in sorted( + set(candidates), + key=lambda item: (0 if item.lower().endswith(".sml") else 1, item.lower()), + ) + ] + + +def resolve_scene_geometry_metadata_files(scene_dir: str) -> List[str]: + return _scene_geometry_metadata_candidates( + scene_dir, + ( + "*.sml", + "*.SML", + "*.meta.xml", + "*.META.XML", + ), + ) + + +def resolve_scene_geometry_metadata_file(scene_dir: str) -> str: + candidates = resolve_scene_geometry_metadata_files(scene_dir) + return candidates[0] if candidates else "" + + +def calculate_looks_from_scene_metadata( + *, + master_metadata: str, + slave_metadata: str, + target_resolution_m: float, +) -> Dict[str, Any]: + target_resolution = float(target_resolution_m) + if target_resolution <= 0: + raise ValueError("target_resolution_m must be greater than 0") + + master = _read_scene_geometry_metadata(master_metadata) + slave = _read_scene_geometry_metadata(slave_metadata) + + avg_azimuth = ( + float(master["azimuth_pixel_spacing_m"]) + float(slave["azimuth_pixel_spacing_m"]) + ) / 2.0 + master_ground_range = float(master["range_pixel_spacing_m"]) / math.sin( + math.radians(float(master["incidence_angle_deg"])) + ) + slave_ground_range = float(slave["range_pixel_spacing_m"]) / math.sin( + math.radians(float(slave["incidence_angle_deg"])) + ) + avg_ground_range = (master_ground_range + slave_ground_range) / 2.0 + + range_ratio = target_resolution / avg_ground_range + azimuth_ratio = target_resolution / avg_azimuth + range_looks = max(1, int(math.floor(range_ratio + 0.5))) + azimuth_looks = max(1, int(math.floor(azimuth_ratio + 0.5))) + + return { + "mode": "target_grid_size", + "target_resolution_m": target_resolution, + "range_looks": range_looks, + "azimuth_looks": azimuth_looks, + "avg_ground_range_spacing_m": avg_ground_range, + "avg_azimuth_spacing_m": avg_azimuth, + "range_look_ratio": range_ratio, + "azimuth_look_ratio": azimuth_ratio, + "resolved_ground_range_spacing_m": avg_ground_range * range_looks, + "resolved_azimuth_spacing_m": avg_azimuth * azimuth_looks, + "master": master, + "slave": slave, + } + + +def calculate_looks_from_task_dir(task_dir: str, target_resolution_m: float) -> Dict[str, Any]: + task_root = os.path.normpath(str(task_dir or "").strip()) + master_candidates = resolve_scene_geometry_metadata_files(os.path.join(task_root, "master")) + slave_candidates = resolve_scene_geometry_metadata_files(os.path.join(task_root, "slave")) + if not master_candidates or not slave_candidates: + raise ValueError(f"Cannot find SML/meta XML metadata under task: {task_root}") + errors: List[str] = [] + for master_metadata in master_candidates: + for slave_metadata in slave_candidates: + try: + return calculate_looks_from_scene_metadata( + master_metadata=master_metadata, + slave_metadata=slave_metadata, + target_resolution_m=target_resolution_m, + ) + except Exception as exc: + errors.append(f"{os.path.basename(master_metadata)} + {os.path.basename(slave_metadata)}: {exc}") + detail = "; ".join(errors[:3]) if errors else "unknown metadata parsing error" + raise ValueError(f"Cannot calculate looks from task metadata under {task_root}: {detail}") + + +def calculate_dem_oversampling( + *, + dem_resolution_m: float, + target_grid_size_m: float, +) -> Dict[str, Any]: + dem_resolution = float(dem_resolution_m or 0.0) + target_grid = float(target_grid_size_m or 0.0) + if not math.isfinite(dem_resolution) or dem_resolution <= 0: + dem_resolution = DEFAULT_DEM_RESOLUTION_M + + raw_factor = dem_resolution / target_grid if math.isfinite(target_grid) and target_grid > 0 else None + oversampling = 1.0 + actual_grid = dem_resolution / oversampling if oversampling > 0 else dem_resolution + mismatch_ratio = abs(actual_grid - target_grid) / target_grid if target_grid > 0 else None + return { + "mode": "gamma_dem_oversampling", + "dem_resolution_m": dem_resolution, + "target_grid_size_m": target_grid, + "raw_oversampling": raw_factor, + "oversampling": oversampling, + "actual_grid_size_m": actual_grid, + "mismatch_ratio": mismatch_ratio, + "min_oversampling": DEM_OVERSAMPLING_MIN, + "max_oversampling": DEM_OVERSAMPLING_MAX, + } + + def slugify_text(value: Any, *, default: str = "item", max_len: int = 96) -> str: text = _SAFE_TEXT_RE.sub("_", str(value or "").strip()).strip("._") if not text: @@ -278,7 +525,14 @@ def _gamma_prefix(gamma_env_script_wsl: str) -> str: script = str(gamma_env_script_wsl or "").strip() if not script: return "" - return f". {quote_shell(script)} >/dev/null 2>&1 && " + return f". {quote_shell(script)} >/dev/null 2>&1 || exit 1; " + + +def _pyint_path_prefix(pyint_home_wsl: str) -> str: + home = str(pyint_home_wsl or "").strip().rstrip("/") + if not home: + return "" + return f"export PATH={quote_shell(home + '/pyint')}:\"$PATH\" && " def check_pyint_environment( @@ -320,7 +574,10 @@ def check_pyint_environment( def add(name: str, ok: bool, detail: str = "", skipped: bool = False) -> None: checks.append(PyintCheck(name=name, ok=ok, detail=detail, skipped=skipped)) - rc, out, err = run_wsl_command("echo pyint_alive", distro=distro_value, timeout=15) + def run_check(command: str, timeout: int = 30): + return run_wsl_exec(["bash", "-lc", command], distro=distro_value, timeout=timeout) + + rc, out, err = run_check("echo pyint_alive", timeout=15) wsl_ok = rc == 0 and "pyint_alive" in out add("WSL distro", wsl_ok, out or err or distro_value) @@ -331,17 +588,15 @@ def check_pyint_environment( message=f"WSL distro is unavailable: {distro_value}", ) - rc, out, err = run_wsl_command( + rc, out, err = run_check( f"{quote_shell(python_value)} --version", - distro=distro_value, timeout=15, ) add("WSL Python", rc == 0, out or err or python_value) if pyint_home_wsl: - rc, out, err = run_wsl_command( + rc, out, err = run_check( f"test -d {quote_shell(pyint_home_wsl)} && echo ok", - distro=distro_value, timeout=10, ) add("PYINT_HOME", rc == 0 and "ok" in out, pyint_home_wsl or err) @@ -349,9 +604,8 @@ def check_pyint_environment( add("PYINT_HOME", False, "PYINT_HOME is empty") if pyint_app_wsl: - rc, out, err = run_wsl_command( + rc, out, err = run_check( f"test -f {quote_shell(pyint_app_wsl)} && echo ok", - distro=distro_value, timeout=10, ) add("pyintApp.py", rc == 0 and "ok" in out, pyint_app_wsl or err) @@ -367,17 +621,15 @@ def check_pyint_environment( if not path_text: add(name, False, f"{name} is empty") continue - rc, out, err = run_wsl_command( + rc, out, err = run_check( f"test -d {quote_shell(path_text)} && test -w {quote_shell(path_text)} && echo ok", - distro=distro_value, timeout=10, ) add(name, rc == 0 and "ok" in out, path_text or err) if gamma_env_wsl: - rc, out, err = run_wsl_command( + rc, out, err = run_check( f"test -f {quote_shell(gamma_env_wsl)} && echo ok", - distro=distro_value, timeout=10, ) add("GAMMA env script", rc == 0 and "ok" in out, gamma_env_wsl or err) @@ -385,16 +637,16 @@ def check_pyint_environment( add("GAMMA env script", True, "Not configured; using current PATH", skipped=True) gamma_prefix = _gamma_prefix(gamma_env_wsl) + pyint_prefix = _pyint_path_prefix(pyint_home_wsl) for name, command_name in ( ("GAMMA LT1 import", "LT1_import_SLC_from_zipfiles1"), ("GAMMA geocode_back", "geocode_back"), ): - rc, out, err = run_wsl_command( - gamma_prefix + f"command -v {quote_shell(command_name)}", - distro=distro_value, + rc, out, err = run_check( + gamma_prefix + pyint_prefix + f"command -v {quote_shell(command_name)} >/dev/null 2>&1 && echo ok", timeout=10, ) - add(name, rc == 0 and bool(out.strip()), out or err or command_name) + add(name, rc == 0 and "ok" in out, out or err or command_name) helper_path = ( Path(__file__).resolve().parent.parent @@ -412,7 +664,7 @@ def check_pyint_environment( + gamma_prefix + f"{quote_shell(python_value)} {quote_shell(pyint_app_wsl)} -h >/dev/null" ) - rc, out, err = run_wsl_command(smoke_cmd, distro=distro_value, timeout=60) + rc, out, err = run_check(smoke_cmd, timeout=60) add("PyINT smoke test", rc == 0, out or err or "pyintApp.py -h") else: add("PyINT smoke test", True, "Skipped", skipped=True) diff --git a/backend/app/services/wsl_service.py b/backend/app/services/wsl_service.py index 9735866..d91070f 100644 --- a/backend/app/services/wsl_service.py +++ b/backend/app/services/wsl_service.py @@ -11,8 +11,10 @@ from __future__ import annotations import os import shutil import subprocess +import threading +import time from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Sequence, Tuple +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple # --------------------------------------------------------------------------- @@ -101,6 +103,79 @@ def _run_windows_command( ) +def _run_windows_command_stream( + args: List[str], + timeout: int = 30, + env: Optional[Dict[str, str]] = None, + stdout_callback: Optional[Callable[[str], None]] = None, + stderr_callback: Optional[Callable[[str], None]] = None, +) -> Tuple[int, str, str]: + proc_env = os.environ.copy() + if env: + proc_env.update(env) + + stdout_parts: List[str] = [] + stderr_parts: List[str] = [] + try: + proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=False, + env=proc_env, + ) + except FileNotFoundError: + return -2, "", "wsl.exe not found" + except Exception as exc: + return -3, "", str(exc) + + def _drain(stream: Any, parts: List[str], callback: Optional[Callable[[str], None]]) -> None: + for raw_line in iter(stream.readline, b""): + text = _decode_subprocess_output(raw_line) + if not text: + continue + parts.append(text) + if callback: + try: + callback(text) + except Exception: + pass + + threads = [ + threading.Thread(target=_drain, args=(proc.stdout, stdout_parts, stdout_callback), daemon=True), + threading.Thread(target=_drain, args=(proc.stderr, stderr_parts, stderr_callback), daemon=True), + ] + for thread in threads: + thread.start() + + timed_out = False + deadline = time.monotonic() + max(1, int(timeout or 30)) + while proc.poll() is None: + if time.monotonic() >= deadline: + timed_out = True + try: + proc.kill() + except Exception: + pass + break + time.sleep(0.2) + + try: + returncode = proc.wait(timeout=10) + except subprocess.TimeoutExpired: + returncode = -1 + for thread in threads: + thread.join(timeout=5) + + stdout = "\n".join(stdout_parts) + stderr = "\n".join(stderr_parts) + if timed_out: + timeout_text = f"command timed out ({timeout}s)" + stderr = f"{stderr}\n{timeout_text}".strip() + return -1, stdout, stderr + return returncode, stdout, stderr + + def run_wsl_command( cmd: str, distro: Optional[str] = None, @@ -127,6 +202,33 @@ def run_wsl_command( return -3, "", str(exc) +def run_wsl_command_stream( + cmd: str, + distro: Optional[str] = None, + timeout: int = 30, + env: Optional[Dict[str, str]] = None, + stdout_callback: Optional[Callable[[str], None]] = None, + stderr_callback: Optional[Callable[[str], None]] = None, +) -> Tuple[int, str, str]: + """Run a WSL bash command and stream decoded stdout/stderr lines to callbacks.""" + wsl_exe = _find_wsl_executable() + if not wsl_exe: + return -2, "", "wsl.exe not found" + + wsl_args = [wsl_exe] + if distro: + wsl_args += ["-d", distro] + wsl_args += ["bash", "-lc", cmd] + + return _run_windows_command_stream( + wsl_args, + timeout=timeout, + env=env, + stdout_callback=stdout_callback, + stderr_callback=stderr_callback, + ) + + def run_wsl_exec( argv: Sequence[str], distro: Optional[str] = None, diff --git a/deploy/wsl/profiles/gamma_env.sh b/deploy/wsl/profiles/gamma_env.sh index e9d5e3d..1de7d76 100644 --- a/deploy/wsl/profiles/gamma_env.sh +++ b/deploy/wsl/profiles/gamma_env.sh @@ -60,7 +60,7 @@ for _gamma_dir in \ _gamma_profile_prepend_path "${_gamma_dir}" done -_gamma_profile_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +_gamma_profile_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" _gamma_profile_pyint_dir="${_gamma_profile_repo_root}/third_party/PyINT/pyint" _gamma_profile_prepend_path "${_gamma_profile_pyint_dir}" diff --git a/experiments/isce2_sbas_timeseries/README.md b/experiments/isce2_sbas_timeseries/README.md deleted file mode 100644 index e46f53d..0000000 --- a/experiments/isce2_sbas_timeseries/README.md +++ /dev/null @@ -1,181 +0,0 @@ -# ISCE2 SBAS Time-Series Experiments - -This folder is the isolated sandbox for validating the SBAS/time-series route before wiring it into production code. - -## Purpose - -- verify LT-1 stack compatibility with ISCE2 stack tooling -- validate MintPy input and output expectations -- record runnable command templates -- collect conclusions that should later be promoted into `docs/` or backend services -- maintain the current SBAS product contract before backend embedding - -## Structure - -- `notes/` - - experiment notes, pitfalls, conclusions -- `configs/` - - sample templates, parameter files, manifest drafts -- `scripts/` - - throwaway or semi-stable experiment scripts -- `scratch/` - - local temporary workspace placeholder only - -## Rules - -- do not commit raw SAR scenes -- do not commit large DEM or orbit datasets -- do not commit large intermediate outputs -- keep production code changes out of this folder unless the goal is to prototype file layout or commands -- when an experiment becomes stable, move the result back into the formal backend or `docs/` - -## Suggested first experiments - -1. Check whether LT-1/LUTAN1 scenes can be ingested by official ISCE2 stack tooling. -2. Determine whether MintPy can consume the generated stack layout without extra conversion. -3. Record the minimal command chain needed for one small AOI smoke test. -4. Draft the first `psinsar` manifest and product directory convention. - -## Current scripts - -- `backend/app/isce2_pipeline/lt1_input_resolver.py` - - shared LT-1 input helper reused by D-InSAR and stack experiments - - centralizes DEM resolution, orbit-pool resolution, and LT-1 precise-orbit XML generation -- `scripts/scan_lt1_stack_candidates.py` - - scan LT-1 single-scene folders and build a stack candidate manifest -- `scripts/build_lt1_stack_prep.py` - - consume a selected stack manifest - - resolve orbit pool and DEM - - generate a dry-run `scratch/...` workspace for `stripmapStack --nofocus` - - write the current adapter contract and a preflight run script -- `scripts/materialize_lt1_stack_scenes.py` - - consume `scratch/.../stack_input_manifest.json` - - materialize one or more LT-1 acquisitions into `SLC/YYYYMMDD/` - - write `YYYYMMDD.slc`, `YYYYMMDD.slc.xml`, and `data` -- `scripts/install_isce2_stack_runtime_ubuntu2404.sh` - - install known WSL `isce2` runtime dependencies - - default pip mirror is Tsinghua -- `scripts/install_mintpy_runtime_ubuntu2404.sh` - - create or update a dedicated WSL `mintpy` conda environment - - default conda channels use Tsinghua mirror URLs -- `scripts/install_mintpy_into_cloned_isce2_env_ubuntu2404.sh` - - clone the working WSL `isce2` env into a dedicated unified-env target such as `isce2_mintpy` - - install MintPy into that clone with Tsinghua mirror channels -- `scripts/run_mintpy_unified_env_ubuntu2404.sh` - - run MintPy commands directly inside the cloned unified env -- `scripts/run_mintpy_sbas_unified_env_smoketest_ubuntu2404.sh` - - run the current LT-1 SBAS smoke test in the cloned unified env - - reuses the same strict-mask and patched-launcher helpers as the bridge route -- `scripts/run_mintpy_with_isce_ubuntu2404.sh` - - run MintPy commands in the dedicated `mintpy` env - - bridge only the top-level WSL `isce` package into the `mintpy` env - - avoids pulling conflicting `h5py` / numeric packages from the `isce2` env -- `scripts/create_mintpy_all_ifgram_mask.py` - - build a strict `maskAllValid.h5` from `inputs/ifgramStack.h5` - - keep only pixels valid in all interferograms before SBAS inversion -- `scripts/run_smallbaselineApp_patched.py` - - repo-local launcher for MintPy `smallbaselineApp` - - applies a local workaround for the MintPy `1.6.2` single-pixel partial-network inversion bug -- `scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh` - - run the current LT-1 SBAS smoke test in three steps: - - `load_data` - - strict-mask generation - - `modify_network -> velocity` -- `scripts/export_mintpy_publish_products_ubuntu2404.sh` - - geocode MintPy outputs into latitude/longitude grids - - convert selected outputs into GeoTIFF - - build a publish-style bundle with `manifest.json`, `assets/`, `preview/`, and `metadata/` - - defaults to the bridge runner but now also supports `MINTPY_RUNNER=...` override -- `scripts/export_mintpy_publish_products_unified_env_ubuntu2404.sh` - - run the same publish export logic through the cloned unified env runner -- `scripts/export_conda_env_snapshot_ubuntu2404.sh` - - export one WSL conda environment into reproducible snapshot files - - writes `no_builds.yml`, `explicit.txt`, `conda_list.txt`, and `runtime_versions.txt` -- `scripts/export_phase4_env_snapshots_ubuntu2404.sh` - - export both `isce2` and `isce2_mintpy_v1` snapshots for the current phase-4 record -- `scripts/build_mintpy_publish_bundle.py` - - generate `preview/velocity_preview.png` - - summarize quality masks - - write the publish-style `manifest.json` -- `scripts/prepare_lt1_stack_dem.py` - - clip a stack-local DEM window from the source DEM - - store it under `scratch/.../inputs/dem/` - - avoid global-DEM bbox problems during `createWaterMask` -- `scripts/run_generated_stack_runfile_ubuntu2404.sh` - - execute one generated `run_XX_*` file under `Ubuntu-24.04` - - standardize `PATH`, `PYTHONPATH`, and log output -- `scripts/create_synthetic_watermask.py` - - create a local all-land `geom_reference/waterMask.rdr` - - used only when `run_01_reference` cannot download `SWBD` from Earthdata - -## Reproducible Flow - -1. Generate or refresh the sample stack workspace with `build_lt1_stack_prep.py`. -2. Materialize the LT-1 acquisitions with `materialize_lt1_stack_scenes.py`. -3. Clip a stack-local DEM window with `prepare_lt1_stack_dem.py`. -4. In `Ubuntu-24.04`, run `scripts/install_isce2_stack_runtime_ubuntu2404.sh`. -5. Regenerate the stack workspace so it picks the local DEM. -6. Run the generated wrapper through the validated chain: - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_01_reference` - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_02_focus_split` - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_03_geo2rdr_coarseResamp` - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_04_refineSecondaryTiming` - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_05_invertMisreg` - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_06_fineResamp` - - `scripts/run_generated_stack_runfile_ubuntu2404.sh run_07_grid_baseline` - - if Earthdata credentials are missing, the wrapper now auto-generates a synthetic all-land `waterMask.rdr` from `shadowMask.rdr` and treats `run_01_reference` as recovered -7. In `Ubuntu-24.04`, run `scripts/install_mintpy_runtime_ubuntu2404.sh` before the first MintPy validation. -8. In `Ubuntu-24.04`, run `scripts/run_mintpy_with_isce_ubuntu2404.sh prep_isce.py ...` for the first MintPy metadata preparation on stripmapStack outputs. -9. In `Ubuntu-24.04`, run: - - `scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh ` -10. Review: - - `notes/PHASE2_MINTPY_SBAS_SMOKETEST.md` -11. In `Ubuntu-24.04`, run: - - `scripts/export_mintpy_publish_products_ubuntu2404.sh ` -12. Review: - - `notes/PHASE3_PUBLISH_EXPORT_SMOKETEST.md` -13. Record findings under `notes/` before promoting anything into backend code. - -## Product Contract - -Formal product guidance now lives in: - -- `docs/ISCE2_SBAS_PRODUCT_SPEC.md` - -Current practical rule: - -- runtime success is proven by radar-coordinate `timeseries.h5` and `velocity.h5` -- publish success is proven by a geocoded bundle under `publish/.../` -- system embedding should treat `manifest.json` as the publish entrypoint -- true time-series capability should be judged against `assets/geo_timeseries.h5`, not only `assets/velocity.tif` - -## Unified-Environment Track - -The bridge route remains the validated baseline. - -A separate unified-environment experiment is now staged in: - -- `notes/PHASE4_UNIFIED_ENV_EXPERIMENT.md` -- `notes/PHASE4_UNIFIED_ENV_DECISION.md` - -Current practical rule: - -- the unified env has now completed the same SBAS smoke test chain and publish export in experiment scope -- the unified env is now the preferred SBAS experiment runtime -- do not replace or mutate the current `isce2` env used by D-InSAR production -- do not replace the bridge route as the default fallback until the comparison note is fully written -- current successful unified env: - - `/home/administrator/miniconda3/envs/isce2_mintpy_v1` -- current successful unified SBAS work dir: - - `scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_unified_v1` -- current successful unified publish dir: - - `scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_unified_v1` - -## Current Offline Assumption - -- the LT-1 sample experiment already has local SAR scenes, local orbit XML, and a local DEM -- the remaining optional online dependency is the `SWBD` water mask normally fetched by `createWaterMask.py` -- for this experiment track, do not download `SWBD` -- continue with the local synthetic all-land `waterMask.rdr` fallback until the stack route is otherwise stable -- current validated MintPy boundary is radar-coordinate `timeseries.h5` plus `velocity.h5` -- current validated publish boundary is a geocoded experiment bundle under `publish/.../` diff --git a/experiments/isce2_sbas_timeseries/configs/.gitkeep b/experiments/isce2_sbas_timeseries/configs/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/experiments/isce2_sbas_timeseries/configs/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.conda_list.txt b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.conda_list.txt deleted file mode 100644 index debebc6..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.conda_list.txt +++ /dev/null @@ -1,227 +0,0 @@ -# packages in environment at /home/administrator/miniconda3/envs/isce2: -# -# Name Version Build Channel -_openmp_mutex 4.5 20_gnu conda-forge -aom 3.9.1 hac33072_0 conda-forge -attr 2.5.2 h39aace5_0 conda-forge -aws-c-auth 0.9.3 hef928c7_0 conda-forge -aws-c-cal 0.9.13 h2c9d079_1 conda-forge -aws-c-common 0.12.6 hb03c661_0 conda-forge -aws-c-compression 0.3.1 h8b1a151_9 conda-forge -aws-c-event-stream 0.5.7 h28f887f_1 conda-forge -aws-c-http 0.10.7 ha8fc4e3_5 conda-forge -aws-c-io 0.23.3 hdaf4b65_5 conda-forge -aws-c-mqtt 0.13.3 hc63082f_11 conda-forge -aws-c-s3 0.11.3 h06ab39a_1 conda-forge -aws-c-sdkutils 0.2.4 h8b1a151_4 conda-forge -aws-checksums 0.2.7 h8b1a151_5 conda-forge -aws-crt-cpp 0.35.2 h8824e59_6 conda-forge -aws-sdk-cpp 1.11.606 hf38915e_9 conda-forge -azure-core-cpp 1.16.1 h3a458e0_0 conda-forge -azure-identity-cpp 1.13.2 h3a5f585_1 conda-forge -azure-storage-blobs-cpp 12.15.0 h2a74896_1 conda-forge -azure-storage-common-cpp 12.11.0 h3d7a050_1 conda-forge -azure-storage-files-datalake-cpp 12.13.0 hf38f1be_1 conda-forge -backports.zstd 1.3.0 py311h6b1f9c4_0 conda-forge -blosc 1.21.6 he440d0b_1 conda-forge -brotli-python 1.2.0 py311h66f275b_1 conda-forge -brunsli 0.1 hd1e3526_2 conda-forge -bzip2 1.0.8 hda65f42_9 conda-forge -c-ares 1.34.6 hb03c661_0 conda-forge -c-blosc2 2.23.1 hc31b594_0 conda-forge -ca-certificates 2026.2.25 hbd8a1cb_0 conda-forge -cached-property 1.5.2 hd8ed1ab_1 conda-forge -cached_property 1.5.2 pyha770c72_1 conda-forge -cairo 1.18.4 he90730b_1 conda-forge -capnproto 1.2.0 hfc315d8_0 conda-forge -certifi 2026.2.25 pyhd8ed1ab_0 conda-forge -cfitsio 4.6.3 ha0b56bc_0 conda-forge -charls 2.4.3 hecca717_0 conda-forge -charset-normalizer 3.4.5 pyhd8ed1ab_0 conda-forge -contourpy 1.3.3 pypi_0 pypi -cycler 0.12.1 pypi_0 pypi -cyrus-sasl 2.1.28 hac629b4_1 conda-forge -dav1d 1.2.1 hd590300_0 conda-forge -fftw 3.3.10 nompi_h3b011a4_112 conda-forge -fmt 12.0.0 h2b0788b_0 conda-forge -font-ttf-dejavu-sans-mono 2.37 hab24e00_0 conda-forge -font-ttf-inconsolata 3.000 h77eed37_0 conda-forge -font-ttf-source-code-pro 2.038 h77eed37_0 conda-forge -font-ttf-ubuntu 0.83 h77eed37_3 conda-forge -fontconfig 2.17.1 h27c8c51_0 conda-forge -fonts-conda-ecosystem 1 0 conda-forge -fonts-conda-forge 1 hc364b38_1 conda-forge -fonttools 4.62.1 pypi_0 pypi -freetype 2.14.2 ha770c72_0 conda-forge -freexl 2.0.0 h9dce30a_2 conda-forge -gdal 3.10.3 py311h34ccccb_27 conda-forge -geos 3.14.1 h480dda7_0 conda-forge -geotiff 1.7.4 h1000f5c_4 conda-forge -giflib 5.2.2 hd590300_0 conda-forge -h2 4.3.0 pyhcf101f3_0 conda-forge -h5py 3.15.1 nompi_py311h0b2f468_101 conda-forge -hdf4 4.2.15 h2a13503_7 conda-forge -hdf5 1.14.6 nompi_h19486de_106 conda-forge -hpack 4.1.0 pyhd8ed1ab_0 conda-forge -hyperframe 6.1.0 pyhd8ed1ab_0 conda-forge -icu 78.2 h33c6efd_0 conda-forge -idna 3.11 pyhd8ed1ab_0 conda-forge -imagecodecs 2026.3.6 py311h9837d23_1 conda-forge -imageio 2.37.0 pyhfb79c49_0 conda-forge -isce2 2.6.4 py311h916084f_2 conda-forge -json-c 0.18 h6688a6e_0 conda-forge -jxrlib 1.1 hd590300_3 conda-forge -kealib 1.6.2 hb2f3951_2 conda-forge -keyutils 1.6.3 hb9d3cd8_0 conda-forge -kiwisolver 1.5.0 pypi_0 pypi -krb5 1.22.2 ha1258a1_0 conda-forge -lazy-loader 0.5 pyhd8ed1ab_0 conda-forge -lcms2 2.18 h0c24ade_0 conda-forge -ld_impl_linux-64 2.45.1 default_hbd61a6d_101 conda-forge -lerc 4.1.0 hdb68285_0 conda-forge -libabseil 20250512.1 cxx17_hba17884_0 conda-forge -libacl 2.3.2 h0f662aa_0 conda-forge -libaec 1.1.5 h088129d_0 conda-forge -libarchive 3.8.5 gpl_hc2c16d8_100 conda-forge -libavif16 1.4.0 hcfa2d63_0 conda-forge -libblas 3.11.0 5_h4a7cf45_openblas conda-forge -libbrotlicommon 1.2.0 hb03c661_1 conda-forge -libbrotlidec 1.2.0 hb03c661_1 conda-forge -libbrotlienc 1.2.0 hb03c661_1 conda-forge -libcblas 3.11.0 5_h0358290_openblas conda-forge -libcrc32c 1.1.2 h9c3ff4c_0 conda-forge -libcurl 8.18.0 hcf29cc6_1 conda-forge -libdeflate 1.25 h17f619e_0 conda-forge -libedit 3.1.20250104 pl5321h7949ede_0 conda-forge -libev 4.33 hd590300_2 conda-forge -libexpat 2.7.4 hecca717_0 conda-forge -libffi 3.5.2 h3435931_0 conda-forge -libfreetype 2.14.2 ha770c72_0 conda-forge -libfreetype6 2.14.2 h73754d4_0 conda-forge -libgcc 15.2.0 he0feb66_18 conda-forge -libgcc-ng 15.2.0 h69a702a_18 conda-forge -libgdal 3.10.3 h3b705f5_27 conda-forge -libgdal-core 3.10.3 h1f481a6_27 conda-forge -libgdal-fits 3.10.3 hec9d828_27 conda-forge -libgdal-grib 3.10.3 hb20eef8_27 conda-forge -libgdal-hdf4 3.10.3 ha810028_27 conda-forge -libgdal-hdf5 3.10.3 h966a9c2_27 conda-forge -libgdal-jp2openjpeg 3.10.3 hdd07572_27 conda-forge -libgdal-kea 3.10.3 h2bf108d_27 conda-forge -libgdal-netcdf 3.10.3 ha526aae_27 conda-forge -libgdal-pdf 3.10.3 h20efda7_27 conda-forge -libgdal-pg 3.10.3 h55c2262_27 conda-forge -libgdal-postgisraster 3.10.3 h55c2262_27 conda-forge -libgdal-tiledb 3.10.3 h6c35068_27 conda-forge -libgdal-xls 3.10.3 hdee084c_27 conda-forge -libgfortran 15.2.0 h69a702a_18 conda-forge -libgfortran5 15.2.0 h68bc16d_18 conda-forge -libgl 1.7.0 ha4b6fd6_2 conda-forge -libglib 2.86.4 h6548e54_1 conda-forge -libglvnd 1.7.0 ha4b6fd6_2 conda-forge -libglx 1.7.0 ha4b6fd6_2 conda-forge -libgomp 15.2.0 he0feb66_18 conda-forge -libgoogle-cloud 2.39.0 hdb79228_0 conda-forge -libgoogle-cloud-storage 2.39.0 hdbdcf42_0 conda-forge -libgrpc 1.73.1 h3288cfb_1 conda-forge -libhwy 1.3.0 h4c17acf_1 conda-forge -libiconv 1.18 h3b78370_2 conda-forge -libjpeg-turbo 3.1.2 hb03c661_0 conda-forge -libjxl 0.11.2 ha09017c_0 conda-forge -libkml 1.3.0 haa4a5bd_1022 conda-forge -liblapack 3.11.0 5_h47877c9_openblas conda-forge -liblzma 5.8.2 hb03c661_0 conda-forge -libnetcdf 4.9.3 nompi_hbf2fc22_104 conda-forge -libnghttp2 1.67.0 had1ee68_0 conda-forge -libnsl 2.0.1 hb9d3cd8_1 conda-forge -libntlm 1.8 hb9d3cd8_0 conda-forge -libopenblas 0.3.30 pthreads_h94d23a6_4 conda-forge -libpng 1.6.55 h421ea60_0 conda-forge -libpq 18.3 h9abb657_0 conda-forge -libprotobuf 6.31.1 h49aed37_4 conda-forge -libre2-11 2025.11.05 h7b12aa8_0 conda-forge -librttopo 1.1.0 h46dd2a8_20 conda-forge -libspatialite 5.1.0 gpl_h2abfd87_119 conda-forge -libsqlite 3.52.0 hf4e2dac_0 conda-forge -libssh2 1.11.1 hcf80075_0 conda-forge -libstdcxx 15.2.0 h934c35e_18 conda-forge -libstdcxx-ng 15.2.0 hdf11a46_18 conda-forge -libtiff 4.7.1 h9d88235_1 conda-forge -liburing 2.14 hb700be7_0 conda-forge -libuuid 2.41.3 h5347b49_0 conda-forge -libwebp-base 1.6.0 hd42ef1d_0 conda-forge -libxcb 1.17.0 h8a09558_0 conda-forge -libxcrypt 4.4.36 hd590300_1 conda-forge -libxml2 2.15.2 he237659_0 conda-forge -libxml2-16 2.15.2 hca6bf5a_0 conda-forge -libxml2-devel 2.15.2 he237659_0 conda-forge -libxslt 1.1.43 h711ed8c_1 conda-forge -libzip 1.11.2 h6991a6a_0 conda-forge -libzlib 1.3.1 hb9d3cd8_2 conda-forge -libzopfli 1.0.3 h9c3ff4c_0 conda-forge -lz4-c 1.10.0 h5888daf_1 conda-forge -lzo 2.10 h280c20c_1002 conda-forge -matplotlib 3.10.8 pypi_0 pypi -minizip 4.0.10 h05a5f5f_0 conda-forge -ncurses 6.5 h2d0b736_3 conda-forge -networkx 3.6.1 pyhcf101f3_0 conda-forge -nspr 4.38 h29cc59b_0 conda-forge -nss 3.118 h445c969_0 conda-forge -numpy 1.26.4 py311h64a7726_0 conda-forge -openjpeg 2.5.4 h55fea9a_0 conda-forge -openjph 0.26.3 h8d634f6_0 conda-forge -openldap 2.6.10 hbde042b_1 conda-forge -openmotif 2.3.8 hf55c2fc_5 conda-forge -openssl 3.6.1 h35e630c_1 conda-forge -packaging 26.0 pyhcf101f3_0 conda-forge -pcre2 10.47 haa7fec5_0 conda-forge -pillow 12.1.1 py311hf88fc01_0 conda-forge -pip 26.0.1 pyh8b19718_0 conda-forge -pixman 0.46.4 h54a6638_1 conda-forge -poppler 25.07.0 h13eef12_1 conda-forge -poppler-data 0.4.12 hd8ed1ab_0 conda-forge -postgresql 18.3 h9d31465_0 conda-forge -proj 9.7.1 he0df7b0_3 conda-forge -pthread-stubs 0.4 hb9d3cd8_1002 conda-forge -pyparsing 3.3.2 pypi_0 pypi -pysocks 1.7.1 pyha55dd90_7 conda-forge -python 3.11.15 hd63d673_0_cpython conda-forge -python-dateutil 2.9.0.post0 pypi_0 pypi -python_abi 3.11 8_cp311 conda-forge -rav1e 0.8.1 h1fbca29_0 conda-forge -re2 2025.11.05 h5301d42_0 conda-forge -readline 8.3 h853b02a_0 conda-forge -requests 2.32.5 pyhcf101f3_1 conda-forge -s2n 1.6.2 he8a4886_1 conda-forge -scikit-image 0.26.0 np2py311h2a99c40_0 conda-forge -scipy 1.17.1 py311hbe70eeb_0 conda-forge -setuptools 82.0.1 pyh332efcf_0 conda-forge -six 1.17.0 pypi_0 pypi -snappy 1.2.2 h03e3b7b_1 conda-forge -spdlog 1.16.0 hffee6e0_1 conda-forge -sqlite 3.52.0 h04a0ce9_0 conda-forge -svt-av1 4.0.1 hecca717_0 conda-forge -tifffile 2026.3.3 pyhd8ed1ab_0 conda-forge -tiledb 2.29.2 h8821262_1 conda-forge -tk 8.6.13 noxft_h366c992_103 conda-forge -tzcode 2026a h280c20c_0 conda-forge -tzdata 2025c hc9c84f9_1 conda-forge -uriparser 0.9.8 hac33072_0 conda-forge -urllib3 2.6.3 pyhd8ed1ab_0 conda-forge -wheel 0.46.3 pyhd8ed1ab_0 conda-forge -xerces-c 3.3.0 hd9031aa_1 conda-forge -xorg-libice 1.1.2 hb9d3cd8_0 conda-forge -xorg-libsm 1.2.6 he73a12e_0 conda-forge -xorg-libx11 1.8.13 he1eb515_0 conda-forge -xorg-libxau 1.0.12 hb03c661_1 conda-forge -xorg-libxdmcp 1.1.5 hb03c661_1 conda-forge -xorg-libxext 1.3.7 hb03c661_0 conda-forge -xorg-libxft 2.3.9 h355ab9f_0 conda-forge -xorg-libxmu 1.3.1 hb03c661_0 conda-forge -xorg-libxp 1.0.4 hb03c661_0 conda-forge -xorg-libxrender 0.9.12 hb9d3cd8_0 conda-forge -xorg-libxt 1.3.1 hb9d3cd8_0 conda-forge -zfp 1.0.1 h909a3a2_5 conda-forge -zlib 1.3.1 hb9d3cd8_2 conda-forge -zlib-ng 2.3.3 hceb46e0_1 conda-forge -zstd 1.5.7 hb78ec9c_6 conda-forge diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.explicit.txt b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.explicit.txt deleted file mode 100644 index 802e82d..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.explicit.txt +++ /dev/null @@ -1,221 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -# created-by: conda 26.1.1 -@EXPLICIT -https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda -https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda -https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda -https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda -https://conda.anaconda.org/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda -https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 -https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda -https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda -https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_18.conda -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda -https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda -https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda -https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda -https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda -https://conda.anaconda.org/conda-forge/linux-64/rav1e-0.8.1-h1fbca29_0.conda -https://conda.anaconda.org/conda-forge/linux-64/tzcode-2026a-h280c20c_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda -https://conda.anaconda.org/conda-forge/linux-64/capnproto-1.2.0-hfc315d8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/charls-2.4.3-hecca717_0.conda -https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda -https://conda.anaconda.org/conda-forge/linux-64/fmt-12.0.0-h2b0788b_0.conda -https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.1-h480dda7_0.conda -https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda -https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda -https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda -https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libacl-2.3.2-h0f662aa_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.5-h088129d_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_18.conda -https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.3.0-h4c17acf_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.55-h421ea60_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_18.conda -https://conda.anaconda.org/conda-forge/linux-64/liburing-2.14-hb700be7_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda -https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda -https://conda.anaconda.org/conda-forge/linux-64/nspr-4.38-h29cc59b_0.conda -https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda -https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda -https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda -https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda -https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda -https://conda.anaconda.org/conda-forge/linux-64/svt-av1-4.0.1-hecca717_0.conda -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda -https://conda.anaconda.org/conda-forge/linux-64/zfp-1.0.1-h909a3a2_5.conda -https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda -https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda -https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda -https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda -https://conda.anaconda.org/conda-forge/linux-64/brunsli-0.1-hd1e3526_2.conda -https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-2.23.1-hc31b594_0.conda -https://conda.anaconda.org/conda-forge/linux-64/fftw-3.3.10-nompi_h3b011a4_112.conda -https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda -https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 -https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.2-h73754d4_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.4-h6548e54_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-ha09017c_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda -https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_4.conda -https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h7b12aa8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h46dd2a8_20.conda -https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.2-hca6bf5a_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libzopfli-1.0.3-h9c3ff4c_0.tar.bz2 -https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.10-h05a5f5f_0.conda -https://conda.anaconda.org/conda-forge/linux-64/spdlog-1.16.0-hffee6e0_1.conda -https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.3.0-hd9031aa_1.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda -https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda -https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda -https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.18-h0c24ade_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libavif16-1.4.0-hcfa2d63_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda -https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.2-ha770c72_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda -https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-haa4a5bd_1022.conda -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.2-he237659_0.conda -https://conda.anaconda.org/conda-forge/linux-64/nss-3.118-h445c969_0.conda -https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda -https://conda.anaconda.org/conda-forge/linux-64/openjph-0.26.3-h8d634f6_0.conda -https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-hd63d673_0_cpython.conda -https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_0.conda -https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.52.0-h04a0ce9_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda -https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.1-h3a458e0_0.conda -https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py311h6b1f9c4_0.conda -https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda -https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 -https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/cfitsio-4.6.3-ha0b56bc_0.conda -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.17.1-h27c8c51_0.conda -https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.2-ha770c72_0.conda -https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_106.conda -https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.5-gpl_hc2c16d8_100.conda -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda -https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda -https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.73.1-h3288cfb_1.conda -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-5_h47877c9_openblas.conda -https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.2-he237659_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda -https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda -https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.10-hbde042b_1.conda -https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda -https://conda.anaconda.org/conda-forge/linux-64/pillow-12.1.1-py311hf88fc01_0.conda -https://conda.anaconda.org/conda-forge/linux-64/proj-9.7.1-he0df7b0_3.conda -https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda -https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxmu-1.3.1-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxp-1.0.4-hb03c661_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda -https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.2-h3a5f585_1.conda -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.11.0-h3d7a050_1.conda -https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 -https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda -https://conda.anaconda.org/conda-forge/linux-64/geotiff-1.7.4-h1000f5c_4.conda -https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda -https://conda.anaconda.org/conda-forge/linux-64/kealib-1.6.2-hb2f3951_2.conda -https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.5-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-hdb79228_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_hbf2fc22_104.conda -https://conda.anaconda.org/conda-forge/linux-64/libpq-18.3-h9abb657_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_h2abfd87_119.conda -https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py311h64a7726_0.conda -https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxft-2.3.9-h355ab9f_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.2-h8824e59_6.conda -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.15.0-h2a74896_1.conda -https://conda.anaconda.org/conda-forge/linux-64/h5py-3.15.1-nompi_py311h0b2f468_101.conda -https://conda.anaconda.org/conda-forge/linux-64/imagecodecs-2026.3.6-py311h9837d23_1.conda -https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-core-3.10.3-h1f481a6_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_0.conda -https://conda.anaconda.org/conda-forge/linux-64/openmotif-2.3.8-hf55c2fc_5.conda -https://conda.anaconda.org/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda -https://conda.anaconda.org/conda-forge/linux-64/poppler-25.07.0-h13eef12_1.conda -https://conda.anaconda.org/conda-forge/linux-64/postgresql-18.3-h9d31465_0.conda -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py311hbe70eeb_0.conda -https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-hf38915e_9.conda -https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.13.0-hf38f1be_1.conda -https://conda.anaconda.org/conda-forge/linux-64/gdal-3.10.3-py311h34ccccb_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-fits-3.10.3-hec9d828_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-grib-3.10.3-hb20eef8_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-hdf4-3.10.3-ha810028_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-hdf5-3.10.3-h966a9c2_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-jp2openjpeg-3.10.3-hdd07572_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-pdf-3.10.3-h20efda7_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-pg-3.10.3-h55c2262_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-postgisraster-3.10.3-h55c2262_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-xls-3.10.3-hdee084c_27.conda -https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda -https://conda.anaconda.org/conda-forge/noarch/tifffile-2026.3.3-pyhd8ed1ab_0.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-kea-3.10.3-h2bf108d_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-netcdf-3.10.3-ha526aae_27.conda -https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py311h2a99c40_0.conda -https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.29.2-h8821262_1.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-tiledb-3.10.3-h6c35068_27.conda -https://conda.anaconda.org/conda-forge/linux-64/libgdal-3.10.3-h3b705f5_27.conda -https://conda.anaconda.org/conda-forge/linux-64/isce2-2.6.4-py311h916084f_2.conda diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.no_builds.yml b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.no_builds.yml deleted file mode 100644 index f1225c9..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.no_builds.yml +++ /dev/null @@ -1,231 +0,0 @@ -name: isce2 -channels: - - conda-forge - - defaults -dependencies: - - _openmp_mutex=4.5 - - aom=3.9.1 - - attr=2.5.2 - - aws-c-auth=0.9.3 - - aws-c-cal=0.9.13 - - aws-c-common=0.12.6 - - aws-c-compression=0.3.1 - - aws-c-event-stream=0.5.7 - - aws-c-http=0.10.7 - - aws-c-io=0.23.3 - - aws-c-mqtt=0.13.3 - - aws-c-s3=0.11.3 - - aws-c-sdkutils=0.2.4 - - aws-checksums=0.2.7 - - aws-crt-cpp=0.35.2 - - aws-sdk-cpp=1.11.606 - - azure-core-cpp=1.16.1 - - azure-identity-cpp=1.13.2 - - azure-storage-blobs-cpp=12.15.0 - - azure-storage-common-cpp=12.11.0 - - azure-storage-files-datalake-cpp=12.13.0 - - backports.zstd=1.3.0 - - blosc=1.21.6 - - brotli-python=1.2.0 - - brunsli=0.1 - - bzip2=1.0.8 - - c-ares=1.34.6 - - c-blosc2=2.23.1 - - ca-certificates=2026.2.25 - - cached-property=1.5.2 - - cached_property=1.5.2 - - cairo=1.18.4 - - capnproto=1.2.0 - - certifi=2026.2.25 - - cfitsio=4.6.3 - - charls=2.4.3 - - charset-normalizer=3.4.5 - - cyrus-sasl=2.1.28 - - dav1d=1.2.1 - - fftw=3.3.10 - - fmt=12.0.0 - - font-ttf-dejavu-sans-mono=2.37 - - font-ttf-inconsolata=3.000 - - font-ttf-source-code-pro=2.038 - - font-ttf-ubuntu=0.83 - - fontconfig=2.17.1 - - fonts-conda-ecosystem=1 - - fonts-conda-forge=1 - - freetype=2.14.2 - - freexl=2.0.0 - - gdal=3.10.3 - - geos=3.14.1 - - geotiff=1.7.4 - - giflib=5.2.2 - - h2=4.3.0 - - h5py=3.15.1 - - hdf4=4.2.15 - - hdf5=1.14.6 - - hpack=4.1.0 - - hyperframe=6.1.0 - - icu=78.2 - - idna=3.11 - - imagecodecs=2026.3.6 - - imageio=2.37.0 - - isce2=2.6.4 - - json-c=0.18 - - jxrlib=1.1 - - kealib=1.6.2 - - keyutils=1.6.3 - - krb5=1.22.2 - - lazy-loader=0.5 - - lcms2=2.18 - - ld_impl_linux-64=2.45.1 - - lerc=4.1.0 - - libabseil=20250512.1 - - libacl=2.3.2 - - libaec=1.1.5 - - libarchive=3.8.5 - - libavif16=1.4.0 - - libblas=3.11.0 - - libbrotlicommon=1.2.0 - - libbrotlidec=1.2.0 - - libbrotlienc=1.2.0 - - libcblas=3.11.0 - - libcrc32c=1.1.2 - - libcurl=8.18.0 - - libdeflate=1.25 - - libedit=3.1.20250104 - - libev=4.33 - - libexpat=2.7.4 - - libffi=3.5.2 - - libfreetype=2.14.2 - - libfreetype6=2.14.2 - - libgcc=15.2.0 - - libgcc-ng=15.2.0 - - libgdal=3.10.3 - - libgdal-core=3.10.3 - - libgdal-fits=3.10.3 - - libgdal-grib=3.10.3 - - libgdal-hdf4=3.10.3 - - libgdal-hdf5=3.10.3 - - libgdal-jp2openjpeg=3.10.3 - - libgdal-kea=3.10.3 - - libgdal-netcdf=3.10.3 - - libgdal-pdf=3.10.3 - - libgdal-pg=3.10.3 - - libgdal-postgisraster=3.10.3 - - libgdal-tiledb=3.10.3 - - libgdal-xls=3.10.3 - - libgfortran=15.2.0 - - libgfortran5=15.2.0 - - libgl=1.7.0 - - libglib=2.86.4 - - libglvnd=1.7.0 - - libglx=1.7.0 - - libgomp=15.2.0 - - libgoogle-cloud=2.39.0 - - libgoogle-cloud-storage=2.39.0 - - libgrpc=1.73.1 - - libhwy=1.3.0 - - libiconv=1.18 - - libjpeg-turbo=3.1.2 - - libjxl=0.11.2 - - libkml=1.3.0 - - liblapack=3.11.0 - - liblzma=5.8.2 - - libnetcdf=4.9.3 - - libnghttp2=1.67.0 - - libnsl=2.0.1 - - libntlm=1.8 - - libopenblas=0.3.30 - - libpng=1.6.55 - - libpq=18.3 - - libprotobuf=6.31.1 - - libre2-11=2025.11.05 - - librttopo=1.1.0 - - libspatialite=5.1.0 - - libsqlite=3.52.0 - - libssh2=1.11.1 - - libstdcxx=15.2.0 - - libstdcxx-ng=15.2.0 - - libtiff=4.7.1 - - liburing=2.14 - - libuuid=2.41.3 - - libwebp-base=1.6.0 - - libxcb=1.17.0 - - libxcrypt=4.4.36 - - libxml2=2.15.2 - - libxml2-16=2.15.2 - - libxml2-devel=2.15.2 - - libxslt=1.1.43 - - libzip=1.11.2 - - libzlib=1.3.1 - - libzopfli=1.0.3 - - lz4-c=1.10.0 - - lzo=2.10 - - minizip=4.0.10 - - ncurses=6.5 - - networkx=3.6.1 - - nspr=4.38 - - nss=3.118 - - numpy=1.26.4 - - openjpeg=2.5.4 - - openjph=0.26.3 - - openldap=2.6.10 - - openmotif=2.3.8 - - openssl=3.6.1 - - packaging=26.0 - - pcre2=10.47 - - pillow=12.1.1 - - pip=26.0.1 - - pixman=0.46.4 - - poppler=25.07.0 - - poppler-data=0.4.12 - - postgresql=18.3 - - proj=9.7.1 - - pthread-stubs=0.4 - - pysocks=1.7.1 - - python=3.11.15 - - python_abi=3.11 - - rav1e=0.8.1 - - re2=2025.11.05 - - readline=8.3 - - requests=2.32.5 - - s2n=1.6.2 - - scikit-image=0.26.0 - - scipy=1.17.1 - - setuptools=82.0.1 - - snappy=1.2.2 - - spdlog=1.16.0 - - sqlite=3.52.0 - - svt-av1=4.0.1 - - tifffile=2026.3.3 - - tiledb=2.29.2 - - tk=8.6.13 - - tzcode=2026a - - tzdata=2025c - - uriparser=0.9.8 - - urllib3=2.6.3 - - wheel=0.46.3 - - xerces-c=3.3.0 - - xorg-libice=1.1.2 - - xorg-libsm=1.2.6 - - xorg-libx11=1.8.13 - - xorg-libxau=1.0.12 - - xorg-libxdmcp=1.1.5 - - xorg-libxext=1.3.7 - - xorg-libxft=2.3.9 - - xorg-libxmu=1.3.1 - - xorg-libxp=1.0.4 - - xorg-libxrender=0.9.12 - - xorg-libxt=1.3.1 - - zfp=1.0.1 - - zlib=1.3.1 - - zlib-ng=2.3.3 - - zstd=1.5.7 - - pip: - - contourpy==1.3.3 - - cycler==0.12.1 - - fonttools==4.62.1 - - kiwisolver==1.5.0 - - matplotlib==3.10.8 - - pyparsing==3.3.2 - - python-dateutil==2.9.0.post0 - - six==1.17.0 -prefix: /home/administrator/miniconda3/envs/isce2 diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.runtime_versions.txt b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.runtime_versions.txt deleted file mode 100644 index 7e67fcb..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2.runtime_versions.txt +++ /dev/null @@ -1,12 +0,0 @@ -python_executable=/home/administrator/miniconda3/envs/isce2/bin/python -python_version=3.11.15 -isce_present=True -mintpy_present=False -h5py_present=True -isce_file=/home/administrator/miniconda3/envs/isce2/lib/python3.11/site-packages/isce/__init__.py -isce_version=2.6.3 -2026-04-06 15:31:15,137 - h5py._conv - DEBUG - Creating converter from 7 to 5 -2026-04-06 15:31:15,138 - h5py._conv - DEBUG - Creating converter from 5 to 7 -2026-04-06 15:31:15,138 - h5py._conv - DEBUG - Creating converter from 7 to 5 -2026-04-06 15:31:15,139 - h5py._conv - DEBUG - Creating converter from 5 to 7 -h5py_version=3.15.1 diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.conda_list.txt b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.conda_list.txt deleted file mode 100644 index 86b4024..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.conda_list.txt +++ /dev/null @@ -1,350 +0,0 @@ -# packages in environment at /home/administrator/miniconda3/envs/isce2_mintpy_v1: -# -# Name Version Build Channel -_openmp_mutex 4.5 20_gnu https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -_x86_64-microarch-level 3 3_skylake https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aom 3.9.1 hac33072_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -argcomplete 3.6.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -attr 2.5.2 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -attrs 26.1.0 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-auth 0.9.3 hef928c7_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-cal 0.9.13 h2c9d079_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-common 0.12.6 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-compression 0.3.1 h8b1a151_9 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-event-stream 0.5.7 h28f887f_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-http 0.10.7 ha8fc4e3_5 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-io 0.23.3 hdaf4b65_5 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-mqtt 0.13.3 hc63082f_11 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-s3 0.11.3 h06ab39a_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-c-sdkutils 0.2.4 h8b1a151_4 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-checksums 0.2.7 h8b1a151_5 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-crt-cpp 0.35.2 h8824e59_6 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -aws-sdk-cpp 1.11.606 hf38915e_9 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -azure-core-cpp 1.16.1 h3a458e0_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -azure-identity-cpp 1.13.2 h3a5f585_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -azure-storage-blobs-cpp 12.15.0 h2a74896_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -azure-storage-common-cpp 12.11.0 h3d7a050_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -azure-storage-files-datalake-cpp 12.13.0 hf38f1be_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -backports.zstd 1.3.0 py311h6b1f9c4_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -blosc 1.21.6 he440d0b_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -bokeh 3.9.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -brotli 1.2.0 hed03a55_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -brotli-bin 1.2.0 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -brotli-python 1.2.0 py311h66f275b_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -brunsli 0.1 hd1e3526_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -bzip2 1.0.8 hda65f42_9 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -c-ares 1.34.6 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -c-blosc2 2.23.1 hc31b594_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -ca-certificates 2026.2.25 hbd8a1cb_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cached-property 1.5.2 hd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cached_property 1.5.2 pyha770c72_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cairo 1.18.4 he90730b_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -capnproto 1.2.0 hfc315d8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cartopy 0.25.0 py311hed34c8f_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cdsapi 0.7.7 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -certifi 2026.2.25 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cffi 2.0.0 py311h03d9500_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cfgv 3.5.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cfitsio 4.6.3 ha0b56bc_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -charls 2.4.3 hecca717_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -charset-normalizer 3.4.5 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -click 8.3.1 pyh8f84b5b_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cloudpickle 3.1.2 pyhcf101f3_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -configobj 5.0.9 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -contourpy 1.3.3 py311h724c32c_4 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cvxopt 1.3.3 py311h3d1f434_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cycler 0.12.1 pyhcf101f3_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cyrus-sasl 2.1.28 hac629b4_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -cytoolz 1.1.0 py311h49ec1c0_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -dask 2026.3.0 pyhc364b38_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -dask-core 2026.3.0 pyhc364b38_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -dask-jobqueue 0.9.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -dav1d 1.2.1 hd590300_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -distlib 0.4.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -distributed 2026.3.0 pyhc364b38_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -donfig 0.8.1.post1 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -dsdp 5.8 hd9d9efa_1203 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -eccodes 2.46.0 h83bc92c_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -ecmwf-datastores-client 0.5.1 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fftw 3.3.10 nompi_h3b011a4_112 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -filelock 3.25.2 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fmt 12.0.0 h2b0788b_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -font-ttf-dejavu-sans-mono 2.37 hab24e00_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -font-ttf-inconsolata 3.000 h77eed37_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -font-ttf-source-code-pro 2.038 h77eed37_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -font-ttf-ubuntu 0.83 h77eed37_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fontconfig 2.17.1 h27c8c51_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fonts-conda-ecosystem 1 0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fonts-conda-forge 1 hc364b38_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fonttools 4.62.1 pypi_0 pypi -freeglut 3.2.2 ha6d2627_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -freetype 2.14.2 ha770c72_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -freexl 2.0.0 h9dce30a_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -fsspec 2026.3.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -gdal 3.10.3 py311h34ccccb_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -geos 3.14.1 h480dda7_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -geotiff 1.7.4 h1000f5c_4 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -gflags 2.2.2 h5888daf_1005 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -giflib 5.2.2 hd590300_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -glog 0.7.1 hbabe93e_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -glpk 5.0 h445213a_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -gmp 6.3.0 hac33072_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -gsl 2.7 he838d99_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -h2 4.3.0 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -h5py 3.15.1 nompi_py311h0b2f468_101 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -hdf4 4.2.15 h2a13503_7 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -hdf5 1.14.6 nompi_h19486de_106 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -hpack 4.1.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -hyperframe 6.1.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -icu 78.2 h33c6efd_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -identify 2.6.18 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -idna 3.11 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -imagecodecs 2026.3.6 py311h9837d23_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -imageio 2.37.0 pyhfb79c49_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -importlib-metadata 8.8.0 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -isce2 2.6.4 py311h916084f_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -jasper 4.2.9 h1588d4d_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -jinja2 3.1.6 pyhcf101f3_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -joblib 1.5.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -json-c 0.18 h6688a6e_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -jxrlib 1.1 hd590300_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -kealib 1.6.2 hb2f3951_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -keyutils 1.6.3 hb9d3cd8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -kiwisolver 1.5.0 py311h724c32c_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -krb5 1.22.2 ha1258a1_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lazy-loader 0.5 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lcms2 2.18 h0c24ade_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -ld_impl_linux-64 2.45.1 default_hbd61a6d_102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lerc 4.1.0 hdb68285_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libabseil 20250512.1 cxx17_hba17884_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libacl 2.3.2 h0f662aa_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libaec 1.1.5 h088129d_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libamd 3.3.3 haaf9dc3_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libarchive 3.8.5 gpl_hc2c16d8_100 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libarrow 22.0.0 h2937f24_4_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libarrow-acero 22.0.0 hb826db4_4_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libarrow-compute 22.0.0 h58682fd_4_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libarrow-dataset 22.0.0 hb826db4_4_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libarrow-substrait 22.0.0 h9d9f3f8_4_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libattr 2.5.2 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libavif16 1.4.0 hcfa2d63_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libblas 3.11.0 5_h4a7cf45_openblas https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libbrotlicommon 1.2.0 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libbrotlidec 1.2.0 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libbrotlienc 1.2.0 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libbtf 2.3.2 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcamd 3.3.3 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcblas 3.11.0 5_h0358290_openblas https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libccolamd 3.3.4 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcholmod 5.3.1 h59ddab4_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcolamd 3.3.4 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcrc32c 1.1.2 h9c3ff4c_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcurl 8.18.0 hcf29cc6_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libcxsparse 4.4.1 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libdeflate 1.25 h17f619e_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libedit 3.1.20250104 pl5321h7949ede_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libev 4.33 hd590300_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libevent 2.1.12 hf998b51_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libexpat 2.7.4 hecca717_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libffi 3.5.2 h3435931_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libfreetype 2.14.2 ha770c72_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libfreetype6 2.14.2 h73754d4_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgcc 15.2.0 he0feb66_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgcc-ng 15.2.0 h69a702a_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal 3.10.3 h3b705f5_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-core 3.10.3 h1f481a6_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-fits 3.10.3 hec9d828_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-grib 3.10.3 hb20eef8_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-hdf4 3.10.3 ha810028_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-hdf5 3.10.3 h966a9c2_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-jp2openjpeg 3.10.3 hdd07572_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-kea 3.10.3 h2bf108d_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-netcdf 3.10.3 ha526aae_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-pdf 3.10.3 h20efda7_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-pg 3.10.3 h55c2262_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-postgisraster 3.10.3 h55c2262_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-tiledb 3.10.3 h6c35068_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgdal-xls 3.10.3 hdee084c_27 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgfortran 15.2.0 h69a702a_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgfortran5 15.2.0 h68bc16d_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgl 1.7.0 ha4b6fd6_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libglib 2.86.4 h6548e54_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libglu 9.0.3 h5888daf_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libglvnd 1.7.0 ha4b6fd6_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libglx 1.7.0 ha4b6fd6_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgomp 15.2.0 he0feb66_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgoogle-cloud 2.39.0 hdb79228_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgoogle-cloud-storage 2.39.0 hdbdcf42_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libgrpc 1.73.1 h3288cfb_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libhwy 1.3.0 h4c17acf_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libiconv 1.18 h3b78370_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libjpeg-turbo 3.1.2 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libjxl 0.11.2 ha09017c_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libklu 2.3.5 hf24d653_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libkml 1.3.0 haa4a5bd_1022 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -liblapack 3.11.0 5_h47877c9_openblas https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libldl 3.3.2 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -liblzma 5.8.2 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libnetcdf 4.9.3 nompi_hbf2fc22_104 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libnghttp2 1.67.0 had1ee68_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libnsl 2.0.1 hb9d3cd8_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libntlm 1.8 hb9d3cd8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libopenblas 0.3.30 pthreads_h94d23a6_4 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libopengl 1.7.0 ha4b6fd6_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libopentelemetry-cpp 1.21.0 hb9b0907_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libopentelemetry-cpp-headers 1.21.0 ha770c72_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libparquet 22.0.0 h31208bf_4_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libparu 1.0.0 h17147ab_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libpng 1.6.55 h421ea60_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libpq 18.3 h9abb657_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libprotobuf 6.31.1 h49aed37_4 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -librbio 4.3.4 h32481e8_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libre2-11 2025.11.05 h7b12aa8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -librttopo 1.1.0 h46dd2a8_20 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libspatialite 5.1.0 gpl_h2abfd87_119 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libspex 3.2.3 had10066_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libspqr 4.3.4 h852d39f_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libsqlite 3.52.0 hf4e2dac_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libssh2 1.11.1 hcf80075_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libstdcxx 15.2.0 h934c35e_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libstdcxx-ng 15.2.0 hdf11a46_18 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libsuitesparseconfig 7.10.1 h92d6892_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libthrift 0.22.0 h454ac66_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libtiff 4.7.1 h9d88235_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libumfpack 6.3.5 heb53515_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -liburing 2.14 hb700be7_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libutf8proc 2.11.3 hfe17d71_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libuuid 2.41.3 h5347b49_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libwebp-base 1.6.0 hd42ef1d_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libxcb 1.17.0 h8a09558_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libxcrypt 4.4.36 hd590300_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libxml2 2.15.2 he237659_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libxml2-16 2.15.2 hca6bf5a_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libxml2-devel 2.15.2 he237659_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libxslt 1.1.43 h711ed8c_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libzip 1.11.2 h6991a6a_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libzlib 1.3.1 hb9d3cd8_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -libzopfli 1.0.3 h9c3ff4c_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -locket 1.0.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lxml 6.0.2 py311h8840267_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lz4 4.4.5 py311h1c460e0_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lz4-c 1.10.0 h5888daf_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -lzo 2.10 h280c20c_1002 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -markdown-it-py 4.0.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -markupsafe 3.0.3 py311h3778330_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -matplotlib-base 3.10.8 py311h0f3be63_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -mdurl 0.1.2 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -metis 5.1.0 hd0bcaf9_1007 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -minizip 4.0.10 h05a5f5f_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -mintpy 1.6.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -mpfr 4.2.2 he0a73b1_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -msgpack-python 1.1.2 py311hdf67eae_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -multiurl 0.3.7 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -munkres 1.1.4 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -narwhals 2.18.1 pyhcf101f3_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -ncurses 6.5 h2d0b736_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -networkx 3.6.1 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -nlohmann_json 3.12.0 h54a6638_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -nodeenv 1.10.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -nspr 4.38 h29cc59b_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -nss 3.118 h445c969_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -numpy 1.26.4 py311h64a7726_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -openjpeg 2.5.4 h55fea9a_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -openjph 0.26.3 h8d634f6_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -openldap 2.6.10 hbde042b_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -openmotif 2.3.8 he4bd66d_6 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -openssl 3.6.1 h35e630c_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -orc 2.2.1 hd747db4_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -packaging 26.0 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pandas 3.0.2 py311h8032f78_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -partd 1.4.2 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pcre2 10.47 haa7fec5_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pillow 12.1.1 py311hf88fc01_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pip 26.0.1 pyh8b19718_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pixman 0.46.4 h54a6638_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -platformdirs 4.9.4 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -poppler 25.07.0 h13eef12_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -poppler-data 0.4.12 hd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -postgresql 18.3 h9d31465_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pre-commit 4.5.1 pyha770c72_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -proj 9.7.1 he0df7b0_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -prometheus-cpp 1.3.0 ha5d0236_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -psutil 7.2.2 py311haee01d2_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pthread-stubs 0.4 hb9d3cd8_1002 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyaps3 0.3.7 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyarrow 22.0.0 py311h38be061_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyarrow-core 22.0.0 py311hbabfba9_2_cuda https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pycparser 2.22 pyh29332c3_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pygments 2.20.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pygrib 2.1.8 py311he4f3390_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pykdtree 1.4.3 py311h0372a8f_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pykml 0.2.0 pyhd8ed1ab_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyparsing 3.3.2 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyproj 3.7.2 py311h400b93a_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyresample 1.35.0 py311h1ddb823_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyshp 3.0.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pysocks 1.7.1 pyha55dd90_7 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pysolid 0.3.4 py311h9bb1bfa_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -python 3.11.15 hd63d673_0_cpython https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -python-dateutil 2.9.0.post0 pyhe01879c_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -python-discovery 1.2.1 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -python_abi 3.11 8_cp311 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pytz 2026.1.post1 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -pyyaml 6.0.3 py311h3778330_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -qhull 2020.2 h434a139_5 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -rav1e 0.8.1 h1fbca29_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -re2 2025.11.05 h5301d42_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -readline 8.3 h853b02a_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -requests 2.32.5 pyhcf101f3_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -rich 14.3.3 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -s2n 1.6.2 he8a4886_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -scikit-image 0.26.0 np2py311h2a99c40_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -scipy 1.17.1 py311hbe70eeb_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -setuptools 82.0.1 pyh332efcf_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -shapely 2.1.2 py311h8a92878_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -six 1.17.0 pyhe01879c_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -snappy 1.2.2 h03e3b7b_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -sortedcontainers 2.4.0 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -spdlog 1.16.0 hffee6e0_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -sqlite 3.52.0 h04a0ce9_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -suitesparse 7.10.1 ha0f6916_7100102 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -svt-av1 4.0.1 hecca717_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tblib 3.2.2 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tifffile 2026.3.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tiledb 2.29.2 h8821262_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tk 8.6.13 noxft_h366c992_103 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -toolz 1.1.0 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tornado 6.5.5 py311h49ec1c0_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tqdm 4.67.3 pyh8f84b5b_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -typing_extensions 4.15.0 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tzcode 2026a h280c20c_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -tzdata 2025c hc9c84f9_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -ukkonen 1.1.0 py311hdf67eae_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -unicodedata2 17.0.1 py311h49ec1c0_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -uriparser 0.9.8 hac33072_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -urllib3 2.6.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -utm 0.7.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -virtualenv 21.2.0 pyhcf101f3_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -wheel 0.46.3 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xerces-c 3.3.0 hd9031aa_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libice 1.1.2 hb9d3cd8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libsm 1.2.6 he73a12e_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libx11 1.8.13 he1eb515_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxau 1.0.12 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxdmcp 1.1.5 hb03c661_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxext 1.3.7 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxfixes 6.0.2 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxft 2.3.9 h355ab9f_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxi 1.8.2 hb9d3cd8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxmu 1.3.1 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxp 1.0.4 hb03c661_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxrender 0.9.12 hb9d3cd8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xorg-libxt 1.3.1 hb9d3cd8_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -xyzservices 2026.3.0 pyhd8ed1ab_0 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -yaml 0.2.5 h280c20c_3 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -zfp 1.0.1 h909a3a2_5 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -zict 3.0.0 pyhd8ed1ab_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -zipp 3.23.0 pyhcf101f3_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -zlib 1.3.1 hb9d3cd8_2 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -zlib-ng 2.3.3 hceb46e0_1 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge -zstd 1.5.7 hb78ec9c_6 https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.explicit.txt b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.explicit.txt deleted file mode 100644 index c3390b4..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.explicit.txt +++ /dev/null @@ -1,352 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -# created-by: conda 26.1.1 -@EXPLICIT -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/_x86_64-microarch-level-3-3_skylake.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/python_abi-3.11-8_cp311.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libattr-2.5.2-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libjpeg-turbo-3.1.2-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/rav1e-0.8.1-h1fbca29_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/tzcode-2026a-h280c20c_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/attr-2.5.2-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/capnproto-1.2.0-hfc315d8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/charls-2.4.3-hecca717_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/fmt-12.0.0-h2b0788b_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/geos-3.14.1-h480dda7_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libaec-1.1.5-h088129d_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libev-4.33-hd590300_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libglu-9.0.3-h5888daf_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libhwy-1.3.0-h4c17acf_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libpng-1.6.55-h421ea60_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_18.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/liburing-2.14-hb700be7_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/metis-5.1.0-hd0bcaf9_1007.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/nspr-4.38-h29cc59b_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/readline-8.3-h853b02a_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/svt-av1-4.0.1-hecca717_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/zfp-1.0.1-h909a3a2_5.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/brunsli-0.1-hd1e3526_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/c-blosc2-2.23.1-hc31b594_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/fftw-3.3.10-nompi_h3b011a4_112.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libacl-2.3.2-h0f662aa_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libfreetype6-2.14.2-h73754d4_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libglib-2.86.4-h6548e54_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libjxl-0.11.2-ha09017c_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_4.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libre2-11-2025.11.05-h7b12aa8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/librttopo-1.1.0-h46dd2a8_20.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libsuitesparseconfig-7.10.1-h92d6892_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libxml2-16-2.15.2-hca6bf5a_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libzopfli-1.0.3-h9c3ff4c_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/minizip-4.0.10-h05a5f5f_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/spdlog-1.16.0-hffee6e0_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xerces-c-3.3.0-hd9031aa_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/glpk-5.0-h445213a_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/lcms2-2.18-h0c24ade_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libamd-3.3.3-haaf9dc3_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libavif16-1.4.0-hcfa2d63_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libbtf-2.3.2-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcamd-3.3.3-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libccolamd-3.3.4-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcolamd-3.3.4-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcxsparse-4.4.1-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libfreetype-2.14.2-ha770c72_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libkml-1.3.0-haa4a5bd_1022.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libldl-3.3.2-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/librbio-4.3.4-h32481e8_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libxml2-2.15.2-he237659_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/mpfr-4.2.2-he0a73b1_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/nss-3.118-h445c969_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/openjph-0.26.3-h8d634f6_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/orc-2.2.1-hd747db4_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/python-3.11.15-hd63d673_0_cpython.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/re2-2025.11.05-h5301d42_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/sqlite-3.52.0-h04a0ce9_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/argcomplete-3.6.3-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/azure-core-cpp-1.16.1-h3a458e0_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/backports.zstd-1.3.0-py311h6b1f9c4_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cfitsio-4.6.3-ha0b56bc_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/distlib-0.4.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/filelock-3.25.2-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/fontconfig-2.17.1-h27c8c51_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/freetype-2.14.2-ha770c72_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/fsspec-2026.3.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_106.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/kiwisolver-1.5.0-py311h724c32c_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libarchive-3.8.5-gpl_hc2c16d8_100.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgrpc-1.73.1-h3288cfb_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/liblapack-3.11.0-5_h47877c9_openblas.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libspex-3.2.3-had10066_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libxml2-devel-2.15.2-he237659_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/lz4-4.4.5-py311h1c460e0_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/msgpack-python-1.1.2-py311hdf67eae_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/narwhals-2.18.1-pyhcf101f3_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/openldap-2.6.10-hbde042b_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pillow-12.1.1-py311hf88fc01_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/platformdirs-4.9.4-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/proj-9.7.1-he0df7b0_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pyshp-3.0.3-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pytz-2026.1.post1-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/tornado-6.5.5-py311h49ec1c0_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/unicodedata2-17.0.1-py311h49ec1c0_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/utm-0.7.0-pyhd8ed1ab_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxmu-1.3.1-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxp-1.0.4-hb03c661_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/azure-identity-cpp-1.13.2-h3a5f585_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/azure-storage-common-cpp-12.11.0-h3d7a050_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/configobj-5.0.9-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cytoolz-1.1.0-py311h49ec1c0_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/dsdp-5.8-hd9d9efa_1203.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/fonttools-4.62.0-py311h3778330_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/freeglut-3.2.2-ha6d2627_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/geotiff-1.7.4-h1000f5c_4.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/gsl-2.7-he838d99_0.tar.bz2 -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/kealib-1.6.2-hb2f3951_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/lazy-loader-0.5-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libcholmod-5.3.1-h59ddab4_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgoogle-cloud-2.39.0-hdb79228_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libnetcdf-4.9.3-nompi_hbf2fc22_104.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-hb9b0907_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libpq-18.3-h9abb657_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libspatialite-5.1.0-gpl_h2abfd87_119.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/lxml-6.0.2-py311h8840267_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/numpy-1.26.4-py311h64a7726_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pyproj-3.7.2-py311h400b93a_3.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/python-discovery-1.2.1-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/xorg-libxft-2.3.9-h355ab9f_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-crt-cpp-0.35.2-h8824e59_6.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/azure-storage-blobs-cpp-12.15.0-h2a74896_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/contourpy-1.3.3-py311h724c32c_4.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/dask-core-2026.3.0-pyhc364b38_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/h5py-3.15.1-nompi_py311h0b2f468_101.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/imagecodecs-2026.3.6-py311h9837d23_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/jasper-4.2.9-h1588d4d_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-core-3.10.3-h1f481a6_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libklu-2.3.5-hf24d653_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libspqr-4.3.4-h852d39f_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libumfpack-6.3.5-heb53515_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/openmotif-2.3.8-he4bd66d_6.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pandas-3.0.2-py311h8032f78_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/poppler-25.07.0-h13eef12_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/postgresql-18.3-h9d31465_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pykdtree-1.4.3-py311h0372a8f_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pykml-0.2.0-pyhd8ed1ab_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/rich-14.3.3-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/scipy-1.17.1-py311hbe70eeb_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/shapely-2.1.2-py311h8a92878_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/ukkonen-1.1.0-py311hdf67eae_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/virtualenv-21.2.0-pyhcf101f3_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/aws-sdk-cpp-1.11.606-hf38915e_9.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.13.0-hf38f1be_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/bokeh-3.9.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/distributed-2026.3.0-pyhc364b38_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/eccodes-2.46.0-h83bc92c_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/gdal-3.10.3-py311h34ccccb_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/identify-2.6.18-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-fits-3.10.3-hec9d828_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-grib-3.10.3-hb20eef8_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-hdf4-3.10.3-ha810028_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-hdf5-3.10.3-h966a9c2_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-jp2openjpeg-3.10.3-hdd07572_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-pdf-3.10.3-h20efda7_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-pg-3.10.3-h55c2262_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-postgisraster-3.10.3-h55c2262_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-xls-3.10.3-hdee084c_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libparu-1.0.0-h17147ab_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/matplotlib-base-3.10.8-py311h0f3be63_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pyresample-1.35.0-py311h1ddb823_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pysolid-0.3.4-py311h9bb1bfa_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/tifffile-2026.3.3-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cartopy-0.25.0-py311hed34c8f_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/dask-jobqueue-0.9.0-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libarrow-22.0.0-h2937f24_4_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-kea-3.10.3-h2bf108d_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-netcdf-3.10.3-ha526aae_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/multiurl-0.3.7-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pre-commit-4.5.1-pyha770c72_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pygrib-2.1.8-py311he4f3390_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/scikit-image-0.26.0-np2py311h2a99c40_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/suitesparse-7.10.1-ha0f6916_7100102.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/tiledb-2.29.2-h8821262_1.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/cvxopt-1.3.3-py311h3d1f434_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/ecmwf-datastores-client-0.5.1-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libarrow-compute-22.0.0-h58682fd_4_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-tiledb-3.10.3-h6c35068_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libparquet-22.0.0-h31208bf_4_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/cdsapi-0.7.7-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libarrow-acero-22.0.0-hb826db4_4_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libgdal-3.10.3-h3b705f5_27.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pyarrow-core-22.0.0-py311hbabfba9_2_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/isce2-2.6.4-py311h916084f_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libarrow-dataset-22.0.0-hb826db4_4_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/pyaps3-0.3.7-pyhd8ed1ab_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/libarrow-substrait-22.0.0-h9d9f3f8_4_cuda.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64/pyarrow-22.0.0-py311h38be061_2.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/dask-2026.3.0-pyhc364b38_0.conda -https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/noarch/mintpy-1.6.3-pyhd8ed1ab_0.conda diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.no_builds.yml b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.no_builds.yml deleted file mode 100644 index 074111f..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.no_builds.yml +++ /dev/null @@ -1,354 +0,0 @@ -name: isce2_mintpy_v1 -channels: - - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge - - defaults -dependencies: - - _openmp_mutex=4.5 - - _x86_64-microarch-level=3 - - aom=3.9.1 - - argcomplete=3.6.3 - - attr=2.5.2 - - attrs=26.1.0 - - aws-c-auth=0.9.3 - - aws-c-cal=0.9.13 - - aws-c-common=0.12.6 - - aws-c-compression=0.3.1 - - aws-c-event-stream=0.5.7 - - aws-c-http=0.10.7 - - aws-c-io=0.23.3 - - aws-c-mqtt=0.13.3 - - aws-c-s3=0.11.3 - - aws-c-sdkutils=0.2.4 - - aws-checksums=0.2.7 - - aws-crt-cpp=0.35.2 - - aws-sdk-cpp=1.11.606 - - azure-core-cpp=1.16.1 - - azure-identity-cpp=1.13.2 - - azure-storage-blobs-cpp=12.15.0 - - azure-storage-common-cpp=12.11.0 - - azure-storage-files-datalake-cpp=12.13.0 - - backports.zstd=1.3.0 - - blosc=1.21.6 - - bokeh=3.9.0 - - brotli=1.2.0 - - brotli-bin=1.2.0 - - brotli-python=1.2.0 - - brunsli=0.1 - - bzip2=1.0.8 - - c-ares=1.34.6 - - c-blosc2=2.23.1 - - ca-certificates=2026.2.25 - - cached-property=1.5.2 - - cached_property=1.5.2 - - cairo=1.18.4 - - capnproto=1.2.0 - - cartopy=0.25.0 - - cdsapi=0.7.7 - - certifi=2026.2.25 - - cffi=2.0.0 - - cfgv=3.5.0 - - cfitsio=4.6.3 - - charls=2.4.3 - - charset-normalizer=3.4.5 - - click=8.3.1 - - cloudpickle=3.1.2 - - configobj=5.0.9 - - contourpy=1.3.3 - - cvxopt=1.3.3 - - cycler=0.12.1 - - cyrus-sasl=2.1.28 - - cytoolz=1.1.0 - - dask=2026.3.0 - - dask-core=2026.3.0 - - dask-jobqueue=0.9.0 - - dav1d=1.2.1 - - distlib=0.4.0 - - distributed=2026.3.0 - - donfig=0.8.1.post1 - - dsdp=5.8 - - eccodes=2.46.0 - - ecmwf-datastores-client=0.5.1 - - fftw=3.3.10 - - filelock=3.25.2 - - fmt=12.0.0 - - font-ttf-dejavu-sans-mono=2.37 - - font-ttf-inconsolata=3.000 - - font-ttf-source-code-pro=2.038 - - font-ttf-ubuntu=0.83 - - fontconfig=2.17.1 - - fonts-conda-ecosystem=1 - - fonts-conda-forge=1 - - freeglut=3.2.2 - - freetype=2.14.2 - - freexl=2.0.0 - - fsspec=2026.3.0 - - gdal=3.10.3 - - geos=3.14.1 - - geotiff=1.7.4 - - gflags=2.2.2 - - giflib=5.2.2 - - glog=0.7.1 - - glpk=5.0 - - gmp=6.3.0 - - gsl=2.7 - - h2=4.3.0 - - h5py=3.15.1 - - hdf4=4.2.15 - - hdf5=1.14.6 - - hpack=4.1.0 - - hyperframe=6.1.0 - - icu=78.2 - - identify=2.6.18 - - idna=3.11 - - imagecodecs=2026.3.6 - - imageio=2.37.0 - - importlib-metadata=8.8.0 - - isce2=2.6.4 - - jasper=4.2.9 - - jinja2=3.1.6 - - joblib=1.5.3 - - json-c=0.18 - - jxrlib=1.1 - - kealib=1.6.2 - - keyutils=1.6.3 - - kiwisolver=1.5.0 - - krb5=1.22.2 - - lazy-loader=0.5 - - lcms2=2.18 - - ld_impl_linux-64=2.45.1 - - lerc=4.1.0 - - libabseil=20250512.1 - - libacl=2.3.2 - - libaec=1.1.5 - - libamd=3.3.3 - - libarchive=3.8.5 - - libarrow=22.0.0 - - libarrow-acero=22.0.0 - - libarrow-compute=22.0.0 - - libarrow-dataset=22.0.0 - - libarrow-substrait=22.0.0 - - libattr=2.5.2 - - libavif16=1.4.0 - - libblas=3.11.0 - - libbrotlicommon=1.2.0 - - libbrotlidec=1.2.0 - - libbrotlienc=1.2.0 - - libbtf=2.3.2 - - libcamd=3.3.3 - - libcblas=3.11.0 - - libccolamd=3.3.4 - - libcholmod=5.3.1 - - libcolamd=3.3.4 - - libcrc32c=1.1.2 - - libcurl=8.18.0 - - libcxsparse=4.4.1 - - libdeflate=1.25 - - libedit=3.1.20250104 - - libev=4.33 - - libevent=2.1.12 - - libexpat=2.7.4 - - libffi=3.5.2 - - libfreetype=2.14.2 - - libfreetype6=2.14.2 - - libgcc=15.2.0 - - libgcc-ng=15.2.0 - - libgdal=3.10.3 - - libgdal-core=3.10.3 - - libgdal-fits=3.10.3 - - libgdal-grib=3.10.3 - - libgdal-hdf4=3.10.3 - - libgdal-hdf5=3.10.3 - - libgdal-jp2openjpeg=3.10.3 - - libgdal-kea=3.10.3 - - libgdal-netcdf=3.10.3 - - libgdal-pdf=3.10.3 - - libgdal-pg=3.10.3 - - libgdal-postgisraster=3.10.3 - - libgdal-tiledb=3.10.3 - - libgdal-xls=3.10.3 - - libgfortran=15.2.0 - - libgfortran5=15.2.0 - - libgl=1.7.0 - - libglib=2.86.4 - - libglu=9.0.3 - - libglvnd=1.7.0 - - libglx=1.7.0 - - libgomp=15.2.0 - - libgoogle-cloud=2.39.0 - - libgoogle-cloud-storage=2.39.0 - - libgrpc=1.73.1 - - libhwy=1.3.0 - - libiconv=1.18 - - libjpeg-turbo=3.1.2 - - libjxl=0.11.2 - - libklu=2.3.5 - - libkml=1.3.0 - - liblapack=3.11.0 - - libldl=3.3.2 - - liblzma=5.8.2 - - libnetcdf=4.9.3 - - libnghttp2=1.67.0 - - libnsl=2.0.1 - - libntlm=1.8 - - libopenblas=0.3.30 - - libopengl=1.7.0 - - libopentelemetry-cpp=1.21.0 - - libopentelemetry-cpp-headers=1.21.0 - - libparquet=22.0.0 - - libparu=1.0.0 - - libpng=1.6.55 - - libpq=18.3 - - libprotobuf=6.31.1 - - librbio=4.3.4 - - libre2-11=2025.11.05 - - librttopo=1.1.0 - - libspatialite=5.1.0 - - libspex=3.2.3 - - libspqr=4.3.4 - - libsqlite=3.52.0 - - libssh2=1.11.1 - - libstdcxx=15.2.0 - - libstdcxx-ng=15.2.0 - - libsuitesparseconfig=7.10.1 - - libthrift=0.22.0 - - libtiff=4.7.1 - - libumfpack=6.3.5 - - liburing=2.14 - - libutf8proc=2.11.3 - - libuuid=2.41.3 - - libwebp-base=1.6.0 - - libxcb=1.17.0 - - libxcrypt=4.4.36 - - libxml2=2.15.2 - - libxml2-16=2.15.2 - - libxml2-devel=2.15.2 - - libxslt=1.1.43 - - libzip=1.11.2 - - libzlib=1.3.1 - - libzopfli=1.0.3 - - locket=1.0.0 - - lxml=6.0.2 - - lz4=4.4.5 - - lz4-c=1.10.0 - - lzo=2.10 - - markdown-it-py=4.0.0 - - markupsafe=3.0.3 - - matplotlib-base=3.10.8 - - mdurl=0.1.2 - - metis=5.1.0 - - minizip=4.0.10 - - mintpy=1.6.3 - - mpfr=4.2.2 - - msgpack-python=1.1.2 - - multiurl=0.3.7 - - munkres=1.1.4 - - narwhals=2.18.1 - - ncurses=6.5 - - networkx=3.6.1 - - nlohmann_json=3.12.0 - - nodeenv=1.10.0 - - nspr=4.38 - - nss=3.118 - - numpy=1.26.4 - - openjpeg=2.5.4 - - openjph=0.26.3 - - openldap=2.6.10 - - openmotif=2.3.8 - - openssl=3.6.1 - - orc=2.2.1 - - packaging=26.0 - - pandas=3.0.2 - - partd=1.4.2 - - pcre2=10.47 - - pillow=12.1.1 - - pip=26.0.1 - - pixman=0.46.4 - - platformdirs=4.9.4 - - poppler=25.07.0 - - poppler-data=0.4.12 - - postgresql=18.3 - - pre-commit=4.5.1 - - proj=9.7.1 - - prometheus-cpp=1.3.0 - - psutil=7.2.2 - - pthread-stubs=0.4 - - pyaps3=0.3.7 - - pyarrow=22.0.0 - - pyarrow-core=22.0.0 - - pycparser=2.22 - - pygments=2.20.0 - - pygrib=2.1.8 - - pykdtree=1.4.3 - - pykml=0.2.0 - - pyparsing=3.3.2 - - pyproj=3.7.2 - - pyresample=1.35.0 - - pyshp=3.0.3 - - pysocks=1.7.1 - - pysolid=0.3.4 - - python=3.11.15 - - python-dateutil=2.9.0.post0 - - python-discovery=1.2.1 - - python_abi=3.11 - - pytz=2026.1.post1 - - pyyaml=6.0.3 - - qhull=2020.2 - - rav1e=0.8.1 - - re2=2025.11.05 - - readline=8.3 - - requests=2.32.5 - - rich=14.3.3 - - s2n=1.6.2 - - scikit-image=0.26.0 - - scipy=1.17.1 - - setuptools=82.0.1 - - shapely=2.1.2 - - six=1.17.0 - - snappy=1.2.2 - - sortedcontainers=2.4.0 - - spdlog=1.16.0 - - sqlite=3.52.0 - - suitesparse=7.10.1 - - svt-av1=4.0.1 - - tblib=3.2.2 - - tifffile=2026.3.3 - - tiledb=2.29.2 - - tk=8.6.13 - - toolz=1.1.0 - - tornado=6.5.5 - - tqdm=4.67.3 - - typing_extensions=4.15.0 - - tzcode=2026a - - tzdata=2025c - - ukkonen=1.1.0 - - unicodedata2=17.0.1 - - uriparser=0.9.8 - - urllib3=2.6.3 - - utm=0.7.0 - - virtualenv=21.2.0 - - wheel=0.46.3 - - xerces-c=3.3.0 - - xorg-libice=1.1.2 - - xorg-libsm=1.2.6 - - xorg-libx11=1.8.13 - - xorg-libxau=1.0.12 - - xorg-libxdmcp=1.1.5 - - xorg-libxext=1.3.7 - - xorg-libxfixes=6.0.2 - - xorg-libxft=2.3.9 - - xorg-libxi=1.8.2 - - xorg-libxmu=1.3.1 - - xorg-libxp=1.0.4 - - xorg-libxrender=0.9.12 - - xorg-libxt=1.3.1 - - xyzservices=2026.3.0 - - yaml=0.2.5 - - zfp=1.0.1 - - zict=3.0.0 - - zipp=3.23.0 - - zlib=1.3.1 - - zlib-ng=2.3.3 - - zstd=1.5.7 - - pip: - - fonttools==4.62.1 -prefix: /home/administrator/miniconda3/envs/isce2_mintpy_v1 diff --git a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.runtime_versions.txt b/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.runtime_versions.txt deleted file mode 100644 index 292ab5c..0000000 --- a/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406/isce2_mintpy_v1.runtime_versions.txt +++ /dev/null @@ -1,14 +0,0 @@ -python_executable=/home/administrator/miniconda3/envs/isce2_mintpy_v1/bin/python -python_version=3.11.15 -isce_present=True -mintpy_present=True -h5py_present=True -isce_file=/home/administrator/miniconda3/envs/isce2_mintpy_v1/lib/python3.11/site-packages/isce/__init__.py -isce_version=2.6.3 -mintpy_file=/home/administrator/miniconda3/envs/isce2_mintpy_v1/lib/python3.11/site-packages/mintpy/__init__.py -mintpy_version=1.6.2 -2026-04-06 15:31:20,151 - h5py._conv - DEBUG - Creating converter from 7 to 5 -2026-04-06 15:31:20,152 - h5py._conv - DEBUG - Creating converter from 5 to 7 -2026-04-06 15:31:20,153 - h5py._conv - DEBUG - Creating converter from 7 to 5 -2026-04-06 15:31:20,153 - h5py._conv - DEBUG - Creating converter from 5 to 7 -h5py_version=3.15.1 diff --git a/experiments/isce2_sbas_timeseries/configs/phase2_bridge_smoketest_20260406_smallbaseline.cfg b/experiments/isce2_sbas_timeseries/configs/phase2_bridge_smoketest_20260406_smallbaseline.cfg deleted file mode 100644 index 0ff1682..0000000 --- a/experiments/isce2_sbas_timeseries/configs/phase2_bridge_smoketest_20260406_smallbaseline.cfg +++ /dev/null @@ -1,64 +0,0 @@ -# vim: set filetype=cfg: -## LT-1 stripmapStack -> MintPy SBAS smoke-test config -## Workspace: -## phase2_bridge_smoketest_20260406 -## Sample stack: -## LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1 -## dates: 20250118, 20250315, 20250510, 20250705, 20250830 -## Work dir suggestion: -## /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/mintpy_sbas_unified_phase2_20260407 - -########## computing resource configuration -mintpy.compute.cluster = none -mintpy.compute.numWorker = 4 -mintpy.compute.maxMemory = 8.0 - -########## 1. load_data -mintpy.load.processor = isce -mintpy.load.autoPath = no -mintpy.load.updateMode = yes -mintpy.load.compression = lzf -mintpy.load.metaFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/merged/SLC/20250510/referenceShelve/data.dat -mintpy.load.baselineDir = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/baselines -mintpy.load.unwFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/Igrams/*/filt*_snaphu.unw -mintpy.load.corFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/Igrams/*/filt_*.cor -mintpy.load.connCompFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/Igrams/*/filt*_snaphu.unw.conncomp -mintpy.load.intFile = None -mintpy.load.demFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/hgt.rdr -mintpy.load.lookupYFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/lat.rdr -mintpy.load.lookupXFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/lon.rdr -mintpy.load.incAngleFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/los.rdr -mintpy.load.azAngleFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/los.rdr -mintpy.load.shadowMaskFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/shadowMask.rdr -mintpy.load.waterMaskFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/geom_reference/waterMask.rdr - -########## 2. modify_network -mintpy.network.coherenceBased = no -mintpy.network.areaRatioBased = no - -########## 3. reference_point -mintpy.reference.yx = 1994,52 -mintpy.reference.maskFile = maskAllValid.h5 - -########## 4. correct_unwrap_error -mintpy.unwrapError.method = no - -########## 5. invert_network -mintpy.networkInversion.weightFunc = no -mintpy.networkInversion.maskDataset = no -mintpy.networkInversion.minRedundancy = 1.0 -mintpy.networkInversion.waterMaskFile = maskAllValid.h5 - -########## 6-10. optional corrections disabled for the first unified-env replay -mintpy.solidEarthTides = no -mintpy.ionosphericDelay.method = no -mintpy.troposphericDelay.method = no -mintpy.deramp = no -mintpy.topographicResidual = no - -########## 11-13. outputs -mintpy.reference.date = 20250510 -mintpy.geocode = no -mintpy.save.kmz = no -mintpy.save.hdfEos5 = no -mintpy.plot = no diff --git a/experiments/isce2_sbas_timeseries/configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json b/experiments/isce2_sbas_timeseries/configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json deleted file mode 100644 index 4262721..0000000 --- a/experiments/isce2_sbas_timeseries/configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "schema_version": "psinsar.sbas.v1", - "catalog_name": "psinsar", - "mode": "sbas", - "engine_code": "isce2", - "processor_code": "isce2_stack_mintpy", - "sample_group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "reference_date": "20250510", - "reference_point_yx": [ - 1994, - 52 - ], - "stack_dates": [ - "20250118", - "20250315", - "20250510", - "20250705", - "20250830" - ], - "network_pair_count": 10, - "mintpy_work_dir_wsl": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_v5", - "publish_dir_wsl": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_v5", - "quality_summary": { - "strict_valid_pixels": 1219001, - "strict_valid_pixel_ratio": 0.2991, - "mask_temp_coh_pixels": 62987, - "mask_temp_coh_threshold": 0.7 - }, - "artifacts": [ - { - "product_type": "timeseries_cube", - "role": "primary", - "dataset": "timeseries", - "publish_relpath": "assets/geo_timeseries.h5", - "source_relpath": "assets/geo_timeseries.h5" - }, - { - "product_type": "velocity_map", - "role": "primary", - "dataset": "velocity", - "publish_relpath": "assets/geo_velocity.h5", - "source_relpath": "assets/geo_velocity.h5" - }, - { - "product_type": "velocity_geotiff", - "role": "primary", - "dataset": "velocity", - "publish_relpath": "assets/velocity.tif", - "source_relpath": "assets/velocity.tif" - }, - { - "product_type": "temporal_coherence", - "role": "quality", - "dataset": "temporalCoherence", - "publish_relpath": "assets/geo_temporalCoherence.h5", - "source_relpath": "assets/geo_temporalCoherence.h5" - }, - { - "product_type": "temporal_coherence_geotiff", - "role": "quality", - "dataset": "temporalCoherence", - "publish_relpath": "assets/temporalCoherence.tif", - "source_relpath": "assets/temporalCoherence.tif" - }, - { - "product_type": "quality_mask", - "role": "quality", - "dataset": "mask", - "publish_relpath": "assets/geo_maskTempCoh.h5", - "source_relpath": "assets/geo_maskTempCoh.h5" - }, - { - "product_type": "quality_mask_geotiff", - "role": "quality", - "dataset": "mask", - "publish_relpath": "assets/maskTempCoh.tif", - "source_relpath": "assets/maskTempCoh.tif" - }, - { - "product_type": "ifgram_network", - "role": "diagnostic", - "dataset": "mask", - "publish_relpath": "runtime/numTriNonzeroIntAmbiguity.h5", - "source_relpath": "numTriNonzeroIntAmbiguity.h5" - }, - { - "product_type": "preview_png", - "role": "primary", - "publish_relpath": "preview/velocity_preview.png", - "source_relpath": "preview/velocity_preview.png" - }, - { - "product_type": "diagnostic_png", - "role": "diagnostic", - "publish_relpath": "preview/numTriNonzeroIntAmbiguity.png", - "source_relpath": "preview/numTriNonzeroIntAmbiguity.png" - } - ], - "retained_runtime_artifacts": [ - { - "purpose": "strict_all_ifgram_mask", - "source_relpath": "maskAllValid.h5" - }, - { - "purpose": "average_spatial_coherence", - "source_relpath": "avgSpatialCoh.h5" - }, - { - "purpose": "config_backup", - "source_relpath": "smallbaselineApp.cfg" - } - ], - "notes": [ - "This is a sample experiment manifest for system-integration design only.", - "Current experiment includes geocoded HDF5 outputs, GeoTIFF exports, and a publish-style manifest bundle." - ] -} diff --git a/experiments/isce2_sbas_timeseries/configs/sample_smallbaseline_lt1_e123p3_n46p1.cfg b/experiments/isce2_sbas_timeseries/configs/sample_smallbaseline_lt1_e123p3_n46p1.cfg deleted file mode 100644 index 7d52888..0000000 --- a/experiments/isce2_sbas_timeseries/configs/sample_smallbaseline_lt1_e123p3_n46p1.cfg +++ /dev/null @@ -1,62 +0,0 @@ -# vim: set filetype=cfg: -## LT-1 stripmapStack -> MintPy SBAS smoke-test config -## Sample stack: -## LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1 -## dates: 20250118, 20250315, 20250510, 20250705, 20250830 -## Work dir suggestion: -## /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas - -########## computing resource configuration -mintpy.compute.cluster = none -mintpy.compute.numWorker = 4 -mintpy.compute.maxMemory = 8.0 - -########## 1. load_data -mintpy.load.processor = isce -mintpy.load.autoPath = no -mintpy.load.updateMode = yes -mintpy.load.compression = lzf -mintpy.load.metaFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/merged/SLC/20250510/referenceShelve/data.dat -mintpy.load.baselineDir = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/baselines -mintpy.load.unwFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/Igrams/*/filt*_snaphu.unw -mintpy.load.corFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/Igrams/*/filt_*.cor -mintpy.load.connCompFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/Igrams/*/filt*_snaphu.unw.conncomp -mintpy.load.intFile = None -mintpy.load.demFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/hgt.rdr -mintpy.load.lookupYFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/lat.rdr -mintpy.load.lookupXFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/lon.rdr -mintpy.load.incAngleFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/los.rdr -mintpy.load.azAngleFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/los.rdr -mintpy.load.shadowMaskFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/shadowMask.rdr -mintpy.load.waterMaskFile = /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference/waterMask.rdr - -########## 2. modify_network -mintpy.network.coherenceBased = no -mintpy.network.areaRatioBased = no - -########## 3. reference_point -mintpy.reference.yx = 1994,52 -mintpy.reference.maskFile = maskAllValid.h5 - -########## 4. correct_unwrap_error -mintpy.unwrapError.method = no - -########## 5. invert_network -mintpy.networkInversion.weightFunc = no -mintpy.networkInversion.maskDataset = no -mintpy.networkInversion.minRedundancy = 1.0 -mintpy.networkInversion.waterMaskFile = maskAllValid.h5 - -########## 6-10. optional corrections disabled for first offline LT-1 smoke test -mintpy.solidEarthTides = no -mintpy.ionosphericDelay.method = no -mintpy.troposphericDelay.method = no -mintpy.deramp = no -mintpy.topographicResidual = no - -########## 11-13. outputs -mintpy.reference.date = 20250510 -mintpy.geocode = no -mintpy.save.kmz = no -mintpy.save.hdfEos5 = no -mintpy.plot = no diff --git a/experiments/isce2_sbas_timeseries/configs/sample_stack_e123p3_n46p1.json b/experiments/isce2_sbas_timeseries/configs/sample_stack_e123p3_n46p1.json deleted file mode 100644 index 21af4ab..0000000 --- a/experiments/isce2_sbas_timeseries/configs/sample_stack_e123p3_n46p1.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "source_root_windows": "F:\\Insar_data_pool_1", - "source_root_wsl": "/mnt/f/Insar_data_pool_1", - "group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "tile_key": "E123.3_N46.1", - "scene_count": 5, - "reference_strategy": "middle_by_date", - "reference_date": "20250510", - "stack_group": { - "satellite": "LT1A", - "imaging_mode": "STRIP1", - "polarization": "HH", - "orbit_direction": "DESCENDING", - "receiving_stations": [ - "KSC", - "SYC" - ] - }, - "proposed_scratch_windows": "Z:\\Code\\Insar_management_system_v2\\experiments\\isce2_sbas_timeseries\\scratch\\lt1a_strip1_hh_descending_e123p3_n46p1", - "proposed_scratch_wsl": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1", - "proposed_layout": { - "stack_input_manifest": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_input_manifest.json", - "slc_dir": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/SLC", - "orbits_dir": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/orbits", - "logs_dir": "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/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": [ - { - "folder_name": "LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780", - "folder_path": "F:\\Insar_data_pool_1\\LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780", - "folder_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780", - "tiff_path": "F:\\Insar_data_pool_1\\LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780\\LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780.tiff", - "tiff_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780/LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780.tiff", - "meta_path": "F:\\Insar_data_pool_1\\LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780\\LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780.meta.xml", - "meta_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780/LT1A_MONO_SYC_STRIP1_016197_E123.3_N46.1_20250118_SLC_HH_S2A_0000623780.meta.xml", - "file_size_bytes": 1632365702, - "satellite": "LT1A", - "imaging_date": "20250118", - "imaging_mode": "STRIP1", - "polarization": "HH", - "orbit_direction": "DESCENDING", - "satellite_mode": "MONOSTATIC", - "receiving_station": "SYC", - "orbit_circle": "16197", - "scene_center_lon": 123.3290291621, - "scene_center_lat": 46.0963941396, - "acquisition_time_utc": "2025-01-18T22:5:18.778600", - "product_type": "COMPLEX", - "product_level": "LEVEL2A", - "product_unique_id": "0000623780", - "tile_key": "E123.3_N46.1", - "group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "orbit_txt_expected_name": "LT1A_GpsData_GAS_C_20250118.txt" - }, - { - "folder_name": "LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238", - "folder_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238", - "folder_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238", - "tiff_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238\\LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238.tiff", - "tiff_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238/LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238.tiff", - "meta_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238\\LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238.meta.xml", - "meta_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238/LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238.meta.xml", - "file_size_bytes": 1630628342, - "satellite": "LT1A", - "imaging_date": "20250315", - "imaging_mode": "STRIP1", - "polarization": "HH", - "orbit_direction": "DESCENDING", - "satellite_mode": "MONOSTATIC", - "receiving_station": "KSC", - "orbit_circle": "17030", - "scene_center_lon": 123.3320867599, - "scene_center_lat": 46.1149596404, - "acquisition_time_utc": "2025-03-15T22:5:19.382029", - "product_type": "COMPLEX", - "product_level": "LEVEL2A", - "product_unique_id": "0000678238", - "tile_key": "E123.3_N46.1", - "group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "orbit_txt_expected_name": "LT1A_GpsData_GAS_C_20250315.txt" - }, - { - "folder_name": "LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820", - "folder_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820", - "folder_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820", - "tiff_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820\\LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820.tiff", - "tiff_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820/LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820.tiff", - "meta_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820\\LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820.meta.xml", - "meta_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820/LT1A_MONO_KSC_STRIP1_017863_E123.3_N46.1_20250510_SLC_HH_S2A_0000738820.meta.xml", - "file_size_bytes": 1631554934, - "satellite": "LT1A", - "imaging_date": "20250510", - "imaging_mode": "STRIP1", - "polarization": "HH", - "orbit_direction": "DESCENDING", - "satellite_mode": "MONOSTATIC", - "receiving_station": "KSC", - "orbit_circle": "17863", - "scene_center_lon": 123.3311389524, - "scene_center_lat": 46.1155150436, - "acquisition_time_utc": "2025-05-10T22:5:20.466535", - "product_type": "COMPLEX", - "product_level": "LEVEL2A", - "product_unique_id": "0000738820", - "tile_key": "E123.3_N46.1", - "group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "orbit_txt_expected_name": "LT1A_GpsData_GAS_C_20250510.txt" - }, - { - "folder_name": "LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680", - "folder_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680", - "folder_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680", - "tiff_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680\\LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680.tiff", - "tiff_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680/LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680.tiff", - "meta_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680\\LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680.meta.xml", - "meta_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680/LT1A_MONO_KSC_STRIP1_018697_E123.3_N46.1_20250705_SLC_HH_S2A_0000796680.meta.xml", - "file_size_bytes": 1633523942, - "satellite": "LT1A", - "imaging_date": "20250705", - "imaging_mode": "STRIP1", - "polarization": "HH", - "orbit_direction": "DESCENDING", - "satellite_mode": "MONOSTATIC", - "receiving_station": "KSC", - "orbit_circle": "18697", - "scene_center_lon": 123.3345082568, - "scene_center_lat": 46.1000329747, - "acquisition_time_utc": "2025-07-05T22:5:20.665803", - "product_type": "COMPLEX", - "product_level": "LEVEL2A", - "product_unique_id": "0000796680", - "tile_key": "E123.3_N46.1", - "group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "orbit_txt_expected_name": "LT1A_GpsData_GAS_C_20250705.txt" - }, - { - "folder_name": "LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029", - "folder_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029", - "folder_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029", - "tiff_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029\\LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029.tiff", - "tiff_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029/LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029.tiff", - "meta_path": "F:\\Insar_data_pool_1\\LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029\\LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029.meta.xml", - "meta_path_wsl": "/mnt/f/Insar_data_pool_1/LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029/LT1A_MONO_KSC_STRIP1_019530_E123.3_N46.1_20250830_SLC_HH_S2A_0000857029.meta.xml", - "file_size_bytes": 1629875486, - "satellite": "LT1A", - "imaging_date": "20250830", - "imaging_mode": "STRIP1", - "polarization": "HH", - "orbit_direction": "DESCENDING", - "satellite_mode": "MONOSTATIC", - "receiving_station": "KSC", - "orbit_circle": "19530", - "scene_center_lon": 123.3286377886, - "scene_center_lat": 46.1152894101, - "acquisition_time_utc": "2025-08-30T22:5:26.558015", - "product_type": "COMPLEX", - "product_level": "LEVEL2A", - "product_unique_id": "0000857029", - "tile_key": "E123.3_N46.1", - "group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1", - "orbit_txt_expected_name": "LT1A_GpsData_GAS_C_20250830.txt" - } - ] -} \ No newline at end of file diff --git a/experiments/isce2_sbas_timeseries/notes/.gitkeep b/experiments/isce2_sbas_timeseries/notes/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/experiments/isce2_sbas_timeseries/notes/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE0_ENV_CHECK.md b/experiments/isce2_sbas_timeseries/notes/PHASE0_ENV_CHECK.md deleted file mode 100644 index a8d274d..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE0_ENV_CHECK.md +++ /dev/null @@ -1,62 +0,0 @@ -# Phase 0 Environment Check - -Updated: 2026-04-03 - -## Confirmed - -- Project workspace: - - Windows repo root: `Z:\Code\Insar_management_system_v2` - - WSL mount path: `/mnt/z/Code/Insar_management_system_v2` -- Windows project Python env: - - `C:\Users\Administrator\.conda\envs\InSAR` -- WSL experiment distro: - - `Ubuntu-24.04` -- WSL project access: - - `/mnt/z/Code/Insar_management_system_v2` - - `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries` -- WSL system Python: - - `Python 3.12.3` -- WSL conda root: - - `/home/administrator/miniconda3` -- WSL conda envs found: - - `base` - - `isce2` -- ISCE2 env package: - - `isce2 2.6.4` -- ISCE2 Python import path: - - `/home/administrator/miniconda3/envs/isce2/lib/python3.11/site-packages/isce/__init__.py` -- Lutan1 sensor module: - - `/home/administrator/miniconda3/envs/isce2/lib/python3.11/site-packages/isce/components/isceobj/Sensor/Lutan1.py` -- Official stack directories present: - - `/home/administrator/miniconda3/envs/isce2/share/isce2/stripmapStack` - - `/home/administrator/miniconda3/envs/isce2/share/isce2/topsStack` - -## Confirmed Gaps - -- `conda` is not currently on the default shell `PATH` inside `Ubuntu-24.04`. - - Use `/home/administrator/miniconda3/bin/conda` directly in scripts. -- `MintPy` is not installed in the `isce2` env yet. - - `conda list -n isce2 mintpy` returned no match. -- Calling `conda list -n isce2 ...` from a WSL bash script triggered a segmentation fault once. - - For experiment scripts, prefer `conda run -n isce2 python ...` checks over `conda list`. - -## Implication - -Phase 0 can start immediately for: - -- LT-1 / ISCE2 stack compatibility checks -- workspace and path validation -- command-chain drafting - -But the full SBAS chain cannot run end to end until one of these is true: - -- `MintPy` is installed into `isce2`, or -- a separate WSL env with `MintPy` is prepared - -## Next Checks - -1. Verify ISCE2 stack scripts actually exist in `Ubuntu-24.04`. -2. Read `stripmapStack/README.md` and `stackStripMap.py` to identify required stack inputs. -3. Decide whether `MintPy` should share the `isce2` env or live in a separate env. -4. Verify LT-1 / LUTAN1 support is usable for stack-mode inputs, not only single-pair mode. -5. Draft a minimal stack experiment command chain under this folder. diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE0_REPO_READER_FINDINGS.md b/experiments/isce2_sbas_timeseries/notes/PHASE0_REPO_READER_FINDINGS.md deleted file mode 100644 index 5b8f942..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE0_REPO_READER_FINDINGS.md +++ /dev/null @@ -1,123 +0,0 @@ -# Phase 0 Repo Reader Findings - -Updated: 2026-04-03 - -## Existing repo reader path - -The current repository already has a stable LT-1 single-scene metadata ingestion path. - -Main code path: - -- `backend/app/services/data_service.py` - - `scan_radar_data()` -- `backend/app/utils.py` - - `parse_lt1_radar_filename()` - - `find_xml_file()` - - `parse_xml_metadata()` - -## What the existing reader does - -### 1. Folder-name parsing - -`parse_lt1_radar_filename()` extracts from the directory name: - -- `satellite` -- `satellite_mode` -- `receiving_station` -- `imaging_mode` -- `orbit_circle` -- `scene_center_lon` -- `scene_center_lat` -- `imaging_date` -- `acquisition_time_utc` -- `product_type` -- `polarization` -- `product_level` -- `product_unique_id` - -Example supported name: - -```text -LT1B_MONO_SYC_STRIP1_018153_E135.4_N48.3_20250701_SLC_HH_S2A_0000790171 -``` - -### 2. XML discovery - -`find_xml_file()` prefers: - -- `*.meta.xml` - -and falls back to: - -- the only XML file in the directory, if there is just one - -### 3. XML parsing - -`parse_xml_metadata()` extracts: - -- `orbit_direction` -- `imaging_mode` -- `polarization` -- `receiving_station` -- `satellite_mode` -- `orbit_circle` from `absOrbit` -- `scene_center_lon` -- `scene_center_lat` -- `acquisition_time_utc` -- `product_type` -- `product_level` -- `product_unique_id` -- `look_direction` -- corner coordinates and coverage polygon - -### 4. Merge rule - -`scan_radar_data()` merges: - -- folder-name metadata -- XML metadata - -with XML preferred for most fields, except `product_unique_id` where the folder-name value is preserved if present. - -## Why this matters for SBAS experiments - -This means the SBAS experiment should not invent a separate metadata interpretation unless absolutely necessary. - -Recommended rule: - -- reuse the same field semantics already used by `RadarDataORM` -- reuse the same `.meta.xml` discovery logic -- treat `scan_radar_data()` output as the canonical single-scene asset layer - -## Data layout check against `F:\Insar_data_pool_1` - -Sample scene directories under `F:\Insar_data_pool_1` are compatible with the current single-scene reader: - -- one folder per scene -- directory name matches LT-1 parser expectations -- contains `*.meta.xml` -- contains `*.tiff` -- contains preview and auxiliary files - -Example sample directory: - -```text -F:\Insar_data_pool_1\LT1A_MONO_KSC_STRIP1_017030_E123.3_N46.1_20250315_SLC_HH_S2A_0000678238 -``` - -Example files inside: - -- `...meta.xml` -- `...tiff` -- `...rpc` -- `...browse.jpg` -- `...thumb.jpg` - -## Practical implication - -For phase 1 design and experiments: - -- the current repo already knows how to ingest these LT-1 scene folders as `RadarData` -- stack preparation should build on top of this asset layer -- the real unknown is not scene metadata parsing -- the real unknown is how to transform these scene folders into a stack layout acceptable to `stripmapStack` diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE0_SAMPLE_STACK_SELECTION.md b/experiments/isce2_sbas_timeseries/notes/PHASE0_SAMPLE_STACK_SELECTION.md deleted file mode 100644 index be75532..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE0_SAMPLE_STACK_SELECTION.md +++ /dev/null @@ -1,92 +0,0 @@ -# Phase 0 Sample Stack Selection - -Updated: 2026-04-03 - -## Selected baseline sample - -Current baseline sample stack: - -- group key: - - `LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1` -- manifest: - - `experiments/isce2_sbas_timeseries/configs/sample_stack_e123p3_n46p1.json` - -## Sample summary - -- satellite: - - `LT1A` -- mode: - - `STRIP1` -- polarization: - - `HH` -- orbit direction: - - `DESCENDING` -- scene count: - - `5` -- dates: - - `20250118` - - `20250315` - - `20250510` - - `20250705` - - `20250830` -- recommended reference date: - - `20250510` -- receiving stations observed: - - `SYC` - - `KSC` - -## Why this sample is useful - -- It already satisfies a minimal SBAS smoke-test stack size. -- All scenes share the same: - - satellite - - imaging mode - - polarization - - orbit direction - - tile key -- The dates are evenly spaced enough to act as a first time-series experiment set. - -## Important adjacent-tile signal - -This sample is not isolated. - -The same date sequence also appears in multiple neighboring descending tiles, including: - -- `E123.5_N46.6` -- `E123.6_N47.0` -- `E123.8_N47.5` -- `E123.9_N48.0` -- `E124.5_N49.9` -- `E124.7_N50.3` -- `E124.8_N50.8` -- `E125.0_N51.3` -- `E125.1_N51.8` -- `E125.3_N52.2` -- `E125.5_N52.7` - -These neighboring tiles share the same 5 acquisition dates: - -- `20250118` -- `20250315` -- `20250510` -- `20250705` -- `20250830` - -## Implication - -This strongly suggests the data pool contains a larger repeated strip-family, not just isolated scenes. - -Recommended experiment order: - -1. Start with one tile-level stack smoke test using `E123.3_N46.1`. -2. If stack-prep works, expand to multiple adjacent tiles with the same date family. -3. Only after that, test a wider strip or mosaic strategy. - -## Current risk judgment - -The main remaining uncertainty is still not scene selection. - -The main uncertainty is: - -- how to convert LT-1 per-scene `tiff + meta.xml` folders -- into a stack layout and sensor input form acceptable to the ISCE2 stripmap stack workflow diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE0_STACK_FINDINGS.md b/experiments/isce2_sbas_timeseries/notes/PHASE0_STACK_FINDINGS.md deleted file mode 100644 index 923e0ba..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE0_STACK_FINDINGS.md +++ /dev/null @@ -1,140 +0,0 @@ -# Phase 0 Stack Findings - -Updated: 2026-04-03 - -## Confirmed - -- Official stack tooling exists in the `isce2` env: - - `/home/administrator/miniconda3/envs/isce2/share/isce2/stripmapStack` - - `/home/administrator/miniconda3/envs/isce2/share/isce2/topsStack` -- `stackStripMap.py` exists and is the stripmap stack entry point. -- `prepStripmap4timeseries.py` exists in the official `stripmapStack` toolset. -- `stackStripMap.py` expects: - - an SLC root directory via `-s/--slc_directory` - - a DEM via `-d/--dem` - - an optional reference date via `-m/--reference_date` - - temporal and baseline thresholds -- The script scans date subdirectories under the SLC root. -- Default behavior looks for `.raw` inside each acquisition directory. -- With `--nofocus`, it instead looks for `.slc`. -- Deeper code inspection confirms: - - `topo.py` opens `/data` - - `geo2rdr.py` opens each secondary `/data` - - `refineSecondaryTiming` uses both `.slc` and the acquisition directory as metadata roots - -## Important implication - -Official stack processing expects a stack-style input layout such as: - -```text -SLC/ - YYYYMMDD/ - YYYYMMDD.raw -``` - -or, when data are already focused: - -```text -SLC/ - YYYYMMDD/ - YYYYMMDD.slc - YYYYMMDD.slc.xml - data -``` - -This is different from the current repository's custom LT-1 single-pair production flow. - -## Time-series bridge signal - -- `prepStripmap4timeseries.py` takes: - - pair/interferogram directories - - baseline directory - - geometry directory - - shelve metadata directory -- The script writes `.rsc` sidecars and explicitly references `pysar`-style downstream usage. - -This is useful because it confirms the official stripmap stack toolset already contains a bridge from stack outputs toward time-series preparation. -The weak point is still the LT-1 stack input/preparation stage, not the existence of a downstream time-series bridge. - -## Existing repo bridge signal - -The repository's current LT-1 single-pair pipeline already proves one important thing: - -- `stripmapApp.py` can be driven with: - - `sensor name = LUTAN1` - - direct `tiff` input path - - direct `orbitFile` XML path - -See: - -- `backend/app/isce2_pipeline/run_lt1_dinsar_pipeline.py` - -The generated XML writes: - -- `Reference -> tiff` -- `Reference -> orbitFile` -- `Secondary -> tiff` -- `Secondary -> orbitFile` - -This suggests a promising adapter direction: - -- do not try to pretend LT-1 is ALOS or another officially prepared raw sensor -- instead, explore generating LT-1-aware stack configs directly from: - - scene `tiff` - - scene `meta.xml` - - converted orbit XML - -That does not prove the official stack driver will accept this without modification. -But it is the strongest current indication for how a custom LT-1 `stack-prep` layer should be shaped. - -## LT-1 / LUTAN1 signal so far - -- ISCE core does include a `Lutan1.py` sensor module. -- But no direct `lutan` match was found in the `stripmapStack` helper scripts. -- The `stripmapStack` README examples and preparation hints mention: - - `prepRawALOS.py` - - `prepRawSensor.py` -- The README explicitly states automatic raw-data preparation support is currently oriented to: - - ALOS - - CSK -- `prepRawSensors.py` automatic raw detection currently covers: - - Envisat - - ERS CEOS - - ERS ENV - - ALOS1 - - CSK -- `prepSlcSensors.py` automatic SLC detection currently covers: - - Envisat - - ALOS1 - - CSK - - RSAT2 - - TSX/TDX -- No LT-1 or LUTAN1 hook was found in these official stack preparation scripts. - -## Interim conclusion - -Current evidence suggests: - -- ISCE2 core can parse LT-1/LUTAN1 at the sensor level. -- Official `stripmapStack` tooling is present. -- But the official stack preparation helpers do not currently advertise LT-1/LUTAN1 support. -- There is no direct evidence yet that LT-1 can be fed into the official stack helpers without an adapter step. -- `--nofocus` does not remove the need for acquisition metadata preparation. - - It still needs a per-date `data` shelve and an ISCE-style `.slc` image. - -This means the working assumption should be: - -- `LT-1 stack via official stripmapStack` is possible but unproven -- an LT-1-specific stack preparation or conversion layer is required unless an existing hidden tool can materialize `data` + `.slc` directly for LUTAN1 - -## Next checks - -1. Implement a dry-run LT-1 stack-prep workspace generator against the selected sample manifest. -2. Design a scene materializer that transforms one LT-1 scene into: - - `YYYYMMDD.slc` - - `YYYYMMDD.slc.xml` - - `data` -3. Decide whether that materializer should: - - call ISCE/LUTAN1 directly, or - - reuse parts of the existing pair pipeline -4. After materialization is proven, run `stackStripMap.py --nofocus` on one tile-level stack diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE1_STACK_GENERATION_SMOKETEST.md b/experiments/isce2_sbas_timeseries/notes/PHASE1_STACK_GENERATION_SMOKETEST.md deleted file mode 100644 index ffeed27..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE1_STACK_GENERATION_SMOKETEST.md +++ /dev/null @@ -1,285 +0,0 @@ -# Phase 1 Stack Generation Smoke Test - -Updated: 2026-04-05 - -Follow-up note: - -- MintPy SBAS continuation is now recorded separately in: - - `notes/PHASE2_MINTPY_SBAS_SMOKETEST.md` - -## Goal - -Validate that one LT-1 stack can be transformed from: - -- per-scene `tiff + meta.xml + orbit.xml` - -into: - -- `stripmapStack --nofocus` compatible acquisition directories -- a generated stripmap stack work plan - -without changing production code yet. - -## Sample - -- group key: - - `LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1` -- dates: - - `20250118` - - `20250315` - - `20250510` - - `20250705` - - `20250830` -- reference date: - - `20250510` - -## Commands Used - -1. Build dry-run stack workspace: - -```text -C:\Users\Administrator\.conda\envs\InSAR\python.exe experiments\isce2_sbas_timeseries\scripts\build_lt1_stack_prep.py --manifest-path experiments\isce2_sbas_timeseries\configs\sample_stack_e123p3_n46p1.json -``` - -2. Materialize LT-1 acquisitions inside `Ubuntu-24.04`: - -```text -wsl -d Ubuntu-24.04 /home/administrator/miniconda3/bin/conda run -n isce2 python /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py --stack-manifest /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_input_manifest.json -``` - -3. Run generated wrapper: - -```text -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/run_stripmap_stack_dryrun.sh -``` - -4. Execute the frozen stack step chain inside `Ubuntu-24.04`: - -```text -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_01_reference -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_02_focus_split -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_03_geo2rdr_coarseResamp -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_04_refineSecondaryTiming -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_05_invertMisreg -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_06_fineResamp -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_07_grid_baseline -``` - -5. Regenerate the stack in `interferogram` workflow mode and execute the new pair-processing stage: - -```text -C:\Users\Administrator\.conda\envs\InSAR\python.exe experiments\isce2_sbas_timeseries\scripts\build_lt1_stack_prep.py --manifest-path experiments\isce2_sbas_timeseries\configs\sample_stack_e123p3_n46p1.json --workflow interferogram -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/run_stripmap_stack_dryrun.sh -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1 run_08_igram -``` - -6. Prepare MintPy metadata in the dedicated `mintpy` env while bridging the working `isce2` Python package: - -```text -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/install_mintpy_runtime_ubuntu2404.sh -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_mintpy_with_isce_ubuntu2404.sh prep_isce.py -f "/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/Igrams/*/filt_*.unw" -m /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/Igrams/20250118_20250315/referenceShelve/data.dat -b /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/baselines -g /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/geom_reference -``` - -## Confirmed Results - -- All 5 acquisitions were materialized into: - - `scratch/lt1a_strip1_hh_descending_e123p3_n46p1/SLC/YYYYMMDD/` -- Each acquisition directory now contains: - - `YYYYMMDD.slc` - - `YYYYMMDD.slc.xml` - - `YYYYMMDD.slc.vrt` - - `data.dat/.dir/.bak` -- Example materialized output: - - `20250510.slc` - - size: `3262658784` bytes -- `stackStripMap.py --nofocus` ran successfully far enough to: - - discover all 5 acquisitions - - estimate stack baselines - - select interferometric pairs - - generate stack config files - - generate run files - -## Baseline Snapshot - -Relative to reference date `20250510`, the generated stack reported: - -- `20250118` - - `-199.6873591838194` -- `20250315` - - `-89.0330513325973` -- `20250705` - - `-561.9056433404087` -- `20250830` - - `215.98283570831154` - -The generated network reported: - -- minimum connection degree: - - `4.0` -- number of pairs: - - `10` - -## Generated Stack Work Products - -Under `scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/`: - -- `baselines/` -- `configs/` -- `run_files/` -- `pairs.pdf` - -Generated run files: - -- `run_01_reference` -- `run_02_focus_split` -- `run_03_geo2rdr_coarseResamp` -- `run_04_refineSecondaryTiming` -- `run_05_invertMisreg` -- `run_06_fineResamp` -- `run_07_grid_baseline` - -## Important Runtime Fixes - -- `matplotlib` was missing from the `isce2` env. - - fixed by installing it with `pip` inside the WSL `isce2` environment -- `stackStripMap.py` could not import `stripmapStack.Stack` by default. - - fixed by exporting: - - `PYTHONPATH=/home/administrator/miniconda3/envs/isce2/share/isce2` - - `PATH=/home/administrator/miniconda3/envs/isce2/share/isce2/stripmapStack:$PATH` -- the generated run files now include these prefixes automatically -- `run_01_reference` reached `topo` successfully but `createWaterMask.py` failed without Earthdata credentials. - - root cause: - - `SWBD` download requires `~/.netrc` for `urs.earthdata.nasa.gov` - - current judgment: - - local DEM is already sufficient for this experiment - - the only missing optional online input is the water-body mask download - - experimental fallback: - - `scripts/run_generated_stack_runfile_ubuntu2404.sh` now auto-generates a synthetic all-land `geom_reference/waterMask.rdr` - - the helper script is `scripts/create_synthetic_watermask.py` - - limitation: - - this fallback preserves stack execution but does not provide a true coastline mask -- LT-1 input preparation now has a shared helper: - - `backend/app/isce2_pipeline/lt1_input_resolver.py` - - purpose: - - centralize DEM resolution - - centralize orbit-pool resolution - - centralize LT-1 precise-orbit XML reuse or generation - - compatibility rule: - - this is a refactor of shared input-prep logic - - the original D-InSAR execution path was not removed - - `run_lt1_dinsar_pipeline.py` still keeps its original public workflow entry and now calls the helper internally - -## Latest Execution Status - -- `run_01_reference` - - `topo` completed successfully in `Ubuntu-24.04` - - local `geom_reference/waterMask.rdr` was synthesized from `shadowMask.rdr` -- `run_02_focus_split` - - completed successfully - - generated configs were effectively no-op under the current `--nofocus` contract -- `run_03_geo2rdr_coarseResamp` - - completed successfully in `Ubuntu-24.04` - - generated `offsets//range.off` and `azimuth.off` for: - - `20250118` - - `20250315` - - `20250705` - - `20250830` - - generated `coregSLC/Coarse//YYYYMMDD.slc` products for: - - `20250118` - - `20250315` - - `20250705` - - `20250830` - - runtime observation: - - this stage is long-running and mostly silent in the log file - - progress is easier to confirm from product directories than from stdout -- `run_04_refineSecondaryTiming` - - completed successfully in `Ubuntu-24.04` - - generated pair-level `refineSecondaryTiming/pairs//misreg.*` for all 10 pairs - - log observation: - - `Bad match at level 1` and `correlation error` appeared in the log - - despite that noise, the stage exited `0` and downstream inversion succeeded -- `run_05_invertMisreg` - - completed successfully in `Ubuntu-24.04` - - generated date-level `refineSecondaryTiming/dates//misreg.*` for: - - `20250118` - - `20250315` - - `20250510` - - `20250705` - - `20250830` - - inversion observation: - - design matrix was reported as full rank - - RMSE in azimuth was `0.002341399255443996` pixels - - RMSE in range was `0.0027480408593210303` pixels -- `run_06_fineResamp` - - completed successfully in `Ubuntu-24.04` - - generated fine coregistered `merged/SLC//YYYYMMDD.slc` for all 5 dates - - each merged date directory now also includes: - - `referenceShelve/` - - `secondaryShelve/` -- `run_07_grid_baseline` - - completed successfully in `Ubuntu-24.04` - - generated `merged/baselines//` baseline grids for all 5 dates - - each date-level baseline directory now includes: - - raw baseline raster - - `.xml` - - `.vrt` - - `.full.vrt` -- `build_lt1_stack_prep.py --workflow interferogram` - - now regenerates the official stripmapStack command in `interferogram` mode instead of hard-coding `slc` - - regenerated run files now include: - - `run_08_igram` -- `run_08_igram` - - completed successfully in `Ubuntu-24.04` - - generated 10 pair directories under `stack_work/Igrams/` - - each pair now includes: - - wrapped interferogram `.int` - - amplitude `.amp` - - filtered interferogram `filt_*.int` - - coherence `filt_*.cor` - - unwrapped phase `filt_*_snaphu.unw` - - connected components `*.unw.conncomp` - - `referenceShelve/data.*` -- MintPy runtime bootstrap - - `scripts/install_mintpy_runtime_ubuntu2404.sh` created a dedicated WSL env: - - `mintpy` - - verified commands: - - `smallbaselineApp.py` - - `prep_isce.py` - - installed MintPy version: - - `1.6.2` -- `prep_isce.py` - - first run in the clean `mintpy` env failed because `mintpy.utils.isce_utils` imports `isce` - - experimental resolution: - - `scripts/run_mintpy_with_isce_ubuntu2404.sh` now bridges only the top-level `isce` package from the WSL `isce2` env into the `mintpy` env - - result: - - `prep_isce.py` completed successfully over the LT-1 `stripmapStack` outputs - - geometry `.rsc` files were written under `stack_work/geom_reference/` - - observation `.rsc` files were written for all 10 unwrapped interferograms under `stack_work/Igrams/*/` - -## Current Boundary - -This smoke test confirms: - -- LT-1 scenes can be materialized into `stripmapStack` acquisition directories -- the official stripmap stack driver can build the stack work plan over those LT-1 products -- the generated `run_01` to `run_07` chain can complete offline in `Ubuntu-24.04` over the sample LT-1 stack -- local DEM plus local orbit data are sufficient for this stack-preparation stage -- Earthdata credentials are not a hard blocker for this experiment track because the wrapper can recover `run_01_reference` with a synthetic all-land `waterMask` -- the same LT-1 stack can be regenerated in `interferogram` workflow mode to produce 10 filtered and unwrapped pair products -- MintPy metadata preparation is now viable through the dedicated `mintpy` env plus the explicit ISCE bridge wrapper - -This smoke test does not yet confirm: - -- `smallbaselineApp.py` execution beyond `prep_isce.py` -- final time-series or velocity products - -Follow-up status: - -- both of the above were later confirmed in: - - `notes/PHASE2_MINTPY_SBAS_SMOKETEST.md` - -## Next Tasks - -1. Keep this note focused on stack-generation findings only. -2. Use `notes/PHASE2_MINTPY_SBAS_SMOKETEST.md` for MintPy SBAS runtime conclusions. -3. Promote the stable runtime and artifact contract into backend workflow code and `docs/`. diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE2_BRIDGE_SMOKETEST_20260406.md b/experiments/isce2_sbas_timeseries/notes/PHASE2_BRIDGE_SMOKETEST_20260406.md deleted file mode 100644 index fc25f24..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE2_BRIDGE_SMOKETEST_20260406.md +++ /dev/null @@ -1,157 +0,0 @@ -# Phase 2 Bridge Smoketest - -Date: 2026-04-06 - -## Scope - -Validate the current SBAS bridge chain with the previously verified LT-1 sample stack: - -1. `build_lt1_stack_prep.py` -2. `materialize_lt1_stack_scenes.py` -3. `build_lt1_stack_prep.py` refresh - -This run validates the current bridge boundary only: - -- raw LT-1 scenes -- local precise orbit pool -- local prepared DEM -- fresh scratch workspace - -It does not run: - -- `stripmapStack` -- MintPy -- geocode/export/publish - -## Inputs - -- sample manifest: - - `experiments/isce2_sbas_timeseries/configs/sample_stack_e123p3_n46p1.json` -- orbit pool: - - `/mnt/d/orbit_pools/isce2` -- DEM: - - `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/inputs/dem/stack_dem_window.wgs84` - -## Workspace - -- scratch root: - - `experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406` - -## Result - -- final readiness: `True` -- scene count: `5` -- reference date: `20250510` -- all orbits resolved: `True` -- all `.slc/.slc.xml` present: `True` -- all `data` shelves present: `True` - -## Materialization Summary - -- dates: - - `20250118` - - `20250315` - - `20250510` - - `20250705` - - `20250830` -- status counts: - - `materialized: 5` -- total bytes written: - - `16313641344` - -## Artifacts - -- selected manifest used for this WSL run: - - `experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/selected_stack_manifest_wsl.json` -- generated stack manifest: - - `experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_input_manifest.json` -- materialization summary: - - `experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/materialization_summary.json` -- generated stack dry-run wrapper: - - `experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/run_stripmap_stack_dryrun.sh` -- synthetic water-mask recovery report: - - `experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/logs/run_01_reference.synthetic_watermask.json` - -## Finding - -`build_lt1_stack_prep.py` currently reads `scene["tiff_path"]` and `scene["meta_path"]` directly. -When the script is run inside WSL against the sample manifest, the original `F:\...` Windows paths are not readable as Linux paths. - -For this smoketest, a temporary WSL-path manifest copy was generated and used: - -- `selected_stack_manifest_wsl.json` - -This is an experiment-side workaround only. -No production/system logic was changed for this run. - -## Update 2026-04-07 - -The same fresh workspace was then continued through the stripmap stack run files. - -### Additional Result - -- `run_01_reference` - - reached `createWaterMask` - - failed on remote `SWBD` retrieval from: - - `https://e4ftl01.cr.usgs.gov/MEASURES/SRTMSWBD.003/...` - - recovered with a local synthetic all-land `waterMask.rdr` -- `run_02_focus_split` - - completed -- `run_03_geo2rdr_coarseResamp` - - completed -- `run_04_refineSecondaryTiming` - - completed -- `run_05_invertMisreg` - - completed -- `run_06_fineResamp` - - completed -- `run_07_grid_baseline` - - completed -- `run_08_igram` - - completed through interferogram generation, filtering, coherence, and `snaphu` unwrapping - -### Interferogram Snapshot - -- pair count on disk: - - `10` -- verified pair folders: - - `20250118_20250315` - - `20250118_20250510` - - `20250118_20250705` - - `20250118_20250830` - - `20250315_20250510` - - `20250315_20250705` - - `20250315_20250830` - - `20250510_20250705` - - `20250510_20250830` - - `20250705_20250830` -- verified key files in every pair directory: - - `filt_.int` - - `filt_.cor` - - `filt__snaphu.unw` - - `filt__snaphu.unw.conncomp` - -### Finding Update - -The original `run_generated_stack_runfile_ubuntu2404.sh` fallback only matched the `.netrc` credential failure text. -This workspace showed a second offline failure mode: - -- `createWaterMask` started normally -- ISCE2 `DataRetriever` failed during `SWBD` file retrieval -- the wrapper therefore did not auto-recover on the first attempt - -The experiment helper has now been widened to recognize both: - -- missing Earthdata credential text -- direct `SWBD` retrieval failure text from `createWaterMask` - -No production/system runtime was changed by this fix. - -## Recommended Next Step - -Use this fresh workspace to continue with: - -1. unified-env MintPy smoketest using: - - `configs/phase2_bridge_smoketest_20260406_smallbaseline.cfg` -2. publish-style geocode/export if MintPy succeeds -3. compare this fresh replay with the earlier baseline workspace diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE2_MINTPY_SBAS_SMOKETEST.md b/experiments/isce2_sbas_timeseries/notes/PHASE2_MINTPY_SBAS_SMOKETEST.md deleted file mode 100644 index 0f3c0a2..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE2_MINTPY_SBAS_SMOKETEST.md +++ /dev/null @@ -1,200 +0,0 @@ -# Phase 2 MintPy SBAS Smoke Test - -Updated: 2026-04-05 - -Follow-up note: - -- publish-style geocode/export continuation is now recorded separately in: - - `notes/PHASE3_PUBLISH_EXPORT_SMOKETEST.md` - -## Goal - -Validate that the LT-1 sample stack can continue from `stripmapStack` interferogram products into MintPy SBAS outputs under `Ubuntu-24.04`. - -## Sample - -- group key: - - `LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1` -- dates: - - `20250118` - - `20250315` - - `20250510` - - `20250705` - - `20250830` -- pair count: - - `10` -- reference date: - - `20250510` -- reference point: - - `y/x = 1994,52` - -## Successful Run - -Successful SBAS smoke-test work directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_v5` - -Successful WSL command: - -```text -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/configs/sample_smallbaseline_lt1_e123p3_n46p1.cfg /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_v5 -``` - -This run completed through: - -- `load_data` -- `modify_network` -- `reference_point` -- `quick_overview` -- `invert_network` -- `reference_date` -- `velocity` - -Disabled for this first offline smoke test: - -- unwrap-error correction -- solid-earth-tide correction -- ionosphere correction -- troposphere correction -- deramp -- topographic residual correction -- geocode - -## Output Snapshot - -Generated under `mintpy_sbas_v5/`: - -- `timeseries.h5` - - size: `82576752` bytes -- `velocity.h5` - - size: `83086408` bytes -- `temporalCoherence.h5` - - size: `16632424` bytes -- `maskTempCoh.h5` - - size: `4136792` bytes -- `avgSpatialCoh.h5` - - size: `16631784` bytes -- `numTriNonzeroIntAmbiguity.h5` - - size: `16632000` bytes -- `numTriNonzeroIntAmbiguity.png` - - size: `232878` bytes - -Quality summary from the successful run: - -- strict valid pixels for inversion: - - `1219001 / 4076199` - - `29.91%` -- reliable pixels in `maskTempCoh.h5` with threshold `0.7`: - - `62987` - -## Required Runtime Decisions - -### 1. Keep MintPy separate from ISCE2 - -- keep stack processing in WSL conda env: - - `isce2` -- keep MintPy in WSL conda env: - - `mintpy` - -Reason: - -- avoids mutating the already working ISCE2 processing env on the development machine - -### 2. Bridge only the top-level `isce` package - -Required helper: - -- `scripts/run_mintpy_with_isce_ubuntu2404.sh` - -Current rule: - -- do not add the entire `isce2` `site-packages` into `PYTHONPATH` -- only bridge the top-level `isce` package into a cache directory - -Reason: - -- adding the whole `site-packages` caused MintPy to import `h5py` from the wrong env and fail during `load_data` - -### 3. Do not load `wrapPhase` for this LT-1 smoke test - -Current config rule: - -- `mintpy.load.intFile = None` - -Reason: - -- loading `filt_*.int` into MintPy `wrapPhase` caused HDF5 type-conversion failure during `load_data` - -### 4. Build a strict `maskAllValid.h5` before inversion - -Required helper: - -- `scripts/create_mintpy_all_ifgram_mask.py` - -Current rule: - -- keep only pixels that are finite and non-zero in all unwrapped interferograms -- also require non-zero connected components in all interferograms - -Reason: - -- this reduces unstable partial-network pixels before SBAS inversion - -### 5. Use the repo-local patched launcher - -Required helper: - -- `scripts/run_smallbaselineApp_patched.py` - -Current workaround: - -- patch `mintpy.ifgram_inversion.estimate_timeseries()` at runtime -- coerce shape-`(1,)` inversion-quality output into a scalar for the single-pixel partial-network branch - -Reason: - -- MintPy `1.6.2` hit a `ValueError: setting an array element with a sequence` -- failure point: - - `mintpy/ifgram_inversion.py` - - partial-network pixel branch inside `run_ifgram_inversion_patch()` - -Current judgment: - -- this is a MintPy runtime issue in the current environment -- it is better to keep the workaround in repo-local launcher code than silently editing the third-party env - -## Current Boundary - -This smoke test now confirms: - -- LT-1 `stripmapStack` outputs can be loaded by MintPy in the dedicated `mintpy` env -- the LT-1 sample stack can be inverted into radar-coordinate `timeseries.h5` -- the LT-1 sample stack can generate radar-coordinate `velocity.h5` -- the current repo-local workaround chain is reproducible in `Ubuntu-24.04` - -This smoke test does not yet confirm: - -- geocoded SBAS exports -- atmospheric or DEM-residual correction quality -- product publishing into backend `psinsar` catalog -- frontend rendering of published SBAS products - -## System Embedding Implications - -The current experiment suggests the future backend runtime contract should be: - -1. Run ISCE2 stack workflow in WSL `isce2`. -2. Run MintPy through the repo-controlled bridge runner instead of calling upstream `smallbaselineApp.py` directly. -3. Generate a strict inversion mask after `load_data`. -4. Persist the following files as first-class workflow artifacts: - - `timeseries.h5` - - `velocity.h5` - - `temporalCoherence.h5` - - `maskTempCoh.h5` - - `numTriNonzeroIntAmbiguity.h5` - - `numTriNonzeroIntAmbiguity.png` -5. Convert these into a stable publish manifest before catalog registration. - -Current sample publish-manifest draft: - -- `configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json` diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE3_PUBLISH_EXPORT_SMOKETEST.md b/experiments/isce2_sbas_timeseries/notes/PHASE3_PUBLISH_EXPORT_SMOKETEST.md deleted file mode 100644 index 0018930..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE3_PUBLISH_EXPORT_SMOKETEST.md +++ /dev/null @@ -1,142 +0,0 @@ -# Phase 3 Publish Export Smoke Test - -Updated: 2026-04-06 - -## Goal - -Validate that the successful MintPy SBAS experiment can be converted into a publish-style artifact bundle without touching the main system. - -## Inputs - -Source MintPy work directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_v5` - -Source stack: - -- group key: - - `LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1` -- dates: - - `20250118` - - `20250315` - - `20250510` - - `20250705` - - `20250830` - -## Successful Command - -```text -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_v5 /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_v5 -``` - -## Current Export Scripts - -- `scripts/export_mintpy_publish_products_ubuntu2404.sh` - - geocode core MintPy outputs - - convert selected outputs to GeoTIFF - - copy preview and metadata files - - build root `manifest.json` -- `scripts/build_mintpy_publish_bundle.py` - - generate `preview/velocity_preview.png` - - generate `metadata/source_quality_summary.json` - - generate publish-style root `manifest.json` - -## Geocode Contract - -Current experiment settings: - -- lookup source: - - `inputs/geometryRadar.h5` -- output pixel size: - - latitude step: - - `-0.000185185` - - longitude step: - - `0.000185185` -- interpolation: - - `nearest` - -Observed geocoded grid: - -- extent: - - south: - - `45.80391` - - north: - - `46.42206` - - west: - - `122.930244` - - east: - - `123.76654` -- shape: - - rows: - - `3338` - - columns: - - `4516` - -## Generated Publish Bundle - -Successful publish-style directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_v5` - -Main outputs: - -- `manifest.json` -- `assets/geo_timeseries.h5` -- `assets/geo_velocity.h5` -- `assets/geo_temporalCoherence.h5` -- `assets/geo_maskTempCoh.h5` -- `assets/velocity.tif` -- `assets/temporalCoherence.tif` -- `assets/maskTempCoh.tif` -- `preview/velocity_preview.png` -- `preview/numTriNonzeroIntAmbiguity.png` -- `metadata/smallbaselineApp.cfg` -- `metadata/source_quality_summary.json` - -## Current Interpretation - -This confirms: - -- the experiment now supports radar-coordinate MintPy inversion -- the experiment also supports geocoded HDF5 exports -- the experiment can produce publish-style GeoTIFF outputs -- the experiment can build a stable manifest-driven bundle outside the MintPy work directory - -This does not yet confirm: - -- direct backend catalog registration -- frontend rendering against the real system APIs -- whether `EPSG:4326` should remain the final publish CRS decision - -## Important Notes - -### 1. Current CRS assumption - -`save_gdal.py` warned that no explicit `EPSG` metadata was found and assumed: - -- `EPSG:4326` - -Current judgment: - -- acceptable for this experiment because the geocoded outputs are in latitude/longitude grids -- should still be checked when formalizing the production publish contract - -### 2. Group key in the generated manifest - -Because PowerShell treats `|` specially, passing group keys on the command line is awkward from Windows. - -Current practical rule: - -- the sample manifest stored in git remains the clean reference: - - `configs/sample_psinsar_manifest_lt1_e123p3_n46p1.json` -- the generated publish bundle manifest can be post-filled or generated from backend metadata later - -## System Embedding Implication - -At this point the experiment-layer chain is split cleanly into three stages: - -1. `stripmapStack` preprocessing -2. MintPy SBAS inversion -3. geocode + publish-bundle export - -That means the future system workflow can wire them as separate workflow steps without changing the validated experiment logic first. diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_DECISION.md b/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_DECISION.md deleted file mode 100644 index 5835236..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_DECISION.md +++ /dev/null @@ -1,120 +0,0 @@ -# Phase 4 Unified-Environment Decision - -Updated: 2026-04-06 - -## Decision - -Current experiment preference: - -- prefer the unified WSL environment for SBAS experiment work - -Current production safety rule: - -- do not replace or mutate the existing D-InSAR production environment -- keep pair-oriented D-InSAR on the existing WSL `isce2` env -- keep the current backend/public D-InSAR entry unchanged - -Current environment split: - -- D-InSAR production baseline: - - `isce2` -- SBAS experiment preferred runtime: - - `isce2_mintpy_v1` - -## Why This Decision Is Reasonable - -The unified env has now completed the full current experiment chain: - -- MintPy command invocation without the `isce` bridge -- `load_data` -- strict-mask generation -- `modify_network -> velocity` -- publish export to geocoded HDF5, GeoTIFF, preview, and `manifest.json` - -This makes the unified env a valid experiment baseline. - -At the same time, the existing pair-oriented D-InSAR route is already working and should not be destabilized just to simplify the SBAS experiment runtime. - -The safest rule is therefore: - -- let SBAS experiments move forward in a separate unified env -- do not touch the current `isce2` production env used by D-InSAR - -## Evidence - -Successful unified env: - -- `/home/administrator/miniconda3/envs/isce2_mintpy_v1` - -Successful unified SBAS work directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_unified_v1` - -Successful unified publish directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_unified_v1` - -Matched quality indicators: - -- `maskAllValid`: - - `1219001 / 4076199` - - `29.91%` -- `maskTempCoh`: - - `62987 / 4076199` - - `1.55%` - -Key package difference: - -- current `isce2` env does not provide the MintPy-side package set needed for this SBAS route -- current `isce2_mintpy_v1` env includes: - - `mintpy` - - `cartopy` - - `pyaps3` - - `pykml` - - `cvxopt` - -## What This Decision Does Not Mean - -It does not mean: - -- the backend should immediately switch to unified-env execution -- the bridge route must be deleted now -- the current D-InSAR runtime should be modified - -It only means: - -- for the next experiment steps, unified env is the preferred path -- for current production safety, `isce2` remains untouched - -## Required Guardrails - -For the next phase, keep these rules: - -- do not install MintPy into the existing `isce2` env -- do not redirect current D-InSAR scripts to `isce2_mintpy_v1` -- do not remove the bridge-based helpers yet -- keep all SBAS work in experiment scripts, notes, and scratch directories - -## Reproducibility - -Environment snapshots should be exported and kept with the experiment record. - -Current snapshot command: - -```text -wsl -d Ubuntu-24.04 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/export_phase4_env_snapshots_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/configs/env_snapshots/20260406 -``` - -## Next Stable Experiment Steps - -Before any system integration work: - -1. Keep the unified env as the default SBAS experiment runtime. -2. Preserve environment snapshots for both `isce2` and `isce2_mintpy_v1`. -3. Write one comparison note focused on: - - runtime simplicity - - reproducibility - - remaining workarounds - - risk to D-InSAR production -4. Optionally repeat the chain on one more LT-1 sample stack. -5. Only after the experiment is stable, design the system integration boundary. diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_EXPERIMENT.md b/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_EXPERIMENT.md deleted file mode 100644 index 4bfe38f..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_EXPERIMENT.md +++ /dev/null @@ -1,221 +0,0 @@ -# Phase 4 Unified-Environment Experiment - -Updated: 2026-04-06 - -## Goal - -Validate whether the current WSL `isce2` runtime can be cloned and extended with MintPy so that the SBAS experiment can run without the temporary `isce` bridge helper. - -This is still an experiment-layer task. - -Do not change the production backend yet. - -## Why run this phase - -The bridge-based route is already validated, but a unified environment may be cleaner because: - -- many ISCE2 + MintPy users operate in one environment -- command invocation becomes simpler -- future worker deployment may be easier if one runtime is stable - -The bridge-based route still remains the fallback baseline until this phase is verified. - -## Current Known Starting Point - -WSL distro: - -- `Ubuntu-24.04` - -Current environments observed on 2026-04-06: - -- `isce2` -- `mintpy` - -Observed package state: - -- `isce2` env: - - `isce2 2.6.4` - - `h5py 3.15.1` - - `mintpy` not installed -- dedicated `mintpy` env: - - `mintpy 1.6.3` - -## Initial Hypothesis - -Expected best-case outcome: - -- clone `isce2` into `isce2_mintpy` -- install `mintpy` directly into the clone -- reuse the same repo-local strict-mask and patched-launcher helpers -- run the same LT-1 smoke test without the `isce` bridge wrapper - -Main risk areas: - -- package solver may replace or downgrade key ISCE2-side numeric dependencies -- MintPy may still require the same repo-local runtime workaround even in a unified env -- GDAL / h5py / pyaps3 dependency changes may alter the known-good stack behavior - -## Reproducible Commands - -### 1. Bootstrap the unified env - -```text -wsl -d Ubuntu-24.04 env TARGET_ENV=isce2_mintpy_v1 BOOTSTRAP_MODE=recreate USE_TUNA_MIRROR=1 MINTPY_SPEC=mintpy=1.6.3 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/install_mintpy_into_cloned_isce2_env_ubuntu2404.sh -``` - -Why `BOOTSTRAP_MODE=recreate`: - -- direct `conda create --clone` was not stable enough for this machine -- it still followed source-package URLs and hit channel/TOS friction -- the successful path was: - - export the current `isce2` dependency list - - recreate the env through Tsinghua mirror channels - - reinstall exported pip packages - - install MintPy into the recreated env - -Optional environment override: - -```text -TARGET_ENV=isce2_mintpy_v1 MINTPY_SPEC='mintpy=1.6.3' -``` - -### 2. Run MintPy commands directly inside the unified env - -```text -wsl -d Ubuntu-24.04 env MINTPY_ENV=isce2_mintpy_v1 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_mintpy_unified_env_ubuntu2404.sh prep_isce.py -h -``` - -### 3. Re-run the current LT-1 SBAS smoke test in the unified env - -```text -wsl -d Ubuntu-24.04 env MINTPY_ENV=isce2_mintpy_v1 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_unified_env_smoketest_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/configs/sample_smallbaseline_lt1_e123p3_n46p1.cfg /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_unified_v1 -``` - -### 4. Export publish bundle in the unified env - -```text -wsl -d Ubuntu-24.04 env MINTPY_ENV=isce2_mintpy_v1 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_unified_env_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_unified_v1 /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_unified_v1 -``` - -## Comparison Checklist - -When this phase is executed, compare it against the bridge route on: - -- package versions after install -- whether `prep_isce.py` imports cleanly without bridge -- whether `load_data` succeeds -- whether the strict-mask step is still required -- whether the patched launcher is still required -- whether output files match the existing bridge-based artifact set -- whether geocode/export still succeeds from the unified env - -## Current Status - -Successful environment: - -- `/home/administrator/miniconda3/envs/isce2_mintpy_v1` - -Observed package/version state in the successful unified env: - -- `conda list` shows: - - `mintpy 1.6.3` -- runtime `mintpy.__version__` reports: - - `1.6.2` -- `isce` import path: - - `/home/administrator/miniconda3/envs/isce2_mintpy_v1/lib/python3.11/site-packages/isce/__init__.py` -- `mintpy` import path: - - `/home/administrator/miniconda3/envs/isce2_mintpy_v1/lib/python3.11/site-packages/mintpy/__init__.py` -- `h5py`: - - `3.15.1` - -Successful unified-env SBAS work directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/stack_work/mintpy_sbas_unified_v1` - -Successful unified-env publish directory: - -- `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/lt1a_strip1_hh_descending_e123p3_n46p1/publish/mintpy_sbas_unified_v1` - -Validated in the unified env: - -- `prep_isce.py -h` works without the `isce` bridge -- `load_data` succeeded -- strict-mask generation succeeded -- `modify_network -> velocity` succeeded -- publish export succeeded through geocoded HDF5, GeoTIFF, preview, and `manifest.json` - -Observed unified-env output set under `mintpy_sbas_unified_v1/`: - -- `timeseries.h5` -- `velocity.h5` -- `temporalCoherence.h5` -- `maskTempCoh.h5` -- `maskAllValid.h5` -- `avgSpatialCoh.h5` -- `numTriNonzeroIntAmbiguity.h5` -- `numTriNonzeroIntAmbiguity.png` - -Observed publish bundle under `publish/mintpy_sbas_unified_v1/`: - -- `manifest.json` -- `assets/geo_timeseries.h5` -- `assets/geo_velocity.h5` -- `assets/geo_temporalCoherence.h5` -- `assets/geo_maskTempCoh.h5` -- `assets/velocity.tif` -- `assets/temporalCoherence.tif` -- `assets/maskTempCoh.tif` -- `preview/velocity_preview.png` -- `preview/numTriNonzeroIntAmbiguity.png` -- `metadata/smallbaselineApp.cfg` -- `metadata/source_quality_summary.json` - -Quality summary matched the bridge-based route: - -- `maskAllValid`: - - `1219001 / 4076199` - - `29.91%` -- `maskTempCoh`: - - `62987 / 4076199` - - `1.55%` - -Still required in the unified env: - -- strict `maskAllValid.h5` before inversion -- repo-local patched `smallbaselineApp` launcher - -Prepared: - -- unified-env bootstrap script -- unified-env MintPy runner -- unified-env SBAS smoke-test runner -- unified-env publish-export wrapper - -Completed: - -- actual clone + install execution -- actual smoke-test result capture -- actual publish-export capture - -Pending: - -- deeper comparison of output metadata against the bridge route -- decide whether unified env or bridge env should be the default production candidate -- decide whether to pin the runtime to the conda package label `1.6.3` or the internal MintPy version string `1.6.2` - -## Current Judgment - -At experiment level, the unified environment is now viable. - -This phase confirms: - -- the current LT-1 SBAS route does not fundamentally require the `isce` bridge -- a recreated `isce2 + mintpy` WSL env can complete: - - MintPy load/inversion - - geocode/export - - publish-bundle generation - -Current recommendation: - -- keep the bridge route as the already-known baseline until a fuller diff is written -- but treat the unified env as a valid candidate for the future production runtime diff --git a/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_REPLAY_PHASE2_20260408.md b/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_REPLAY_PHASE2_20260408.md deleted file mode 100644 index 80dda4b..0000000 --- a/experiments/isce2_sbas_timeseries/notes/PHASE4_UNIFIED_ENV_REPLAY_PHASE2_20260408.md +++ /dev/null @@ -1,193 +0,0 @@ -# Phase 4 Unified-Env Replay On Fresh Phase-2 Workspace - -Updated: 2026-04-08 - -## Goal - -Re-run the already validated unified-env SBAS route on the fresh workspace: - -- `scratch/phase2_bridge_smoketest_20260406` - -This checks that the current experiment does not rely on the older sample workspace only. - -## Inputs - -- WSL distro: - - `Ubuntu-24.04` -- unified env: - - `/home/administrator/miniconda3/envs/isce2_mintpy_v1` -- stack config: - - `configs/phase2_bridge_smoketest_20260406_smallbaseline.cfg` -- stack workspace: - - `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406` -- MintPy work dir: - - `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/mintpy_sbas_unified_phase2_20260407` -- publish dir: - - `/mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/publish/mintpy_sbas_unified_phase2_20260407` - -## Successful Commands - -Unified-env MintPy smoke test: - -```text -wsl -d Ubuntu-24.04 env MINTPY_ENV=isce2_mintpy_v1 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_unified_env_smoketest_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/configs/phase2_bridge_smoketest_20260406_smallbaseline.cfg /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/mintpy_sbas_unified_phase2_20260407 -``` - -Unified-env publish export: - -```text -wsl -d Ubuntu-24.04 env MINTPY_ENV=isce2_mintpy_v1 bash /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_unified_env_ubuntu2404.sh /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/stack_work/mintpy_sbas_unified_phase2_20260407 /mnt/z/Code/Insar_management_system_v2/experiments/isce2_sbas_timeseries/scratch/phase2_bridge_smoketest_20260406/publish/mintpy_sbas_unified_phase2_20260407 -``` - -## Result - -The fresh workspace replay succeeded through: - -- `load_data` -- strict `maskAllValid.h5` generation -- `modify_network` -- `reference_point` -- `quick_overview` -- `invert_network` -- `reference_date` -- `velocity` -- geocode/export -- publish-bundle generation - -This confirms the current fresh workspace now reaches the same experiment boundary as the earlier baseline sample: - -- radar-coordinate MintPy runtime products -- geocoded publish bundle - -## Runtime Output Snapshot - -Generated under `stack_work/mintpy_sbas_unified_phase2_20260407/`: - -- `timeseries.h5` - - `82579888` bytes -- `velocity.h5` - - `83086408` bytes -- `temporalCoherence.h5` - - `16632424` bytes -- `maskTempCoh.h5` - - `4136848` bytes -- `maskAllValid.h5` - - `4095215` bytes -- `avgSpatialCoh.h5` - - `16631784` bytes -- `numTriNonzeroIntAmbiguity.h5` - - `16632000` bytes -- `numTriNonzeroIntAmbiguity.png` - - `232923` bytes - -## Publish Bundle Snapshot - -Generated under `publish/mintpy_sbas_unified_phase2_20260407/`: - -- `manifest.json` - - `32442` bytes -- `assets/geo_timeseries.h5` - - `304443360` bytes -- `assets/geo_velocity.h5` - - `305627744` bytes -- `assets/geo_temporalCoherence.h5` - - `61141152` bytes -- `assets/geo_maskTempCoh.h5` - - `15260536` bytes -- `assets/velocity.tif` - - `60318026` bytes -- `assets/temporalCoherence.tif` - - `60318026` bytes -- `assets/maskTempCoh.tif` - - `15094802` bytes -- `preview/velocity_preview.png` - - `813928` bytes -- `preview/numTriNonzeroIntAmbiguity.png` - - `232923` bytes -- `metadata/smallbaselineApp.cfg` - - `26419` bytes -- `metadata/source_quality_summary.json` - - `403` bytes - -## Quality Summary - -From `metadata/source_quality_summary.json`: - -- `maskAllValid` - - `1219067 / 4076199` - - `29.91%` -- `maskTempCoh` - - `63092 / 4076199` - - `1.55%` -- preview stretch: - - `vmin = -0.2251889556646347` - - `vmax = 0.2251889556646347` - -## Findings - -### 1. The fresh workspace replay is reproducible - -The new workspace produced: - -- the full 10-pair interferogram stack -- MintPy `timeseries.h5` -- MintPy `velocity.h5` -- publish-layer geocoded HDF5 / GeoTIFF / preview / `manifest.json` - -This is the current strongest experiment proof that the SBAS route is not tied to the earlier historical scratch directory. - -### 2. The same two MintPy experiment helpers are still required - -The unified env still depends on: - -- `create_mintpy_all_ifgram_mask.py` -- `run_smallbaselineApp_patched.py` - -Current interpretation: - -- unified env removes the temporary `isce` bridge -- it does not remove the current strict-mask or patched-launcher workarounds - -### 3. Offline water-mask strategy remains valid - -This replay consumed the synthetic all-land `waterMask.rdr` created earlier in the fresh workspace. - -No Earthdata / `SWBD` download was needed for the MintPy or publish stages. - -### 4. Geocode/export completed with a tolerable warning - -`save_gdal.py` warned that no EPSG / UTM metadata was found and assumed: - -- `EPSG:4326` - -For this experiment chain, that is acceptable because the export was already driven by latitude / longitude lookup plus explicit `--lalo` sampling. - -This warning should still be recorded for later production hardening. - -### 5. `group_key` should come from system metadata - -This host-side replay exported a valid publish bundle, and the bundle should carry: - -- `"group_key": "LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1"` - -Reason: - -- the bundle builder accepts `group_key` as a CLI argument -- the LT-1 group key contains pipe characters: - - `LT1A|STRIP1|HH|DESCENDING|E123.3_N46.1` -- when invoked naively from Windows PowerShell into WSL, the value may be truncated or split by the host shell - -Current judgment: - -- this is not a blocker for the SBAS scientific chain -- future system embedding should write `group_key` from task/run metadata inside backend code instead of relying on manual shell arguments - -## Current Judgment - -The current fresh workspace now confirms the full experiment chain: - -- `raw LT-1 -> stripmapStack -> MintPy SBAS -> geocode/export -> publish bundle` - -At experiment level, the unified env remains the preferred SBAS runtime. - -At production-safety level, the existing D-InSAR `isce2` environment should still remain untouched. diff --git a/experiments/isce2_sbas_timeseries/scripts/.gitkeep b/experiments/isce2_sbas_timeseries/scripts/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py b/experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py deleted file mode 100644 index d8df430..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/build_lt1_stack_prep.py +++ /dev/null @@ -1,647 +0,0 @@ -#!/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, PurePosixPath -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_SHARED_ENV_NAME = "insar_wsl_v1" -DEFAULT_CONDA_ROOT_WSL = "/home/administrator/miniconda3" -DEFAULT_CONDA_WSL = f"{DEFAULT_CONDA_ROOT_WSL}/bin/conda" -SUPPORTED_STACK_WORKFLOWS = ("slc", "interferogram", "ionosphere") - - -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 _default_python_wsl(env_name: str) -> str: - normalized_env = str(env_name or "").strip() or DEFAULT_SHARED_ENV_NAME - return f"{DEFAULT_CONDA_ROOT_WSL}/envs/{normalized_env}/bin/python" - - -def _resolve_runtime_paths(env_values: Dict[str, Any]) -> Dict[str, str]: - env_name = str( - env_values.get("TIMESERIES_ENV_NAME") - or env_values.get("WSL_SHARED_CONDA_ENV") - or DEFAULT_SHARED_ENV_NAME - ).strip() or DEFAULT_SHARED_ENV_NAME - python_wsl = str( - env_values.get("TIMESERIES_PYTHON") - or env_values.get("WSL_SHARED_PYTHON") - or env_values.get("ISCE2_PYTHON") - or _default_python_wsl(env_name) - ).strip() or _default_python_wsl(env_name) - - python_path = PurePosixPath(python_wsl) - env_root = python_path.parent.parent - conda_root = env_root.parent.parent - conda_bin_wsl = str(conda_root / "bin" / "conda") - isce2_share_wsl = str(env_root / "share" / "isce2") - stripmap_stack_dir_wsl = str(PurePosixPath(isce2_share_wsl) / "stripmapStack") - stack_script_wsl = str(PurePosixPath(stripmap_stack_dir_wsl) / "stackStripMap.py") - stack_text_cmd = ( - f"export PATH={stripmap_stack_dir_wsl}:$PATH; " - f"export PYTHONPATH={stripmap_stack_dir_wsl}:{isce2_share_wsl}${{PYTHONPATH:+:$PYTHONPATH}}; " - ) - - return { - "env_name": env_name, - "python_wsl": python_wsl, - "conda_bin_wsl": conda_bin_wsl, - "isce2_share_wsl": isce2_share_wsl, - "stripmap_stack_dir_wsl": stripmap_stack_dir_wsl, - "stack_script_wsl": stack_script_wsl, - "stack_text_cmd": stack_text_cmd, - } - - -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, - runtime: Dict[str, str], -) -> List[str]: - return [ - runtime["conda_bin_wsl"], - "run", - "-n", - runtime["env_name"], - "python", - runtime["stack_script_wsl"], - "-s", - slc_dir_wsl, - "-d", - dem_wsl, - "-w", - work_dir_wsl, - "-m", - reference_date, - "--nofocus", - "-W", - workflow, - "-u", - "snaphu", - "-c", - runtime["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"] - runtime = report["runtime"] - - 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)} -CONDA_ENV={shell_quote(runtime["env_name"])} -CONDA_BIN={shell_quote(runtime["conda_bin_wsl"])} -ISCE2_SHARE={shell_quote(runtime["isce2_share_wsl"])} -STRIPMAP_STACK_DIR={shell_quote(runtime["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 "Conda env: $CONDA_ENV" -echo "Conda bin: $CONDA_BIN" -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") - runtime = _resolve_runtime_paths(env_values) - - 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, - runtime=runtime, - ) - - 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", - "runtime": runtime, - "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()) diff --git a/experiments/isce2_sbas_timeseries/scripts/build_mintpy_publish_bundle.py b/experiments/isce2_sbas_timeseries/scripts/build_mintpy_publish_bundle.py deleted file mode 100644 index 51561ce..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/build_mintpy_publish_bundle.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/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()) diff --git a/experiments/isce2_sbas_timeseries/scripts/check_env_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/check_env_ubuntu2404.sh deleted file mode 100644 index ccc0100..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/check_env_ubuntu2404.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/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 diff --git a/experiments/isce2_sbas_timeseries/scripts/create_mintpy_all_ifgram_mask.py b/experiments/isce2_sbas_timeseries/scripts/create_mintpy_all_ifgram_mask.py deleted file mode 100644 index e1cffc5..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/create_mintpy_all_ifgram_mask.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/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()) diff --git a/experiments/isce2_sbas_timeseries/scripts/create_synthetic_watermask.py b/experiments/isce2_sbas_timeseries/scripts/create_synthetic_watermask.py deleted file mode 100644 index fe14b71..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/create_synthetic_watermask.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/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()) diff --git a/experiments/isce2_sbas_timeseries/scripts/export_conda_env_snapshot_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/export_conda_env_snapshot_ubuntu2404.sh deleted file mode 100644 index 317a13d..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/export_conda_env_snapshot_ubuntu2404.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " >&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__', '') - except Exception as exc: - return f'' - -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__\", \"\")}') - -if importlib.util.find_spec('mintpy') is not None: - import mintpy - print(f'mintpy_file={mintpy.__file__}') - print(f'mintpy_version={getattr(mintpy, \"__version__\", \"\")}') - -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" diff --git a/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_ubuntu2404.sh deleted file mode 100644 index 330e287..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_ubuntu2404.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -lt 2 || $# -gt 3 ]]; then - echo "Usage: $0 [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 diff --git a/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_unified_env_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_unified_env_ubuntu2404.sh deleted file mode 100644 index 998f2a3..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/export_mintpy_publish_products_unified_env_ubuntu2404.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/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" "$@" diff --git a/experiments/isce2_sbas_timeseries/scripts/export_phase4_env_snapshots_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/export_phase4_env_snapshots_ubuntu2404.sh deleted file mode 100644 index 1e0f88f..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/export_phase4_env_snapshots_ubuntu2404.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -ne 1 ]]; then - echo "Usage: $0 " >&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" diff --git a/experiments/isce2_sbas_timeseries/scripts/install_isce2_stack_runtime_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/install_isce2_stack_runtime_ubuntu2404.sh deleted file mode 100644 index 6d26a1d..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/install_isce2_stack_runtime_ubuntu2404.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/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" diff --git a/experiments/isce2_sbas_timeseries/scripts/install_mintpy_into_cloned_isce2_env_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/install_mintpy_into_cloned_isce2_env_ubuntu2404.sh deleted file mode 100644 index 2135b47..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/install_mintpy_into_cloned_isce2_env_ubuntu2404.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/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" diff --git a/experiments/isce2_sbas_timeseries/scripts/install_mintpy_runtime_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/install_mintpy_runtime_ubuntu2404.sh deleted file mode 100644 index a0bfbf7..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/install_mintpy_runtime_ubuntu2404.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/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" diff --git a/experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py b/experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py deleted file mode 100644 index c49f2f3..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/materialize_lt1_stack_scenes.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/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()) diff --git a/experiments/isce2_sbas_timeseries/scripts/phase0_todo.md b/experiments/isce2_sbas_timeseries/scripts/phase0_todo.md deleted file mode 100644 index a3b5c39..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/phase0_todo.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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. diff --git a/experiments/isce2_sbas_timeseries/scripts/prepare_lt1_stack_dem.py b/experiments/isce2_sbas_timeseries/scripts/prepare_lt1_stack_dem.py deleted file mode 100644 index c79cac3..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/prepare_lt1_stack_dem.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/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()) diff --git a/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh deleted file mode 100644 index ab015c5..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/run_generated_stack_runfile_ubuntu2404.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -lt 2 ]]; then - echo "Usage: $0 " >&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_ROOT="${CONDA_ROOT:-/home/administrator/miniconda3}" -CONDA_ENV="${CONDA_ENV:-insar_wsl_v1}" -CONDA_BIN="${CONDA_BIN:-$CONDA_ROOT/bin/conda}" -ISCE2_SHARE="${ISCE2_SHARE:-$CONDA_ROOT/envs/$CONDA_ENV/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 "Conda bin: $CONDA_BIN" -echo "ISCE2 share: $ISCE2_SHARE" -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" diff --git a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh deleted file mode 100644 index a7ea76a..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_smoketest_ubuntu2404.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " >&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 diff --git a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_unified_env_smoketest_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_unified_env_smoketest_ubuntu2404.sh deleted file mode 100644 index d0bd8f6..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_sbas_unified_env_smoketest_ubuntu2404.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " >&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 diff --git a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_unified_env_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/run_mintpy_unified_env_ubuntu2404.sh deleted file mode 100644 index 10144af..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_unified_env_ubuntu2404.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: $0 [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" "$@" diff --git a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_with_isce_ubuntu2404.sh b/experiments/isce2_sbas_timeseries/scripts/run_mintpy_with_isce_ubuntu2404.sh deleted file mode 100644 index a67387b..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/run_mintpy_with_isce_ubuntu2404.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: $0 [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" "$@" diff --git a/experiments/isce2_sbas_timeseries/scripts/run_smallbaselineApp_patched.py b/experiments/isce2_sbas_timeseries/scripts/run_smallbaselineApp_patched.py deleted file mode 100644 index df82aa9..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/run_smallbaselineApp_patched.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/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:])) diff --git a/experiments/isce2_sbas_timeseries/scripts/scan_lt1_stack_candidates.py b/experiments/isce2_sbas_timeseries/scripts/scan_lt1_stack_candidates.py deleted file mode 100644 index 4fac679..0000000 --- a/experiments/isce2_sbas_timeseries/scripts/scan_lt1_stack_candidates.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/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"^(?PLT1[AB])_" - r"(?P[^_]+)_" - r"(?P[^_]+)_" - r"(?P[^_]+)_" - r"(?P\d+)_" - r"(?PE\d+\.\d+)_" - r"(?PN\d+\.\d+)_" - r"(?P\d{8})_" - r"(?P[^_]+)_" - r"(?P[^_]+)_" - r"(?P[^_]+)_" - r"(?P\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()) diff --git a/frontend/src/DinsarProductionPanel.jsx b/frontend/src/DinsarProductionPanel.jsx index b9136c1..80dfa68 100644 --- a/frontend/src/DinsarProductionPanel.jsx +++ b/frontend/src/DinsarProductionPanel.jsx @@ -129,6 +129,7 @@ function epochSeconds(value) { function taskToRunRow(task) { const taskId = task?.task_id || ''; + const message = task?.message || task?.task_name || ''; return { run_id: taskId, record_type: 'task', @@ -145,7 +146,8 @@ function taskToRunRow(task) { workflow_run_id: '', root_dir: '', publish_root_dir: '', - message: task?.message || task?.task_name || '', + message, + inferred_paths: inferTaskPaths(task), total_items: null, completed_items: null, failed_items: null, @@ -153,6 +155,17 @@ function taskToRunRow(task) { }; } +function inferTaskPaths(task) { + const message = String(task?.message || '').trim(); + const workMatch = message.match(/work_dir=([^)\s]+)/); + const workDir = workMatch ? workMatch[1] : ''; + if (!workDir) return {}; + return { + work_dir: workDir, + ifgrams_dir: workDir ? `${workDir}\\\\ifgrams` : '', + }; +} + function mergeRunRows(productionRuns, recentTasks, limit = 20) { const rows = (productionRuns || []).map(run => ({ ...run, @@ -206,6 +219,49 @@ function formatPyintPreciseOrbitMode(mode) { return PYINT_PRECISE_ORBIT_MODE_LABEL[mode] || mode || '-'; } +function getSchemaOptions(schema) { + if (Array.isArray(schema?.enum) && schema.enum.length > 0) return schema.enum; + if (Array.isArray(schema?.choices) && schema.choices.length > 0) return schema.choices; + return []; +} + +function formatPathValue(value) { + const text = String(value || '').trim(); + return text || '-'; +} + +function RunPathBlock({ run }) { + const items = Array.isArray(run?.items) ? run.items : []; + const item = items.find(entry => entry?.status === 'RUNNING') || items[0] || null; + const paths = item?.paths || run?.inferred_paths || {}; + if (!item && Object.keys(paths).length === 0) return null; + const rows = [ + ['任务', item?.task_alias || item?.task_name || run?.message || '-'], + ['运行目录', paths.run_dir], + ['native', paths.native_dir], + ['assets', paths.assets_dir], + ]; + if (run?.engine === 'pyint') { + rows.push(['work', paths.work_dir]); + rows.push(['project', paths.project_dir]); + rows.push(['ifgrams', paths.ifgrams_dir]); + rows.push(['重去平日志', paths.reflatten_dir]); + } + + return ( +
+ {rows.filter(([, value]) => String(value || '').trim()).map(([label, value]) => ( +
+ {label} + + {formatPathValue(value)} + +
+ ))} +
+ ); +} + function PreviewIssueList({ title, items, tone = 'warning' }) { if (!Array.isArray(items) || items.length === 0) { return null; @@ -387,7 +443,8 @@ function ParamField({ name, schema, value, disabled, onChange }) { ); } - if (Array.isArray(schema.enum) && schema.enum.length > 0) { + const options = getSchemaOptions(schema); + if (options.length > 0) { return (
@@ -407,12 +464,12 @@ function ParamField({ name, schema, value, disabled, onChange }) { )}
@@ -493,7 +550,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) const currentParamSections = buildParamSections(currentParamSchema); const currentDefaultTimeoutSec = Number(currentEngineObj?.default_timeout_seconds || 0) || 0; const currentParamHelpText = selectedEngine === 'pyint' - ? 'PyINT/Gamma 默认按目标网格尺寸自动换算多视,逻辑与 ENVI/SARscape 自定义流程一致;通常只需要设置目标网格、并行度和是否执行解缠/地理编码。' + ? 'PyINT/Gamma 会按目标网格尺寸自动换算多视;新增 Gamma 残余重去平在解缠后执行 rascc_mask/quad_fit/quad_sub,再导出 native 和标准 GeoTIFF。' : selectedEngine === 'isce2' ? '这些参数现在按执行、交付、增强分组展示。结果异常时,优先尝试关闭增强项,再回看基础几何和配对质量。' : '这些参数影响当前引擎的生产模板。建议先使用默认值,只有在结果边界、噪声或几何表现异常时再逐项调整。'; @@ -1090,7 +1147,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) )} {selectedEngine === 'pyint' && currentDefaultTimeoutSec > 0 && (
- PyINT 默认按单对任务使用 {currentDefaultTimeoutSec} 秒;当前会逐对串行创建工作区并运行外部 PyINT / Gamma 流程。 + PyINT 默认按单对任务使用 {currentDefaultTimeoutSec} 秒;当前会逐对串行创建工作区并运行外部 PyINT / Gamma 流程,native 会在主流程和重去平后统一写入。
)}
@@ -1512,7 +1569,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) - {['运行ID', '引擎', '状态', '时间', '操作'].map(header => ( + {['运行ID', '引擎', '状态', '时间', '路径', '操作'].map(header => (
{run.started_at ? new Date(run.started_at * 1000).toLocaleString() : '-'} + + +