abdessamad-bourkibate's picture
Update main.py
63584f1 verified
# main.py — FDRSM-4 Threshold Engine (App)
# Clean LaTeX rendering + 8-page PDF report (no overlap)
import io
import datetime
from typing import Dict, Any, List, Tuple
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import gradio as gr
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib import colors
from engine.validation import validate_inputs
from engine.thresholds import compute_diagnostics, interpret_governance, euler_simulation
RIGHTS_HOLDER_LINE = "Rights holder: Abdessamad Bourkibate (Morocco) — Apache-2.0"
# ==========================================================
# PDF typography + layout helpers
# ==========================================================
def _register_pdf_font() -> str:
candidates = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
]
for p in candidates:
try:
pdfmetrics.registerFont(TTFont("UFont", p))
return "UFont"
except Exception:
continue
return "Helvetica"
def _fig_to_png_bytes(fig, dpi=420) -> io.BytesIO:
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
buf.seek(0)
return buf
def _wrap_lines(font_name: str, font_size: int, text: str, max_width_pt: float) -> List[str]:
words = (text or "").replace("\r", "").split()
if not words:
return [""]
lines, cur = [], words[0]
for w in words[1:]:
trial = cur + " " + w
if pdfmetrics.stringWidth(trial, font_name, font_size) <= max_width_pt:
cur = trial
else:
lines.append(cur)
cur = w
lines.append(cur)
return lines
def _draw_wrapped(c: canvas.Canvas, x: float, y: float, font: str, size: int,
text: str, max_w: float, leading: float = 13.0) -> float:
c.setFont(font, size)
for para in (text or "").split("\n"):
if para.strip() == "":
y -= leading
continue
for ln in _wrap_lines(font, size, para, max_w):
c.drawString(x, y, ln)
y -= leading
return y
def _section_bar(c: canvas.Canvas, x: float, y: float, w: float, title: str,
font: str, bg=(0.07, 0.19, 0.22), fg=(0.97, 0.99, 0.99)) -> float:
c.setFillColorRGB(*bg)
c.roundRect(x, y - 16, w, 20, 6, fill=1, stroke=0)
c.setFillColorRGB(*fg)
c.setFont(font, 11)
c.drawString(x + 10, y - 3, title)
return y - 28
def _kpi_box(c: canvas.Canvas, x: float, y: float, w: float, h: float,
label: str, value: str, font: str):
c.setFillColorRGB(0.96, 0.98, 0.99)
c.setStrokeColorRGB(0.85, 0.90, 0.93)
c.roundRect(x, y - h, w, h, 10, fill=1, stroke=1)
c.setFillColorRGB(0.12, 0.12, 0.12)
c.setFont(font, 9)
c.drawString(x + 10, y - 16, label)
c.setFont(font, 12)
c.drawString(x + 10, y - 36, value)
def _draw_table(c: canvas.Canvas, x: float, y: float, col_w: List[float], rows: List[List[str]],
font: str, header: bool = True, row_h: float = 18.0) -> float:
# Simple clean table with wrapping
total_w = sum(col_w)
nrows = len(rows)
cur_y = y
# Header style
for r in range(nrows):
is_header = header and (r == 0)
bg = (0.92, 0.96, 0.97) if is_header else (1, 1, 1)
c.setFillColorRGB(*bg)
c.setStrokeColorRGB(0.84, 0.88, 0.90)
c.rect(x, cur_y - row_h, total_w, row_h, fill=1, stroke=1)
cx = x
for j, cell in enumerate(rows[r]):
c.setStrokeColorRGB(0.84, 0.88, 0.90)
c.rect(cx, cur_y - row_h, col_w[j], row_h, fill=0, stroke=1)
c.setFillColorRGB(0.10, 0.10, 0.10)
c.setFont(font, 9 if not is_header else 9.5)
max_w = col_w[j] - 10
lines = _wrap_lines(font, 9 if not is_header else 9.5, str(cell), max_w)
# draw first line only (tight layout); keep table clean
c.drawString(cx + 6, cur_y - 13, lines[0][:140])
cx += col_w[j]
cur_y -= row_h
return cur_y - 8
# ==========================================================
# Additional analytics for "comprehensive" report
# ==========================================================
def _parameter_sweep(alpha: float, beta: float, D: float, k: float, tol: float, eps: float,
D_span=(0, 10), k_span=(0, 10), n=90) -> Dict[str, Any]:
Dg = np.linspace(D_span[0], D_span[1], n)
kg = np.linspace(k_span[0], k_span[1], n)
Lam = np.zeros((n, n), dtype=float)
F = np.zeros((n, n), dtype=float)
for i, Di in enumerate(Dg):
for j, kj in enumerate(kg):
lam = (alpha * Di) - (beta * kj)
Delta = (beta * kj) - (alpha * Di)
denom = abs(alpha * Di) + abs(beta * kj) + eps
frag = abs(Delta) / denom
Lam[i, j] = lam
F[i, j] = frag
# stable mask for visualization
stable = Lam < -tol
near = np.abs(Lam) <= tol
unstable = Lam > tol
return {"Dg": Dg, "kg": kg, "Lam": Lam, "F": F, "stable": stable, "near": near, "unstable": unstable}
def _make_figures(diag: Dict[str, Any], params: Dict[str, Any], t: np.ndarray, R: np.ndarray) -> Dict[str, ImageReader]:
alpha = float(params["alpha"]); beta = float(params["beta"])
D = float(params["D"]); k = float(params["k"])
tol = float(params["tol"]); eps = float(params["eps"])
# Shared rc
plt.rcParams.update({
"font.size": 10,
"axes.titlesize": 11,
"axes.labelsize": 10,
"axes.spines.top": False,
"axes.spines.right": False,
"grid.alpha": 0.25,
})
# 1) Risk trajectory
fig1 = plt.figure(figsize=(6.6, 4.2))
plt.plot(t, R, linewidth=2.6)
plt.xlabel("Time t")
plt.ylabel("Risk R(t)")
plt.title("Risk trajectory (Euler integration)")
plt.grid(True)
img1 = ImageReader(_fig_to_png_bytes(fig1, dpi=440))
plt.close(fig1)
# 2) Stability map
fig2 = plt.figure(figsize=(6.6, 4.2))
Dmax = max(10.0, D * 2.0 + 1.0)
Dg = np.linspace(0, Dmax, 420)
k_line = (alpha / beta) * Dg if beta != 0 else np.full_like(Dg, np.nan)
plt.plot(Dg, k_line, linestyle="--", linewidth=2.2, label="Boundary: k = (α/β)·D")
plt.scatter([D], [k], s=95, label="Current point (D,k)")
plt.xlabel("Dependency intensity D")
plt.ylabel("Authority gain k")
plt.title("Stability map (stable region above boundary)")
plt.grid(True)
plt.legend()
img2 = ImageReader(_fig_to_png_bytes(fig2, dpi=440))
plt.close(fig2)
# 3) Sensitivity bar
fig3 = plt.figure(figsize=(6.6, 4.2))
ranked = diag["sens_ranked"]
labels = [a for a, _ in ranked][::-1]
vals = [abs(v) for _, v in ranked][::-1]
plt.barh(labels, vals)
plt.xlabel("|sensitivity|")
plt.title("Local sensitivity ranking of λ")
plt.grid(True, axis="x")
img3 = ImageReader(_fig_to_png_bytes(fig3, dpi=440))
plt.close(fig3)
# 4) Sweep heatmap (regime regions)
sweep = _parameter_sweep(alpha, beta, D, k, tol, eps, n=95)
fig4 = plt.figure(figsize=(6.6, 4.8))
# regime map: 0 stable, 1 near, 2 unstable
Z = np.zeros_like(sweep["Lam"])
Z[sweep["near"]] = 1
Z[sweep["unstable"]] = 2
plt.imshow(
Z.T,
origin="lower",
aspect="auto",
extent=[sweep["Dg"][0], sweep["Dg"][-1], sweep["kg"][0], sweep["kg"][-1]]
)
plt.plot(sweep["Dg"], (alpha / beta) * sweep["Dg"] if beta != 0 else np.nan, linestyle="--", linewidth=1.8)
plt.scatter([D], [k], s=70)
plt.xlabel("D")
plt.ylabel("k")
plt.title("Regime map over (D,k): stable / near-boundary / unstable")
img4 = ImageReader(_fig_to_png_bytes(fig4, dpi=440))
plt.close(fig4)
# 5) Fragility proximity heatmap (F)
fig5 = plt.figure(figsize=(6.6, 4.8))
plt.imshow(
sweep["F"].T,
origin="lower",
aspect="auto",
extent=[sweep["Dg"][0], sweep["Dg"][-1], sweep["kg"][0], sweep["kg"][-1]]
)
plt.plot(sweep["Dg"], (alpha / beta) * sweep["Dg"] if beta != 0 else np.nan, linestyle="--", linewidth=1.8)
plt.scatter([D], [k], s=70)
plt.xlabel("D")
plt.ylabel("k")
plt.title("Fragility proximity surface F over (D,k)")
img5 = ImageReader(_fig_to_png_bytes(fig5, dpi=440))
plt.close(fig5)
return {"traj": img1, "map": img2, "sens": img3, "regmap": img4, "fragmap": img5}
# ==========================================================
# 8-page PDF report
# ==========================================================
def make_pdf(
title: str,
params: Dict[str, Any],
diag: Dict[str, Any],
gov: Dict[str, str],
t: np.ndarray,
R: np.ndarray,
figures: Dict[str, ImageReader],
) -> str:
font = _register_pdf_font()
out_path = "/tmp/FDRSM4_Threshold_Report_Extended.pdf"
c = canvas.Canvas(out_path, pagesize=A4)
W, H = A4
mx = 2.0 * cm
max_w = W - 4.0 * cm
def header(subtitle: str = ""):
# Calm psych-friendly palette
c.setFillColorRGB(0.07, 0.19, 0.22)
c.rect(0, H - 2.2 * cm, W, 2.2 * cm, fill=1, stroke=0)
c.setFillColorRGB(0.97, 0.99, 0.99)
c.setFont(font, 14)
c.drawString(mx, H - 1.25 * cm, (title or "FDRSM-4 Report")[:110])
c.setFont(font, 9.5)
c.drawString(mx, H - 1.85 * cm, f"Generated: {datetime.date.today().isoformat()}")
if subtitle:
c.setFont(font, 9.5)
c.drawRightString(W - mx, H - 1.85 * cm, subtitle[:90])
def footer(page_no: int):
c.setFillColorRGB(0.35, 0.40, 0.48)
c.setFont(font, 9)
c.drawString(mx, 1.1 * cm, RIGHTS_HOLDER_LINE)
c.drawRightString(W - mx, 1.1 * cm, f"Page {page_no}")
page = 1
# ---------------- Page 1: Cover ----------------
header("Extended Report")
y = H - 3.2 * cm
c.setFillColorRGB(0.10, 0.10, 0.10)
c.setFont(font, 20)
c.drawString(mx, y, "FDRSM-4 — Threshold Engine")
y -= 1.1 * cm
c.setFont(font, 11)
y = _draw_wrapped(
c, mx, y, font, 11,
"A decision-facing diagnostic report for authority–dependency stability in family digital risk systems.\n"
"This report is structural and analytical: no content moderation, no behavioral profiling, no surveillance tooling.",
max_w, leading=15
)
y -= 0.2 * cm
# KPI row
_kpi_box(c, mx, y, (max_w/3)-8, 55, "Regime", str(diag["regime"]), font)
_kpi_box(c, mx + (max_w/3), y, (max_w/3)-8, 55, "Fragility", str(diag["fragility"]), font)
_kpi_box(c, mx + 2*(max_w/3), y, (max_w/3)-8, 55, "Margin to boundary", f"{diag['margin_to_boundary']:.6f}", font)
y -= 2.0 * cm
y = _section_bar(c, mx, y, max_w, "Parameter snapshot", font)
snap = (
f"α={float(params['alpha']):g}, β={float(params['beta']):g}, D={float(params['D']):g}, k={float(params['k']):g}\n"
f"R(0)={float(params['R0']):g}, T={float(params['T']):g}, n={int(params['n'])}, tol={float(params['tol']):g}, ε={float(params['eps']):g}\n"
f"λ={diag['lambda']:.6f}, Δ={diag['Delta']:.6f}, F={diag['F']:.6f}"
)
y = _draw_wrapped(c, mx, y, font, 10, snap, max_w, leading=13)
footer(page)
c.showPage()
page += 1
# ---------------- Page 2: Executive summary ----------------
header("Executive Summary")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Summary", font)
summary = (
f"Regime classification: {diag['regime']}\n"
f"Fragility class: {diag['fragility']}\n"
f"Boundary point: k* = (α/β)·D = {diag['boundary_k']:.6f}\n"
f"Distance to boundary: k − k* = {diag['margin_to_boundary']:.6f}\n\n"
"Interpretation (decision-facing):\n"
f"- Posture: {gov['posture']}\n"
f"- Decision: {gov['decision']}\n"
f"- Risk note: {gov['risk']}\n"
)
y = _draw_wrapped(c, mx, y, font, 10, summary, max_w, leading=13)
y -= 0.3 * cm
y = _section_bar(c, mx, y, max_w, "What this report is / is not", font)
scope = (
"This report provides a structural diagnostic of stability conditions in a formal model.\n"
"It does NOT infer individual behavior, does NOT profile users, and does NOT implement monitoring or surveillance.\n"
"Its purpose is reproducible interpretation for governance, policy, and research discussion."
)
y = _draw_wrapped(c, mx, y, font, 10, scope, max_w, leading=13)
footer(page)
c.showPage()
page += 1
# ---------------- Page 3: Diagnostics table ----------------
header("Diagnostics")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Formal diagnostics table", font)
rows = [
["Metric", "Value", "Meaning"],
["λ = αD − βk", f"{diag['lambda']:.6f}", "Stability rate (negative → decay; positive → escalation)"],
["Δ = βk − αD", f"{diag['Delta']:.6f}", "Signed stability margin (positive → stable margin)"],
["F", f"{diag['F']:.6f}", "Normalized proximity (smaller → closer to boundary)"],
["k* = (α/β)D", f"{diag['boundary_k']:.6f}", "Boundary authority required for stability"],
["k − k*", f"{diag['margin_to_boundary']:.6f}", "Distance to boundary (robustness indicator)"],
["Regime", str(diag["regime"]), "Stable / Near-boundary / Unstable"],
["Fragility", str(diag["fragility"]), "Low / Moderate / High (based on F)"],
]
col_w = [4.0*cm, 4.0*cm, max_w - 8.0*cm]
y = _draw_table(c, mx, y, col_w, rows, font, header=True, row_h=19)
y = _section_bar(c, mx, y, max_w, "Local sensitivities of λ", font)
sens_rows = [["Sensitivity term", "Value (signed)", "Impact intuition"]]
for name, val in diag["sens_ranked"]:
sens_rows.append([name, f"{val:+.6f}", "Higher magnitude → stronger local influence on regime switching"])
y = _draw_table(c, mx, y, [7.0*cm, 4.0*cm, max_w - 11.0*cm], sens_rows, font, header=True, row_h=18)
footer(page)
c.showPage()
page += 1
# ---------------- Page 4: Figure 1 (trajectory) ----------------
header("Figures")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Figure 1 — Risk trajectory", font)
img_w = max_w
img_h = 13.2 * cm
c.drawImage(figures["traj"], mx, y - img_h, width=img_w, height=img_h, mask="auto")
y -= img_h + 0.6 * cm
caption = (
"Caption: Euler integration of the baseline dynamic. A decaying trajectory indicates stability; "
"growth indicates escalation. The shape reflects the sign and magnitude of λ."
)
y = _draw_wrapped(c, mx, y, font, 10, caption, max_w, leading=13)
footer(page)
c.showPage()
page += 1
# ---------------- Page 5: Figure 2 (stability map) ----------------
header("Figures")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Figure 2 — Stability map", font)
img_h2 = 13.2 * cm
c.drawImage(figures["map"], mx, y - img_h2, width=img_w, height=img_h2, mask="auto")
y -= img_h2 + 0.6 * cm
caption2 = (
"Caption: Stability boundary k*=(α/β)D. Points above the line (higher k for given D) are stable. "
"Near the line: small drift can flip the regime."
)
y = _draw_wrapped(c, mx, y, font, 10, caption2, max_w, leading=13)
footer(page)
c.showPage()
page += 1
# ---------------- Page 6: Figure 3 (sensitivity) ----------------
header("Figures")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Figure 3 — Sensitivity ranking", font)
img_h3 = 13.2 * cm
c.drawImage(figures["sens"], mx, y - img_h3, width=img_w, height=img_h3, mask="auto")
y -= img_h3 + 0.6 * cm
caption3 = (
"Caption: Local derivatives indicate which parameter perturbations most strongly shift λ. "
"This is a local diagnostic: it explains directionally which knobs are most influential around the current point."
)
y = _draw_wrapped(c, mx, y, font, 10, caption3, max_w, leading=13)
footer(page)
c.showPage()
page += 1
# ---------------- Page 7: Regime map sweep ----------------
header("Stress view")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Figure 4 — Regime map over (D,k)", font)
img_h4 = 11.0 * cm
c.drawImage(figures["regmap"], mx, y - img_h4, width=img_w, height=img_h4, mask="auto")
y -= img_h4 + 0.5 * cm
y = _section_bar(c, mx, y, max_w, "Figure 5 — Fragility surface F over (D,k)", font)
img_h5 = 11.0 * cm
c.drawImage(figures["fragmap"], mx, y - img_h5, width=img_w, height=img_h5, mask="auto")
footer(page)
c.showPage()
page += 1
# ---------------- Page 8: Appendix (formulas + governance memo) ----------------
header("Appendix")
y = H - 3.0 * cm
y = _section_bar(c, mx, y, max_w, "Appendix A — Model definitions", font)
appA = (
"Baseline:\n"
" dR/dt = (αD − βk)R\n\n"
"Definitions:\n"
" λ = αD − βk\n"
" Δ = βk − αD\n"
" F = |Δ| / (|αD| + |βk| + ε)\n\n"
"Regime rules:\n"
" Stable if λ < −tol\n"
" Near-boundary if |λ| ≤ tol\n"
" Unstable if λ > tol\n"
)
y = _draw_wrapped(c, mx, y, font, 10, appA, max_w, leading=13)
y -= 0.2 * cm
y = _section_bar(c, mx, y, max_w, "Appendix B — Governance interpretation output", font)
appB = (
f"Posture: {gov['posture']}\n"
f"Decision: {gov['decision']}\n"
f"Risk note: {gov['risk']}\n\n"
"Note: The interpretation is structural. It maps regime/fragility to governance stance without behavioral inference."
)
y = _draw_wrapped(c, mx, y, font, 10, appB, max_w, leading=13)
footer(page)
c.save()
return out_path
# ==========================================================
# Engine runner
# ==========================================================
def run_engine(alpha, beta, D, k, R0, T, n, tol, eps):
v = validate_inputs(alpha, beta, D, k, R0, T, n, tol, eps)
if not v["ok"]:
msg = "Input errors:\n- " + "\n- ".join(v["errors"])
return None, None, None, msg, None
diag = compute_diagnostics(alpha, beta, D, k, tol, eps)
gov = interpret_governance(diag["regime"], diag["fragility"], diag["Delta"], diag["margin_to_boundary"])
t, R = euler_simulation(alpha, beta, D, k, R0, T, int(n))
params = {
"alpha": float(alpha), "beta": float(beta), "D": float(D), "k": float(k),
"R0": float(R0), "T": float(T), "n": int(n), "tol": float(tol), "eps": float(eps)
}
figures = _make_figures(diag, params, t, R)
pdf_path = make_pdf("FDRSM-4 — Threshold Engine Report", params, diag, gov, t, R, figures)
# UI plots (2 only)
fig1 = plt.figure(figsize=(6.2, 4.0))
plt.plot(t, R, linewidth=2.4)
plt.xlabel("Time t")
plt.ylabel("Risk R(t)")
plt.title("Risk trajectory (Euler integration)")
plt.grid(True, alpha=0.25)
fig2 = plt.figure(figsize=(6.2, 4.0))
Dmax = max(10.0, float(D) * 2.0 + 1.0)
Dg = np.linspace(0, Dmax, 360)
k_line = (float(alpha) / float(beta)) * Dg
plt.plot(Dg, k_line, linestyle="--", linewidth=2.0, label="Boundary k = (α/β)·D")
plt.scatter([float(D)], [float(k)], s=80, label="Current (D,k)")
plt.xlabel("Dependency intensity D")
plt.ylabel("Authority gain k")
plt.title("Stability map (stable region above boundary)")
plt.grid(True, alpha=0.25)
plt.legend()
# Decision report text
sens_lines = ["Local sensitivity ranking of λ:"]
for name, val in diag["sens_ranked"]:
sens_lines.append(f"- {name}: {val:+.6f}")
report = (
f"Regime: {diag['regime']}\n"
f"Fragility: {diag['fragility']}\n\n"
f"λ = {diag['lambda']:.6f}\n"
f"Δ = {diag['Delta']:.6f}\n"
f"F = {diag['F']:.6f}\n"
f"k* = (α/β)D = {diag['boundary_k']:.6f}\n"
f"k − k* = {diag['margin_to_boundary']:.6f}\n\n"
f"Posture: {gov['posture']}\n"
f"Decision: {gov['decision']}\n"
f"Risk note: {gov['risk']}\n\n"
+ "\n".join(sens_lines)
)
return fig1, fig2, pdf_path, report, diag
# ==========================================================
# UI (LaTeX fixed) — no Docker mention
# ==========================================================
THEME = gr.themes.Soft(
primary_hue="teal",
secondary_hue="blue",
neutral_hue="slate",
radius_size=gr.themes.sizes.radius_lg,
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
)
CSS = """
.gradio-container {max-width: 1180px !important;}
.small {font-size: 12px; opacity: 0.85;}
"""
HEADER_MD = r"""
# FDRSM-4 — Threshold Engine
This Space implements a **decision-facing threshold layer** on top of the FDRSM series.
## Model
$$
\dot{R}(t) = (\alpha D - \beta k)R(t)
$$
$$
\lambda = \alpha D - \beta k,\quad
\Delta = \beta k - \alpha D,\quad
F = \frac{|\Delta|}{|\alpha D| + |\beta k| + \varepsilon}
$$
**Outputs:** stability regime, fragility class, stability map, trajectory plot, and an extended PDF report (≈ 8 pages).
"""
with gr.Blocks(theme=THEME, css=CSS, title="FDRSM-4 — Threshold Engine") as demo:
# ✅ FIX: Force LaTeX delimiters to render (solves broken formulas)
gr.Markdown(
HEADER_MD,
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False},
],
)
with gr.Row():
with gr.Column(scale=1):
alpha = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="α (dependency amplification)")
beta = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="β (authority damping)")
D = gr.Slider(0.0, 10.0, value=3.0, step=0.1, label="D (dependency intensity)")
k = gr.Slider(0.0, 10.0, value=3.2, step=0.1, label="k (authority gain)")
R0 = gr.Slider(0.01, 10.0, value=1.0, step=0.01, label="R(0)")
T = gr.Slider(1.0, 80.0, value=25.0, step=1.0, label="T (horizon)")
n = gr.Slider(200, 6000, value=1400, step=50, label="n (steps)")
tol = gr.Slider(0.0, 0.5, value=0.006, step=0.0005, label="tol (near-boundary)")
eps = gr.Number(value=1e-12, label="ε", precision=12)
run = gr.Button("Run Threshold Engine", variant="primary")
with gr.Column(scale=1):
p1 = gr.Plot(label="Risk trajectory")
p2 = gr.Plot(label="Stability map")
pdf = gr.File(label="Extended PDF report (≈ 8 pages)")
txt = gr.Textbox(label="Decision-facing report", lines=18)
diag_state = gr.State({})
def _run(alpha, beta, D, k, R0, T, n, tol, eps):
fig1, fig2, pdf_path, report, diag = run_engine(alpha, beta, D, k, R0, T, n, tol, eps)
return fig1, fig2, pdf_path, report, diag
run.click(
fn=_run,
inputs=[alpha, beta, D, k, R0, T, n, tol, eps],
outputs=[p1, p2, pdf, txt, diag_state]
)
demo.launch()