From 92ad8bf1ea6a5b51ad49d40ab5d1bd0bf0d55ce7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 9 Jul 2026 11:44:58 +0200 Subject: [PATCH 1/2] Add expectation and trace to tableau python bindings --- .../src/interface_tableau.rs | 12 ++++ ppvm-python/src/ppvm/_core.pyi | 2 + ppvm-python/src/ppvm/generalized_tableau.py | 28 +++++++++ .../generalized_tableau/test_expectation.py | 61 +++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 ppvm-python/test/generalized_tableau/test_expectation.py diff --git a/crates/ppvm-python-native/src/interface_tableau.rs b/crates/ppvm-python-native/src/interface_tableau.rs index d4858a256..cbb39d480 100644 --- a/crates/ppvm-python-native/src/interface_tableau.rs +++ b/crates/ppvm-python-native/src/interface_tableau.rs @@ -84,6 +84,18 @@ macro_rules! create_interface { self.inner.coefficients.len() } + /// `⟨ψ|word|ψ⟩` for the multi-qubit Pauli string `word`. + pub fn expectation(&self, word: String) -> f64 { + let w: PauliWord<<$type as Config>::Storage> = word.into(); + self.inner.expectation(&w) + } + + /// `Σ_{P matches pattern} ⟨ψ|P|ψ⟩`. + pub fn trace(&self, pattern: String) -> f64 { + let pat: PauliPattern = pattern.into(); + self.inner.trace(&pat) + } + // clifford pub fn x(&mut self, targets: Vec) { self.inner.x_many(targets.as_slice()); diff --git a/ppvm-python/src/ppvm/_core.pyi b/ppvm-python/src/ppvm/_core.pyi index c4a91ca6d..cb7230727 100644 --- a/ppvm-python/src/ppvm/_core.pyi +++ b/ppvm-python/src/ppvm/_core.pyi @@ -117,6 +117,8 @@ class _GeneralizedTableauBase: def current_measurement_record(self) -> list[int]: ... def coefficients(self) -> dict[int, complex]: ... def num_coefficients(self) -> int: ... + def expectation(self, word: str) -> float: ... + def trace(self, pattern: str) -> float: ... def x(self, targets: Sequence[int]) -> None: ... def y(self, targets: Sequence[int]) -> None: ... def z(self, targets: Sequence[int]) -> None: ... diff --git a/ppvm-python/src/ppvm/generalized_tableau.py b/ppvm-python/src/ppvm/generalized_tableau.py index b0e6cbe2c..0721b4c8f 100644 --- a/ppvm-python/src/ppvm/generalized_tableau.py +++ b/ppvm-python/src/ppvm/generalized_tableau.py @@ -208,6 +208,34 @@ def num_coefficients(self) -> int: """ return self._interface.num_coefficients() + def expectation(self, word: str) -> float: + """Compute ``⟨ψ|word|ψ⟩`` for a single multi-qubit Pauli string. + + Args: + word: A Pauli string such as ``"ZZ"`` or ``"X0Y1"`` (underscores + are ignored). + + Returns: + The real expectation value of ``word`` in the current state. + """ + return self._interface.expectation(word) + + def trace(self, pattern: str) -> float: + """Sum Pauli expectations over every word matching ``pattern``. + + Enumerates each ``PauliWord`` accepted by ``pattern`` and returns + the sum of their expectations. Star quantifiers (``X*``) are not + supported — use counted repetition (``Z?{n}``) or positional + anchors instead. + + Args: + pattern: A Pauli pattern string (e.g. ``"Z?{2}"`` or ``"Z0Z1"``). + + Returns: + The summed expectation value. + """ + return self._interface.trace(pattern) + def u3(self, addr0: int, theta: float, phi: float, lam: float): """Apply the U3 gate to the specified qubit. diff --git a/ppvm-python/test/generalized_tableau/test_expectation.py b/ppvm-python/test/generalized_tableau/test_expectation.py new file mode 100644 index 000000000..25bfc11e0 --- /dev/null +++ b/ppvm-python/test/generalized_tableau/test_expectation.py @@ -0,0 +1,61 @@ +import math + +import pytest + +from ppvm import GeneralizedTableau + + +def _bell() -> GeneralizedTableau: + tab = GeneralizedTableau(2) + tab.h(0) + tab.cnot(0, 1) + return tab + + +def test_expectation_z_on_zero_state(): + tab = GeneralizedTableau(1) + assert tab.expectation("Z") == pytest.approx(1.0, abs=1e-10) + assert tab.expectation("X") == pytest.approx(0.0, abs=1e-10) + assert tab.expectation("I") == pytest.approx(1.0, abs=1e-10) + + +def test_expectation_x_on_plus_state(): + tab = GeneralizedTableau(1) + tab.h(0) + assert tab.expectation("X") == pytest.approx(1.0, abs=1e-10) + assert tab.expectation("Z") == pytest.approx(0.0, abs=1e-10) + + +def test_bell_state_pauli_expectations(): + tab = _bell() + assert tab.expectation("II") == pytest.approx(1.0, abs=1e-10) + assert tab.expectation("ZZ") == pytest.approx(1.0, abs=1e-10) + assert tab.expectation("XX") == pytest.approx(1.0, abs=1e-10) + assert tab.expectation("YY") == pytest.approx(-1.0, abs=1e-10) + assert tab.expectation("IZ") == pytest.approx(0.0, abs=1e-10) + assert tab.expectation("ZI") == pytest.approx(0.0, abs=1e-10) + + +def test_ry_rotation_z_expectation_is_cos_theta(): + tab = GeneralizedTableau(1) + theta = math.pi / 3 + tab.ry(0, theta) + assert tab.expectation("Z") == pytest.approx(math.cos(theta), abs=1e-10) + assert tab.expectation("X") == pytest.approx(math.sin(theta), abs=1e-10) + + +def test_trace_z_or_identity_pattern_on_bell(): + tab = _bell() + # ⟨II⟩ + ⟨IZ⟩ + ⟨ZI⟩ + ⟨ZZ⟩ = 1 + 0 + 0 + 1 = 2 + assert tab.trace("Z?{2}") == pytest.approx(2.0, abs=1e-10) + + +def test_trace_y_or_identity_pattern_on_bell_is_zero(): + tab = _bell() + # ⟨II⟩ + ⟨IY⟩ + ⟨YI⟩ + ⟨YY⟩ = 1 + 0 + 0 + (-1) = 0 + assert tab.trace("Y?{2}") == pytest.approx(0.0, abs=1e-10) + + +def test_trace_positional_pattern_matches_single_pauli(): + tab = _bell() + assert tab.trace("Z0Z1") == pytest.approx(tab.expectation("ZZ"), abs=1e-10) From 151394ea406b58b316bcc667a509c78f1b4f7cb5 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 10 Jul 2026 09:20:36 +0200 Subject: [PATCH 2/2] docs(python): correct expectation() word-syntax docstring The GeneralizedTableau.expectation docstring advertised "X0Y1" (which panics on the digit) and claimed underscores are ignored (they are dropped from bit placement but still inflate nqubits). Document the actual accepted syntax: a dense I/X/Y/Z string whose length equals the qubit count. Co-Authored-By: Claude Opus 4.8 (1M context) --- ppvm-python/src/ppvm/generalized_tableau.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ppvm-python/src/ppvm/generalized_tableau.py b/ppvm-python/src/ppvm/generalized_tableau.py index 0721b4c8f..53082681c 100644 --- a/ppvm-python/src/ppvm/generalized_tableau.py +++ b/ppvm-python/src/ppvm/generalized_tableau.py @@ -212,8 +212,10 @@ def expectation(self, word: str) -> float: """Compute ``⟨ψ|word|ψ⟩`` for a single multi-qubit Pauli string. Args: - word: A Pauli string such as ``"ZZ"`` or ``"X0Y1"`` (underscores - are ignored). + word: A dense Pauli string with one character per qubit, each + drawn from ``I``, ``X``, ``Y``, ``Z`` (e.g. ``"ZZ"`` for a + two-qubit state). Its length must equal the tableau's qubit + count. Returns: The real expectation value of ``word`` in the current state.