abdessamad-bourkibate commited on
Commit
63584f1
·
verified ·
1 Parent(s): dd52d35

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +553 -194
main.py CHANGED
@@ -1,13 +1,12 @@
1
- # ==========================================================
2
- # main.py FDRSM-4 Threshold Engine
3
- # Clean UI + Robust 8-page PDF (NO broken equations, NO blank pages)
4
- # ==========================================================
5
 
6
  import io
7
  import datetime
8
- from typing import Dict, Any, List
9
 
10
  import numpy as np
 
11
  import matplotlib
12
  matplotlib.use("Agg")
13
  import matplotlib.pyplot as plt
@@ -20,266 +19,626 @@ from reportlab.lib.units import cm
20
  from reportlab.lib.utils import ImageReader
21
  from reportlab.pdfbase import pdfmetrics
22
  from reportlab.pdfbase.ttfonts import TTFont
 
23
 
24
  from engine.validation import validate_inputs
25
- from engine.thresholds import (
26
- compute_diagnostics,
27
- interpret_governance,
28
- euler_simulation,
29
- )
30
 
31
- # ==========================================================
32
- # Metadata
33
- # ==========================================================
34
- RIGHTS_HOLDER_LINE = "Abdessamad Bourkibate — Apache-2.0"
35
- REPORT_TITLE = "FDRSM-4 — Threshold Engine Report"
36
 
37
  # ==========================================================
38
- # PDF helpers
39
  # ==========================================================
40
- def register_font() -> str:
41
- for p in [
42
  "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
43
  "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf",
44
- ]:
 
 
45
  try:
46
  pdfmetrics.registerFont(TTFont("UFont", p))
47
  return "UFont"
48
  except Exception:
49
- pass
50
  return "Helvetica"
51
 
52
 
53
- def fig_to_img(fig) -> ImageReader:
54
  buf = io.BytesIO()
55
- fig.savefig(buf, format="png", dpi=420, bbox_inches="tight")
56
  buf.seek(0)
57
- return ImageReader(buf)
58
-
59
-
60
- def draw_paragraph(c, text, x, y, w, font, size, leading=14):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  c.setFont(font, size)
62
- for line in text.split("\n"):
63
- c.drawString(x, y, line)
64
- y -= leading
 
 
 
 
65
  return y
66
 
67
 
68
- def header(c, font):
69
- W, H = A4
70
- c.setFont(font, 14)
71
- c.drawString(2*cm, H-2*cm, REPORT_TITLE)
 
 
 
 
 
 
 
 
 
 
 
 
72
  c.setFont(font, 9)
73
- c.drawRightString(
74
- W-2*cm,
75
- H-2*cm,
76
- f"Generated: {datetime.date.today().isoformat()}",
77
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
 
 
 
 
 
 
 
79
 
80
- def footer(c, page):
81
- W, _ = A4
82
- c.setFont("Helvetica", 8.5)
83
- c.drawString(2*cm, 1.2*cm, RIGHTS_HOLDER_LINE)
84
- c.drawRightString(W-2*cm, 1.2*cm, f"Page {page}")
85
 
86
 
87
  # ==========================================================
88
- # PDF generator (8 pages guaranteed)
89
  # ==========================================================
90
- def make_pdf(params, diag, gov, t, R) -> str:
91
- font = register_font()
92
- out = "/tmp/FDRSM4_Extended_Report.pdf"
93
- c = canvas.Canvas(out, pagesize=A4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  W, H = A4
95
- x = 2*cm
96
- w = W - 4*cm
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  page = 1
99
 
100
- # -------- Page 1: Cover --------
101
- header(c, font)
102
- y = H - 4*cm
103
- c.setFont(font, 18)
104
- c.drawString(x, y, "Decision-facing stability diagnostics")
105
- y -= 1.5*cm
106
- y = draw_paragraph(
107
- c,
108
- "This report presents a formal threshold-based interpretation\n"
109
- "of authority–dependency stability in family digital risk systems.\n\n"
110
- "This analysis is structural and analytical:\n"
111
- "- No behavioral profiling\n"
112
- "- No content moderation\n"
113
- "- No surveillance tooling",
114
- x, y, w, font, 11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  )
116
- footer(c, page)
 
 
117
  c.showPage()
118
  page += 1
119
 
120
- # -------- Page 2: Executive summary --------
121
- header(c, font)
122
- y = H - 4*cm
123
- y = draw_paragraph(
124
- c,
125
- f"Regime: {diag['regime']}\n"
126
- f"Fragility: {diag['fragility']}\n"
127
- f"Boundary k*: {diag['boundary_k']:.6f}\n"
128
- f"Margin (k k*): {diag['margin_to_boundary']:.6f}\n\n"
129
- f"Governance posture: {gov['posture']}\n"
130
- f"Decision guidance:\n{gov['decision']}\n\n"
131
- f"Risk note:\n{gov['risk']}",
132
- x, y, w, font, 11
 
 
 
 
 
 
 
 
 
 
133
  )
134
- footer(c, page)
 
 
135
  c.showPage()
136
  page += 1
137
 
138
- # -------- Page 3: Model definition --------
139
- header(c, font)
140
- y = H - 4*cm
141
- y = draw_paragraph(
142
- c,
143
- "Model definition:\n\n"
144
- "Baseline differential equation:\n"
145
- "dR/dt = (α·D − β·k) · R\n\n"
146
- "Definitions:\n"
147
- "λ = α·D β·k\n"
148
- "Δ = β·k α·D\n"
149
- "F = |Δ| / (|α·D| + |β·k| + ε)\n\n"
150
- "Stability conditions:\n"
151
- "- Stable if λ < 0\n"
152
- "- Near-boundary if |λ| ≤ tol\n"
153
- "- Unstable if λ > 0",
154
- x, y, w, font, 11
155
- )
156
- footer(c, page)
 
 
 
 
 
 
157
  c.showPage()
158
  page += 1
159
 
160
- # -------- Page 4: Risk trajectory --------
161
- header(c, font)
162
- fig1 = plt.figure(figsize=(6.5,4.5))
163
- plt.plot(t, R, linewidth=2.5)
164
- plt.xlabel("Time")
165
- plt.ylabel("Risk R(t)")
166
- plt.title("Risk trajectory")
167
- plt.grid(True)
168
- img1 = fig_to_img(fig1)
169
- plt.close(fig1)
170
- c.drawImage(img1, x, 4*cm, width=w, height=11*cm)
171
- footer(c, page)
 
 
 
 
 
172
  c.showPage()
173
  page += 1
174
 
175
- # -------- Page 5: Stability map --------
176
- header(c, font)
177
- D = params["D"]; k = params["k"]
178
- alpha = params["alpha"]; beta = params["beta"]
179
- Dg = np.linspace(0, max(10, D*2), 300)
180
- fig2 = plt.figure(figsize=(6.5,4.5))
181
- plt.plot(Dg, (alpha/beta)*Dg, "--", label="Boundary")
182
- plt.scatter([D],[k], s=80)
183
- plt.xlabel("D"); plt.ylabel("k")
184
- plt.title("Stability map")
185
- plt.grid(True); plt.legend()
186
- img2 = fig_to_img(fig2)
187
- plt.close(fig2)
188
- c.drawImage(img2, x, 4*cm, width=w, height=11*cm)
189
- footer(c, page)
 
190
  c.showPage()
191
  page += 1
192
 
193
- # -------- Page 6: Sensitivity --------
194
- header(c, font)
195
- y = H - 4*cm
196
- lines = ["Local sensitivity ranking of λ:"]
197
- for name, val in diag["sens_ranked"]:
198
- lines.append(f"{name}: {val:+.4f}")
199
- y = draw_paragraph(c, "\n".join(lines), x, y, w, font, 11)
200
- footer(c, page)
 
 
 
 
 
 
 
 
201
  c.showPage()
202
  page += 1
203
 
204
- # -------- Page 7: Governance interpretation --------
205
- header(c, font)
206
- y = H - 4*cm
207
- y = draw_paragraph(
208
- c,
209
- "Governance interpretation logic:\n\n"
210
- "The output maps formal stability diagnostics\n"
211
- "to proportional governance postures.\n\n"
212
- "This mapping is structural and does not\n"
213
- "infer individual intent or behavior.",
214
- x, y, w, font, 11
215
- )
216
- footer(c, page)
 
217
  c.showPage()
218
  page += 1
219
 
220
- # -------- Page 8: Appendix --------
221
- header(c, font)
222
- y = H - 4*cm
223
- y = draw_paragraph(
224
- c,
225
- "Appendix:\n\n"
226
- "This report is part of the FDRSM series:\n"
227
- "- FDRSM-1: Baseline stability\n"
228
- "- FDRSM-2: Sensitivity & fragility\n"
229
- "- FDRSM-3: Governance scenarios\n"
230
- "- FDRSM-4: Threshold engine\n\n"
231
- "Author:\n"
232
- "Abdessamad Bourkibate (Morocco)",
233
- x, y, w, font, 11
 
 
 
 
 
 
 
 
 
 
 
 
234
  )
235
- footer(c, page)
 
 
236
  c.save()
237
- return out
238
 
239
 
240
  # ==========================================================
241
- # Engine runner + UI
242
  # ==========================================================
243
  def run_engine(alpha, beta, D, k, R0, T, n, tol, eps):
244
  v = validate_inputs(alpha, beta, D, k, R0, T, n, tol, eps)
245
  if not v["ok"]:
246
- return None, None, "Input error:\n" + "\n".join(v["errors"])
 
247
 
248
  diag = compute_diagnostics(alpha, beta, D, k, tol, eps)
249
- gov = interpret_governance(
250
- diag["regime"],
251
- diag["fragility"],
252
- diag["Delta"],
253
- diag["margin_to_boundary"],
254
- )
255
  t, R = euler_simulation(alpha, beta, D, k, R0, T, int(n))
256
- params = dict(alpha=alpha, beta=beta, D=D, k=k, R0=R0, T=T, n=n, tol=tol, eps=eps)
257
 
258
- pdf = make_pdf(params, diag, gov, t, R)
 
 
 
259
 
260
- fig = plt.figure(figsize=(6,4))
261
- plt.plot(t,R); plt.grid(True)
262
- return fig, pdf, f"Regime: {diag['regime']} | Fragility: {diag['fragility']}"
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- with gr.Blocks(title="FDRSM-4 — Threshold Engine") as demo:
266
- gr.Markdown("## FDRSM-4 — Threshold Engine")
267
- with gr.Row():
268
- a = gr.Slider(0.1,5,1,label="α")
269
- b = gr.Slider(0.1,5,1,label="β")
270
- D = gr.Slider(0,10,3,label="D")
271
- k = gr.Slider(0,10,3.2,label="k")
272
  with gr.Row():
273
- R0 = gr.Slider(0.01,5,1,label="R(0)")
274
- T = gr.Slider(5,80,25,label="T")
275
- n = gr.Slider(200,4000,1200,label="steps")
276
- tol = gr.Slider(0,0.2,0.01,label="tol")
277
- eps = gr.Number(1e-12,label="ε")
278
- btn = gr.Button("Run analysis")
279
- plot = gr.Plot()
280
- file = gr.File()
281
- txt = gr.Textbox(lines=2)
282
-
283
- btn.click(run_engine,[a,b,D,k,R0,T,n,tol,eps],[plot,file,txt])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  demo.launch()
 
1
+ # main.py — FDRSM-4 Threshold Engine (App)
2
+ # Clean LaTeX rendering + 8-page PDF report (no overlap)
 
 
3
 
4
  import io
5
  import datetime
6
+ from typing import Dict, Any, List, Tuple
7
 
8
  import numpy as np
9
+
10
  import matplotlib
11
  matplotlib.use("Agg")
12
  import matplotlib.pyplot as plt
 
19
  from reportlab.lib.utils import ImageReader
20
  from reportlab.pdfbase import pdfmetrics
21
  from reportlab.pdfbase.ttfonts import TTFont
22
+ from reportlab.lib import colors
23
 
24
  from engine.validation import validate_inputs
25
+ from engine.thresholds import compute_diagnostics, interpret_governance, euler_simulation
26
+
27
+
28
+ RIGHTS_HOLDER_LINE = "Rights holder: Abdessamad Bourkibate (Morocco) — Apache-2.0"
 
29
 
 
 
 
 
 
30
 
31
  # ==========================================================
32
+ # PDF typography + layout helpers
33
  # ==========================================================
34
+ def _register_pdf_font() -> str:
35
+ candidates = [
36
  "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
37
  "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf",
38
+ "/usr/share/fonts/truetype/freefont/FreeSans.ttf",
39
+ ]
40
+ for p in candidates:
41
  try:
42
  pdfmetrics.registerFont(TTFont("UFont", p))
43
  return "UFont"
44
  except Exception:
45
+ continue
46
  return "Helvetica"
47
 
48
 
49
+ def _fig_to_png_bytes(fig, dpi=420) -> io.BytesIO:
50
  buf = io.BytesIO()
51
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
52
  buf.seek(0)
53
+ return buf
54
+
55
+
56
+ def _wrap_lines(font_name: str, font_size: int, text: str, max_width_pt: float) -> List[str]:
57
+ words = (text or "").replace("\r", "").split()
58
+ if not words:
59
+ return [""]
60
+ lines, cur = [], words[0]
61
+ for w in words[1:]:
62
+ trial = cur + " " + w
63
+ if pdfmetrics.stringWidth(trial, font_name, font_size) <= max_width_pt:
64
+ cur = trial
65
+ else:
66
+ lines.append(cur)
67
+ cur = w
68
+ lines.append(cur)
69
+ return lines
70
+
71
+
72
+ def _draw_wrapped(c: canvas.Canvas, x: float, y: float, font: str, size: int,
73
+ text: str, max_w: float, leading: float = 13.0) -> float:
74
  c.setFont(font, size)
75
+ for para in (text or "").split("\n"):
76
+ if para.strip() == "":
77
+ y -= leading
78
+ continue
79
+ for ln in _wrap_lines(font, size, para, max_w):
80
+ c.drawString(x, y, ln)
81
+ y -= leading
82
  return y
83
 
84
 
85
+ def _section_bar(c: canvas.Canvas, x: float, y: float, w: float, title: str,
86
+ font: str, bg=(0.07, 0.19, 0.22), fg=(0.97, 0.99, 0.99)) -> float:
87
+ c.setFillColorRGB(*bg)
88
+ c.roundRect(x, y - 16, w, 20, 6, fill=1, stroke=0)
89
+ c.setFillColorRGB(*fg)
90
+ c.setFont(font, 11)
91
+ c.drawString(x + 10, y - 3, title)
92
+ return y - 28
93
+
94
+
95
+ def _kpi_box(c: canvas.Canvas, x: float, y: float, w: float, h: float,
96
+ label: str, value: str, font: str):
97
+ c.setFillColorRGB(0.96, 0.98, 0.99)
98
+ c.setStrokeColorRGB(0.85, 0.90, 0.93)
99
+ c.roundRect(x, y - h, w, h, 10, fill=1, stroke=1)
100
+ c.setFillColorRGB(0.12, 0.12, 0.12)
101
  c.setFont(font, 9)
102
+ c.drawString(x + 10, y - 16, label)
103
+ c.setFont(font, 12)
104
+ c.drawString(x + 10, y - 36, value)
105
+
106
+
107
+ def _draw_table(c: canvas.Canvas, x: float, y: float, col_w: List[float], rows: List[List[str]],
108
+ font: str, header: bool = True, row_h: float = 18.0) -> float:
109
+ # Simple clean table with wrapping
110
+ total_w = sum(col_w)
111
+ nrows = len(rows)
112
+ cur_y = y
113
+
114
+ # Header style
115
+ for r in range(nrows):
116
+ is_header = header and (r == 0)
117
+ bg = (0.92, 0.96, 0.97) if is_header else (1, 1, 1)
118
+ c.setFillColorRGB(*bg)
119
+ c.setStrokeColorRGB(0.84, 0.88, 0.90)
120
+ c.rect(x, cur_y - row_h, total_w, row_h, fill=1, stroke=1)
121
+
122
+ cx = x
123
+ for j, cell in enumerate(rows[r]):
124
+ c.setStrokeColorRGB(0.84, 0.88, 0.90)
125
+ c.rect(cx, cur_y - row_h, col_w[j], row_h, fill=0, stroke=1)
126
 
127
+ c.setFillColorRGB(0.10, 0.10, 0.10)
128
+ c.setFont(font, 9 if not is_header else 9.5)
129
+ max_w = col_w[j] - 10
130
+ lines = _wrap_lines(font, 9 if not is_header else 9.5, str(cell), max_w)
131
+ # draw first line only (tight layout); keep table clean
132
+ c.drawString(cx + 6, cur_y - 13, lines[0][:140])
133
+ cx += col_w[j]
134
 
135
+ cur_y -= row_h
136
+
137
+ return cur_y - 8
 
 
138
 
139
 
140
  # ==========================================================
141
+ # Additional analytics for "comprehensive" report
142
  # ==========================================================
143
+ def _parameter_sweep(alpha: float, beta: float, D: float, k: float, tol: float, eps: float,
144
+ D_span=(0, 10), k_span=(0, 10), n=90) -> Dict[str, Any]:
145
+ Dg = np.linspace(D_span[0], D_span[1], n)
146
+ kg = np.linspace(k_span[0], k_span[1], n)
147
+ Lam = np.zeros((n, n), dtype=float)
148
+ F = np.zeros((n, n), dtype=float)
149
+
150
+ for i, Di in enumerate(Dg):
151
+ for j, kj in enumerate(kg):
152
+ lam = (alpha * Di) - (beta * kj)
153
+ Delta = (beta * kj) - (alpha * Di)
154
+ denom = abs(alpha * Di) + abs(beta * kj) + eps
155
+ frag = abs(Delta) / denom
156
+ Lam[i, j] = lam
157
+ F[i, j] = frag
158
+
159
+ # stable mask for visualization
160
+ stable = Lam < -tol
161
+ near = np.abs(Lam) <= tol
162
+ unstable = Lam > tol
163
+
164
+ return {"Dg": Dg, "kg": kg, "Lam": Lam, "F": F, "stable": stable, "near": near, "unstable": unstable}
165
+
166
+
167
+ def _make_figures(diag: Dict[str, Any], params: Dict[str, Any], t: np.ndarray, R: np.ndarray) -> Dict[str, ImageReader]:
168
+ alpha = float(params["alpha"]); beta = float(params["beta"])
169
+ D = float(params["D"]); k = float(params["k"])
170
+ tol = float(params["tol"]); eps = float(params["eps"])
171
+
172
+ # Shared rc
173
+ plt.rcParams.update({
174
+ "font.size": 10,
175
+ "axes.titlesize": 11,
176
+ "axes.labelsize": 10,
177
+ "axes.spines.top": False,
178
+ "axes.spines.right": False,
179
+ "grid.alpha": 0.25,
180
+ })
181
+
182
+ # 1) Risk trajectory
183
+ fig1 = plt.figure(figsize=(6.6, 4.2))
184
+ plt.plot(t, R, linewidth=2.6)
185
+ plt.xlabel("Time t")
186
+ plt.ylabel("Risk R(t)")
187
+ plt.title("Risk trajectory (Euler integration)")
188
+ plt.grid(True)
189
+ img1 = ImageReader(_fig_to_png_bytes(fig1, dpi=440))
190
+ plt.close(fig1)
191
+
192
+ # 2) Stability map
193
+ fig2 = plt.figure(figsize=(6.6, 4.2))
194
+ Dmax = max(10.0, D * 2.0 + 1.0)
195
+ Dg = np.linspace(0, Dmax, 420)
196
+ k_line = (alpha / beta) * Dg if beta != 0 else np.full_like(Dg, np.nan)
197
+ plt.plot(Dg, k_line, linestyle="--", linewidth=2.2, label="Boundary: k = (α/β)·D")
198
+ plt.scatter([D], [k], s=95, label="Current point (D,k)")
199
+ plt.xlabel("Dependency intensity D")
200
+ plt.ylabel("Authority gain k")
201
+ plt.title("Stability map (stable region above boundary)")
202
+ plt.grid(True)
203
+ plt.legend()
204
+ img2 = ImageReader(_fig_to_png_bytes(fig2, dpi=440))
205
+ plt.close(fig2)
206
+
207
+ # 3) Sensitivity bar
208
+ fig3 = plt.figure(figsize=(6.6, 4.2))
209
+ ranked = diag["sens_ranked"]
210
+ labels = [a for a, _ in ranked][::-1]
211
+ vals = [abs(v) for _, v in ranked][::-1]
212
+ plt.barh(labels, vals)
213
+ plt.xlabel("|sensitivity|")
214
+ plt.title("Local sensitivity ranking of λ")
215
+ plt.grid(True, axis="x")
216
+ img3 = ImageReader(_fig_to_png_bytes(fig3, dpi=440))
217
+ plt.close(fig3)
218
+
219
+ # 4) Sweep heatmap (regime regions)
220
+ sweep = _parameter_sweep(alpha, beta, D, k, tol, eps, n=95)
221
+ fig4 = plt.figure(figsize=(6.6, 4.8))
222
+ # regime map: 0 stable, 1 near, 2 unstable
223
+ Z = np.zeros_like(sweep["Lam"])
224
+ Z[sweep["near"]] = 1
225
+ Z[sweep["unstable"]] = 2
226
+ plt.imshow(
227
+ Z.T,
228
+ origin="lower",
229
+ aspect="auto",
230
+ extent=[sweep["Dg"][0], sweep["Dg"][-1], sweep["kg"][0], sweep["kg"][-1]]
231
+ )
232
+ plt.plot(sweep["Dg"], (alpha / beta) * sweep["Dg"] if beta != 0 else np.nan, linestyle="--", linewidth=1.8)
233
+ plt.scatter([D], [k], s=70)
234
+ plt.xlabel("D")
235
+ plt.ylabel("k")
236
+ plt.title("Regime map over (D,k): stable / near-boundary / unstable")
237
+ img4 = ImageReader(_fig_to_png_bytes(fig4, dpi=440))
238
+ plt.close(fig4)
239
+
240
+ # 5) Fragility proximity heatmap (F)
241
+ fig5 = plt.figure(figsize=(6.6, 4.8))
242
+ plt.imshow(
243
+ sweep["F"].T,
244
+ origin="lower",
245
+ aspect="auto",
246
+ extent=[sweep["Dg"][0], sweep["Dg"][-1], sweep["kg"][0], sweep["kg"][-1]]
247
+ )
248
+ plt.plot(sweep["Dg"], (alpha / beta) * sweep["Dg"] if beta != 0 else np.nan, linestyle="--", linewidth=1.8)
249
+ plt.scatter([D], [k], s=70)
250
+ plt.xlabel("D")
251
+ plt.ylabel("k")
252
+ plt.title("Fragility proximity surface F over (D,k)")
253
+ img5 = ImageReader(_fig_to_png_bytes(fig5, dpi=440))
254
+ plt.close(fig5)
255
+
256
+ return {"traj": img1, "map": img2, "sens": img3, "regmap": img4, "fragmap": img5}
257
+
258
+
259
+ # ==========================================================
260
+ # 8-page PDF report
261
+ # ==========================================================
262
+ def make_pdf(
263
+ title: str,
264
+ params: Dict[str, Any],
265
+ diag: Dict[str, Any],
266
+ gov: Dict[str, str],
267
+ t: np.ndarray,
268
+ R: np.ndarray,
269
+ figures: Dict[str, ImageReader],
270
+ ) -> str:
271
+ font = _register_pdf_font()
272
+ out_path = "/tmp/FDRSM4_Threshold_Report_Extended.pdf"
273
+ c = canvas.Canvas(out_path, pagesize=A4)
274
  W, H = A4
275
+ mx = 2.0 * cm
276
+ max_w = W - 4.0 * cm
277
+
278
+ def header(subtitle: str = ""):
279
+ # Calm psych-friendly palette
280
+ c.setFillColorRGB(0.07, 0.19, 0.22)
281
+ c.rect(0, H - 2.2 * cm, W, 2.2 * cm, fill=1, stroke=0)
282
+ c.setFillColorRGB(0.97, 0.99, 0.99)
283
+ c.setFont(font, 14)
284
+ c.drawString(mx, H - 1.25 * cm, (title or "FDRSM-4 Report")[:110])
285
+ c.setFont(font, 9.5)
286
+ c.drawString(mx, H - 1.85 * cm, f"Generated: {datetime.date.today().isoformat()}")
287
+ if subtitle:
288
+ c.setFont(font, 9.5)
289
+ c.drawRightString(W - mx, H - 1.85 * cm, subtitle[:90])
290
+
291
+ def footer(page_no: int):
292
+ c.setFillColorRGB(0.35, 0.40, 0.48)
293
+ c.setFont(font, 9)
294
+ c.drawString(mx, 1.1 * cm, RIGHTS_HOLDER_LINE)
295
+ c.drawRightString(W - mx, 1.1 * cm, f"Page {page_no}")
296
 
297
  page = 1
298
 
299
+ # ---------------- Page 1: Cover ----------------
300
+ header("Extended Report")
301
+ y = H - 3.2 * cm
302
+
303
+ c.setFillColorRGB(0.10, 0.10, 0.10)
304
+ c.setFont(font, 20)
305
+ c.drawString(mx, y, "FDRSM-4 — Threshold Engine")
306
+ y -= 1.1 * cm
307
+
308
+ c.setFont(font, 11)
309
+ y = _draw_wrapped(
310
+ c, mx, y, font, 11,
311
+ "A decision-facing diagnostic report for authority–dependency stability in family digital risk systems.\n"
312
+ "This report is structural and analytical: no content moderation, no behavioral profiling, no surveillance tooling.",
313
+ max_w, leading=15
314
+ )
315
+ y -= 0.2 * cm
316
+
317
+ # KPI row
318
+ _kpi_box(c, mx, y, (max_w/3)-8, 55, "Regime", str(diag["regime"]), font)
319
+ _kpi_box(c, mx + (max_w/3), y, (max_w/3)-8, 55, "Fragility", str(diag["fragility"]), font)
320
+ _kpi_box(c, mx + 2*(max_w/3), y, (max_w/3)-8, 55, "Margin to boundary", f"{diag['margin_to_boundary']:.6f}", font)
321
+ y -= 2.0 * cm
322
+
323
+ y = _section_bar(c, mx, y, max_w, "Parameter snapshot", font)
324
+ snap = (
325
+ f"α={float(params['alpha']):g}, β={float(params['beta']):g}, D={float(params['D']):g}, k={float(params['k']):g}\n"
326
+ 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"
327
+ f"λ={diag['lambda']:.6f}, Δ={diag['Delta']:.6f}, F={diag['F']:.6f}"
328
  )
329
+ y = _draw_wrapped(c, mx, y, font, 10, snap, max_w, leading=13)
330
+
331
+ footer(page)
332
  c.showPage()
333
  page += 1
334
 
335
+ # ---------------- Page 2: Executive summary ----------------
336
+ header("Executive Summary")
337
+ y = H - 3.0 * cm
338
+
339
+ y = _section_bar(c, mx, y, max_w, "Summary", font)
340
+ summary = (
341
+ f"Regime classification: {diag['regime']}\n"
342
+ f"Fragility class: {diag['fragility']}\n"
343
+ f"Boundary point: k* = (α/β)·D = {diag['boundary_k']:.6f}\n"
344
+ f"Distance to boundary: k − k* = {diag['margin_to_boundary']:.6f}\n\n"
345
+ "Interpretation (decision-facing):\n"
346
+ f"- Posture: {gov['posture']}\n"
347
+ f"- Decision: {gov['decision']}\n"
348
+ f"- Risk note: {gov['risk']}\n"
349
+ )
350
+ y = _draw_wrapped(c, mx, y, font, 10, summary, max_w, leading=13)
351
+
352
+ y -= 0.3 * cm
353
+ y = _section_bar(c, mx, y, max_w, "What this report is / is not", font)
354
+ scope = (
355
+ "This report provides a structural diagnostic of stability conditions in a formal model.\n"
356
+ "It does NOT infer individual behavior, does NOT profile users, and does NOT implement monitoring or surveillance.\n"
357
+ "Its purpose is reproducible interpretation for governance, policy, and research discussion."
358
  )
359
+ y = _draw_wrapped(c, mx, y, font, 10, scope, max_w, leading=13)
360
+
361
+ footer(page)
362
  c.showPage()
363
  page += 1
364
 
365
+ # ---------------- Page 3: Diagnostics table ----------------
366
+ header("Diagnostics")
367
+ y = H - 3.0 * cm
368
+
369
+ y = _section_bar(c, mx, y, max_w, "Formal diagnostics table", font)
370
+ rows = [
371
+ ["Metric", "Value", "Meaning"],
372
+ ["λ = αD − βk", f"{diag['lambda']:.6f}", "Stability rate (negative → decay; positive → escalation)"],
373
+ ["Δ = βk − αD", f"{diag['Delta']:.6f}", "Signed stability margin (positive → stable margin)"],
374
+ ["F", f"{diag['F']:.6f}", "Normalized proximity (smaller → closer to boundary)"],
375
+ ["k* = (α/β)D", f"{diag['boundary_k']:.6f}", "Boundary authority required for stability"],
376
+ ["k k*", f"{diag['margin_to_boundary']:.6f}", "Distance to boundary (robustness indicator)"],
377
+ ["Regime", str(diag["regime"]), "Stable / Near-boundary / Unstable"],
378
+ ["Fragility", str(diag["fragility"]), "Low / Moderate / High (based on F)"],
379
+ ]
380
+ col_w = [4.0*cm, 4.0*cm, max_w - 8.0*cm]
381
+ y = _draw_table(c, mx, y, col_w, rows, font, header=True, row_h=19)
382
+
383
+ y = _section_bar(c, mx, y, max_w, "Local sensitivities of λ", font)
384
+ sens_rows = [["Sensitivity term", "Value (signed)", "Impact intuition"]]
385
+ for name, val in diag["sens_ranked"]:
386
+ sens_rows.append([name, f"{val:+.6f}", "Higher magnitude → stronger local influence on regime switching"])
387
+ 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)
388
+
389
+ footer(page)
390
  c.showPage()
391
  page += 1
392
 
393
+ # ---------------- Page 4: Figure 1 (trajectory) ----------------
394
+ header("Figures")
395
+ y = H - 3.0 * cm
396
+ y = _section_bar(c, mx, y, max_w, "Figure 1 — Risk trajectory", font)
397
+
398
+ img_w = max_w
399
+ img_h = 13.2 * cm
400
+ c.drawImage(figures["traj"], mx, y - img_h, width=img_w, height=img_h, mask="auto")
401
+
402
+ y -= img_h + 0.6 * cm
403
+ caption = (
404
+ "Caption: Euler integration of the baseline dynamic. A decaying trajectory indicates stability; "
405
+ "growth indicates escalation. The shape reflects the sign and magnitude of λ."
406
+ )
407
+ y = _draw_wrapped(c, mx, y, font, 10, caption, max_w, leading=13)
408
+
409
+ footer(page)
410
  c.showPage()
411
  page += 1
412
 
413
+ # ---------------- Page 5: Figure 2 (stability map) ----------------
414
+ header("Figures")
415
+ y = H - 3.0 * cm
416
+ y = _section_bar(c, mx, y, max_w, "Figure 2 Stability map", font)
417
+
418
+ img_h2 = 13.2 * cm
419
+ c.drawImage(figures["map"], mx, y - img_h2, width=img_w, height=img_h2, mask="auto")
420
+
421
+ y -= img_h2 + 0.6 * cm
422
+ caption2 = (
423
+ "Caption: Stability boundary k*=(α/β)D. Points above the line (higher k for given D) are stable. "
424
+ "Near the line: small drift can flip the regime."
425
+ )
426
+ y = _draw_wrapped(c, mx, y, font, 10, caption2, max_w, leading=13)
427
+
428
+ footer(page)
429
  c.showPage()
430
  page += 1
431
 
432
+ # ---------------- Page 6: Figure 3 (sensitivity) ----------------
433
+ header("Figures")
434
+ y = H - 3.0 * cm
435
+ y = _section_bar(c, mx, y, max_w, "Figure 3 Sensitivity ranking", font)
436
+
437
+ img_h3 = 13.2 * cm
438
+ c.drawImage(figures["sens"], mx, y - img_h3, width=img_w, height=img_h3, mask="auto")
439
+
440
+ y -= img_h3 + 0.6 * cm
441
+ caption3 = (
442
+ "Caption: Local derivatives indicate which parameter perturbations most strongly shift λ. "
443
+ "This is a local diagnostic: it explains directionally which knobs are most influential around the current point."
444
+ )
445
+ y = _draw_wrapped(c, mx, y, font, 10, caption3, max_w, leading=13)
446
+
447
+ footer(page)
448
  c.showPage()
449
  page += 1
450
 
451
+ # ---------------- Page 7: Regime map sweep ----------------
452
+ header("Stress view")
453
+ y = H - 3.0 * cm
454
+ y = _section_bar(c, mx, y, max_w, "Figure 4 — Regime map over (D,k)", font)
455
+
456
+ img_h4 = 11.0 * cm
457
+ c.drawImage(figures["regmap"], mx, y - img_h4, width=img_w, height=img_h4, mask="auto")
458
+ y -= img_h4 + 0.5 * cm
459
+
460
+ y = _section_bar(c, mx, y, max_w, "Figure 5 Fragility surface F over (D,k)", font)
461
+ img_h5 = 11.0 * cm
462
+ c.drawImage(figures["fragmap"], mx, y - img_h5, width=img_w, height=img_h5, mask="auto")
463
+
464
+ footer(page)
465
  c.showPage()
466
  page += 1
467
 
468
+ # ---------------- Page 8: Appendix (formulas + governance memo) ----------------
469
+ header("Appendix")
470
+ y = H - 3.0 * cm
471
+
472
+ y = _section_bar(c, mx, y, max_w, "Appendix A — Model definitions", font)
473
+ appA = (
474
+ "Baseline:\n"
475
+ " dR/dt = (αD − βk)R\n\n"
476
+ "Definitions:\n"
477
+ " λ = αD − βk\n"
478
+ " Δ = βk − αD\n"
479
+ " F = |Δ| / (|αD| + |βk| + ε)\n\n"
480
+ "Regime rules:\n"
481
+ " Stable if λ < −tol\n"
482
+ " Near-boundary if |λ| ≤ tol\n"
483
+ " Unstable if λ > tol\n"
484
+ )
485
+ y = _draw_wrapped(c, mx, y, font, 10, appA, max_w, leading=13)
486
+
487
+ y -= 0.2 * cm
488
+ y = _section_bar(c, mx, y, max_w, "Appendix B — Governance interpretation output", font)
489
+ appB = (
490
+ f"Posture: {gov['posture']}\n"
491
+ f"Decision: {gov['decision']}\n"
492
+ f"Risk note: {gov['risk']}\n\n"
493
+ "Note: The interpretation is structural. It maps regime/fragility to governance stance without behavioral inference."
494
  )
495
+ y = _draw_wrapped(c, mx, y, font, 10, appB, max_w, leading=13)
496
+
497
+ footer(page)
498
  c.save()
499
+ return out_path
500
 
501
 
502
  # ==========================================================
503
+ # Engine runner
504
  # ==========================================================
505
  def run_engine(alpha, beta, D, k, R0, T, n, tol, eps):
506
  v = validate_inputs(alpha, beta, D, k, R0, T, n, tol, eps)
507
  if not v["ok"]:
508
+ msg = "Input errors:\n- " + "\n- ".join(v["errors"])
509
+ return None, None, None, msg, None
510
 
511
  diag = compute_diagnostics(alpha, beta, D, k, tol, eps)
512
+ gov = interpret_governance(diag["regime"], diag["fragility"], diag["Delta"], diag["margin_to_boundary"])
 
 
 
 
 
513
  t, R = euler_simulation(alpha, beta, D, k, R0, T, int(n))
 
514
 
515
+ params = {
516
+ "alpha": float(alpha), "beta": float(beta), "D": float(D), "k": float(k),
517
+ "R0": float(R0), "T": float(T), "n": int(n), "tol": float(tol), "eps": float(eps)
518
+ }
519
 
520
+ figures = _make_figures(diag, params, t, R)
521
+ pdf_path = make_pdf("FDRSM-4 — Threshold Engine Report", params, diag, gov, t, R, figures)
 
522
 
523
+ # UI plots (2 only)
524
+ fig1 = plt.figure(figsize=(6.2, 4.0))
525
+ plt.plot(t, R, linewidth=2.4)
526
+ plt.xlabel("Time t")
527
+ plt.ylabel("Risk R(t)")
528
+ plt.title("Risk trajectory (Euler integration)")
529
+ plt.grid(True, alpha=0.25)
530
+
531
+ fig2 = plt.figure(figsize=(6.2, 4.0))
532
+ Dmax = max(10.0, float(D) * 2.0 + 1.0)
533
+ Dg = np.linspace(0, Dmax, 360)
534
+ k_line = (float(alpha) / float(beta)) * Dg
535
+ plt.plot(Dg, k_line, linestyle="--", linewidth=2.0, label="Boundary k = (α/β)·D")
536
+ plt.scatter([float(D)], [float(k)], s=80, label="Current (D,k)")
537
+ plt.xlabel("Dependency intensity D")
538
+ plt.ylabel("Authority gain k")
539
+ plt.title("Stability map (stable region above boundary)")
540
+ plt.grid(True, alpha=0.25)
541
+ plt.legend()
542
+
543
+ # Decision report text
544
+ sens_lines = ["Local sensitivity ranking of λ:"]
545
+ for name, val in diag["sens_ranked"]:
546
+ sens_lines.append(f"- {name}: {val:+.6f}")
547
+
548
+ report = (
549
+ f"Regime: {diag['regime']}\n"
550
+ f"Fragility: {diag['fragility']}\n\n"
551
+ f"λ = {diag['lambda']:.6f}\n"
552
+ f"Δ = {diag['Delta']:.6f}\n"
553
+ f"F = {diag['F']:.6f}\n"
554
+ f"k* = (α/β)D = {diag['boundary_k']:.6f}\n"
555
+ f"k − k* = {diag['margin_to_boundary']:.6f}\n\n"
556
+ f"Posture: {gov['posture']}\n"
557
+ f"Decision: {gov['decision']}\n"
558
+ f"Risk note: {gov['risk']}\n\n"
559
+ + "\n".join(sens_lines)
560
+ )
561
+
562
+ return fig1, fig2, pdf_path, report, diag
563
+
564
+
565
+ # ==========================================================
566
+ # UI (LaTeX fixed) — no Docker mention
567
+ # ==========================================================
568
+ THEME = gr.themes.Soft(
569
+ primary_hue="teal",
570
+ secondary_hue="blue",
571
+ neutral_hue="slate",
572
+ radius_size=gr.themes.sizes.radius_lg,
573
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
574
+ )
575
+
576
+ CSS = """
577
+ .gradio-container {max-width: 1180px !important;}
578
+ .small {font-size: 12px; opacity: 0.85;}
579
+ """
580
+
581
+ HEADER_MD = r"""
582
+ # FDRSM-4 — Threshold Engine
583
+
584
+ This Space implements a **decision-facing threshold layer** on top of the FDRSM series.
585
+
586
+ ## Model
587
+
588
+ $$
589
+ \dot{R}(t) = (\alpha D - \beta k)R(t)
590
+ $$
591
+
592
+ $$
593
+ \lambda = \alpha D - \beta k,\quad
594
+ \Delta = \beta k - \alpha D,\quad
595
+ F = \frac{|\Delta|}{|\alpha D| + |\beta k| + \varepsilon}
596
+ $$
597
+
598
+ **Outputs:** stability regime, fragility class, stability map, trajectory plot, and an extended PDF report (≈ 8 pages).
599
+ """
600
+
601
+ with gr.Blocks(theme=THEME, css=CSS, title="FDRSM-4 — Threshold Engine") as demo:
602
+ # ✅ FIX: Force LaTeX delimiters to render (solves broken formulas)
603
+ gr.Markdown(
604
+ HEADER_MD,
605
+ latex_delimiters=[
606
+ {"left": "$$", "right": "$$", "display": True},
607
+ {"left": "$", "right": "$", "display": False},
608
+ ],
609
+ )
610
 
 
 
 
 
 
 
 
611
  with gr.Row():
612
+ with gr.Column(scale=1):
613
+ alpha = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="α (dependency amplification)")
614
+ beta = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="β (authority damping)")
615
+ D = gr.Slider(0.0, 10.0, value=3.0, step=0.1, label="D (dependency intensity)")
616
+ k = gr.Slider(0.0, 10.0, value=3.2, step=0.1, label="k (authority gain)")
617
+
618
+ R0 = gr.Slider(0.01, 10.0, value=1.0, step=0.01, label="R(0)")
619
+ T = gr.Slider(1.0, 80.0, value=25.0, step=1.0, label="T (horizon)")
620
+ n = gr.Slider(200, 6000, value=1400, step=50, label="n (steps)")
621
+ tol = gr.Slider(0.0, 0.5, value=0.006, step=0.0005, label="tol (near-boundary)")
622
+ eps = gr.Number(value=1e-12, label="ε", precision=12)
623
+
624
+ run = gr.Button("Run Threshold Engine", variant="primary")
625
+
626
+ with gr.Column(scale=1):
627
+ p1 = gr.Plot(label="Risk trajectory")
628
+ p2 = gr.Plot(label="Stability map")
629
+ pdf = gr.File(label="Extended PDF report (≈ 8 pages)")
630
+ txt = gr.Textbox(label="Decision-facing report", lines=18)
631
+
632
+ diag_state = gr.State({})
633
+
634
+ def _run(alpha, beta, D, k, R0, T, n, tol, eps):
635
+ fig1, fig2, pdf_path, report, diag = run_engine(alpha, beta, D, k, R0, T, n, tol, eps)
636
+ return fig1, fig2, pdf_path, report, diag
637
+
638
+ run.click(
639
+ fn=_run,
640
+ inputs=[alpha, beta, D, k, R0, T, n, tol, eps],
641
+ outputs=[p1, p2, pdf, txt, diag_state]
642
+ )
643
 
644
  demo.launch()