diff --git a/docs/src/data/pyQuickStart.ts b/docs/src/data/pyQuickStart.ts new file mode 100644 index 000000000..0f45d1f14 --- /dev/null +++ b/docs/src/data/pyQuickStart.ts @@ -0,0 +1,19 @@ +// Section index for the Python Quick Start. Used by every page under +// `quickstart/python/*` so the left-hand section nav stays consistent and +// the order is defined in a single place. +const base = (import.meta.env.BASE_URL || "/").replace(/\/$/, ""); + +export interface PyQuickStartSection { + id: string; + label: string; + href: string; +} + +export const pyQuickStartSections: PyQuickStartSection[] = [ + { id: "install", label: "§ 1 Install", href: `${base}/quickstart/python` }, + { id: "stim", label: "§ 2 Stim circuits & sampling", href: `${base}/quickstart/python/stim` }, + { id: "tableau", label: "§ 3 Generalized Tableau", href: `${base}/quickstart/python/tableau` }, + { id: "pauli-propagation", label: "§ 4 Pauli Propagation", href: `${base}/quickstart/python/pauli-propagation` }, + { id: "loss-channel", label: "§ 5 Loss channel details", href: `${base}/quickstart/python/loss-channel` }, + { id: "next-steps", label: "§ 6 Next steps", href: `${base}/quickstart/python/next-steps` }, +]; diff --git a/docs/src/layouts/Base.astro b/docs/src/layouts/Base.astro index 3b9fec9b7..46500567c 100644 --- a/docs/src/layouts/Base.astro +++ b/docs/src/layouts/Base.astro @@ -5,20 +5,27 @@ interface TocItem { label: string; href: string; } +interface SectionItem { + id: string; + label: string; + href: string; +} interface Props { title: string; description?: string; - current?: "home" | "quickstart" | "tutorials" | "examples" | "develop" | "api"; + current?: "home" | "quickstart" | "examples" | "develop" | "api"; toc?: TocItem[]; tocTitle?: string; + sections?: SectionItem[]; + currentSection?: string; + sectionsTitle?: string; } -const { title, description = "ppvm — fast Pauli propagation and stabilizer-tableau quantum simulator.", current, toc, tocTitle = "On this page" } = Astro.props; +const { title, description = "ppvm — fast Pauli propagation and stabilizer-tableau quantum simulator.", current, toc, tocTitle = "On this page", sections, currentSection, sectionsTitle = "Sections" } = Astro.props; const base = import.meta.env.BASE_URL.replace(/\/$/, ""); const nav = [ { id: "home", label: "Overview", href: `${base}/` }, { id: "quickstart", label: "Quick Start", href: `${base}/quickstart` }, - { id: "tutorials", label: "Tutorials", href: `${base}/tutorials` }, { id: "examples", label: "Examples", href: `${base}/examples` }, { id: "develop", label: "Develop", href: `${base}/develop` }, { id: "api", label: "Reference", href: `${base}/api` }, @@ -77,7 +84,38 @@ const nav = [
- {toc && toc.length > 0 ? ( + {sections && sections.length > 0 ? ( +
+ +
+ +
+ {toc && toc.length > 0 ? ( + + ) : ( + + )} +
+ ) : toc && toc.length > 0 ? (
@@ -74,8 +80,8 @@ const adjointInline = String.raw`$\langle A \rangle = \operatorname{tr}(A\, \mat

Loss channel

The action of an independent loss channel is to map part of the - population from the qubit subspace into the $|L\rangle$ state. - This corresponds to reducing the trace of the density operator. + population from the qubit subspace into the $|L\rangle$ state. This + corresponds to reducing the trace of the density operator.

The Kraus operators for a loss channel are

{krausLoss}

@@ -84,9 +90,9 @@ const adjointInline = String.raw`$\langle A \rangle = \operatorname{tr}(A\, \mat

{chanPauli}

{chanLossL}

- Note that this channel is not symmetric under forward and - backward propagation. The above action on Paulis corresponds to - the adjoint channel: {adjointInline}. + Note that this channel is not symmetric under forward and backward + propagation. The above action on Paulis corresponds to the + adjoint channel: {adjointInline}.

The action of the channel is clear from a physical perspective: @@ -94,8 +100,8 @@ const adjointInline = String.raw`$\langle A \rangle = \operatorname{tr}(A\, \mat of the qubit subspace.

- However, if there is no loss present in the initial state, we - will never map any part of a Pauli string to $L$. This might seem + However, if there is no loss present in the initial state, we will + never map any part of a Pauli string to $L$. This might seem surprising, but is accurate. Under the loss channel described here, we simply lose population out of the qubit subspace. Any expectation value of a qubit-subspace operator will tend towards @@ -119,11 +125,11 @@ const adjointInline = String.raw`$\langle A \rangle = \operatorname{tr}(A\, \mat

While this is conceptually accurate, the resulting loss does not capture what happens on hardware. If there is no loss detection, - then a qubit that has been lost will be falsely counted as being - in the $|0\rangle$ state when measured. If there is loss - detection, we could either reject the trajectory via - post-selection (equivalent to having no loss in the system), or - reset the qubit to $|0\rangle$ during the circuit. + then a qubit that has been lost will be falsely counted as being in + the $|0\rangle$ state when measured. If there is loss detection, we + could either reject the trajectory via post-selection (equivalent + to having no loss in the system), or reset the qubit to $|0\rangle$ + during the circuit.

@@ -148,23 +154,23 @@ const adjointInline = String.raw`$\langle A \rangle = \operatorname{tr}(A\, \mat

- This reset channel is sufficient to model either actually - resetting a lost qubit, or the error that arises when falsely - counting a lost qubit as 0. For the latter, we simply apply the - loss channel to all qubits at the end of the circuit. This - ensures lost qubits don't partake in the dynamics of the - circuit, but are then counted as 0 in a measurement. + This reset channel is sufficient to model either actually resetting + a lost qubit, or the error that arises when falsely counting a lost + qubit as 0. For the latter, we simply apply the loss channel to all + qubits at the end of the circuit. This ensures lost qubits don't + partake in the dynamics of the circuit, but are then counted as 0 + in a measurement.

- Note that since we are doing Pauli Propagation, we always have - to apply the adjoint circuit. Thus, resetting lost qubits at the - end of the circuit means we apply the reset channel at the very + Note that since we are doing Pauli Propagation, we always have to + apply the adjoint circuit. Thus, resetting lost qubits at the end + of the circuit means we apply the reset channel at the very beginning when propagating a Pauli string.

@@ -172,35 +178,33 @@ const adjointInline = String.raw`$\langle A \rangle = \operatorname{tr}(A\, \mat

Comment on branching and scaling

- Both channels above branch, which without truncation would lead - to exponential scaling. + Both channels above branch, which without truncation would lead to + exponential scaling.

- For the loss channel, it is easy to see that coefficient - truncation can deal with branching, since it only branches on - $L \to p_L I + L$. Branches therefore scale with $p_L$, which is - usually $\ll 1$. + For the loss channel, it is easy to see that coefficient truncation + can deal with branching, since it only branches on $L \to p_L I + + L$. Branches therefore scale with $p_L$, which is usually $\ll 1$.

- The reset channel, on the other hand, branches on both $I$ and - $Z$ into $L$ without any leading coefficient. The probability of - having lost the $i$-th qubit at the end of the circuit is just - given by $\langle L_i \rangle$. Since there are no coherences - between $L$ and the qubit subspace, expectation values - factorize — for any Pauli string with $P_i \neq L_i$: + The reset channel, on the other hand, branches on both $I$ and $Z$ + into $L$ without any leading coefficient. The probability of having + lost the $i$-th qubit at the end of the circuit is just given by + $\langle L_i \rangle$. Since there are no coherences between $L$ + and the qubit subspace, expectation values factorize — for any + Pauli string with $P_i \neq L_i$:

{factorize}

- In a slight abuse of notation, we note that - $\langle L_i \rangle \propto p_L$; the contribution of Pauli - strings that have an $L$ on many positions is very small. - Therefore, we can truncate these to suppress exponential - branching. + In a slight abuse of notation, we note that $\langle L_i \rangle + \propto p_L$; the contribution of Pauli strings that have an $L$ on + many positions is very small. Therefore, we can truncate these to + suppress exponential branching.

- In practice, this works similarly to large-weight truncation and - is achieved by a designated truncation strategy, configured - through max_loss_weight on + In practice, this works similarly to large-weight truncation and is + achieved by a designated truncation strategy, configured through + max_loss_weight on LossyPauliSum.

diff --git a/docs/src/pages/quickstart/python/next-steps.astro b/docs/src/pages/quickstart/python/next-steps.astro new file mode 100644 index 000000000..56d0007ff --- /dev/null +++ b/docs/src/pages/quickstart/python/next-steps.astro @@ -0,0 +1,50 @@ +--- +import Base from "../../../layouts/Base.astro"; +import { pyQuickStartSections } from "../../../data/pyQuickStart"; +const base = import.meta.env.BASE_URL.replace(/\/$/, ""); +const toc = [ + { label: "Keep exploring", href: "#explore" }, + { label: "Other path", href: "#other" }, +]; +--- + +
+
+ +

Next steps

+ +
+ +
+

Keep exploring

+ +
+ +
+

Other path

+ +
+
+ diff --git a/docs/src/pages/tutorials/pauli-propagation.astro b/docs/src/pages/quickstart/python/pauli-propagation.astro similarity index 64% rename from docs/src/pages/tutorials/pauli-propagation.astro rename to docs/src/pages/quickstart/python/pauli-propagation.astro index 3e62ab917..d4df481b7 100644 --- a/docs/src/pages/tutorials/pauli-propagation.astro +++ b/docs/src/pages/quickstart/python/pauli-propagation.astro @@ -1,18 +1,20 @@ --- -import Base from "../../layouts/Base.astro"; +import Base from "../../../layouts/Base.astro"; +import ExampleBlock from "../../../components/ExampleBlock.astro"; +import paulisumGhz from "../../../../examples/paulisum_ghz.py?raw"; +import { pyQuickStartSections } from "../../../data/pyQuickStart"; const base = import.meta.env.BASE_URL.replace(/\/$/, ""); const toc = [ - { label: "Overview", href: "#overview" }, - { label: "Basic usage", href: "#basic-usage" }, - { label: "Term notation", href: "#term-notation" }, - { label: "Truncation", href: "#truncation" }, + { label: "Overview", href: "#overview" }, + { label: "Basic usage", href: "#basic-usage" }, + { label: "Term notation", href: "#term-notation" }, + { label: "GHZ example", href: "#ghz" }, + { label: "Truncation", href: "#truncation" }, { label: "Simulating loss", href: "#loss" }, ]; -// Code snippets live in the frontmatter as raw strings so the -// `
` body stays a plain `{snippet}` expression. Embedding
-// Python f-strings directly in Astro JSX would otherwise need awkward
-// brace escapes (`{`{i}`}`) that obscure the rendered code.
+// Inline Python snippets — f-string `{...}` braces inside JSX would need
+// awkward escaping, so we keep them as raw strings in the frontmatter.
 const basicUsage = `from ppvm import PauliSum
 
 # Observable: Z on each qubit
@@ -50,10 +52,16 @@ print(f": {z_exp}")`;
 
 const lossWeight = `ps = LossyPauliSum.new(3, "ZZZ", max_loss_weight=2)`;
 ---
-
-  
+ +
- +

Pauli Propagation

This makes it possible to simulate large, deep circuits — including - noisy ones — at a fraction of the cost of full state-vector simulation. + noisy ones — at a fraction of the cost of full state-vector + simulation.

@@ -90,13 +99,24 @@ const lossWeight = `ps = LossyPauliSum.new(3, "ZZZ", max_loss_weight=2)`;

Term notation

- Terms can be specified as full Pauli strings ("XZI") or - in compact notation ("X0Z1" — Pauli + qubit index). + Terms can be specified as full Pauli strings ("XZI") + or in compact notation ("X0Z1" — Pauli + qubit index). Coefficients default to 1.0 but can be set explicitly:

{termNotation}
+
+

End-to-end: GHZ expectation values

+

+ For observable-centric studies — “what is the expectation value of + this operator after this circuit?” — the snippet below propagates + the all-Z observable backwards through a GHZ-prep + circuit and prints its overlap with |0…0⟩: +

+ +
+

Truncation

Two truncation strategies control the approximation/performance trade-off:

@@ -111,6 +131,14 @@ const lossWeight = `ps = LossyPauliSum.new(3, "ZZZ", max_loss_weight=2)`;
{truncationKwargs}
+

+ Pauli propagation scales well when the observable has small support + and the circuit doesn't grow that support too quickly. Truncation + strategies (CoefficientThreshold, + MaxPauliWeight, MaxLossWeight, + CombinedStrategy) let you bound the working set on the + fly. +

@@ -124,9 +152,10 @@ const lossWeight = `ps = LossyPauliSum.new(3, "ZZZ", max_loss_weight=2)`;

This separation exists because we need to extend the Pauli basis to account for loss (see - Loss channel details - for the full background). This comes at a storage overhead — we now - need 3 bits to represent a character in a Pauli string rather than 2. + § 5 Loss channel + details for the full background). This comes at a storage + overhead — we now need 3 bits to represent a character in a Pauli + string rather than 2.

Here is a small example:

{lossExample}
@@ -134,9 +163,9 @@ const lossWeight = `ps = LossyPauliSum.new(3, "ZZZ", max_loss_weight=2)`;
  • Loss weight truncation - (max_loss_weight): drops terms with more than a given - number of L operators. Since the contribution of - strings with L on many positions scales as + (max_loss_weight): drops terms with more than a + given number of L operators. Since the contribution + of strings with L on many positions scales as $p_L^k$, this effectively controls the branching from loss and reset channels.
  • diff --git a/docs/src/pages/quickstart/python/stim.astro b/docs/src/pages/quickstart/python/stim.astro new file mode 100644 index 000000000..e2902223b --- /dev/null +++ b/docs/src/pages/quickstart/python/stim.astro @@ -0,0 +1,68 @@ +--- +import Base from "../../../layouts/Base.astro"; +import ExampleBlock from "../../../components/ExampleBlock.astro"; +import stimSampling from "../../../../examples/stim_sampling.py?raw"; +import { pyQuickStartSections } from "../../../data/pyQuickStart"; +const base = import.meta.env.BASE_URL.replace(/\/$/, ""); +const toc = [ + { label: "Parse and sample", href: "#sample" }, + { label: "Measurement results", href: "#results" }, + { label: "Where to go next", href: "#next" }, +]; +--- + +
    +
    + +

    Stim circuits & sampling

    + +
    + +
    +

    Parse and sample a Stim program

    +

    + The snippet below prepares a GHZ-2 state and tallies the (q₀, q₁) + outcomes over 200 shots. +

    + +
    + +
    +

    Loss-aware measurement results

    +

    + Every shot is a list of MeasurementResult values: + ZERO, ONE, or LOST when an + atom has been lost. Loss is first-class, so you can model + neutral-atom hardware effects without leaving the Stim flow. +

    +

    + The mathematical background for how loss is represented internally + — the extended five-symbol Pauli basis and the channel mappings — + is covered in + § 5 Loss channel + details. +

    +
    + +
    +

    Where to go next

    +

    + Stim is convenient when you already have a circuit description in + that grammar. When you'd rather build the circuit gate-by-gate from + Python — or you need non-Clifford gates the Stim grammar can't + express — continue to § + 3 Generalized Tableau. +

    +
    +
    + diff --git a/docs/src/pages/tutorials/generalized-tableau.astro b/docs/src/pages/quickstart/python/tableau.astro similarity index 55% rename from docs/src/pages/tutorials/generalized-tableau.astro rename to docs/src/pages/quickstart/python/tableau.astro index bbf07c826..7575eb684 100644 --- a/docs/src/pages/tutorials/generalized-tableau.astro +++ b/docs/src/pages/quickstart/python/tableau.astro @@ -1,28 +1,20 @@ --- -import Base from "../../layouts/Base.astro"; +import Base from "../../../layouts/Base.astro"; +import ExampleBlock from "../../../components/ExampleBlock.astro"; +import tableauGhz from "../../../../examples/tableau_ghz.py?raw"; +import { pyQuickStartSections } from "../../../data/pyQuickStart"; const base = import.meta.env.BASE_URL.replace(/\/$/, ""); const toc = [ - { label: "Overview", href: "#overview" }, - { label: "Basic usage", href: "#basic-usage" }, + { label: "Overview", href: "#overview" }, + { label: "Drive it from Python", href: "#basic-usage" }, { label: "Forking for multi-shot", href: "#forking" }, - { label: "Stim circuit support", href: "#stim" }, - { label: "Noise and loss", href: "#noise" }, - { label: "Coefficient pruning", href: "#pruning" }, + { label: "Stim circuit support", href: "#stim" }, + { label: "Noise and loss", href: "#noise" }, + { label: "Coefficient pruning", href: "#pruning" }, ]; -// Python snippets live in the frontmatter so f-string `{...}` braces -// don't fight Astro's JSX parser. See the matching note in -// `pauli-propagation.astro`. -const basicUsage = `from ppvm import GeneralizedTableau - -tab = GeneralizedTableau(n_qubits=2) -tab.h(0) -tab.cnot(0, 1) - -r0 = tab.measure(0) -r1 = tab.measure(1) -print(f"Qubit 0: {r0}, Qubit 1: {r1}") # always correlated (Bell state)`; - +// Snippets in the frontmatter so f-string `{...}` braces don't fight +// Astro's JSX parser. const forking = `tab = GeneralizedTableau(n_qubits=2, seed=42) tab.h(0) tab.cnot(0, 1) @@ -54,23 +46,29 @@ shots = GeneralizedTableau.sample(prog, n_qubits=85, num_shots=100, seed=0)`; const lossExample = `tab = GeneralizedTableau(n_qubits=1, seed=0) tab.loss_channel(0, 1.0) # deterministic loss -print(tab.is_lost(0)) # True +print(tab.is_lost(0)) # True print(tab.measure(0)) # MeasurementResult.LOST tab.reset_loss_channel(0) -print(tab.is_lost(0)) # False`; +print(tab.is_lost(0)) # False`; const pruning = `tab = GeneralizedTableau(n_qubits=4, min_abs_coeff=1e-8)`; --- - -
    + +
    - +

    Generalized Stabilizer Tableau

    @@ -84,40 +82,58 @@ const pruning = `tab = GeneralizedTableau(n_qubits=4, min_abs_coeff=1e-8)`;

    Clifford operations are tracked efficiently in the stabilizer tableau. Non-Clifford gates (T gates, arbitrary rotations) expand - the state into a superposition of stabilizer states, tracked via - a sparse coefficient vector. The cost scales exponentially only - in the number of non-Clifford gates, making it well-suited for - circuits with few T gates and many Clifford operations. + the state into a superposition of stabilizer states, tracked via a + sparse coefficient vector. The cost scales exponentially only in + the number of non-Clifford gates, making this backend well-suited + for circuits with few T gates and many Clifford operations.

-

Basic usage

-
{basicUsage}
+

Drive it from Python

+

+ When you'd rather build the circuit gate-by-gate in Python — or + when you need non-Clifford gates the Stim grammar can't express — + drive a GeneralizedTableau directly: +

+ +

+ The tableau exposes single- and two-qubit Clifford gates, T and + rotation gates for non-Clifford steps, projective Z-basis + measurements, and depolarizing / loss / Pauli-error noise channels. + See the Python API + reference for the full list. +

Forking for multiple shots

- Measurement mutates the tableau, so running multiple shots - requires creating independent copies. Use fork() to - clone the quantum state with a fresh RNG: + Measurement mutates the tableau, so running multiple shots requires + creating independent copies. Use fork() to clone the + quantum state with a fresh RNG:

{forking}

- To preserve the RNG state exactly (e.g., for checkpointing), - use copy.copy() instead. + To preserve the RNG state exactly (e.g. for checkpointing), use + copy.copy() instead.

Stim circuit support

- Circuits in STIM + Circuits in Stim format are parsed and prepared once into a StimProgram, - then executed. + then executed:

{stimUsage}
+

+ For the sample-many-shots shortcut you saw in + § 2, the + sample_stim helper takes care of forking and + bookkeeping for you. +

@@ -132,12 +148,18 @@ const pruning = `tab = GeneralizedTableau(n_qubits=4, min_abs_coeff=1e-8)`;
  • Loss: loss_channel(addr, p) and correlated_loss_channel(addr0, addr1, p).
  • - When a qubit is lost, subsequent measurement returns + When a qubit is lost, a subsequent measurement returns MeasurementResult.LOST instead of ZERO or ONE. You can check and reset loss state:

    {lossExample}
    +

    + The mathematical model behind the loss and reset channels is + documented in + § 5 Loss channel + details. +

    diff --git a/docs/src/pages/tutorials/index.astro b/docs/src/pages/tutorials/index.astro deleted file mode 100644 index a24fc3df9..000000000 --- a/docs/src/pages/tutorials/index.astro +++ /dev/null @@ -1,50 +0,0 @@ ---- -import Base from "../../layouts/Base.astro"; -const base = import.meta.env.BASE_URL.replace(/\/$/, ""); ---- - -
    -
    -
    Conceptual guides
    -

    Tutorials

    - -
    - -
      -
    • - Pauli Propagation -

      - Heisenberg-picture observable propagation with PauliSum: - basic usage, compact and full Pauli notation, the two truncation - knobs that control the approximation/performance trade-off, and - the loss-aware LossyPauliSum variant. -

      -
    • -
    • - Loss channel details -

      - The mathematical background for simulating qubit loss — the - extended five-symbol Pauli basis (I, X, - Y, Z, L), the Kraus - operators of the loss channel, and how it interacts with - downstream gates and measurement. -

      -
    • -
    • - Generalized Stabilizer Tableau -

      - The Schrödinger-picture backend: stabilizer tableau extended with a - sparse coefficient vector for non-Clifford gates, multi-shot - sampling via fork(), executing Stim programs, and the - noise channels available on top. -

      -
    • -
    -
    - diff --git a/docs/src/styles/global.css b/docs/src/styles/global.css index 6eece4cf3..ee177f973 100644 --- a/docs/src/styles/global.css +++ b/docs/src/styles/global.css @@ -632,6 +632,74 @@ li { margin: 0.25em 0; } .page-toc ol { columns: 1; } } +/* Three-column layout: sections nav (left) | content | per-page TOC (right). + Used by the consolidated Python Quick Start so readers can jump between + sections on the left while still seeing the in-page anchors on the right. */ +.page-shell--3col { + max-width: 86rem; + grid-template-columns: 14rem minmax(0, 1fr) 13rem; + gap: 2.2rem; +} +.page-sections { + position: sticky; + top: 4.5rem; + max-height: calc(100vh - 5.5rem); + overflow-y: auto; + font-size: 0.88rem; + padding-right: 0.6rem; + border-right: 1px solid var(--rule); +} +.page-sections ol { + list-style: none; + padding: 0; + margin: 0; + counter-reset: sec; +} +.page-sections li { margin: 0; counter-increment: sec; } +.page-sections a { + display: block; + color: var(--ink-soft); + border: 0; + padding: 0.4em 0.6em 0.4em 0.9em; + border-left: 2px solid transparent; + font-size: 0.92rem; + line-height: 1.35; + text-decoration: none; + transition: color 120ms ease, border-color 120ms ease; +} +.page-sections a:hover { color: var(--ink); } +.page-sections a[aria-current="page"] { + color: var(--accent); + border-left-color: var(--accent); + font-weight: 500; +} +.page-toc--right { + border-right: 0; + border-left: 1px solid var(--rule); + padding-right: 0; + padding-left: 0.6rem; +} +.page-toc--empty { visibility: hidden; } + +@media (max-width: 1100px) { + .page-shell--3col { grid-template-columns: 13rem minmax(0, 1fr); } + .page-toc--right { display: none; } +} +@media (max-width: 900px) { + .page-shell--3col { grid-template-columns: 1fr; } + .page-sections { + position: static; + max-height: none; + overflow: visible; + border-right: 0; + border-bottom: 1px solid var(--rule); + padding: 0 0 1rem; + } + .page-sections ol { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; } + .page-sections a { padding: 0.2em 0; border-left: 0; } + .page-sections a[aria-current="page"] { border-left: 0; } +} + /* Title block */ .titleblock { margin-bottom: 2.6rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--rule); } .titleblock .eyebrow {