"""models/runner.py 의 synthetic_bo() 를 '그대로' 재실행해 실제 수치로 그림을 만든다.

로직(시드, 커널, 표본 수, 범위, 실현가능 경계)은 원본과 100% 동일하게 유지한다.
여기서는 시각화만 추가한다 (GP 사후평균 히트맵, 후보 산점도, 실현가능 경계,
선택된 후보). 합성 전용(synthetic-only) 데모이며 AC28 권고가 아님을 그림에
그대로 명시한다.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.font_manager as fm
fm.fontManager.addfont("/System/Library/Fonts/AppleSDGothicNeo.ttc")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import Matern, WhiteKernel
from scipy.stats import norm

# ── models/runner.py::synthetic_bo() 와 동일한 로직 (수정 없음) ──────────────
rng = np.random.default_rng(17)
x = rng.uniform([0, .2], [1, .8], (28, 2))
y = (x[:, 0] - .62) ** 2 + 1.4 * (x[:, 1] - .42) ** 2
gp = GaussianProcessRegressor(Matern(nu=2.5) + WhiteKernel(.001), normalize_y=True, random_state=17).fit(x, y)

cand = rng.uniform([0, .2], [1, .8], (2000, 2))
mean, sd = gp.predict(cand, return_std=True)
z = (y.min() - mean) / (sd + 1e-9)
ei = (y.min() - mean) * norm.cdf(z) + sd * norm.pdf(z)
feasible = (cand[:, 0] >= .15) & (cand[:, 0] <= .85) & (cand[:, 1] >= .25) & (cand[:, 1] <= .75)
pick = int(np.argmax(np.where(feasible, ei, -np.inf)))
picked_point = cand[pick]
picked_ei = float(ei[pick])
# ── 원본 로직 끝 ──────────────────────────────────────────────────────────

print(f"실제 실행 결과 — picked={picked_point.round(5).tolist()}  EI={picked_ei:.5f}  "
      f"관측 {len(x)}개  후보 {len(cand)}개  실현가능 {int(feasible.sum())}개")

# 배경 사후평균 히트맵용 격자 (후보 산점도와 같은 GP, 같은 범위)
gx = np.linspace(0, 1, 220)
gy = np.linspace(0.2, 0.8, 160)
GX, GY = np.meshgrid(gx, gy)
grid = np.column_stack([GX.ravel(), GY.ravel()])
grid_mean, grid_sd = gp.predict(grid, return_std=True)
grid_mean = grid_mean.reshape(GX.shape)
grid_sd = grid_sd.reshape(GX.shape)

INK = "#24292F"
MUTED = "#606B7A"
BORDER = "#C6CCD3"
ACCENT_OBS = "#2A5C8A"    # 설계 · 관측
ACCENT_PICK = "#C2641F"   # 선택된 후보
ACCENT_WARN = "#B3261E"   # 실현가능 경계

plt.rcParams.update({
    "font.size": 13, "text.color": INK, "axes.labelcolor": INK,
    "xtick.color": MUTED, "ytick.color": MUTED, "axes.edgecolor": BORDER,
    "font.family": "Apple SD Gothic Neo",
    "axes.unicode_minus": False,
})

fig, axes = plt.subplots(1, 2, figsize=(13.6, 5.7), dpi=200)

# ── 좌: GP 사후평균 + 관측점 + 실현가능 경계 ──────────────────────────────
ax = axes[0]
cf = ax.contourf(GX, GY, grid_mean, levels=22, cmap="viridis", alpha=0.92)
ax.contour(GX, GY, grid_mean, levels=8, colors="white", linewidths=0.4, alpha=0.5)
ax.scatter(x[:, 0], x[:, 1], s=60, color="white", edgecolor=ACCENT_OBS, linewidth=1.6,
           zorder=5, label=f"관측 {len(x)}개 (사전 실행)")
rect = Rectangle((.15, .25), .70, .50, fill=False, edgecolor=ACCENT_WARN, linewidth=2.2,
                  linestyle=(0, (5, 3)), zorder=6, label="하드 실현가능 경계")
ax.add_patch(rect)
ax.scatter(*picked_point, marker="*", s=480, color=ACCENT_PICK, edgecolor="white",
           linewidth=1.3, zorder=7, label=f"선택된 후보 (최대 EI = {picked_ei:.4f})")
ax.set_xlim(0, 1); ax.set_ylim(.2, .8)
ax.set_xlabel("파라미터 p0"); ax.set_ylabel("파라미터 p1")
ax.set_title("GP 사후평균 (Matern 5/2 + White)", fontsize=13, pad=10, color=INK)
cb = fig.colorbar(cf, ax=ax, fraction=0.046, pad=0.03)
cb.set_label("사후평균 목적값 (낮을수록 좋음)", color=MUTED, fontsize=10)
cb.ax.tick_params(colors=MUTED, labelsize=8)
leg = ax.legend(loc="upper right", fontsize=8.5, framealpha=0.95, edgecolor=BORDER)

# ── 우: 2000개 후보 산점 + EI 색상 + 실현가능/선택 강조 ───────────────────
ax2 = axes[1]
sc = ax2.scatter(cand[~feasible, 0], cand[~feasible, 1], s=10, c="#D9DEE4",
                  label=f"실현불가 후보 ({int((~feasible).sum())}개)", zorder=2)
sc2 = ax2.scatter(cand[feasible, 0], cand[feasible, 1], s=16, c=ei[feasible],
                   cmap="magma", zorder=3, label=f"실현가능 후보 ({int(feasible.sum())}개)")
rect2 = Rectangle((.15, .25), .70, .50, fill=False, edgecolor=ACCENT_WARN, linewidth=2.0,
                   linestyle=(0, (5, 3)), zorder=4)
ax2.add_patch(rect2)
ax2.scatter(*picked_point, marker="*", s=480, color=ACCENT_PICK, edgecolor="white",
            linewidth=1.3, zorder=6, label="선택된 후보 (max EI)")
ax2.set_xlim(0, 1); ax2.set_ylim(.2, .8)
ax2.set_xlabel("파라미터 p0"); ax2.set_ylabel("파라미터 p1")
ax2.set_title(f"후보 {len(cand)}개 · Expected Improvement", fontsize=13, pad=10, color=INK)
cb2 = fig.colorbar(sc2, ax=ax2, fraction=0.046, pad=0.03)
cb2.set_label("Expected Improvement", color=MUTED, fontsize=10)
cb2.ax.tick_params(colors=MUTED, labelsize=8)
ax2.legend(loc="upper right", fontsize=8.5, framealpha=0.95, edgecolor=BORDER)

fig.suptitle(
    "합성 전용 데모 — sklearn GaussianProcessRegressor + Expected Improvement  ·  models/runner.py: synthetic_bo()",
    fontsize=12.5, color=ACCENT_WARN, y=1.015, fontweight="bold",
)
fig.text(0.5, -0.02,
         "AC28 · ME1a 어떤 실측 데이터도 쓰지 않음 — 2차원 합성 목적함수(y = (p0-0.62)^2 + 1.4(p1-0.42)^2)에 대한 API/알고리즘 시연. "
         "공정조건 권고·정확도·실현가능성 추론이 아님.",
         ha="center", fontsize=9.5, color=MUTED)

fig.tight_layout(rect=[0, 0.01, 1, 0.96])
out = "/Users/bsk/Downloads/몰텍스/reports/261014/ppt/assets/bo_synthetic_demo.png"
fig.savefig(out, dpi=200, bbox_inches="tight", facecolor="white")
print("저장:", out)
