# Gear Parent Inference Algorithm Spec ## 문서 목적 이 문서는 현재 구현된 어구 모선 추적 알고리즘을 모듈, 메서드, 파라미터, 판단 기준, 저장소, 엔드포인트, 영향 관계 기준으로 정리한 구현 명세다. `GEAR-PARENT-INFERENCE-DATAFLOW-PAPER.md`가 서술형 통합 문서라면, 이 문서는 구현과 후속 변경 작업에 바로 연결할 수 있는 참조 스펙이다. ## 1. 시스템 요약 ### 1.1 현재 목적 - 최근 24시간 한국 수역 AIS를 캐시에 유지한다. - 어구 이름 패턴과 위치를 기준으로 어구 그룹을 만든다. - 주변 선박/오분류 어구를 correlation 후보로 평가한다. - 후보 중 대표 모선 가능성이 높은 선박을 추론한다. - 사람의 라벨/제외를 별도 저장소에 남겨 향후 모델 평가와 자동화 전환에 활용한다. ### 1.2 현재 점수 구조의 역할 구분 - `gear_correlation_scores.current_score` - 후보 스크리닝용 correlation score - EMA 기반 단기 메모리 - `gear_group_parent_candidate_snapshots.final_score` - 모선 추론용 최종 후보 점수 - coverage-aware 보정과 이름/안정성/episode/lineage/label prior 반영 - `gear_group_parent_resolution` - 그룹별 현재 추론 상태 - `gear_group_episodes`, `gear_group_episode_snapshots` - `sub_cluster_id`와 분리된 continuity memory - `gear_parent_label_tracking_cycles` - 라벨 세션 동안의 자동 추론 성능 추적 ## 2. 현재 DB 저장소와 유지 기간 | 저장소 | 역할 | 현재 유지 규칙 | | --- | --- | --- | | `group_polygon_snapshots` | `1h/1h-fb/6h` 그룹 스냅샷 | `7일` cleanup | | `gear_correlation_raw_metrics` | correlation raw metric 시계열 | `7일` retention partition | | `gear_correlation_scores` | correlation EMA score 현재 상태 | `30일` 미관측 시 cleanup | | `gear_group_parent_candidate_snapshots` | cycle별 parent candidate snapshot | 현재 자동 cleanup 없음 | | `gear_group_parent_resolution` | 그룹별 현재 추론 상태 1행 | 현재 자동 cleanup 없음 | | `gear_group_episodes` | active/merged/expired episode 현재 상태 | 현재 자동 cleanup 없음 | | `gear_group_episode_snapshots` | cycle별 episode continuity 스냅샷 | 현재 자동 cleanup 없음 | | `gear_parent_candidate_exclusions` | 그룹/전역 후보 제외 | 기간 종료 또는 수동 해제까지 | | `gear_parent_label_sessions` | 정답 라벨 세션 | 만료 시 `EXPIRED`, row는 유지 | | `gear_parent_label_tracking_cycles` | 라벨 세션 cycle별 추적 | 현재 자동 cleanup 없음 | ## 3. 모듈 인덱스 ### 3.1 시간/원천 적재 | 모듈 | 메서드 | 역할 | | --- | --- | --- | | `prediction/time_bucket.py` | `compute_safe_bucket()` | DB 적재 완료 전 bucket 차단 | | `prediction/time_bucket.py` | `compute_initial_window_start()` | 초기 24h window 시작점 | | `prediction/time_bucket.py` | `compute_incremental_window_start()` | overlap backfill 시작점 | | `prediction/db/snpdb.py` | `fetch_all_tracks()` | safe bucket까지 초기 bulk 적재 | | `prediction/db/snpdb.py` | `fetch_incremental()` | backfill 포함 증분 적재 | | `prediction/cache/vessel_store.py` | `load_initial()` | 초기 메모리 캐시 구성 | | `prediction/cache/vessel_store.py` | `merge_incremental()` | 증분 merge + dedupe | | `prediction/cache/vessel_store.py` | `evict_stale()` | 24h sliding window 유지 | ### 3.2 어구 identity / 그룹 | 모듈 | 메서드 | 역할 | | --- | --- | --- | | `prediction/fleet_tracker.py` | `track_gear_identity()` | 어구 이름 파싱, identity log 관리 | | `prediction/algorithms/gear_name_rules.py` | `normalize_parent_name()` | 모선명 정규화 | | `prediction/algorithms/gear_name_rules.py` | `is_trackable_parent_name()` | 짧은 이름 제외 | | `prediction/algorithms/polygon_builder.py` | `detect_gear_groups()` | 어구 그룹 및 서브클러스터 생성 | | `prediction/algorithms/polygon_builder.py` | `build_all_group_snapshots()` | `1h/1h-fb/6h` 스냅샷 저장용 생성 | ### 3.3 correlation / parent inference | 모듈 | 메서드 | 역할 | | --- | --- | --- | | `prediction/algorithms/gear_correlation.py` | `run_gear_correlation()` | raw metric + EMA score 계산 | | `prediction/algorithms/gear_correlation.py` | `_compute_gear_vessel_metrics()` | proximity/visit/activity 계산 | | `prediction/algorithms/gear_correlation.py` | `update_score()` | EMA + freeze/decay 상태 전이 | | `prediction/algorithms/gear_parent_episode.py` | `build_episode_plan()` | continuity source와 episode assignment 계산 | | `prediction/algorithms/gear_parent_episode.py` | `compute_prior_bonus_components()` | episode/lineage/label prior bonus 계산 | | `prediction/algorithms/gear_parent_episode.py` | `sync_episode_states()` | `gear_group_episodes` upsert | | `prediction/algorithms/gear_parent_episode.py` | `insert_episode_snapshots()` | episode snapshot append | | `prediction/algorithms/gear_parent_inference.py` | `run_gear_parent_inference()` | 최종 모선 추론 실행 | | `prediction/algorithms/gear_parent_inference.py` | `_build_candidate_scores()` | 후보별 상세 점수 계산 | | `prediction/algorithms/gear_parent_inference.py` | `_name_match_score()` | 이름 점수 규칙 | | `prediction/algorithms/gear_parent_inference.py` | `_build_track_coverage_metrics()` | coverage-aware evidence 계산 | | `prediction/algorithms/gear_parent_inference.py` | `_select_status()` | 상태 전이 규칙 | ### 3.4 backend read model / workflow | 모듈 | 메서드 | 역할 | | --- | --- | --- | | `GroupPolygonService.java` | group list/review/detail SQL | 최신 `1h` live + stale suppression read model | | `ParentInferenceWorkflowController.java` | exclusion/label API | 사람 판단 저장소 API | ## 4. 메서드 상세 ## 4.1 `prediction/time_bucket.py` ### `compute_safe_bucket(now: datetime | None = None) -> datetime` - 입력: - 현재 시각 - 출력: - `safe_delay`를 뺀 뒤 5분 단위로 내림한 KST naive bucket - 기준: - `SNPDB_SAFE_DELAY_MIN` - 영향: - 초기 적재, 증분 적재, eviction 기준점 ### `compute_incremental_window_start(last_bucket: datetime) -> datetime` - 입력: - 현재 캐시의 마지막 처리 bucket - 출력: - `last_bucket - SNPDB_BACKFILL_BUCKETS * 5m` - 의미: - 늦게 들어온 같은 bucket row 재흡수 ## 4.2 `prediction/db/snpdb.py` ### `fetch_all_tracks(hours: int = 24) -> pd.DataFrame` - 역할: - safe bucket까지 최근 `N`시간 full load - 핵심 쿼리 조건: - bbox: `122,31,132,39` - `time_bucket > window_start` - `time_bucket <= safe_bucket` - 출력 컬럼: - `mmsi`, `timestamp`, `time_bucket`, `lat`, `lon`, `raw_sog` ### `fetch_incremental(last_bucket: datetime) -> pd.DataFrame` - 역할: - overlap backfill 포함 증분 load - 핵심 쿼리 조건: - `time_bucket > from_bucket` - `time_bucket <= safe_bucket` - 주의: - 이미 본 bucket도 일부 다시 읽는 구조다 ## 4.3 `prediction/cache/vessel_store.py` ### `load_initial(hours: int = 24) -> None` - 역할: - 초기 bulk DataFrame을 MMSI별 track cache로 구성 - 파생 효과: - `_last_bucket` 갱신 - static info refresh - permit registry refresh ### `merge_incremental(df_new: pd.DataFrame) -> None` - 역할: - 증분 batch merge - 기준: - `timestamp`, `time_bucket` 정렬 - `timestamp` 기준 dedupe - 영향: - 같은 bucket overlap backfill에서도 최종 row만 유지 ### `evict_stale(hours: int = 24) -> None` - 역할: - sliding 24h 유지 - 기준: - `time_bucket` 있으면 bucket 기준 - 없으면 timestamp fallback ## 4.4 `prediction/fleet_tracker.py` ### `track_gear_identity(gear_signals, conn) -> None` - 역할: - 어구 이름 패턴에서 `parent_name`, `gear_index_1`, `gear_index_2` 추출 - `gear_identity_log` insert/update - 입력: - gear signal list - 주요 기준: - 정규화 길이 `< 4`면 건너뜀 - 같은 이름, 다른 MMSI는 identity migration 처리 - 영향: - `gear_correlation_scores.target_mmsi`를 새 MMSI로 이전 가능 ## 4.5 `prediction/algorithms/polygon_builder.py` ### `detect_gear_groups(vessel_store) -> list[dict]` - 역할: - 어구 이름 기반 raw group 생성 - 거리 기반 서브클러스터 분리 - 근접 병합 - 입력: - `all_positions` - 주요 기준: - 어구 패턴 매칭 - `440/441` 제외 - `is_trackable_parent_name()` - `MAX_DIST_DEG = 0.15` - 출력: - `parent_name`, `parent_mmsi`, `sub_cluster_id`, `members` ### `build_all_group_snapshots(vessel_store, company_vessels, companies) -> list[dict]` - 역할: - `FLEET`, `GEAR_IN_ZONE`, `GEAR_OUT_ZONE`의 `1h/1h-fb/6h` snapshot 생성 - 주요 기준: - 같은 `parent_name` 전체 기준 1h active member 수 - `GEAR_OUT_ZONE` 최소 멤버 수 - parent nearby 시 `isParent=true` ## 4.6 `prediction/algorithms/gear_correlation.py` ### `run_gear_correlation(vessel_store, gear_groups, conn) -> dict` - 역할: - 그룹당 후보 탐색 - raw metric 저장 - EMA score 갱신 - 입력: - `gear_groups` - 출력: - `updated`, `models`, `raw_inserted` ### `_compute_gear_vessel_metrics(gear_center_lat, gear_center_lon, gear_radius_nm, vessel_track, params) -> dict` - 출력 metric: - `proximity_ratio` - `visit_score` - `activity_sync` - `composite` - 한계: - raw metric은 짧은 항적에 과대 우호적일 수 있음 - 이 문제는 parent inference 단계의 coverage-aware 보정에서 완화 ### `update_score(prev_score, raw_score, streak, last_observed, now, gear_group_active_ratio, shadow_bonus, params) -> tuple` - 상태: - `ACTIVE` - `PATTERN_DIVERGE` - `GROUP_QUIET` - `NORMAL_GAP` - `SIGNAL_LOSS` - 의미: - correlation score는 장기 기억보다 short-memory EMA에 가깝다 ## 4.7 `prediction/algorithms/gear_parent_inference.py` ### `run_gear_parent_inference(vessel_store, gear_groups, conn) -> dict[str, int]` - 역할: - direct parent 보강 - active exclusion/label 적용 - 후보 점수 계산 - 상태 전이 - snapshot/resolution/tracking 저장 ### `_load_existing_resolution(conn, group_keys) -> dict` - 역할: - 현재 그룹의 이전 resolution 상태 로드 - 현재 쓰임: - `PREVIOUS_SELECTION` 후보 seed - `stable_cycles` - `MANUAL_CONFIRMED` 유지 - reject cooldown ### `_build_candidate_scores(...) -> list[CandidateScore]` - 후보 집합 원천: - 상위 correlation 후보 - registry name exact bucket - previous selection - 제거 규칙: - global exclusion - group exclusion - reject cooldown - 점수 항목: - `base_corr_score` - `name_match_score` - `track_similarity_score` - `visit_score_6h` - `proximity_score_6h` - `activity_sync_score_6h` - `stability_score` - `registry_bonus` - `china_mmsi_bonus` 후가산 ### `_name_match_score(parent_name, candidate_name, registry) -> float` - 규칙: - 원문 동일 `1.0` - 정규화 동일 `0.8` - prefix/contains `0.5` - 숫자 제거 후 문자 부분 동일 `0.3` - else `0.0` ### `_build_track_coverage_metrics(center_track, vessel_track, gear_center_lat, gear_center_lon) -> dict` - 역할: - short-track 과대평가 방지용 증거 강도 계산 - 핵심 출력: - `trackCoverageFactor` - `visitCoverageFactor` - `activityCoverageFactor` - `coverageFactor` - downstream: - `track`, `visit`, `proximity`, `activity` raw score에 곱해 effective score 생성 ## 4.8 `prediction/algorithms/gear_parent_episode.py` ### `build_episode_plan(groups, previous_by_lineage) -> EpisodePlan` - 역할: - 현재 cycle group을 이전 active episode와 매칭 - `NEW`, `CONTINUED`, `SPLIT_CONTINUE`, `SPLIT_NEW`, `MERGE_NEW` 결정 - 입력: - `GroupEpisodeInput[]` - 최근 `6h` active `EpisodeState[]` - continuity score: - `0.75 * member_jaccard + 0.25 * center_support` - 기준: - `member_jaccard` - 중심점 거리 `12nm` - continuity score threshold `0.45` - merge score threshold `0.35` - 출력: - assignment map - expired episode set - merged target map ### `compute_prior_bonus_components(...) -> dict[str, float]` - 역할: - 동일 candidate에 대한 episode/lineage/label prior bonus 계산 - 입력 집계 범위: - episode prior: `24h` - lineage prior: `7d` - label prior: `30d` - cap: - `episode <= 0.10` - `lineage <= 0.05` - `label <= 0.10` - `total <= 0.20` - 출력: - `episodePriorBonus` - `lineagePriorBonus` - `labelPriorBonus` - `priorBonusTotal` ### `sync_episode_states(conn, observed_at, plan) -> None` - 역할: - active/merged/expired episode 상태를 `gear_group_episodes`에 반영 - 기준: - merge 대상은 `MERGED` - continuity 없는 old episode는 `EXPIRED` ### `insert_episode_snapshots(conn, observed_at, plan, payloads) -> int` - 역할: - cycle별 continuity 결과와 top candidate/result를 `gear_group_episode_snapshots`에 저장 - 기록: - `episode_id` - `parent_episode_ids` - `top_candidate_mmsi` - `top_candidate_score` - `resolution_status` ### `_select_status(top_candidate, margin, stable_cycles) -> tuple[str, str]` - 상태: - `NO_CANDIDATE` - `AUTO_PROMOTED` - `REVIEW_REQUIRED` - `UNRESOLVED` - auto promotion 조건: - `target_type == VESSEL` - `CORRELATION` source 포함 - `final_score >= 0.72` - `margin >= 0.15` - `stable_cycles >= 3` - review 조건: - `final_score >= 0.60` ## 5. 현재 엔드포인트 스펙 ## 5.1 조회 계열 ### `/api/kcg/vessel-analysis/groups/parent-inference/review` - 역할: - 최신 전역 `1h` 기준 검토 대기 목록 - 조건: - stale resolution 숨김 - candidate count는 latest candidate snapshot 기준 ### `/api/kcg/vessel-analysis/groups/{groupKey}/parent-inference` - 역할: - 특정 그룹의 현재 live sub-cluster 상세 - 주의: - “현재 최신 전역 `1h`에 실제 존재하는 sub-cluster만” 반환 ### `/api/kcg/vessel-analysis/parent-inference/candidate-exclusions` - 역할: - 그룹/전역 제외 목록 조회 ### `/api/kcg/vessel-analysis/parent-inference/label-sessions` - 역할: - active 또는 전체 라벨 세션 조회 ## 5.2 액션 계열 ### `POST /candidate-exclusions/global` - 역할: - 전역 후보 제외 생성 - 영향: - 다음 cycle부터 모든 그룹에서 해당 MMSI 제거 ### `POST /groups/{groupKey}/parent-inference/{subClusterId}/exclude` - 역할: - 그룹 단위 후보 제외 생성 - 영향: - 다음 cycle부터 해당 그룹에서만 제거 ### `POST /groups/{groupKey}/parent-inference/{subClusterId}/label` - 역할: - 기간형 정답 라벨 세션 생성 - 영향: - 다음 cycle부터 tracking row 누적 ## 6. 현재 기억 구조와 prior bonus ### 6.1 short-memory와 long-memory의 분리 - `gear_correlation_scores` - EMA short-memory - 미관측 시 decay - 현재 후보 seed 역할 - `gear_group_parent_resolution` - 현재 상태 1행 - same-episode가 아니면 `PREVIOUS_SELECTION` carry를 직접 사용하지 않음 - `gear_group_episodes` - continuity memory - `candidate_snapshots` - bonus 집계 원천 ### 6.2 현재 final score의 장기 기억 반영 현재는 과거 점수를 직접 carry하지 않고, 약한 prior bonus만 후가산한다. ```text final_score = current_signal_score + china_mmsi_bonus + prior_bonus_total ``` 여기서 `prior_bonus_total`은: - `episode_prior_bonus` - `lineage_prior_bonus` - `label_prior_bonus` 의 합이며 총합 cap은 `0.20`이다. ### 6.3 왜 weak prior인가 과거 점수를 그대로 넘기면: - 다른 episode로 잘못 관성이 전이될 수 있다 - split/merge 이후 잘못된 top1이 고착될 수 있다 - 오래된 오답이 장기 drift로 남을 수 있다 그래서 현재 구현은 과거 점수를 “현재 score 자체”가 아니라 “작은 bonus”로만 쓴다. ## 7. 현재 continuity / prior 동작 ### 7.1 episode continuity - 같은 lineage 안에서 최근 `6h` active episode를 불러온다. - continuity score가 높은 이전 episode가 있으면 `CONTINUED` - 1개 parent episode가 여러 current cluster로 이어지면 `SPLIT_CONTINUE` + `SPLIT_NEW` - 여러 previous episode가 하나 current cluster로 모이면 `MERGE_NEW` - 어떤 current와도 연결되지 못한 old episode는 `EXPIRED` ### 7.2 prior 집계 | prior | 참조 범위 | 현재 집계 값 | | --- | --- | --- | | episode prior | 최근 동일 episode `24h` | seen_count, top1_count, avg_score, last_seen_at | | lineage prior | 동일 이름 lineage `7d` | seen_count, top1_count, top3_count, avg_score, last_seen_at | | label prior | 라벨 세션 `30d` | session_count, last_labeled_at | ### 7.3 구현 시 주의 - 과거 점수를 직접 누적하지 말 것 - prior는 bonus로만 사용하고 cap을 둘 것 - split/merge 이후 parent 후보 관성은 약하게만 상속할 것 - stale live sub-cluster와 vanished old sub-cluster를 혼동하지 않도록, aggregation도 최신 episode anchor를 기준으로 할 것 ## 8. 참조 문서 - [GEAR-PARENT-INFERENCE-DATAFLOW-PAPER.md](/Users/lht/work/devProjects/iran-airstrike-replay-codex/docs/GEAR-PARENT-INFERENCE-DATAFLOW-PAPER.md) - [GEAR-PARENT-INFERENCE-WORKFLOW-V2.md](/Users/lht/work/devProjects/iran-airstrike-replay-codex/docs/GEAR-PARENT-INFERENCE-WORKFLOW-V2.md) - [GEAR-PARENT-INFERENCE-WORKFLOW-V2-PHASE1.md](/Users/lht/work/devProjects/iran-airstrike-replay-codex/docs/GEAR-PARENT-INFERENCE-WORKFLOW-V2-PHASE1.md)