90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""build_state — NegotiationSnapshot + StateConfig → State/state_index (우리 자체 구현).
|
|
|
|
각 차원 인덱스 산출 규칙(기능적 방법)은 우리 설계이며, 임계값/코드맵은 config 주입이다.
|
|
도메인은 tenant-agnostic: 같은 config + 같은 snapshot 이면 항상 같은 출력(결정론).
|
|
"""
|
|
|
|
from typing import List
|
|
|
|
from negotiation.qtable.domain.model.snapshot import NegotiationSnapshot
|
|
from negotiation.qtable.domain.model.state import State, encode_index
|
|
from tenancy.config import StateConfig
|
|
|
|
|
|
def _threshold_bucket(value: float, thresholds: List[float]) -> int:
|
|
"""thresholds 경계로 구간 인덱스 산출. value <= thresholds[i] 이면 i, 모두 초과면 마지막 구간.
|
|
|
|
경계 포함 규칙: value <= threshold → 해당 구간(하한). (예: th=[10,30] → ≤10:0, ≤30:1, else:2)
|
|
"""
|
|
for i, th in enumerate(thresholds):
|
|
if value <= th:
|
|
return i
|
|
return len(thresholds) # 마지막 구간 (= dim-1, dim = len(thresholds)+1)
|
|
|
|
|
|
def _acceptance_bucket(ratio: float, thresholds: List[float]) -> int:
|
|
"""수용률 구간: < thresholds[0] → 0, <= thresholds[1] → 1, ... else 마지막.
|
|
|
|
하한은 strict-less, 이후 경계는 inclusive (low 는 미만, mid 이상은 이하).
|
|
"""
|
|
if ratio < thresholds[0]:
|
|
return 0
|
|
for i in range(1, len(thresholds)):
|
|
if ratio <= thresholds[i]:
|
|
return i
|
|
return len(thresholds)
|
|
|
|
|
|
def _partner_bucket(count: int) -> int:
|
|
"""파트너 수 → 인덱스. 규약: single=0, multiple=1, none=2."""
|
|
if count <= 0:
|
|
return 2 # none
|
|
if count == 1:
|
|
return 0 # single
|
|
return 1 # multiple
|
|
|
|
|
|
def _price_zone_bucket(input_price: float, anchor_price: float, target_price: float) -> int:
|
|
"""입력가격 구간 (KT 구매자 관점, anchor=협력사 기준가 ≥ target=KT 목표 매입가).
|
|
|
|
협력사 제시가가 앵커가 이하면 우선협상 가능 구간(0), 초과면 추가 협상 구간(1).
|
|
"""
|
|
if anchor_price <= 0 or target_price <= 0:
|
|
raise ValueError("anchor_price/target_price must be positive")
|
|
if input_price <= anchor_price:
|
|
return 0 # at_or_below_anchor (우선협상 가능)
|
|
return 1 # above_anchor (협상 지속)
|
|
|
|
|
|
def state_dims(cfg: StateConfig) -> List[int]:
|
|
"""각 차원의 크기. config 의 weights/code_map 길이로 결정."""
|
|
return [
|
|
len(cfg.revenue.weights),
|
|
len(cfg.distribution.weights),
|
|
len(cfg.partner.weights),
|
|
len(cfg.acceptance.weights),
|
|
len(cfg.price_zone.weights),
|
|
]
|
|
|
|
|
|
def build_state(snapshot: NegotiationSnapshot, cfg: StateConfig) -> State:
|
|
"""snapshot 을 config 기준으로 이산 State 로 변환."""
|
|
revenue_idx = _threshold_bucket(snapshot.revenue_amount, cfg.revenue.thresholds)
|
|
|
|
code = (snapshot.distribution_code or "").strip()
|
|
if code not in cfg.distribution.code_map:
|
|
raise ValueError(f"unknown distribution code: {snapshot.distribution_code!r} (code_map keys={list(cfg.distribution.code_map)})")
|
|
distribution_idx = cfg.distribution.code_map[code]
|
|
|
|
partner_idx = _partner_bucket(snapshot.partner_count)
|
|
acceptance_idx = _acceptance_bucket(snapshot.acceptance_ratio, cfg.acceptance.thresholds)
|
|
price_zone_idx = _price_zone_bucket(snapshot.input_price, snapshot.anchor_price, snapshot.target_price)
|
|
|
|
return State(revenue_idx, distribution_idx, partner_idx, acceptance_idx, price_zone_idx)
|
|
|
|
|
|
def state_index(snapshot: NegotiationSnapshot, cfg: StateConfig) -> int:
|
|
"""snapshot → 단일 정수 state_index (mixed-radix)."""
|
|
st = build_state(snapshot, cfg)
|
|
return encode_index(list(st.to_tuple()), state_dims(cfg))
|