chore: sync repository with current workspace state
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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] + "...<truncated>"
|
||||
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", ""),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
@@ -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
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user