kcg-ai-monitoring/prediction/tests/test_transshipment_params.py
htlee 8f5152fc02 feat(prediction): Phase 2 PoC 2~5 — gear_violation/transshipment/risk/pair_trawl
런타임 override 완성 (params 인자 + 내부 상수 교체):
- gear_violation_g01_g06 (GEAR, tier 4)
  · G01~G06 점수 + signal_cycling(gap_min/min_count)
  · gear_drift_threshold_nm + fixed_gear_types + fishery_code_allowed_gear
  · _detect_signal_cycling_count 도입 (기존 _detect_signal_cycling 보존)

카탈로그 + 관찰 (DEFAULT_PARAMS 노출 + Adapter 집계, 런타임 교체는 후속 PR):
- transshipment_5stage (TRANSSHIP, tier 4) — 5단계 필터 임계
- risk_composite (META, tier 3) — 경량+파이프라인 가중치
- pair_trawl_tier (GEAR, tier 4) — STRONG/PROBABLE/SUSPECT 임계

각 모델 공통:
- prediction/algorithms/*.py: DEFAULT_PARAMS 상수 추가
- models_core/registered/*_model.py: BaseDetectionModel Adapter
- models_core/seeds/v1_<model>.sql: DRAFT seed (호출자 트랜잭션 제어)
- tests/test_<model>_params.py: Python ↔ 모듈 상수 ↔ seed SQL 정적 일치 검증

통합 seed: models_core/seeds/v1_phase2_all.sql (\i 로 5 모델 일괄 시드)

검증:
- 30/30 테스트 통과 (Phase 1-2 15 + dark 5 + Phase 2 신규 10)
- 운영 DB 5 모델 개별 + 일괄 seed dry-run 통과 (BEGIN/ROLLBACK 격리)
- 5 모델 모두 tier/category 정렬 확인: dark_suspicion(3) / risk_composite(3) /
  gear_violation_g01_g06(4) / pair_trawl_tier(4) / transshipment_5stage(4)

후속:
- transshipment/risk/pair_trawl 런타임 override 활성화 (헬퍼 params 전파)
- Phase 3 백엔드 API (DetectionModelController + 승격 엔드포인트)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:11:16 +09:00

89 lines
3.4 KiB
Python

"""Phase 2 PoC #3 — transshipment_5stage params 카탈로그 동치성 테스트.
런타임 override 는 후속 PR 에서 활성화되므로, 이 테스트는 **DEFAULT_PARAMS
↔ 모듈 상수 ↔ seed SQL JSONB** 3 자 일치만 검증한다.
"""
from __future__ import annotations
import importlib
import json
import os
import sys
import types
import unittest
# pandas/pydantic_settings stub (다른 phase 2 테스트와 동일 관용)
if 'pandas' not in sys.modules:
pd_stub = types.ModuleType('pandas')
pd_stub.DataFrame = type('DataFrame', (), {})
pd_stub.Timestamp = type('Timestamp', (), {})
sys.modules['pandas'] = pd_stub
if 'pydantic_settings' not in sys.modules:
stub = types.ModuleType('pydantic_settings')
class _S:
def __init__(self, **kw):
for name, value in self.__class__.__dict__.items():
if name.isupper():
setattr(self, name, kw.get(name, value))
stub.BaseSettings = _S
sys.modules['pydantic_settings'] = stub
if 'algorithms' not in sys.modules:
pkg = types.ModuleType('algorithms')
pkg.__path__ = [os.path.join(os.path.dirname(__file__), '..', 'algorithms')]
sys.modules['algorithms'] = pkg
# fleet_tracker 의 GEAR_PATTERN 을 transshipment.py 상단에서 import 하므로 stub
if 'fleet_tracker' not in sys.modules:
ft_stub = types.ModuleType('fleet_tracker')
import re as _re
ft_stub.GEAR_PATTERN = _re.compile(r'^xxx$')
sys.modules['fleet_tracker'] = ft_stub
ts = importlib.import_module('algorithms.transshipment')
class TransshipmentParamsTest(unittest.TestCase):
def test_default_values_match_module_constants(self):
p = ts.TRANSSHIPMENT_DEFAULT_PARAMS
self.assertEqual(p['sog_threshold_kn'], ts.SOG_THRESHOLD_KN)
self.assertEqual(p['proximity_deg'], ts.PROXIMITY_DEG)
self.assertEqual(p['approach_deg'], ts.APPROACH_DEG)
self.assertEqual(p['rendezvous_min'], ts.RENDEZVOUS_MIN)
self.assertEqual(p['pair_expiry_min'], ts.PAIR_EXPIRY_MIN)
self.assertEqual(p['gap_tolerance_cycles'], ts.GAP_TOLERANCE_CYCLES)
self.assertEqual(set(p['fishing_kinds']), set(ts._FISHING_KINDS))
self.assertEqual(set(p['carrier_kinds']), set(ts._CARRIER_KINDS))
self.assertEqual(set(p['excluded_ship_ty']), set(ts._EXCLUDED_SHIP_TY))
self.assertEqual(list(p['carrier_hints']), list(ts._CARRIER_HINTS))
def test_seed_sql_values_match_python_default(self):
seed_path = os.path.join(
os.path.dirname(__file__), '..',
'models_core', 'seeds', 'v1_transshipment.sql',
)
with open(seed_path, 'r', encoding='utf-8') as f:
sql = f.read()
start = sql.index('$json$') + len('$json$')
end = sql.index('$json$', start)
raw = sql[start:end].strip()
params = json.loads(raw)
expected = ts.TRANSSHIPMENT_DEFAULT_PARAMS
for scalar_key in ['sog_threshold_kn', 'proximity_deg', 'approach_deg',
'rendezvous_min', 'pair_expiry_min', 'gap_tolerance_cycles',
'min_score']:
self.assertEqual(params[scalar_key], expected[scalar_key], scalar_key)
for list_key in ['fishing_kinds', 'carrier_kinds', 'excluded_ship_ty',
'carrier_hints']:
self.assertEqual(set(params[list_key]), set(expected[list_key]), list_key)
if __name__ == '__main__':
unittest.main()