import io import datetime 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 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" def _register_pdf_font() -> str: candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.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=360) -> 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): 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, x, y, font, size, text, max_w, leading=13): 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 make_pdf(title: str, diagnostics: dict, gov: dict, t: np.ndarray, R: np.ndarray) -> str: font = _register_pdf_font() out_path = "/tmp/FDRSM4_Threshold_Report.pdf" c = canvas.Canvas(out_path, pagesize=A4) W, H = A4 mx = 2.0 * cm max_w = W - 4.0 * cm def header(): # Calm teal/blue (psych-friendly) 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")[:95]) c.setFont(font, 9.5) c.drawString(mx, H - 1.85*cm, f"Generated: {datetime.date.today().isoformat()}") def footer(page_no): 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}") # Figure: trajectory fig = 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) img = ImageReader(_fig_to_png_bytes(fig, dpi=360)) plt.close(fig) # Page 1 (text) page = 1 header() y = H - 3.0*cm c.setFillColorRGB(0.10, 0.10, 0.10) y = _draw_wrapped( c, mx, y, font, 11, "Threshold Engine — Formal Diagnostics", max_w, leading=14 ) y -= 6 diag_txt = ( "Model:\n" " dR/dt = (αD − βk)R\n\n" f"Computed:\n" f" λ = {diagnostics['lambda']:.6f}\n" f" Δ = {diagnostics['Delta']:.6f}\n" f" F = {diagnostics['F']:.6f}\n" f" Regime: {diagnostics['regime']}\n" f" Fragility: {diagnostics['fragility']}\n" f" Boundary k*: (α/β)D = {diagnostics['boundary_k']:.6f}\n" f" Margin to boundary: k − k* = {diagnostics['margin_to_boundary']:.6f}\n" ) y = _draw_wrapped(c, mx, y, font, 10, diag_txt, max_w, leading=13) y -= 8 gov_txt = ( "Governance interpretation:\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, gov_txt, max_w, leading=13) footer(page) c.showPage() page += 1 # Page 2 (figure) header() y = H - 3.0*cm c.setFillColorRGB(0.10, 0.10, 0.10) c.setFont(font, 11) c.drawString(mx, y, "Figure — Risk trajectory") y -= 0.6*cm img_w = W - 4.0*cm img_h = 13.0*cm c.drawImage(img, mx, y - img_h, width=img_w, height=img_h, mask="auto") footer(page) c.save() return out_path 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)) # Plot 1: R(t) plt.rcParams.update({ "font.size": 10, "axes.titlesize": 11, "axes.labelsize": 10, "axes.spines.top": False, "axes.spines.right": False, }) 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) # Plot 2: stability map fig2 = plt.figure(figsize=(6.2, 4.0)) Dmax = max(6.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() # Sensitivity table as 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) ) pdf_path = make_pdf("FDRSM-4 — Threshold Engine Report", diag, gov, t, R) return fig1, fig2, pdf_path, report, diag 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"], ) HEADER_MD = r""" # FDRSM-4 — Threshold Engine (Docker) This Space implements a **decision-facing threshold layer** on top of the FDRSM series. \[ \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 a clean PDF report. """ CSS = """ .gradio-container {max-width: 1180px !important;} .small {font-size: 12px; opacity: 0.85;} """ with gr.Blocks(theme=THEME, css=CSS, title="FDRSM-4 — Threshold Engine") as demo: gr.Markdown(HEADER_MD) 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="PDF report") 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()