Add SARscape SBAS prepared stack workflow
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import os
|
||||
import re as _re
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -55,6 +55,12 @@ class ExtractDispRequest(BaseModel):
|
||||
dest_dir: Optional[str] = None
|
||||
|
||||
|
||||
class SarscapeSbasInspectRequest(BaseModel):
|
||||
task_names: Optional[List[str]] = None
|
||||
include_parameters: bool = False
|
||||
timeout_seconds: Optional[int] = Field(default=120, ge=10, le=600)
|
||||
|
||||
|
||||
def _normalize_existing_dir(path: Optional[str]) -> Optional[str]:
|
||||
text = str(path or "").strip()
|
||||
if not text:
|
||||
@@ -162,6 +168,22 @@ async def inspect_dinsar_endpoint(
|
||||
return envi_service.inspect_dinsar(request.root_dir)
|
||||
|
||||
|
||||
@router.post("/idl/inspect/sarscape-sbas")
|
||||
async def inspect_sarscape_sbas_endpoint(
|
||||
request: SarscapeSbasInspectRequest,
|
||||
admin_user: AuthUserORM = Depends(_require_admin),
|
||||
):
|
||||
_ = admin_user
|
||||
try:
|
||||
return envi_service.inspect_sarscape_sbas_tasks_subprocess(
|
||||
request.task_names,
|
||||
timeout_seconds=request.timeout_seconds or 120,
|
||||
include_parameters=bool(request.include_parameters),
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/idl/jobs/import")
|
||||
async def run_import_job_endpoint(
|
||||
request: ImportJobRequest,
|
||||
|
||||
@@ -18,6 +18,8 @@ from ..models import (
|
||||
RadarData,
|
||||
TimeseriesStackPlan,
|
||||
TimeseriesStackPlanDetail,
|
||||
TimeseriesStackPlanEdge,
|
||||
TimeseriesStackPlanEdgeORM,
|
||||
TimeseriesStackPlanItem,
|
||||
TimeseriesStackPlanItemORM,
|
||||
TimeseriesStackPlanORM,
|
||||
@@ -89,11 +91,21 @@ def get_pairing_request_from_form(
|
||||
|
||||
def get_ps_request_from_form(
|
||||
initial_overlap_threshold: float = Form(0.3),
|
||||
final_overlap_threshold: float = Form(0.95)
|
||||
final_overlap_threshold: float = Form(0.95),
|
||||
time_baseline_min: int = Form(1),
|
||||
time_baseline_max: int = Form(90),
|
||||
spatial_baseline_max_meters: int = Form(3000),
|
||||
network_overlap_threshold: float = Form(0.5),
|
||||
num_connections: int = Form(1),
|
||||
) -> PsRequest:
|
||||
return PsRequest(
|
||||
initial_overlap_threshold=initial_overlap_threshold,
|
||||
final_overlap_threshold=final_overlap_threshold,
|
||||
time_baseline_min=time_baseline_min,
|
||||
time_baseline_max=time_baseline_max,
|
||||
spatial_baseline_max_meters=spatial_baseline_max_meters,
|
||||
network_overlap_threshold=network_overlap_threshold,
|
||||
num_connections=num_connections,
|
||||
)
|
||||
|
||||
|
||||
@@ -205,11 +217,20 @@ async def get_timeseries_stack_plan_endpoint(
|
||||
.where(TimeseriesStackPlanItemORM.plan_ref_id == plan.id)
|
||||
.order_by(TimeseriesStackPlanItemORM.scene_rank.asc(), TimeseriesStackPlanItemORM.id.asc())
|
||||
)
|
||||
edges_result = await db.execute(
|
||||
select(TimeseriesStackPlanEdgeORM)
|
||||
.where(TimeseriesStackPlanEdgeORM.plan_ref_id == plan.id)
|
||||
.order_by(TimeseriesStackPlanEdgeORM.edge_rank.asc(), TimeseriesStackPlanEdgeORM.id.asc())
|
||||
)
|
||||
payload = TimeseriesStackPlan.model_validate(plan).model_dump()
|
||||
payload["items"] = [
|
||||
TimeseriesStackPlanItem.model_validate(item)
|
||||
for item in items_result.scalars().all()
|
||||
]
|
||||
payload["edges"] = [
|
||||
TimeseriesStackPlanEdge.model_validate(edge)
|
||||
for edge in edges_result.scalars().all()
|
||||
]
|
||||
return TimeseriesStackPlanDetail.model_validate(payload)
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from ..models import (
|
||||
PsTaskItemORM,
|
||||
RadarData,
|
||||
RadarPair,
|
||||
TimeseriesStackPlanEdgeORM,
|
||||
TimeseriesStackPlanItemORM,
|
||||
TimeseriesStackPlanORM,
|
||||
)
|
||||
@@ -144,6 +145,7 @@ def _normalize_lookup_key(value: Optional[str]) -> str:
|
||||
def _build_plan_context(
|
||||
plan: TimeseriesStackPlanORM,
|
||||
plan_items: List[TimeseriesStackPlanItemORM],
|
||||
plan_edges: Optional[List[TimeseriesStackPlanEdgeORM]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
request_params = plan.request_params_json if isinstance(plan.request_params_json, dict) else {}
|
||||
ordered_items = sorted(
|
||||
@@ -164,6 +166,10 @@ def _build_plan_context(
|
||||
}
|
||||
for item in ordered_items
|
||||
]
|
||||
ordered_edges = sorted(
|
||||
list(plan_edges or []),
|
||||
key=lambda item: (int(item.edge_rank or 0), int(item.id or 0)),
|
||||
)
|
||||
return {
|
||||
"source": "timeseries_stack_plan",
|
||||
"plan_id": plan.plan_id,
|
||||
@@ -176,12 +182,41 @@ def _build_plan_context(
|
||||
"aoi_summary": plan.aoi_summary_json if isinstance(plan.aoi_summary_json, dict) else None,
|
||||
"initial_overlap_threshold": request_params.get("initial_overlap_threshold"),
|
||||
"final_overlap_threshold": request_params.get("final_overlap_threshold"),
|
||||
"time_baseline_min": request_params.get("time_baseline_min"),
|
||||
"time_baseline_max": request_params.get("time_baseline_max"),
|
||||
"spatial_baseline_max_meters": request_params.get("spatial_baseline_max_meters"),
|
||||
"network_overlap_threshold": request_params.get("network_overlap_threshold"),
|
||||
"num_connections": request_params.get("num_connections"),
|
||||
"network_edge_count": len(ordered_edges),
|
||||
"stack_dates": [
|
||||
str(item.imaging_date).strip()
|
||||
for item in ordered_items
|
||||
if str(item.imaging_date or "").strip()
|
||||
],
|
||||
"scenes": scenes,
|
||||
"network_edges": [
|
||||
{
|
||||
"edge_id": item.id,
|
||||
"edge_rank": item.edge_rank,
|
||||
"master_plan_item_ref_id": item.master_plan_item_ref_id,
|
||||
"slave_plan_item_ref_id": item.slave_plan_item_ref_id,
|
||||
"metric_cache_ref_id": item.metric_cache_ref_id,
|
||||
"master_scene_ref_id": item.master_scene_ref_id,
|
||||
"slave_scene_ref_id": item.slave_scene_ref_id,
|
||||
"master_imaging_date": item.master_imaging_date,
|
||||
"slave_imaging_date": item.slave_imaging_date,
|
||||
"temporal_baseline_days": item.temporal_baseline_days,
|
||||
"spatial_baseline_meters": item.spatial_baseline_meters,
|
||||
"perpendicular_baseline_meters": item.perpendicular_baseline_meters,
|
||||
"scene_overlap_ratio": item.scene_overlap_ratio,
|
||||
"pair_aoi_overlap_ratio": item.pair_aoi_overlap_ratio,
|
||||
"selection_reason": item.selection_reason,
|
||||
"selection_score": item.selection_score,
|
||||
"enabled": bool(item.enabled),
|
||||
"selection_meta": item.selection_meta_json if isinstance(item.selection_meta_json, dict) else None,
|
||||
}
|
||||
for item in ordered_edges
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -383,6 +418,7 @@ async def create_ps_batch_endpoint(
|
||||
effective_plan_id = explicit_plan_id or (inferred_plan_ids[0] if inferred_plan_ids else None)
|
||||
plan: Optional[TimeseriesStackPlanORM] = None
|
||||
plan_items: List[TimeseriesStackPlanItemORM] = []
|
||||
plan_edges: List[TimeseriesStackPlanEdgeORM] = []
|
||||
plan_item_by_id: Dict[int, TimeseriesStackPlanItemORM] = {}
|
||||
plan_item_by_scene_id: Dict[int, TimeseriesStackPlanItemORM] = {}
|
||||
plan_item_by_path: Dict[str, TimeseriesStackPlanItemORM] = {}
|
||||
@@ -408,6 +444,12 @@ async def create_ps_batch_endpoint(
|
||||
.order_by(TimeseriesStackPlanItemORM.scene_rank.asc(), TimeseriesStackPlanItemORM.id.asc())
|
||||
)
|
||||
plan_items = items_result.scalars().all()
|
||||
edges_result = await db.execute(
|
||||
select(TimeseriesStackPlanEdgeORM)
|
||||
.where(TimeseriesStackPlanEdgeORM.plan_ref_id == plan.id)
|
||||
.order_by(TimeseriesStackPlanEdgeORM.edge_rank.asc(), TimeseriesStackPlanEdgeORM.id.asc())
|
||||
)
|
||||
plan_edges = edges_result.scalars().all()
|
||||
plan_item_by_id = {int(item.id): item for item in plan_items if item.id is not None}
|
||||
plan_item_by_scene_id = {
|
||||
int(item.radar_data_ref_id): item
|
||||
@@ -419,15 +461,18 @@ async def create_ps_batch_endpoint(
|
||||
for item in plan_items
|
||||
if _normalize_lookup_key(item.file_path)
|
||||
}
|
||||
plan_context = _build_plan_context(plan, plan_items, plan_edges)
|
||||
if not planning_context:
|
||||
planning_context = _build_plan_context(plan, plan_items)
|
||||
planning_context = plan_context
|
||||
else:
|
||||
merged_context = {
|
||||
**_build_plan_context(plan, plan_items),
|
||||
**plan_context,
|
||||
**planning_context,
|
||||
}
|
||||
if "scenes" not in planning_context:
|
||||
merged_context["scenes"] = _build_plan_context(plan, plan_items).get("scenes") or []
|
||||
merged_context["scenes"] = plan_context.get("scenes") or []
|
||||
if "network_edges" not in planning_context:
|
||||
merged_context["network_edges"] = plan_context.get("network_edges") or []
|
||||
planning_context = merged_context
|
||||
|
||||
batch_id = str(uuid.uuid4())
|
||||
@@ -466,7 +511,7 @@ async def create_ps_batch_endpoint(
|
||||
planning_summary = {
|
||||
key: value
|
||||
for key, value in planning_context.items()
|
||||
if key != "scenes"
|
||||
if key not in {"scenes", "network_edges"}
|
||||
}
|
||||
remark_payload = {
|
||||
**planning_summary,
|
||||
|
||||
@@ -20,6 +20,8 @@ class TimeseriesRunCreateRequest(BaseModel):
|
||||
run_name: Optional[str] = Field(default=None, max_length=255)
|
||||
reference_date: Optional[str] = Field(default=None, pattern=r"^\d{8}$|^$")
|
||||
water_mask_mode: str = Field(default="synthetic_fallback", max_length=64)
|
||||
processor_code: str = Field(default="isce2_stack_mintpy", max_length=64)
|
||||
execution_mode: Optional[str] = Field(default=None, max_length=32)
|
||||
notes: Optional[str] = Field(default=None, max_length=1000)
|
||||
|
||||
@field_validator("batch_id", mode="before")
|
||||
@@ -58,6 +60,21 @@ class TimeseriesPreflightRequest(BaseModel):
|
||||
return text
|
||||
|
||||
|
||||
class SarscapeSbasPreflightRequest(BaseModel):
|
||||
batch_id: str = Field(..., description="PS batch id")
|
||||
reference_date: Optional[str] = Field(default=None, pattern=r"^\d{8}$|^$")
|
||||
include_task_discovery: bool = True
|
||||
discovery_timeout_seconds: int = Field(default=120, ge=10, le=600)
|
||||
|
||||
@field_validator("batch_id", mode="before")
|
||||
@classmethod
|
||||
def _validate_batch_id(cls, value: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
raise ValueError("batch_id is required")
|
||||
return text
|
||||
|
||||
|
||||
class TimeseriesRetryStepRequest(BaseModel):
|
||||
step_id: str = Field(..., max_length=128)
|
||||
|
||||
@@ -82,6 +99,8 @@ async def create_timeseries_run(
|
||||
run_name=request.run_name,
|
||||
reference_date=request.reference_date,
|
||||
water_mask_mode=request.water_mask_mode,
|
||||
processor_code=request.processor_code,
|
||||
execution_mode=request.execution_mode,
|
||||
notes=request.notes,
|
||||
created_by=getattr(current_user, "username", None),
|
||||
db=db,
|
||||
@@ -125,6 +144,25 @@ async def run_timeseries_preflight(
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/sarscape-sbas/preflight")
|
||||
async def run_sarscape_sbas_preflight(
|
||||
request: SarscapeSbasPreflightRequest,
|
||||
current_user: AuthUserORM = Depends(_require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ = current_user
|
||||
try:
|
||||
return await timeseries_service.get_sarscape_sbas_preflight_report(
|
||||
batch_id=request.batch_id,
|
||||
reference_date=request.reference_date,
|
||||
include_task_discovery=request.include_task_discovery,
|
||||
discovery_timeout_seconds=request.discovery_timeout_seconds,
|
||||
db=db,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/runs")
|
||||
async def list_timeseries_runs(
|
||||
limit: int = 50,
|
||||
@@ -149,6 +187,19 @@ async def get_timeseries_run_detail(
|
||||
return detail
|
||||
|
||||
|
||||
@router.get("/runs/{run_id}/prepared-stack")
|
||||
async def get_timeseries_run_prepared_stack(
|
||||
run_id: str,
|
||||
current_user: AuthUserORM = Depends(_get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ = current_user
|
||||
summary = await timeseries_service.get_prepared_stack_summary(db, run_id=run_id)
|
||||
if summary is None:
|
||||
raise HTTPException(status_code=404, detail="Timeseries run not found")
|
||||
return summary
|
||||
|
||||
|
||||
@router.post("/runs/{run_id}/retry-step", status_code=202)
|
||||
async def retry_timeseries_run_step(
|
||||
run_id: str,
|
||||
|
||||
Reference in New Issue
Block a user