| 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(): |
| |
| 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}") |
|
|
| |
| 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 |
| 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 |
|
|
| |
| 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)) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|
| |
| 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() |