23 KiB
D-InSAR 配对与分发逻辑梳理
更新时间:2026-05-09
本文按当前代码实现梳理 D-InSAR 从“雷达数据入库”到“配对规划”、“批次保存”、“数据分发”和“多引擎生产执行”的主链路。重点依据源码,而不是早期设计文档。
1. 总览
当前 D-InSAR 链路分为两层:
- 配对规划层:把
radar_data中的影像先预计算成pairing_metric_cache候选边,再按用户阈值和策略筛选,最后固化为一次pairing_network_runs和若干pairing_network_edges。 - 分发执行层:配对结果可保存为
dinsar_task_batches/items,再复制成Task_*/master、Task_*/slave生产目录;生产面板再以这个根目录提交到 SARscape、ISCE2 或 PyINT/Gamma 引擎,由 DB job queue 和 worker 执行。
核心入口:
- 配对 API:backend/app/routers/pairing.py
- 配对服务:backend/app/services/spatial_service.py
- 配对缓存:backend/app/services/pairing_cache_service.py
- 批次 API:backend/app/routers/task_batches.py
- 数据分发 API:backend/app/routers/tools.py
- 数据复制执行:backend/app/copier.py
- 生产提交 API:backend/app/routers/dinsar_production.py
- 生产运行状态:backend/app/services/dinsar_production_service.py
- job 队列和 worker:backend/app/services/job_queue_service.py、backend/app/services/job_worker.py
2. 数据入库与配对缓存失效
雷达数据扫描在 backend/app/services/data_service.py 中写入或更新 radar_data。每个 scene 使用 unique_id 做 upsert;如果发现新 scene 或补齐了轨道文件,会调用 pairing_state_service.mark_scenes_dirty() 或 mark_global_dirty()。
配对缓存状态由 backend/app/services/pairing_state_service.py 管理:
- 全局状态表:
pairing_cache_state - 待重算 scene 表:
pairing_dirty_scenes - 当前指标版本:
2026.05.raw.v1 - 当前 master/slave 定向规则:
date_then_scene_uid_v1
应用启动时会调用 bootstrap_pairing_cache_state(),但不会自动全量重建候选边。缓存如果是 DIRTY,配对仍可返回旧缓存结果并给 warning;如果是 FAILED、UNINITIALIZED、ERROR,或 scene 数大于 1 但候选边为 0,/find-pairs 会拒绝并提示先修复缓存。
3. 候选边缓存构建
候选边缓存由 pairing_cache_service.py 写入 pairing_metric_cache。
全量重建逻辑:
- 删除全部
pairing_metric_cache - 从
radar_data m JOIN radar_data s重新生成候选边 - 只保留满足硬约束的 pair:
m.id <> s.id- 两景都有
geom imaging_date是 8 位日期- 两景都有
orbit_direction且方向一致 - 两景都是可用于 InSAR 的原始复数源:
insar_source_ready = true - 如果两景都有
look_direction,要求视向一致 - 几何相交
ST_Intersects - 按
date_then_scene_uid_v1只保留一个方向,避免 A-B 和 B-A 双向重复
写入的主要指标:
time_baseline_days:两景日期差的绝对值scene_center_distance_meters:两景 footprint 质心的球面距离spatial_baseline_meters:兼容旧 API 的历史字段;新缓存中暂存同一个 footprint 中心距,不能解释为 SAR 空间/垂直基线scene_overlap_ratio:两景交集面积 / 两景较大 footprint 面积same_satellitesame_satellite_family:同一卫星族,例如 LT1A/LT1B 归为LT1same_look_directionsame_imaging_modesame_polarizationpair_uid = md5(master_scene_uid + '|' + slave_scene_uid)
增量重算逻辑:
- 如果 dirty scene 数过多、占比过高、或缓存为空,会转全量重建
- 否则删除涉及 dirty scene 的缓存边
- 对每个 dirty scene 与其他 scene 重新计算边
- resolved 对应 dirty rows
阈值:
- dirty scene 数量达到 64 触发全量重建
- dirty scene 占 scene 总数比例达到 25% 触发全量重建
4. /find-pairs 配对查询
前端在 frontend/src/hooks/usePairingLogic.js 中把配对参数、AOI 文件或行政区 GeoJSON 组装为 FormData,提交到 POST /api/find-pairs。
后端入口是 pairing.py:
- 解析配对参数为
PairingRequest - 解析 AOI:支持上传 Shapefile 或传入 GeoJSON
- 调用
spatial_service.find_dinsar_pairs() - 返回
PairingResponse,包含 pairs、warnings、network_run_id、policy_version、候选数和入选边数
PairingRequest 在 backend/app/models/schemas.py 中定义,主要参数包括:
time_baseline_min/maxoverlap_thresholdspatial_baseline_max_meterscoverage_diversity_penaltyrequire_same_imaging_moderequire_same_polarizationaoi_overlap_threshold- master/slave 日期范围
strategy:all | sbas | sequential | starnum_connectionsreference_image_idallowed_satellitescross_satellite_pairingstart_date兼容旧参数
5. 候选池过滤条件
spatial_service._query_pairing_metric_cache() 只查询缓存表,不再实时两两计算。基础过滤条件:
metric_version == 2026.05.raw.v1status == READYtime_baseline_days在请求范围内scene_center_distance_meters <= spatial_baseline_max_metersscene_overlap_ratio >= overlap_thresholdsame_look_direction = true- 如果
require_orbit_data = true,master 和 slave 都要有精轨 - 默认要求同卫星族;除非
cross_satellite_pairing = true - 默认要求成像模式一致、极化一致
- 如果
allowed_satellites不为空,master/slave 的卫星名或卫星族都必须在列表内 - 如果传入 master/slave 日期范围,分别约束
master_imaging_date和slave_imaging_date - 如果有 AOI,master/slave footprint 都要与 AOI 相交
- 如果
aoi_overlap_threshold有值,master/slave 各自覆盖 AOI 的比例都要达标
排序默认按:
- master 日期升序
- slave 日期升序
- overlap 降序
- pair_uid 升序
6. 配对策略
策略选择在 spatial_service._apply_strategy()。
6.1 all
all 策略不再做网络抽稀,直接返回过滤后的全部候选边。每条边的:
selection_reason = all_candidateselection_score综合时间基线、footprint 中心距、重叠率、源数据可用性和精轨状态
6.2 sequential
sequential 策略先从候选池提取 scene,按稳定时间键排序:
- 优先
acquisition_time_utc - 否则
imaging_date - 再按 scene_uid 和 id 打平同日多景
然后每个 scene 向后寻找最多 num_connections 个有候选边的后继 scene。不存在于候选池的边不会被补造。
输出边:
selection_reason = sequential_neighborselection_score综合时间基线、footprint 中心距、重叠率、源数据可用性和精轨状态
6.3 star
star 策略要求参考影像固定作为 master。
如果用户未指定 reference_image_id,系统会在时间序列中找靠近中位位置、且能作为 master 的 scene 自动作为参考影像。注意当前实现不会把 slave 侧边反转为 master 侧边;如果参考影像在候选边中只能出现在 slave 侧,这些边会被跳过并给 warning。
输出边:
selection_reason = star_reference_masteris_reference_edge = truereference_image_id写入 edge meta
6.4 sbas
sbas 策略用于构造小基线网络,流程是:
- 按时间顺序先选相邻 scene 的候选边,形成时间骨架。
- 如果网络有多个连通分量,优先选能连接分量的候选边。
- 继续补低度数节点,直到达到目标连接数或达到最大边数。
- 如果无法形成完整连通图,或存在 0 度/低度数节点,返回 warning。
关键参数:
min_degree = min(max(1, num_connections), scene_count - 1)max_degree = min(max(min_degree + 2, 3), scene_count - 1)max_edges = min(candidate_count, max(scene_count - 1, scene_count * min_degree))
候选边评分:
score =
0.30 * time_score
+ 0.15 * center_distance_score
+ 0.30 * overlap_score
+ 0.10 * aoi_gain
+ 0.10 * source_ready_score
+ 0.05 * orbit_score
- coverage_diversity_penalty * redundancy_penalty
其中 aoi_gain 和 redundancy_penalty 基于 master/slave 交集几何计算;如果有 AOI,会先把交集裁到 AOI 范围。
7. 网络运行留痕
每次 /find-pairs 都会创建一条 pairing_network_runs:
network_run_id = pnr_<uuid>strategypolicy_version = 2026.05.raw-source.v1request_hash- 请求参数 JSON
- AOI hash 和 summary
- 候选边数量、入选边数量、warning 数量
每条入选边写入 pairing_network_edges:
- 指向
pairing_metric_cache edge_rankselection_reasonselection_scoreselection_meta_jsonis_reference_edge
之后 RadarPair 响应会携带:
pair_keypair_uidmetric_cache_ref_idnetwork_run_idnetwork_edge_idpolicy_versionselection_strategyselection_scoreselection_reasonscene_center_distance_meterstask_name/task_alias
task_alias 由 dinsar_naming.py 生成,格式是 Task_YYYYMMDD_YYYYMMDD;同名时追加 _1、_2 保证唯一。
8. 批次保存
前端找到 pairs 后,用户勾选结果并调用 createDinsarBatch(),提交到 POST /api/task-batches/dinsar。
后端 task_batches.py 会创建:
dinsar_task_batchesdinsar_task_items
每条 item 会保存:
task_name/task_aliaspair_keyscene_pair_uidnetwork_run_idnetwork_edge_idpolicy_versionselection_strategy- master/slave 文件路径
- master/slave 卫星、日期、成像模式、极化
- 时间基线、footprint 中心距
- 人工审核状态,默认
PENDING
前端批次面板可把 item 状态改成:
PENDINGIN_PROGRESSCOMPLETEDFAILED
数据分发默认只复制 COMPLETED 状态的条目。
9. 数据分发到 Task 目录
数据分发入口是 POST /api/tools/copy-dinsar-pairs,代码在 tools.py。
请求参数:
batch_iddest_dircopy_statuses,为空时默认["COMPLETED"]include_orbit_files,默认false;为true时把 master/slave 精轨复制到 Task 内的orbit/package_mode,支持task_folder、task_zip、source_bundleexport_zip,兼容旧参数;为true且package_mode=task_folder时等价于task_zipskip_existing,默认truemax_items,每次最多处理的新 Task 或新 pair 数量;为空或 0 表示不限制
后端动作:
- 校验目标路径。
- 创建
SystemTask,类型为COPY_DATA。 - 创建
SystemJob,job_type 也是COPY_DATA。 - worker 领取 job 后进入
job_handlers._handle_copy_data()。 _handle_copy_data()根据batch_id查询dinsar_task_items,只取copy_statuses命中的条目。- 根据
package_mode调用 backend/app/copier.py 的run_dinsar_copy_items()或run_dinsar_source_bundle_items()。
9.1 Task 文件夹 / ZIP 模式
run_dinsar_copy_items() 对每个 item 执行:
- 文件夹模式目标目录:
<dest_dir>/<task_alias>/ - zip 模式目标文件:
<dest_dir>/<task_alias>.zip - master 目录:
<task_alias>/master - slave 目录:
<task_alias>/slave - 如果启用
include_orbit_files,从radar_data.orbit_file_path找 master/slave 精轨并复制到<task_alias>/orbit/ - 直接复制配对时保存的原始产品目录;D-InSAR 分发不再优先使用
envi_import/ - 先复制到临时目录,完成后再替换为最终 Task 目录或 ZIP,避免留下半成品
- 写入
<task_alias>/.dinsar_pair.json skip_existing=true时,文件夹模式检查<task_alias>/master和<task_alias>/slave非空即跳过;ZIP 模式只检查<task_alias>.zip存在且大小大于 0,不打开 ZIP 做深度校验max_items限制本次新复制数量;已跳过的既有 Task 不消耗本次额度
.dinsar_pair.json 是后续生产追踪的关键 sidecar,包含:
pair_keytask_name/task_alias- master/slave 原始路径和元数据
time_baseline_daysspatial_baseline_metersscene_center_distance_meterspackage_formatinclude_orbit_filesorbit_filesscene_pair_uid/pair_uidnetwork_run_idnetwork_edge_idpolicy_versionselection_strategycopied_at
当前实现不再合并写入已有 Task 目录。目标 Task 已完整存在时跳过;目标同名目录存在但不完整时,为避免误覆盖,会报错并要求人工处理。
9.2 去重源数据包模式
package_mode=source_bundle 时,分发不生成每个 Task_*,而是在同一个目标目录内维护:
<dest_dir>/
data/
orbit/
pairs.json
manifest.json
规则:
data/只复制唯一源影像目录或文件,命名为scene_<source_path_hash>_<source_name>。orbit/只复制唯一精密轨道文件,命名为orbit_<source_path_hash>_<orbit_name>。pairs.json记录每个 pair 的 master/slave 数据相对路径、轨道相对路径、pair 元数据和identity_key。manifest.json记录 package 统计信息、场景清单、轨道清单和本次追加统计。- 同一目标目录再次分发时,系统先读取已有
pairs.json/manifest.json,根据identity_key、scene_pair_uid/pair_uid、pair_key、network_run_id + network_edge_id、源路径或 bundle 相对路径识别已导出的 pair。 max_items在跳过已导出 pair 后生效。因此 500 个 pair 第一次限制 100,第二次同一目录限制 100,会追加下一批未导出的 100 个 pair。data//orbit/仍按文件存在性跳过重复复制;pair 级续跑以pairs.json为准。pairs.json和manifest.json写入时先写临时文件,再原子替换。
这个模式面向外部分发和后续离线还原,不直接作为本系统生产输入。反向还原工具任务书见 DINSAR_SOURCE_BUNDLE_REVERSE_TOOL_TASK_20260511.md。
10. 生产提交与运行分发
生产入口是 POST /api/dinsar-production/run,前端在 frontend/src/DinsarProductionPanel.jsx 手动输入“根目录或单个任务目录”并选择引擎/模板。
支持引擎来自 backend/app/dinsar_engines/registry.py:
sarscapeisce2pyintlandsar,目前预留,不进入 D-InSAR queued production 主链路
提交流程:
- 校验 engine 是否注册且可用。
- 校验 profile 是否属于该 engine。
- 对 ISCE2/PyINT 调用 engine 的
validate_root_dir()和normalize_extra()。 - PyINT 会额外做输入资产预检。
- 当前 SARscape、ISCE2、PyINT 都走 managed production run。
- 调用
dinsar_production_service.create_run()。
create_run() 做的事情:
- 根据引擎映射 task_type:
- SARscape ->
IDL_RUN_DINSAR - ISCE2 ->
ISCE2_RUN - PyINT/Gamma ->
PYINT_RUN
- SARscape ->
- 扫描 root 下的
Task_*目录,或把 root 本身当单个 Task 目录 - 从
.dinsar_pair.json解析 pair identity;如果没有 sidecar,则按目录名和路径生成 fallback - 根据
rerun_mode跳过已有 current pointer 的完成项 - 创建
SystemTask - 创建
dinsar_production_runs - 创建
dinsar_production_run_items - 创建一个 workflow run,只有一个 step:
execute_items - workflow step 入队为
SystemJob
注意:生产面板目前不直接从 dinsar_task_batches 选择批次。实际串联方式是:先在“分发”面板把批次复制到生产根目录,再在“生产”面板提交这个根目录。
11. worker 与执行控制
后台 worker 在 job_worker.py:
- 周期性
claim_next_job() - DB 查询使用
FOR UPDATE SKIP LOCKED - 按
priority DESC, id ASC领取READY/RETRYjob - 支持 worker heartbeat
- 支持 stale RUNNING job 恢复为 RETRY 或 FAILED
run_worker_loop()参数支持 job 级并发,但默认并发为 1
SystemTask 在 task_service.py 管理:
- 创建任务时会检查同一
task_type是否已有PENDING/RUNNING - PostgreSQL 下使用 advisory lock 防止并发创建同类任务
- 因此同一类生产任务天然串行提交
workflow 在 workflow_service.py:
- 创建 workflow run 和 steps
- 没有依赖的 step 立即入队
- job 完成后 mark step completed
- step 全部终态后 workflow run 完成
12. 各引擎生产控制器
job handler 在 job_handlers.py。
12.1 SARscape
_handle_idl_run_dinsar() 如果 payload 有 production_run_id,会进入 _run_dinsar_production_controller()。
执行特点:
- 使用
engine_lock_service.acquire("envi_taskengine"),保证 ENVI/SARscape taskengine 串行 - 对 run item 逐个执行
- 每个 item 创建一个
DinsarProductionExecution - 调用
build_envi_runner_command()启动 runner - 运行结束后规范化输出目录
- 写
execution_manifest.json - 写
current/<engine>__<profile>.json - 标记 item completed/failed/cancelled
- 成功输出目录会进入
result_catalog_service.publish_from_sources()
12.2 ISCE2 与 PyINT/Gamma
_handle_isce2_run() 和 _handle_pyint_run() 在 managed 模式下都进入 _run_wsl_dinsar_production_controller()。
执行特点:
- 使用
engine_lock_service.acquire(f"wsl_dinsar_{engine_code}") - 每个 item 构造独立 managed run 目录:
- run dir
- native dir
- workflow dir
- export dir
- orbit output dir
- 构造
RunRequest调用 engine 的run() - engine 返回
primary_file、source_files、native_output_dir - 校验 primary output 存在
- 写
execution_manifest.json - 写 current pointer
- 标记 item 状态
- 发布成功包,并对结果 catalog 做 rebuild
一个 production run 内部 item 是串行执行的。多个 worker 可以领取不同 job,但同类任务创建限制和 engine lock 会进一步限制实际并发。
13. 结果发布与追踪
生产完成后会生成标准包结构,并由 result catalog 接管。execution_manifest.json 中保留:
run_idtask_idengine_codeprofile_coderuntime_idtask_name/task_aliaspair_keypair_uidnetwork_run_idnetwork_edge_idpolicy_versionselection_strategysource_task_dirresults_root_dirpublish_root_dirprimary_filesource_filesmetrics
catalog 注册逻辑在 backend/app/services/result_catalog_service.py 中会继续把 pairing trace 字段写到结果产品,便于从结果反查配对网络。
14. 关键表关系
配对规划:
radar_datapairing_cache_statepairing_dirty_scenespairing_metric_cachepairing_network_runspairing_network_edges
人工批次:
dinsar_task_batchesdinsar_task_items
后台任务:
system_taskstask_logssystem_jobssystem_worker_heartbeatsworkflow_runsworkflow_steps
生产执行:
dinsar_production_runsdinsar_production_run_itemsdinsar_production_executions
15. 常用 API 链路
配对健康和修复:
GET /api/pairing/healthPOST /api/pairing/rebuild-cachePOST /api/pairing/reconcile-dirty?force_full=false
配对规划:
POST /api/find-pairsGET /api/pairing/networks/{network_run_id}
批次:
POST /api/task-batches/dinsarGET /api/task-batches/dinsarGET /api/task-batches/dinsar/{batch_id}/itemsPATCH /api/task-batches/dinsar/items/{item_id}PATCH /api/task-batches/dinsar/{batch_id}/complete-all
数据分发:
POST /api/tools/copy-dinsar-pairsGET /api/tools/copy-status/{task_id}
生产:
GET /api/dinsar-production/enginesPOST /api/dinsar-production/engines/pyint/preview-input-assetsPOST /api/dinsar-production/runGET /api/dinsar-production/runs
16. 当前实现边界
- 配对查询完全依赖
pairing_metric_cache。缓存未初始化、失败、或 scene 足够但 pair 为 0 时不会降级实时计算。 scene_center_distance_meters是 footprint 质心距离;spatial_baseline_meters仅为旧 API 兼容字段,不是 SAR 几何中的垂直基线。- master/slave 方向在缓存层已经固定为“日期优先、scene_uid 次之”。
star策略不会把参考影像位于 slave 的边翻转。 aoi_overlap_threshold约束的是每一景对 AOI 的覆盖比例,不是 pair 交集对 AOI 的覆盖比例。- 数据分发默认只复制
COMPLETED状态 item;如果用户没有在批次面板审核或一键完成,分发可能没有条目。 - 数据分发使用
dirs_exist_ok=True合并复制,不会自动清理目标旧内容。 - 生产提交和批次保存之间没有数据库级直接引用;生产侧通过
Task_*目录和.dinsar_pair.jsonsidecar 重新恢复 pair trace。 - 每个 production run 内部 item 串行执行;job worker 可并发,但 task_type 冲突检查和 engine lock 会限制同类引擎并发。
landsar已注册为 engine,但当前/dinsar-production/run仅对 SARscape、ISCE2、PyINT 建立 queued production 主链路。
17. 推荐排查路径
配对为空:
- 查
GET /api/pairing/health - 看
pair_count、dirty_scene_count、status - 必要时执行
POST /api/pairing/reconcile-dirty或POST /api/pairing/rebuild-cache - 放宽
time_baseline_max、spatial_baseline_max_meters、overlap_threshold - 检查
insar_source_ready、require_orbit_data、同卫星族、同视向、同模式、同极化约束
分发为空:
- 查 batch item 是否存在
- 查 item 状态是否命中
copy_statuses,默认只取COMPLETED - 查 master/slave 源路径是否存在
- 查目标目录是否已有旧文件影响判断
生产未执行:
- 查
system_tasks状态和 task logs - 查
system_jobs是否 READY/RUNNING/FAILED - 查 worker heartbeat
- 查 engine lock 是否被长任务持有
- 查生产根目录是否包含有效
Task_*/master、Task_*/slave - 对 PyINT 先跑输入资产预检