Fix D-InSAR pairing payload and center-distance controls

This commit is contained in:
2026-05-10 21:42:24 +08:00
parent 3818f6e03e
commit 6893041a9f
11 changed files with 1089 additions and 44 deletions
+9 -2
View File
@@ -21,6 +21,12 @@ MAX_SCAN_PATH_LENGTH = read_int_env(
minimum=64, minimum=64,
maximum=32767, maximum=32767,
) )
PAIRING_CENTER_DISTANCE_MAX_METERS = read_int_env(
"PAIRING_CENTER_DISTANCE_MAX_METERS",
20_000_000,
minimum=1,
maximum=50_000_000,
)
def _normalize_directory_list(value: Any, field_name: str) -> List[str]: def _normalize_directory_list(value: Any, field_name: str) -> List[str]:
@@ -246,14 +252,15 @@ class PairingRequest(BaseModel):
time_baseline_min: int = Field(default=1, ge=0, le=3650) time_baseline_min: int = Field(default=1, ge=0, le=3650)
time_baseline_max: int = Field(default=90, ge=1, le=3650) time_baseline_max: int = Field(default=90, ge=1, le=3650)
overlap_threshold: float = Field(default=0.5, ge=0.0, le=1.0) overlap_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
spatial_baseline_max_meters: int = Field(default=3000, ge=0, le=100000) spatial_baseline_max_meters: int = Field(default=3000, ge=0, le=PAIRING_CENTER_DISTANCE_MAX_METERS)
limit_footprint_center_distance: bool = False
coverage_diversity_penalty: float = Field(default=0.3, ge=0.0, le=1.0) coverage_diversity_penalty: float = Field(default=0.3, ge=0.0, le=1.0)
require_same_imaging_mode: bool = True require_same_imaging_mode: bool = True
require_same_polarization: bool = True require_same_polarization: bool = True
aoi_overlap_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0) aoi_overlap_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)
max_temporal_baseline_days: Optional[int] = Field(default=None, ge=1, le=3650) max_temporal_baseline_days: Optional[int] = Field(default=None, ge=1, le=3650)
pair_footprint_overlap_min_ratio: Optional[float] = Field(default=None, ge=0.0, le=1.0) pair_footprint_overlap_min_ratio: Optional[float] = Field(default=None, ge=0.0, le=1.0)
footprint_center_distance_max_meters: Optional[int] = Field(default=None, ge=0, le=100000) footprint_center_distance_max_meters: Optional[int] = Field(default=None, ge=0, le=PAIRING_CENTER_DISTANCE_MAX_METERS)
# === 双池日期(新增) === # === 双池日期(新增) ===
master_date_from: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$') master_date_from: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$')
+6
View File
@@ -4,6 +4,7 @@ import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from pydantic import ValidationError
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -42,6 +43,7 @@ def get_pairing_request_from_form(
time_baseline_max: int = Form(90), time_baseline_max: int = Form(90),
overlap_threshold: float = Form(0.5), overlap_threshold: float = Form(0.5),
spatial_baseline_max_meters: int = Form(3000), spatial_baseline_max_meters: int = Form(3000),
limit_footprint_center_distance: bool = Form(False),
max_temporal_baseline_days: Optional[int] = Form(None), max_temporal_baseline_days: Optional[int] = Form(None),
pair_footprint_overlap_min_ratio: Optional[float] = Form(None), pair_footprint_overlap_min_ratio: Optional[float] = Form(None),
footprint_center_distance_max_meters: Optional[int] = Form(None), footprint_center_distance_max_meters: Optional[int] = Form(None),
@@ -70,11 +72,13 @@ def get_pairing_request_from_form(
except Exception: except Exception:
satellites_list = None satellites_list = None
try:
return PairingRequest( return PairingRequest(
time_baseline_min=time_baseline_min, time_baseline_min=time_baseline_min,
time_baseline_max=time_baseline_max, time_baseline_max=time_baseline_max,
overlap_threshold=overlap_threshold, overlap_threshold=overlap_threshold,
spatial_baseline_max_meters=spatial_baseline_max_meters, spatial_baseline_max_meters=spatial_baseline_max_meters,
limit_footprint_center_distance=limit_footprint_center_distance,
max_temporal_baseline_days=max_temporal_baseline_days, max_temporal_baseline_days=max_temporal_baseline_days,
pair_footprint_overlap_min_ratio=pair_footprint_overlap_min_ratio, pair_footprint_overlap_min_ratio=pair_footprint_overlap_min_ratio,
footprint_center_distance_max_meters=footprint_center_distance_max_meters, footprint_center_distance_max_meters=footprint_center_distance_max_meters,
@@ -93,6 +97,8 @@ def get_pairing_request_from_form(
allowed_satellites=satellites_list, allowed_satellites=satellites_list,
cross_satellite_pairing=cross_satellite_pairing, cross_satellite_pairing=cross_satellite_pairing,
) )
except ValidationError as exc:
raise HTTPException(status_code=422, detail=exc.errors()) from exc
def get_ps_request_from_form( def get_ps_request_from_form(
+35 -7
View File
@@ -7,7 +7,7 @@ from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select from sqlalchemy.future import select
@@ -24,7 +24,6 @@ from ..models import (
PsTaskItem, PsTaskItem,
PsTaskItemORM, PsTaskItemORM,
RadarData, RadarData,
RadarPair,
TimeseriesStackPlanEdgeORM, TimeseriesStackPlanEdgeORM,
TimeseriesStackPlanItemORM, TimeseriesStackPlanItemORM,
TimeseriesStackPlanORM, TimeseriesStackPlanORM,
@@ -97,13 +96,41 @@ def _normalize_list_pagination(limit: int, offset: int) -> tuple[int, int]:
return safe_limit, safe_offset return safe_limit, safe_offset
class DinsarBatchSceneCreate(BaseModel):
file_path: str = Field(min_length=1)
satellite: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH)
imaging_date: Optional[str] = Field(default=None, max_length=32)
imaging_mode: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH)
polarization: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH)
model_config = ConfigDict(extra="ignore")
class DinsarBatchPairCreate(BaseModel):
master: DinsarBatchSceneCreate
slave: DinsarBatchSceneCreate
task_name: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH)
task_alias: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH)
pair_key: Optional[str] = Field(default=None, max_length=128)
pair_uid: Optional[str] = Field(default=None, max_length=64)
network_run_id: Optional[str] = Field(default=None, max_length=64)
network_edge_id: Optional[int] = None
policy_version: Optional[str] = Field(default=None, max_length=32)
selection_strategy: Optional[str] = Field(default=None, max_length=32)
time_baseline_days: Optional[int] = None
spatial_baseline_meters: Optional[float] = None
scene_center_distance_meters: Optional[float] = None
model_config = ConfigDict(extra="ignore")
class DinsarBatchCreateRequest(BaseModel): class DinsarBatchCreateRequest(BaseModel):
name: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH) name: Optional[str] = Field(default=None, max_length=BATCH_TEXT_MAX_LENGTH)
pairs: List[RadarPair] pairs: List[DinsarBatchPairCreate]
@field_validator("pairs") @field_validator("pairs")
@classmethod @classmethod
def _validate_pairs_size(cls, value: List[RadarPair]) -> List[RadarPair]: def _validate_pairs_size(cls, value: List[DinsarBatchPairCreate]) -> List[DinsarBatchPairCreate]:
if len(value) > TASK_BATCH_MAX_ITEMS: if len(value) > TASK_BATCH_MAX_ITEMS:
raise ValueError( raise ValueError(
f"pairs exceeds max item count ({TASK_BATCH_MAX_ITEMS})." f"pairs exceeds max item count ({TASK_BATCH_MAX_ITEMS})."
@@ -244,10 +271,11 @@ async def create_dinsar_batch_endpoint(
for pair in request.pairs: for pair in request.pairs:
master = pair.master master = pair.master
slave = pair.slave slave = pair.slave
task_name = pair.task_name or pair.task_alias or f"DINSAR_{master.imaging_date or 'master'}_{slave.imaging_date or 'slave'}"
item = DinsarTaskItemORM( item = DinsarTaskItemORM(
batch_id=batch_id, batch_id=batch_id,
task_name=pair.task_name, task_name=task_name,
task_alias=pair.task_alias or pair.task_name, task_alias=pair.task_alias or task_name,
pair_key=pair.pair_key, pair_key=pair.pair_key,
scene_pair_uid=pair.pair_uid, scene_pair_uid=pair.pair_uid,
network_run_id=pair.network_run_id, network_run_id=pair.network_run_id,
@@ -264,7 +292,7 @@ async def create_dinsar_batch_endpoint(
slave_imaging_date=slave.imaging_date, slave_imaging_date=slave.imaging_date,
slave_imaging_mode=slave.imaging_mode, slave_imaging_mode=slave.imaging_mode,
slave_polarization=slave.polarization, slave_polarization=slave.polarization,
time_baseline_days=pair.time_baseline_days, time_baseline_days=int(pair.time_baseline_days) if pair.time_baseline_days is not None else None,
spatial_baseline_meters=pair.spatial_baseline_meters, spatial_baseline_meters=pair.spatial_baseline_meters,
scene_center_distance_meters=( scene_center_distance_meters=(
pair.scene_center_distance_meters pair.scene_center_distance_meters
+11 -3
View File
@@ -169,12 +169,14 @@ class SpatialService:
PairingMetricCacheORM.status == "READY", PairingMetricCacheORM.status == "READY",
PairingMetricCacheORM.time_baseline_days >= params.time_baseline_min, PairingMetricCacheORM.time_baseline_days >= params.time_baseline_min,
PairingMetricCacheORM.time_baseline_days <= params.time_baseline_max, PairingMetricCacheORM.time_baseline_days <= params.time_baseline_max,
center_distance_expr <= params.spatial_baseline_max_meters,
PairingMetricCacheORM.scene_overlap_ratio >= params.overlap_threshold, PairingMetricCacheORM.scene_overlap_ratio >= params.overlap_threshold,
PairingMetricCacheORM.same_look_direction.is_(True), PairingMetricCacheORM.same_look_direction.is_(True),
) )
) )
if params.limit_footprint_center_distance:
stmt = stmt.where(center_distance_expr <= params.spatial_baseline_max_meters)
if require_orbit_data: if require_orbit_data:
stmt = stmt.where( stmt = stmt.where(
master_alias.has_orbit_data.is_(True), master_alias.has_orbit_data.is_(True),
@@ -765,9 +767,12 @@ class SpatialService:
def _score_pair_candidate(self, candidate: dict, params: PairingRequest) -> float: def _score_pair_candidate(self, candidate: dict, params: PairingRequest) -> float:
max_time = max(float(params.time_baseline_max or 1), 1.0) max_time = max(float(params.time_baseline_max or 1), 1.0)
max_center = max(float(params.spatial_baseline_max_meters or 1), 1.0)
time_score = 1.0 - min(float(candidate.get("days") or 0) / max_time, 1.0) time_score = 1.0 - min(float(candidate.get("days") or 0) / max_time, 1.0)
if params.limit_footprint_center_distance:
max_center = max(float(params.spatial_baseline_max_meters or 1), 1.0)
center_score = 1.0 - min(float(candidate.get("dist") or 0) / max_center, 1.0) center_score = 1.0 - min(float(candidate.get("dist") or 0) / max_center, 1.0)
else:
center_score = 1.0
overlap_score = min(max(float(candidate.get("overlap_ratio") or 0), 0.0), 1.0) overlap_score = min(max(float(candidate.get("overlap_ratio") or 0), 0.0), 1.0)
source_score = 1.0 if ( source_score = 1.0 if (
bool(getattr(candidate.get("master"), "insar_source_ready", False)) bool(getattr(candidate.get("master"), "insar_source_ready", False))
@@ -1215,9 +1220,12 @@ class SpatialService:
aoi_poly, aoi_poly,
) -> float: ) -> float:
max_time = max(float(params.time_baseline_max or 1), 1.0) max_time = max(float(params.time_baseline_max or 1), 1.0)
max_space = max(float(params.spatial_baseline_max_meters or 1), 1.0)
time_score = 1.0 - min(float(candidate.get("days") or 0) / max_time, 1.0) time_score = 1.0 - min(float(candidate.get("days") or 0) / max_time, 1.0)
if params.limit_footprint_center_distance:
max_space = max(float(params.spatial_baseline_max_meters or 1), 1.0)
spatial_score = 1.0 - min(float(candidate.get("dist") or 0) / max_space, 1.0) spatial_score = 1.0 - min(float(candidate.get("dist") or 0) / max_space, 1.0)
else:
spatial_score = 1.0
overlap_score = min(max(float(candidate.get("overlap_ratio") or 0), 0.0), 1.0) overlap_score = min(max(float(candidate.get("overlap_ratio") or 0), 0.0), 1.0)
source_score = 1.0 if ( source_score = 1.0 if (
bool(getattr(candidate.get("master"), "insar_source_ready", False)) bool(getattr(candidate.get("master"), "insar_source_ready", False))
+6
View File
@@ -60,6 +60,12 @@
## 4. 配对与前端导航 ## 4. 配对与前端导航
- [SENTINEL1_SYSTEM_ENHANCEMENT_MASTER_PLAN_20260510.md](SENTINEL1_SYSTEM_ENHANCEMENT_MASTER_PLAN_20260510.md)
Sentinel-1 系统增强主维护文档,汇总数据管理、精轨、配对、任务分发、Gamma/PyINT 生产、结果管理和分阶段实施边界。
- [SENTINEL1_DATA_MANAGEMENT_ADAPTATION_PLAN_20260510.md](SENTINEL1_DATA_MANAGEMENT_ADAPTATION_PLAN_20260510.md)
Sentinel-1 源数据管理、精轨匹配、配对约束、分发和后续生产适配的分阶段改造规划。
- [SENTINEL1_DINSAR_GAMMA_ISCE2_FEASIBILITY_20260510.md](SENTINEL1_DINSAR_GAMMA_ISCE2_FEASIBILITY_20260510.md)
Sentinel-1 D-InSAR 在不使用 ENVI + SARscape 核心时,基于 Gamma/PyINT 与 ISCE2 的可行性、接入边界和推荐实施顺序。
- [DINSAR_PAIRING_DISTRIBUTION_LOGIC_20260508.md](DINSAR_PAIRING_DISTRIBUTION_LOGIC_20260508.md) - [DINSAR_PAIRING_DISTRIBUTION_LOGIC_20260508.md](DINSAR_PAIRING_DISTRIBUTION_LOGIC_20260508.md)
2026-05-08 源码走读记录,梳理 D-InSAR 配对缓存、策略筛选、批次保存、数据分发和生产 worker 执行链路。 2026-05-08 源码走读记录,梳理 D-InSAR 配对缓存、策略筛选、批次保存、数据分发和生产 worker 执行链路。
- [PAIRING_ENHANCEMENT_DESIGN.md](PAIRING_ENHANCEMENT_DESIGN.md) - [PAIRING_ENHANCEMENT_DESIGN.md](PAIRING_ENHANCEMENT_DESIGN.md)
@@ -0,0 +1,340 @@
# Sentinel-1 数据管理改造规划
日期:2026-05-10
本文只规划“让系统能管理哨兵一号 Sentinel-1 数据”的改造范围,不记录已完成实现。当前建议把目标分成两层:先支持 Sentinel-1 源数据入库、检索、预览、配对和分发;真正进入 ISCE2 / PyINT / SARscape 的 Sentinel-1 自动生产链路,作为后续独立阶段处理。
## 1. 目标边界
本轮建议做:
1. 扫描并登记 Sentinel-1 SLC `.SAFE` 解包目录。
2. 解析 Sentinel-1 基础元数据,包括卫星、成像模式、极化、起止时间、绝对轨道、相对轨道、升降轨、footprint。
3. 管理 Sentinel-1 精密轨道 `.EOF` 文件,并把影像按 acquisition time 匹配到覆盖该时间窗的轨道文件。
4. 在源数据检索、配对规划、任务批次和数据分发中正确显示和使用 Sentinel-1。
5. 对 Sentinel-1 配对增加必要的轻量约束,避免只靠 footprint 交叠产生不科学的候选对。
本轮不建议做:
1. 不把 Sentinel-1 自动送入现有 LT1 专用 ISCE2 / PyINT 流水线。
2. 不做 burst 级精确公共覆盖计算。
3. 不计算真实垂直基线,不把 footprint 中心距伪装成 SAR 空间基线。
4. 不强制支持 `.SAFE.zip` 直接入库;优先支持已解包的 `.SAFE` 目录,zip 可作为后续增强。
## 2. 当前代码现状
已有基础:
- [backend/app/utils.py](../backend/app/utils.py) 已经有 `parse_s1_radar_filename()``parse_s1_orbit_filename()` 的雏形。
- [backend/app/services/data_service.py](../backend/app/services/data_service.py) 的 `scan_radar_data()` 是通用源数据扫描入口,理论上可以扫描 LT1 / S1 / GF3。
- `radar_data` 已有 `satellite``satellite_family``imaging_date``imaging_mode``polarization``orbit_direction``product_type``source_product_token``has_orbit_data``orbit_file_path``geom``coverage_polygon` 等通用字段。
- [backend/app/services/pairing_cache_service.py](../backend/app/services/pairing_cache_service.py) 已经把 `S1A/S1B` 归为 `S1` family,配对层具备同卫星族筛选能力。
- [backend/app/copier.py](../backend/app/copier.py) 分发逻辑是复制原始产品目录,不再依赖 `envi_import`,这对 `.SAFE` 目录是有利的。
主要缺口:
- Sentinel-1 `.SAFE` 的关键元数据主要在 `manifest.safe``annotation/*.xml`,当前 `find_xml_file()` / `parse_xml_metadata()` 更偏 LT1 风格 XML,不能可靠解析 S1 footprint、升降轨、相对轨道。
- 当前精轨模块 [backend/app/services/orbit_converter.py](../backend/app/services/orbit_converter.py) 基本写死 LT1 `LT1A_GpsData_GAS_C_YYYYMMDD.txt`,并会同步/转换到 ENVI、ISCE2 池;Sentinel-1 `.EOF` 不能走这套转换逻辑。
- 当前配对缓存只要求同升降轨、同 look direction、footprint 相交、同卫星族、同模式/极化等。对 Sentinel-1 来说还缺少“同相对轨道/同轨道方向/同 beam mode/必要时同 slice 或同 burst 覆盖”的约束。
- 前端卫星组 [frontend/src/config/appConstants.js](../frontend/src/config/appConstants.js) 目前只有 LT-1、GF-3;配对默认 `allowed_satellites``LT1A/LT1B`,导入 S1 后如果不改,会默认把 S1 排除。
- 现有 ISCE2 / PyINT 生产脚本明显是 LT1 专用,不能因为管理了 S1 数据就默认允许一键生产。
## 3. Sentinel-1 文件名能提供的信息
标准 Sentinel-1 SLC SAFE 名称通常类似:
```text
S1A_IW_SLC__1SDV_20250101T104105_20250101T104132_XXXXXX_YYYYYY_ZZZZ.SAFE
```
可直接从文件名得到:
- `satellite`: `S1A` / `S1B`
- `satellite_family`: `S1`
- `imaging_mode`: `IW` / `EW` / `SM`
- `product_type`: `SLC`
- `product_level`: 可从 `1SDV` 中解析出 level 1
- `polarization`: `DV``DH``SV``SH`
- `acquisition_start_time_utc`
- `acquisition_stop_time_utc`
- `imaging_date`
- `absolute_orbit`,即文件名中的 6 位轨道号
- datatake id / product unique id
文件名通常不能可靠提供:
- footprint
- orbit direction,升轨/降轨
- relative orbit / track
- look direction
- burst 范围
- 精轨覆盖关系
因此只靠文件名可以完成“粗入库”,但要做可靠配对,至少还要读取 `manifest.safe` 或 annotation XML。
## 4. 建议新增的元数据模型
为了控制工作量,不建议把 Sentinel-1 所有元数据拆成大量列。建议采用“少量关键列 + JSON 扩展”的方式。
建议新增字段:
- `acquisition_start_time_utc`
- `acquisition_stop_time_utc`
- `relative_orbit`
- `absolute_orbit`
- `slice_number`
- `sensor_operational_mode`,可与现有 `imaging_mode` 保持一致或作为补充
- `swath_list`,可用 JSON 或逗号文本保存 `IW1/IW2/IW3`
- `source_format`,例如 `SAFE_DIR``SAFE_ZIP``LT1_DIR`
- `metadata_json`,保存 Sentinel-1 专用扩展信息
如果想更轻量,也可以第一阶段只增加:
- `relative_orbit`
- `acquisition_start_time_utc`
- `acquisition_stop_time_utc`
- `metadata_json`
这四个字段已经足够支撑 Sentinel-1 基础管理和更科学的配对过滤。
数据库处理要求:
- 新增 ORM 字段和 Pydantic schema 字段。
- 新增幂等迁移 `010_sentinel1_source_fields.sql`
- 把该迁移加入 [backend/app/db_maintenance.py](../backend/app/db_maintenance.py) 的 `MIGRATION_FILES`
- 健康检查里至少确认新增列存在;如果后续扩展精轨库存表,再把库存一致性纳入健康检查。
## 5. 入库扫描设计
建议把现有解析逻辑拆成“产品类型适配器”:
```text
SourceProductParser
- LT1 parser
- Sentinel1 SAFE parser
- GF3 parser
```
Sentinel-1 parser 负责:
1. 识别 `.SAFE` 目录。
2. 从目录名解析粗元数据。
3. 读取 `manifest.safe`
4. 读取 annotation XML 获取或校正:
- pass / orbit direction
- relative orbit
- absolute orbit
- footprint 坐标
- start / stop time
- polarization / swath
5. 生成 `coverage_polygon``geom``min/max lon/lat`
6. 判断 `insar_source_ready`
- 必须是 `SLC`
- 必须有 footprint
- 必须有 imaging date / acquisition time
- 必须有 orbit direction
- Sentinel-1 推荐必须有 relative orbit
兼容策略:
- 已解包 `.SAFE` 目录优先。
- `.SAFE.zip` 不在第一阶段直接解析,除非用户明确需要。后续可以在扫描器里只读 zip 内 `manifest.safe`,但这会明显增加实现和测试量。
- 预览图优先复用 SAFE 内 `preview/quick-look.png` 或其他 quicklook 文件;当前 `find_radar_preview_source()` 已有关键词扫描,可少量适配。
## 6. Sentinel-1 精轨管理设计
Sentinel-1 精轨文件是 `.EOF`,匹配逻辑不是“卫星 + 日期等于影像日期”,而是:
```text
orbit.satellite == scene.satellite
and orbit.validity_start <= scene.acquisition_time <= orbit.validity_stop
```
建议新增一个 Sentinel-1 orbit inventory 解析路径:
- 解析 `S1A/S1B`
- 解析轨道类型:`AUX_POEORB` / `AUX_RESORB`
- 解析 validity start / stop
- 记录原始 `.EOF` 路径
第一阶段可以不建独立轨道表,仍然把匹配结果写回 `radar_data.has_orbit_data``radar_data.orbit_file_path`。但代码上要避免把 `.EOF` 送进 LT1 的 txt->xml 转换流程。
更稳的设计是后续新增 `orbit_files` 表:
- `satellite`
- `satellite_family`
- `orbit_type`
- `validity_start_utc`
- `validity_stop_utc`
- `file_path`
- `metadata_json`
考虑到用户希望初级任务不要太复杂,第一阶段建议先不建表,只做扫描时内存索引和 `radar_data` 回填。
## 7. 配对逻辑改造
当前配对缓存可继续使用,但 Sentinel-1 应增加轻量硬约束。
建议硬约束:
-`satellite_family = S1`
- 默认允许 `S1A/S1B` 跨星,但必须同 Sentinel-1 family
-`orbit_direction`
-`relative_orbit`
-`imaging_mode`
-`polarization` 或至少主极化兼容
- footprint 有交叠
- `insar_source_ready = true`
建议筛选指标:
- `time_baseline_days`
- `scene_overlap_ratio`
- `scene_center_distance_meters`,只作为 footprint 中心距,不叫真实空间基线
- `has_orbit_data`
不建议第一阶段做:
- 真实垂直基线计算
- TOPS burst 级 overlap
- 自动下载 ASF / Copernicus metadata
- Sentinel-1 burst id 图层化管理
配对缓存字段建议:
-`pairing_metric_cache` 增加 `same_relative_orbit` 或通用 `same_track`
- 或者在 SQL 硬约束里按 `radar_data.relative_orbit` 直接过滤。
-`selection_meta_json` 中记录 `relative_orbit``orbit_direction``source_family`,便于追溯。
## 8. 数据分发设计
分发层整体可以复用:
- master 复制完整 `.SAFE` 目录到 `Task_*/master/`
- slave 复制完整 `.SAFE` 目录到 `Task_*/slave/`
- 如果启用精轨分发,则把 `.EOF` 复制到 `Task_*/orbit/`
- `.dinsar_pair.json` 中增加 Sentinel-1 专用元数据,如 relative orbit、acquisition start/stop、orbit EOF 路径
需要注意:
- `.SAFE` 目录可能很大,zip 分发开关已经存在的话可以复用。
- 不建议在分发阶段裁剪 SAFE 或抽取 measurement 文件,这会把“数据管理”变成“预处理”。
- 如果源数据是 `.SAFE.zip`,第一阶段可以要求用户先解包;否则分发逻辑要支持复制 zip 并在生产侧再解包。
## 9. 前端改造
需要处理的点:
- `SATELLITE_GROUPS` 增加 `S1` / `Sentinel-1`
- 源数据检索高级字段增加相对轨道、起止时间、源格式等字段。
- 配对弹窗默认不要固定 `LT1A/LT1B`;建议默认“不限定卫星”,或者根据当前检索结果自动选择可见卫星。
- 配对参数文案继续使用“footprint 中心距上限”,不要改回“空间基线”。
- 对 Sentinel-1 配对显示 `relative_orbit``orbit_direction`,让用户一眼能看出是不是同轨道。
- 生产提交页如果任务包是 Sentinel-1,但选择了 LT1 专用 PyINT/ISCE2 profile,应给出阻止或强提示。
## 10. 生产链路边界
现有生产引擎不应直接宣布支持 Sentinel-1
- ISCE2 当前 pipeline 和 profile 明显是 LT1 定制,包含 LT1 wavelength、LT1 输入 resolver、LT1 轨道转换。
- PyINT / Gamma 当前输入搜索是 `LT1*.tar.gz` / `LT1*.tiff`,也属于 LT1 专用。
- SARscape 理论上能处理 Sentinel-1,但当前系统侧是否能用 raw SAFE 驱动 SARscape D-InSAR,需要单独验证模板、参数和 task runner。
所以建议第一阶段只做到:
```text
Sentinel-1 数据管理 -> 配对规划 -> 批次保存 -> 数据分发
```
生产执行留到第二阶段:
```text
Sentinel-1 Task_* -> 指定 Sentinel-1 engine/profile -> 预检 -> 生产
```
## 11. 实施阶段建议
### 阶段 1:管理与检索
工作内容:
- 完成 Sentinel-1 SAFE parser。
-`manifest.safe` / annotation XML 提取 footprint、relative orbit、pass、start/stop time。
- 支持 `.EOF` 精轨扫描和时间窗匹配。
- 增加必要数据库字段和迁移。
- 前端增加 Sentinel-1 过滤和显示。
预估工作量:2-4 天。
风险:主要在不同 SAFE 版本 XML 结构差异,需要准备 3-5 个真实样本覆盖 S1A/S1B、IW、单双极化。
### 阶段 2:配对与分发
工作内容:
- 配对缓存加入 `relative_orbit` 约束。
- 配对结果、批次 item、`.dinsar_pair.json` 保留 Sentinel-1 扩展元数据。
- 分发 `.SAFE` + `.EOF`
- 前端配对弹窗默认卫星选择调整。
预估工作量:1-2 天。
风险:如果数据库新增字段后没有正确标 dirty,需要强制重建 pairing cache。
### 阶段 3:生产预检保护
工作内容:
- 识别 task source family。
- Sentinel-1 批次提交到 LT1 专用 engine/profile 时阻止。
- 在生产面板显示“当前仅支持管理和分发,生产适配待完成”。
预估工作量:0.5-1 天。
风险:低。
### 阶段 4Sentinel-1 生产适配
工作内容:
- 选择 SARscape、ISCE2 TOPS、GMTSAR 或其他处理链。
- 设计 Sentinel-1 专用 engine/profile。
- 做 TOPS 配准、轨道、DEM、burst overlap、输出发布包和 catalog 适配。
预估工作量:按引擎不同约 1-3 周,不建议和第一阶段混在一起。
## 12. 推荐最小方案
如果目标是“先把系统能管理哨兵数据”,推荐最小闭环如下:
1. 只支持已解包 `.SAFE` 目录入库。
2. 只解析 SLC 产品。
3. 只新增 `relative_orbit``acquisition_start_time_utc``acquisition_stop_time_utc``metadata_json` 四类关键字段。
4. `.EOF` 精轨只做匹配和路径保存,不做转换。
5. 配对只增加同 relative orbit 约束,不做真实空间基线。
6. 分发复制完整 SAFE 和 EOF。
7. 生产页阻止 Sentinel-1 进入 LT1 专用 profile。
这个方案对现有 LT1 链路侵入最小,能让 Sentinel-1 数据先进入“可检索、可配对、可分发、可追踪”的状态。
## 13. 验收标准
最小验收:
- 扫描一个包含 S1A/S1B `.SAFE` 的目录后,`radar_data` 能看到 S1 数据。
- 源数据检索可以按 Sentinel-1、IW、SLC、极化、升降轨、相对轨道过滤。
- 每个 S1 scene 有 footprint,可在地图上显示。
- 有匹配 `.EOF``has_orbit_data = true``orbit_file_path` 指向 EOF。
- 配对结果不会跨 relative orbit。
- 批次保存和数据分发能生成 `Task_*`,其中包含 master/slave SAFE 和可选 orbit EOF。
- Sentinel-1 批次不会误提交到 LT1 专用生产 profile。
## 14. 需要用户确认的输入
开工前最好确认:
1. Sentinel-1 数据池是 `.SAFE` 解包目录,还是 `.SAFE.zip` 为主。
2. 精轨 `.EOF` 是否已经本地保存,还是希望后续系统自动下载。
3. 第一阶段是否只做管理/配对/分发,不接生产。
4. 是否接受新增少量数据库字段和一份幂等迁移。
@@ -0,0 +1,257 @@
# Sentinel-1 D-InSAR Gamma / ISCE2 可行性评估
日期:2026-05-10
本文只评估设计,不涉及代码修改。重点回答:在不使用 ENVI + SARscape 核心的前提下,现有 D-InSAR 生产链路是否适合接入 Sentinel-1Gamma/PyINT 与 ISCE2 两条路线的实际工作量和风险分别是什么。
## 1. 结论
Gamma/PyINT 路线适合优先做 Sentinel-1 D-InSAR。
`D:\Code\PyINT` 中的专家代码对 Sentinel-1 支持比较完整,尤其是 Sentinel-1 TOPS/IW 的 ZIP 导入、精轨下载、burst 选择、TOPS burst 裁剪、ScanSAR/TOPS 配准、差分干涉、解缠、地理编码等流程。它不是只停留在文件名解析层面,而是已经围绕 GAMMA 命令组织了 Sentinel-1 的实际处理脚本。
ISCE2 本身也能处理 Sentinel-1 TOPS 数据,但本项目当前接入的 ISCE2 托管链路是 `lt1_stripmap`,核心是 LT-1 stripmap 的 `stripmapApp.py` 流程,不是 Sentinel-1 TOPS 的 `topsApp.py` 流程。因此 ISCE2 的 Sentinel-1 能力不能直接等价为“当前系统里马上可用”。如果要做,属于新增一个 `s1_tops` 生产 profile,工作量明显大于复用 PyINT/Gamma 的 Sentinel-1 脚本。
推荐顺序:
1. 先做 `s1_gamma_dinsar` profile,复用 `D:\Code\PyINT` 的 Sentinel-1 Gamma 处理路线。
2. 配对阶段保持轻量,只做同轨、同模式、同方向、同极化、覆盖重叠、时间基线等前置筛选。
3. 真正的垂直基线、可处理性和失败原因交给 Gamma/PyINT 在 SLC/RSLC 生成后校验和回填。
4. ISCE2 的 `s1_tops` profile 放到第二阶段,等 Gamma 路线跑通后再做。
## 2. 本项目当前 D-InSAR 引擎状态
### 2.1 PyINT/Gamma 当前是 LT-1 专用接入
本项目现有 PyINT 托管入口主要围绕 `lt1_gamma_dinsar`
- `.env` 中当前 PyINT/Gamma D-InSAR 配置说明面向 LT-1。
- `backend/app/services/pyint_engine.py` 当前只接受 `lt1_gamma_dinsar`
- `backend/app/pyint_pipeline/run_lt1_pyint_pipeline.py` 是 LT-1 专用 runner。
- `backend/app/services/pyint_input_assets_service.py` 负责 LT-1 归档、LT-1 精轨和输入资产准备。
这说明当前系统“接入 PyINT”不等于“已经接入 Sentinel-1 PyINT”。要支持 Sentinel-1,合理做法是新增一个 profile,而不是把 LT-1 profile 混改成多卫星逻辑。
### 2.2 ISCE2 当前是 LT-1 stripmap 接入
本项目现有 ISCE2 托管入口主要围绕 `lt1_stripmap`
- `.env` 中配置的是 `ISCE2_PROFILE=lt1_stripmap`
- 当前 pipeline 脚本是 `backend/app/isce2_pipeline/run_lt1_dinsar_pipeline.py`
- 当前使用的是 `stripmapApp.py`,不是 Sentinel-1 TOPS 常用的 `topsApp.py`
- WSL runtime registry 允许的 ISCE2 profile 目前也是 LT-1 stripmap 方向。
因此,ISCE2 对 Sentinel-1 的支持在算法生态上成立,但在本项目现有托管实现里还没有落地。
## 3. `D:\Code\PyINT` 对 Sentinel-1 的支持证据
`D:\Code\PyINT` 的 README 和用户指南明确把 Sentinel-1 作为支持对象。代码中也存在成套 Sentinel-1 处理脚本:
- `pyint/down2slc_sen.py`
Sentinel-1 ZIP 下载数据转 SLC。它会调用 `eof` 获取精轨,并通过 GAMMA 相关脚本读取 TOPS SLC。
- `pyint/down2slc_sen_all.py`
批量发现 `DOWNLOAD/S1*.zip`,按日期调用 Sentinel-1 SLC 生成逻辑。
- `pyint/slc_sen_cat.py`
老版本 Sentinel-1 SLC 导入与拼接逻辑,说明 PyINT 对多 swath / 多 burst / 多文件拼接有历史支持。
- `pyint/extract_s1_bursts.py`
基于 master/slave 的 burst 参数,裁剪共同 burst 区间。
- `pyint/coreg_s1_gamma.py`
使用 GAMMA `ScanSAR_coreg.py` 做 Sentinel-1 TOPS/ScanSAR 配准。
- `pyint/raw2ifg_s1.py`
Sentinel-1 单对 D-InSAR 的一站式流程:下载数据转 SLC、burst 提取、DEM、配准、差分干涉、解缠、地理编码。
- `pyint/select_pairs.py`
基于 GAMMA `base_calc` 计算基线并按 `max_tb``max_sb` 等约束选网。
这批脚本说明 PyINT 的 Sentinel-1 支持不是临时拼出来的文件名适配,而是围绕 GAMMA Sentinel-1 TOPS 处理能力组织出的完整流程。
## 4. Gamma/PyINT 接入方案
### 4.1 新增 profile
建议新增独立 profile
```text
s1_gamma_dinsar
```
不要复用或扩展 `lt1_gamma_dinsar` 的内部假设。两者输入资产、轨道文件、SLC 生成方式、配准方式和模板字段都不同。
最小改造对象:
- 新增 Sentinel-1 输入资产准备逻辑。
- 新增 Sentinel-1 PyINT runner,例如 `run_s1_pyint_pipeline.py`
- 生产引擎 registry 增加 `s1_gamma_dinsar`
- 任务 preflight 增加 Sentinel-1 ZIP、EOF、GAMMA 命令、模板字段检查。
- 输出 catalog 增加 Sentinel-1 结果归档映射。
### 4.2 输入数据约定
PyINT 现有 Sentinel-1 脚本默认从:
```text
$SCRATCHDIR/<project>/DOWNLOAD/S1*.zip
```
发现数据。
因此最省事、风险最低的约定是:系统管理 Sentinel-1 时尽量保留原始 `.zip` 产品,并在生产分发时把 master/slave 的 ZIP 放入 PyINT 项目的 `DOWNLOAD` 目录。
如果数据池里只有解压后的 `.SAFE` 目录,当前 PyINT 脚本不能直接等价复用。可选方案有三个:
1. 要求 Sentinel-1 生产任务必须绑定原始 ZIP。
2. 在分发阶段把 `.SAFE` 目录重新打包成 ZIP。
3. 改造 PyINT 的 Sentinel-1 导入脚本,让它直接接受 `.SAFE` 路径。
推荐第 1 种。第 2 种会增加磁盘和时间成本,第 3 种会扩大对专家代码的修改面。
### 4.3 单对 D-InSAR 最小流程
对于本系统的 D-InSAR 配对任务,建议先做“系统选出一对,PyINT 处理这一对”,而不是马上做完整时序网。
可控 runner 可以按以下顺序执行:
1. 准备 `$SCRATCHDIR/<project>/DOWNLOAD`,放入 master/slave Sentinel-1 ZIP。
2. 写入 `$TEMPLATEDIR/<project>.template`,包含 masterDate、slaveDate、swath、burst、look、轨道、DEM、网络参数等。
3. 调用 `down2slc_sen.py <project> <masterDate>`
4. 调用 `down2slc_sen.py <project> <slaveDate>`
5. 调用 `extract_s1_bursts.py <project> <masterDate> <slaveDate>`
6. 调用 `generate_rdc_dem.py <project>` 或等价 DEM 生成步骤。
7. 调用 `coreg_s1_gamma.py <project> <masterDate> <slaveDate>`
8. 调用 `diff_gamma.py <project> <masterDate> <slaveDate>`
9. 调用 `unwrap_gamma.py <project> <masterDate> <slaveDate>`
10. 调用 `geocode_gamma.py <project> <masterDate>-<slaveDate>`
也可以参考 `raw2ifg_s1.py` 作为端到端样板,但不建议生产系统直接无控制地调用全流程脚本。托管系统应该显式控制每一步、日志、失败原因、输出发布和清理策略。
### 4.4 精轨处理
PyINT 的 Sentinel-1 脚本会使用 `eof` / OPOD 目录获取精轨。系统前面已经设计了 Sentinel-1 精轨管理和分发逻辑,因此两者可以分工:
- 数据管理层负责发现、缓存、匹配和可选分发 EOF。
- PyINT runner 负责把 EOF 目录暴露给 PyINT/GAMMA。
- preflight 负责确认 master/slave 覆盖时段都有可用 EOF。
这样可以避免每次生产任务都临时联网下载精轨,也能保证任务可复现。
## 5. ISCE2 接入方案
ISCE2 Sentinel-1 TOPS 路线理论上可行,但本项目当前没有现成托管实现。
如果做 ISCE2,需要新增:
- `s1_tops` profile。
- `topsApp.py` 路径配置,例如 `ISCE2_TOPS_APP`
- Sentinel-1 SAFE/ZIP 输入准备逻辑。
- EOF 轨道文件挂载逻辑。
- `topsApp.py` XML 生成器。
- topsStack 或 topsApp 的执行脚本。
- Sentinel-1 TOPS 输出目录识别、catalog 发布和日志解析。
- 与 LT-1 `stripmapApp.py` profile 隔离的健康检查。
这条路线的主要风险不是 ISCE2 不支持 Sentinel-1,而是当前系统的 ISCE2 封装抽象是为 LT-1 stripmap 写的。直接把 Sentinel-1 塞进现有 `lt1_stripmap` runner 会形成大量条件分支,后期维护会很差。
如果要做,建议完全独立成 `s1_tops`,不要污染 LT-1 runner。
## 6. 配对与基线策略
Sentinel-1 的配对建议分为两层。
第一层是系统内的轻量预筛选:
- 同一 relative orbit。
- 同一升降轨方向。
- 同一 beam mode,例如 IW。
- 极化兼容,例如 VV 对 VV、VH 对 VH。
- 覆盖范围有足够重叠。
- 时间基线在阈值内。
- 产品级别满足生产要求,优先 SLC。
- EOF 精轨可获得。
第二层是处理引擎内的精确校验:
- SLC/TOPS 导入是否成功。
- master/slave 是否有共同 burst。
- DEM 覆盖是否足够。
- GAMMA `base_calc` 计算出的垂直基线是否超过阈值。
- TOPS 配准质量是否达标。
- 干涉、解缠、地理编码是否生成有效产物。
这符合“配对是初级任务,不要太复杂”的目标。系统配对不应该冒充严密的 SAR 处理器;它只需要筛出大概率能跑的候选对。真正的物理基线和可处理性,由 GAMMA/ISCE2 在生成 SLC/RSLC 后确认。
注意命名上应避免再把 footprint 质心距离叫作 `spatial_baseline_meters`。对 Sentinel-1 更合理的字段区分是:
- `centroid_distance_meters`:覆盖 footprint 质心距离,只是几何覆盖近似指标。
- `temporal_baseline_days`:时间基线。
- `perpendicular_baseline_meters`:由处理引擎计算或轨道模型计算出的垂直基线。
## 7. 工作量评估
### Gamma/PyINT 优先路线
前提:Sentinel-1 数据管理、ZIP 保留、EOF 匹配和分发机制已经具备。
预计工作量:
- 最小可用单对 D-InSAR:约 4 到 7 个工作日。
- 加上稳定的前端配置、健康检查、日志归档、失败原因归类和输出发布:约 1 到 2 周。
- 扩展到时序网、自动 burst 推荐、多 pair 网络:另算,不建议第一阶段做。
主要风险:
- 原始数据是否保留 ZIP。
- WSL/GAMMA 环境中的 Sentinel-1 命令是否完整。
- `eof` 工具和 OPOD 目录是否稳定。
- PyINT 模板字段默认值是否适配当前项目。
- 大量 ZIP、SLC、RSLC 中间产物带来的磁盘压力。
### ISCE2 Sentinel-1 路线
预计工作量:
- 可用 demo:约 1 周。
- 托管生产级接入:约 2 周或更长。
主要风险:
- 当前 `isce2_engine.py` 和 WSL registry 偏 LT-1 stripmap。
- 需要新增 topsApp/topsStack 方向的 XML、输入资产和产物识别。
- 输出结构和错误日志与当前 LT-1 ISCE2 产物不一致。
- 对系统健康检查和 profile registry 的影响更大。
## 8. 推荐实施路线
第一阶段只做 Gamma/PyINT Sentinel-1 单对 D-InSAR
1. 完成 Sentinel-1 数据管理文档中的数据入库、元数据、EOF 匹配和分发约定。
2. 新增 `s1_gamma_dinsar` profile。
3. 新增 Sentinel-1 PyINT runner,显式调用 `D:\Code\PyINT` 中已存在的 Sentinel-1 脚本链。
4. 生产任务只接受 master/slave 两景 SLC ZIP。
5. 配对表只保存轻量预筛选指标和处理后回填的真实 `perpendicular_baseline_meters`
6. 跑通后再考虑自动 burst 推荐、多 pair 网络和 ISCE2 `s1_tops`
第二阶段再做 ISCE2 Sentinel-1
1. 新增 `s1_tops` profile。
2. 使用 `topsApp.py` 或 topsStack,不复用 LT-1 stripmap runner。
3. 与 Gamma 路线共享数据管理和 EOF 匹配能力。
4. 输出 catalog 与生产日志保持同一前端体验。
## 9. 验收标准
Gamma/PyINT Sentinel-1 接入的第一阶段验收建议如下:
- 同一对 Sentinel-1 ZIP 可以从任务分发目录进入 PyINT `DOWNLOAD`
- preflight 能明确报告 ZIP、EOF、DEM、GAMMA 命令和模板字段是否齐全。
- master/slave 能成功生成 SLC。
- 能识别共同 burst 并完成裁剪。
- 能完成 TOPS 配准并生成 RSLC。
- 能生成差分干涉图、解缠结果和地理编码结果。
- 生产日志能展示每一步命令、耗时、退出码和失败原因。
- 结果 catalog 能发布核心产物。
- 配对记录能回填真实 `perpendicular_baseline_meters`,同时保留原始 `centroid_distance_meters`
## 10. 总体判断
不使用 ENVI + SARscape 核心没有问题。对 Sentinel-1 D-InSAR 来说,本项目更应该优先利用 Gamma/PyINT 的专家代码路线。
Gamma/PyINT 是“已有专家脚本,需要做系统托管适配”;ISCE2 是“算法生态支持,但本项目需要新增 tops profile”。所以第一阶段选择 Gamma/PyINT 更稳、更快,也更符合“配对不要做得过度复杂,但生产尽可能有精度”的目标。
@@ -0,0 +1,347 @@
# Sentinel-1 系统增强总纲
日期:2026-05-10
本文作为 Sentinel-1 增强工作的主维护文档。后续如果继续推进 Sentinel-1 数据管理、D-InSAR 配对、Gamma/PyINT 生产和结果归档,优先维护本文;更细的调研材料保留为参考:
- [SENTINEL1_DATA_MANAGEMENT_ADAPTATION_PLAN_20260510.md](SENTINEL1_DATA_MANAGEMENT_ADAPTATION_PLAN_20260510.md)
- [SENTINEL1_DINSAR_GAMMA_ISCE2_FEASIBILITY_20260510.md](SENTINEL1_DINSAR_GAMMA_ISCE2_FEASIBILITY_20260510.md)
## 1. 总体目标
让当前系统能够管理 Sentinel-1 数据,并把 Sentinel-1 D-InSAR 生产纳入现有生产管理和结果管理体系。
推荐边界是:
- 系统负责数据管理、元数据解析、精轨匹配、配对、任务分发、生产调度、日志、结果归档和前端展示。
- Gamma + PyINT 负责 Sentinel-1 D-InSAR 的处理内核,包括 SLC 导入、burst 裁剪、TOPS 配准、差分干涉、解缠和地理编码。
- ISCE2 Sentinel-1 TOPS 作为后续可选 profile,不作为第一阶段主线。
第一阶段不要求系统自己实现 SAR 核心算法,也不要求配对阶段计算真实垂直基线。
## 2. 总体架构
建议新增一条独立 Sentinel-1 Gamma 生产链:
```text
Sentinel-1 数据池
-> 数据扫描与元数据入库
-> EOF 精轨管理与匹配
-> D-InSAR 轻量配对
-> 任务批次保存
-> Task 目录分发
-> s1_gamma_dinsar profile
-> PyINT + Gamma 处理
-> 结果扫描与 catalog 发布
-> 生产管理 / 结果管理前端展示
```
关键原则:
- Sentinel-1 不混进 LT-1 专用的 `lt1_gamma_dinsar``lt1_stripmap`
- 新增 `s1_gamma_dinsar` profile 作为第一阶段生产入口。
- 真实 `perpendicular_baseline_meters` 由 Gamma/PyINT 在生成 SLC/RSLC 后计算或回填。
- footprint 质心距离只能叫 `centroid_distance_meters`,不能再叫空间基线。
## 3. 数据管理
### 3.1 支持的数据形态
第一阶段建议以 Sentinel-1 SLC ZIP 为生产标准输入。
系统可以管理两类形态:
- `.SAFE` 目录:适合浏览、入库、解析元数据。
- `.zip` 原始产品:适合 PyINT/Gamma 生产分发。
如果数据池只有 `.SAFE` 目录,也可以入库和配对;但进入 PyINT/Gamma 生产时,建议要求关联原始 ZIP。原因是 `D:\Code\PyINT` 的 Sentinel-1 脚本默认从 `DOWNLOAD/S1*.zip` 发现和导入数据。
### 3.2 元数据解析
文件名可以提供粗信息:
- `S1A` / `S1B`
- `IW` / `EW` / `SM`
- `SLC`
- 极化组合,例如 `DV``DH``SV``SH`
- 起止时间
- absolute orbit
- datatake id / product id
可靠配对还需要从 `manifest.safe` 或 annotation XML 中解析:
- relative orbit
- 升降轨方向
- footprint
- swath / burst 相关信息
- 更完整的极化和产品结构
建议采用“关键列 + JSON 扩展”的模型。关键列服务检索、筛选和配对;JSON 保存 Sentinel-1 专有细节,避免一开始改出大量窄字段。
### 3.3 精轨管理
Sentinel-1 EOF 不走 LT-1 的精轨转换链路。
建议新增 Sentinel-1 EOF 管理逻辑:
- 扫描 `.EOF` 文件。
- 解析 mission、validity start、validity stop、generation time。
- 按影像 acquisition time 匹配覆盖该时段的 EOF。
- 允许把匹配到的 EOF 复制到 Task 目录。
- PyINT runner 将 EOF 目录作为 `OPOD_DIR` 暴露给 PyINT/GAMMA。
这样可以避免生产时临时联网下载精轨,也便于任务复现。
## 4. D-InSAR 配对
Sentinel-1 配对适合嵌入现有 D-InSAR 配对体系,但应使用独立策略:
```text
sentinel1_dinsar_pairing
```
第一阶段配对只做轻量预筛选:
- 同一卫星族:`S1A` / `S1B` 可以互配,不按字面同卫星硬卡死。
- 同一成像模式:优先只支持 `IW`
- 同一产品类型:优先只支持 `SLC`
- 同一升降轨方向。
- 同一 relative orbit。
- 极化兼容,例如 `VV``VV`
- footprint 重叠率达到阈值。
- 时间基线不超过阈值。
- master/slave 都能匹配 EOF。
不在配对阶段强制完成:
- 真实垂直基线计算。
- burst 级精确公共覆盖计算。
- TOPS 配准质量判断。
- 解缠可行性判断。
这些应交给 Gamma/PyINT 的处理链路完成。配对阶段只负责筛出大概率可跑的候选对。
## 5. 任务分发
Sentinel-1 D-InSAR 任务分发需要输出一个可复现的 Task 目录。
建议包含:
```text
Task/
manifest.json
pair.json
input/
master/
<S1 master zip or SAFE>
slave/
<S1 slave zip or SAFE>
orbit/
<matched EOF files>
dem/
<optional dem reference or config>
pyint/
<project>.template
```
分发开关建议:
- 是否复制 EOF 到 Task 目录。
- 是否输出 ZIP 压缩包。
- 是否保留中间 SLC/RSLC 产物。
- 是否只发布核心结果。
如果生产目标是 PyINT/GammaTask 到 PyINT scratch 目录之间可以由 runner 再做一次结构化投放,把 master/slave ZIP 放入:
```text
$SCRATCHDIR/<project>/DOWNLOAD/
```
## 6. Gamma/PyINT 生产
第一阶段新增 profile
```text
s1_gamma_dinsar
```
它应独立于当前 `lt1_gamma_dinsar`
推荐 runner 行为:
1. 读取 Task manifest 和 pair 信息。
2. 创建 PyINT project 目录。
3. 复制或链接 master/slave Sentinel-1 ZIP 到 `DOWNLOAD/`
4. 准备 EOF / OPOD 目录。
5. 写入 PyINT template。
6. 调用 Sentinel-1 PyINT 脚本链。
7. 收集每一步日志和退出码。
8. 扫描输出产物并发布到系统结果 catalog。
可复用的 `D:\Code\PyINT` Sentinel-1 脚本包括:
- `pyint/down2slc_sen.py`
- `pyint/down2slc_sen_all.py`
- `pyint/extract_s1_bursts.py`
- `pyint/coreg_s1_gamma.py`
- `pyint/raw2ifg_s1.py`
- `pyint/select_pairs.py`
生产系统不建议直接无控制地调用完整 app,而应显式控制步骤、日志、失败原因和产物发布。
## 7. 结果管理
PyINT/Gamma 生产结束后,系统负责结果归档。
第一阶段建议发布:
- pair manifest
- PyINT template
- 全量日志
- preflight 报告
- SLC/RSLC 生成状态
- 共同 burst 检查结果
- interferogram
- coherence
- unwrapped phase
- geocoded result
- quicklook
- 真实 `perpendicular_baseline_meters`
结果管理应复用现有生产结果 catalog,不新建一套孤立页面。前端仍然从生产管理和结果管理入口查看,只是引擎 profile 显示为 `s1_gamma_dinsar`
## 8. 前端入口
前端需要在现有页面中增强,而不是新增一个割裂的 Sentinel-1 子系统。
建议改动范围:
- 数据管理页面支持 Sentinel-1 过滤、详情和 EOF 匹配状态。
- 配对页面新增 Sentinel-1 策略和参数。
- 配对结果中显示 relative orbit、升降轨、极化、时间基线、重叠率和 EOF 状态。
- 生产管理中支持 `s1_gamma_dinsar` profile。
- 任务分发 UI 中增加 EOF 复制和 ZIP 导出开关。
- 结果详情中显示 PyINT/Gamma 日志、核心产物和真实垂直基线。
第一阶段不需要让用户配置所有 PyINT template 字段。可以只暴露少量参数,其余使用系统默认值。
## 9. 数据库和自检
本系统有数据库自维护和系统自检机制,Sentinel-1 增强必须纳入这两部分。
需要评估的数据库变更:
- Sentinel-1 关键元数据字段是否已有通用列可复用。
- relative orbit、acquisition start/stop、source archive path、orbit match status 是否需要新增列。
- Sentinel-1 专有元数据是否放入 JSON 扩展字段。
- pairing cache 是否需要保存 `centroid_distance_meters``perpendicular_baseline_meters` 的区分。
- 生产结果 catalog 是否需要新增 engine profile 或产品类型枚举。
自检需要增加:
- Sentinel-1 数据根目录是否存在。
- EOF 根目录是否存在。
- Sentinel-1 解析器是否可用。
- `s1_gamma_dinsar` profile 是否启用。
- PyINT 路径是否存在。
- GAMMA 环境是否可用。
- `eof` 工具是否可用。
- 关键 GAMMA/PyINT 命令是否可调用。
- DEM 配置是否完整。
任何 schema 改动都必须同步数据库自维护逻辑,避免启动时 schema check 报错。
## 10. 分阶段实施
### 阶段 1:数据管理和配对
目标:
- Sentinel-1 SAFE / ZIP 可扫描入库。
- 基础元数据和 footprint 可解析。
- EOF 可扫描、匹配和显示。
- Sentinel-1 配对可生成候选对。
- 配对不进入生产也能保存批次。
验收:
- 同一 relative orbit 的 S1 数据能被正确筛出。
- 不同方向、不同 relative orbit、不同模式的数据不会误配。
- footprint 重叠率和时间基线显示正确。
- EOF 状态清晰可见。
### 阶段 2:任务分发
目标:
- Sentinel-1 pair 可分发为 Task 目录。
- 可选复制 EOF。
- 可选导出 ZIP。
- manifest 可复现输入数据和参数。
验收:
- Task 目录包含 master/slave、EOF、pair manifest、profile 参数。
- ZIP 导出可直接交给生产或转移归档。
### 阶段 3Gamma/PyINT 生产
目标:
- 新增 `s1_gamma_dinsar` profile。
- 托管调用 PyINT/Gamma 完成单对 Sentinel-1 D-InSAR。
- 生产日志进入系统。
验收:
- 至少一对 Sentinel-1 SLC ZIP 能跑通到 geocoded 结果。
- 失败时能看到明确步骤和原因。
- 真实垂直基线可以回填或记录。
### 阶段 4:结果管理
目标:
- PyINT/Gamma 产物进入现有结果 catalog。
- 前端可以查看日志、manifest、quicklook 和核心产物。
验收:
- 用户不需要进入 PyINT 工作目录即可查看结果。
- 结果与 pair、task、engine profile 可追踪。
### 阶段 5ISCE2 Sentinel-1 可选增强
目标:
- 新增 `s1_tops` profile。
- 使用 ISCE2 `topsApp.py` 或 topsStack。
- 与 Gamma 路线共享数据管理和 EOF 管理。
验收:
- 不污染现有 LT-1 `lt1_stripmap` runner。
- Sentinel-1 TOPS 生产有独立健康检查、日志和结果发布。
## 11. 主要风险
- 数据池只保留 `.SAFE`,没有原始 ZIP,会增加 PyINT/Gamma 接入复杂度。
- EOF 匹配不稳定会导致生产不可复现。
- Sentinel-1 annotation XML 解析不完整会影响 relative orbit、方向和 footprint。
- 中间产物体积大,SLC/RSLC 保留策略要可配置。
- PyINT 专家代码可用,但生产系统仍要做日志、失败分类和输出扫描。
- ISCE2 Sentinel-1 虽然可行,但当前系统没有 tops profile,不能当作低成本改动。
## 12. 当前推荐决策
建议确认以下产品决策后再进入代码实现:
1. Sentinel-1 生产第一阶段要求保留原始 SLC ZIP。
2. 配对策略命名为 `sentinel1_dinsar_pairing`
3. 生产 profile 命名为 `s1_gamma_dinsar`
4. 第一阶段只支持 `IW + SLC + 同 relative orbit + 同方向 + 兼容极化`
5. `centroid_distance_meters``perpendicular_baseline_meters` 严格区分。
6. 真实垂直基线由 Gamma/PyINT 处理后回填。
7. ISCE2 Sentinel-1 TOPS 放到第二阶段。
这套边界可以把系统改造控制在数据管理、配对、分发、生产托管和结果管理范围内,把 SAR 处理精度交给 Gamma/PyINT,避免在配对阶段过度复杂化。
+26 -6
View File
@@ -11,23 +11,23 @@ const STRATEGY_DESCRIPTIONS = {
title: '全部配对(默认)', title: '全部配对(默认)',
description: '列出所有满足约束条件的候选干涉对,由用户自行筛选。', description: '列出所有满足约束条件的候选干涉对,由用户自行筛选。',
details: [ details: [
'• 系统遍历所有影像组合,保留满足时间基线、footprint 中心距和两景 footprint 最小重叠率的配对', '• 系统遍历所有影像组合,保留满足时间基线和两景 footprint 最小重叠率的配对',
'• 结果按时间排序,用户可在配对列表中逐一勾选或取消', '• 结果按时间排序,用户可在配对列表中逐一勾选或取消',
'• 适用于研究型场景,需要精确控制每一对干涉组合', '• 适用于研究型场景,需要精确控制每一对干涉组合',
'• 配对数量可能较多,建议配合 AOI 和日期范围缩小结果' '• 配对数量可能较多,建议配合 AOI 和日期范围缩小结果'
], ],
params: '参数:时间基线范围、中心距上限、两景 footprint 最小重叠率' params: '参数:时间基线范围、两景 footprint 最小重叠率、可选 footprint 中心距上限'
}, },
sbas: { sbas: {
title: 'SBAS (短基线子集)', title: 'SBAS (短基线子集)',
description: '基于短基线原则的配对策略,通过覆盖优化算法自动筛选配对。', description: '基于短基线原则的配对策略,通过覆盖优化算法自动筛选配对。',
details: [ details: [
'• 优先选择时间间隔 footprint 中心距都较短的配对', '• 优先选择时间间隔较短、覆盖质量较好的配对;可按需启用 footprint 中心距限制',
'• 通过覆盖优化算法,去除冗余配对,确保时间序列连续性', '• 通过覆盖优化算法,去除冗余配对,确保时间序列连续性',
'• 适用于大范围、长时间序列的形变监测', '• 适用于大范围、长时间序列的形变监测',
'• 配对数量会比"全部配对"少,但覆盖更均匀' '• 配对数量会比"全部配对"少,但覆盖更均匀'
], ],
params: '参数:时间基线、中心距、两景 footprint 最小重叠率、覆盖多样性惩罚' params: '参数:时间基线、两景 footprint 最小重叠率、覆盖多样性惩罚、可选 footprint 中心距上限'
}, },
sequential: { sequential: {
title: 'Sequential (顺序配对)', title: 'Sequential (顺序配对)',
@@ -337,10 +337,30 @@ function PairingModal({
<input type="number" step="0.1" min="0" max="1" value={pairingParams.overlap_threshold} <input type="number" step="0.1" min="0" max="1" value={pairingParams.overlap_threshold}
onChange={e => setPairingParams({...pairingParams, overlap_threshold: parseFloat(e.target.value) || 0})} /> onChange={e => setPairingParams({...pairingParams, overlap_threshold: parseFloat(e.target.value) || 0})} />
</div> </div>
<div className="form-group checkbox-group">
<input
type="checkbox"
id="limit-footprint-center-distance"
checked={Boolean(pairingParams.limit_footprint_center_distance)}
onChange={e => setPairingParams({
...pairingParams,
limit_footprint_center_distance: e.target.checked
})}
/>
<label htmlFor="limit-footprint-center-distance">限制 footprint 中心距</label>
</div>
<div className="form-group"> <div className="form-group">
<label>footprint 中心距上限 ():</label> <label>footprint 中心距上限 ():</label>
<input type="number" min="0" value={pairingParams.spatial_baseline_max_meters} <input
onChange={e => setPairingParams({...pairingParams, spatial_baseline_max_meters: parseInt(e.target.value) || 3000})} /> type="number"
min="0"
value={pairingParams.spatial_baseline_max_meters}
disabled={!pairingParams.limit_footprint_center_distance}
onChange={e => setPairingParams({...pairingParams, spatial_baseline_max_meters: parseInt(e.target.value) || 3000})}
/>
{!pairingParams.limit_footprint_center_distance && (
<div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>默认不按中心距过滤勾选后使用上方数值</div>
)}
</div> </div>
<div className="form-group"> <div className="form-group">
<label>覆盖多样性惩罚 (0-1):</label> <label>覆盖多样性惩罚 (0-1):</label>
+26 -1
View File
@@ -10,6 +10,30 @@ import {
} from '../store'; } from '../store';
import { getSelectedRegionTreeId } from '../utils/appUiHelpers'; import { getSelectedRegionTreeId } from '../utils/appUiHelpers';
const compactDinsarBatchScene = (scene = {}) => ({
file_path: scene.file_path || '',
satellite: scene.satellite || null,
imaging_date: scene.imaging_date || null,
imaging_mode: scene.imaging_mode || null,
polarization: scene.polarization || null,
});
const compactDinsarBatchPair = (pair = {}) => ({
task_name: pair.task_name || null,
task_alias: pair.task_alias || pair.task_name || null,
pair_key: pair.pair_key || null,
pair_uid: pair.pair_uid || null,
network_run_id: pair.network_run_id || null,
network_edge_id: pair.network_edge_id ?? null,
policy_version: pair.policy_version || null,
selection_strategy: pair.selection_strategy || null,
time_baseline_days: pair.time_baseline_days ?? null,
spatial_baseline_meters: pair.spatial_baseline_meters ?? null,
scene_center_distance_meters: pair.scene_center_distance_meters ?? pair.spatial_baseline_meters ?? null,
master: compactDinsarBatchScene(pair.master),
slave: compactDinsarBatchScene(pair.slave),
});
export default function usePairingLogic({ export default function usePairingLogic({
fetchRegionGeometry, fetchRegionGeometry,
refreshBatchList, refreshBatchList,
@@ -116,9 +140,10 @@ export default function usePairingLogic({
return; return;
} }
try { try {
const batchPairs = selectedPairs.map(compactDinsarBatchPair);
const response = await apiClient.post('/task-batches/dinsar', { const response = await apiClient.post('/task-batches/dinsar', {
name: `DINSAR_${new Date().toISOString().slice(0, 10)}`, name: `DINSAR_${new Date().toISOString().slice(0, 10)}`,
pairs: selectedPairs pairs: batchPairs
}); });
const batchId = response.data?.batch_id || ''; const batchId = response.data?.batch_id || '';
addLog('success', `已创建 D-InSAR 批次: ${batchId || 'OK'}`); addLog('success', `已创建 D-InSAR 批次: ${batchId || 'OK'}`);
+1
View File
@@ -18,6 +18,7 @@ export const usePairingStore = create((set) => ({
time_baseline_max: 90, time_baseline_max: 90,
overlap_threshold: 0.5, overlap_threshold: 0.5,
spatial_baseline_max_meters: 3000, spatial_baseline_max_meters: 3000,
limit_footprint_center_distance: false,
coverage_diversity_penalty: 0.3, coverage_diversity_penalty: 0.3,
require_same_imaging_mode: false, require_same_imaging_mode: false,
require_same_polarization: false, require_same_polarization: false,