feat: add Gamma SBAS workflow and coverage design
This commit is contained in:
@@ -221,6 +221,27 @@ PYINT_GAMMA_ENV_SCRIPT=D:\Code\Insar_management_system_v2\deploy\wsl\profiles\ga
|
|||||||
PYINT_DEFAULT_TIMEOUT_SECONDS=43200
|
PYINT_DEFAULT_TIMEOUT_SECONDS=43200
|
||||||
PYINT_SMOKE_TEST_ENABLED=false
|
PYINT_SMOKE_TEST_ENABLED=false
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Gamma SBAS-InSAR
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
GAMMA_SBAS_ENABLED=true
|
||||||
|
GAMMA_SBAS_RUNTIME_ID=gamma_sbas_runtime_v1
|
||||||
|
GAMMA_SBAS_WSL_DISTRO=Ubuntu-24.04
|
||||||
|
GAMMA_SBAS_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
|
||||||
|
GAMMA_SBAS_ENV_SCRIPT=D:\Code\Insar_management_system_v2\deploy\wsl\profiles\gamma_env.sh
|
||||||
|
GAMMA_SBAS_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\sbas_insar_production
|
||||||
|
GAMMA_SBAS_PRODUCT_ROOT=D:\production_results\timeseries\sbas
|
||||||
|
GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\templates\gamma_sbas
|
||||||
|
GAMMA_SBAS_SOURCE_ROOTS=D:\LuTan1_Image_Pool
|
||||||
|
GAMMA_SBAS_ORBIT_ROOTS=D:\orbit_pools\envi
|
||||||
|
GAMMA_SBAS_DEFAULT_RLKS=8
|
||||||
|
GAMMA_SBAS_DEFAULT_AZLKS=8
|
||||||
|
GAMMA_SBAS_DEFAULT_MB_MODE=0
|
||||||
|
GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW=16
|
||||||
|
GAMMA_SBAS_AUTO_APPROVE_ITAB=true
|
||||||
|
GAMMA_SBAS_STEP_TIMEOUT_SECONDS=43200
|
||||||
|
GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS=172800
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# 时序 InSAR(旧 ISCE2/MintPy 链路)
|
# 时序 InSAR(旧 ISCE2/MintPy 链路)
|
||||||
|
|||||||
Binary file not shown.
@@ -308,6 +308,25 @@ class Settings(BaseSettings):
|
|||||||
PYINT_GAMMA_ENV_SCRIPT: str = ""
|
PYINT_GAMMA_ENV_SCRIPT: str = ""
|
||||||
PYINT_DEFAULT_TIMEOUT_SECONDS: int = 43200
|
PYINT_DEFAULT_TIMEOUT_SECONDS: int = 43200
|
||||||
PYINT_SMOKE_TEST_ENABLED: bool = False
|
PYINT_SMOKE_TEST_ENABLED: bool = False
|
||||||
|
|
||||||
|
GAMMA_SBAS_ENABLED: bool = True
|
||||||
|
GAMMA_SBAS_RUNTIME_ID: str = ""
|
||||||
|
GAMMA_SBAS_WSL_DISTRO: str = ""
|
||||||
|
GAMMA_SBAS_PYTHON: str = ""
|
||||||
|
GAMMA_SBAS_ENV_SCRIPT: str = ""
|
||||||
|
GAMMA_SBAS_WORK_ROOT: str = ""
|
||||||
|
GAMMA_SBAS_PRODUCT_ROOT: str = ""
|
||||||
|
GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: str = ""
|
||||||
|
GAMMA_SBAS_SOURCE_ROOTS: str = ""
|
||||||
|
GAMMA_SBAS_ORBIT_ROOTS: str = ""
|
||||||
|
GAMMA_SBAS_DEFAULT_RLKS: int = 8
|
||||||
|
GAMMA_SBAS_DEFAULT_AZLKS: int = 8
|
||||||
|
GAMMA_SBAS_DEFAULT_MB_MODE: int = 0
|
||||||
|
GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW: int = 16
|
||||||
|
GAMMA_SBAS_AUTO_APPROVE_ITAB: bool = True
|
||||||
|
GAMMA_SBAS_STEP_TIMEOUT_SECONDS: int = 43200
|
||||||
|
GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS: int = 172800
|
||||||
|
|
||||||
JOB_WORKER_HEALTH_TIMEOUT: int = 60
|
JOB_WORKER_HEALTH_TIMEOUT: int = 60
|
||||||
JOB_WORKER_JOB_HEARTBEAT_INTERVAL: float = 5.0
|
JOB_WORKER_JOB_HEARTBEAT_INTERVAL: float = 5.0
|
||||||
JOB_WORKER_STALE_RECOVER_INTERVAL: float = 15.0
|
JOB_WORKER_STALE_RECOVER_INTERVAL: float = 15.0
|
||||||
@@ -623,6 +642,79 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
if not self.PYINT_ORBIT_POOL_TXT:
|
if not self.PYINT_ORBIT_POOL_TXT:
|
||||||
object.__setattr__(self, "PYINT_ORBIT_POOL_TXT", self.ORBIT_POOL_ENVI)
|
object.__setattr__(self, "PYINT_ORBIT_POOL_TXT", self.ORBIT_POOL_ENVI)
|
||||||
|
if not self.GAMMA_SBAS_RUNTIME_ID:
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_RUNTIME_ID", "gamma_sbas_runtime_v1")
|
||||||
|
if not self.GAMMA_SBAS_WSL_DISTRO:
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_WSL_DISTRO",
|
||||||
|
self.WSL_DISTRO or self.PYINT_WSL_DISTRO or self.ISCE2_WSL_DISTRO,
|
||||||
|
)
|
||||||
|
if not self.GAMMA_SBAS_PYTHON:
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_PYTHON",
|
||||||
|
self.WSL_SHARED_PYTHON or self.PYINT_WSL_PYTHON or self.ISCE2_PYTHON,
|
||||||
|
)
|
||||||
|
if not self.GAMMA_SBAS_ENV_SCRIPT:
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_ENV_SCRIPT", self.PYINT_GAMMA_ENV_SCRIPT)
|
||||||
|
if not self.GAMMA_SBAS_WORK_ROOT:
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_WORK_ROOT",
|
||||||
|
os.path.join(backend_dir, "runtime", "sbas_insar_production"),
|
||||||
|
)
|
||||||
|
if not self.GAMMA_SBAS_PRODUCT_ROOT:
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_PRODUCT_ROOT",
|
||||||
|
os.path.join(self.TIMESERIES_PRODUCT_DIR, "sbas"),
|
||||||
|
)
|
||||||
|
if not self.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT:
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT",
|
||||||
|
os.path.join(backend_dir, "templates", "gamma_sbas"),
|
||||||
|
)
|
||||||
|
if not self.GAMMA_SBAS_SOURCE_ROOTS:
|
||||||
|
def _local_split_paths(raw: str | None) -> list[str]:
|
||||||
|
items: list[str] = []
|
||||||
|
for part in str(raw or "").replace(";", ",").split(","):
|
||||||
|
text = part.strip().strip('"').strip("'")
|
||||||
|
if text:
|
||||||
|
items.append(text)
|
||||||
|
return items
|
||||||
|
lt1_roots = [
|
||||||
|
item
|
||||||
|
for value in (self.SOURCE_PRODUCT_DIRS, self.MONITOR_RADAR_DIRS, self.INSAR_STORAGE_DIRS)
|
||||||
|
for item in _local_split_paths(value)
|
||||||
|
if "lutan" in item.lower() or "lt1" in item.lower()
|
||||||
|
]
|
||||||
|
lt1_roots = list(dict.fromkeys(lt1_roots))
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_SOURCE_ROOTS", ";".join(lt1_roots) or r"D:\LuTan1_Image_Pool")
|
||||||
|
if not self.GAMMA_SBAS_ORBIT_ROOTS:
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_ORBIT_ROOTS", self.PYINT_ORBIT_POOL_TXT or self.ORBIT_POOL_ENVI)
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_DEFAULT_RLKS", max(1, int(self.GAMMA_SBAS_DEFAULT_RLKS or 8)))
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_DEFAULT_AZLKS", max(1, int(self.GAMMA_SBAS_DEFAULT_AZLKS or 8)))
|
||||||
|
gamma_sbas_mb_mode = int(self.GAMMA_SBAS_DEFAULT_MB_MODE or 0)
|
||||||
|
if gamma_sbas_mb_mode not in {0, 1, 2}:
|
||||||
|
gamma_sbas_mb_mode = 0
|
||||||
|
object.__setattr__(self, "GAMMA_SBAS_DEFAULT_MB_MODE", gamma_sbas_mb_mode)
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW",
|
||||||
|
max(1, int(self.GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW or 16)),
|
||||||
|
)
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_STEP_TIMEOUT_SECONDS",
|
||||||
|
max(60, int(self.GAMMA_SBAS_STEP_TIMEOUT_SECONDS or 43200)),
|
||||||
|
)
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
"GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS",
|
||||||
|
max(self.GAMMA_SBAS_STEP_TIMEOUT_SECONDS, int(self.GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS or 172800)),
|
||||||
|
)
|
||||||
if self.TIMESERIES_ENABLED:
|
if self.TIMESERIES_ENABLED:
|
||||||
if not self.TIMESERIES_WSL_DISTRO:
|
if not self.TIMESERIES_WSL_DISTRO:
|
||||||
object.__setattr__(self, "TIMESERIES_WSL_DISTRO", self.WSL_DISTRO or self.ISCE2_WSL_DISTRO)
|
object.__setattr__(self, "TIMESERIES_WSL_DISTRO", self.WSL_DISTRO or self.ISCE2_WSL_DISTRO)
|
||||||
@@ -755,6 +847,10 @@ class Settings(BaseSettings):
|
|||||||
os.makedirs(settings.PYINT_WORK_ROOT, exist_ok=True)
|
os.makedirs(settings.PYINT_WORK_ROOT, exist_ok=True)
|
||||||
os.makedirs(settings.PYINT_OUTPUT_ROOT, exist_ok=True)
|
os.makedirs(settings.PYINT_OUTPUT_ROOT, exist_ok=True)
|
||||||
os.makedirs(settings.PYINT_DEM_ROOT, exist_ok=True)
|
os.makedirs(settings.PYINT_DEM_ROOT, exist_ok=True)
|
||||||
|
if settings.GAMMA_SBAS_ENABLED:
|
||||||
|
os.makedirs(settings.GAMMA_SBAS_WORK_ROOT, exist_ok=True)
|
||||||
|
os.makedirs(settings.GAMMA_SBAS_PRODUCT_ROOT, exist_ok=True)
|
||||||
|
os.makedirs(settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, exist_ok=True)
|
||||||
if settings.TIMESERIES_ENABLED and settings.TIMESERIES_WORK_ROOT:
|
if settings.TIMESERIES_ENABLED and settings.TIMESERIES_WORK_ROOT:
|
||||||
os.makedirs(settings.TIMESERIES_WORK_ROOT, exist_ok=True)
|
os.makedirs(settings.TIMESERIES_WORK_ROOT, exist_ok=True)
|
||||||
|
|
||||||
@@ -1033,6 +1129,42 @@ def validate_runtime_config() -> dict[str, Any]:
|
|||||||
expect_file=True,
|
expect_file=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.GAMMA_SBAS_ENABLED:
|
||||||
|
if not settings.GAMMA_SBAS_RUNTIME_ID:
|
||||||
|
errors.append("GAMMA_SBAS_ENABLED=true but GAMMA_SBAS_RUNTIME_ID is not configured.")
|
||||||
|
if not settings.GAMMA_SBAS_WSL_DISTRO:
|
||||||
|
errors.append("GAMMA_SBAS_ENABLED=true but GAMMA_SBAS_WSL_DISTRO is not configured.")
|
||||||
|
if not settings.GAMMA_SBAS_PYTHON:
|
||||||
|
errors.append("GAMMA_SBAS_ENABLED=true but GAMMA_SBAS_PYTHON is not configured.")
|
||||||
|
_check_path(
|
||||||
|
label="GAMMA_SBAS_ENV_SCRIPT",
|
||||||
|
value=settings.GAMMA_SBAS_ENV_SCRIPT,
|
||||||
|
errors=errors,
|
||||||
|
warnings=warnings,
|
||||||
|
expect_file=True,
|
||||||
|
)
|
||||||
|
_check_path(
|
||||||
|
label="GAMMA_SBAS_WORK_ROOT",
|
||||||
|
value=settings.GAMMA_SBAS_WORK_ROOT,
|
||||||
|
errors=errors,
|
||||||
|
warnings=warnings,
|
||||||
|
expect_file=False,
|
||||||
|
)
|
||||||
|
_check_path(
|
||||||
|
label="GAMMA_SBAS_PRODUCT_ROOT",
|
||||||
|
value=settings.GAMMA_SBAS_PRODUCT_ROOT,
|
||||||
|
errors=errors,
|
||||||
|
warnings=warnings,
|
||||||
|
expect_file=False,
|
||||||
|
)
|
||||||
|
_check_path(
|
||||||
|
label="GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT",
|
||||||
|
value=settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT,
|
||||||
|
errors=errors,
|
||||||
|
warnings=warnings,
|
||||||
|
expect_file=False,
|
||||||
|
)
|
||||||
|
|
||||||
if settings.ISCE2_ENABLED or settings.PYINT_ENABLED:
|
if settings.ISCE2_ENABLED or settings.PYINT_ENABLED:
|
||||||
info.append(
|
info.append(
|
||||||
"WSL shared runtime: "
|
"WSL shared runtime: "
|
||||||
|
|||||||
@@ -190,6 +190,16 @@ async def lifespan(app: FastAPI):
|
|||||||
)
|
)
|
||||||
if pairing_bootstrap.get("error"):
|
if pairing_bootstrap.get("error"):
|
||||||
print(f">>> [Pairing] Startup bootstrap failed: {pairing_bootstrap['error']}")
|
print(f">>> [Pairing] Startup bootstrap failed: {pairing_bootstrap['error']}")
|
||||||
|
print(
|
||||||
|
">>> [Gamma SBAS] enabled={0} runtime={1} distro={2} python={3} work_root={4} product_root={5}".format(
|
||||||
|
"YES" if settings.GAMMA_SBAS_ENABLED else "NO",
|
||||||
|
settings.GAMMA_SBAS_RUNTIME_ID or "?",
|
||||||
|
settings.GAMMA_SBAS_WSL_DISTRO or "?",
|
||||||
|
settings.GAMMA_SBAS_PYTHON or "?",
|
||||||
|
settings.GAMMA_SBAS_WORK_ROOT or "?",
|
||||||
|
settings.GAMMA_SBAS_PRODUCT_ROOT or "?",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# 当前项目默认 Manual-only 扫描模式,保留调度器关闭状态。
|
# 当前项目默认 Manual-only 扫描模式,保留调度器关闭状态。
|
||||||
# scheduler_manager.start()
|
# scheduler_manager.start()
|
||||||
|
|||||||
@@ -88,6 +88,59 @@ class SbasCoregistrationJobRequest(BaseModel):
|
|||||||
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
|
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasRdcDemRequest(BaseModel):
|
||||||
|
execute: bool = False
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasRdcDemJobRequest(BaseModel):
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasInterferogramsRequest(BaseModel):
|
||||||
|
execute: bool = False
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
azlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
unwrap_threshold: float = Field(default=0.20, ge=0.01, le=0.95)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasInterferogramsJobRequest(BaseModel):
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
azlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
unwrap_threshold: float = Field(default=0.20, ge=0.01, le=0.95)
|
||||||
|
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasIptaTimeseriesRequest(BaseModel):
|
||||||
|
execute: bool = False
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
reference_window: int = Field(default=16, ge=1, le=256)
|
||||||
|
mb_mode: int = Field(default=0, ge=0, le=2)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasIptaTimeseriesJobRequest(BaseModel):
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
reference_window: int = Field(default=16, ge=1, le=256)
|
||||||
|
mb_mode: int = Field(default=0, ge=0, le=2)
|
||||||
|
timeout_seconds: int = Field(default=43200, ge=60, le=172800)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasWorkflowPrepareRequest(BaseModel):
|
||||||
|
force: bool = False
|
||||||
|
rlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
azlks: int = Field(default=8, ge=1, le=64)
|
||||||
|
reference_window: int = Field(default=16, ge=1, le=256)
|
||||||
|
mb_mode: int = Field(default=0, ge=0, le=2)
|
||||||
|
|
||||||
|
|
||||||
|
class SbasWorkflowJobRequest(SbasWorkflowPrepareRequest):
|
||||||
|
from_step: str | None = Field(default=None, max_length=64)
|
||||||
|
to_step: str | None = Field(default=None, max_length=64)
|
||||||
|
only_steps: list[str] | None = None
|
||||||
|
timeout_seconds: int = Field(default=172800, ge=60, le=604800)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/capabilities")
|
@router.get("/capabilities")
|
||||||
async def get_sbas_insar_capabilities():
|
async def get_sbas_insar_capabilities():
|
||||||
return sbas_insar_production_service.get_capabilities()
|
return sbas_insar_production_service.get_capabilities()
|
||||||
@@ -168,6 +221,76 @@ async def get_sbas_insar_run(run_id: str):
|
|||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/workflow", status_code=202)
|
||||||
|
async def prepare_sbas_insar_workflow(run_id: str, request: SbasWorkflowPrepareRequest):
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_workflow,
|
||||||
|
run_id,
|
||||||
|
force=request.force,
|
||||||
|
rlks=request.rlks,
|
||||||
|
azlks=request.azlks,
|
||||||
|
reference_window=request.reference_window,
|
||||||
|
mb_mode=request.mb_mode,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/workflow/jobs", status_code=202)
|
||||||
|
async def submit_sbas_insar_workflow_job(run_id: str, request: SbasWorkflowJobRequest):
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_workflow,
|
||||||
|
run_id,
|
||||||
|
force=request.force,
|
||||||
|
rlks=request.rlks,
|
||||||
|
azlks=request.azlks,
|
||||||
|
reference_window=request.reference_window,
|
||||||
|
mb_mode=request.mb_mode,
|
||||||
|
)
|
||||||
|
from ..services.job_handlers import JOB_TYPE_SBAS_GAMMA_WORKFLOW
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"run_id": run_id,
|
||||||
|
"force": request.force,
|
||||||
|
"rlks": request.rlks,
|
||||||
|
"azlks": request.azlks,
|
||||||
|
"reference_window": request.reference_window,
|
||||||
|
"mb_mode": request.mb_mode,
|
||||||
|
"from_step": request.from_step,
|
||||||
|
"to_step": request.to_step,
|
||||||
|
"only_steps": request.only_steps or [],
|
||||||
|
"timeout_seconds": request.timeout_seconds,
|
||||||
|
}
|
||||||
|
task_id = await task_service.create_task(
|
||||||
|
task_type=JOB_TYPE_SBAS_GAMMA_WORKFLOW,
|
||||||
|
task_name=f"Gamma SBAS Workflow {run_id}",
|
||||||
|
params=payload,
|
||||||
|
)
|
||||||
|
job_id = await job_queue_service.create_job(
|
||||||
|
job_type=JOB_TYPE_SBAS_GAMMA_WORKFLOW,
|
||||||
|
payload=payload,
|
||||||
|
task_id=task_id,
|
||||||
|
max_attempts=1,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"message": "Gamma SBAS workflow job queued.",
|
||||||
|
"run_id": run_id,
|
||||||
|
"task_id": task_id,
|
||||||
|
"job_id": job_id,
|
||||||
|
"job_type": JOB_TYPE_SBAS_GAMMA_WORKFLOW,
|
||||||
|
}
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
message = str(exc)
|
||||||
|
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
|
||||||
|
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.post("/runs/{run_id}/baseline-audit", status_code=202)
|
@router.post("/runs/{run_id}/baseline-audit", status_code=202)
|
||||||
async def run_sbas_insar_baseline_audit(run_id: str, request: SbasBaselineAuditRequest):
|
async def run_sbas_insar_baseline_audit(run_id: str, request: SbasBaselineAuditRequest):
|
||||||
try:
|
try:
|
||||||
@@ -274,6 +397,219 @@ async def submit_sbas_insar_coregistration_job(run_id: str, request: SbasCoregis
|
|||||||
raise HTTPException(status_code=status_code, detail=message) from exc
|
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/rdc-dem", status_code=202)
|
||||||
|
async def prepare_sbas_insar_rdc_dem(run_id: str, request: SbasRdcDemRequest):
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_rdc_dem,
|
||||||
|
run_id,
|
||||||
|
execute=request.execute,
|
||||||
|
rlks=request.rlks,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/rdc-dem/jobs", status_code=202)
|
||||||
|
async def submit_sbas_insar_rdc_dem_job(run_id: str, request: SbasRdcDemJobRequest):
|
||||||
|
try:
|
||||||
|
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||||
|
status = str((run_detail.get("run") or {}).get("status") or "").strip()
|
||||||
|
if status in {"COREGISTRATION_READY", "RDC_DEM_FAILED"}:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_rdc_dem,
|
||||||
|
run_id,
|
||||||
|
execute=False,
|
||||||
|
rlks=request.rlks,
|
||||||
|
)
|
||||||
|
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||||
|
status = str((run_detail.get("run") or {}).get("status") or "").strip()
|
||||||
|
if status not in {"RDC_DEM_SCRIPT_READY", "RDC_DEM_RUNNING"}:
|
||||||
|
raise ValueError(f"run status does not allow RDC DEM job submission: {status}")
|
||||||
|
if status == "RDC_DEM_RUNNING":
|
||||||
|
raise ValueError("RDC DEM is already running for this run")
|
||||||
|
|
||||||
|
from ..services.job_handlers import JOB_TYPE_SBAS_RDC_DEM
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"run_id": run_id,
|
||||||
|
"rlks": request.rlks,
|
||||||
|
"timeout_seconds": request.timeout_seconds,
|
||||||
|
}
|
||||||
|
task_id = await task_service.create_task(
|
||||||
|
task_type=JOB_TYPE_SBAS_RDC_DEM,
|
||||||
|
task_name=f"SBAS-InSAR RDC DEM {run_id}",
|
||||||
|
params=payload,
|
||||||
|
)
|
||||||
|
job_id = await job_queue_service.create_job(
|
||||||
|
job_type=JOB_TYPE_SBAS_RDC_DEM,
|
||||||
|
payload=payload,
|
||||||
|
task_id=task_id,
|
||||||
|
max_attempts=1,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"message": "SBAS-InSAR RDC DEM job queued.",
|
||||||
|
"run_id": run_id,
|
||||||
|
"task_id": task_id,
|
||||||
|
"job_id": job_id,
|
||||||
|
"job_type": JOB_TYPE_SBAS_RDC_DEM,
|
||||||
|
}
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
message = str(exc)
|
||||||
|
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
|
||||||
|
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/interferograms", status_code=202)
|
||||||
|
async def prepare_sbas_insar_interferograms(run_id: str, request: SbasInterferogramsRequest):
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_interferograms,
|
||||||
|
run_id,
|
||||||
|
execute=request.execute,
|
||||||
|
rlks=request.rlks,
|
||||||
|
azlks=request.azlks,
|
||||||
|
unwrap_threshold=request.unwrap_threshold,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/interferograms/jobs", status_code=202)
|
||||||
|
async def submit_sbas_insar_interferograms_job(run_id: str, request: SbasInterferogramsJobRequest):
|
||||||
|
try:
|
||||||
|
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||||
|
status = str((run_detail.get("run") or {}).get("status") or "").strip()
|
||||||
|
if status in {"RDC_DEM_READY", "INTERFEROGRAMS_FAILED"}:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_interferograms,
|
||||||
|
run_id,
|
||||||
|
execute=False,
|
||||||
|
rlks=request.rlks,
|
||||||
|
azlks=request.azlks,
|
||||||
|
unwrap_threshold=request.unwrap_threshold,
|
||||||
|
)
|
||||||
|
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||||
|
status = str((run_detail.get("run") or {}).get("status") or "").strip()
|
||||||
|
if status not in {"INTERFEROGRAMS_SCRIPT_READY", "INTERFEROGRAMS_RUNNING"}:
|
||||||
|
raise ValueError(f"run status does not allow interferogram job submission: {status}")
|
||||||
|
if status == "INTERFEROGRAMS_RUNNING":
|
||||||
|
raise ValueError("interferograms are already running for this run")
|
||||||
|
|
||||||
|
from ..services.job_handlers import JOB_TYPE_SBAS_INTERFEROGRAMS
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"run_id": run_id,
|
||||||
|
"rlks": request.rlks,
|
||||||
|
"azlks": request.azlks,
|
||||||
|
"unwrap_threshold": request.unwrap_threshold,
|
||||||
|
"timeout_seconds": request.timeout_seconds,
|
||||||
|
}
|
||||||
|
task_id = await task_service.create_task(
|
||||||
|
task_type=JOB_TYPE_SBAS_INTERFEROGRAMS,
|
||||||
|
task_name=f"SBAS-InSAR Interferograms {run_id}",
|
||||||
|
params=payload,
|
||||||
|
)
|
||||||
|
job_id = await job_queue_service.create_job(
|
||||||
|
job_type=JOB_TYPE_SBAS_INTERFEROGRAMS,
|
||||||
|
payload=payload,
|
||||||
|
task_id=task_id,
|
||||||
|
max_attempts=1,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"message": "SBAS-InSAR interferogram job queued.",
|
||||||
|
"run_id": run_id,
|
||||||
|
"task_id": task_id,
|
||||||
|
"job_id": job_id,
|
||||||
|
"job_type": JOB_TYPE_SBAS_INTERFEROGRAMS,
|
||||||
|
}
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
message = str(exc)
|
||||||
|
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
|
||||||
|
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/ipta-timeseries", status_code=202)
|
||||||
|
async def prepare_sbas_insar_ipta_timeseries(run_id: str, request: SbasIptaTimeseriesRequest):
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_ipta_timeseries,
|
||||||
|
run_id,
|
||||||
|
execute=request.execute,
|
||||||
|
rlks=request.rlks,
|
||||||
|
reference_window=request.reference_window,
|
||||||
|
mb_mode=request.mb_mode,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/runs/{run_id}/ipta-timeseries/jobs", status_code=202)
|
||||||
|
async def submit_sbas_insar_ipta_timeseries_job(run_id: str, request: SbasIptaTimeseriesJobRequest):
|
||||||
|
try:
|
||||||
|
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||||
|
status = str((run_detail.get("run") or {}).get("status") or "").strip()
|
||||||
|
if status in {"INTERFEROGRAMS_READY", "IPTA_TIMESERIES_FAILED"}:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.prepare_ipta_timeseries,
|
||||||
|
run_id,
|
||||||
|
execute=False,
|
||||||
|
rlks=request.rlks,
|
||||||
|
reference_window=request.reference_window,
|
||||||
|
mb_mode=request.mb_mode,
|
||||||
|
)
|
||||||
|
run_detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||||
|
status = str((run_detail.get("run") or {}).get("status") or "").strip()
|
||||||
|
if status not in {"IPTA_TIMESERIES_SCRIPT_READY", "IPTA_TIMESERIES_RUNNING"}:
|
||||||
|
raise ValueError(f"run status does not allow IPTA time-series job submission: {status}")
|
||||||
|
if status == "IPTA_TIMESERIES_RUNNING":
|
||||||
|
raise ValueError("IPTA time-series is already running for this run")
|
||||||
|
|
||||||
|
from ..services.job_handlers import JOB_TYPE_SBAS_IPTA_TIMESERIES
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"run_id": run_id,
|
||||||
|
"rlks": request.rlks,
|
||||||
|
"reference_window": request.reference_window,
|
||||||
|
"mb_mode": request.mb_mode,
|
||||||
|
"timeout_seconds": request.timeout_seconds,
|
||||||
|
}
|
||||||
|
task_id = await task_service.create_task(
|
||||||
|
task_type=JOB_TYPE_SBAS_IPTA_TIMESERIES,
|
||||||
|
task_name=f"SBAS-InSAR IPTA Timeseries {run_id}",
|
||||||
|
params=payload,
|
||||||
|
)
|
||||||
|
job_id = await job_queue_service.create_job(
|
||||||
|
job_type=JOB_TYPE_SBAS_IPTA_TIMESERIES,
|
||||||
|
payload=payload,
|
||||||
|
task_id=task_id,
|
||||||
|
max_attempts=1,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"message": "SBAS-InSAR IPTA time-series job queued.",
|
||||||
|
"run_id": run_id,
|
||||||
|
"task_id": task_id,
|
||||||
|
"job_id": job_id,
|
||||||
|
"job_type": JOB_TYPE_SBAS_IPTA_TIMESERIES,
|
||||||
|
}
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
message = str(exc)
|
||||||
|
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() else 400
|
||||||
|
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.get("/runs/{run_id}/artifacts/{relative_path:path}")
|
@router.get("/runs/{run_id}/artifacts/{relative_path:path}")
|
||||||
async def get_sbas_insar_run_artifact(run_id: str, relative_path: str):
|
async def get_sbas_insar_run_artifact(run_id: str, relative_path: str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1272,6 +1272,7 @@ async def _check_wsl_runtime() -> Dict[str, Any]:
|
|||||||
required_by_engine = {
|
required_by_engine = {
|
||||||
"isce2": bool(settings.ISCE2_ENABLED or settings.TIMESERIES_ENABLED),
|
"isce2": bool(settings.ISCE2_ENABLED or settings.TIMESERIES_ENABLED),
|
||||||
"pyint": bool(settings.PYINT_ENABLED),
|
"pyint": bool(settings.PYINT_ENABLED),
|
||||||
|
"gamma": bool(settings.GAMMA_SBAS_ENABLED),
|
||||||
}
|
}
|
||||||
for runtime in wsl_runtime_registry.runtimes.values():
|
for runtime in wsl_runtime_registry.runtimes.values():
|
||||||
required = bool(required_by_engine.get(runtime.engine_code, False))
|
required = bool(required_by_engine.get(runtime.engine_code, False))
|
||||||
@@ -1285,6 +1286,9 @@ async def _check_wsl_runtime() -> Dict[str, Any]:
|
|||||||
distro_matches_shared = str(runtime.distro or "").strip() == str(
|
distro_matches_shared = str(runtime.distro or "").strip() == str(
|
||||||
wsl_runtime_registry.shared_distro or ""
|
wsl_runtime_registry.shared_distro or ""
|
||||||
).strip()
|
).strip()
|
||||||
|
if runtime.engine_code == "gamma":
|
||||||
|
python_matches_shared = bool(str(runtime.python_path or "").strip())
|
||||||
|
distro_matches_shared = bool(str(runtime.distro or "").strip())
|
||||||
runtime_ok = runner_exists and python_matches_shared and distro_matches_shared
|
runtime_ok = runner_exists and python_matches_shared and distro_matches_shared
|
||||||
if env_profile_exists is False and required:
|
if env_profile_exists is False and required:
|
||||||
runtime_ok = False
|
runtime_ok = False
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ JOB_TYPE_REBUILD_DINSAR_CATALOG = "REBUILD_DINSAR_CATALOG"
|
|||||||
JOB_TYPE_REBUILD_PSINSAR_CATALOG = "REBUILD_PSINSAR_CATALOG"
|
JOB_TYPE_REBUILD_PSINSAR_CATALOG = "REBUILD_PSINSAR_CATALOG"
|
||||||
JOB_TYPE_SCAN_ASSET_INVENTORY = "SCAN_ASSET_INVENTORY"
|
JOB_TYPE_SCAN_ASSET_INVENTORY = "SCAN_ASSET_INVENTORY"
|
||||||
JOB_TYPE_SBAS_COREGISTRATION = "SBAS_COREGISTRATION"
|
JOB_TYPE_SBAS_COREGISTRATION = "SBAS_COREGISTRATION"
|
||||||
|
JOB_TYPE_SBAS_RDC_DEM = "SBAS_RDC_DEM"
|
||||||
|
JOB_TYPE_SBAS_INTERFEROGRAMS = "SBAS_INTERFEROGRAMS"
|
||||||
|
JOB_TYPE_SBAS_IPTA_TIMESERIES = "SBAS_IPTA_TIMESERIES"
|
||||||
|
JOB_TYPE_SBAS_GAMMA_WORKFLOW = "SBAS_GAMMA_WORKFLOW"
|
||||||
|
|
||||||
COPY_ALLOWED_STATUSES = {"PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"}
|
COPY_ALLOWED_STATUSES = {"PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"}
|
||||||
|
|
||||||
@@ -4263,6 +4267,346 @@ async def _handle_sbas_coregistration(job: SystemJobORM) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_sbas_rdc_dem(job: SystemJobORM) -> None:
|
||||||
|
if not job.task_id:
|
||||||
|
raise ValueError("SBAS_RDC_DEM requires task_id for progress tracking.")
|
||||||
|
payload = job.payload or {}
|
||||||
|
run_id = str(payload.get("run_id") or "").strip()
|
||||||
|
if not run_id:
|
||||||
|
raise ValueError("SBAS_RDC_DEM requires run_id payload.")
|
||||||
|
|
||||||
|
rlks = _normalize_positive_int(payload.get("rlks")) or 8
|
||||||
|
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or 43200
|
||||||
|
|
||||||
|
await task_service.start_task(job.task_id, message="正在执行 SBAS-InSAR Gamma RDC DEM...")
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=5,
|
||||||
|
message=f"准备运行 Gamma gc_map1/gc_map_fine: run_id={run_id}",
|
||||||
|
)
|
||||||
|
await task_service.add_log(
|
||||||
|
job.task_id,
|
||||||
|
"INFO",
|
||||||
|
f"SBAS RDC DEM queued: run_id={run_id}, rlks={rlks}, timeout={timeout_seconds}s",
|
||||||
|
)
|
||||||
|
|
||||||
|
from .sbas_insar_production_service import sbas_insar_production_service
|
||||||
|
|
||||||
|
async def _task_keepalive() -> None:
|
||||||
|
progress = 12
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
progress = min(88, progress + 2)
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=progress,
|
||||||
|
message=f"Gamma RDC DEM 仍在运行: run_id={run_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner_task = asyncio.create_task(
|
||||||
|
asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.execute_rdc_dem,
|
||||||
|
run_id,
|
||||||
|
rlks=rlks,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keepalive_task = asyncio.create_task(_task_keepalive())
|
||||||
|
try:
|
||||||
|
result = await runner_task
|
||||||
|
finally:
|
||||||
|
keepalive_task.cancel()
|
||||||
|
try:
|
||||||
|
await keepalive_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
manifest = result.get("manifest") or {}
|
||||||
|
run = result.get("run") or {}
|
||||||
|
summary = (manifest.get("rdc_dem") or {}).get("summary") or {}
|
||||||
|
status = str(run.get("status") or manifest.get("status") or "").strip()
|
||||||
|
if status != "RDC_DEM_READY":
|
||||||
|
raise RuntimeError(
|
||||||
|
"SBAS RDC DEM failed: "
|
||||||
|
f"status={status or 'UNKNOWN'}, "
|
||||||
|
f"missing_outputs={summary.get('missing_outputs') or []}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
status="COMPLETED",
|
||||||
|
progress=100,
|
||||||
|
message=(
|
||||||
|
"SBAS-InSAR RDC DEM 完成: "
|
||||||
|
f"rdc_dem={((summary.get('outputs') or {}).get('rdc_dem') or {}).get('path') or '-'}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_sbas_interferograms(job: SystemJobORM) -> None:
|
||||||
|
if not job.task_id:
|
||||||
|
raise ValueError("SBAS_INTERFEROGRAMS requires task_id for progress tracking.")
|
||||||
|
payload = job.payload or {}
|
||||||
|
run_id = str(payload.get("run_id") or "").strip()
|
||||||
|
if not run_id:
|
||||||
|
raise ValueError("SBAS_INTERFEROGRAMS requires run_id payload.")
|
||||||
|
|
||||||
|
rlks = _normalize_positive_int(payload.get("rlks")) or 8
|
||||||
|
azlks = _normalize_positive_int(payload.get("azlks")) or 8
|
||||||
|
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or 43200
|
||||||
|
try:
|
||||||
|
unwrap_threshold = float(payload.get("unwrap_threshold") or 0.20)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
unwrap_threshold = 0.20
|
||||||
|
|
||||||
|
await task_service.start_task(job.task_id, message="正在执行 SBAS-InSAR Gamma 差分干涉图...")
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=5,
|
||||||
|
message=f"准备运行 Gamma phase_sim_orb/SLC_diff_intf/mcf: run_id={run_id}",
|
||||||
|
)
|
||||||
|
await task_service.add_log(
|
||||||
|
job.task_id,
|
||||||
|
"INFO",
|
||||||
|
(
|
||||||
|
f"SBAS interferograms queued: run_id={run_id}, rlks={rlks}, azlks={azlks}, "
|
||||||
|
f"unwrap_threshold={unwrap_threshold}, timeout={timeout_seconds}s"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
from .sbas_insar_production_service import sbas_insar_production_service
|
||||||
|
|
||||||
|
async def _task_keepalive() -> None:
|
||||||
|
progress = 12
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
progress = min(88, progress + 2)
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=progress,
|
||||||
|
message=f"Gamma 差分干涉图仍在运行: run_id={run_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner_task = asyncio.create_task(
|
||||||
|
asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.execute_interferograms,
|
||||||
|
run_id,
|
||||||
|
rlks=rlks,
|
||||||
|
azlks=azlks,
|
||||||
|
unwrap_threshold=unwrap_threshold,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keepalive_task = asyncio.create_task(_task_keepalive())
|
||||||
|
try:
|
||||||
|
result = await runner_task
|
||||||
|
finally:
|
||||||
|
keepalive_task.cancel()
|
||||||
|
try:
|
||||||
|
await keepalive_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
manifest = result.get("manifest") or {}
|
||||||
|
run = result.get("run") or {}
|
||||||
|
summary = (manifest.get("interferograms") or {}).get("summary") or {}
|
||||||
|
status = str(run.get("status") or manifest.get("status") or "").strip()
|
||||||
|
if status != "INTERFEROGRAMS_READY":
|
||||||
|
raise RuntimeError(
|
||||||
|
"SBAS interferograms failed: "
|
||||||
|
f"status={status or 'UNKNOWN'}, "
|
||||||
|
f"missing_pairs={summary.get('missing_pairs') or []}, "
|
||||||
|
f"missing_tabs={summary.get('missing_tabs') or []}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
status="COMPLETED",
|
||||||
|
progress=100,
|
||||||
|
message=(
|
||||||
|
"SBAS-InSAR 差分干涉图完成: "
|
||||||
|
f"{summary.get('ready_pair_count', 0)}/{summary.get('pair_count', 0)} pairs ready"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_sbas_ipta_timeseries(job: SystemJobORM) -> None:
|
||||||
|
if not job.task_id:
|
||||||
|
raise ValueError("SBAS_IPTA_TIMESERIES requires task_id for progress tracking.")
|
||||||
|
payload = job.payload or {}
|
||||||
|
run_id = str(payload.get("run_id") or "").strip()
|
||||||
|
if not run_id:
|
||||||
|
raise ValueError("SBAS_IPTA_TIMESERIES requires run_id payload.")
|
||||||
|
|
||||||
|
rlks = _normalize_positive_int(payload.get("rlks")) or 8
|
||||||
|
reference_window = _normalize_positive_int(payload.get("reference_window")) or 16
|
||||||
|
try:
|
||||||
|
mb_mode = int(payload.get("mb_mode") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
mb_mode = 0
|
||||||
|
if mb_mode not in {0, 1, 2}:
|
||||||
|
mb_mode = 0
|
||||||
|
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or 43200
|
||||||
|
|
||||||
|
await task_service.start_task(job.task_id, message="Running SBAS-InSAR Gamma IPTA time-series inversion...")
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=5,
|
||||||
|
message=f"Preparing Gamma mb/ts_rate: run_id={run_id}",
|
||||||
|
)
|
||||||
|
await task_service.add_log(
|
||||||
|
job.task_id,
|
||||||
|
"INFO",
|
||||||
|
(
|
||||||
|
f"SBAS IPTA time-series queued: run_id={run_id}, rlks={rlks}, "
|
||||||
|
f"reference_window={reference_window}, mb_mode={mb_mode}, "
|
||||||
|
f"timeout={timeout_seconds}s"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
from .sbas_insar_production_service import sbas_insar_production_service
|
||||||
|
|
||||||
|
async def _task_keepalive() -> None:
|
||||||
|
progress = 12
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
progress = min(88, progress + 2)
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=progress,
|
||||||
|
message=f"Gamma IPTA mb/ts_rate still running: run_id={run_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner_task = asyncio.create_task(
|
||||||
|
asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.execute_ipta_timeseries,
|
||||||
|
run_id,
|
||||||
|
rlks=rlks,
|
||||||
|
reference_window=reference_window,
|
||||||
|
mb_mode=mb_mode,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keepalive_task = asyncio.create_task(_task_keepalive())
|
||||||
|
try:
|
||||||
|
result = await runner_task
|
||||||
|
finally:
|
||||||
|
keepalive_task.cancel()
|
||||||
|
try:
|
||||||
|
await keepalive_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
manifest = result.get("manifest") or {}
|
||||||
|
run = result.get("run") or {}
|
||||||
|
summary = (manifest.get("ipta_timeseries") or {}).get("summary") or {}
|
||||||
|
status = str(run.get("status") or manifest.get("status") or "").strip()
|
||||||
|
if status != "IPTA_TIMESERIES_READY":
|
||||||
|
raise RuntimeError(
|
||||||
|
"SBAS IPTA time-series failed: "
|
||||||
|
f"status={status or 'UNKNOWN'}, "
|
||||||
|
f"missing_outputs={summary.get('missing_outputs') or []}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
status="COMPLETED",
|
||||||
|
progress=100,
|
||||||
|
message=(
|
||||||
|
"SBAS-InSAR IPTA time-series complete: "
|
||||||
|
f"ts_rate={((summary.get('outputs') or {}).get('ts_rate') or {}).get('path') or '-'}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_sbas_gamma_workflow(job: SystemJobORM) -> None:
|
||||||
|
if not job.task_id:
|
||||||
|
raise ValueError("SBAS_GAMMA_WORKFLOW requires task_id for progress tracking.")
|
||||||
|
payload = job.payload or {}
|
||||||
|
run_id = str(payload.get("run_id") or "").strip()
|
||||||
|
if not run_id:
|
||||||
|
raise ValueError("SBAS_GAMMA_WORKFLOW requires run_id payload.")
|
||||||
|
|
||||||
|
from_step = str(payload.get("from_step") or "").strip() or None
|
||||||
|
to_step = str(payload.get("to_step") or "").strip() or None
|
||||||
|
only_steps_raw = payload.get("only_steps") or []
|
||||||
|
only_steps = [str(item).strip() for item in only_steps_raw if str(item).strip()] if isinstance(only_steps_raw, list) else []
|
||||||
|
force = bool(payload.get("force", False))
|
||||||
|
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or int(settings.GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS)
|
||||||
|
|
||||||
|
await task_service.start_task(job.task_id, message="Running Gamma SBAS expert workflow...")
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=5,
|
||||||
|
message=f"Preparing Gamma SBAS manifest runner: run_id={run_id}",
|
||||||
|
)
|
||||||
|
await task_service.add_log(
|
||||||
|
job.task_id,
|
||||||
|
"INFO",
|
||||||
|
(
|
||||||
|
f"SBAS Gamma workflow queued: run_id={run_id}, from={from_step or '-'}, "
|
||||||
|
f"to={to_step or '-'}, only={only_steps or '-'}, force={force}, timeout={timeout_seconds}s"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
from .sbas_insar_production_service import sbas_insar_production_service
|
||||||
|
|
||||||
|
async def _task_keepalive() -> None:
|
||||||
|
progress = 10
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
progress = min(92, progress + 2)
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
progress=progress,
|
||||||
|
message="Gamma SBAS workflow is still running...",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner_task = asyncio.create_task(
|
||||||
|
asyncio.to_thread(
|
||||||
|
sbas_insar_production_service.execute_workflow,
|
||||||
|
run_id,
|
||||||
|
from_step=from_step,
|
||||||
|
to_step=to_step,
|
||||||
|
only_steps=only_steps,
|
||||||
|
force=force,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keepalive_task = asyncio.create_task(_task_keepalive())
|
||||||
|
try:
|
||||||
|
result = await runner_task
|
||||||
|
finally:
|
||||||
|
keepalive_task.cancel()
|
||||||
|
try:
|
||||||
|
await keepalive_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
run = result.get("run") or {}
|
||||||
|
manifest = result.get("manifest") or {}
|
||||||
|
workflow = manifest.get("workflow") or {}
|
||||||
|
summary = workflow.get("summary") or {}
|
||||||
|
status = str(run.get("status") or manifest.get("status") or "").strip()
|
||||||
|
if status not in {"WORKFLOW_COMPLETED", "WORKFLOW_PARTIAL"}:
|
||||||
|
raise RuntimeError(
|
||||||
|
"SBAS Gamma workflow failed: "
|
||||||
|
f"status={status or 'UNKNOWN'}, failed_steps={summary.get('failed_count') or 0}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await task_service.update_task(
|
||||||
|
job.task_id,
|
||||||
|
status="COMPLETED",
|
||||||
|
progress=100,
|
||||||
|
message=(
|
||||||
|
f"Gamma SBAS expert workflow {status.lower()}: "
|
||||||
|
f"completed={summary.get('completed_count', 0)}, "
|
||||||
|
f"skipped={summary.get('skipped_count', 0)}, "
|
||||||
|
f"planned={summary.get('planned_count', 0)}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_HANDLERS = {
|
_HANDLERS = {
|
||||||
JOB_TYPE_SCAN_DATA: _handle_scan_data,
|
JOB_TYPE_SCAN_DATA: _handle_scan_data,
|
||||||
JOB_TYPE_SCAN_ASSET_INVENTORY: _handle_scan_asset_inventory,
|
JOB_TYPE_SCAN_ASSET_INVENTORY: _handle_scan_asset_inventory,
|
||||||
@@ -4301,6 +4645,10 @@ _HANDLERS = {
|
|||||||
JOB_TYPE_GF3_UNPACK: _handle_gf3_unpack,
|
JOB_TYPE_GF3_UNPACK: _handle_gf3_unpack,
|
||||||
JOB_TYPE_GF3_BATCH_PROCESS: _handle_gf3_batch_process,
|
JOB_TYPE_GF3_BATCH_PROCESS: _handle_gf3_batch_process,
|
||||||
JOB_TYPE_SBAS_COREGISTRATION: _handle_sbas_coregistration,
|
JOB_TYPE_SBAS_COREGISTRATION: _handle_sbas_coregistration,
|
||||||
|
JOB_TYPE_SBAS_RDC_DEM: _handle_sbas_rdc_dem,
|
||||||
|
JOB_TYPE_SBAS_INTERFEROGRAMS: _handle_sbas_interferograms,
|
||||||
|
JOB_TYPE_SBAS_IPTA_TIMESERIES: _handle_sbas_ipta_timeseries,
|
||||||
|
JOB_TYPE_SBAS_GAMMA_WORKFLOW: _handle_sbas_gamma_workflow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,7 @@ def build_wsl_runtime_registry() -> WslRuntimeRegistry:
|
|||||||
|
|
||||||
isce2_runner_windows = _project_file_windows("deploy", "wsl", "runners", "isce2_runner.py")
|
isce2_runner_windows = _project_file_windows("deploy", "wsl", "runners", "isce2_runner.py")
|
||||||
gamma_runner_windows = _project_file_windows("deploy", "wsl", "runners", "gamma_pyint_runner.py")
|
gamma_runner_windows = _project_file_windows("deploy", "wsl", "runners", "gamma_pyint_runner.py")
|
||||||
|
gamma_sbas_runner_windows = _project_file_windows("deploy", "wsl", "runners", "gamma_sbas_runner.py")
|
||||||
gamma_profile_windows = _project_file_windows("deploy", "wsl", "profiles", "gamma_env.sh")
|
gamma_profile_windows = _project_file_windows("deploy", "wsl", "profiles", "gamma_env.sh")
|
||||||
|
|
||||||
runtimes = {
|
runtimes = {
|
||||||
@@ -151,6 +152,33 @@ def build_wsl_runtime_registry() -> WslRuntimeRegistry:
|
|||||||
"legacy_profile_env_var": "PYINT_GAMMA_ENV_SCRIPT",
|
"legacy_profile_env_var": "PYINT_GAMMA_ENV_SCRIPT",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
settings.GAMMA_SBAS_RUNTIME_ID: WslRuntimeDefinition(
|
||||||
|
runtime_id=settings.GAMMA_SBAS_RUNTIME_ID,
|
||||||
|
engine_code="gamma",
|
||||||
|
display_name="Gamma SBAS Runtime V1",
|
||||||
|
distro=str(settings.GAMMA_SBAS_WSL_DISTRO or shared_distro).strip() or shared_distro,
|
||||||
|
conda_env_name=shared_conda_env,
|
||||||
|
python_path=str(settings.GAMMA_SBAS_PYTHON or shared_python_path).strip() or shared_python_path,
|
||||||
|
runner_path_windows=gamma_sbas_runner_windows,
|
||||||
|
runner_path_wsl=_windows_path_to_wsl_mount(gamma_sbas_runner_windows),
|
||||||
|
allowed_operations=("lt1_gamma_sbas_workflow", "lt1_gamma_sbas_step"),
|
||||||
|
env_profile_path_windows=str(settings.GAMMA_SBAS_ENV_SCRIPT or gamma_profile_windows).strip(),
|
||||||
|
env_profile_path_wsl=_windows_path_to_wsl_mount(
|
||||||
|
str(settings.GAMMA_SBAS_ENV_SCRIPT or gamma_profile_windows).strip()
|
||||||
|
),
|
||||||
|
metadata_json={
|
||||||
|
"shared_runtime": True,
|
||||||
|
"workflow_code": "sbas_insar",
|
||||||
|
"processor_code": "gamma_ipta_sbas",
|
||||||
|
"env_vars": {
|
||||||
|
"runtime_id": "GAMMA_SBAS_RUNTIME_ID",
|
||||||
|
"python": "GAMMA_SBAS_PYTHON",
|
||||||
|
"env_profile": "GAMMA_SBAS_ENV_SCRIPT",
|
||||||
|
"work_root": "GAMMA_SBAS_WORK_ROOT",
|
||||||
|
"product_root": "GAMMA_SBAS_PRODUCT_ROOT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return WslRuntimeRegistry(
|
return WslRuntimeRegistry(
|
||||||
|
|||||||
@@ -0,0 +1,369 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def read_gamma_value(path: Path, key: str) -> str:
|
||||||
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if parts and parts[0].rstrip(":") == key.rstrip(":"):
|
||||||
|
return parts[1]
|
||||||
|
raise KeyError(f"{key} not found in {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_float32(path: Path, shape: tuple[int, int] | None = None) -> np.ndarray:
|
||||||
|
data = np.fromfile(path, dtype=">f4")
|
||||||
|
if shape is not None:
|
||||||
|
data = data.reshape(shape)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def read_float32_pixel(path: Path, width: int, x: int, y: int) -> float:
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
handle.seek((y * width + x) * 4)
|
||||||
|
chunk = handle.read(4)
|
||||||
|
if len(chunk) != 4:
|
||||||
|
return float("nan")
|
||||||
|
return float(struct.unpack(">f", chunk)[0])
|
||||||
|
|
||||||
|
|
||||||
|
def write_scaled_float32(input_path: Path, output_path: Path, scale: float) -> None:
|
||||||
|
data = np.fromfile(input_path, dtype=">f4")
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
(data * float(scale)).astype(">f4", copy=False).tofile(output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_auto_point(rate: np.ndarray, sigma: np.ndarray) -> tuple[int, int]:
|
||||||
|
lines, width = rate.shape
|
||||||
|
yy, xx = np.indices(rate.shape)
|
||||||
|
edge_mask = (
|
||||||
|
(xx > width * 0.1)
|
||||||
|
& (xx < width * 0.9)
|
||||||
|
& (yy > lines * 0.1)
|
||||||
|
& (yy < lines * 0.9)
|
||||||
|
)
|
||||||
|
finite = np.isfinite(rate) & np.isfinite(sigma)
|
||||||
|
valid = finite & edge_mask & (rate != 0.0) & (sigma > 0.0)
|
||||||
|
if not valid.any():
|
||||||
|
raise RuntimeError("No valid pixels available for monitor point selection")
|
||||||
|
|
||||||
|
abs_rate = np.abs(rate[valid])
|
||||||
|
sig = sigma[valid]
|
||||||
|
rate_min = np.percentile(abs_rate, 85)
|
||||||
|
rate_max = np.percentile(abs_rate, 99)
|
||||||
|
sigma_max = np.percentile(sig, 40)
|
||||||
|
candidate = valid & (np.abs(rate) >= rate_min) & (np.abs(rate) <= rate_max) & (sigma <= sigma_max)
|
||||||
|
if not candidate.any():
|
||||||
|
candidate = valid
|
||||||
|
|
||||||
|
score = np.zeros(rate.shape, dtype=np.float32)
|
||||||
|
score[candidate] = np.abs(rate[candidate]) / (sigma[candidate] + 1.0e-6)
|
||||||
|
y, x = np.unravel_index(int(np.argmax(score)), rate.shape)
|
||||||
|
return int(x), int(y)
|
||||||
|
|
||||||
|
|
||||||
|
def dem_grid(dem_par: Path) -> dict[str, float | int]:
|
||||||
|
return {
|
||||||
|
"width": int(read_gamma_value(dem_par, "width")),
|
||||||
|
"nlines": int(read_gamma_value(dem_par, "nlines")),
|
||||||
|
"corner_lon": float(read_gamma_value(dem_par, "corner_lon")),
|
||||||
|
"corner_lat": float(read_gamma_value(dem_par, "corner_lat")),
|
||||||
|
"post_lon": float(read_gamma_value(dem_par, "post_lon")),
|
||||||
|
"post_lat": float(read_gamma_value(dem_par, "post_lat")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def radar_to_lonlat(x: int, y: int, dem_par: Path, lookup: Path) -> tuple[float | None, float | None]:
|
||||||
|
grid = dem_grid(dem_par)
|
||||||
|
width = int(grid["width"])
|
||||||
|
lines = int(grid["nlines"])
|
||||||
|
lut = np.fromfile(lookup, dtype=">c8").reshape((lines, width))
|
||||||
|
rng = lut.real
|
||||||
|
az = lut.imag
|
||||||
|
valid = np.isfinite(rng) & np.isfinite(az) & (rng > 0.0) & (az > 0.0)
|
||||||
|
if not valid.any():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
distance = np.full(rng.shape, np.inf, dtype=np.float32)
|
||||||
|
distance[valid] = (rng[valid] - float(x)) ** 2 + (az[valid] - float(y)) ** 2
|
||||||
|
gy, gx = np.unravel_index(int(np.argmin(distance)), distance.shape)
|
||||||
|
lon = float(grid["corner_lon"]) + (gx + 0.5) * float(grid["post_lon"])
|
||||||
|
lat = float(grid["corner_lat"]) + (gy + 0.5) * float(grid["post_lat"])
|
||||||
|
return float(lon), float(lat)
|
||||||
|
|
||||||
|
|
||||||
|
def lonlat_to_radar(lon: float, lat: float, dem_par: Path, lookup: Path) -> tuple[int, int]:
|
||||||
|
grid = dem_grid(dem_par)
|
||||||
|
width = int(grid["width"])
|
||||||
|
lines = int(grid["nlines"])
|
||||||
|
gx = int(round((lon - float(grid["corner_lon"])) / float(grid["post_lon"]) - 0.5))
|
||||||
|
gy = int(round((lat - float(grid["corner_lat"])) / float(grid["post_lat"]) - 0.5))
|
||||||
|
gx = max(0, min(width - 1, gx))
|
||||||
|
gy = max(0, min(lines - 1, gy))
|
||||||
|
lut = np.fromfile(lookup, dtype=">c8").reshape((lines, width))
|
||||||
|
value = lut[gy, gx]
|
||||||
|
if not (np.isfinite(value.real) and np.isfinite(value.imag) and value.real > 0 and value.imag > 0):
|
||||||
|
raise RuntimeError(f"manual lon/lat maps to invalid lookup pixel: lon={lon}, lat={lat}")
|
||||||
|
return int(round(float(value.real))), int(round(float(value.imag)))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_point_id(value: str, fallback: str) -> str:
|
||||||
|
text = str(value or "").strip() or fallback
|
||||||
|
text = re.sub(r"[^A-Za-z0-9_.-]+", "_", text)[:64]
|
||||||
|
return text or fallback
|
||||||
|
|
||||||
|
|
||||||
|
def load_diff_files(timeseries_dir: Path) -> list[Path]:
|
||||||
|
tab = timeseries_dir / "diff_ts.tab"
|
||||||
|
if tab.is_file():
|
||||||
|
rows = [line.strip() for line in tab.read_text(encoding="utf-8", errors="replace").splitlines() if line.strip()]
|
||||||
|
files = [Path(row.split()[0]) for row in rows if row.split()]
|
||||||
|
files = [path for path in files if path.is_file()]
|
||||||
|
if files:
|
||||||
|
return files
|
||||||
|
return sorted(timeseries_dir.glob("diff_ts_*.diff"))
|
||||||
|
|
||||||
|
|
||||||
|
def point_records(
|
||||||
|
diff_files: list[Path],
|
||||||
|
*,
|
||||||
|
dates: list[str],
|
||||||
|
width: int,
|
||||||
|
x: int,
|
||||||
|
y: int,
|
||||||
|
scale_mm: float,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
records: list[dict[str, Any]] = []
|
||||||
|
for index, path in enumerate(diff_files):
|
||||||
|
phase = read_float32_pixel(path, width, x, y)
|
||||||
|
away_mm = float(phase * scale_mm) if math.isfinite(phase) else float("nan")
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"date": dates[index] if index < len(dates) else f"step_{index + 1:03d}",
|
||||||
|
"phase_rad": float(phase),
|
||||||
|
"los_away_mm": away_mm,
|
||||||
|
"los_toward_mm": -away_mm if math.isfinite(away_mm) else float("nan"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def write_point_outputs(
|
||||||
|
point_dir: Path,
|
||||||
|
point: dict[str, Any],
|
||||||
|
*,
|
||||||
|
records: list[dict[str, Any]],
|
||||||
|
rate_value: float,
|
||||||
|
sigma_value: float,
|
||||||
|
wavelength: float,
|
||||||
|
reference_date: str,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
import matplotlib
|
||||||
|
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
point_id = str(point["point_id"])
|
||||||
|
csv_path = point_dir / f"{point_id}_timeseries.csv"
|
||||||
|
json_path = point_dir / f"{point_id}_metadata.json"
|
||||||
|
png_path = point_dir / f"{point_id}_timeseries.png"
|
||||||
|
|
||||||
|
with csv_path.open("w", newline="", encoding="utf-8") as handle:
|
||||||
|
writer = csv.DictWriter(handle, fieldnames=["date", "phase_rad", "los_away_mm", "los_toward_mm"])
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(records)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"schema": "insar.sbas-monitor-point/v1",
|
||||||
|
"point_id": point_id,
|
||||||
|
"selection": point.get("selection"),
|
||||||
|
"radar_pixel": {"range": int(point["range_pixel"]), "azimuth": int(point["azimuth_line"])},
|
||||||
|
"approx_lonlat": {"lon": point.get("lon"), "lat": point.get("lat")},
|
||||||
|
"reference_date": reference_date,
|
||||||
|
"los_convention": "toward radar positive; away from radar negative",
|
||||||
|
"los_rate_toward_mm_per_year": rate_value,
|
||||||
|
"los_sigma_mm_per_year": sigma_value,
|
||||||
|
"wavelength_m": wavelength,
|
||||||
|
"records": records,
|
||||||
|
}
|
||||||
|
json_path.write_text(json.dumps(metadata, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
dates = [record["date"] for record in records]
|
||||||
|
disp = [record["los_toward_mm"] for record in records]
|
||||||
|
plt.figure(figsize=(8.0, 4.6), dpi=160)
|
||||||
|
plt.plot(dates, disp, marker="o", linewidth=2.0, color="#1f77b4")
|
||||||
|
plt.axhline(0, color="#666666", linewidth=0.8)
|
||||||
|
plt.grid(True, color="#dddddd", linewidth=0.7)
|
||||||
|
plt.title(
|
||||||
|
f"LOS displacement time series ({point_id})\n"
|
||||||
|
f"toward radar positive, rate={rate_value:.2f} mm/yr, sigma={sigma_value:.2f} mm/yr",
|
||||||
|
fontsize=10,
|
||||||
|
)
|
||||||
|
plt.xlabel("Date")
|
||||||
|
plt.ylabel("LOS displacement (mm)")
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(png_path)
|
||||||
|
plt.close()
|
||||||
|
return {"png": str(png_path), "csv": str(csv_path), "metadata": str(json_path)}
|
||||||
|
|
||||||
|
|
||||||
|
def run_phase_to_los(args: argparse.Namespace) -> int:
|
||||||
|
write_scaled_float32(Path(args.input), Path(args.output), float(args.scale))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def run_monitor_points(args: argparse.Namespace) -> int:
|
||||||
|
timeseries_dir = Path(args.timeseries_dir)
|
||||||
|
export_dir = Path(args.export_dir)
|
||||||
|
point_dir = Path(args.point_dir)
|
||||||
|
mli_par = Path(args.mli_par)
|
||||||
|
slc_par = Path(args.slc_par) if args.slc_par else mli_par
|
||||||
|
dem_par = Path(args.dem_par)
|
||||||
|
lookup = Path(args.lookup)
|
||||||
|
monitor_config_path = Path(args.monitor_config)
|
||||||
|
summary_path = Path(args.summary_path)
|
||||||
|
point_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
width = int(read_gamma_value(mli_par, "range_samples"))
|
||||||
|
lines = int(read_gamma_value(mli_par, "azimuth_lines"))
|
||||||
|
shape = (lines, width)
|
||||||
|
dates = [item.strip() for item in str(args.dates or "").split(",") if item.strip()]
|
||||||
|
reference_date = str(args.reference_date or "").strip()
|
||||||
|
|
||||||
|
radar_freq = float(read_gamma_value(slc_par, "radar_frequency"))
|
||||||
|
wavelength = 299792458.0 / radar_freq
|
||||||
|
scale_mm = wavelength / (4.0 * math.pi) * 1000.0
|
||||||
|
|
||||||
|
rate_toward = read_float32(export_dir / "los_rate_toward_mm_per_year.rdc", shape)
|
||||||
|
sigma = read_float32(export_dir / "los_sigma_mm_per_year.rdc", shape)
|
||||||
|
diff_files = load_diff_files(timeseries_dir)
|
||||||
|
if not diff_files:
|
||||||
|
raise RuntimeError(f"No diff_ts files found in {timeseries_dir}")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
if monitor_config_path.is_file():
|
||||||
|
config = json.loads(monitor_config_path.read_text(encoding="utf-8"))
|
||||||
|
mode = str(config.get("mode") or "auto_low_sigma_high_rate")
|
||||||
|
selected_points: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
if mode == "manual_lonlat" and config.get("points"):
|
||||||
|
for index, raw in enumerate(config.get("points") or []):
|
||||||
|
lon = float(raw["lon"])
|
||||||
|
lat = float(raw["lat"])
|
||||||
|
x, y = lonlat_to_radar(lon, lat, dem_par, lookup)
|
||||||
|
selected_points.append(
|
||||||
|
{
|
||||||
|
"point_id": safe_point_id(raw.get("point_id"), f"manual_{index + 1:03d}"),
|
||||||
|
"selection": "manual_lonlat_nearest_lookup_pixel",
|
||||||
|
"range_pixel": x,
|
||||||
|
"azimuth_line": y,
|
||||||
|
"lon": lon,
|
||||||
|
"lat": lat,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
x, y = pick_auto_point(rate_toward, sigma)
|
||||||
|
lon, lat = radar_to_lonlat(x, y, dem_par, lookup)
|
||||||
|
selected_points.append(
|
||||||
|
{
|
||||||
|
"point_id": "auto_low_sigma_high_rate",
|
||||||
|
"selection": "automatic_low_sigma_high_rate_non_edge",
|
||||||
|
"range_pixel": x,
|
||||||
|
"azimuth_line": y,
|
||||||
|
"lon": lon,
|
||||||
|
"lat": lat,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs: list[dict[str, Any]] = []
|
||||||
|
for point in selected_points:
|
||||||
|
x = int(point["range_pixel"])
|
||||||
|
y = int(point["azimuth_line"])
|
||||||
|
if not (0 <= x < width and 0 <= y < lines):
|
||||||
|
raise ValueError(f"pixel out of bounds: x={x}, y={y}, width={width}, lines={lines}")
|
||||||
|
records = point_records(diff_files, dates=dates, width=width, x=x, y=y, scale_mm=scale_mm)
|
||||||
|
rate_value = float(rate_toward[y, x])
|
||||||
|
sigma_value = float(sigma[y, x])
|
||||||
|
files = write_point_outputs(
|
||||||
|
point_dir,
|
||||||
|
point,
|
||||||
|
records=records,
|
||||||
|
rate_value=rate_value,
|
||||||
|
sigma_value=sigma_value,
|
||||||
|
wavelength=wavelength,
|
||||||
|
reference_date=reference_date,
|
||||||
|
)
|
||||||
|
outputs.append(
|
||||||
|
{
|
||||||
|
**point,
|
||||||
|
"los_rate_toward_mm_per_year": rate_value,
|
||||||
|
"los_sigma_mm_per_year": sigma_value,
|
||||||
|
"record_count": len(records),
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"schema": "insar.gamma-sbas-monitor-points-summary/v1",
|
||||||
|
"generated_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
||||||
|
"ready": bool(outputs),
|
||||||
|
"mode": mode,
|
||||||
|
"reference_date": reference_date,
|
||||||
|
"width": width,
|
||||||
|
"lines": lines,
|
||||||
|
"wavelength_m": wavelength,
|
||||||
|
"diff_ts_count": len(diff_files),
|
||||||
|
"date_count": len(dates),
|
||||||
|
"monitor_points": outputs,
|
||||||
|
}
|
||||||
|
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="Gamma SBAS product helper tools")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
phase = subparsers.add_parser("phase-to-los")
|
||||||
|
phase.add_argument("input")
|
||||||
|
phase.add_argument("output")
|
||||||
|
phase.add_argument("scale", type=float)
|
||||||
|
phase.set_defaults(func=run_phase_to_los)
|
||||||
|
|
||||||
|
monitor = subparsers.add_parser("monitor-points")
|
||||||
|
monitor.add_argument("--monitor-config", required=True)
|
||||||
|
monitor.add_argument("--timeseries-dir", required=True)
|
||||||
|
monitor.add_argument("--export-dir", required=True)
|
||||||
|
monitor.add_argument("--point-dir", required=True)
|
||||||
|
monitor.add_argument("--mli-par", required=True)
|
||||||
|
monitor.add_argument("--slc-par", required=True)
|
||||||
|
monitor.add_argument("--dem-par", required=True)
|
||||||
|
monitor.add_argument("--lookup", required=True)
|
||||||
|
monitor.add_argument("--dates", default="")
|
||||||
|
monitor.add_argument("--reference-date", default="")
|
||||||
|
monitor.add_argument("--summary-path", required=True)
|
||||||
|
monitor.set_defaults(func=run_monitor_points)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
return int(args.func(args))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
TERMINAL_STATUSES = {"COMPLETED", "FAILED", "SKIPPED"}
|
||||||
|
|
||||||
|
|
||||||
|
def _utcnow() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(path: str | Path) -> dict[str, Any]:
|
||||||
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: str | Path, payload: dict[str, Any]) -> None:
|
||||||
|
target = Path(path)
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _tail(text: str, limit: int = 4000) -> str:
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
return text[-limit:]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_step_ids(value: str | None) -> set[str]:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return set()
|
||||||
|
return {item.strip() for item in text.replace(";", ",").split(",") if item.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_workflow_manifest(broker_manifest: dict[str, Any]) -> tuple[dict[str, Any], Path]:
|
||||||
|
payload = broker_manifest.get("payload") or {}
|
||||||
|
workflow_manifest_path = payload.get("workflow_manifest_wsl") or payload.get("workflow_manifest")
|
||||||
|
if not workflow_manifest_path:
|
||||||
|
raise ValueError("payload.workflow_manifest_wsl is required")
|
||||||
|
manifest_path = Path(str(workflow_manifest_path))
|
||||||
|
return _load_json(manifest_path), manifest_path
|
||||||
|
|
||||||
|
|
||||||
|
def _state_path(workflow_manifest: dict[str, Any]) -> Path:
|
||||||
|
state = workflow_manifest.get("state") or {}
|
||||||
|
explicit = state.get("step_status_path")
|
||||||
|
if explicit:
|
||||||
|
return Path(str(explicit))
|
||||||
|
run_root = Path(str(workflow_manifest.get("run_root_wsl") or workflow_manifest.get("run_root") or "."))
|
||||||
|
return run_root / "state" / "step_status.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _script_env(workflow_manifest: dict[str, Any], step: dict[str, Any]) -> dict[str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
run_root = str(workflow_manifest.get("run_root_wsl") or workflow_manifest.get("run_root") or "")
|
||||||
|
params = workflow_manifest.get("params") or {}
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"GAMMA_SBAS_RUN_ROOT": run_root,
|
||||||
|
"GAMMA_SBAS_MANIFEST": str(workflow_manifest.get("manifest_path_wsl") or ""),
|
||||||
|
"GAMMA_SBAS_STEP_ID": str(step.get("id") or ""),
|
||||||
|
"GAMMA_SBAS_STEP_NAME": str(step.get("name") or step.get("id") or ""),
|
||||||
|
"GAMMA_SBAS_RLKS": str(params.get("rlks") or ""),
|
||||||
|
"GAMMA_SBAS_AZLKS": str(params.get("azlks") or ""),
|
||||||
|
"GAMMA_SBAS_MB_MODE": str(params.get("mb_mode") or ""),
|
||||||
|
"GAMMA_SBAS_REFERENCE_WINDOW": str(params.get("reference_window") or ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for key, value in (step.get("env") or {}).items():
|
||||||
|
env[str(key)] = str(value)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_steps(steps: list[dict[str, Any]], only_steps: set[str], from_step: str | None, to_step: str | None) -> list[dict[str, Any]]:
|
||||||
|
if only_steps:
|
||||||
|
return [step for step in steps if str(step.get("id") or "") in only_steps]
|
||||||
|
if not from_step and not to_step:
|
||||||
|
return steps
|
||||||
|
|
||||||
|
selected: list[dict[str, Any]] = []
|
||||||
|
active = from_step is None
|
||||||
|
for step in steps:
|
||||||
|
step_id = str(step.get("id") or "")
|
||||||
|
if step_id == from_step:
|
||||||
|
active = True
|
||||||
|
if active:
|
||||||
|
selected.append(step)
|
||||||
|
if step_id == to_step:
|
||||||
|
break
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def _run_step(
|
||||||
|
workflow_manifest: dict[str, Any],
|
||||||
|
step: dict[str, Any],
|
||||||
|
*,
|
||||||
|
state: dict[str, Any],
|
||||||
|
force: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
timeout_seconds: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
step_id = str(step.get("id") or "").strip()
|
||||||
|
if not step_id:
|
||||||
|
raise ValueError("workflow step id must not be empty")
|
||||||
|
|
||||||
|
if step.get("enabled") is False:
|
||||||
|
return {
|
||||||
|
"id": step_id,
|
||||||
|
"name": step.get("name") or step_id,
|
||||||
|
"status": "SKIPPED",
|
||||||
|
"skipped_reason": "step disabled in workflow manifest",
|
||||||
|
"started_at": _utcnow(),
|
||||||
|
"ended_at": _utcnow(),
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = (state.get("steps") or {}).get(step_id) or {}
|
||||||
|
if previous.get("status") == "COMPLETED" and not force:
|
||||||
|
return {**previous, "status": "SKIPPED", "skipped_reason": "already completed"}
|
||||||
|
|
||||||
|
script = Path(str(step.get("script_wsl") or step.get("script") or ""))
|
||||||
|
if not script.is_file():
|
||||||
|
raise FileNotFoundError(f"step script not found: {script}")
|
||||||
|
|
||||||
|
log_path = Path(str(step.get("log_wsl") or step.get("log") or ""))
|
||||||
|
if not log_path:
|
||||||
|
run_root = Path(str(workflow_manifest.get("run_root_wsl") or "."))
|
||||||
|
log_path = run_root / "logs" / f"{step_id}.log"
|
||||||
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
started_at = _utcnow()
|
||||||
|
if dry_run:
|
||||||
|
return {
|
||||||
|
"id": step_id,
|
||||||
|
"name": step.get("name") or step_id,
|
||||||
|
"status": "DRY_RUN",
|
||||||
|
"script": str(script),
|
||||||
|
"log": str(log_path),
|
||||||
|
"started_at": started_at,
|
||||||
|
"ended_at": _utcnow(),
|
||||||
|
"returncode": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = subprocess.run(
|
||||||
|
["bash", str(script)],
|
||||||
|
cwd=str(script.parent),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=timeout_seconds,
|
||||||
|
check=False,
|
||||||
|
env=_script_env(workflow_manifest, step),
|
||||||
|
)
|
||||||
|
log_path.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
f"# step={step_id}",
|
||||||
|
f"# started_at={started_at}",
|
||||||
|
f"# ended_at={_utcnow()}",
|
||||||
|
f"# returncode={proc.returncode}",
|
||||||
|
"",
|
||||||
|
"## stdout",
|
||||||
|
proc.stdout or "",
|
||||||
|
"",
|
||||||
|
"## stderr",
|
||||||
|
proc.stderr or "",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": step_id,
|
||||||
|
"name": step.get("name") or step_id,
|
||||||
|
"status": "COMPLETED" if proc.returncode == 0 else "FAILED",
|
||||||
|
"script": str(script),
|
||||||
|
"log": str(log_path),
|
||||||
|
"started_at": started_at,
|
||||||
|
"ended_at": _utcnow(),
|
||||||
|
"returncode": proc.returncode,
|
||||||
|
"stdout_tail": _tail(proc.stdout),
|
||||||
|
"stderr_tail": _tail(proc.stderr),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Gamma SBAS manifest runner.")
|
||||||
|
parser.add_argument("--manifest", required=True, help="WSL path to broker manifest.")
|
||||||
|
parser.add_argument("--from-step", default="", help="First workflow step id to execute.")
|
||||||
|
parser.add_argument("--to-step", default="", help="Last workflow step id to execute.")
|
||||||
|
parser.add_argument("--only-steps", default="", help="Comma-separated step ids to execute.")
|
||||||
|
parser.add_argument("--force", action="store_true", help="Run completed steps again.")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Validate manifest and write dry-run state.")
|
||||||
|
parser.add_argument("--timeout-seconds", type=int, default=43200)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
broker_manifest = _load_json(args.manifest)
|
||||||
|
workflow_manifest, workflow_manifest_path = _load_workflow_manifest(broker_manifest)
|
||||||
|
workflow_manifest["manifest_path_wsl"] = str(workflow_manifest_path)
|
||||||
|
|
||||||
|
state_file = _state_path(workflow_manifest)
|
||||||
|
state = _load_json(state_file) if state_file.is_file() else {
|
||||||
|
"schema": "insar.gamma-sbas-step-status/v1",
|
||||||
|
"run_id": workflow_manifest.get("run_id"),
|
||||||
|
"steps": {},
|
||||||
|
}
|
||||||
|
state.setdefault("steps", {})
|
||||||
|
state["updated_at"] = _utcnow()
|
||||||
|
state["runner_manifest"] = args.manifest
|
||||||
|
|
||||||
|
all_steps = list(workflow_manifest.get("steps") or [])
|
||||||
|
selected = _selected_steps(
|
||||||
|
all_steps,
|
||||||
|
_normalize_step_ids(args.only_steps),
|
||||||
|
str(args.from_step or "").strip() or None,
|
||||||
|
str(args.to_step or "").strip() or None,
|
||||||
|
)
|
||||||
|
if not selected:
|
||||||
|
raise ValueError("no workflow steps selected")
|
||||||
|
|
||||||
|
overall_rc = 0
|
||||||
|
executed: list[str] = []
|
||||||
|
for step in selected:
|
||||||
|
step_id = str(step.get("id") or "")
|
||||||
|
result = _run_step(
|
||||||
|
workflow_manifest,
|
||||||
|
step,
|
||||||
|
state=state,
|
||||||
|
force=args.force,
|
||||||
|
dry_run=args.dry_run,
|
||||||
|
timeout_seconds=max(60, int(args.timeout_seconds or 43200)),
|
||||||
|
)
|
||||||
|
state["steps"][step_id] = result
|
||||||
|
state["updated_at"] = _utcnow()
|
||||||
|
_write_json(state_file, state)
|
||||||
|
executed.append(step_id)
|
||||||
|
if result.get("status") == "FAILED":
|
||||||
|
overall_rc = int(result.get("returncode") or 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"runner": "gamma_sbas_runtime_v1",
|
||||||
|
"operation": broker_manifest.get("operation"),
|
||||||
|
"workflow_manifest": str(workflow_manifest_path),
|
||||||
|
"state_path": str(state_file),
|
||||||
|
"executed_steps": executed,
|
||||||
|
"returncode": overall_rc,
|
||||||
|
"dry_run": bool(args.dry_run),
|
||||||
|
}
|
||||||
|
print(json.dumps(summary, ensure_ascii=False))
|
||||||
|
return overall_rc
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+3
-1
@@ -55,6 +55,8 @@
|
|||||||
|
|
||||||
- [SBAS_INSAR_PRODUCTION_PIPELINE_DESIGN_20260519.md](SBAS_INSAR_PRODUCTION_PIPELINE_DESIGN_20260519.md)
|
- [SBAS_INSAR_PRODUCTION_PIPELINE_DESIGN_20260519.md](SBAS_INSAR_PRODUCTION_PIPELINE_DESIGN_20260519.md)
|
||||||
Gamma/LT1 SBAS-InSAR 独立生产页面与统一流水线收口设计。
|
Gamma/LT1 SBAS-InSAR 独立生产页面与统一流水线收口设计。
|
||||||
|
- [SBAS_INSAR_GEOJSON_RESULT_MANAGEMENT_DESIGN_20260527.md](SBAS_INSAR_GEOJSON_RESULT_MANAGEMENT_DESIGN_20260527.md)
|
||||||
|
SBAS-InSAR GeoJSON 覆盖范围、行政区/AOI 按需生产、独立结果管理和产品 catalog 设计。
|
||||||
- [TIMESERIES_LEGACY_DEPRECATION_20260521.md](TIMESERIES_LEGACY_DEPRECATION_20260521.md)
|
- [TIMESERIES_LEGACY_DEPRECATION_20260521.md](TIMESERIES_LEGACY_DEPRECATION_20260521.md)
|
||||||
旧 ISCE2/MintPy 时序生产链路停用记录,定义当前入口隐藏、配置默认关闭和后续物理删除条件。
|
旧 ISCE2/MintPy 时序生产链路停用记录,定义当前入口隐藏、配置默认关闭和后续物理删除条件。
|
||||||
- [GAMMA_IPTA_LT1_SBAS_TRIAL_RUNBOOK_20260518.md](GAMMA_IPTA_LT1_SBAS_TRIAL_RUNBOOK_20260518.md)
|
- [GAMMA_IPTA_LT1_SBAS_TRIAL_RUNBOOK_20260518.md](GAMMA_IPTA_LT1_SBAS_TRIAL_RUNBOOK_20260518.md)
|
||||||
@@ -137,4 +139,4 @@
|
|||||||
- 已归档的各类 `*_EXPERIMENT_*` / `*_PROGRESS_*` / `*_TODO_*`
|
- 已归档的各类 `*_EXPERIMENT_*` / `*_PROGRESS_*` / `*_TODO_*`
|
||||||
|
|
||||||
最后更新:2026-05-15
|
最后更新:2026-05-15
|
||||||
最近修订:2026-05-21
|
最近修订:2026-05-27
|
||||||
|
|||||||
@@ -0,0 +1,531 @@
|
|||||||
|
# SBAS-InSAR GeoJSON Coverage And Result Management Design
|
||||||
|
|
||||||
|
Date: 2026-05-27
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
Gamma SBAS-InSAR production has completed an end-to-end twelve-node run. The next step is to make production and results usable by geography, not only by time sequence and run status.
|
||||||
|
|
||||||
|
This design defines:
|
||||||
|
|
||||||
|
- how production pages show approximate geographic coverage with GeoJSON/bbox;
|
||||||
|
- how users select administrative regions or AOI to discover and produce SBAS stacks on demand;
|
||||||
|
- how completed SBAS runs become result products with searchable geographic extent;
|
||||||
|
- how the results page should display LOS velocity, LOS sigma, and monitoring-point curves.
|
||||||
|
|
||||||
|
## 2. Current Facts
|
||||||
|
|
||||||
|
The current successful run already contains usable geographic metadata:
|
||||||
|
|
||||||
|
```text
|
||||||
|
run_id = sbas_7537cc71c998
|
||||||
|
stack center bucket = E129.2_N44.1
|
||||||
|
stack bbox = 128.7690438245, 43.7486321624, 129.6293024728, 44.3582486206
|
||||||
|
monitor point = 129.10207098755, 44.15041727515
|
||||||
|
```
|
||||||
|
|
||||||
|
Available metadata sources:
|
||||||
|
|
||||||
|
```text
|
||||||
|
stack_manifest.scenes[*].bbox
|
||||||
|
stack_manifest.scenes[*].center_lon / center_lat
|
||||||
|
stack_manifest.stack.center_bucket
|
||||||
|
rdc_dem_summary.dem_source.stack_bbox
|
||||||
|
monitor_points_summary.monitor_outputs[*].metadata.approx_lonlat
|
||||||
|
published GeoTIFF bounds from GDAL metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing platform capabilities:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/geojson/全国行政区.geojson
|
||||||
|
backend/geojson/层级映射.json
|
||||||
|
GET /api/aoi/regions/children
|
||||||
|
GET /api/aoi/regions/{tree_id}/geometry
|
||||||
|
backend AOI helpers for region_tree_id / GeoJSON / uploaded AOI parsing
|
||||||
|
frontend Leaflet map and L.geoJSON support
|
||||||
|
frontend App.jsx existing source-scene footprint and AOI overlay patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
The missing piece is productized SBAS-specific coverage and catalog behavior.
|
||||||
|
|
||||||
|
## 3. Design Principles
|
||||||
|
|
||||||
|
1. SBAS production is temporal, but SBAS result consumption is geographic.
|
||||||
|
2. Production UI should answer "am I processing the right place?" before a long workflow is submitted.
|
||||||
|
3. Results UI should answer "where is this product, what time range does it cover, and can I download the main outputs?"
|
||||||
|
4. One completed SBAS run is one result product bundle, not one product per GeoTIFF.
|
||||||
|
5. GeoJSON/bbox coverage is enough for the first production UX. Full raster map rendering can come later.
|
||||||
|
6. Administrative-region filtering should reuse existing AOI infrastructure rather than introduce a parallel region system.
|
||||||
|
|
||||||
|
## 4. Coverage Model
|
||||||
|
|
||||||
|
### 4.1 Stack Coverage
|
||||||
|
|
||||||
|
For stack discovery and production run display:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"center": {"lon": 129.199, "lat": 44.053},
|
||||||
|
"bbox": {
|
||||||
|
"min_lon": 128.769,
|
||||||
|
"min_lat": 43.749,
|
||||||
|
"max_lon": 129.629,
|
||||||
|
"max_lat": 44.358
|
||||||
|
},
|
||||||
|
"bbox_geojson": {
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {"role": "sbas_stack_bbox"},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[128.769, 43.749],
|
||||||
|
[129.629, 43.749],
|
||||||
|
[129.629, 44.358],
|
||||||
|
[128.769, 44.358],
|
||||||
|
[128.769, 43.749]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scene_bbox_count": 7,
|
||||||
|
"scene_footprints_geojson": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First implementation may use stack bbox only. Per-scene rectangles can be added when the map needs to show coverage stability.
|
||||||
|
|
||||||
|
### 4.2 Product Coverage
|
||||||
|
|
||||||
|
For completed results:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"coverage_source": "stack_manifest_bbox_union",
|
||||||
|
"geotiff_bounds_verified": true,
|
||||||
|
"bbox": {...},
|
||||||
|
"center": {...},
|
||||||
|
"footprint_geojson": {...},
|
||||||
|
"administrative_hint": {
|
||||||
|
"province": "黑龙江省",
|
||||||
|
"city": null,
|
||||||
|
"county": null,
|
||||||
|
"method": "center_point_lookup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first administrative hint can be center-point based. Later it should become intersection-based and return all intersected regions with approximate overlap area.
|
||||||
|
|
||||||
|
## 5. Administrative Region And AOI Production
|
||||||
|
|
||||||
|
### 5.1 User Workflow
|
||||||
|
|
||||||
|
Production page should support three AOI sources:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. Administrative region selection
|
||||||
|
province -> city -> county, backed by /aoi/regions endpoints
|
||||||
|
|
||||||
|
2. Map-drawn rectangle
|
||||||
|
converted to bbox GeoJSON
|
||||||
|
|
||||||
|
3. Uploaded GeoJSON/SHP
|
||||||
|
reuse existing AOI parsing helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
The user flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
select AOI / administrative region
|
||||||
|
discover SBAS stack candidates
|
||||||
|
show candidate time density + geographic coverage
|
||||||
|
select candidate
|
||||||
|
create production Run
|
||||||
|
submit Gamma SBAS workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Discovery Filter
|
||||||
|
|
||||||
|
Stack discovery should accept:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"region_tree_id": "230000",
|
||||||
|
"aoi_geojson": {},
|
||||||
|
"aoi_bbox": {
|
||||||
|
"min_lon": 128.7,
|
||||||
|
"min_lat": 43.7,
|
||||||
|
"max_lon": 129.7,
|
||||||
|
"max_lat": 44.4
|
||||||
|
},
|
||||||
|
"aoi_overlap_min": 0.0,
|
||||||
|
"stable_stack_overlap_min": 0.3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Initial filtering can use bbox intersection:
|
||||||
|
|
||||||
|
```text
|
||||||
|
candidate stack is valid when union_bbox intersects AOI bbox
|
||||||
|
```
|
||||||
|
|
||||||
|
Second-stage filtering should use polygon intersection:
|
||||||
|
|
||||||
|
```text
|
||||||
|
candidate_score += common_stack_area_intersection_ratio
|
||||||
|
candidate_score += scene_count / temporal_density
|
||||||
|
candidate_score -= sparse_time_gap_penalty
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Production Guardrails
|
||||||
|
|
||||||
|
Before running workflow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
show bbox and administrative hint
|
||||||
|
show date list and max temporal gap
|
||||||
|
show DEM coverage status
|
||||||
|
warn when DEM covers center but not full stack bbox
|
||||||
|
warn when AOI overlap is low
|
||||||
|
```
|
||||||
|
|
||||||
|
The current run demonstrated a real issue:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DEM source covers stack center = true
|
||||||
|
DEM source covers full stack bbox = false
|
||||||
|
```
|
||||||
|
|
||||||
|
This must be visible in the production page before the user trusts the result.
|
||||||
|
|
||||||
|
## 6. Production Page UI
|
||||||
|
|
||||||
|
Add `Geographic Coverage` block to `SbasInsarProductionPanel`.
|
||||||
|
|
||||||
|
Minimum fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Center: lon / lat
|
||||||
|
BBox: min_lon, min_lat, max_lon, max_lat
|
||||||
|
Scene footprints: count
|
||||||
|
DEM coverage: bbox / center
|
||||||
|
Administrative hint
|
||||||
|
Monitor points if generated
|
||||||
|
Actions: view on map, copy GeoJSON, zoom to footprint
|
||||||
|
```
|
||||||
|
|
||||||
|
Map preview options:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Phase 1: small unframed Leaflet map with rectangle overlay
|
||||||
|
Phase 2: shared main map overlay using existing App.jsx layer mechanisms
|
||||||
|
Phase 3: per-scene footprints and monitor points overlay
|
||||||
|
```
|
||||||
|
|
||||||
|
The production page should not become the result browser. It only provides enough geographic context to avoid wrong-location production.
|
||||||
|
|
||||||
|
## 7. Result Management Module
|
||||||
|
|
||||||
|
### 7.1 Module Boundary
|
||||||
|
|
||||||
|
Create a separate SBAS-InSAR result management module:
|
||||||
|
|
||||||
|
```text
|
||||||
|
navigation: Results / SBAS-InSAR Results
|
||||||
|
backend catalog: catalog_name = sbas_insar
|
||||||
|
product type: sbas_insar_bundle
|
||||||
|
source: completed Gamma SBAS production Run
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not merge SBAS result semantics into D-InSAR result pages. Reuse the common catalog tables but keep service, API, and frontend module separate.
|
||||||
|
|
||||||
|
### 7.2 Product Row
|
||||||
|
|
||||||
|
One completed run becomes one product row:
|
||||||
|
|
||||||
|
```text
|
||||||
|
result_products.catalog_name = sbas_insar
|
||||||
|
result_products.run_key = sbas run_id
|
||||||
|
result_products.engine_code = gamma
|
||||||
|
result_products.processor_code = gamma_ipta_sbas
|
||||||
|
result_products.display_name = platform / orbit / area / date span
|
||||||
|
```
|
||||||
|
|
||||||
|
Required product metadata:
|
||||||
|
|
||||||
|
```text
|
||||||
|
platform
|
||||||
|
relative_orbit
|
||||||
|
orbit_direction
|
||||||
|
polarization
|
||||||
|
reference_date
|
||||||
|
start_date
|
||||||
|
end_date
|
||||||
|
scene_count
|
||||||
|
pair_count
|
||||||
|
bbox_min_lon / bbox_min_lat / bbox_max_lon / bbox_max_lat
|
||||||
|
center_lon / center_lat
|
||||||
|
administrative_hint
|
||||||
|
status
|
||||||
|
health_status
|
||||||
|
source_run_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Asset Roles
|
||||||
|
|
||||||
|
Primary assets from the expert document:
|
||||||
|
|
||||||
|
```text
|
||||||
|
primary_velocity_geotiff
|
||||||
|
current: publish/geotiff/los_rate_toward_m_per_year.tif
|
||||||
|
expert equivalent: geo_los_def_rate.tif
|
||||||
|
|
||||||
|
primary_velocity_preview
|
||||||
|
current: publish/geotiff/los_rate_toward_m_per_year.hls.geo_preview.png
|
||||||
|
expert equivalent: los_def_rate.bmp / geo_los_def_rate.bmp
|
||||||
|
|
||||||
|
primary_velocity_rgb_geotiff
|
||||||
|
current: publish/geotiff/los_rate_toward_m_per_year.hls.geo_rgb.tif
|
||||||
|
expert equivalent: geo_los_def_rate_rgb.tif
|
||||||
|
|
||||||
|
quality_sigma_geotiff
|
||||||
|
current: publish/geotiff/los_sigma_m_per_year.tif
|
||||||
|
expert equivalent: diff.sigma_ts / geo_diff.sigma_ts
|
||||||
|
|
||||||
|
quality_sigma_preview
|
||||||
|
current: publish/geotiff/los_sigma_m_per_year.cc.geo_preview.png
|
||||||
|
expert equivalent: diff.sigma_ts.masked.bmp with cc.cm
|
||||||
|
|
||||||
|
monitor_timeseries_csv
|
||||||
|
current: publish/monitor_points/*_timeseries.csv
|
||||||
|
expert equivalent: disp_prt_2d output table
|
||||||
|
|
||||||
|
monitor_timeseries_plot
|
||||||
|
current: publish/monitor_points/*_timeseries.png
|
||||||
|
|
||||||
|
support_manifest
|
||||||
|
run_manifest.json, workflow_summary.json, gamma_command_manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Default result display should foreground:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LOS velocity preview
|
||||||
|
LOS sigma preview
|
||||||
|
monitor point curve if present
|
||||||
|
footprint map
|
||||||
|
```
|
||||||
|
|
||||||
|
Audit/support files should be grouped separately.
|
||||||
|
|
||||||
|
## 8. Backend Design
|
||||||
|
|
||||||
|
### 8.1 Service
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/app/services/sbas_insar_catalog_service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
```text
|
||||||
|
scan completed SBAS run directories
|
||||||
|
validate product bundle readiness
|
||||||
|
derive bbox/center/footprint GeoJSON
|
||||||
|
extract or verify GeoTIFF bounds
|
||||||
|
upsert result_products/result_assets/result_issues
|
||||||
|
provide list/detail/download APIs
|
||||||
|
bootstrap self-maintenance on startup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 API
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/sbas-insar-products/catalog-status
|
||||||
|
POST /api/sbas-insar-products/rebuild-catalog
|
||||||
|
GET /api/sbas-insar-products
|
||||||
|
GET /api/sbas-insar-products/{product_id}
|
||||||
|
GET /api/sbas-insar-products/{product_id}/assets/{asset_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
List filters:
|
||||||
|
|
||||||
|
```text
|
||||||
|
date_from
|
||||||
|
date_to
|
||||||
|
reference_date
|
||||||
|
platform
|
||||||
|
relative_orbit
|
||||||
|
orbit_direction
|
||||||
|
status
|
||||||
|
health_status
|
||||||
|
region_tree_id
|
||||||
|
aoi_bbox
|
||||||
|
aoi_geojson
|
||||||
|
intersects_bbox
|
||||||
|
has_monitor_points
|
||||||
|
limit / offset
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Run Detail Extension
|
||||||
|
|
||||||
|
Extend production run detail:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"geographic_coverage": {
|
||||||
|
"center": {"lon": 129.199, "lat": 44.053},
|
||||||
|
"bbox": {"min_lon": 128.769, "min_lat": 43.749, "max_lon": 129.629, "max_lat": 44.358},
|
||||||
|
"bbox_geojson": {},
|
||||||
|
"scene_bbox_count": 7,
|
||||||
|
"dem_covers_stack_bbox": false,
|
||||||
|
"dem_covers_stack_center": true,
|
||||||
|
"monitor_points": [{"point_id": "auto_low_sigma_high_rate", "lon": 129.102, "lat": 44.150}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Startup Self-Maintenance
|
||||||
|
|
||||||
|
On startup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ensure result catalog tables exist through existing maintenance
|
||||||
|
bootstrap sbas_insar catalog state
|
||||||
|
scan completed SBAS run publish bundles
|
||||||
|
upsert missing product rows
|
||||||
|
record issues for:
|
||||||
|
missing bbox
|
||||||
|
missing primary velocity GeoTIFF
|
||||||
|
missing primary preview
|
||||||
|
missing sigma GeoTIFF
|
||||||
|
missing sigma preview
|
||||||
|
missing monitor files when monitor summary says ready
|
||||||
|
DEM coverage mismatch
|
||||||
|
```
|
||||||
|
|
||||||
|
This matches the current self-maintenance direction used by D-InSAR and PsInSAR catalogs.
|
||||||
|
|
||||||
|
## 9. Frontend Design
|
||||||
|
|
||||||
|
### 9.1 Production Page
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GeographicCoveragePanel
|
||||||
|
bbox text
|
||||||
|
center text
|
||||||
|
administrative hint
|
||||||
|
DEM coverage status
|
||||||
|
mini map rectangle
|
||||||
|
copy GeoJSON
|
||||||
|
zoom/open on main map
|
||||||
|
```
|
||||||
|
|
||||||
|
Candidate stack cards should show:
|
||||||
|
|
||||||
|
```text
|
||||||
|
date span
|
||||||
|
scene count
|
||||||
|
center bucket
|
||||||
|
center lon/lat
|
||||||
|
bbox short text
|
||||||
|
AOI overlap indicator when AOI is selected
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Result Page
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SbasInsarProductsPanel
|
||||||
|
```
|
||||||
|
|
||||||
|
List page:
|
||||||
|
|
||||||
|
```text
|
||||||
|
filters: date range, administrative region, map AOI, platform, orbit, status
|
||||||
|
cards/table: product name, date span, bbox/admin hint, scene/pair count, preview thumbnail, health badge
|
||||||
|
actions: open detail, zoom to map, download primary GeoTIFF
|
||||||
|
```
|
||||||
|
|
||||||
|
Detail page:
|
||||||
|
|
||||||
|
```text
|
||||||
|
footprint map
|
||||||
|
LOS velocity preview
|
||||||
|
LOS sigma preview
|
||||||
|
monitor curve
|
||||||
|
key metadata table
|
||||||
|
asset table grouped by role
|
||||||
|
quality and issue summary
|
||||||
|
link to production Run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 Map Layer Strategy
|
||||||
|
|
||||||
|
Use GeoJSON for the first implementation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
bbox polygon for stack/product footprint
|
||||||
|
administrative region boundary layer from existing AOI endpoints
|
||||||
|
monitor point markers
|
||||||
|
optional per-scene footprints
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not display RDC BMP as map layer. Only geocoded preview PNG/GeoTIFF-derived bounds should be used for map-oriented display.
|
||||||
|
|
||||||
|
## 10. Implementation Sequence
|
||||||
|
|
||||||
|
Recommended order:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. Backend geographic_coverage in SBAS run detail.
|
||||||
|
2. Production page coverage block and bbox GeoJSON mini-map.
|
||||||
|
3. Add AOI/region filters to SBAS stack discovery request/response.
|
||||||
|
4. Add sbas_insar_catalog_service and catalog rebuild API.
|
||||||
|
5. Register current completed run as first SBAS result product.
|
||||||
|
6. Build SbasInsarProductsPanel list/detail.
|
||||||
|
7. Add map AOI filtering and administrative-region filtering to result page.
|
||||||
|
8. Add startup bootstrap/self-check output.
|
||||||
|
```
|
||||||
|
|
||||||
|
The first user-visible win is step 1-2: the operator can immediately see whether a Run covers the intended location.
|
||||||
|
|
||||||
|
## 11. Validation
|
||||||
|
|
||||||
|
Use `sbas_7537cc71c998` as the first validation run.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
|
||||||
|
```text
|
||||||
|
geographic_coverage.bbox is present
|
||||||
|
bbox_geojson draws a rectangle in Leaflet
|
||||||
|
admin hint is present or explicitly unknown
|
||||||
|
DEM coverage warning is visible when bbox is not fully covered
|
||||||
|
result product row exists after catalog rebuild
|
||||||
|
result detail opens velocity/sigma previews
|
||||||
|
asset downloads work
|
||||||
|
map zoom to footprint works
|
||||||
|
AOI filter returns this product when AOI intersects bbox
|
||||||
|
AOI filter excludes this product when AOI is far away
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Open Questions
|
||||||
|
|
||||||
|
1. Administrative-region naming should start with center-point lookup or intersection lookup?
|
||||||
|
Recommendation: center-point lookup first, intersection later.
|
||||||
|
|
||||||
|
2. Should stack discovery require AOI overlap, or only rank by AOI overlap?
|
||||||
|
Recommendation: default to rank/filter by bbox intersection, expose minimum overlap later.
|
||||||
|
|
||||||
|
3. Should results use `sbas_insar` or `psinsar` catalog name?
|
||||||
|
Recommendation: use `sbas_insar`. The current Gamma product is SBAS-InSAR, and old `psinsar` catalog semantics should not be overloaded.
|
||||||
|
|
||||||
|
4. Should GeoTIFF raster be rendered on map immediately?
|
||||||
|
Recommendation: not in the first slice. Start with footprint GeoJSON and preview images; raster tile rendering can be added after catalog registration is stable.
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,12 @@ export const listSbasInsarRuns = () =>
|
|||||||
export const getSbasInsarRun = runId =>
|
export const getSbasInsarRun = runId =>
|
||||||
apiClient.get(`/sbas-insar-production/runs/${encodeURIComponent(runId)}`).then(r => r.data);
|
apiClient.get(`/sbas-insar-production/runs/${encodeURIComponent(runId)}`).then(r => r.data);
|
||||||
|
|
||||||
|
export const prepareSbasInsarWorkflow = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/workflow`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const submitSbasInsarWorkflowJob = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/workflow/jobs`, payload).then(r => r.data);
|
||||||
|
|
||||||
export const runSbasInsarBaselineAudit = (runId, payload = {}) =>
|
export const runSbasInsarBaselineAudit = (runId, payload = {}) =>
|
||||||
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/baseline-audit`, payload).then(r => r.data);
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/baseline-audit`, payload).then(r => r.data);
|
||||||
|
|
||||||
@@ -36,14 +42,23 @@ export const prepareSbasInsarCoregistration = (runId, payload = {}) =>
|
|||||||
export const submitSbasInsarCoregistrationJob = (runId, payload = {}) =>
|
export const submitSbasInsarCoregistrationJob = (runId, payload = {}) =>
|
||||||
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/coregistration/jobs`, payload).then(r => r.data);
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/coregistration/jobs`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const prepareSbasInsarRdcDem = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/rdc-dem`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const submitSbasInsarRdcDemJob = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/rdc-dem/jobs`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const prepareSbasInsarInterferograms = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/interferograms`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const submitSbasInsarInterferogramsJob = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/interferograms/jobs`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const prepareSbasInsarIptaTimeseries = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/ipta-timeseries`, payload).then(r => r.data);
|
||||||
|
|
||||||
|
export const submitSbasInsarIptaTimeseriesJob = (runId, payload = {}) =>
|
||||||
|
apiClient.post(`/sbas-insar-production/runs/${encodeURIComponent(runId)}/ipta-timeseries/jobs`, payload).then(r => r.data);
|
||||||
|
|
||||||
export const getSbasInsarRunArtifactUrl = (runId, relativePath) =>
|
export const getSbasInsarRunArtifactUrl = (runId, relativePath) =>
|
||||||
`/api/sbas-insar-production/runs/${encodeURIComponent(runId)}/artifacts/${encodeArtifactPath(relativePath)}`;
|
`/api/sbas-insar-production/runs/${encodeURIComponent(runId)}/artifacts/${encodeArtifactPath(relativePath)}`;
|
||||||
|
|
||||||
export const listSbasInsarTrialRuns = () =>
|
|
||||||
apiClient.get('/sbas-insar-production/trial-runs').then(r => r.data);
|
|
||||||
|
|
||||||
export const getSbasInsarTrialRun = trialId =>
|
|
||||||
apiClient.get(`/sbas-insar-production/trial-runs/${encodeURIComponent(trialId)}`).then(r => r.data);
|
|
||||||
|
|
||||||
export const getSbasInsarArtifactUrl = (trialId, relativePath) =>
|
|
||||||
`/api/sbas-insar-production/trial-runs/${encodeURIComponent(trialId)}/artifacts/${encodeArtifactPath(relativePath)}`;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user