Spaces:
Running on Zero
Running on Zero
| """Visual components for the console. | |
| WHY THESE ARE PLOTS AND NOT TEXT. "Decision: ACCEPT" as a line of text discards | |
| the information that produced it. An operator needs to see *how close* the call | |
| was -- whether the confidence interval crosses a grade boundary, whether the | |
| chosen action was cheapest by a wide margin or a hair, and how much of the | |
| escape budget the decision consumes. Each of those is a spatial question, and | |
| answering it in prose asks the reader to reconstruct a picture from numbers. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any, Sequence | |
| import matplotlib | |
| import numpy as np | |
| from app import theme | |
| matplotlib.use("Agg", force=True) | |
| import matplotlib.pyplot as plt # noqa: E402 | |
| from matplotlib.patches import Rectangle # noqa: E402 | |
| plt.rcParams.update(theme.matplotlib_style()) | |
| def _close(fig): | |
| """Figures accumulate in matplotlib's registry until closed.""" | |
| return fig | |
| def decision_plot( | |
| predicted: float, lower: float, upper: float, | |
| boundaries: dict[str, float], warranty: int, assigned: str, | |
| ) -> plt.Figure: | |
| """Predicted life against the grade boundaries, with the conformal band drawn. | |
| The question this answers at a glance: does the interval CROSS a boundary? | |
| A prediction of 600 cycles with a band of 550-660 is a very different | |
| decision from 600 with a band of 400-900, and a point estimate hides that. | |
| """ | |
| fig, ax = plt.subplots(figsize=(9, 2.9)) | |
| thresholds = sorted(v for v in boundaries.values() if v > 0) | |
| upper_limit = max(upper * 1.25, max(thresholds) * 1.3) | |
| lower_limit = min(lower * 0.75, 100) | |
| # Grade bands as background shading, ordered least to most demanding. | |
| ordered = sorted(boundaries.items(), key=lambda kv: kv[1]) | |
| for i, (grade, floor) in enumerate(ordered): | |
| ceiling = ordered[i + 1][1] if i + 1 < len(ordered) else upper_limit | |
| ax.add_patch(Rectangle( | |
| (floor, 0), max(ceiling - floor, 1), 1, | |
| facecolor=theme.DECISION_COLOURS.get(grade, theme.GRID), | |
| alpha=0.10, edgecolor="none", zorder=0, | |
| )) | |
| centre = floor + (min(ceiling, upper_limit) - floor) / 2 | |
| if lower_limit < centre < upper_limit: | |
| ax.text(centre, 0.86, f"GRADE {grade}", ha="center", va="center", | |
| fontsize=8, color=theme.TEXT_MUTED, family="monospace") | |
| # The conformal interval, drawn as a band rather than error bars. | |
| ax.add_patch(Rectangle( | |
| (lower, 0.36), max(upper - lower, 1), 0.28, | |
| facecolor=theme.DATA_BAND, alpha=0.42, edgecolor=theme.DATA, linewidth=1.2, zorder=3, | |
| )) | |
| ax.plot([predicted], [0.5], "o", color=theme.DATA, markersize=11, | |
| markeredgecolor=theme.BG, markeredgewidth=1.6, zorder=5) | |
| for value, label, colour in ((warranty, "warranty target", theme.TEXT),): | |
| ax.axvline(value, color=colour, linestyle="--", linewidth=1.4, zorder=4) | |
| ax.text(value, 1.06, label, ha="center", fontsize=7.5, | |
| color=theme.TEXT_MUTED, family="monospace") | |
| for grade, floor in boundaries.items(): | |
| if floor > 0: | |
| ax.axvline(floor, color=theme.BORDER_STRONG, linewidth=0.9, zorder=2) | |
| crosses = any(lower < t < upper for t in thresholds) | |
| caption = ("interval CROSSES a grade boundary -- the decision is marginal" | |
| if crosses else "interval sits inside one grade -- the decision is clear") | |
| ax.text(0.5, -0.30, caption, transform=ax.transAxes, ha="center", | |
| fontsize=8, color=theme.CONTINUE if crosses else theme.TEXT_MUTED, | |
| family="monospace") | |
| ax.set_xlim(lower_limit, upper_limit) | |
| ax.set_ylim(0, 1.14) | |
| ax.set_yticks([]) | |
| ax.set_xscale("log") | |
| ax.set_xlabel("cycle life (log scale)") | |
| ax.set_title(f"Predicted cycle life and 90% conformal interval | assigned GRADE {assigned}", | |
| loc="left", family="monospace") | |
| ax.grid(axis="y", alpha=0) | |
| fig.tight_layout() | |
| return _close(fig) | |
| def cost_plot(action_costs: dict[str, float], chosen: str) -> plt.Figure: | |
| """Expected cost of every action, so the choice is visibly the cheapest. | |
| Showing only the chosen action asks the reader to trust the arithmetic. A | |
| comparison lets them see whether it won by a wide margin or a hair -- which | |
| is exactly what determines whether a supervisor should look closer. | |
| """ | |
| actions = list(action_costs) | |
| values = [action_costs[a] for a in actions] | |
| colours = [theme.DECISION_COLOURS.get(a, theme.DATA) if a == chosen | |
| else theme.BORDER_STRONG for a in actions] | |
| fig, ax = plt.subplots(figsize=(4.6, 2.9)) | |
| bars = ax.barh(range(len(actions)), values, color=colours, height=0.6) | |
| ax.set_yticks(range(len(actions))) | |
| ax.set_yticklabels([f"Grade {a}" if len(a) == 1 else a for a in actions], | |
| family="monospace", fontsize=9) | |
| ax.invert_yaxis() | |
| span = max(values) if max(values) > 0 else 1.0 | |
| for bar, value, action in zip(bars, values, actions): | |
| ax.text(value + span * 0.03, bar.get_y() + bar.get_height() / 2, | |
| f"{value:.2f}", va="center", fontsize=9, family="monospace", | |
| color=theme.TEXT if action == chosen else theme.TEXT_MUTED, | |
| fontweight="bold" if action == chosen else "normal") | |
| ax.set_xlim(0, span * 1.25) | |
| ax.set_xlabel("expected cost (relative units)") | |
| ax.set_title("Cost of each action", loc="left", family="monospace") | |
| ax.grid(axis="y", alpha=0) | |
| fig.tight_layout() | |
| return _close(fig) | |
| def risk_gauge(escape_probability: float, alpha: float, guaranteed: bool = True) -> plt.Figure: | |
| """Escape risk against the chosen bound, as a bar rather than a bare number. | |
| A percentage tells the reader the value; a gauge tells them whether it is | |
| within budget, which is the actual question. | |
| WHY `guaranteed` EXISTS. The bound is a conformal statement, and conformal | |
| validity requires exchangeability. Under campaign shift that assumption | |
| fails, so the number this gauge draws is no longer a guarantee -- and Phase | |
| 10 measured exactly the trap: coverage collapsed to 42.5% while intervals | |
| got NARROWER, meaning the reassuring reading is produced by the same | |
| conditions that invalidate it. Rendering a green "within budget" underneath | |
| a campaign-shift alarm would commit, in the interface, the precise error | |
| this project was built to expose. | |
| """ | |
| fig, ax = plt.subplots(figsize=(4.6, 1.7)) | |
| ax.add_patch(Rectangle((0, 0.28), 1, 0.44, facecolor=theme.PANEL_RAISED, | |
| edgecolor=theme.BORDER, linewidth=1)) | |
| fraction = min(escape_probability / max(alpha, 1e-9), 1.35) | |
| within = escape_probability <= alpha | |
| if not guaranteed: | |
| # Neutral hatching, never green: the value is displayed but withdrawn. | |
| ax.add_patch(Rectangle((0, 0.28), min(fraction, 1.0), 0.44, | |
| facecolor=theme.TEXT_DIM, alpha=0.35, | |
| hatch="///", edgecolor=theme.ALARM, linewidth=1.0)) | |
| else: | |
| ax.add_patch(Rectangle((0, 0.28), min(fraction, 1.0), 0.44, | |
| facecolor=theme.ACCEPT if within else theme.REJECT, | |
| alpha=0.85, edgecolor="none")) | |
| ax.axvline(1.0, color=theme.TEXT, linestyle="--", linewidth=1.4) | |
| ax.text(1.0, 0.80, f"bound α={alpha:.2f}", ha="center", fontsize=8, | |
| color=theme.TEXT_MUTED, family="monospace") | |
| if not guaranteed: | |
| ax.text(0.02, 0.06, | |
| f"P = {escape_probability:.3f} — BOUND VOID under campaign shift", | |
| fontsize=8.5, family="monospace", color=theme.ALARM) | |
| ax.set_title("Escape risk — guarantee suspended", loc="left", | |
| family="monospace", color=theme.ALARM) | |
| else: | |
| ax.text(0.02, 0.06, | |
| f"P(cell below warranty) = {escape_probability:.3f}" | |
| f"{' — within budget' if within else ' — EXCEEDS BOUND'}", | |
| fontsize=8.5, family="monospace", | |
| color=theme.ACCEPT if within else theme.REJECT) | |
| ax.set_title("Escape risk vs the guarantee", loc="left", family="monospace") | |
| ax.set_xlim(0, 1.4); ax.set_ylim(0, 1) | |
| ax.set_xticks([]); ax.set_yticks([]) | |
| ax.grid(alpha=0) | |
| for spine in ax.spines.values(): | |
| spine.set_visible(False) | |
| fig.tight_layout() | |
| return _close(fig) | |
| def budget_advisor_plot( | |
| budgets: Sequence[int], widths: Sequence[float], costs: Sequence[float], | |
| selected: int, knee: int, | |
| ) -> plt.Figure: | |
| """Interval width and expected cost against budget, with the current pick marked. | |
| This is the plot that carries RQ1: the cost curve is FLAT, so the user can | |
| see for themselves that moving the slider from 100 to 5 cycles costs | |
| essentially nothing -- a conclusion far more convincing discovered than | |
| asserted. | |
| """ | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 3.4)) | |
| ax1.plot(budgets, widths, "o-", color=theme.DATA, linewidth=2, markersize=7) | |
| ax1.axvline(selected, color=theme.DATA_ALT, linestyle="--", linewidth=1.6) | |
| index = list(budgets).index(selected) if selected in budgets else 0 | |
| ax1.plot([selected], [widths[index]], "o", color=theme.DATA_ALT, markersize=12, | |
| markeredgecolor=theme.BG, markeredgewidth=1.6, zorder=5) | |
| ax1.set_xlabel("diagnostic budget (cycles observed)") | |
| ax1.set_ylabel("interval width (log10 cycle life)") | |
| ax1.set_title("More cycles buy a tighter statement", loc="left", family="monospace") | |
| ax2.plot(budgets, costs, "o-", color=theme.DATA, linewidth=2, markersize=7) | |
| ax2.axvline(knee, color=theme.ACCEPT, linestyle="--", linewidth=1.6) | |
| ax2.text(knee, max(costs), f" knee N={knee}", color=theme.ACCEPT, | |
| fontsize=8, family="monospace", va="top") | |
| ax2.axvline(selected, color=theme.DATA_ALT, linestyle="--", linewidth=1.6) | |
| span = max(costs) - min(costs) | |
| ax2.set_ylim(min(costs) - span * 2.2 - 0.05, max(costs) + span * 2.2 + 0.05) | |
| ax2.set_xlabel("diagnostic budget (cycles observed)") | |
| ax2.set_ylabel("expected cost per cell") | |
| ax2.set_title("...but cost is flat: the axis is deliberately widened", | |
| loc="left", family="monospace") | |
| fig.tight_layout() | |
| return _close(fig) | |
| def shap_waterfall(drivers: Sequence[dict[str, Any]], base: float, | |
| predicted: float) -> plt.Figure: | |
| """A readable waterfall, not a raw SHAP dump. | |
| The stock plot is dense and uses feature names. This one uses the | |
| plain-language description and orders by magnitude, because the reader is a | |
| process engineer rather than an analyst. | |
| """ | |
| labels, values = [], [] | |
| for driver in drivers: | |
| text = driver["description"] | |
| labels.append(text if len(text) <= 58 else text[:55] + "...") | |
| values.append(driver["shap"]) | |
| fig, ax = plt.subplots(figsize=(9.5, 0.55 * len(labels) + 1.6)) | |
| colours = [theme.ACCEPT if v > 0 else theme.REJECT for v in values] | |
| ax.barh(range(len(values)), values, color=colours, height=0.6, alpha=0.9) | |
| ax.set_yticks(range(len(labels))) | |
| ax.set_yticklabels(labels, fontsize=8.5) | |
| ax.invert_yaxis() | |
| ax.axvline(0, color=theme.BORDER_STRONG, linewidth=1.2) | |
| span = max(abs(min(values)), abs(max(values))) or 1.0 | |
| for i, value in enumerate(values): | |
| offset = span * 0.04 * (1 if value > 0 else -1) | |
| ax.text(value + offset, i, f"{value:+.4f}", va="center", | |
| ha="left" if value > 0 else "right", fontsize=8, family="monospace", | |
| color=theme.TEXT_MUTED) | |
| ax.set_xlim(-span * 1.5, span * 1.5) | |
| ax.set_xlabel("effect on predicted log10 cycle life") | |
| ax.set_title("What drove this decision " | |
| "(green raised the prediction, red lowered it)", | |
| loc="left", family="monospace") | |
| ax.grid(axis="y", alpha=0) | |
| fig.tight_layout() | |
| return _close(fig) | |
| def allocation_plot(policies: dict[str, float], escape: dict[str, float]) -> plt.Figure: | |
| """Greedy against the alternatives under a hard capacity constraint.""" | |
| names = list(policies) | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 3.4)) | |
| best = min(policies, key=policies.get) | |
| colours = [theme.DATA if n == best else theme.BORDER_STRONG for n in names] | |
| ax1.bar(range(len(names)), [policies[n] for n in names], color=colours) | |
| ax1.set_xticks(range(len(names))) | |
| ax1.set_xticklabels([n.replace("_", "\n") for n in names], fontsize=7.5, | |
| family="monospace") | |
| ax1.set_ylabel("cost per cell") | |
| ax1.set_title("Cost by allocation policy", loc="left", family="monospace") | |
| ax2.bar(range(len(names)), [escape.get(n, 0) for n in names], | |
| color=[theme.DATA if n == best else theme.BORDER_STRONG for n in names]) | |
| ax2.set_xticks(range(len(names))) | |
| ax2.set_xticklabels([n.replace("_", "\n") for n in names], fontsize=7.5, | |
| family="monospace") | |
| ax2.set_ylabel("escape rate") | |
| ax2.set_title("Escape rate by allocation policy", loc="left", family="monospace") | |
| fig.tight_layout() | |
| return _close(fig) | |