Repository navigation
feat(python): per-gate truncate kwarg + explicit PauliSum.truncate() - #100
Conversation
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>
|
There was a problem hiding this comment.
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 = Trueto all PyO3 gate/channel signatures ininterface.rs(PauliSum) andinterface_tableau.rs(ignored, for signature symmetry); guardself.inner.truncate()calls behind it; expose newPauliSum.truncate(). - Forward
truncate=truncatefrom Python mixins (CliffordMixin,RotationsMixin,CliffordExtensionMixin,NoiseMixin,LossMixin) and from the explicitLossyPauliSum/amplitude_dampingwrappers; addPauliSum.truncate()Python method. - Add
ppvm-python/test/test_truncate_kwarg.pywith 7 cases covering backward compatibility, deferred truncation, theexchange-equivalence idiom, Σ Z conservation, explicittruncate(), 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.
| self.inner.measure(addr0) | ||
| } | ||
|
|
||
| // All gate methods take a `truncate: bool = true` kwarg |
There was a problem hiding this comment.
technically we can have a truncate step tho? @david-pl
There was a problem hiding this comment.
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.
Summary
Replaces the per-gate auto-truncate with an opt-out: every gate method on
PauliSum/LossyPauliSum(andGeneralizedTableau, for signature symmetry) gains atruncate: bool = Truekwarg, andPauliSumgains a top-leveltruncate()method. Default behaviour is exactly unchanged; existing user code is unaffected.Why
Supersedes #94 (
u1-conserving). That branch added a dedicatedexchange(andxyzz) gate whose only job was to callrxx + ryyback-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 byrxx + ryy's two truncates.Letting the Python caller control
truncatedirectly subsumesexchangeand 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
PauliSuminterface auto-truncate now lives behindif truncate { self.inner.truncate(); }on every gate. The kwarg defaults totrue, so callers that don't pass it see no change.PauliSum.truncate()calls the configured strategy explicitly.CliffordMixin,RotationsMixin,CliffordExtensionMixin,NoiseMixin,LossMixin) forwardtruncate=truncateto 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 ≡ explicittruncate=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=Falsekeeps them visible in the post-gate state.test_truncate_false_pair_reproduces_old_exchange— the new idiom produces the same state as whatexchangedid 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— explicittruncate()is wired through and runs the configured strategy.test_noise_channels_accept_truncate_kwarg— the kwarg works onpauli_errortoo, 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
u1-conservingbranch andexchange/xyzzgates are no longer needed. The deferred-truncate idiom in this PR replaces them; seetest_truncate_false_pair_reproduces_old_exchangefor the one-line migration.u1-conservingbranch will be deleted from the remote once this PR lands.