feat: improve D-InSAR pairing and distribution
This commit is contained in:
@@ -33,7 +33,13 @@ from ..config import settings
|
||||
from ..models import (
|
||||
RadarDataORM, RadarData, DinsarResultORM, HazardPointORM, HazardPoint, ScanStateORM
|
||||
)
|
||||
from ..utils import get_parser, RADAR_PARSERS, find_xml_file, parse_xml_metadata
|
||||
from ..utils import (
|
||||
get_parser,
|
||||
RADAR_PARSERS,
|
||||
find_xml_file,
|
||||
normalize_satellite_family,
|
||||
parse_xml_metadata,
|
||||
)
|
||||
from .task_service import task_service
|
||||
from .image_service import image_service
|
||||
from .orbit_converter import get_source_orbit_inventory, sync_orbit_pools
|
||||
@@ -50,7 +56,7 @@ def _safe_mtime(path: str) -> float:
|
||||
_DATE_RE = re.compile(r"^\d{8}$")
|
||||
|
||||
|
||||
def _valid_imaging_date(value: str) -> bool:
|
||||
def _valid_imaging_date(value: Optional[str]) -> bool:
|
||||
return bool(value and _DATE_RE.match(value))
|
||||
|
||||
|
||||
@@ -63,6 +69,62 @@ def _extract_date_from_text(value: Optional[str]) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _text_or_none(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
text_value = str(value).strip()
|
||||
return text_value or None
|
||||
|
||||
|
||||
def _bool_or_none(value: Any) -> Optional[bool]:
|
||||
if value is None or isinstance(value, bool):
|
||||
return value
|
||||
text_value = str(value).strip().lower()
|
||||
if text_value in {"1", "true", "yes", "y"}:
|
||||
return True
|
||||
if text_value in {"0", "false", "no", "n"}:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _build_insar_source_readiness(
|
||||
meta: Dict[str, Any],
|
||||
coverage_polygon: Optional[List[Tuple[float, float]]],
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
reasons: List[str] = []
|
||||
if not coverage_polygon or len(coverage_polygon) < 3:
|
||||
reasons.append("missing_footprint")
|
||||
if not _valid_imaging_date(_text_or_none(meta.get("imaging_date"))):
|
||||
reasons.append("missing_date")
|
||||
for field_name, reason in (
|
||||
("orbit_direction", "missing_orbit_direction"),
|
||||
("imaging_mode", "missing_imaging_mode"),
|
||||
("polarization", "missing_polarization"),
|
||||
("satellite_family", "missing_satellite_family"),
|
||||
):
|
||||
if not _text_or_none(meta.get(field_name)):
|
||||
reasons.append(reason)
|
||||
|
||||
geocoded_flag = _bool_or_none(meta.get("geocoded_flag"))
|
||||
if geocoded_flag is True:
|
||||
reasons.append("geocoded_product")
|
||||
|
||||
complex_tokens = {
|
||||
_text_or_none(meta.get("image_data_type")),
|
||||
_text_or_none(meta.get("product_type")),
|
||||
_text_or_none(meta.get("source_product_token")),
|
||||
_text_or_none(meta.get("product_variant")),
|
||||
}
|
||||
normalized_tokens = {str(token).strip().upper() for token in complex_tokens if token}
|
||||
is_complex_source = bool(normalized_tokens.intersection({"COMPLEX", "SLC", "SSC"}))
|
||||
if not is_complex_source:
|
||||
reasons.append("not_complex_source")
|
||||
|
||||
if reasons:
|
||||
return False, ";".join(reasons)
|
||||
return True, None
|
||||
|
||||
|
||||
def _iter_dirs(root: str, last_mtime: float):
|
||||
stack = [root]
|
||||
while stack:
|
||||
@@ -586,20 +648,31 @@ class DataService:
|
||||
continue
|
||||
merged_meta[key] = value
|
||||
|
||||
satellite = merged_meta.get("satellite")
|
||||
imaging_date = merged_meta.get("imaging_date")
|
||||
imaging_mode = merged_meta.get("imaging_mode")
|
||||
polarization = merged_meta.get("polarization")
|
||||
orbit_direction = merged_meta.get("orbit_direction")
|
||||
satellite_mode = merged_meta.get("satellite_mode")
|
||||
receiving_station = merged_meta.get("receiving_station")
|
||||
orbit_circle = merged_meta.get("orbit_circle")
|
||||
satellite = _text_or_none(merged_meta.get("satellite"))
|
||||
imaging_date = _text_or_none(merged_meta.get("imaging_date"))
|
||||
imaging_mode = _text_or_none(merged_meta.get("imaging_mode"))
|
||||
polarization = _text_or_none(merged_meta.get("polarization"))
|
||||
orbit_direction = _text_or_none(merged_meta.get("orbit_direction"))
|
||||
satellite_mode = _text_or_none(merged_meta.get("satellite_mode"))
|
||||
receiving_station = _text_or_none(merged_meta.get("receiving_station"))
|
||||
orbit_circle = _text_or_none(merged_meta.get("orbit_circle"))
|
||||
scene_center_lon = merged_meta.get("scene_center_lon")
|
||||
scene_center_lat = merged_meta.get("scene_center_lat")
|
||||
acquisition_time_utc = merged_meta.get("acquisition_time_utc")
|
||||
product_type = merged_meta.get("product_type")
|
||||
product_level = merged_meta.get("product_level")
|
||||
product_unique_id = merged_meta.get("product_unique_id")
|
||||
acquisition_time_utc = _text_or_none(merged_meta.get("acquisition_time_utc"))
|
||||
product_type = _text_or_none(merged_meta.get("product_type"))
|
||||
source_product_token = _text_or_none(merged_meta.get("source_product_token"))
|
||||
image_data_type = _text_or_none(merged_meta.get("image_data_type"))
|
||||
image_data_format = _text_or_none(merged_meta.get("image_data_format"))
|
||||
product_variant = _text_or_none(merged_meta.get("product_variant"))
|
||||
product_level = _text_or_none(merged_meta.get("product_level"))
|
||||
product_unique_id = _text_or_none(merged_meta.get("product_unique_id"))
|
||||
satellite_family = normalize_satellite_family(
|
||||
_text_or_none(merged_meta.get("satellite_family")) or satellite
|
||||
)
|
||||
look_direction = _text_or_none(merged_meta.get("look_direction"))
|
||||
if look_direction:
|
||||
look_direction = look_direction.upper()
|
||||
geocoded_flag = _bool_or_none(merged_meta.get("geocoded_flag"))
|
||||
|
||||
if not satellite:
|
||||
continue
|
||||
@@ -635,9 +708,27 @@ class DataService:
|
||||
scene_center_lon = scene_center_lon if scene_center_lon is not None else poly.centroid.x
|
||||
scene_center_lat = scene_center_lat if scene_center_lat is not None else poly.centroid.y
|
||||
|
||||
readiness_meta = {
|
||||
"satellite_family": satellite_family,
|
||||
"imaging_date": imaging_date,
|
||||
"imaging_mode": imaging_mode,
|
||||
"orbit_direction": orbit_direction,
|
||||
"polarization": polarization,
|
||||
"product_type": product_type,
|
||||
"source_product_token": source_product_token,
|
||||
"image_data_type": image_data_type,
|
||||
"product_variant": product_variant,
|
||||
"geocoded_flag": geocoded_flag,
|
||||
}
|
||||
insar_source_ready, insar_source_reason = _build_insar_source_readiness(
|
||||
readiness_meta,
|
||||
coverage_polygon,
|
||||
)
|
||||
|
||||
data_to_upsert = {
|
||||
"unique_id": unique_id,
|
||||
"satellite": satellite,
|
||||
"satellite_family": satellite_family,
|
||||
"imaging_date": imaging_date,
|
||||
"imaging_mode": imaging_mode,
|
||||
"orbit_direction": orbit_direction,
|
||||
@@ -649,8 +740,16 @@ class DataService:
|
||||
"scene_center_lat": scene_center_lat,
|
||||
"acquisition_time_utc": acquisition_time_utc,
|
||||
"product_type": product_type,
|
||||
"source_product_token": source_product_token,
|
||||
"image_data_type": image_data_type,
|
||||
"image_data_format": image_data_format,
|
||||
"product_variant": product_variant,
|
||||
"product_level": product_level,
|
||||
"product_unique_id": product_unique_id,
|
||||
"look_direction": look_direction,
|
||||
"geocoded_flag": geocoded_flag,
|
||||
"insar_source_ready": insar_source_ready,
|
||||
"insar_source_reason": insar_source_reason,
|
||||
"file_path": radar_folder_path,
|
||||
"has_orbit_data": has_orbit_data,
|
||||
"orbit_file_path": orbit_file_path,
|
||||
|
||||
@@ -530,6 +530,7 @@ def _write_envi_run_sidecar(
|
||||
"slave_polarization": pair_meta.get("slave_polarization"),
|
||||
"time_baseline_days": pair_meta.get("time_baseline_days"),
|
||||
"spatial_baseline_meters": pair_meta.get("spatial_baseline_meters"),
|
||||
"scene_center_distance_meters": pair_meta.get("scene_center_distance_meters"),
|
||||
"scene_pair_uid": pair_meta.get("scene_pair_uid") or pair_meta.get("pair_uid"),
|
||||
"pair_uid": pair_meta.get("pair_uid") or pair_meta.get("scene_pair_uid"),
|
||||
"network_run_id": pair_meta.get("network_run_id"),
|
||||
|
||||
@@ -19,7 +19,7 @@ from sqlalchemy import select
|
||||
|
||||
from .. import database
|
||||
from ..config import settings
|
||||
from ..models import SystemJobORM, DinsarResultORM, HazardPointORM, DinsarTaskItemORM, PsTaskItemORM, SARSceneGeoORM, FloodDetectionORM, WaterDetectionORM, GF3ProcessingORM, AiDiagnosisORM
|
||||
from ..models import SystemJobORM, DinsarResultORM, HazardPointORM, DinsarTaskItemORM, PsTaskItemORM, RadarDataORM, SARSceneGeoORM, FloodDetectionORM, WaterDetectionORM, GF3ProcessingORM, AiDiagnosisORM
|
||||
from ..scheduler import scan_data_job
|
||||
from .data_service import data_service
|
||||
from .dinsar_compat_service import dinsar_compat_service
|
||||
@@ -287,6 +287,8 @@ async def _handle_copy_data(job: SystemJobORM) -> None:
|
||||
dest_dir = payload.get("dest_dir")
|
||||
batch_id = payload.get("batch_id")
|
||||
copy_statuses = _normalize_copy_statuses(payload.get("copy_statuses"))
|
||||
include_orbit_files = bool(payload.get("include_orbit_files"))
|
||||
export_zip = bool(payload.get("export_zip"))
|
||||
|
||||
if not batch_id:
|
||||
raise ValueError("COPY_DATA requires batch_id payload.")
|
||||
@@ -318,6 +320,26 @@ async def _handle_copy_data(job: SystemJobORM) -> None:
|
||||
.where(DinsarTaskItemORM.status.in_(copy_statuses))
|
||||
.order_by(DinsarTaskItemORM.id.asc())
|
||||
)
|
||||
task_items = result.scalars().all()
|
||||
orbit_by_path: Dict[str, Optional[str]] = {}
|
||||
if include_orbit_files:
|
||||
scene_paths = [
|
||||
str(path)
|
||||
for item in task_items
|
||||
for path in (item.master_path, item.slave_path)
|
||||
if path
|
||||
]
|
||||
if scene_paths:
|
||||
scene_result = await db.execute(
|
||||
select(RadarDataORM.file_path, RadarDataORM.orbit_file_path).where(
|
||||
RadarDataORM.file_path.in_(scene_paths)
|
||||
)
|
||||
)
|
||||
orbit_by_path = {
|
||||
os.path.normcase(os.path.normpath(str(file_path))): orbit_path
|
||||
for file_path, orbit_path in scene_result.all()
|
||||
if file_path
|
||||
}
|
||||
items = [
|
||||
{
|
||||
"task_name": item.task_name,
|
||||
@@ -335,6 +357,13 @@ async def _handle_copy_data(job: SystemJobORM) -> None:
|
||||
"slave_polarization": item.slave_polarization,
|
||||
"time_baseline_days": item.time_baseline_days,
|
||||
"spatial_baseline_meters": item.spatial_baseline_meters,
|
||||
"scene_center_distance_meters": item.scene_center_distance_meters,
|
||||
"master_orbit_file_path": orbit_by_path.get(
|
||||
os.path.normcase(os.path.normpath(str(item.master_path)))
|
||||
) if include_orbit_files and item.master_path else None,
|
||||
"slave_orbit_file_path": orbit_by_path.get(
|
||||
os.path.normcase(os.path.normpath(str(item.slave_path)))
|
||||
) if include_orbit_files and item.slave_path else None,
|
||||
"scene_pair_uid": item.scene_pair_uid,
|
||||
"pair_uid": item.scene_pair_uid,
|
||||
"network_run_id": item.network_run_id,
|
||||
@@ -342,13 +371,19 @@ async def _handle_copy_data(job: SystemJobORM) -> None:
|
||||
"policy_version": item.policy_version,
|
||||
"selection_strategy": item.selection_strategy,
|
||||
}
|
||||
for item in result.scalars().all()
|
||||
for item in task_items
|
||||
]
|
||||
if not items:
|
||||
raise ValueError(
|
||||
f"No D-InSAR items matched copy statuses: {', '.join(copy_statuses)}"
|
||||
)
|
||||
await run_dinsar_copy_items(job.task_id, items, dest_dir)
|
||||
await run_dinsar_copy_items(
|
||||
job.task_id,
|
||||
items,
|
||||
dest_dir,
|
||||
include_orbit_files=include_orbit_files,
|
||||
export_zip=export_zip,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(f"Unknown COPY_DATA file_type: {file_type}")
|
||||
|
||||
@@ -28,6 +28,38 @@ def _scene_uid_expr(alias: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _satellite_family_expr(alias: str) -> str:
|
||||
compact = (
|
||||
f"upper(replace(replace(replace(COALESCE({alias}.satellite, ''), '-', ''), '_', ''), ' ', ''))"
|
||||
)
|
||||
return (
|
||||
f"COALESCE(NULLIF({alias}.satellite_family, ''), "
|
||||
f"CASE "
|
||||
f"WHEN {compact} IN ('LT1', 'LT1A', 'LT1B', 'LUTAN1', 'LUTAN1A', 'LUTAN1B') THEN 'LT1' "
|
||||
f"WHEN {compact} IN ('S1', 'S1A', 'S1B', 'SENTINEL1', 'SENTINEL1A', 'SENTINEL1B') THEN 'S1' "
|
||||
f"WHEN NULLIF({alias}.satellite, '') IS NOT NULL THEN upper({alias}.satellite) "
|
||||
f"ELSE NULL END)"
|
||||
)
|
||||
|
||||
|
||||
def _same_satellite_family_expr(left_alias: str, right_alias: str) -> str:
|
||||
left_family = _satellite_family_expr(left_alias)
|
||||
right_family = _satellite_family_expr(right_alias)
|
||||
return (
|
||||
f"(NULLIF({left_family}, '') IS NOT NULL "
|
||||
f"AND NULLIF({right_family}, '') IS NOT NULL "
|
||||
f"AND {left_family} = {right_family})"
|
||||
)
|
||||
|
||||
|
||||
def _same_look_direction_expr(left_alias: str, right_alias: str) -> str:
|
||||
return (
|
||||
f"(NULLIF({left_alias}.look_direction, '') IS NULL "
|
||||
f"OR NULLIF({right_alias}.look_direction, '') IS NULL "
|
||||
f"OR {left_alias}.look_direction = {right_alias}.look_direction)"
|
||||
)
|
||||
|
||||
|
||||
def _orientation_is_left_master_expr(left_alias: str, right_alias: str) -> str:
|
||||
left_uid = _scene_uid_expr(left_alias)
|
||||
right_uid = _scene_uid_expr(right_alias)
|
||||
@@ -48,6 +80,9 @@ def _hard_constraints_expr(left_alias: str, right_alias: str) -> str:
|
||||
f"AND {left_alias}.orbit_direction IS NOT NULL "
|
||||
f"AND {right_alias}.orbit_direction IS NOT NULL "
|
||||
f"AND {left_alias}.orbit_direction = {right_alias}.orbit_direction "
|
||||
f"AND COALESCE({left_alias}.insar_source_ready, false) "
|
||||
f"AND COALESCE({right_alias}.insar_source_ready, false) "
|
||||
f"AND { _same_look_direction_expr(left_alias, right_alias) } "
|
||||
f"AND ST_Intersects({left_alias}.geom, {right_alias}.geom)"
|
||||
)
|
||||
|
||||
@@ -55,6 +90,9 @@ def _hard_constraints_expr(left_alias: str, right_alias: str) -> str:
|
||||
def _full_rebuild_insert_sql() -> str:
|
||||
master_uid = _scene_uid_expr("m")
|
||||
slave_uid = _scene_uid_expr("s")
|
||||
center_distance = "ST_DistanceSphere(ST_Centroid(m.geom), ST_Centroid(s.geom))::double precision"
|
||||
master_family = _satellite_family_expr("m")
|
||||
slave_family = _satellite_family_expr("s")
|
||||
return f"""
|
||||
INSERT INTO pairing_metric_cache (
|
||||
master_scene_ref_id,
|
||||
@@ -66,19 +104,26 @@ def _full_rebuild_insert_sql() -> str:
|
||||
orientation_rule_version,
|
||||
time_baseline_days,
|
||||
spatial_baseline_meters,
|
||||
scene_center_distance_meters,
|
||||
scene_overlap_ratio,
|
||||
orbit_direction,
|
||||
same_satellite,
|
||||
same_satellite_family,
|
||||
same_look_direction,
|
||||
same_imaging_mode,
|
||||
same_polarization,
|
||||
master_imaging_date,
|
||||
slave_imaging_date,
|
||||
master_satellite,
|
||||
slave_satellite,
|
||||
master_satellite_family,
|
||||
slave_satellite_family,
|
||||
master_imaging_mode,
|
||||
slave_imaging_mode,
|
||||
master_polarization,
|
||||
slave_polarization,
|
||||
master_look_direction,
|
||||
slave_look_direction,
|
||||
master_file_path,
|
||||
slave_file_path,
|
||||
status,
|
||||
@@ -93,13 +138,16 @@ def _full_rebuild_insert_sql() -> str:
|
||||
:metric_version AS metric_version,
|
||||
:orientation_rule_version AS orientation_rule_version,
|
||||
ABS(to_date(s.imaging_date, 'YYYYMMDD') - to_date(m.imaging_date, 'YYYYMMDD')) AS time_baseline_days,
|
||||
ST_DistanceSphere(ST_Centroid(m.geom), ST_Centroid(s.geom))::double precision AS spatial_baseline_meters,
|
||||
{center_distance} AS spatial_baseline_meters,
|
||||
{center_distance} AS scene_center_distance_meters,
|
||||
(
|
||||
ST_Area(ST_Intersection(m.geom, s.geom)::geography) /
|
||||
NULLIF(GREATEST(ST_Area(m.geom::geography), ST_Area(s.geom::geography)), 0)
|
||||
)::double precision AS scene_overlap_ratio,
|
||||
m.orbit_direction,
|
||||
(m.satellite IS NOT NULL AND s.satellite IS NOT NULL AND m.satellite = s.satellite) AS same_satellite,
|
||||
{ _same_satellite_family_expr('m', 's') } AS same_satellite_family,
|
||||
{ _same_look_direction_expr('m', 's') } AS same_look_direction,
|
||||
(
|
||||
NULLIF(m.imaging_mode, '') IS NOT NULL
|
||||
AND NULLIF(s.imaging_mode, '') IS NOT NULL
|
||||
@@ -114,10 +162,14 @@ def _full_rebuild_insert_sql() -> str:
|
||||
s.imaging_date AS slave_imaging_date,
|
||||
m.satellite AS master_satellite,
|
||||
s.satellite AS slave_satellite,
|
||||
{master_family} AS master_satellite_family,
|
||||
{slave_family} AS slave_satellite_family,
|
||||
m.imaging_mode AS master_imaging_mode,
|
||||
s.imaging_mode AS slave_imaging_mode,
|
||||
m.polarization AS master_polarization,
|
||||
s.polarization AS slave_polarization,
|
||||
m.look_direction AS master_look_direction,
|
||||
s.look_direction AS slave_look_direction,
|
||||
m.file_path AS master_file_path,
|
||||
s.file_path AS slave_file_path,
|
||||
'READY' AS status,
|
||||
@@ -133,6 +185,9 @@ def _incremental_insert_sql() -> str:
|
||||
dirty_uid = _scene_uid_expr("d")
|
||||
other_uid = _scene_uid_expr("o")
|
||||
dirty_is_master = _orientation_is_left_master_expr("d", "o")
|
||||
center_distance = "ST_DistanceSphere(ST_Centroid(d.geom), ST_Centroid(o.geom))::double precision"
|
||||
dirty_family = _satellite_family_expr("d")
|
||||
other_family = _satellite_family_expr("o")
|
||||
return f"""
|
||||
INSERT INTO pairing_metric_cache (
|
||||
master_scene_ref_id,
|
||||
@@ -144,19 +199,26 @@ def _incremental_insert_sql() -> str:
|
||||
orientation_rule_version,
|
||||
time_baseline_days,
|
||||
spatial_baseline_meters,
|
||||
scene_center_distance_meters,
|
||||
scene_overlap_ratio,
|
||||
orbit_direction,
|
||||
same_satellite,
|
||||
same_satellite_family,
|
||||
same_look_direction,
|
||||
same_imaging_mode,
|
||||
same_polarization,
|
||||
master_imaging_date,
|
||||
slave_imaging_date,
|
||||
master_satellite,
|
||||
slave_satellite,
|
||||
master_satellite_family,
|
||||
slave_satellite_family,
|
||||
master_imaging_mode,
|
||||
slave_imaging_mode,
|
||||
master_polarization,
|
||||
slave_polarization,
|
||||
master_look_direction,
|
||||
slave_look_direction,
|
||||
master_file_path,
|
||||
slave_file_path,
|
||||
status,
|
||||
@@ -176,13 +238,16 @@ def _incremental_insert_sql() -> str:
|
||||
:metric_version AS metric_version,
|
||||
:orientation_rule_version AS orientation_rule_version,
|
||||
ABS(to_date(o.imaging_date, 'YYYYMMDD') - to_date(d.imaging_date, 'YYYYMMDD')) AS time_baseline_days,
|
||||
ST_DistanceSphere(ST_Centroid(d.geom), ST_Centroid(o.geom))::double precision AS spatial_baseline_meters,
|
||||
{center_distance} AS spatial_baseline_meters,
|
||||
{center_distance} AS scene_center_distance_meters,
|
||||
(
|
||||
ST_Area(ST_Intersection(d.geom, o.geom)::geography) /
|
||||
NULLIF(GREATEST(ST_Area(d.geom::geography), ST_Area(o.geom::geography)), 0)
|
||||
)::double precision AS scene_overlap_ratio,
|
||||
d.orbit_direction,
|
||||
(d.satellite IS NOT NULL AND o.satellite IS NOT NULL AND d.satellite = o.satellite) AS same_satellite,
|
||||
{ _same_satellite_family_expr('d', 'o') } AS same_satellite_family,
|
||||
{ _same_look_direction_expr('d', 'o') } AS same_look_direction,
|
||||
(
|
||||
NULLIF(d.imaging_mode, '') IS NOT NULL
|
||||
AND NULLIF(o.imaging_mode, '') IS NOT NULL
|
||||
@@ -197,10 +262,14 @@ def _incremental_insert_sql() -> str:
|
||||
CASE WHEN {dirty_is_master} THEN o.imaging_date ELSE d.imaging_date END AS slave_imaging_date,
|
||||
CASE WHEN {dirty_is_master} THEN d.satellite ELSE o.satellite END AS master_satellite,
|
||||
CASE WHEN {dirty_is_master} THEN o.satellite ELSE d.satellite END AS slave_satellite,
|
||||
CASE WHEN {dirty_is_master} THEN {dirty_family} ELSE {other_family} END AS master_satellite_family,
|
||||
CASE WHEN {dirty_is_master} THEN {other_family} ELSE {dirty_family} END AS slave_satellite_family,
|
||||
CASE WHEN {dirty_is_master} THEN d.imaging_mode ELSE o.imaging_mode END AS master_imaging_mode,
|
||||
CASE WHEN {dirty_is_master} THEN o.imaging_mode ELSE d.imaging_mode END AS slave_imaging_mode,
|
||||
CASE WHEN {dirty_is_master} THEN d.polarization ELSE o.polarization END AS master_polarization,
|
||||
CASE WHEN {dirty_is_master} THEN o.polarization ELSE d.polarization END AS slave_polarization,
|
||||
CASE WHEN {dirty_is_master} THEN d.look_direction ELSE o.look_direction END AS master_look_direction,
|
||||
CASE WHEN {dirty_is_master} THEN o.look_direction ELSE d.look_direction END AS slave_look_direction,
|
||||
CASE WHEN {dirty_is_master} THEN d.file_path ELSE o.file_path END AS master_file_path,
|
||||
CASE WHEN {dirty_is_master} THEN o.file_path ELSE d.file_path END AS slave_file_path,
|
||||
'READY' AS status,
|
||||
@@ -228,7 +297,11 @@ class PairingCacheService:
|
||||
return state
|
||||
|
||||
async def _count_pair_rows(self, db: AsyncSession) -> int:
|
||||
result = await db.execute(select(func.count(PairingMetricCacheORM.id)))
|
||||
result = await db.execute(
|
||||
select(func.count(PairingMetricCacheORM.id)).where(
|
||||
PairingMetricCacheORM.metric_version == pairing_state_service.metric_version
|
||||
)
|
||||
)
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
async def _count_scene_rows(self, db: AsyncSession) -> int:
|
||||
@@ -382,6 +455,17 @@ class PairingCacheService:
|
||||
pair_count = await self._count_pair_rows(db)
|
||||
scene_count = await self._count_scene_rows(db)
|
||||
|
||||
if dirty_scene_count == 0 and self._should_full_rebuild(
|
||||
dirty_scene_count=dirty_scene_count,
|
||||
scene_count=scene_count,
|
||||
pair_count=pair_count,
|
||||
force_full=force_full,
|
||||
):
|
||||
result = await self.rebuild_metric_cache(db, commit=commit)
|
||||
result["trigger_dirty_scene_count"] = dirty_scene_count
|
||||
result["forced"] = force_full
|
||||
return result
|
||||
|
||||
if dirty_scene_count == 0:
|
||||
summary = await self._finalize_state_success(db, full_rebuild=False)
|
||||
if commit:
|
||||
|
||||
@@ -18,7 +18,7 @@ from ..models import (
|
||||
|
||||
|
||||
PAIRING_CACHE_SCOPE_GLOBAL = "global"
|
||||
DEFAULT_PAIRING_METRIC_VERSION = "2026.04.v1"
|
||||
DEFAULT_PAIRING_METRIC_VERSION = "2026.05.raw.v1"
|
||||
PAIRING_ORIENTATION_RULE_VERSION = "date_then_scene_uid_v1"
|
||||
|
||||
|
||||
@@ -66,7 +66,11 @@ class PairingStateService:
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
async def _count_metric_cache_rows(self, db: AsyncSession) -> int:
|
||||
result = await db.execute(select(func.count(PairingMetricCacheORM.id)))
|
||||
result = await db.execute(
|
||||
select(func.count(PairingMetricCacheORM.id)).where(
|
||||
PairingMetricCacheORM.metric_version == self.metric_version
|
||||
)
|
||||
)
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
async def _get_global_state(self, db: AsyncSession) -> Optional[PairingCacheStateORM]:
|
||||
@@ -135,7 +139,12 @@ class PairingStateService:
|
||||
await db.flush()
|
||||
created = True
|
||||
|
||||
state.metric_version = state.metric_version or self.metric_version
|
||||
if state.metric_version != self.metric_version:
|
||||
state.metric_version = self.metric_version
|
||||
state.status = "DIRTY"
|
||||
state.last_error = None
|
||||
else:
|
||||
state.metric_version = state.metric_version or self.metric_version
|
||||
state.scene_count = scene_count
|
||||
state.pair_count = metric_cache_count
|
||||
state.dirty_scene_count = dirty_scene_count
|
||||
|
||||
@@ -149,6 +149,11 @@ def _build_pairing_trace_payload(
|
||||
task_network_edge_id = getattr(task_item, "network_edge_id", None) if task_item is not None else None
|
||||
task_policy_version = getattr(task_item, "policy_version", None) if task_item is not None else None
|
||||
task_selection_strategy = getattr(task_item, "selection_strategy", None) if task_item is not None else None
|
||||
task_scene_center_distance = (
|
||||
getattr(task_item, "scene_center_distance_meters", None)
|
||||
if task_item is not None
|
||||
else None
|
||||
)
|
||||
|
||||
candidate_network_edge_id = _coerce_optional_int(candidate_meta.get("network_edge_id"))
|
||||
task_network_edge_id = _coerce_optional_int(task_network_edge_id)
|
||||
@@ -176,6 +181,11 @@ def _build_pairing_trace_payload(
|
||||
candidate_meta.get("selection_strategy"),
|
||||
task_selection_strategy,
|
||||
),
|
||||
"scene_center_distance_meters": (
|
||||
candidate_meta.get("scene_center_distance_meters")
|
||||
if candidate_meta.get("scene_center_distance_meters") is not None
|
||||
else task_scene_center_distance
|
||||
),
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
@@ -240,6 +250,7 @@ def _resolve_candidate_identity(candidate: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"slave_polarization",
|
||||
"time_baseline_days",
|
||||
"spatial_baseline_meters",
|
||||
"scene_center_distance_meters",
|
||||
):
|
||||
resolved[field] = run_meta.get(field)
|
||||
if resolved[field] in (None, ""):
|
||||
@@ -608,8 +619,11 @@ class ResultCatalogService:
|
||||
"slave_polarization": metric.slave_polarization,
|
||||
"time_baseline_days": metric.time_baseline_days,
|
||||
"spatial_baseline_meters": metric.spatial_baseline_meters,
|
||||
"scene_center_distance_meters": metric.scene_center_distance_meters,
|
||||
"scene_overlap_ratio": metric.scene_overlap_ratio,
|
||||
"same_satellite": metric.same_satellite,
|
||||
"same_satellite_family": metric.same_satellite_family,
|
||||
"same_look_direction": metric.same_look_direction,
|
||||
"same_imaging_mode": metric.same_imaging_mode,
|
||||
"same_polarization": metric.same_polarization,
|
||||
"status": metric.status,
|
||||
@@ -721,6 +735,12 @@ class ResultCatalogService:
|
||||
"orbit_direction": None,
|
||||
"time_baseline_days": getattr(task_item, "time_baseline_days", None) or candidate_meta.get("time_baseline_days"),
|
||||
"spatial_baseline_meters": getattr(task_item, "spatial_baseline_meters", None) or candidate_meta.get("spatial_baseline_meters"),
|
||||
"scene_center_distance_meters": (
|
||||
getattr(task_item, "scene_center_distance_meters", None)
|
||||
or candidate_meta.get("scene_center_distance_meters")
|
||||
or getattr(task_item, "spatial_baseline_meters", None)
|
||||
or candidate_meta.get("spatial_baseline_meters")
|
||||
),
|
||||
"grid_size_m": profile_params.get("target_grid_size_m") or profile_params.get("geocoding_pixel_size_m"),
|
||||
"radar_wavelength": profile_params.get("wavelength"),
|
||||
"orbit_clip_margin": profile_params.get("orbit_margin_sec"),
|
||||
@@ -1138,6 +1158,7 @@ class ResultCatalogService:
|
||||
orbit_direction=profile_payload.get("orbit_direction"),
|
||||
time_baseline_days=profile_payload.get("time_baseline_days"),
|
||||
spatial_baseline_meters=profile_payload.get("spatial_baseline_meters"),
|
||||
scene_center_distance_meters=profile_payload.get("scene_center_distance_meters"),
|
||||
grid_size_m=profile_payload.get("grid_size_m"),
|
||||
radar_wavelength=profile_payload.get("radar_wavelength"),
|
||||
orbit_clip_margin=profile_payload.get("orbit_clip_margin"),
|
||||
@@ -1532,6 +1553,7 @@ class ResultCatalogService:
|
||||
"orbit_direction": profile.orbit_direction,
|
||||
"time_baseline_days": profile.time_baseline_days,
|
||||
"spatial_baseline_meters": profile.spatial_baseline_meters,
|
||||
"scene_center_distance_meters": profile.scene_center_distance_meters,
|
||||
"grid_size_m": profile.grid_size_m,
|
||||
"radar_wavelength": profile.radar_wavelength,
|
||||
"orbit_clip_margin": profile.orbit_clip_margin,
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy import and_, cast, func
|
||||
from sqlalchemy import and_, cast, func, or_
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from geoalchemy2 import Geography
|
||||
@@ -43,7 +43,7 @@ from .dinsar_naming import build_pair_key, build_task_alias, ensure_unique_task_
|
||||
from .pairing_state_service import pairing_state_service
|
||||
|
||||
|
||||
PAIRING_POLICY_VERSION = "2026.04.phase3.v1"
|
||||
PAIRING_POLICY_VERSION = "2026.05.raw-source.v1"
|
||||
PAIRING_WARNING_CANDIDATE_THRESHOLD = 3000
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -155,6 +155,10 @@ class SpatialService:
|
||||
) -> List[dict]:
|
||||
master_alias = aliased(RadarDataORM)
|
||||
slave_alias = aliased(RadarDataORM)
|
||||
center_distance_expr = func.coalesce(
|
||||
PairingMetricCacheORM.scene_center_distance_meters,
|
||||
PairingMetricCacheORM.spatial_baseline_meters,
|
||||
)
|
||||
|
||||
stmt = (
|
||||
select(PairingMetricCacheORM, master_alias, slave_alias)
|
||||
@@ -165,8 +169,9 @@ class SpatialService:
|
||||
PairingMetricCacheORM.status == "READY",
|
||||
PairingMetricCacheORM.time_baseline_days >= params.time_baseline_min,
|
||||
PairingMetricCacheORM.time_baseline_days <= params.time_baseline_max,
|
||||
PairingMetricCacheORM.spatial_baseline_meters <= params.spatial_baseline_max_meters,
|
||||
center_distance_expr <= params.spatial_baseline_max_meters,
|
||||
PairingMetricCacheORM.scene_overlap_ratio >= params.overlap_threshold,
|
||||
PairingMetricCacheORM.same_look_direction.is_(True),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -177,7 +182,7 @@ class SpatialService:
|
||||
)
|
||||
|
||||
if not params.cross_satellite_pairing:
|
||||
stmt = stmt.where(PairingMetricCacheORM.same_satellite.is_(True))
|
||||
stmt = stmt.where(PairingMetricCacheORM.same_satellite_family.is_(True))
|
||||
|
||||
if params.require_same_imaging_mode:
|
||||
stmt = stmt.where(PairingMetricCacheORM.same_imaging_mode.is_(True))
|
||||
@@ -186,9 +191,20 @@ class SpatialService:
|
||||
stmt = stmt.where(PairingMetricCacheORM.same_polarization.is_(True))
|
||||
|
||||
if params.allowed_satellites:
|
||||
allowed_satellites = [
|
||||
str(item).strip().upper()
|
||||
for item in params.allowed_satellites
|
||||
if str(item).strip()
|
||||
]
|
||||
stmt = stmt.where(
|
||||
master_alias.satellite.in_(params.allowed_satellites),
|
||||
slave_alias.satellite.in_(params.allowed_satellites),
|
||||
or_(
|
||||
func.upper(master_alias.satellite).in_(allowed_satellites),
|
||||
func.upper(master_alias.satellite_family).in_(allowed_satellites),
|
||||
),
|
||||
or_(
|
||||
func.upper(slave_alias.satellite).in_(allowed_satellites),
|
||||
func.upper(slave_alias.satellite_family).in_(allowed_satellites),
|
||||
),
|
||||
)
|
||||
|
||||
if params.master_date_from:
|
||||
@@ -220,12 +236,18 @@ class SpatialService:
|
||||
PairingMetricCacheORM.master_imaging_date.asc(),
|
||||
PairingMetricCacheORM.slave_imaging_date.asc(),
|
||||
func.coalesce(PairingMetricCacheORM.scene_overlap_ratio, 0).desc(),
|
||||
center_distance_expr.asc(),
|
||||
PairingMetricCacheORM.pair_uid.asc(),
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
candidate_pool: List[dict] = []
|
||||
for metric_row, master_row, slave_row in result.all():
|
||||
center_distance = float(
|
||||
metric_row.scene_center_distance_meters
|
||||
if metric_row.scene_center_distance_meters is not None
|
||||
else (metric_row.spatial_baseline_meters or 0)
|
||||
)
|
||||
candidate_pool.append(
|
||||
{
|
||||
"metric_cache_ref_id": int(metric_row.id),
|
||||
@@ -235,7 +257,8 @@ class SpatialService:
|
||||
"master": RadarData.model_validate(master_row),
|
||||
"slave": RadarData.model_validate(slave_row),
|
||||
"days": int(metric_row.time_baseline_days or 0),
|
||||
"dist": float(metric_row.spatial_baseline_meters or 0),
|
||||
"dist": center_distance,
|
||||
"scene_center_distance_meters": center_distance,
|
||||
"overlap_ratio": float(metric_row.scene_overlap_ratio or 0),
|
||||
}
|
||||
)
|
||||
@@ -270,6 +293,11 @@ class SpatialService:
|
||||
selection_reason=candidate.get("selection_reason"),
|
||||
time_baseline_days=int(candidate["days"]),
|
||||
spatial_baseline_meters=float(candidate["dist"]),
|
||||
scene_center_distance_meters=float(
|
||||
candidate.get("scene_center_distance_meters")
|
||||
if candidate.get("scene_center_distance_meters") is not None
|
||||
else candidate.get("dist") or 0
|
||||
),
|
||||
)
|
||||
)
|
||||
return result_pairs
|
||||
@@ -356,6 +384,12 @@ class SpatialService:
|
||||
"pair_uid": candidate.get("pair_uid"),
|
||||
"time_baseline_days": int(candidate.get("days") or 0),
|
||||
"spatial_baseline_meters": float(candidate.get("dist") or 0.0),
|
||||
"scene_center_distance_meters": float(
|
||||
candidate.get("scene_center_distance_meters")
|
||||
if candidate.get("scene_center_distance_meters") is not None
|
||||
else candidate.get("dist") or 0.0
|
||||
),
|
||||
"legacy_spatial_baseline_field": "scene_center_distance_meters",
|
||||
"scene_overlap_ratio": float(candidate.get("overlap_ratio") or 0.0),
|
||||
}
|
||||
|
||||
@@ -420,6 +454,10 @@ class SpatialService:
|
||||
|
||||
master_alias = aliased(RadarDataORM)
|
||||
slave_alias = aliased(RadarDataORM)
|
||||
center_distance_expr = func.coalesce(
|
||||
PairingMetricCacheORM.scene_center_distance_meters,
|
||||
PairingMetricCacheORM.spatial_baseline_meters,
|
||||
)
|
||||
stmt = (
|
||||
select(PairingMetricCacheORM, master_alias, slave_alias)
|
||||
.join(master_alias, master_alias.id == PairingMetricCacheORM.master_scene_ref_id)
|
||||
@@ -431,14 +469,15 @@ class SpatialService:
|
||||
PairingMetricCacheORM.slave_scene_ref_id.in_(scene_ids),
|
||||
PairingMetricCacheORM.time_baseline_days >= params.time_baseline_min,
|
||||
PairingMetricCacheORM.time_baseline_days <= params.time_baseline_max,
|
||||
PairingMetricCacheORM.spatial_baseline_meters <= params.spatial_baseline_max_meters,
|
||||
center_distance_expr <= params.spatial_baseline_max_meters,
|
||||
PairingMetricCacheORM.scene_overlap_ratio >= params.network_overlap_threshold,
|
||||
PairingMetricCacheORM.same_look_direction.is_(True),
|
||||
)
|
||||
.order_by(
|
||||
PairingMetricCacheORM.master_imaging_date.asc(),
|
||||
PairingMetricCacheORM.slave_imaging_date.asc(),
|
||||
PairingMetricCacheORM.time_baseline_days.asc(),
|
||||
PairingMetricCacheORM.spatial_baseline_meters.asc(),
|
||||
center_distance_expr.asc(),
|
||||
func.coalesce(PairingMetricCacheORM.scene_overlap_ratio, 0).desc(),
|
||||
PairingMetricCacheORM.id.asc(),
|
||||
)
|
||||
@@ -447,6 +486,11 @@ class SpatialService:
|
||||
|
||||
candidate_pool: List[dict] = []
|
||||
for metric_row, master_row, slave_row in result.all():
|
||||
center_distance = float(
|
||||
metric_row.scene_center_distance_meters
|
||||
if metric_row.scene_center_distance_meters is not None
|
||||
else (metric_row.spatial_baseline_meters or 0.0)
|
||||
)
|
||||
candidate_pool.append(
|
||||
{
|
||||
"metric_cache_ref_id": int(metric_row.id),
|
||||
@@ -456,7 +500,8 @@ class SpatialService:
|
||||
"master": RadarData.model_validate(master_row),
|
||||
"slave": RadarData.model_validate(slave_row),
|
||||
"days": int(metric_row.time_baseline_days or 0),
|
||||
"dist": float(metric_row.spatial_baseline_meters or 0.0),
|
||||
"dist": center_distance,
|
||||
"scene_center_distance_meters": center_distance,
|
||||
"overlap_ratio": float(metric_row.scene_overlap_ratio or 0.0),
|
||||
}
|
||||
)
|
||||
@@ -511,6 +556,11 @@ class SpatialService:
|
||||
"slave_imaging_date": slave.imaging_date,
|
||||
"temporal_baseline_days": int(candidate.get("days") or 0),
|
||||
"spatial_baseline_meters": float(candidate.get("dist") or 0.0),
|
||||
"scene_center_distance_meters": float(
|
||||
candidate.get("scene_center_distance_meters")
|
||||
if candidate.get("scene_center_distance_meters") is not None
|
||||
else candidate.get("dist") or 0.0
|
||||
),
|
||||
"scene_overlap_ratio": float(candidate.get("overlap_ratio") or 0.0),
|
||||
"selection_reason": candidate.get("selection_reason"),
|
||||
"selection_score": (
|
||||
@@ -523,6 +573,11 @@ class SpatialService:
|
||||
"selection_mode": selection_mode,
|
||||
"pair_uid": candidate.get("pair_uid"),
|
||||
"metric_version": pairing_state_service.metric_version,
|
||||
"scene_center_distance_meters": float(
|
||||
candidate.get("scene_center_distance_meters")
|
||||
if candidate.get("scene_center_distance_meters") is not None
|
||||
else candidate.get("dist") or 0.0
|
||||
),
|
||||
"time_baseline_min": params.time_baseline_min,
|
||||
"time_baseline_max": params.time_baseline_max,
|
||||
"spatial_baseline_max_meters": params.spatial_baseline_max_meters,
|
||||
@@ -703,18 +758,44 @@ class SpatialService:
|
||||
if params.strategy == "sbas":
|
||||
return self._apply_sbas_strategy(candidate_pool, params, aoi_wkt=aoi_wkt)
|
||||
if params.strategy == "sequential":
|
||||
return self._apply_sequential_strategy(candidate_pool, params.num_connections)
|
||||
return self._apply_sequential_strategy(candidate_pool, params.num_connections, params)
|
||||
if params.strategy == "star":
|
||||
return self._apply_star_strategy(candidate_pool, params.reference_image_id)
|
||||
return self._apply_all_strategy(candidate_pool)
|
||||
return self._apply_star_strategy(candidate_pool, params.reference_image_id, params)
|
||||
return self._apply_all_strategy(candidate_pool, params)
|
||||
|
||||
def _apply_all_strategy(self, candidate_pool: List[dict]) -> Tuple[List[dict], List[str]]:
|
||||
def _score_pair_candidate(self, candidate: dict, params: PairingRequest) -> float:
|
||||
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)
|
||||
center_score = 1.0 - min(float(candidate.get("dist") or 0) / max_center, 1.0)
|
||||
overlap_score = min(max(float(candidate.get("overlap_ratio") or 0), 0.0), 1.0)
|
||||
source_score = 1.0 if (
|
||||
bool(getattr(candidate.get("master"), "insar_source_ready", False))
|
||||
and bool(getattr(candidate.get("slave"), "insar_source_ready", False))
|
||||
) else 0.0
|
||||
orbit_score = 1.0 if (
|
||||
bool(getattr(candidate.get("master"), "has_orbit_data", False))
|
||||
and bool(getattr(candidate.get("slave"), "has_orbit_data", False))
|
||||
) else 0.0
|
||||
return (
|
||||
0.25 * time_score
|
||||
+ 0.20 * center_score
|
||||
+ 0.35 * overlap_score
|
||||
+ 0.15 * source_score
|
||||
+ 0.05 * orbit_score
|
||||
)
|
||||
|
||||
def _apply_all_strategy(
|
||||
self,
|
||||
candidate_pool: List[dict],
|
||||
params: PairingRequest,
|
||||
) -> Tuple[List[dict], List[str]]:
|
||||
return (
|
||||
[
|
||||
{
|
||||
**candidate,
|
||||
"selection_reason": "all_candidate",
|
||||
"selection_score": float(candidate.get("overlap_ratio") or 0),
|
||||
"selection_score": self._score_pair_candidate(candidate, params),
|
||||
}
|
||||
for candidate in self._sorted_candidates(candidate_pool)
|
||||
],
|
||||
@@ -725,6 +806,7 @@ class SpatialService:
|
||||
self,
|
||||
candidate_pool: List[dict],
|
||||
num_connections: int,
|
||||
params: PairingRequest,
|
||||
) -> Tuple[List[dict], List[str]]:
|
||||
"""
|
||||
Sequential: 按稳定时间序列排序,每景连接后续 N 景。
|
||||
@@ -759,7 +841,7 @@ class SpatialService:
|
||||
{
|
||||
**candidate,
|
||||
"selection_reason": "sequential_neighbor",
|
||||
"selection_score": float(candidate.get("overlap_ratio") or 0),
|
||||
"selection_score": self._score_pair_candidate(candidate, params),
|
||||
}
|
||||
)
|
||||
picked_count += 1
|
||||
@@ -770,6 +852,7 @@ class SpatialService:
|
||||
self,
|
||||
candidate_pool: List[dict],
|
||||
reference_image_id: Optional[int],
|
||||
params: PairingRequest,
|
||||
) -> Tuple[List[dict], List[str]]:
|
||||
"""
|
||||
Star: 参考像固定为 master。
|
||||
@@ -820,7 +903,7 @@ class SpatialService:
|
||||
{
|
||||
**candidate,
|
||||
"selection_reason": "star_reference_master",
|
||||
"selection_score": float(candidate.get("overlap_ratio") or 0),
|
||||
"selection_score": self._score_pair_candidate(candidate, params),
|
||||
"is_reference_edge": True,
|
||||
"reference_image_id": int(reference_image_id),
|
||||
}
|
||||
@@ -1136,6 +1219,14 @@ class SpatialService:
|
||||
time_score = 1.0 - min(float(candidate.get("days") or 0) / max_time, 1.0)
|
||||
spatial_score = 1.0 - min(float(candidate.get("dist") or 0) / max_space, 1.0)
|
||||
overlap_score = min(max(float(candidate.get("overlap_ratio") or 0), 0.0), 1.0)
|
||||
source_score = 1.0 if (
|
||||
bool(getattr(candidate.get("master"), "insar_source_ready", False))
|
||||
and bool(getattr(candidate.get("slave"), "insar_source_ready", False))
|
||||
) else 0.0
|
||||
orbit_score = 1.0 if (
|
||||
bool(getattr(candidate.get("master"), "has_orbit_data", False))
|
||||
and bool(getattr(candidate.get("slave"), "has_orbit_data", False))
|
||||
) else 0.0
|
||||
|
||||
aoi_gain = 0.0
|
||||
redundancy_penalty = 0.0
|
||||
@@ -1153,10 +1244,12 @@ class SpatialService:
|
||||
redundancy_penalty = max(0.0, min(overlap_area / candidate_area, 1.0))
|
||||
|
||||
return (
|
||||
0.35 * time_score
|
||||
+ 0.20 * spatial_score
|
||||
0.30 * time_score
|
||||
+ 0.15 * spatial_score
|
||||
+ 0.30 * overlap_score
|
||||
+ 0.15 * aoi_gain
|
||||
+ 0.10 * aoi_gain
|
||||
+ 0.10 * source_score
|
||||
+ 0.05 * orbit_score
|
||||
- float(params.coverage_diversity_penalty or 0.0) * redundancy_penalty
|
||||
)
|
||||
|
||||
@@ -1588,7 +1681,7 @@ class SpatialService:
|
||||
slave: RadarDataORM
|
||||
) -> float:
|
||||
"""
|
||||
Calculate spatial baseline in meters using PostGIS sphere distance.
|
||||
Calculate footprint center distance in meters using PostGIS sphere distance.
|
||||
"""
|
||||
master_alias = RadarDataORM.__table__.alias("master")
|
||||
slave_alias = RadarDataORM.__table__.alias("slave")
|
||||
|
||||
Reference in New Issue
Block a user