| | 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() |