Integrate SBAS workflows and redesign task center

This commit is contained in:
2026-06-13 13:10:12 +08:00
parent b931e8db53
commit 58a87706ef
275 changed files with 18750 additions and 42524 deletions
@@ -25,6 +25,8 @@ from ..models import (
WorkflowRunORM,
WorkflowStepORM,
)
from ..utils import normalize_satellite_family
from .dinsar_naming import build_fallback_pair_key
from .envi_service import RUNTIME_DIR, _collect_task_folders, _resolve_dinsar_pair_identity, _to_local_path
from .task_service import task_service
from .workflow_service import workflow_service
@@ -33,6 +35,7 @@ from .workflow_service import workflow_service
TASK_TYPE_DINSAR_PRODUCTION = "IDL_RUN_DINSAR"
TASK_TYPE_ISCE2_DINSAR_PRODUCTION = "ISCE2_RUN"
TASK_TYPE_PYINT_DINSAR_PRODUCTION = "PYINT_RUN"
TASK_TYPE_LANDSAR_DINSAR_PRODUCTION = "LANDSAR_RUN"
RUN_STATUS_PENDING = "PENDING"
RUN_STATUS_RUNNING = "RUNNING"
RUN_STATUS_COMPLETED = "COMPLETED"
@@ -83,6 +86,8 @@ def _task_type_for_engine(engine_code: str) -> str:
return TASK_TYPE_ISCE2_DINSAR_PRODUCTION
if normalized in {"pyint", "gamma"}:
return TASK_TYPE_PYINT_DINSAR_PRODUCTION
if normalized == "landsar":
return TASK_TYPE_LANDSAR_DINSAR_PRODUCTION
raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}")
@@ -94,6 +99,8 @@ def _workflow_name_for_engine(engine_code: str) -> str:
return "dinsar_isce2_production"
if normalized in {"pyint", "gamma"}:
return "dinsar_pyint_gamma_production"
if normalized == "landsar":
return "dinsar_landsar_production"
raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}")
@@ -105,6 +112,8 @@ def _workflow_step_name_for_engine(engine_code: str) -> str:
return "Execute ISCE2 D-InSAR items"
if normalized in {"pyint", "gamma"}:
return "Execute PyINT/Gamma D-InSAR items"
if normalized == "landsar":
return "Execute LandSAR D-InSAR items"
raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}")
@@ -150,6 +159,33 @@ def _looks_like_task_dir(path: str) -> bool:
return os.path.isdir(os.path.join(path, "master")) and os.path.isdir(os.path.join(path, "slave"))
def _looks_like_landsar_task_dir(path: str) -> bool:
return os.path.isdir(os.path.join(path, "Input_Data"))
def _looks_like_landsar_raw_task_dir(path: str) -> bool:
normalized = os.path.normpath(os.path.abspath(_to_local_path(path)))
master_dir = os.path.join(normalized, "master")
slave_dir = os.path.join(normalized, "slave")
if not os.path.isdir(master_dir) or not os.path.isdir(slave_dir):
return False
def _has_lt1_file(directory: str) -> bool:
try:
with os.scandir(directory) as entries:
for entry in entries:
if not entry.is_file():
continue
name = entry.name.lower()
if name.startswith("lt1") and name.endswith((".xml", ".tif", ".tiff")):
return True
except OSError:
return False
return False
return _has_lt1_file(master_dir) and _has_lt1_file(slave_dir)
def _normalize_rerun_mode(value: Optional[str]) -> str:
normalized = str(value or "").strip().lower()
if normalized in VALID_RERUN_MODES:
@@ -182,6 +218,64 @@ def _discover_run_items(root_dir: str) -> List[Dict[str, Any]]:
return items
def _discover_landsar_run_items(root_dir: str) -> List[Dict[str, Any]]:
if _looks_like_landsar_task_dir(root_dir) or _looks_like_landsar_raw_task_dir(root_dir):
task_folders = [root_dir]
else:
task_folders = [
folder
for folder in _collect_task_folders(root_dir)
if _looks_like_landsar_task_dir(folder) or _looks_like_landsar_raw_task_dir(folder)
]
items: List[Dict[str, Any]] = []
try:
from ..dinsar_engines.landsar_engine import parse_lt1_slc_pair
except Exception:
parse_lt1_slc_pair = None
for order_index, folder in enumerate(task_folders, start=1):
task_name = os.path.basename(folder)
task_alias, pair_key, pair_meta = _resolve_dinsar_pair_identity(folder, task_name)
has_input_data = _looks_like_landsar_task_dir(folder)
has_raw_input = _looks_like_landsar_raw_task_dir(folder)
pair = (
parse_lt1_slc_pair(os.path.join(folder, "Input_Data"))
if parse_lt1_slc_pair is not None and has_input_data
else None
)
if parse_lt1_slc_pair is not None and has_input_data and not pair and not has_raw_input:
continue
if pair and not pair_meta.get("pair_key"):
pair_key = build_fallback_pair_key(
task_alias,
"||".join([pair["master_xml"], pair["slave_xml"]]),
satellite_family=normalize_satellite_family("lt1"),
)
elif not pair_meta.get("pair_key") and has_raw_input:
pair_key = build_fallback_pair_key(
task_alias,
folder,
satellite_family=normalize_satellite_family("lt1"),
)
items.append(
{
"order_index": order_index,
"task_name": task_name,
"task_alias": task_alias,
"pair_key": pair_key,
"pair_uid": pair_meta.get("pair_uid") or pair_meta.get("scene_pair_uid"),
"network_run_id": pair_meta.get("network_run_id"),
"network_edge_id": pair_meta.get("network_edge_id"),
"policy_version": pair_meta.get("policy_version"),
"selection_strategy": pair_meta.get("selection_strategy"),
"source_task_dir": folder,
"results_root_dir": os.path.join(settings.DINSAR_PRODUCT_DIR, pair_key),
}
)
return items
def _current_pointer_path_for_root(
results_root_dir: str,
*,
@@ -256,7 +350,12 @@ def _select_run_items(
num_to_process: int,
rerun_mode: Optional[str],
) -> Dict[str, Any]:
discovered_items = _discover_run_items(root_dir)
normalized_engine = str(engine_code or "").strip().lower()
discovered_items = (
_discover_landsar_run_items(root_dir)
if normalized_engine == "landsar"
else _discover_run_items(root_dir)
)
normalized_mode = _normalize_rerun_mode(rerun_mode)
skipped_completed_count = 0
+50 -7
View File
@@ -25,9 +25,15 @@ from ..models import (
from ..idl_service import get_idl_status
from .product_package_schema import CANONICAL_PACKAGE_SCHEMA
from .pairing_state_service import pairing_state_service
from .sbas_insar_catalog_service import sbas_insar_catalog_service
from .wsl_runtime_registry import wsl_runtime_registry
ALLOWED_PRODUCT_PACKAGE_SCHEMAS = {
CANONICAL_PACKAGE_SCHEMA,
"insar.gamma-ipta-sbas-run/v1",
"insar.gamma-sbas-run/v1",
}
DEFAULT_WORKER_TIMEOUT_SECONDS = 60
DEFAULT_SCHEMA_CACHE_SECONDS = 120
_SCHEMA_CACHE_LOCK = asyncio.Lock()
@@ -202,13 +208,22 @@ def _build_catalog_status(
catalog_name: str,
storage_root: str,
enabled: bool = True,
storage_roots: Optional[List[str]] = None,
) -> Dict[str, Any]:
normalized_roots = [str(item or "").strip() for item in (storage_roots or [storage_root]) if str(item or "").strip()]
if not normalized_roots and storage_root:
normalized_roots = [storage_root]
return {
"ok": False,
"catalog_name": catalog_name,
"enabled": enabled,
"storage_root": storage_root,
"storage_root_exists": os.path.isdir(storage_root),
"storage_roots": normalized_roots,
"storage_root_exists": bool(storage_root) and os.path.isdir(storage_root),
"storage_roots_status": [
{"path": root, "exists": os.path.isdir(root)}
for root in normalized_roots
],
"state_present": False,
"catalog_status": None,
"needs_rebuild": None,
@@ -228,11 +243,13 @@ async def _check_catalog(
catalog_name: str,
storage_root: str,
enabled: bool = True,
storage_roots: Optional[List[str]] = None,
) -> Dict[str, Any]:
status = _build_catalog_status(
catalog_name=catalog_name,
storage_root=storage_root,
enabled=enabled,
storage_roots=storage_roots,
)
if not enabled:
status["ok"] = True
@@ -262,6 +279,16 @@ async def _check_catalog(
status["last_message"] = state.last_message
if state.storage_root:
status["storage_root"] = state.storage_root
root_status_by_path = {
item["path"]: item
for item in status.get("storage_roots_status", [])
}
if state.storage_root not in root_status_by_path:
status.setdefault("storage_roots", []).insert(0, state.storage_root)
status.setdefault("storage_roots_status", []).insert(
0,
{"path": state.storage_root, "exists": os.path.isdir(state.storage_root)},
)
status["storage_root_exists"] = os.path.isdir(state.storage_root)
count_result = await db.execute(
@@ -273,7 +300,9 @@ async def _check_catalog(
manifest_count = int(status["manifest_count"] or 0)
needs_rebuild = bool(status["needs_rebuild"])
status["ok"] = bool(status["storage_root_exists"]) and not (
roots_status = status.get("storage_roots_status") or []
roots_exist = all(bool(item.get("exists")) for item in roots_status) if roots_status else bool(status["storage_root_exists"])
status["ok"] = roots_exist and not (
manifest_count > 0 and needs_rebuild
)
except Exception as exc:
@@ -299,10 +328,13 @@ async def _check_timeseries_result_catalog() -> Dict[str, Any]:
async def _check_sbas_insar_result_catalog() -> Dict[str, Any]:
run_roots = sbas_insar_catalog_service.get_run_roots()
primary_root = run_roots[0] if run_roots else os.path.join(settings.GAMMA_SBAS_WORK_ROOT, "runs")
return await _check_catalog(
catalog_name="sbas_insar",
storage_root=os.path.join(settings.GAMMA_SBAS_WORK_ROOT, "runs"),
enabled=bool(settings.GAMMA_SBAS_ENABLED),
storage_root=primary_root,
storage_roots=run_roots,
enabled=bool(settings.GAMMA_SBAS_ENABLED or settings.LANDSAR_SBAS_ENABLED),
)
@@ -389,6 +421,8 @@ def _sanitize_product_package_status(payload: Dict[str, Any]) -> Dict[str, Any]:
"ok": bool(payload.get("ok")),
"total_count": int(payload.get("total_count") or 0),
"canonical_count": int(payload.get("canonical_count") or 0),
"valid_schema_count": int(payload.get("valid_schema_count") or 0),
"invalid_schema_count": int(payload.get("invalid_schema_count") or 0),
"missing_manifest_count": int(payload.get("missing_manifest_count") or 0),
"missing_publish_dir_count": int(payload.get("missing_publish_dir_count") or 0),
"missing_processor_count": int(payload.get("missing_processor_count") or 0),
@@ -1033,8 +1067,11 @@ async def _check_product_packages() -> Dict[str, Any]:
status = {
"ok": False,
"canonical_schema": CANONICAL_PACKAGE_SCHEMA,
"allowed_schemas": sorted(ALLOWED_PRODUCT_PACKAGE_SCHEMAS),
"total_count": 0,
"canonical_count": 0,
"valid_schema_count": 0,
"invalid_schema_count": 0,
"missing_manifest_count": 0,
"missing_publish_dir_count": 0,
"missing_processor_count": 0,
@@ -1077,8 +1114,13 @@ async def _check_product_packages() -> Dict[str, Any]:
status["by_family"][family_key] = int(status["by_family"].get(family_key, 0)) + 1
status["by_engine"][engine_key] = int(status["by_engine"].get(engine_key, 0)) + 1
if str(package_schema or "").strip() == CANONICAL_PACKAGE_SCHEMA:
schema_key = str(package_schema or "").strip()
if schema_key == CANONICAL_PACKAGE_SCHEMA:
status["canonical_count"] += 1
if schema_key in ALLOWED_PRODUCT_PACKAGE_SCHEMAS:
status["valid_schema_count"] += 1
else:
status["invalid_schema_count"] += 1
if not str(manifest_path or "").strip() or not os.path.isfile(str(manifest_path)):
status["missing_manifest_count"] += 1
if not str(publish_dir or "").strip() or not os.path.isdir(str(publish_dir)):
@@ -1097,7 +1139,7 @@ async def _check_product_packages() -> Dict[str, Any]:
status["missing_processor_count"] == 0,
status["missing_runtime_count"] == 0,
status["missing_native_output_count"] == 0,
status["canonical_count"] == status["total_count"],
status["invalid_schema_count"] == 0,
]
)
except Exception as exc:
@@ -1432,7 +1474,8 @@ async def get_health_status(
wsl_runtime_status.get("ok"),
pairing_system_status.get("ok"),
(not settings.TIMESERIES_ENABLED) or timeseries_result_catalog_status.get("ok"),
(not settings.GAMMA_SBAS_ENABLED) or sbas_insar_result_catalog_status.get("ok"),
(not (settings.GAMMA_SBAS_ENABLED or settings.LANDSAR_SBAS_ENABLED))
or sbas_insar_result_catalog_status.get("ok"),
]
)
+252 -5
View File
@@ -91,6 +91,7 @@ JOB_TYPE_GF3_SARSCAPE_SYNC = "GF3_SARSCAPE_SYNC"
JOB_TYPE_GF3_SARSCAPE_CLEAN = "GF3_SARSCAPE_CLEAN"
JOB_TYPE_ISCE2_RUN = "ISCE2_RUN"
JOB_TYPE_PYINT_RUN = "PYINT_RUN"
JOB_TYPE_LANDSAR_RUN = "LANDSAR_RUN"
JOB_TYPE_PUBLISH_DINSAR_PRODUCTS = "PUBLISH_DINSAR_PRODUCTS"
JOB_TYPE_REBUILD_DINSAR_CATALOG = "REBUILD_DINSAR_CATALOG"
JOB_TYPE_REBUILD_PSINSAR_CATALOG = "REBUILD_PSINSAR_CATALOG"
@@ -101,6 +102,7 @@ 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"
JOB_TYPE_SBAS_LANDSAR_WORKFLOW = "SBAS_LANDSAR_WORKFLOW"
COPY_ALLOWED_STATUSES = {"PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"}
@@ -2602,6 +2604,12 @@ async def _run_wsl_dinsar_production_controller(
managed_run_dir = os.path.normpath(execution.output_dir)
managed_native_output_dir = os.path.join(managed_run_dir, "native")
if engine_code == "landsar":
landsar_work_root = str(getattr(settings, "LANDSAR_WORK_ROOT", "") or "").strip()
if landsar_work_root:
managed_native_output_dir = os.path.normpath(
os.path.join(landsar_work_root, run_key, "native")
)
managed_work_dir = os.path.join(managed_native_output_dir, "workflow")
managed_export_dir = os.path.join(managed_native_output_dir, "export")
managed_orbit_output_dir = os.path.join(managed_work_dir, "orbits")
@@ -2740,10 +2748,12 @@ async def _run_wsl_dinsar_production_controller(
task_result = ((detail.get("task_results") or [{}])[0]) if result else {}
try:
if not result or not result.success or not bool(task_result.get("success", result.success if result else False)):
result_error = str(result.error or "").strip() if result else ""
result_success = bool(result.success) if result else False
if not result or not result_success or not bool(task_result.get("success", result_success)):
error_message = (
str(task_result.get("error") or "").strip()
or str(result.error or "").strip()
or result_error
or run_exception_text
or str(task_result.get("stderr_tail") or "").strip()
or f"{engine_title} run failed."
@@ -2853,19 +2863,19 @@ async def _run_wsl_dinsar_production_controller(
await task_service.add_log(
job.task_id,
"INFO",
f"WSL command [{item_label}]: {task_result.get('command')}",
f"{engine_title} command [{item_label}]: {task_result.get('command')}",
)
if task_result.get("stdout_tail"):
await task_service.add_log(
job.task_id,
"INFO",
f"WSL stdout tail [{item_label}]:\n{task_result.get('stdout_tail')}",
f"{engine_title} stdout tail [{item_label}]:\n{task_result.get('stdout_tail')}",
)
if task_result.get("stderr_tail"):
await task_service.add_log(
job.task_id,
"WARNING",
f"WSL stderr tail [{item_label}]:\n{task_result.get('stderr_tail')}",
f"{engine_title} stderr tail [{item_label}]:\n{task_result.get('stderr_tail')}",
)
publish_result = None
@@ -3093,6 +3103,60 @@ async def _handle_pyint_run(job: SystemJobORM) -> None:
)
async def _handle_landsar_run(job: SystemJobORM) -> None:
production_run_id = str((job.payload or {}).get("production_run_id") or "").strip()
if production_run_id:
try:
await _run_wsl_dinsar_production_controller(
job,
engine_code="landsar",
engine_title="LandSAR",
fallback_timeout_seconds=int(getattr(settings, "LANDSAR_DINSAR_TIMEOUT_SECONDS", 0) or 43200),
)
except Exception as exc:
latest_message = f"LandSAR D-InSAR production controller failed: {exc}"
try:
async with AsyncSessionLocal() as db:
run = await dinsar_production_service.get_run(production_run_id, db)
if run is not None and str(run.status or "").strip().upper() not in {"COMPLETED", "FAILED", "CANCELLED"}:
summary_payload = dict(run.summary_json or {})
summary_payload["controller_error"] = str(exc)
await dinsar_production_service.finalize_run(
run,
db=db,
status="FAILED",
summary_payload=summary_payload,
latest_message=latest_message,
)
dinsar_production_service.append_run_log(
run.run_id,
f"[controller-failed] {exc}",
)
except Exception:
pass
try:
current_task = await task_service.get_task(job.task_id)
if current_task and current_task.status not in {"COMPLETED", "FAILED", "CANCELLED"}:
await task_service.add_log(job.task_id, "ERROR", latest_message)
await task_service.update_task(
job.task_id,
status="FAILED",
progress=100,
message=latest_message,
)
except Exception:
pass
raise
return
await _handle_queued_engine_run(
job,
engine_title="LandSAR",
fallback_timeout_seconds=int(getattr(settings, "LANDSAR_DINSAR_TIMEOUT_SECONDS", 0) or 43200),
)
async def _handle_water_geocode(job: SystemJobORM) -> None:
"""单景 SAR 地理编码 job handler(多视 + 地理编码 + 辐射定标)。"""
from .water_service import run_geocoding_workflow, WATER_RESULTS_DIR
@@ -4963,6 +5027,187 @@ async def _handle_sbas_gamma_workflow(job: SystemJobORM) -> None:
)
async def _handle_sbas_landsar_workflow(job: SystemJobORM) -> None:
from .landsar_sbas_service import landsar_sbas_service
payload = job.payload or {}
run_id = str(payload.get("run_id") or "").strip()
auto_select = bool(payload.get("auto_select"))
if not run_id and not auto_select:
raise ValueError("SBAS_LANDSAR_WORKFLOW requires run_id or auto_select")
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or int(
getattr(settings, "LANDSAR_SBAS_TIMEOUT_SECONDS", 0) or 172800
)
await task_service.start_task(
job.task_id,
message=(
"LandSAR SBAS auto workflow started: selecting LT-1 stack"
if auto_select
else f"LandSAR SBAS workflow started: {run_id}"
),
)
await task_service.update_task(
job.task_id,
progress=5,
message=(
"Using Gamma SBAS production-area stack discovery for LandSAR input selection..."
if auto_select
else f"Preparing LandSAR SBAS workflow: {run_id}"
),
)
last_log_at = 0.0
loop = asyncio.get_running_loop()
def _progress(event: dict[str, Any]) -> None:
nonlocal last_log_at
level = str(event.get("level") or "INFO").upper()
message = str(event.get("message") or "").strip()
if not message:
return
now = time.monotonic()
if level == "INFO" and now - last_log_at < 0.2:
return
last_log_at = now
try:
asyncio.run_coroutine_threadsafe(
task_service.add_log(job.task_id, level, message),
loop,
)
except Exception:
pass
if auto_select:
selection_request = dict(payload.get("selection_request") or {})
selection_limit = selection_request.get("limit")
if selection_limit is None:
selection_limit = 30
await task_service.add_log(
job.task_id,
"INFO",
(
"LandSAR auto workflow is reusing Gamma stack discovery: "
f"admin_region={selection_request.get('admin_region') or '-'}, "
f"min_scenes={selection_request.get('min_scenes') or '-'}"
),
)
try:
detail = await asyncio.to_thread(
landsar_sbas_service.create_run_from_best_stack,
run_label=selection_request.get("run_label"),
source_roots=selection_request.get("source_roots"),
orbit_roots=selection_request.get("orbit_roots"),
min_scenes=selection_request.get("min_scenes"),
discovery_mode=selection_request.get("discovery_mode") or "strict",
admin_region=selection_request.get("admin_region"),
aoi_bbox=selection_request.get("aoi_bbox"),
min_aoi_coverage_ratio=selection_request.get("min_aoi_coverage_ratio", 0.01),
min_common_overlap_ratio=selection_request.get(
"min_common_overlap_ratio",
settings.GAMMA_SBAS_MIN_COMMON_OVERLAP_RATIO,
),
limit=selection_limit,
dem_path=selection_request.get("dem_path"),
timeout_seconds=selection_request.get("timeout_seconds"),
import_timeout_seconds=selection_request.get("import_timeout_seconds"),
params=dict(selection_request.get("params") or {}),
)
except Exception as exc:
await task_service.add_log(job.task_id, "ERROR", f"LandSAR auto stack selection failed: {exc}")
raise
run_id = (
(detail.get("run") or {}).get("run_id")
or (detail.get("manifest") or {}).get("run_id")
or ""
)
if not run_id:
raise ValueError("LandSAR auto workflow did not create a run.")
selection = detail.get("selection") or {}
await task_service.add_log(
job.task_id,
"INFO",
(
"LandSAR auto stack selected: "
f"stack_id={selection.get('selected_stack_id') or '-'}, run_id={run_id}"
),
)
await task_service.update_task(
job.task_id,
progress=15,
message=f"LandSAR SBAS Run created from Gamma-selected stack: {run_id}",
)
runner_task = asyncio.create_task(
asyncio.to_thread(
landsar_sbas_service.execute_run,
run_id,
timeout_seconds=timeout_seconds,
progress_callback=_progress,
)
)
async def _keepalive() -> None:
while not runner_task.done():
await asyncio.sleep(30)
await task_service.update_task(
job.task_id,
progress=50,
message=f"LandSAR SBAS workflow is still running: {run_id}",
)
keepalive_task = asyncio.create_task(_keepalive())
try:
result = await runner_task
finally:
keepalive_task.cancel()
try:
await keepalive_task
except asyncio.CancelledError:
pass
manifest = result.get("manifest") or {}
workflow = manifest.get("workflow") or {}
summary = workflow.get("summary") or {}
status = str(manifest.get("status") or "").strip().upper()
if status not in {"LANDSAR_SBAS_COMPLETED", "LANDSAR_SBAS_PARTIAL"}:
failed_count = summary.get("failed_count") or manifest.get("failed_task_count") or 0
if status == "LANDSAR_SBAS_RUNTIME_UNSUPPORTED":
unsupported_count = summary.get("unsupported_proid_count") or 0
configured_proid = manifest.get("proid") or "unknown"
message = (
f"LandSAR SBAS runtime unsupported: configured proID {configured_proid} is not recognized by "
f"this LandSAR installation. failure_kind=unsupported_proid, "
f"next_stage={manifest.get('next_stage') or 'configure_landsar_sbas_runtime'}, "
f"unsupported_tasks={unsupported_count}"
)
await task_service.add_log(job.task_id, "ERROR", message)
await task_service.update_task(
job.task_id,
status="FAILED",
progress=100,
message=message,
)
raise RuntimeError(message)
raise RuntimeError(
"LandSAR SBAS workflow failed: "
f"status={status or 'UNKNOWN'}, failed={failed_count}"
)
await task_service.update_task(
job.task_id,
status="COMPLETED",
progress=100,
message=(
f"LandSAR SBAS workflow {status.lower()}: "
f"completed={summary.get('completed_count', 0)}, "
f"failed={summary.get('failed_count', 0)}"
),
)
_HANDLERS = {
JOB_TYPE_SCAN_DATA: _handle_scan_data,
JOB_TYPE_SCAN_ASSET_INVENTORY: _handle_scan_asset_inventory,
@@ -4993,6 +5238,7 @@ _HANDLERS = {
JOB_TYPE_IDL_RUN_DINSAR: _handle_idl_run_dinsar,
JOB_TYPE_ISCE2_RUN: _handle_isce2_run,
JOB_TYPE_PYINT_RUN: _handle_pyint_run,
JOB_TYPE_LANDSAR_RUN: _handle_landsar_run,
JOB_TYPE_WATER_GEOCODE: _handle_water_geocode,
JOB_TYPE_SAR_SCENE_PREPROCESS: _handle_sar_scene_preprocess,
JOB_TYPE_WATER_FLOOD: _handle_water_flood,
@@ -5009,6 +5255,7 @@ _HANDLERS = {
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,
JOB_TYPE_SBAS_LANDSAR_WORKFLOW: _handle_sbas_landsar_workflow,
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,7 +17,7 @@ def _clean_dict(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
def _kind_from_engine(engine_code: str) -> Optional[str]:
normalized = str(engine_code or "").strip().lower()
if normalized in {"envi", "sarscape"}:
if normalized in {"envi", "sarscape", "landsar"}:
return "windows"
if normalized in {"isce2", "pyint", "gamma"}:
return "wsl"
@@ -340,6 +340,8 @@ def _iter_flat_result_candidates(root_dir: str) -> Iterable[Dict[str, Any]]:
if is_standard_isce2_disp_file(normalized_root, entry.path):
source_dir = os.path.dirname(os.path.dirname(os.path.dirname(entry.path)))
run_meta = find_json_sidecar(source_dir, RUN_META_FILENAME, max_levels=0) or {}
engine_code = _first_text(run_meta.get("engine_code")) or "isce2"
source_files = [entry.path]
coh_candidates = (
os.path.join(source_dir, "assets", "coh", "coh.tif"),
@@ -350,7 +352,7 @@ def _iter_flat_result_candidates(root_dir: str) -> Iterable[Dict[str, Any]]:
source_files.append(coh_path)
break
yield {
"engine_code": "isce2",
"engine_code": engine_code,
"name": os.path.splitext(entry.name)[0],
"task_name": "",
"source_dir": source_dir,
@@ -359,6 +359,16 @@ def _build_root_specs_from_settings() -> List[RootSpec]:
owner_engine="dinsar",
)
)
specs.extend(
_iter_single_root_specs(
env_var="LANDSAR_WORK_ROOT",
path=settings.LANDSAR_WORK_ROOT,
root_role="work_root_landsar",
display_name="LandSAR Work Root",
scan_mode="workspace",
owner_engine="landsar",
)
)
specs.extend(
_iter_single_root_specs(
env_var="TIMESERIES_PRODUCT_DIR",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -45,6 +45,7 @@ from .pairing_state_service import pairing_state_service
PAIRING_POLICY_VERSION = "2026.05.raw-source.v2"
PAIRING_WARNING_CANDIDATE_THRESHOLD = 3000
PAIRING_ALL_STRATEGY_HARD_LIMIT = 20000
logger = logging.getLogger(__name__)
@@ -144,6 +145,12 @@ class SpatialService:
require_orbit_data=require_orbit_data,
)
if effective_params.strategy == "all" and len(candidate_pool) > PAIRING_ALL_STRATEGY_HARD_LIMIT:
raise RuntimeError(
f"全部配对命中 {len(candidate_pool)} 条候选边,超过系统一次性返回上限 "
f"{PAIRING_ALL_STRATEGY_HARD_LIMIT}。请改用 SBAS/Sequential 策略,或收紧 AOI、日期范围、重叠率。"
)
if len(candidate_pool) > PAIRING_WARNING_CANDIDATE_THRESHOLD:
warnings.append(
f"候选配对数超过 {PAIRING_WARNING_CANDIDATE_THRESHOLD}(当前: {len(candidate_pool)}),建议收紧参数或缩小 AOI。"