AQIT 0.1.0
Loading...
Searching...
No Matches
confidence_analysis_check.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""`aquin check confidence --check` — save JSON + diagram to the current working directory."""
3
4from __future__ import annotations
5
6import json
7from datetime import datetime, timezone
8from pathlib import Path
9from typing import Any
10
11_JSON_NAME = "confidence-analysis-check.json"
12_PNG_NAME = "confidence-analysis-check.png"
13
14
15def _normalize_confidence_analysis_result(result: dict[str, Any]) -> dict[str, Any]:
16 if not isinstance(result, dict):
17 return {"error": "Invalid confidence-analysis result"}
18
19 data = result
20 card = result.get("card")
21 if isinstance(card, dict) and isinstance(card.get("data"), dict):
22 card_data = card["data"]
23 data = {
24 "schema_version": result.get("schema_version", 1),
25 "model_id": card_data.get("modelId") or result.get("model_id"),
26 "mode": card_data.get("mode") or result.get("mode"),
27 "n_probes": card_data.get("nProbes") or result.get("n_probes"),
28 "threshold": card_data.get("threshold") or result.get("threshold"),
29 "mean_confidence": card_data.get("meanConfidence") or result.get("mean_confidence"),
30 "aggregate_ece_proxy": card_data.get("aggregateEceProxy") or result.get("aggregate_ece_proxy"),
31 "low_confidence_count": card_data.get("lowConfidenceCount") or result.get("low_confidence_count"),
32 "join_sae": card_data.get("joinSae") if card_data.get("joinSae") is not None else result.get("join_sae"),
33 "sae_layer": card_data.get("saeLayer") or result.get("sae_layer"),
34 "probes": card_data.get("probes") or result.get("probes", []),
35 "stressor_summary": card_data.get("stressorSummary") or result.get("stressor_summary", []),
36 "heatmap": card_data.get("heatmap") or result.get("heatmap", {}),
37 "saved_to": card_data.get("savedTo") or result.get("saved_to"),
38 }
39 elif isinstance(result.get("content"), dict):
40 data = result["content"]
41
42 if result.get("error"):
43 data = {**data, "error": result["error"]}
44 return data
45
46
48 result: dict[str, Any],
49 *,
50 tool_name: str | None,
51 cwd: str | Path,
52) -> tuple[Path, Path]:
53 cwd = Path(cwd)
54 json_path = cwd / _JSON_NAME
55 png_path = cwd / _PNG_NAME
56
58 payload = {
59 "saved_at": datetime.now(timezone.utc).isoformat(),
60 "tool": tool_name,
61 **flat,
62 }
63 json_path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
65 return json_path, png_path
66
67
68def _plot_confidence_analysis_check(result: dict[str, Any], png_path: Path) -> None:
69 import matplotlib
70
71 matplotlib.use("Agg")
72 import matplotlib.pyplot as plt
73
74 if result.get("error"):
75 fig, ax = plt.subplots(figsize=(6, 2), facecolor="#0f1117")
76 ax.axis("off")
77 ax.text(0.5, 0.5, f"Error: {result['error']}", ha="center", va="center", wrap=True, color="#e5e7eb")
78 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
79 plt.close(fig)
80 return
81
82 model = str(result.get("model_id") or "")
83 mode = str(result.get("mode") or "")
84 n_probes = int(result.get("n_probes") or 0)
85 threshold = float(result.get("threshold") or 0.4)
86 mean_conf = float(result.get("mean_confidence") or 0)
87 low_count = int(result.get("low_confidence_count") or 0)
88 join_sae = bool(result.get("join_sae"))
89 sae_layer = result.get("sae_layer")
90
91 title = f"Confidence analysis — {model} · {mode} · {n_probes} probes"
92 subtitle = f"mean conf {mean_conf:.3f} · threshold {threshold:.2f} · low-conf {low_count}"
93 if join_sae and sae_layer is not None:
94 subtitle += f" · SAE L{sae_layer}"
95
96 fig = plt.figure(figsize=(12, 7.5), facecolor="#0f1117")
97 gs = fig.add_gridspec(2, 2, height_ratios=[1, 1.2], hspace=0.38, wspace=0.28)
98 ax_probes = fig.add_subplot(gs[0, 0])
99 ax_stress = fig.add_subplot(gs[0, 1])
100 ax_heat = fig.add_subplot(gs[1, :])
101
102 fig.suptitle(title, color="#e5e7eb", fontsize=11, y=0.98)
103 fig.text(0.5, 0.93, subtitle, ha="center", fontsize=8, color="#9ca3af")
104
105 _plot_probe_confidence(ax_probes, result.get("probes") or [], threshold)
106 _plot_stressor_summary(ax_stress, result.get("stressor_summary") or [])
107 _plot_metric_heatmap(ax_heat, result.get("heatmap") or {})
108
109 for ax in (ax_probes, ax_stress, ax_heat):
110 ax.set_facecolor("#0f1117")
111 for spine in ax.spines.values():
112 spine.set_color("#374151")
113
114 fig.savefig(png_path, dpi=140, bbox_inches="tight", facecolor="#0f1117")
115 plt.close(fig)
116
117
118def _plot_probe_confidence(ax: Any, probes: list[dict[str, Any]], threshold: float) -> None:
119 ax.set_title("Per-probe confidence", color="#e5e7eb", fontsize=10)
120
121 if not probes:
122 ax.axis("off")
123 ax.text(0.5, 0.5, "No probe data", ha="center", va="center", color="#9ca3af")
124 return
125
126 rows = sorted(probes, key=lambda p: float(p.get("mean_confidence") or 0))[:16]
127 labels = [_probe_label(p) for p in rows]
128 vals = [float(p.get("mean_confidence") or 0) for p in rows]
129 colors = ["#f87171" if v < threshold else "#34d399" if v >= 0.7 else "#6366f1" for v in vals]
130
131 y_pos = list(range(len(rows)))
132 ax.barh(y_pos, vals, color=colors, height=0.65, alpha=0.9)
133 ax.axvline(threshold, color="#fbbf24", linestyle="--", linewidth=0.8, alpha=0.7)
134 ax.set_yticks(y_pos)
135 ax.set_yticklabels(labels, fontsize=7, color="#d1d5db")
136 ax.invert_yaxis()
137 ax.set_xlim(0, 1.05)
138 ax.set_xlabel("Mean confidence", color="#9ca3af", fontsize=8)
139 ax.tick_params(axis="x", colors="#6b7280", labelsize=8)
140 ax.grid(axis="x", alpha=0.2, color="#4b5563")
141
142
143def _plot_stressor_summary(ax: Any, summary: list[dict[str, Any]]) -> None:
144 ax.set_title("By stressor", color="#e5e7eb", fontsize=10)
145
146 if not summary:
147 ax.axis("off")
148 ax.text(0.5, 0.5, "No stressor summary", ha="center", va="center", color="#9ca3af")
149 return
150
151 labels = [str(s.get("stressor") or "?") for s in summary]
152 vals = [float(s.get("mean_confidence") or 0) for s in summary]
153 y_pos = list(range(len(summary)))
154 ax.barh(y_pos, vals, color="#38bdf8", height=0.65, alpha=0.9)
155 ax.set_yticks(y_pos)
156 ax.set_yticklabels(labels, fontsize=8, color="#d1d5db")
157 ax.invert_yaxis()
158 ax.set_xlim(0, 1.05)
159 ax.set_xlabel("Mean confidence", color="#9ca3af", fontsize=8)
160 ax.tick_params(axis="x", colors="#6b7280", labelsize=8)
161 ax.grid(axis="x", alpha=0.2, color="#4b5563")
162 for i, s in enumerate(summary):
163 low = int(s.get("low_confidence_count") or 0)
164 ax.text(min(vals[i] + 0.02, 0.92), i, f"{vals[i]:.3f} ({low} low)", va="center", fontsize=7, color="#9ca3af")
165
166
167def _plot_metric_heatmap(ax: Any, heatmap: dict[str, Any]) -> None:
168 rows = heatmap.get("rows") or []
169 cols = heatmap.get("cols") or []
170 values = heatmap.get("values") or []
172 ax.set_title("Stressor × metrics", color="#e5e7eb", fontsize=10, pad=10)
173
174 if not rows or not cols or not values:
175 ax.axis("off")
176 ax.text(0.5, 0.5, "No heatmap data", ha="center", va="center", color="#9ca3af")
177 return
178
179 import numpy as np
180
181 data = np.array(values, dtype=float)
182 if data.ndim != 2:
183 ax.axis("off")
184 ax.text(0.5, 0.5, "Invalid heatmap shape", ha="center", va="center", color="#9ca3af")
185 return
186
187 im = ax.imshow(data, aspect="auto", cmap="plasma", origin="upper")
188 ax.set_xticks(range(len(cols)))
189 ax.set_xticklabels(cols, fontsize=8, color="#d1d5db", rotation=30, ha="right")
190 ax.set_yticks(range(len(rows)))
191 ax.set_yticklabels(rows, fontsize=8, color="#d1d5db")
192 ax.tick_params(axis="x", colors="#6b7280")
193 ax.tick_params(axis="y", colors="#6b7280")
194 cbar = ax.figure.colorbar(im, ax=ax, fraction=0.025, pad=0.02)
195 cbar.ax.tick_params(colors="#9ca3af", labelsize=7)
196
197
198def _probe_label(probe: dict[str, Any]) -> str:
199 pid = str(probe.get("id") or "")
200 stressor = probe.get("stressor")
201 if pid and stressor:
202 return f"{pid} ({stressor})"[:24]
203 if pid:
204 return pid[:24]
205 if stressor:
206 return str(stressor)[:24]
207 return _trunc(str(probe.get("text") or ""), 22)
208
209
210def _trunc(text: str, limit: int) -> str:
211 t = text.strip()
212 if len(t) <= limit:
213 return t or "·"
214 return t[: limit - 1] + "…"
None _plot_stressor_summary(Any ax, list[dict[str, Any]] summary)
None _plot_metric_heatmap(Any ax, dict[str, Any] heatmap)
None _plot_confidence_analysis_check(dict[str, Any] result, Path png_path)
None _plot_probe_confidence(Any ax, list[dict[str, Any]] probes, float threshold)
tuple[Path, Path] write_confidence_analysis_check(dict[str, Any] result, *, str|None tool_name, str|Path cwd)
dict[str, Any] _normalize_confidence_analysis_result(dict[str, Any] result)