# 「LLM 提議、模擬器驗證」：讓大型語言模型（Claude）根據過去的實驗結果提議下一組 PID 增益，
# 由程式檢查範圍、做閉迴路模擬、算出成本，再把結果回饋給它。受控體與成本同 pid_tuning.py（1/(s+1)³、ITAE）。
# 需要：pip install anthropic，並設定 ANTHROPIC_API_KEY。每一輪都會呼叫一次 API（會產生費用），本站沒有執行這個範例。
import json
import math

import anthropic
import numpy as np
from scipy.linalg import expm

# ---------- 驗證用的模擬器（同 pid_tuning.py）----------
DT, T_END, U_MAX, TF = 0.01, 20.0, 10.0, 0.05
A = np.array([[0, 1, 0], [0, 0, 1], [-1, -3, -3]], float)
M = expm(np.block([[A, np.array([[0], [0], [1]])], [np.zeros((1, 4))]]) * DT)
AD, BD = M[:3, :3], M[:3, 3]

def pid_sim(kp, ki, kd):
    n = int(round(T_END / DT)); x, I, D, y_old = np.zeros(3), 0.0, 0.0, 0.0
    ys = np.empty(n + 1)
    for k in range(n + 1):
        y = x[0]; e = 1.0 - y
        D = (TF * D - (y - y_old)) / (TF + DT)
        raw = kp * e + ki * (I + e * DT) + kd * D
        u = min(max(raw, -U_MAX), U_MAX)
        if u == raw:
            I += e * DT
        ys[k], y_old = y, y
        x = AD @ x + BD * u
    return np.arange(n + 1) * DT, ys

def evaluate(kp, ki, kd):
    t, y = pid_sim(kp, ki, kd)
    out = np.where(np.abs(y - 1) > 0.02)[0]
    return {"ITAE": round(float(np.sum(t * np.abs(1 - y)) * DT), 4), "overshoot_pct": round(max(0.0, (y.max() - 1) * 100), 2),
            "settling_s": round(float(t[out[-1] + 1]) if len(out) and out[-1] + 1 < len(t) else (math.inf if len(out) else 0.0), 2)}

BOUNDS = {"Kp": (0.1, 31.6), "Ki": (0.01, 10.0), "Kd": (0.01, 10.0)}  # 硬限制寫在程式裡，不交給 LLM

# ---------- LLM 提議 ----------
client = anthropic.Anthropic()
SCHEMA = {"type": "object", "additionalProperties": False, "required": ["Kp", "Ki", "Kd", "reason"],
          "properties": {"Kp": {"type": "number"}, "Ki": {"type": "number"}, "Kd": {"type": "number"}, "reason": {"type": "string"}}}
SYSTEM = ("你在協助調整一個 PID 控制器。受控體 G(s) = 1/(s+1)^3，單位步階輸入，取樣 0.01 s，控制量限制 ±10，"
          "微分作用在量測值。目標：ITAE 最小，且超越量不超過 5%。每次只提議一組增益，"
          f"範圍 Kp {BOUNDS['Kp']}、Ki {BOUNDS['Ki']}、Kd {BOUNDS['Kd']}。根據過去的實驗結果決定下一組，用一句話說明理由。")

def propose(history):
    msg = "過去的實驗（增益 → 結果）：\n" + "\n".join(json.dumps(h, ensure_ascii=False) for h in history) + "\n請提議下一組。"
    response = client.beta.messages.create(
        model="claude-opus-5-5",
        max_tokens=16000,
        betas=["server-side-fallback-2026-07-01"],
        fallbacks="default",                                            # 被安全分類器拒絕時，由伺服器改用建議的模型重跑
        system=SYSTEM,
        messages=[{"role": "user", "content": msg}],
        output_config={"effort": "medium", "format": {"type": "json_schema", "schema": SCHEMA}},
    )
    if response.stop_reason == "refusal":
        raise RuntimeError("模型拒絕回答")
    text = next(b.text for b in response.content if b.type == "text")
    return json.loads(text)

def verify(p):
    """驗證關卡：數值要有限、在範圍內，否則不做實驗"""
    for k, (lo, hi) in BOUNDS.items():
        v = p.get(k)
        if not isinstance(v, (int, float)) or not math.isfinite(v) or not lo <= v <= hi:
            return f"{k} = {v} 超出範圍 {lo}～{hi}"
    return None

Tu = 2 * math.pi / math.sqrt(3)
zn = {"Kp": 4.8, "Ki": round(4.8 / (Tu / 2), 3), "Kd": round(4.8 * Tu / 8, 3)}
history = [{**zn, **evaluate(zn["Kp"], zn["Ki"], zn["Kd"]), "note": "Ziegler–Nichols 起點"}]
for i in range(15):
    p = propose(history)
    err = verify(p)
    if err:
        history.append({**{k: p.get(k) for k in BOUNDS}, "rejected": err})
        print(f"第 {i + 1} 次：拒絕執行（{err}）")
        continue
    r = evaluate(p["Kp"], p["Ki"], p["Kd"])
    history.append({k: p[k] for k in BOUNDS} | r)
    print(f"第 {i + 1} 次：Kp={p['Kp']:.3g}, Ki={p['Ki']:.3g}, Kd={p['Kd']:.3g} → {r}　理由：{p['reason']}")
ok = [h for h in history if "ITAE" in h and h["overshoot_pct"] <= 5]
best = min(ok, key=lambda h: h["ITAE"]) if ok else None
print("符合超越量 ≤ 5% 的最佳結果：", best)
