benjaminogbonna's picture
Upload app.py
354d757 verified
Raw History Blame Contribute Delete
26.1 kB
"""
Council AI — Theorem Explorer
------------------------------
Interactive companion to "Council AI: Toward Deliberative Artificial Intelligence"
(Michael Jidael, Udochukwu Nwokoma, Benjamin Ogbonna, et al., Nexenda, 2026).
Implements exactly two things, both taken directly from the paper's proofs:
Theorem 1 (Deliberation Convergence):
||H(t) - H*||_F <= gamma^t * ||H(0) - H*||_F
Theorem 2 (Multi-Expert Safety Bound):
P(catastrophic error) <= p * rho^(k-1) if p <= rho
P(catastrophic error) <= p^k if p > rho
No proprietary architecture names, expert-cluster names, or product names
appear anywhere in this app or its outputs. Consistent with the naming
constraints on Paper 1.
"""
import spaces # must come first, before numpy/matplotlib/gradio
@spaces.GPU(duration=1)
def _zerogpu_keepalive():
"""No-op. ZeroGPU refuses to start a Space with no @spaces.GPU function."""
return None
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import gradio as gr
# Links
PAPER_URL = "#"
CODE_URL = "#"
WEBSITE_URL = "https://nexendahq.com/"
# Plot palette. Theme-neutral on purpose: transparent background, mid-grey
# text/grid that reads on both the light and dark card, and the same
# accent colours in both themes. The light/dark switch is therefore pure
# client-side CSS (no server round trip, no theme state).
ORANGE, GREEN, PINK = "#E8651A", "#5E9E4E", "#D6456B"
INK = "#8F8D85"
def style_axes(fig, ax):
fig.patch.set_facecolor("none")
ax.set_facecolor("none")
for spine in ax.spines.values():
spine.set_color(INK)
spine.set_alpha(0.35)
spine.set_linewidth(0.8)
ax.tick_params(colors=INK, labelsize=8.5, length=0, pad=8)
ax.xaxis.label.set_color(INK)
ax.yaxis.label.set_color(INK)
ax.xaxis.label.set_size(8.5)
ax.yaxis.label.set_size(8.5)
ax.grid(color=INK, alpha=0.3, linewidth=0.7, linestyle=(0, (2, 3)))
def legend_html(items):
"""items: (label, colour, dashed?) -> plot header with title + legend."""
keys = "".join(
f'<span><i class="{"dash" if dashed else ""}" style="border-color:{c}"></i>{label}</span>'
for label, c, dashed in items
)
return f'<div class="plot-head"><div class="plot-title">{ICON_CHART}Plot</div><div class="legend">{keys}</div></div>'
def stats_html(pairs):
return '<div class="stats">' + "".join(f"<p>{k}: <b>{v}</b></p>" for k, v in pairs) + "</div>"
# Theorem 1 — Deliberation Convergence
def theorem1(gamma: float, rounds: int, d0: float, tau: float):
t = np.arange(0, rounds + 1)
dist = d0 * (gamma ** t)
agreement = np.clip(1 - dist / d0, 0, 1)
reached = np.argmax(agreement >= tau) if np.any(agreement >= tau) else None
consensus_round = int(reached) if reached is not None and agreement[reached] >= tau else None
fig, ax = plt.subplots(figsize=(8.6, 4.6), dpi=160)
style_axes(fig, ax)
ax.axhline(tau, color=PINK, linestyle=(0, (2, 2)), linewidth=1.4)
ax.plot(t, dist / d0, color=ORANGE, linewidth=1.8)
ax.plot(t, agreement, color=GREEN, linewidth=1.8)
ax.set_xlabel("Deliberation round t", labelpad=12)
ax.set_ylabel("Normalized value", labelpad=12)
ax.set_ylim(-0.02, 1.05)
fig.tight_layout()
plt.close(fig)
legend = legend_html([("Threshold τ", PINK, True),
("Disagreement Bound", ORANGE, False),
("Agreement", GREEN, False)])
stats = stats_html([
("Round consensus reached",
f"{consensus_round}/{rounds}" if consensus_round is not None else "not reached"),
("Residual disagreement at round T", f"{dist[-1]:.3g}"),
("Convergence rate", f"γ = {gamma:.2f} · lower γ = faster"),
])
return fig, legend, stats
DEFAULTS_T1 = (0.55, 15, 1.2, 0.75)
def reset_theorem1():
return (*DEFAULTS_T1, *theorem1(*DEFAULTS_T1))
# Theorem 2 — Multi-Expert Safety Bound
def safety_bound(p: float, rho: float, k: int) -> float:
if p <= rho:
return p * (rho ** (k - 1))
return p ** k
def theorem2(p: float, rho: float, n: int, k: int):
k = min(k, n)
regime = "p ≤ ρ · bound = p·ρ^(k−1)" if p <= rho else "p > ρ · tighter bound = p^k"
ks = np.arange(1, n + 1)
bounds = np.array([safety_bound(p, rho, kk) for kk in ks])
current_bound = safety_bound(p, rho, k)
fig, ax = plt.subplots(figsize=(8.6, 4.6), dpi=160)
style_axes(fig, ax)
ax.axhline(p, color=PINK, linestyle=(0, (2, 2)), linewidth=1.4)
ax.plot(ks, bounds, color=ORANGE, linewidth=1.6, marker="o", markersize=3)
ax.scatter([k], [current_bound], color=ORANGE, s=90, zorder=5, edgecolor="white", linewidth=1.5)
ax.set_yscale("log")
ax.xaxis.set_major_locator(MaxNLocator(integer=True, min_n_ticks=1))
ax.set_xlabel("Experts required for high-confidence agreement (k)", labelpad=12)
ax.set_ylabel("P(catastrophic error)", labelpad=12)
fig.tight_layout()
plt.close(fig)
reduction = p / current_bound if current_bound > 0 else float("inf")
verdict = "below baseline" if current_bound < p else "no improvement"
legend = legend_html([(f"Single-model rate p = {p:.4f}", PINK, True),
("Council bound as k increases", ORANGE, False)])
stats = stats_html([
("Regime", regime),
("Error rate (P)", f"{p:.4g} <em>baseline</em>"),
(f"Council bound at k={k}", f"{current_bound:.4g} <em>{verdict}</em>"),
("Reduction factor", f"{reduction:,.1f}× <em>vs. model</em>"),
])
return fig, legend, stats
DEFAULTS_T2 = (0.01, 0.30, 10, 5)
def reset_theorem2():
return (*DEFAULTS_T2, *theorem2(*DEFAULTS_T2))
# Static HTML
ICON_CHART = ('<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" '
'stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">'
'<path d="M2 2v11.5h12"/><path d="M4.5 10l3-3.5 2.5 2L13.5 4"/></svg>')
ICON_MOON = ('<svg class="i-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">'
'<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>')
ICON_SUN = ('<svg class="i-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/>'
'<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>')
ICON_ARROW = ('<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" '
'stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 9.5l7-7M4 2.5h5.5V8"/></svg>')
ICON_CHEV = ('<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" '
'stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 2.5L8 6l-3.5 3.5"/></svg>')
ICON_INFO = ('<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" '
'stroke-linecap="round"><circle cx="8" cy="8" r="6.3"/><path d="M8 7.2V11M8 5v.1"/></svg>')
ICON_CHEVL = ('<svg width="15" height="15" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.4" '
'stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 2.5L4 6l3.5 3.5"/></svg>')
ICON_CHEVR = ('<svg width="15" height="15" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.4" '
'stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 2.5L8 6l-3.5 3.5"/></svg>')
MOBILE_NAV_HTML = f"""
<div class="mobile-tab-nav">
<button type="button" class="mobile-nav-btn" id="mobile-tab-prev" aria-label="Previous panel">{ICON_CHEVL}</button>
<button type="button" class="mobile-nav-btn" id="mobile-tab-next" aria-label="Next panel">{ICON_CHEVR}</button>
</div>
"""
TOPBAR_HTML = f"""
<div class="topbar">
<div class="wordmark">Nexenda Alignment Research</div>
<div class="top-actions">
<button id="theme-toggle" class="icon-btn" type="button" aria-label="Toggle light/dark theme">{ICON_MOON}{ICON_SUN}</button>
<a class="pill" href="{WEBSITE_URL}" target="_blank" rel="noopener noreferrer">Website {ICON_ARROW}</a>
</div>
</div>
"""
HERO_HTML = f"""
<div class="hero">
<div class="hero-title">Council AI - Theorem Explorer</div>
<p class="hero-sub">Interactive companion to Council AI: Toward Deliberative Artificial Intelligence.<br>
Both panels compute the paper's theorems live.</p>
<div class="hero-actions">
<a class="pill" href="{PAPER_URL}" target="_blank" rel="noopener noreferrer">Read the paper</a>
<a class="ghost" href="{CODE_URL}" target="_blank" rel="noopener noreferrer">View code {ICON_CHEV}</a>
</div>
</div>
"""
T1_STATEMENT = ('<p class="statement">If the deliberation operator <b>Φ</b> is contractive with constant <b>γ &lt; 1</b>, '
'the council state sequence converges exponentially to a unique fixed point: '
'<b>‖H(t) − H*‖ ≤ γᵗ ‖H(0) − H*‖</b>.</p>')
T2_STATEMENT = ('<p class="statement">With <b>n experts</b>, individual error rate p, pairwise error correlation ρ, '
'and <b>k experts</b> required for a high-confidence output: '
'<b>P(catastrophic error) ≤ p·ρ^(k−1) when p ≤ ρ</b>; '
'the tighter bound pᵏ applies when p &gt; ρ.</p>')
NOTICE_HTML = f"""
<div class="notice">{ICON_INFO}<span>This demo illustrates the mathematics proven in Theorems 1 and 2 of the
Council AI paper. It does not run any trained neural model. Parameters are user-selected inputs to formal
bounds, not active system telemetry.</span></div>
"""
# Client-side JS (runs once on load): theme init + toggle, and −/+ steppers
# on every slider's number box.
INIT_JS = """
() => {
if (window.__nxInit) return;
window.__nxInit = true;
const body = document.body;
const apply = (m) => {
document.documentElement.dataset.theme = m;
body.dataset.theme = m;
body.classList.toggle('dark', m === 'dark');
// inline styles so the icon swap never depends on CSS specificity or load order
document.querySelectorAll('#theme-toggle .i-sun').forEach((el) => { el.style.display = m === 'dark' ? 'block' : 'none'; });
document.querySelectorAll('#theme-toggle .i-moon').forEach((el) => { el.style.display = m === 'dark' ? 'none' : 'block'; });
};
let saved = null;
try { saved = localStorage.getItem('nx-theme'); } catch (e) {}
apply(saved || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
document.addEventListener('click', (e) => {
if (!e.target.closest('#theme-toggle')) return;
const next = body.dataset.theme === 'dark' ? 'light' : 'dark';
apply(next);
try { localStorage.setItem('nx-theme', next); } catch (e) {}
});
const switchTab = (dir) => {
const tabs = Array.from(document.querySelectorAll('[role="tablist"] [role="tab"]'));
if (!tabs.length) return;
const cur = tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true');
const next = tabs[(cur + dir + tabs.length) % tabs.length];
if (next) next.click();
};
document.addEventListener('click', (e) => {
if (e.target.closest('#mobile-tab-prev')) switchTab(-1);
if (e.target.closest('#mobile-tab-next')) switchTab(1);
});
const decorate = () => {
apply(body.dataset.theme);
document.querySelectorAll('.dslider').forEach((el) => {
const num = el.querySelector('input[type=number]');
if (!num || num.dataset.stepper) return;
num.dataset.stepper = '1';
const mk = (sym, dir) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'step-btn';
b.textContent = sym;
b.setAttribute('aria-label', dir > 0 ? 'Increase' : 'Decrease');
b.addEventListener('click', (ev) => {
ev.preventDefault();
dir > 0 ? num.stepUp() : num.stepDown();
num.dispatchEvent(new Event('input', { bubbles: true }));
num.dispatchEvent(new Event('change', { bubbles: true }));
});
return b;
};
num.parentNode.insertBefore(mk('−', -1), num);
num.parentNode.insertBefore(mk('+', 1), num.nextSibling);
});
};
new MutationObserver(decorate).observe(document.body, { childList: true, subtree: true });
decorate();
}
"""
# CSS
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Serif:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg:#FEFDF9; --card:#FFFFFF; --border:#E9E7DF; --chip:#F5F3EC; --track:#F0EEE6;
--text:#151513; --muted:#5F5E58; --faint:#9A988F; --accent:#E8651A;
--btn-bg:#111110; --btn-fg:#FEFDF9; --tab-active:#FFFFFF;
}
body[data-theme="dark"] {
--bg:#121211; --card:#1B1B1A; --border:#2B2B29; --chip:#272725; --track:#262624;
--text:#F3F1EA; --muted:#A3A199; --faint:#6D6C66;
--btn-bg:#FEFDF9; --btn-fg:#111110; --tab-active:#2B2B29;
}
body, gradio-app, .gradio-container, .main, .app { background:var(--bg) !important; }
.gradio-container {
max-width:1440px !important; margin:0 auto !important; padding:0 48px 48px !important;
font-family:'Inter',sans-serif !important; color:var(--text);
}
.gradio-container footer { display:none !important; }
.gradio-container .block, .gradio-container .form {
background:transparent !important; border:none !important; box-shadow:none !important; padding:0 !important;
}
/* ---- top bar ---- */
.topbar { display:flex; align-items:center; justify-content:space-between; padding-top:40px; }
.wordmark { font-size:15px; font-weight:500; color:var(--text); letter-spacing:-0.005em; }
.top-actions { display:flex; align-items:center; gap:10px; }
.icon-btn {
width:44px; height:44px; border-radius:50%; border:none; background:var(--chip); color:var(--text);
display:grid; place-items:center; cursor:pointer; padding:0; flex:none;
}
.icon-btn .i-sun { display:none; }
body[data-theme="dark"] .icon-btn .i-sun { display:block; }
body[data-theme="dark"] .icon-btn .i-moon { display:none; }
.gradio-container a.pill {
display:inline-flex; align-items:center; gap:8px; height:44px; padding:0 28px; border-radius:999px;
background:var(--btn-bg); color:var(--btn-fg) !important; text-decoration:none !important;
font-size:14.5px; font-weight:500; letter-spacing:0.005em; white-space:nowrap;
}
.gradio-container a.ghost {
display:inline-flex; align-items:center; gap:6px; height:44px; padding:0 16px;
color:var(--text) !important; text-decoration:none !important; font-size:14.5px; font-weight:500;
}
.gradio-container a.pill:hover, .gradio-container a.ghost:hover, .icon-btn:hover { opacity:0.85; }
.gradio-container a:focus-visible, .icon-btn:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* ---- hero ---- */
.hero { text-align:center; padding:60px 0 52px; }
.hero-title {
font-family:'Noto Serif',serif !important; font-weight:500; font-size:clamp(30px,4.6vw,46px);
letter-spacing:-0.015em; line-height:1.15; color:var(--text);
}
.hero-sub {
font-family:'Noto Serif',serif !important; font-size:16px; line-height:1.6; color:var(--muted) !important;
margin:20px auto 28px; max-width:600px; letter-spacing:0.001em;
}
.hero-actions { display:flex; justify-content:center; align-items:center; gap:24px; flex-wrap:wrap; row-gap:14px; }
/* ---- mobile carousel arrows (tab nav on narrow screens) ---- */
.mobile-tab-nav { display:none; justify-content:flex-end; gap:10px; padding:8px 0 28px; }
.mobile-nav-btn {
width:36px; height:36px; border-radius:50%; border:1px solid var(--border); background:transparent;
color:var(--faint); display:grid; place-items:center; cursor:pointer; padding:0;
}
.mobile-nav-btn:hover { color:var(--text); border-color:var(--faint); }
/* ---- tabs (pill switcher) ---- */
.gradio-container [role="tablist"] {
display:inline-flex !important; width:auto !important; gap:4px; padding:4px !important;
border:1px solid var(--border) !important; border-radius:999px !important; background:var(--chip) !important;
margin-bottom:36px !important;
}
.gradio-container [role="tablist"]::after, .gradio-container [role="tab"]::after { display:none !important; }
.gradio-container [role="tab"] {
display:flex !important; align-items:center !important; height:36px !important; box-sizing:border-box !important;
border:1px solid transparent !important; border-radius:999px !important; background:transparent !important;
color:var(--faint) !important; padding:0 18px !important; font-size:14.5px !important; font-weight:500 !important;
letter-spacing:0.001em !important;
}
.gradio-container [role="tab"][aria-selected="true"] {
background:var(--tab-active) !important; color:var(--text) !important; border-color:var(--border) !important;
}
.gradio-container .tabitem { background:transparent !important; border:none !important; padding:0 !important; }
.statement { font-size:15px; line-height:1.65; color:var(--text) !important; margin:0 0 32px; letter-spacing:0.001em; }
.statement b { font-weight:600; }
/* ---- cards ---- */
.theorem-row { align-items:stretch !important; margin-bottom:32px; }
.card {
background:var(--card) !important; border:1px solid var(--border) !important; border-radius:16px !important;
padding:22px 24px !important; gap:22px !important; box-sizing:border-box !important;
}
.card:not(.plotcard) { height:100% !important; }
.plotcard { display:flex !important; flex-direction:column !important; gap:16px !important; }
.plotcard > div:nth-child(2) img, .plotcard > div:nth-child(2) svg {
width:100% !important; height:auto !important; display:block !important;
}
/* ---- sliders ---- */
.dslider { --slider-color:var(--accent); --neutral-200:var(--track); }
.dslider .head { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; }
.dslider label span, .dslider [data-testid="block-info"] {
font-size:14px !important; font-weight:400 !important; color:var(--muted) !important; background:transparent !important;
}
.dslider .tab-like-container { display:flex; align-items:center; background:var(--chip) !important; border:none !important; border-radius:8px; padding:4px 6px; }
.dslider input[type=number] {
width:52px !important; text-align:center; background:transparent !important; border:none !important; box-shadow:none !important;
color:var(--text) !important; font-weight:600; font-size:14px; padding:4px !important; -moz-appearance:textfield;
}
.dslider input[type=number]::-webkit-inner-spin-button, .dslider input[type=number]::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
.dslider .reset-button { display:none !important; }
.step-btn {
width:22px; height:22px; border:none; background:transparent; color:var(--faint); cursor:pointer;
font-size:15px; line-height:1; border-radius:6px; padding:0;
}
.step-btn:hover { color:var(--text); background:var(--track); }
.dslider .slider_input_container { display:flex; flex-wrap:wrap; align-items:center; }
.dslider input[type=range] {
order:-1; width:100% !important; height:6px !important; border-radius:999px !important;
-webkit-appearance:none; appearance:none; background-color:var(--track) !important;
}
.dslider input[type=range]::-webkit-slider-thumb {
-webkit-appearance:none; width:16px; height:16px; margin-top:-5px; border-radius:50%; background:var(--accent);
border:2px solid #fff; box-shadow:0 0 0 1px rgba(0,0,0,0.08); cursor:pointer;
}
.dslider input[type=range]::-moz-range-thumb {
width:12px; height:12px; border-radius:50%; background:var(--accent); border:2px solid #fff; cursor:pointer;
}
.dslider .min_value, .dslider .max_value { font-size:11px !important; color:var(--faint) !important; margin-top:8px; }
.dslider .min_value { margin-right:auto; }
.gradio-container button.reset-btn {
width:100%; height:48px; border:none !important; border-radius:999px !important; background:var(--chip) !important;
color:var(--text) !important; font-size:15px !important; font-weight:400 !important; box-shadow:none !important;
}
.gradio-container button.reset-btn:hover { background:var(--track) !important; }
/* ---- plot card ---- */
.plot-head { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px 20px; }
.plot-title { display:flex; align-items:center; gap:8px; font-size:15px; font-weight:500; color:var(--text); }
.legend { display:flex; flex-wrap:wrap; gap:6px 18px; font-size:12px; color:var(--muted); }
.legend i { display:inline-block; width:14px; height:0; border-top:2px solid; margin-right:6px; vertical-align:middle; }
.legend i.dash { border-top-style:dashed; }
/* ---- stats (now inside the plot card, under the chart) ---- */
.stats {
display:flex; flex-direction:row; flex-wrap:wrap; align-items:baseline; gap:6px 26px; flex:none;
padding-top:16px; border-top:1px solid var(--border); font-size:12px; color:var(--muted);
}
.stats p { margin:0; white-space:nowrap; }
.stats b { color:var(--text); font-weight:600; }
.stats em { color:var(--text); font-style:italic; font-weight:400; }
.notice {
display:flex; align-items:flex-start; gap:14px; padding:20px 18px; border:1px solid var(--border);
border-radius:14px; background:var(--card); font-size:13px; line-height:1.6; color:var(--muted);
margin-top:8px;
}
.notice svg { flex:none; margin-top:2px; }
@media (max-width:768px) {
.gradio-container { padding:0 16px 32px !important; }
.topbar { padding-top:20px; }
.wordmark { font-size:14px; }
.icon-btn { width:38px; height:38px; }
.gradio-container a.pill { padding:0 20px; height:40px; font-size:14px; }
.hero { padding:36px 0 8px; }
.hero-actions { flex-direction:column; gap:14px; }
.gradio-container [role="tablist"] { display:none !important; }
.mobile-tab-nav { display:flex !important; }
.theorem-row { flex-direction:column !important; }
.theorem-row .card { height:auto !important; }
.stats { flex-direction:column; gap:8px; }
.stats p { white-space:normal; }
}
"""
HEAD_HTML = """
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script>
(function () {
const init = (%s);
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script>
""" % INIT_JS
NEXENDA_THEME = gr.themes.Base(
primary_hue=gr.themes.colors.orange,
neutral_hue=gr.themes.colors.stone,
font=gr.themes.GoogleFont("Inter"),
).set(slider_color="#E8651A", color_accent="#E8651A")
# UI
with gr.Blocks(title="Council AI — Theorem Explorer") as demo:
gr.HTML(TOPBAR_HTML)
gr.HTML(HERO_HTML)
gr.HTML(MOBILE_NAV_HTML)
with gr.Tab("Deliberation Convergence"):
gr.HTML(T1_STATEMENT)
with gr.Row(equal_height=True, elem_classes="theorem-row"):
with gr.Column(scale=1, min_width=320, elem_classes="card"):
gamma_s = gr.Slider(0.1, 0.9, value=DEFAULTS_T1[0], step=0.01, label="Contraction rate γ", elem_classes="dslider")
rounds_s = gr.Slider(4, 30, value=DEFAULTS_T1[1], step=1, label="Deliberation rounds T", elem_classes="dslider")
d0_s = gr.Slider(0.2, 2.5, value=DEFAULTS_T1[2], step=0.05, label="Initial disagreement ‖H(0)−H*‖", elem_classes="dslider")
tau_s = gr.Slider(0.70, 0.99, value=DEFAULTS_T1[3], step=0.01, label="Consensus threshold τ", elem_classes="dslider")
reset1_btn = gr.Button("Reset to defaults", elem_classes="reset-btn")
with gr.Column(scale=3, min_width=420, elem_classes="card plotcard"):
legend1 = gr.HTML()
plot1 = gr.Plot(show_label=False, container=False)
stats1 = gr.HTML()
t1_inputs = [gamma_s, rounds_s, d0_s, tau_s]
t1_outputs = [plot1, legend1, stats1]
for ctrl in t1_inputs:
ctrl.change(theorem1, inputs=t1_inputs, outputs=t1_outputs)
reset1_btn.click(reset_theorem1, inputs=None, outputs=t1_inputs + t1_outputs)
demo.load(theorem1, inputs=t1_inputs, outputs=t1_outputs)
with gr.Tab("Multi-Expert Safety Bound"):
gr.HTML(T2_STATEMENT)
with gr.Row(equal_height=True, elem_classes="theorem-row"):
with gr.Column(scale=1, min_width=320, elem_classes="card"):
p_s = gr.Slider(0.001, 0.15, value=DEFAULTS_T2[0], step=0.001, label="Individual error rate p", elem_classes="dslider")
rho_s = gr.Slider(0.0, 1.0, value=DEFAULTS_T2[1], step=0.01, label="Error correlation ρ", elem_classes="dslider")
n_s = gr.Slider(2, 15, value=DEFAULTS_T2[2], step=1, label="Total experts n", elem_classes="dslider")
k_s = gr.Slider(1, 15, value=DEFAULTS_T2[3], step=1, label="Agreement required k", elem_classes="dslider")
reset2_btn = gr.Button("Reset to defaults", elem_classes="reset-btn")
with gr.Column(scale=3, min_width=420, elem_classes="card plotcard"):
legend2 = gr.HTML()
plot2 = gr.Plot(show_label=False, container=False)
stats2 = gr.HTML()
t2_inputs = [p_s, rho_s, n_s, k_s]
t2_outputs = [plot2, legend2, stats2]
for ctrl in t2_inputs:
ctrl.change(theorem2, inputs=t2_inputs, outputs=t2_outputs)
reset2_btn.click(reset_theorem2, inputs=None, outputs=t2_inputs + t2_outputs)
demo.load(theorem2, inputs=t2_inputs, outputs=t2_outputs)
gr.HTML(NOTICE_HTML)
# theme init/toggle + slider steppers (client-side only)
demo.load(fn=None, inputs=None, outputs=None, js=INIT_JS)
if __name__ == "__main__":
demo.launch(css=CUSTOM_CSS, theme=NEXENDA_THEME, head=HEAD_HTML, ssr_mode=False)