Skip to content

feat(tableau): peek observable expectation — string observables + loss handling#187

Closed
rafaelha wants to merge 1 commit into
mainfrom
rafael/peek-observable-expectation
Closed

feat(tableau): peek observable expectation — string observables + loss handling#187
rafaelha wants to merge 1 commit into
mainfrom
rafael/peek-observable-expectation

Conversation

@rafaelha

Copy link
Copy Markdown
Collaborator

What changes

Adds a non-destructive observable-expectation peek to GeneralizedTableau, layered on top of the expectation machinery from #172/#176:

  • crates/ppvm-tableau/src/observable.rs (new): parsing of Pauli-observable strings. Two notations, each with an optional leading +/- sign:

    • sparse Stim MPP-style products: "X0*X3*Z5*Y7", "-Z0*Y1";
    • dense "IXYZ…" of length n_qubits, with _ accepted as an alias for I so Stim PauliString text ("+__X_Y_Z") parses directly.

    Malformed, out-of-range, or repeated-qubit observables produce a typed ObservableParseError.

  • GeneralizedTableau::pauli_expectation(factors, negate): returns None when any qubit in the observable's support has been lost; otherwise builds the word and delegates to the existing expectation() (feat(tableau): add Pauli expectation/trace and reset_all #172). No state is disturbed.

  • GeneralizedTableau::peek_observable_expectation(&str): string front-end combining the two.

  • Python: GeneralizedTableau.peek_observable_expectation(observable) returns an ExpectationResult — a frozen dataclass with a LOST sentinel, is_lost, and __float__ (which raises on LOST). Complements the plain expectation()/trace() from Add expectation and trace to tableau python bindings #176 with sparse/signed syntax, input validation (ValueError), and loss-aware semantics.

Relation to #172 / #176

This work originally carried its own multi-qubit decomposition path; it has been rebased to reuse compute_decomposition_word and expectation() from #172 instead (the only data.rs change left is making the PhasedPauliWordNoHash alias pub(crate)). What it adds on top is the observable-string grammar, lost-qubit handling, and the Python result type.

Testing

🤖 Generated with Claude Code

…ss handling

Add a non-destructive observable-expectation peek to GeneralizedTableau,
end to end:

- crates/ppvm-tableau/src/observable.rs: parse Pauli-observable strings —
  Stim-style sparse products ("X0*X3*Z5*Y7", optional leading +/-) and
  dense "IXYZ…" / "_"-for-identity forms — with typed errors for
  malformed, out-of-range, or repeated-qubit observables.
- GeneralizedTableau::pauli_expectation((qubit, Pauli) factors + sign):
  returns None when the observable's support touches a lost qubit,
  otherwise delegates to the existing expectation() machinery (#172).
- GeneralizedTableau::peek_observable_expectation(&str): string front-end.
- Python: peek_observable_expectation returning an ExpectationResult
  (frozen dataclass with a LOST sentinel and __float__), plus stubs.

Rebased over #172/#176, which added the core expectation/trace machinery
independently: this commit now reuses their compute_decomposition_word
and expectation() instead of carrying its own decomposition path, and
contributes the string parsing, loss semantics, and Python result type
on top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 14:26
@rafaelha
rafaelha marked this pull request as draft July 17, 2026 14:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a non-destructive “peek” API for Pauli observable expectations on GeneralizedTableau, including a Rust-side observable string parser (dense and sparse Stim-style syntax), loss-aware semantics (None/LOST when support touches a lost qubit), and Python bindings with an ExpectationResult wrapper plus tests.

Changes:

  • Add crates/ppvm-tableau::observable to parse signed dense/sparse Pauli observable strings into typed factors.
  • Add GeneralizedTableau::{pauli_expectation, peek_observable_expectation} (Rust) and GeneralizedTableau.peek_observable_expectation() + ExpectationResult (Python), including loss handling.
  • Add Rust + Python test coverage for parsing, correctness vs known examples, loss behavior, and non-disturbance.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ppvm-python/test/generalized_tableau/test_peek_expectation.py Adds Python tests for peek expectation behavior, parsing forms, loss semantics, and Stim cross-validation.
ppvm-python/src/ppvm/generalized_tableau.py Adds ExpectationResult dataclass and GeneralizedTableau.peek_observable_expectation() wrapper.
ppvm-python/src/ppvm/_core.pyi Extends native interface typing to include peek_observable_expectation.
ppvm-python/src/ppvm/init.py Re-exports ExpectationResult from the public Python package.
crates/ppvm-tableau/src/observable.rs New Rust module implementing dense/sparse observable parsing + tests.
crates/ppvm-tableau/src/measure.rs Adds Rust-side expectation peek APIs and related tests.
crates/ppvm-tableau/src/lib.rs Registers the new observable module and re-exports ObservableParseError in the prelude.
crates/ppvm-tableau/src/data.rs Makes PhasedPauliWordNoHash available to the crate for reuse by peek/expectation code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +56
impl std::fmt::Display for ObservableParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ObservableParseError::BadToken(t) => {
write!(f, "malformed Pauli factor {t:?}; expected e.g. \"X0\"")
}
ObservableParseError::QubitOutOfRange { qubit, n_qubits } => {
write!(
f,
"qubit {qubit} out of range for a {n_qubits}-qubit system"
)
}
ObservableParseError::RepeatedQubit(q) => {
write!(f, "qubit {q} appears in more than one factor")
}
ObservableParseError::DenseLengthMismatch { got, n_qubits } => write!(
f,
"dense observable has length {got}, expected {n_qubits} (one Pauli per qubit)"
),
}
}
}
Comment on lines +141 to +146
if rest.chars().count() != n_qubits {
return Err(ObservableParseError::DenseLengthMismatch {
got: rest.chars().count(),
n_qubits,
});
}
Comment on lines +606 to +618
pub fn pauli_expectation(&self, factors: &[(usize, Pauli)], negate: bool) -> Option<f64> {
for &(qubit, pauli) in factors {
if pauli != Pauli::I && self.is_lost[qubit] {
return None;
}
}
let mut word = PhasedPauliWordNoHash::<T::Storage, T::BuildHasher>::new(self.n_qubits());
for &(qubit, pauli) in factors {
word.set(qubit, pauli);
}
let value = self.expectation(&word.word);
Some(if negate { -value } else { value })
}
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-17 21:09 UTC

@rafaelha

Copy link
Copy Markdown
Collaborator Author

This is actually handled sufficiently well by the expectation API from #172. The current API introduces inconsistent API surfaces. Closing it.

@rafaelha rafaelha closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants