From 92ad8bf1ea6a5b51ad49d40ab5d1bd0bf0d55ce7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 9 Jul 2026 11:44:58 +0200 Subject: [PATCH] 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)