simplicio-prompt 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/adopters.md ADDED
@@ -0,0 +1,24 @@
1
+ # Adopters
2
+
3
+ Projects vendoring the yool/tuple/HAMT spec.
4
+
5
+ | Project | Repo | Status | Vendored Version |
6
+ |---|---|---|---|
7
+ | SendSprint | https://github.com/wesleysimplicio/SendSprint | planning | v0.2 |
8
+ | llm-project-mapper | https://github.com/wesleysimplicio/llm-project-mapper | planning | v0.2 |
9
+
10
+ ## How to vendor
11
+
12
+ 1. Copy `YOOL_TUPLE_HAMT.md` into your repo as `docs/YOOL_TUPLE_HAMT.md`.
13
+ 2. Pin the spec version in your repo's README (e.g., `Spec: simplicio-prompt v0.2`).
14
+ 3. Implement guardrails per §11 (CPU throttle + disk GC). MANDATORY.
15
+ 4. Add your project to the table above via PR.
16
+
17
+ ## Update protocol
18
+
19
+ When the spec changes upstream:
20
+
21
+ 1. Diff your vendored copy against the new canonical version.
22
+ 2. Bump your project's vendored-version pin.
23
+ 3. Run your own integration tests before merging.
24
+ 4. Update the Vendored Version column here.
@@ -0,0 +1,355 @@
1
+ """Generate the prompt benchmark PDF report.
2
+
3
+ Run with the bundled Codex Python when available:
4
+
5
+ python benchmarks/generate_prompt_benchmark_pdf.py
6
+
7
+ The script uses ReportLab if installed. It intentionally keeps the benchmark
8
+ figures in source control so the PDF can be regenerated exactly.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ import sys
15
+
16
+ from reportlab.graphics.shapes import Drawing, Rect, String
17
+ from reportlab.lib import colors
18
+ from reportlab.lib.pagesizes import A4
19
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
20
+ from reportlab.lib.units import cm
21
+ from reportlab.platypus import (
22
+ Paragraph,
23
+ SimpleDocTemplate,
24
+ Spacer,
25
+ Table,
26
+ TableStyle,
27
+ )
28
+
29
+ ROOT = Path(__file__).resolve().parents[1]
30
+ sys.path.insert(0, str(ROOT))
31
+
32
+ from benchmarks.prompt_vs_normal import token_model # noqa: E402
33
+
34
+ OUT = ROOT / "benchmarks" / "prompt_vs_normal_benchmark.pdf"
35
+
36
+ PROMPT = """Use o repo canonico https://github.com/wesleysimplicio/simplicio-prompt.
37
+ Leia antes de editar: YOOL_TUPLE_HAMT.md, kernel/yool_tuple_kernel.py,
38
+ guardrails/cpu_throttle.py, guardrails/disk_gc.py, examples/python/receipts.py
39
+ e scripts/build_hamt.py.
40
+
41
+ Ao receber "Implement X": decomponha em grafo Hilbert-indexed, crie tuple raiz,
42
+ use batch_spawn(depth, branching, compression_threshold) para 1.000.000+
43
+ subagents sem enumerar, execute work ativo com spawn_agent, roteie por out/in/rd,
44
+ route_packet e scan_index, aplique hookwall, compress_token e prune_idle, e use
45
+ LaneWorkerPool respeitando YOOL_TUPLE_* env vars.
46
+
47
+ Execute:
48
+ python kernel/yool_tuple_kernel.py
49
+
50
+ Responda SEMPRE exatamente neste formato (sem variações):
51
+ [Tuple Space Snapshot]
52
+ [Active Agents/Subagents]
53
+ [Total Agents/Subagents]
54
+ [Próximo Yool a executar]
55
+ [Resultado parcial]"""
56
+
57
+
58
+ def p(text: str, style: ParagraphStyle) -> Paragraph:
59
+ return Paragraph(text.replace("\n", "<br/>"), style)
60
+
61
+
62
+ def main() -> int:
63
+ styles = getSampleStyleSheet()
64
+ title = styles["Title"]
65
+ h2 = styles["Heading2"]
66
+ body = styles["BodyText"]
67
+ body.leading = 13
68
+ small = ParagraphStyle("small", parent=body, fontSize=8.5, leading=10.5)
69
+ code = ParagraphStyle(
70
+ "code",
71
+ parent=small,
72
+ fontName="Courier",
73
+ backColor=colors.HexColor("#F4F6F8"),
74
+ borderColor=colors.HexColor("#D7DEE8"),
75
+ borderWidth=0.5,
76
+ borderPadding=6,
77
+ leading=9.5,
78
+ )
79
+
80
+ doc = SimpleDocTemplate(
81
+ str(OUT),
82
+ pagesize=A4,
83
+ rightMargin=1.5 * cm,
84
+ leftMargin=1.5 * cm,
85
+ topMargin=1.4 * cm,
86
+ bottomMargin=1.4 * cm,
87
+ title="Yool Prompt Benchmark",
88
+ author="wesleysimplicio/simplicio-prompt",
89
+ )
90
+
91
+ story = [
92
+ Paragraph("Yool Prompt vs Normal Instruction Benchmark", title),
93
+ p(
94
+ "Repository: wesleysimplicio/simplicio-prompt<br/>Run date: 2026-05-21",
95
+ body,
96
+ ),
97
+ Spacer(1, 0.25 * cm),
98
+ Paragraph("Executive Summary", h2),
99
+ p(
100
+ "The Yool prompt is faster because it turns a generic instruction into an "
101
+ "execution protocol: lazy batch_spawn for scale, tuple-space routing, "
102
+ "LaneWorkerPool fan-out, receipts, hookwall checks, and explicit runtime "
103
+ "guardrails. The benchmark measured local runtime mechanics, not hosted LLM "
104
+ "provider behavior.",
105
+ body,
106
+ ),
107
+ ]
108
+
109
+ story.extend(
110
+ [
111
+ Spacer(1, 0.2 * cm),
112
+ _bar_chart(
113
+ "Speedup (x)",
114
+ [
115
+ ("Scale 131k vs 1M", 1397.0, colors.HexColor("#2B6CB0")),
116
+ ("Scale 262k vs 1M", 5902.0, colors.HexColor("#2B6CB0")),
117
+ ("Active 256 tasks", 6.37, colors.HexColor("#38A169")),
118
+ ("Active 512 tasks", 9.31, colors.HexColor("#38A169")),
119
+ ],
120
+ max_value=5902.0,
121
+ ),
122
+ ]
123
+ )
124
+
125
+ scale_table = Table(
126
+ [
127
+ ["Profile", "Represented agents", "Wall time", "Peak memory"],
128
+ ["Normal instruction, flat list", "131,072", "217.11 ms", "28,749.88 KiB"],
129
+ ["Yool prompt, lazy batch_spawn", "1,048,576", "0.16 ms", "6.32 KiB"],
130
+ ["Observed gain", "8x more agents", "1,397x faster", "4,547x less memory"],
131
+ ],
132
+ colWidths=[6.1 * cm, 3.8 * cm, 3.1 * cm, 4.1 * cm],
133
+ )
134
+ scale_table.setStyle(_table_style())
135
+ story.extend(
136
+ [Spacer(1, 0.2 * cm), Paragraph("Scale Representation", h2), scale_table]
137
+ )
138
+
139
+ larger_scale_table = Table(
140
+ [
141
+ ["Profile", "Represented agents", "Wall time", "Peak memory"],
142
+ ["Normal instruction, flat list", "262,144", "431.45 ms", "57,542.32 KiB"],
143
+ ["Yool prompt, lazy batch_spawn", "1,048,576", "0.07 ms", "6.39 KiB"],
144
+ ["Observed gain", "4x more agents", "5,902x faster", "9,000x less memory"],
145
+ ],
146
+ colWidths=[6.1 * cm, 3.8 * cm, 3.1 * cm, 4.1 * cm],
147
+ )
148
+ larger_scale_table.setStyle(_table_style())
149
+ story.extend([Spacer(1, 0.25 * cm), larger_scale_table])
150
+
151
+ story.extend(
152
+ [
153
+ Spacer(1, 0.25 * cm),
154
+ _bar_chart(
155
+ "Peak memory reduction for scale representation (x)",
156
+ [
157
+ ("131k flat vs 1M lazy", 4547.0, colors.HexColor("#805AD5")),
158
+ ("262k flat vs 1M lazy", 9000.0, colors.HexColor("#805AD5")),
159
+ ],
160
+ max_value=9000.0,
161
+ ),
162
+ ]
163
+ )
164
+
165
+ exec_table = Table(
166
+ [
167
+ ["Profile", "Tasks", "Wall time", "Throughput", "Peak memory"],
168
+ ["Normal sequential", "256", "603.98 ms", "423.9 tasks/s", "17.33 KiB"],
169
+ ["Yool lane fan-out", "256", "94.87 ms", "2,698.3 tasks/s", "879.82 KiB"],
170
+ [
171
+ "Observed gain",
172
+ "same",
173
+ "6.37x faster",
174
+ "6.37x higher",
175
+ "higher active overhead",
176
+ ],
177
+ ],
178
+ colWidths=[5.0 * cm, 2.0 * cm, 3.0 * cm, 3.6 * cm, 3.7 * cm],
179
+ )
180
+ exec_table.setStyle(_table_style())
181
+ story.extend([Spacer(1, 0.35 * cm), Paragraph("Active Execution", h2), exec_table])
182
+
183
+ exec_big_table = Table(
184
+ [
185
+ ["Profile", "Tasks", "Wall time", "Throughput", "Peak memory"],
186
+ ["Normal sequential", "512", "1,212.88 ms", "422.1 tasks/s", "33.55 KiB"],
187
+ [
188
+ "Yool lane fan-out",
189
+ "512",
190
+ "130.28 ms",
191
+ "3,929.9 tasks/s",
192
+ "1,739.41 KiB",
193
+ ],
194
+ [
195
+ "Observed gain",
196
+ "same",
197
+ "9.31x faster",
198
+ "9.31x higher",
199
+ "higher active overhead",
200
+ ],
201
+ ],
202
+ colWidths=[5.0 * cm, 2.0 * cm, 3.0 * cm, 3.6 * cm, 3.7 * cm],
203
+ )
204
+ exec_big_table.setStyle(_table_style())
205
+ story.extend([Spacer(1, 0.25 * cm), exec_big_table])
206
+
207
+ token_rows = token_model()
208
+ token_table_data = [
209
+ ["Scenario", "Normal tokens", "Yool tokens", "Savings", "Savings %"]
210
+ ]
211
+ token_chart_rows = []
212
+ for item in token_rows:
213
+ token_table_data.append(
214
+ [
215
+ item.scenario,
216
+ f"{item.normal_tokens:,}",
217
+ f"{item.yool_tokens:,}",
218
+ f"{item.savings_tokens:,}",
219
+ f"{item.savings_pct:.2f}%",
220
+ ]
221
+ )
222
+ if item.savings_pct > 0:
223
+ token_chart_rows.append(
224
+ (
225
+ item.scenario.replace("_", " "),
226
+ item.savings_pct,
227
+ colors.HexColor("#DD6B20"),
228
+ )
229
+ )
230
+ token_table = Table(
231
+ token_table_data,
232
+ colWidths=[5.2 * cm, 3.0 * cm, 3.0 * cm, 3.0 * cm, 2.4 * cm],
233
+ )
234
+ token_table.setStyle(_table_style())
235
+ story.extend(
236
+ [
237
+ Spacer(1, 0.35 * cm),
238
+ Paragraph("Token Usage and Estimated Savings", h2),
239
+ p(
240
+ "Token numbers are deterministic local estimates using ceil(UTF-8 bytes / 4). "
241
+ "They are not provider billing measurements. The model compares repeated "
242
+ "chat-context orchestration against one prompt plus compact tuple envelopes.",
243
+ body,
244
+ ),
245
+ token_table,
246
+ Spacer(1, 0.2 * cm),
247
+ _bar_chart(
248
+ "Estimated token savings (%)", token_chart_rows, max_value=100.0
249
+ ),
250
+ p(
251
+ "One-off bootstrap is intentionally more expensive with the Yool prompt, "
252
+ "because it carries the execution protocol. Savings appear when work fans out: "
253
+ "the protocol is paid once and subtasks travel as compact tuple envelopes.",
254
+ body,
255
+ ),
256
+ ]
257
+ )
258
+
259
+ story.extend(
260
+ [
261
+ Spacer(1, 0.35 * cm),
262
+ Paragraph("Gains Beyond Speed", h2),
263
+ p(
264
+ "Scalability: million-agent trees without flat lists.<br/>"
265
+ "Auditability: tuple state plus receipts.<br/>"
266
+ "Recovery: pending work can be replayed from tuple state.<br/>"
267
+ "Failure isolation: lanes separate planning, build, test, review, and runtime work.<br/>"
268
+ "Guardrails: CPU, queue, compression, hookwall, and disk GC are named controls.<br/>"
269
+ "Portability: the same prompt points Claude, Codex, Hermes, and scripts at the same files.",
270
+ body,
271
+ ),
272
+ Paragraph("Tradeoffs", h2),
273
+ p(
274
+ "For small active workloads, the Yool prompt uses more memory because it "
275
+ "creates tuple envelopes and worker fan-out structures. It pays off when work "
276
+ "is I/O-bound, uses subprocesses, browser automation, APIs, hosted LLM calls, "
277
+ "or large-scale planning/execution.",
278
+ body,
279
+ ),
280
+ Paragraph("Prompt Used", h2),
281
+ p(PROMPT, code),
282
+ Paragraph("Reproduce", h2),
283
+ p(
284
+ "python benchmarks/prompt_vs_normal.py --json<br/>"
285
+ "python benchmarks/prompt_vs_normal.py --tasks 512 --scale-agents 262144 --sleep-ms 2 --json<br/>"
286
+ "python benchmarks/generate_prompt_benchmark_pdf.py",
287
+ code,
288
+ ),
289
+ ]
290
+ )
291
+
292
+ doc.build(story)
293
+ print(OUT)
294
+ return 0
295
+
296
+
297
+ def _table_style() -> TableStyle:
298
+ return TableStyle(
299
+ [
300
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#172033")),
301
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
302
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
303
+ ("FONTSIZE", (0, 0), (-1, -1), 8.5),
304
+ ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#CAD3DF")),
305
+ ("BACKGROUND", (0, 1), (-1, -1), colors.HexColor("#F9FBFD")),
306
+ ("BACKGROUND", (0, -1), (-1, -1), colors.HexColor("#EAF5EF")),
307
+ ("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
308
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
309
+ ("LEFTPADDING", (0, 0), (-1, -1), 5),
310
+ ("RIGHTPADDING", (0, 0), (-1, -1), 5),
311
+ ("TOPPADDING", (0, 0), (-1, -1), 5),
312
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
313
+ ]
314
+ )
315
+
316
+
317
+ def _bar_chart(
318
+ title: str, rows: list[tuple[str, float, colors.Color]], max_value: float
319
+ ) -> Drawing:
320
+ width = 17 * cm
321
+ row_h = 0.55 * cm
322
+ height = 1.0 * cm + row_h * len(rows)
323
+ drawing = Drawing(width, height)
324
+ drawing.add(
325
+ String(0, height - 0.35 * cm, title, fontName="Helvetica-Bold", fontSize=10)
326
+ )
327
+ label_w = 5.4 * cm
328
+ bar_w = 9.0 * cm
329
+ x = label_w
330
+ y = height - 0.85 * cm
331
+ for label, value, color in rows:
332
+ y -= row_h
333
+ drawing.add(String(0, y + 0.12 * cm, label[:34], fontSize=7.5))
334
+ drawing.add(
335
+ Rect(
336
+ x,
337
+ y,
338
+ bar_w,
339
+ 0.25 * cm,
340
+ fillColor=colors.HexColor("#E8EDF3"),
341
+ strokeColor=None,
342
+ )
343
+ )
344
+ filled = (
345
+ 0 if max_value <= 0 else max(0.0, min(bar_w, bar_w * value / max_value))
346
+ )
347
+ drawing.add(Rect(x, y, filled, 0.25 * cm, fillColor=color, strokeColor=None))
348
+ drawing.add(
349
+ String(x + bar_w + 0.25 * cm, y + 0.08 * cm, f"{value:,.2f}", fontSize=7.5)
350
+ )
351
+ return drawing
352
+
353
+
354
+ if __name__ == "__main__":
355
+ raise SystemExit(main())
@@ -0,0 +1,302 @@
1
+ """Generate the V2 safe-speed benchmark PDF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from reportlab.graphics.shapes import Drawing, Rect, String
9
+ from reportlab.lib import colors
10
+ from reportlab.lib.pagesizes import A4
11
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
12
+ from reportlab.lib.units import cm
13
+ from reportlab.platypus import (
14
+ Paragraph,
15
+ SimpleDocTemplate,
16
+ Spacer,
17
+ Table,
18
+ TableStyle,
19
+ )
20
+
21
+ ROOT = Path(__file__).resolve().parents[1]
22
+ IN_JSON = ROOT / "benchmarks" / "v2_safe_speed_results.json"
23
+ OUT = ROOT / "benchmarks" / "v2_safe_speed_benchmark.pdf"
24
+
25
+
26
+ def main() -> int:
27
+ payload = json.loads(IN_JSON.read_text(encoding="utf-8"))
28
+ results = payload["results"]
29
+ comparisons = payload["comparisons"]
30
+
31
+ styles = getSampleStyleSheet()
32
+ title = styles["Title"]
33
+ h2 = styles["Heading2"]
34
+ body = styles["BodyText"]
35
+ body.leading = 13
36
+ small = ParagraphStyle("small", parent=body, fontSize=8.2, leading=10)
37
+
38
+ doc = SimpleDocTemplate(
39
+ str(OUT),
40
+ pagesize=A4,
41
+ rightMargin=1.4 * cm,
42
+ leftMargin=1.4 * cm,
43
+ topMargin=1.3 * cm,
44
+ bottomMargin=1.3 * cm,
45
+ title="Yool Safe-Speed Benchmark V2",
46
+ author="wesleysimplicio/simplicio-prompt",
47
+ )
48
+
49
+ story = [
50
+ Paragraph("Yool Safe-Speed Benchmark V2", title),
51
+ _p(
52
+ "Comparacao entre instrucao normal, V1 high-throughput e V2 safe-speed.<br/>"
53
+ f"Run date: {payload['run_date']}<br/>"
54
+ f"Python: {payload['environment']['python']}",
55
+ body,
56
+ ),
57
+ Spacer(1, 0.2 * cm),
58
+ Paragraph("Executive Summary", h2),
59
+ _p(
60
+ "A V2 mantem o ganho estrutural da V1 para escala massiva e adiciona "
61
+ "controles seguros de velocidade: cache por input/receipt, LaneWorkerPool "
62
+ "adaptativo, backoff com jitter, circuit breaker por provedor, batching, "
63
+ "compressao de contexto, roteamento local e speculative execution apenas "
64
+ "para tarefas idempotentes.",
65
+ body,
66
+ ),
67
+ Spacer(1, 0.2 * cm),
68
+ _bar_chart(
69
+ "Speed gains (x)",
70
+ _comparison_rows(
71
+ comparisons,
72
+ [
73
+ "scale_representation",
74
+ "active_execution",
75
+ "cache_dedupe",
76
+ "small_task_batching",
77
+ ],
78
+ metric="wall_ms",
79
+ ),
80
+ ),
81
+ Spacer(1, 0.2 * cm),
82
+ _bar_chart(
83
+ "Provider/token reduction (x)",
84
+ _comparison_rows(
85
+ comparisons,
86
+ [
87
+ "cache_dedupe",
88
+ "small_task_batching",
89
+ "provider_failure_control",
90
+ "context_compression",
91
+ ],
92
+ metrics={"provider_calls", "tokens"},
93
+ ),
94
+ ),
95
+ ]
96
+
97
+ for scenario, heading in [
98
+ ("scale_representation", "Scale Representation"),
99
+ ("active_execution", "Active Execution"),
100
+ ("cache_dedupe", "Cache Dedupe"),
101
+ ("small_task_batching", "Small Task Batching"),
102
+ ("provider_failure_control", "Provider Failure Control"),
103
+ ("context_compression", "Context Compression"),
104
+ ]:
105
+ story.extend(
106
+ [
107
+ Spacer(1, 0.25 * cm),
108
+ Paragraph(heading, h2),
109
+ _result_table(
110
+ [item for item in results if item["scenario"] == scenario]
111
+ ),
112
+ ]
113
+ )
114
+
115
+ story.extend(
116
+ [
117
+ Spacer(1, 0.25 * cm),
118
+ Paragraph("Gains Table", h2),
119
+ _comparison_table(comparisons),
120
+ Spacer(1, 0.25 * cm),
121
+ Paragraph("Interpretation", h2),
122
+ _p(
123
+ "V1 ja resolve escala com lazy batch_spawn. V2 melhora a velocidade "
124
+ "real de entrega ao reduzir chamadas repetidas e overhead de orquestracao. "
125
+ "O circuit breaker e o backoff sao importantes porque velocidade sem "
126
+ "controle aumenta risco de rate limit ou bloqueio por provedor. A V2 "
127
+ "acelera evitando trabalho, nao forçando chamadas infinitas.",
128
+ body,
129
+ ),
130
+ Paragraph("Limitations", h2),
131
+ _p(
132
+ "Os numeros medem mecanica local. Provedores reais podem variar por "
133
+ "rate limit, latencia, tamanho de contexto e politica de cache. Para "
134
+ "trabalho CPU-bound puro em Python, subprocessos ou extensoes nativas "
135
+ "ainda sao melhores que threads por causa do GIL.",
136
+ small,
137
+ ),
138
+ ]
139
+ )
140
+
141
+ doc.build(story)
142
+ print(OUT)
143
+ return 0
144
+
145
+
146
+ def _p(text: str, style: ParagraphStyle) -> Paragraph:
147
+ return Paragraph(text.replace("\n", "<br/>"), style)
148
+
149
+
150
+ def _result_table(rows: list[dict]) -> Table:
151
+ table_data = [
152
+ [
153
+ "Profile",
154
+ "Tasks",
155
+ "Wall ms",
156
+ "Throughput/s",
157
+ "Calls",
158
+ "Cache",
159
+ "Blocked",
160
+ "Tokens",
161
+ ]
162
+ ]
163
+ for row in rows:
164
+ table_data.append(
165
+ [
166
+ row["profile"],
167
+ f"{row['tasks']:,}",
168
+ f"{row['wall_ms']:.2f}",
169
+ f"{row['throughput_tasks_s']:.1f}",
170
+ f"{row['provider_calls']:,}",
171
+ f"{row['cache_hits']:,}",
172
+ f"{row['blocked_calls']:,}",
173
+ f"{row['tokens']:,}",
174
+ ]
175
+ )
176
+ table = Table(
177
+ table_data,
178
+ colWidths=[
179
+ 4.2 * cm,
180
+ 2.1 * cm,
181
+ 2.0 * cm,
182
+ 2.3 * cm,
183
+ 1.8 * cm,
184
+ 1.8 * cm,
185
+ 1.8 * cm,
186
+ 1.8 * cm,
187
+ ],
188
+ )
189
+ table.setStyle(_table_style())
190
+ return table
191
+
192
+
193
+ def _comparison_table(comparisons: list[dict]) -> Table:
194
+ table_data = [["Scenario", "Baseline", "Improved", "Metric", "Ratio", "Gain"]]
195
+ for item in comparisons:
196
+ ratio = "n/a" if item["ratio"] is None else f"{item['ratio']:.2f}x"
197
+ table_data.append(
198
+ [
199
+ item["scenario"],
200
+ item["baseline"],
201
+ item["improved"],
202
+ item["metric"],
203
+ ratio,
204
+ f"{item['percent']:.2f}%",
205
+ ]
206
+ )
207
+ table = Table(
208
+ table_data,
209
+ colWidths=[3.4 * cm, 3.5 * cm, 3.5 * cm, 2.4 * cm, 1.8 * cm, 2.0 * cm],
210
+ )
211
+ table.setStyle(_table_style())
212
+ return table
213
+
214
+
215
+ def _comparison_rows(
216
+ comparisons: list[dict],
217
+ scenarios: list[str],
218
+ *,
219
+ metric: str | None = None,
220
+ metrics: set[str] | None = None,
221
+ ) -> list[tuple[str, float, colors.Color]]:
222
+ rows = []
223
+ palette = [
224
+ colors.HexColor("#2B6CB0"),
225
+ colors.HexColor("#38A169"),
226
+ colors.HexColor("#DD6B20"),
227
+ colors.HexColor("#805AD5"),
228
+ ]
229
+ for item in comparisons:
230
+ if item["scenario"] not in scenarios:
231
+ continue
232
+ if metric is not None and item["metric"] != metric:
233
+ continue
234
+ if metrics is not None and item["metric"] not in metrics:
235
+ continue
236
+ if item["ratio"] is None:
237
+ continue
238
+ label = f"{item['scenario']} {item['metric']}"[:38]
239
+ rows.append((label, float(item["ratio"]), palette[len(rows) % len(palette)]))
240
+ return rows
241
+
242
+
243
+ def _bar_chart(title: str, rows: list[tuple[str, float, colors.Color]]) -> Drawing:
244
+ width = 17 * cm
245
+ row_h = 0.55 * cm
246
+ height = 1.0 * cm + row_h * max(1, len(rows))
247
+ drawing = Drawing(width, height)
248
+ drawing.add(
249
+ String(0, height - 0.35 * cm, title, fontName="Helvetica-Bold", fontSize=10)
250
+ )
251
+ if not rows:
252
+ drawing.add(String(0, height - 0.85 * cm, "No comparable rows", fontSize=8))
253
+ return drawing
254
+ max_value = max(value for _label, value, _color in rows)
255
+ label_w = 6.0 * cm
256
+ bar_w = 8.6 * cm
257
+ y = height - 0.85 * cm
258
+ for label, value, color in rows:
259
+ y -= row_h
260
+ drawing.add(String(0, y + 0.12 * cm, label, fontSize=7.2))
261
+ drawing.add(
262
+ Rect(
263
+ label_w,
264
+ y,
265
+ bar_w,
266
+ 0.25 * cm,
267
+ fillColor=colors.HexColor("#E8EDF3"),
268
+ strokeColor=None,
269
+ )
270
+ )
271
+ filled = 0 if max_value <= 0 else min(bar_w, bar_w * value / max_value)
272
+ drawing.add(
273
+ Rect(label_w, y, filled, 0.25 * cm, fillColor=color, strokeColor=None)
274
+ )
275
+ drawing.add(
276
+ String(
277
+ label_w + bar_w + 0.2 * cm, y + 0.08 * cm, f"{value:.2f}x", fontSize=7.2
278
+ )
279
+ )
280
+ return drawing
281
+
282
+
283
+ def _table_style() -> TableStyle:
284
+ return TableStyle(
285
+ [
286
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#172033")),
287
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
288
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
289
+ ("FONTSIZE", (0, 0), (-1, -1), 7.5),
290
+ ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#CAD3DF")),
291
+ ("BACKGROUND", (0, 1), (-1, -1), colors.HexColor("#F9FBFD")),
292
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
293
+ ("LEFTPADDING", (0, 0), (-1, -1), 4),
294
+ ("RIGHTPADDING", (0, 0), (-1, -1), 4),
295
+ ("TOPPADDING", (0, 0), (-1, -1), 4),
296
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
297
+ ]
298
+ )
299
+
300
+
301
+ if __name__ == "__main__":
302
+ raise SystemExit(main())