-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdevelop.astro
More file actions
760 lines (685 loc) · 35.3 KB
/
Copy pathdevelop.astro
File metadata and controls
760 lines (685 loc) · 35.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
---
import Base from "../layouts/Base.astro";
import rustData from "../data/rust-api.json";
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
// Build an index of every Rust item by its full path, so we can resolve
// short symbol names (e.g. "PauliSum") to anchors on /api/.
type RustItem = { name: string; path: string; kind: string };
const rustItems: RustItem[] = (rustData as { crates: { items: RustItem[] }[] })
.crates.flatMap((c) => c.items);
const byPath = new Map<string, RustItem>(rustItems.map((it) => [it.path, it]));
function slugFor(path: string): string {
return "rs-" + path.replace(/[^a-zA-Z0-9]+/g, "-");
}
// Resolve a symbol reference. Accepts:
// - a fully-qualified path like "ppvm_traits::config::Config"
// - a "crate::short_name" pair like "ppvm-traits:Config"
// Returns an absolute href into the API site, or null if unknown.
function apiHref(ref: string): string | null {
const colon = ref.indexOf(":");
let path: string | null = null;
if (ref.includes("::") && !ref.startsWith("crate:")) {
path = byPath.has(ref) ? ref : null;
} else if (colon >= 0) {
const [crate, name] = [ref.slice(0, colon), ref.slice(colon + 1)];
const cratePrefix = crate.replace(/-/g, "_") + "::";
const hit = rustItems.find(
(it) => it.name === name && it.path.startsWith(cratePrefix),
);
path = hit?.path ?? null;
}
if (!path) return null;
return `${base}/api/#${slugFor(path)}`;
}
// Cross-reference shortcuts: short label -> "crate:Name" lookup.
const xrefs: Record<string, string> = {
PauliSum: "ppvm-pauli-sum:PauliSum",
Config: "ppvm-traits:Config",
CoefficientThreshold: "ppvm-pauli-sum:CoefficientThreshold",
MaxPauliWeight: "ppvm-pauli-sum:MaxPauliWeight",
MaxLossWeight: "ppvm-pauli-sum:MaxLossWeight",
CombinedStrategy: "ppvm-pauli-sum:CombinedStrategy",
Clifford: "ppvm-traits:Clifford",
CliffordExtensions: "ppvm-traits:CliffordExtensions",
TGate: "ppvm-traits:TGate",
RotationOne: "ppvm-traits:RotationOne",
RotationTwo: "ppvm-traits:RotationTwo",
U3Gate: "ppvm-traits:U3Gate",
Measure: "ppvm-traits:Measure",
LossyMeasure: "ppvm-traits:LossyMeasure",
Depolarizing: "ppvm-traits:Depolarizing",
PauliError: "ppvm-traits:PauliError",
LossChannel: "ppvm-traits:LossChannel",
CorrelatedLossChannel: "ppvm-traits:CorrelatedLossChannel",
Tableau: "ppvm-tableau:Tableau",
GeneralizedTableau: "ppvm-tableau:GeneralizedTableau",
SparseVector: "ppvm-tableau:SparseVector",
};
// Pre-resolve so the template stays declarative.
const link: Record<string, string | null> = {};
for (const [k, v] of Object.entries(xrefs)) link[k] = apiHref(v);
const toc = [
{ label: "§ 1 Orient yourself", href: "#orient" },
{ label: "§ 2 Build & test", href: "#build" },
{ label: "§ 3 Architecture", href: "#architecture" },
{ label: "§ 4 Conventions", href: "#conventions" },
{ label: "§ 5 Python bindings", href: "#python-bindings" },
{ label: "§ 6 Extending ppvm", href: "#extending" },
{ label: "§ 7 Where to look for X", href: "#where-to-look" },
];
---
<Base title="Developer Guide" current="develop" toc={toc}>
<article class="shell narrow">
<div class="titleblock">
<div class="eyebrow">For contributors · humans & agents</div>
<h1>Developer Guide</h1>
<p class="byline">
How the ppvm repository is organised, how to build and test it, and
where to look when you want to extend it. Written so that an AI agent
can read just the sections it needs.
</p>
</div>
<aside class="agent-callout" aria-label="Notes for AI agents">
<div class="agent-callout-label">For AI agents</div>
<p>
If a human delegated a task to you in this repository, this page is the
canonical guide. Read sections in this order:
</p>
<ol>
<li><a href="#orient">§ 1 Orient yourself</a> — what each crate does and where to look.</li>
<li><a href="#build">§ 2 Build & test</a> — the exact commands to run.</li>
<li><a href="#architecture">§ 3 Architecture</a> — design patterns you must respect.</li>
<li><a href="#conventions">§ 4 Conventions</a> — commits, formatting, file organisation.</li>
<li>Specific sections below only if relevant to your task.</li>
</ol>
<p>
The root <code>AGENTS.md</code> file is a short pointer to this page;
it intentionally does not duplicate this content so that it stays in
sync with the docs.
</p>
</aside>
<h2 id="orient"><span class="sec-num">§ 1</span>Orient yourself</h2>
<p>
ppvm is a Cargo workspace with a Python wrapper layered on top. The
Rust crates are the source of truth; the Python package is a thin
PyO3-based binding.
</p>
<pre><code>ppvm/
├── crates/
│ ├── ppvm-traits # Trait system, Config bundle, Pauli alphabet, map impls
│ ├── ppvm-pauli-word # Packed Pauli strings: PauliWord, phased, lossy, pattern
│ ├── ppvm-pauli-sum # PauliSum engine, truncation strategy, concrete configs
│ ├── ppvm-tableau # Stabilizer + generalized-tableau simulator
│ ├── ppvm-sym # Symbolic (parametric) Pauli propagation
│ ├── ppvm-stim # Stim program execution against the tableau
│ ├── stim-parser # Standalone parser for the Stim circuit format
│ └── ppvm-python-native # PyO3 bindings, compiled into `ppvm` as `ppvm._core`
├── ppvm-python/ # Python package `ppvm` (maturin: wrapper + `ppvm._core`)
├── docs/ # This documentation site (Astro)
├── examples/ # Rust examples (symbolic.rs, trotter.rs)
└── AGENTS.md # Pointer to this guide
</code></pre>
<p>
<strong>Dependency graph.</strong> <code>ppvm-traits</code> is the
foundation; <code>ppvm-pauli-word</code> builds on it, and
<code>ppvm-pauli-sum</code> builds on both. <code>ppvm-tableau</code>,
<code>ppvm-sym</code>, and <code>ppvm-stim</code> depend on the Pauli
crates. <code>ppvm-stim</code> additionally depends on
<code>ppvm-tableau</code> and <code>stim-parser</code>.
<code>ppvm-python-native</code> depends on <code>ppvm-pauli-sum</code>
and <code>ppvm-tableau</code>.
</p>
<h2 id="build"><span class="sec-num">§ 2</span>Build & test</h2>
<h3>Rust</h3>
<pre><code class="language-bash"># All Rust tests
cargo test --workspace
# A single crate
cargo test -p ppvm-tableau
# A single test by name
cargo test -p ppvm-pauli-sum -- test_ghz
# Benchmarks
cargo bench -p ppvm-tableau --bench micro
cargo bench --bench micro -- "gates/single-qubit/h"</code></pre>
<p>
Rust edition 2024. On x86 the default hasher (gxhash) needs
AES/SSE2 target features; this repo sets them for x86_64 in
<code>.cargo/config.toml</code>, and CI does the same. On non-x86 hosts,
build with
<code>--no-default-features --features=indexmap,ahash</code> or similar.
</p>
<h3>WebAssembly (wasm32)</h3>
<p>
The whole workspace except <code>ppvm-python-native</code> (a CPython
extension, never a wasm target) cross-compiles to browser wasm with no
extra flags:
</p>
<pre><code class="language-bash">rustup target add wasm32-unknown-unknown
# The simulators, Pauli engine, Stim parser, and top-level `ppvm` crate.
cargo build --target wasm32-unknown-unknown --workspace --exclude ppvm-python-native</code></pre>
<p>
The build is wasm-clean automatically. Native-only acceleration
dependencies — <code>gxhash</code> (AES intrinsics), <code>dashmap</code> →
<code>rayon</code> (OS threads), and <code>ahash</code> — live in
<code>[target.'cfg(not(target_arch = "wasm32"))'.dependencies]</code>
tables, so on wasm they are pruned and the matching features go inert
(the code that names those crates is gated with the same
<code>not(target_arch = "wasm32")</code>). The <code>fx64hash</code> configs
use native-word <code>[usize; N]</code> storage (<code>u64</code> on
64-bit, <code>u32</code> on wasm) since <code>bitvec</code> only implements
<code>BitStore</code> for <code>u64</code> on 64-bit pointer widths.
<code>ppvm-tableau-sum</code>'s structural fingerprint falls back from
<code>gxhash</code> to <code>fxhash</code> on wasm.
</p>
<p>
<code>rand</code>'s entropy (<code>rand::make_rng()</code>) has no default
source on <code>wasm32-unknown-unknown</code>, so the getrandom
<code>wasm_js</code> backend (Web Crypto API) is selected via a
<code>--cfg getrandom_backend="wasm_js"</code> rustflag in
<code>.cargo/config.toml</code> plus the <code>wasm_js</code> feature in
<code>ppvm-tableau</code>'s wasm-only dependency table — so the JS runtime
supplies randomness. There are no OS threads on wasm: the <code>rayon</code>
feature is unavailable, and the Stim parser runs its recursive grammar
inline instead of on a dedicated stack thread. The <code>wasm32 build</code>
CI job compiles the workspace for this target on every PR.
</p>
<h3>Python</h3>
<pre><code class="language-bash"># Requires uv (https://docs.astral.sh/uv/)
# The native module compiles automatically via maturin on first run.
uv run --project ppvm-python --group dev pytest ppvm-python/test/
# A single file
uv run --project ppvm-python --group dev pytest ppvm-python/test/test_basics.py
# A single test by name
uv run --project ppvm-python --group dev pytest ppvm-python/test/ -k test_ghz</code></pre>
<p>
The compiled <code>ppvm._core</code> is part of the <code>ppvm</code>
wheel, so after changing Rust force a rebuild with <code>uv sync
--project ppvm-python --reinstall-package ppvm</code> (or <code>maturin
develop -m crates/ppvm-python-native/Cargo.toml</code>). The Python
project is configured to use uv-managed Python installations, so a fresh
<code>uv sync</code> avoids linking PyO3 builds against a system Python.
</p>
<h3>This docs site</h3>
<p>
The Astro site you're reading lives under <code>docs/</code>. Every
build step (rustdoc-JSON extraction, griffe-based Python API
extraction, notebook execution, Astro build) is wired into
<code>docs/package.json</code>, so a fresh checkout has one command
to remember:
</p>
<pre><code class="language-bash">cd docs
npm install # one-time
npm run dev # extract everything, then `astro dev` (port 4321)
npm run build # extract everything, then `astro build` → dist/</code></pre>
<p>
<code>npm run dev</code> / <code>npm run build</code> chain the
three extraction steps in order so the rendered site picks up
every public API change automatically. When you're iterating on a
single layer, run that step on its own and refresh the
already-running <code>astro dev</code>:
</p>
<table class="dev-cmd-table">
<thead>
<tr><th>Command</th><th>Rebuilds</th><th>When to use</th></tr>
</thead>
<tbody>
<tr>
<td><code>npm run extract:rust</code></td>
<td><code>src/data/rust-api.json</code></td>
<td>You changed a public Rust item — trait, struct, doc comment — and want it surfaced on <code>/api/</code>. Needs <code>cargo +nightly</code>.</td>
</tr>
<tr>
<td><code>npm run extract:python</code></td>
<td><code>src/data/python-api.json</code></td>
<td>You changed a public Python item under <code>ppvm-python/src/ppvm/</code> and want it surfaced on <code>/api/</code>. Uses <code>griffe</code> via <code>uv</code>.</td>
</tr>
<tr>
<td><code>npm run extract:notebooks</code></td>
<td><code>src/generated/notebooks/*</code></td>
<td>You edited or added a Jupytext file under <code>docs/notebooks/</code>. Re-executes the notebooks against the current ppvm-python build and embeds outputs.</td>
</tr>
<tr>
<td><code>npm run extract</code></td>
<td>All of the above, in order.</td>
<td>Touched several layers at once.</td>
</tr>
<tr>
<td><code>npm run astro:dev</code> / <code>npm run astro:build</code></td>
<td>Just Astro.</td>
<td>You're only editing <code>.astro</code> / <code>.css</code> files and trust the existing extractor outputs — fastest loop.</td>
</tr>
</tbody>
</table>
<p>
<code>docs/src/data/</code> and <code>docs/src/generated/</code> are
both <code>.gitignore</code>'d; the only sources of truth for those
files are the extractor scripts, which CI re-runs on every build.
Adding a notebook is a single drop-in:
<code>docs/notebooks/my_notebook.py</code> (Jupytext-percent
format) → <code>npm run extract:notebooks</code> → the Examples
landing page picks it up from
<code>src/generated/notebooks/index.json</code>.
</p>
<h3 id="notebook-pipeline">2.1 Notebook execution & caching</h3>
<p>
The script behind <code>npm run extract:notebooks</code> lives at
<code><a href="https://github.com/QuEraComputing/ppvm/blob/main/docs/scripts/build-notebooks.py">docs/scripts/build-notebooks.py</a></code>.
Per-notebook pipeline:
</p>
<ol>
<li>
Read the Jupytext <code>percent</code>-format <code>.py</code> file
and convert to an in-memory <code>ipynb</code>.
</li>
<li>
Prepend a hidden setup cell that switches matplotlib to the
IPython <code>inline</code> backend — without this, <code>plt.show()</code>
renders to a buffer that never reaches the cell output and plots
are silently dropped.
</li>
<li>
Execute every code cell via <code>nbclient</code>. Text output,
tracebacks, and matplotlib figures are captured inline; figures
are embedded as base64 PNGs.
</li>
<li>
Drop the hidden setup cell, render to an HTML fragment via
<code>nbconvert</code>'s <code>basic</code> template (no
JupyterLab chrome — the site stylesheet themes the
<code>.jp-*</code> classes), and sanitise through
<code>bleach</code> against an allow-list that permits
<code>data:image/png</code> URLs but strips scripts and
iframes.
</li>
<li>
Write <code>docs/src/generated/notebooks/<slug>.html</code> +
<code><slug>.json</code> (title, ordered headings,
language, source path). The Astro routes at
<code>docs/src/pages/examples/index.astro</code> and
<code>[slug].astro</code> consume <code>index.json</code> +
the per-slug fragments at build time.
</li>
</ol>
<p>
<strong>Content-addressed cache.</strong> Executing every notebook
from scratch on every PR is expensive — the long-running examples
can dominate CI. To avoid that, every successful run also writes
its output to <code>docs/.notebook-cache/<hash>.{html,json}</code>,
keyed by
</p>
<pre><code class="language-text">sha256(CACHE_SCHEMA_VERSION + docs/scripts/build-notebooks.py + notebook source + Cargo.lock + Cargo.toml + crates/*/Cargo.toml + ppvm-python/uv.lock)</code></pre>
<p>
Hashing the extractor itself means that a change to the
rendering / sanitiser / matplotlib-setup logic invalidates
every cached entry automatically — without that, a tweak to the
bleach allow-list would silently keep serving the previous
HTML for every unchanged notebook source. The
<code>CACHE_SCHEMA_VERSION</code> constant at the top of the
script is an explicit global invalidation knob for changes the
hash can't see (e.g. a new field in the sidecar JSON that
downstream Astro pages start depending on).
</p>
<p>
On the next run the script restores from the cache when the hash
matches and only re-executes notebooks whose fingerprint
changed. CI persists the directory via <code>actions/cache</code>
keyed on the same set of files (see the "Restore executed-notebook
cache" step in
<code><a href="https://github.com/QuEraComputing/ppvm/blob/main/.github/workflows/docs.yml">.github/workflows/docs.yml</a></code>),
so a docs-only PR that touches only <code>.astro</code> or
<code>.css</code> hits the cache for every notebook and the
build takes seconds.
</p>
<p>
<strong>What the fingerprint deliberately does <em>not</em>
include</strong>: Rust <code>.rs</code> sources and Python
package sources. Hashing every workspace file would force a
re-execution on any cosmetic edit, which is exactly the cost we
want to avoid. The tradeoff is that a numerical change inside a
Rust crate without a dependency or Cargo.toml bump won't
invalidate cached notebook outputs on a docs-only PR — rely on
the standard test suites (<code>cargo test --workspace</code>,
<code>pytest</code>) to catch those. (A scheduled full-rebuild
workflow as a second safety net would be a reasonable future
addition, but none exists today; bump
<code>CACHE_SCHEMA_VERSION</code> manually if you ever need to
force a global re-execution.)
</p>
<p>
Override knobs (mostly for debugging):
</p>
<ul>
<li>
<code>PPVM_NOTEBOOK_CACHE=0</code> — force re-execution of every
notebook regardless of cache state (use when investigating
suspected numerical drift).
</li>
<li>
<code>PPVM_NOTEBOOK_CACHE_DIR=<path></code> — point the
cache at a non-default directory (CI uses this implicitly via
the default <code>docs/.notebook-cache</code>; tweak only if
you need to share a cache across worktrees).
</li>
</ul>
<p>
<strong>Where to look when you need to change this.</strong>
Adding a new notebook: drop a Jupytext file under
<code>docs/notebooks/</code> — no extractor change needed.
Changing how notebooks render (sanitiser allow-list, matplotlib
DPI, output format): <code>docs/scripts/build-notebooks.py</code>
— every edit to this file already invalidates the cache via the
fingerprint, so no version bump is needed for routine pipeline
tweaks. Changing the fingerprint <em>inputs</em> (e.g. another
lockfile becomes relevant): edit
<code>_shared_fingerprint_files()</code> in that same script
<em>and</em> the <code>hashFiles(...)</code> argument on the
cache step in <code>.github/workflows/docs.yml</code> — those
two lists must stay in sync (note that
<code>docs/scripts/build-notebooks.py</code> itself appears in
both), otherwise the GH Actions cache key drifts from the
script's per-notebook key and you get either stale outputs or
perpetual misses. To force a global invalidation independent of
file content (e.g. cached-artefact schema change), bump
<code>CACHE_SCHEMA_VERSION</code> in the script; bump the
<code>notebooks-v1-</code> prefix in the workflow when the GH
Actions cache itself needs a clean slate. Changing the
Examples landing or per-notebook page chrome: the two
<code>.astro</code> files under
<code>docs/src/pages/examples/</code>.
</p>
<p>
Local prerequisites the scripts assume: <code>node ≥ 20</code>
(Astro 5), <code>uv</code>, Rust nightly
(<code>rustup toolchain install nightly</code>). The full layout
and rationale live in
<code><a href="https://github.com/QuEraComputing/ppvm/blob/main/docs/README.md">docs/README.md</a></code>.
</p>
<h3>Continuous integration</h3>
<p>
CI lives in <code>.github/workflows/ci.yml</code> and is staged so the
cheap, platform-independent checks gate the expensive cross-OS ones:
</p>
<ol>
<li>
<strong><code>pre-commit</code></strong> (Linux) runs the full
<code>prek</code> hook suite — rustfmt, clippy, <code>cargo check
--workspace --all-targets</code>, ruff, ty, hawkeye, and the file
hygiene hooks. Every other job <code>needs:</code> it, so a lint or
type failure stops the run before any test minutes are spent.
</li>
<li>
<strong><code>rust-tests</code></strong> and
<strong><code>python-tests</code></strong> (Linux) run
<code>cargo test --workspace</code> and the <code>pytest</code>
suites. The pure-Rust crates are platform-agnostic, so Linux is the
only OS that runs the full test suites.
</li>
<li>
<strong><code>extension-cross-platform</code></strong> (macOS +
Windows) is the only cross-OS job. It builds the PyO3 extension via
maturin and runs the extension's <code>pytest</code> suite. It
<code>needs: [rust-tests, python-tests]</code>, so the macOS/Windows
runners only start once Linux is fully green.
</li>
</ol>
<p>
<strong>Unused dependencies.</strong> A <code>cargo-machete</code>
<code>prek</code> hook flags unused crate dependencies across the whole
workspace (run from the repo root, <a href="https://github.com/bnjbvr/cargo-machete">cargo-machete</a>
recurses into every member). It is part of the hook suite, so it runs both
locally on commit and in CI via the <code>pre-commit</code> job — there is
no separate machete CI job. The binary is provisioned by <code>mise</code>
(<code>cargo:cargo-machete</code> in <code>mise.toml</code>), so the
mise-action step that sets up the other hooks installs it too. Silence a
false positive per-crate with
<code>[package.metadata.cargo-machete] ignored = […]</code>.
</p>
<p>
<strong>Why cross-OS is extension-only.</strong> The compiled PyO3
module is the only artifact whose build is OS-sensitive — macOS needs
<code>-undefined dynamic_lookup</code> (added by
<code>ppvm-python-native/build.rs</code>; maturin sets it too), Windows
links <code>python3.lib</code>, and Linux needs neither. Building that
extension with maturin also compiles <code>ppvm-python-native</code> and
its entire dependency tree on the target OS, so a cross-platform compile
regression in any crate still surfaces here — without separately running
<code>cargo build</code> for the whole workspace three times.
</p>
<p>
<strong>No global <code>RUSTFLAGS</code>.</strong> gxhash's
<code>+aes,+sse2</code> target features are set <em>arch-scoped</em> in
<code>.cargo/config.toml</code>
(<code>cfg(target_arch = "x86_64")</code>), not as a workflow-wide
<code>RUSTFLAGS</code> — those x86 features are invalid on the aarch64
<code>macos-latest</code> runner and would fail to compile there. Linux
and Windows (x86_64) still pick them up from the config.
</p>
<h2 id="architecture"><span class="sec-num">§ 3</span>Architecture</h2>
<p>
ppvm implements two complementary quantum simulation backends. They
share a common gate / noise trait hierarchy from <code>ppvm-traits</code>.
</p>
<h3>3.1 Pauli propagation (<code>ppvm-pauli-sum</code>)</h3>
<p>
Tracks Pauli operator evolution through circuits in the
<strong>Heisenberg picture</strong> (circuits run backwards). The
central type is <a href={link.PauliSum}><code>PauliSum<T: Config></code></a>,
a dictionary of Pauli strings to coefficients.
</p>
<p>Key design patterns — respect these when editing:</p>
<ul>
<li>
<strong>Config-based generics.</strong> The <a href={link.Config}><code>Config</code></a> trait
bundles Storage, Coefficient, Strategy, Map, and BuildHasher choices
at compile time. Implementations live in <code>config/</code>
(<code>fxhash</code>, <code>indexmap</code>, <code>dashmap</code>,
<code>gxhash</code>). Do not introduce runtime dispatch where a
<a href={link.Config}><code>Config</code></a> bound would do.
</li>
<li>
<strong>Dual-map optimisation.</strong> <a href={link.PauliSum}><code>PauliSum</code></a>
maintains two internal maps (main + auxiliary) and swaps between
them during gate propagation to avoid repeated allocations.
Any new gate that writes to a fresh map must respect this swap.
</li>
<li>
<strong>Strategy pattern.</strong> Truncation policies
(<a href={link.CoefficientThreshold}><code>CoefficientThreshold</code></a>,
<a href={link.MaxPauliWeight}><code>MaxPauliWeight</code></a>,
<a href={link.MaxLossWeight}><code>MaxLossWeight</code></a>,
<a href={link.CombinedStrategy}><code>CombinedStrategy</code></a>) decide
when small terms are dropped. Call <code>.truncate()</code> to apply.
</li>
<li>
<strong>Backward propagation.</strong> Circuits run backwards. To
simulate <code>H(0); CNOT(0,1)</code> in the Heisenberg picture, call
<code>state.cnot(0,1); state.h(0)</code>: the CNOT precedes the
Hadamard in code.
</li>
</ul>
<h3>3.2 Generalized stabilizer tableau (<code>ppvm-tableau</code>)</h3>
<p>
Full state simulation using stabilizer formalism, extended to handle
non-Clifford gates (T, rotations) via stabilizer rank decomposition
with sparse coefficient tracking.
</p>
<ul>
<li>
<a href={link.Tableau}><code>Tableau<T: Config></code></a> — 2n-row stabilizer /
destabilizer tableau (rows <code>0..n</code> = destabilizers,
<code>n..2n</code> = stabilizers).
</li>
<li>
<a href={link.GeneralizedTableau}><code>GeneralizedTableau<T: Config, IndexType></code></a> —
extends <a href={link.Tableau}><code>Tableau</code></a> with a sparse coefficient vector for
non-Clifford state tracking. <code>IndexType</code> can be
<code>usize</code>, <code>u128</code>, or
<code>bnum::types::U256</code> for large qubit counts.
</li>
<li>
<a href={link.SparseVector}><code>SparseVector<T, I></code></a> — stores coefficients
indexed by bitstrings. Indices can be large integers (U256, U512,
U1024) for simulations beyond 64 qubits.
</li>
<li>
<strong>Stim compatibility.</strong> Rust-side Stim support lives in
<code>ppvm_stim</code> (<code>parse_extended</code>,
<code>run_string</code>, <code>run_file</code>). Python-side Stim
parsing uses <code>StimProgram.parse</code> /
<code>StimProgram.from_file</code>. Execute parsed programs with
<code>tab.run(prog)</code> or sample many shots with
<code>ppvm.sample_stim</code> / <code>GeneralizedTableau.sample</code>.
</li>
</ul>
<h3>3.3 Trait hierarchy (<code>ppvm-traits/src/traits/</code>)</h3>
<p>Gate behaviour is defined via traits reused across both backends:</p>
<ul>
<li>
<a href={link.Clifford}><code>Clifford</code></a> /
<a href={link.CliffordExtensions}><code>CliffordExtensions</code></a> —
single- and two-qubit Clifford gates.
</li>
<li>
<a href={link.TGate}><code>TGate</code></a>,
<a href={link.RotationOne}><code>RotationOne</code></a>,
<a href={link.RotationTwo}><code>RotationTwo</code></a>,
<a href={link.U3Gate}><code>U3Gate</code></a> —
non-Clifford gates (branching).
</li>
<li>
<a href={link.Measure}><code>Measure</code></a> /
<a href={link.LossyMeasure}><code>LossyMeasure</code></a> —
Z-basis measurement.
</li>
<li>
<a href={link.Depolarizing}><code>Depolarizing</code></a>,
<a href={link.PauliError}><code>PauliError</code></a>,
<a href={link.LossChannel}><code>LossChannel</code></a>,
<a href={link.CorrelatedLossChannel}><code>CorrelatedLossChannel</code></a> —
noise channels.
</li>
</ul>
<h2 id="conventions"><span class="sec-num">§ 4</span>Conventions</h2>
<h3>4.1 Commit messages</h3>
<p>
Use <a href="https://www.conventionalcommits.org/">Conventional Commits</a>:
<code><type>(<scope>): <description></code>.
</p>
<pre><code>feat(tableau): add correlated loss channel
fix(pauli-sum): handle zero-norm in truncation
test(stim-parser): add fast fuzz/proptest suite
chore: restore lockfile consistency</code></pre>
<h3>4.2 Code style</h3>
<ul>
<li>Run <code>cargo fmt --all</code> before committing Rust.</li>
<li>Run <code>cargo clippy --workspace --all-targets</code>; fix or justify all warnings.</li>
<li>Run <code>cargo machete</code> to catch unused dependencies; it's also a <code>prek</code> hook, run on commit and in CI.</li>
<li>Python is formatted with <code>ruff format</code> and linted with <code>ruff check</code>.</li>
<li>Public Rust items should have doc comments; <code>cargo doc --no-deps</code> must build cleanly because the API site is built from rustdoc JSON.</li>
<li>
Python docstrings use <strong>Google style</strong> (griffe parses with <code>-d google</code>)
and are rendered as <strong>Markdown</strong> via <code>marked</code>. Use backtick spans for
cross-references — <em>not</em> Sphinx/RST syntax:
<ul>
<li>✅ <code>`fork`</code> or <code>`GeneralizedTableau.sample`</code></li>
<li>❌ <code>:meth:`fork`</code>, <code>:func:`ppvm.sample_stim`</code> — these are never parsed and appear as literal text.</li>
</ul>
</li>
</ul>
<h3>4.3 Tests</h3>
<p>
Add tests in the same crate as the code they cover. Prefer property
tests (<code>proptest</code>) for parser and arithmetic changes;
<code>stim-parser</code> already has a proptest suite worth modelling
new tests on.
</p>
<h2 id="python-bindings"><span class="sec-num">§ 5</span>Python bindings</h2>
<p>
<strong>Single mixed wheel.</strong> <code>ppvm-python</code> is one
maturin package: it bundles the pure-Python wrapper under
<code>src/ppvm/</code> together with the PyO3 crate
(<code>ppvm-python-native</code>, Rust → cdylib via PyO3 0.29),
which maturin compiles and drops in as the private
<code>ppvm._core</code> submodule. Users only ever <code>import
ppvm</code>.
</p>
<ul>
<li>Python ≥ 3.10 required (<code>.python-version</code> pins 3.12 for dev). The wheel is built against PyO3's <code>abi3-py310</code> stable ABI, so one <code>cp310-abi3</code> wheel per platform loads on 3.10+.</li>
<li><code>uv</code> manages the venv and deps and triggers the maturin build on <code>uv sync</code>.</li>
<li><code>ppvm-python/pyproject.toml</code> sets <code>build-backend = "maturin"</code> with <code>[tool.maturin]</code> <code>manifest-path</code> → the crate, <code>python-source = "src"</code>, and <code>module-name = "ppvm._core"</code>.</li>
<li>Plain <code>cargo build</code> also links the cdylib (a <code>build.rs</code> in <code>ppvm-python-native</code> adds the macOS <code>-undefined dynamic_lookup</code> flag), so the Rust-only workflows work without maturin.</li>
<li>The native module exports 16 <code>PauliSum</code> variants × 2 (with/without loss) + 32 <code>GeneralizedTableau</code> variants (1–32 qubits) via the <code>create_interface!</code> / <code>create_interface_range!</code> macros.</li>
</ul>
<p>
When adding a new method to a Python-facing type, edit the macro
invocation in <code>ppvm-python-native</code> so every config variant
picks it up; do not hand-write methods for one variant.
</p>
<h2 id="extending"><span class="sec-num">§ 6</span>Extending ppvm</h2>
<h3>Adding a new gate</h3>
<ol>
<li>
Decide which trait it belongs to
(<a href={link.Clifford}><code>Clifford</code></a>,
<a href={link.RotationOne}><code>RotationOne</code></a>, etc.) in
<code>ppvm-traits/src/traits/</code>.
</li>
<li>
Implement it for
<a href={link.PauliSum}><code>PauliSum<T: Config></code></a> in
<code>ppvm-pauli-sum/src/sum/</code>.
</li>
<li>
Implement it for <a href={link.Tableau}><code>Tableau</code></a> /
<a href={link.GeneralizedTableau}><code>GeneralizedTableau</code></a> in
<code>ppvm-tableau/src/gates/</code>.
</li>
<li>Expose it in <code>ppvm-python-native</code> through the relevant <code>create_interface!</code> macro, and wrap it in <code>ppvm-python/src/ppvm/…</code>.</li>
<li>Add tests on both sides and a benchmark if it is on a hot path.</li>
</ol>
<h3>Adding a new noise channel</h3>
<p>
Follow the pattern of
<a href={link.LossChannel}><code>LossChannel</code></a> /
<a href={link.CorrelatedLossChannel}><code>CorrelatedLossChannel</code></a>.
Implement the trait in <code>ppvm-traits/src/traits/noise.rs</code>,
then mirror in <code>ppvm-tableau</code> if it is meaningful in the
tableau picture.
</p>
<h3>Adding a new <code>Config</code></h3>
<p>
Create a module under <code>ppvm-pauli-sum/src/config/</code>, implement
the <a href={link.Config}><code>Config</code></a> trait (defined in
<code>ppvm-traits</code>), and re-export it from
<code>config/mod.rs</code>. If it should be exposed to Python, add a
variant to the <code>create_interface!</code> macro call.
</p>
<h2 id="where-to-look"><span class="sec-num">§ 7</span>Where to look for X</h2>
<dl class="lookup">
<dt>Pauli arithmetic, <a href={link.PauliSum}><code>PauliSum</code></a></dt>
<dd><code>crates/ppvm-pauli-sum/src/sum/</code>; word / phase / loss / pattern types in <code>crates/ppvm-pauli-word/src/</code></dd>
<dt>Gate & noise traits</dt>
<dd><code>crates/ppvm-traits/src/traits/</code></dd>
<dt>Truncation strategies (<a href={link.CoefficientThreshold}><code>CoefficientThreshold</code></a>, <a href={link.MaxPauliWeight}><code>MaxPauliWeight</code></a>, …)</dt>
<dd><code>crates/ppvm-pauli-sum/src/strategy.rs</code>, <code>crates/ppvm-traits/src/traits/strategy.rs</code></dd>
<dt><a href={link.Config}><code>Config</code></a> trait & implementations</dt>
<dd>trait in <code>crates/ppvm-traits/src/config.rs</code>; concrete bundles in <code>crates/ppvm-pauli-sum/src/config/</code></dd>
<dt>Stabilizer tableau core (<a href={link.Tableau}><code>Tableau</code></a>, <a href={link.GeneralizedTableau}><code>GeneralizedTableau</code></a>)</dt>
<dd><code>crates/ppvm-tableau/src/data.rs</code>, <code>tableau_like.rs</code></dd>
<dt>Tableau gates</dt>
<dd><code>crates/ppvm-tableau/src/gates/</code></dd>
<dt>Stim parsing</dt>
<dd><code>crates/stim-parser/</code> (parser only) and <code>crates/ppvm-stim/</code> (execution)</dd>
<dt>PyO3 bindings & macros</dt>
<dd><code>crates/ppvm-python-native/src/</code></dd>
<dt>Python wrapper & mixins</dt>
<dd><code>ppvm-python/src/ppvm/</code></dd>
<dt>Python tests</dt>
<dd><code>ppvm-python/test/</code></dd>
</dl>
<hr class="ornament" />
<p style="text-align: center; color: var(--ink-faint); font-style: italic;">
Found something out of date? Send a PR — this guide is the canonical
source for both human and agent contributors.
</p>
</article>
</Base>