Skip to content

feat(python): per-gate truncate kwarg + explicit PauliSum.truncate() - #100

Merged
david-pl merged 2 commits into
mainfrom
deferred-truncate
Jun 3, 2026
Merged

david-pl merged 2 commits into
mainfrom
deferred-truncate

Conversation

@AlexSchuckert

Copy link
Copy Markdown
Collaborator

Summary

Replaces the per-gate auto-truncate with an opt-out: every gate method on PauliSum / LossyPauliSum (and GeneralizedTableau, for signature symmetry) gains a truncate: bool = True kwarg, and PauliSum gains a top-level truncate() method. Default behaviour is exactly unchanged; existing user code is unaffected.

ps.rxx(a, b, theta, truncate=False)
ps.ryy(a, b, theta, truncate=False)
ps.truncate()        # one strategy run at the end of the commuting pair

Why

Supersedes #94 (u1-conserving). That branch added a dedicated exchange (and xyzz) gate whose only job was to call rxx + ryy back-to-back in Rust so the PyO3-level auto-truncate fired once instead of twice — the U(1) charge structure is preserved by the single PyO3 truncate, not by rxx + ryy's two truncates.

Letting the Python caller control truncate directly subsumes exchange and generalises to any sequence of commuting gates (xyzz = rxx + ryy + rzz, longer fusions, mixed Clifford-rotation blocks). No new dedicated gates needed, no new Rust traits, smaller API surface.

Design

  • The PyO3 PauliSum interface auto-truncate now lives behind if truncate { self.inner.truncate(); } on every gate. The kwarg defaults to true, so callers that don't pass it see no change.
  • A new PauliSum.truncate() calls the configured strategy explicitly.
  • The Python mixins (CliffordMixin, RotationsMixin, CliffordExtensionMixin, NoiseMixin, LossMixin) forward truncate=truncate to the PyO3 layer.
  • GeneralizedTableau's PyO3 binding also accepts the kwarg and silently ignores it — the tableau backend doesn't auto-truncate, so there's nothing to defer, but the parallel signature lets the mixins serve both backends uniformly.

Tests

ppvm-python/test/test_truncate_kwarg.py (7 cases):

  • test_default_truncate_kwarg_is_true — backward-compat: no kwarg ≡ explicit truncate=True.
  • test_truncate_false_then_explicit_matches_implicit — at a threshold that doesn't actually drop anything, deferred ≡ immediate.
  • test_truncate_false_keeps_intermediate_terms_alive — at a threshold where the immediate strategy would drop intermediates, truncate=False keeps them visible in the post-gate state.
  • test_truncate_false_pair_reproduces_old_exchange — the new idiom produces the same state as what exchange did internally.
  • test_deferred_truncate_preserves_total_z_with_loose_threshold — sanity: rxx+ryy chain with deferred truncate conserves Σ Z exactly.
  • test_truncate_method_drops_below_threshold_keys — explicit truncate() is wired through and runs the configured strategy.
  • test_noise_channels_accept_truncate_kwarg — the kwarg works on pauli_error too, not only on rotations.

Test plan

  • cargo test -p ppvm-runtime --lib (78 pass)
  • uv run --project ppvm-python --group dev pytest ppvm-python/test/ (109 pass)
  • cargo fmt, cargo clippy --no-deps, ruff check ppvm-python/

Notes

  • Closes feat(runtime): U(1)-conserving Pauli propagation helpers #94: the u1-conserving branch and exchange / xyzz gates are no longer needed. The deferred-truncate idiom in this PR replaces them; see test_truncate_false_pair_reproduces_old_exchange for the one-line migration.
  • The u1-conserving branch will be deleted from the remote once this PR lands.

Lets the Python caller decide when the configured truncation strategy
fires, instead of forcing a truncate after every gate. Every gate method
on the PyO3 PauliSum interface and its Python wrappers now takes an
optional `truncate: bool = True` kwarg, plus a top-level
`PauliSum.truncate()` method.

```python
ps.rxx(a, b, theta, truncate=False)
ps.ryy(a, b, theta, truncate=False)
ps.truncate()        # one strategy run at the end of the commuting pair
```

This subsumes the (now-deleted) `u1-conserving` branch's `exchange`
gate, which was just `rxx + ryy` back-to-back in Rust with a single
PyO3-level truncate at the end — exactly what the kwarg makes
expressible without a dedicated method. The same idiom generalises to
any sequence of commuting gates (`xyzz = rxx + ryy + rzz`, longer
fusions, mixed Clifford-rotation blocks).

Default `truncate=True` matches the previous behaviour exactly:
existing user code is untouched. The mixins (`CliffordMixin`,
`RotationsMixin`, `CliffordExtensionMixin`, `NoiseMixin`, `LossMixin`)
all forward the new kwarg, so it works on both `PauliSum` and
`LossyPauliSum`. The `GeneralizedTableau` PyO3 binding accepts the
kwarg for signature symmetry and ignores its value — the tableau
backend doesn't auto-truncate, so there's nothing to defer.

Tests (`ppvm-python/test/test_truncate_kwarg.py`, 7 cases):

- `test_default_truncate_kwarg_is_true` — backward-compat: no kwarg
  ≡ explicit `truncate=True`.
- `test_truncate_false_then_explicit_matches_implicit` — at a
  threshold where nothing is dropped, deferred ≡ immediate.
- `test_truncate_false_keeps_intermediate_terms_alive` — at a
  threshold where the immediate strategy *would* drop intermediates,
  `truncate=False` keeps them visible in the post-gate state.
- `test_truncate_false_pair_reproduces_old_exchange` — the new idiom
  produces the same state as what `exchange` did internally.
- `test_deferred_truncate_preserves_total_z_with_loose_threshold` —
  sanity: rxx+ryy chain with deferred truncate conserves Σ Z exactly.
- `test_truncate_method_drops_below_threshold_keys` — explicit
  `truncate()` is wired through and runs the configured strategy.
- `test_noise_channels_accept_truncate_kwarg` — the kwarg works on
  `pauli_error` too, not only on rotations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 29, 2026 12:44
@github-actions

github-actions Bot commented May 29, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-03 12:45 UTC

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

Replaces the implicit per-gate auto-truncate with an opt-out truncate: bool = True kwarg on every gate/channel method of PauliSum, LossyPauliSum, and GeneralizedTableau (where it is silently ignored), and adds an explicit PauliSum.truncate() method. Default behavior is unchanged, but callers can now defer truncation across commuting gate sequences (e.g. rxx + ryy for an XY-exchange step) to preserve conserved-charge components — a more general replacement for the dedicated exchange/xyzz gates proposed in #94.

Changes:

  • Add truncate: bool = True to all PyO3 gate/channel signatures in interface.rs (PauliSum) and interface_tableau.rs (ignored, for signature symmetry); guard self.inner.truncate() calls behind it; expose new PauliSum.truncate().
  • Forward truncate=truncate from Python mixins (CliffordMixin, RotationsMixin, CliffordExtensionMixin, NoiseMixin, LossMixin) and from the explicit LossyPauliSum/amplitude_damping wrappers; add PauliSum.truncate() Python method.
  • Add ppvm-python/test/test_truncate_kwarg.py with 7 cases covering backward compatibility, deferred truncation, the exchange-equivalence idiom, Σ Z conservation, explicit truncate(), and noise-channel kwarg.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
crates/ppvm-python-native/src/interface.rs Adds truncate kwarg to all gate/channel bindings, guards inner truncate() call, exposes new truncate() method.
crates/ppvm-python-native/src/interface_tableau.rs Adds truncate kwarg (silently ignored) to all tableau gate/channel bindings for API symmetry.
ppvm-python/src/ppvm/paulisum.py Forwards truncate for amplitude_damping and LossyPauliSum channels; adds top-level truncate() method with docstring.
ppvm-python/src/ppvm/mixins.py Adds truncate: bool = True keyword-only argument to all mixin gate/channel methods and forwards to interface.
ppvm-python/test/test_truncate_kwarg.py New tests exercising default behaviour, deferred truncation, explicit truncate(), exchange-equivalence and noise-channel kwarg.

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

@david-pl david-pl linked an issue May 29, 2026 that may be closed by this pull request

@Roger-luo Roger-luo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this is an easy PR, I think it would be nice to add a truncate option for tableau too just for completeness before the final release. @david-pl

self.inner.measure(addr0)
}

// All gate methods take a `truncate: bool = true` kwarg

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

technically we can have a truncate step tho? @david-pl

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes, both options are valid. Just have to decide. I still think it's nice to have the default behavior be to automatically truncate after every gate. When working with this, I noticed that this is usually what you want since it gives you the best performance. That's why I bundled it initially. It's also how PP.jl does it.

If we make it a separate step, the user would have to add that after every gate to recover the current default behavior. Is there a more convenient way to have the truncate after each gate behavior when it's a separate step? Otherwise, I'm leaning towards the kwarg approach here.

@david-pl
david-pl enabled auto-merge (squash) June 3, 2026 12:41
@david-pl
david-pl merged commit 4ae44e3 into main Jun 3, 2026
7 checks passed
@david-pl
david-pl deleted the deferred-truncate branch June 3, 2026 12:45
david-pl added a commit that referenced this pull request Jun 9, 2026
This slipped through my review of #100. That PR added a dead truncate
kwarg on the `GeneralizedTableau`, which was part of the public method,
but didn't do anything. This would be quite surprising to users.

Also, this currently breaks CI for #101.
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.

Make truncation after every step optional in PP python bindings

4 participants