Skip to content

feat: qudit noise model (2/5) (#1854) - #1859

Merged
erichulburd merged 11 commits into
masterfrom
1854-qudit-noise-model
Aug 18, 2026
Merged

erichulburd merged 11 commits into
masterfrom
1854-qudit-noise-model

Conversation

@bramathon

@bramathon bramathon commented Jul 4, 2026 •

Copy link
Copy Markdown
Collaborator

Part of the effort to land the experimental noise model and simulation module (originally PR #1848, branch nonbreaking-noise-model) as a reviewable stack. This is PR 2 of 5, stacked on #1858 — review/merge in order. Closes #1854.

This PR introduces the new quax-backed qudit noise model under pyquil/noise/:

  • Channel, MeasurementChannel, ResetChannel, and CycleChannel (_channels.py), and the new NoiseModel container plus DepolarizingNoiseModel / CompositeNoiseModel and fidelity estimators (_noise_model.py).
  • The historical pyquil.noise public API is preserved: pyquil/noise.py is moved to pyquil/noise/_legacy_noise.py and re-exported from pyquil/noise/__init__.py, with the legacy NoiseModel/KrausModel now marked deprecated in favor of the quax-based model.

It also adds qudit DefGate support in quilbase.py (matrix dimensions may now be any perfect power, e.g. 3, 9, …, not just powers of two), which the qutrit noise channels and later simulators rely on, and enables float64 (jax_enable_x64) for the test suite.

A handful of ResetChannel tests verify behavior end-to-end through the density-matrix simulator; since that simulator lands in the next PR, those specific assertions are deferred to #1855 (they are restored there). Everything else in the noise module is fully tested here (test_noise_model.py).

Stack

  1. Drop Python 3.9 #1852 — Drop Python 3.9
  2. Qudit Noise model #1854 — Qudit noise model (this PR)
  3. Experimental exact simulators #1855 — Experimental exact simulators
  4. Trajectory simulator #1856 — Trajectory simulator
  5. Dynamic shape trajectory simulator #1857 — Dynamic-shape trajectory simulator

Tracked in #1863.

@github-actions

github-actions Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

🐰 Bencher Report

Projectpyquil
Branch1854-qudit-noise-model
Testbedci-runner-linux

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencyseconds (s)
test/benchmarks/test_program.py::test_copy_everything_except_instructions📈 view plot
⚠️ NO THRESHOLD
11.08 s
test/benchmarks/test_program.py::test_instructions📈 view plot
⚠️ NO THRESHOLD
3.74 s
test/benchmarks/test_program.py::test_iteration📈 view plot
⚠️ NO THRESHOLD
3.73 s
🐰 View full continuous benchmarking report in Bencher

@asaites asaites self-assigned this Jul 13, 2026
Base automatically changed from 1852-drop-python-39 to master July 16, 2026 19:06

@asaites asaites 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.

I've made several comments, which I hope will be useful. I might spend a little more time looking through _noise_model.py, but for the most part, I think I've gone through this pretty thoroughly (and am doing my best to keep up with the physics).

Most of my comments are more style than anything, but I have a few concerns. Overall, though, this looks pretty solid!

Comment thread test/unit/test_noise.py
Comment thread pyquil/quilbase.py Outdated
Comment thread pyquil/noise/_legacy_noise.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_noise_model.py Outdated
bramathon and others added 2 commits July 17, 2026 08:46
Introduce the new noise model under pyquil/noise/ (Channel, MeasurementChannel,
ResetChannel, CycleChannel and the quax-based NoiseModel), preserving the legacy
pyquil.noise API via _legacy_noise.py. Add qudit DefGate support in quilbase and
enable float64 for tests. Part of splitting PR #1848 into a reviewable stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bramathon
bramathon force-pushed the 1854-qudit-noise-model branch from a2e40e8 to 65aa919 Compare July 17, 2026 09:52
@github-actions

github-actions Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

☂️ Python Coverage

current status: ❌

Overall Coverage

Lines Covered Coverage Threshold Status
8296 7246 87% 87% 🟢

New Files

File Coverage Status
pyquil/noise/init.py 100% 🟢
pyquil/noise/_channels.py 85% 🔴
pyquil/noise/_noise_model.py 76% 🔴
TOTAL 87% 🔴

Modified Files

File Coverage Status
pyquil/quilbase.py 94% 🟢
TOTAL 94% 🟢

updated for commit: 69edd32 by action🐍

Comment thread pyquil/quilbase.py Outdated
Comment thread pyquil/quilbase.py Outdated
Comment thread pyquil/quilbase.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
@bramathon

Copy link
Copy Markdown
Collaborator Author

I've done a substantial refactor to add support for Lindbladian channels. The way this works is as follows:

ChannelProtocol defines the protocol for a channel which provides a superoperator associated with a gate. There are two concrete classes for this: Channel and SuperopChannel. The Channel has the _LindbladianBacked mixin, which means that it not only has a superoperator, but a lindbladian which generates it. This is now our canonical Channel while SuperopChannel is relegated to special use cases where we don't know the Lindbladian.

ResetChannel receives the same treatment, becoming _LindbladianBacked and with two concrete implementations.

CycleChannel and MeasureChannel are unchanged.

Comment thread pyquil/noise/_noise_model.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
@asaites

asaites commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

I think there's a problem with the jax or quax dependency version.

bramathon and others added 4 commits July 21, 2026 17:26
- Remove the awkward `SupportsReal` protocol; type `_resolve_params`
  against the canonical `Sequence[ParameterDesignator]` (drops the
  `ResolvableParam` alias and the unused `Protocol`/`SupportsFloat`/
  `runtime_checkable` imports).
- Inline the one-line noise combination into `Channel.__add__` and have
  `__matmul__` delegate to it (drop `_combine_noise_keeping_gate`).
- Fix D401 (imperative mood) on the two `as_post_gate_noise` docstrings.
- Drop `# type: ignore[attr-defined]` in the channel `__eq__` methods:
  mypy narrows `other` after the `type(self) is not type(other)` guard,
  so the comments were flagged unused.
- Sort the `quilbase` import block (ruff I001).

Makes `ruff check`, `ruff format --check`, and `mypy pyquil` pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViuQ59tEfPw6z36i1fkFhg
The locked jax 0.10.0 predates the `enable_eigvec_derivs` kwarg that
rigetti-quax 0.7.0 passes to `jax.lax.linalg.eig`, so every quax-backed
Channel construction raised `TypeError` in CI. Update the lock to the
newest jax each supported interpreter allows — jax 0.10.2 on Python 3.11
and jax 0.11.0 on Python 3.12 (0.11.0 dropped 3.11) — both of which
include the kwarg. Proper version pinning to follow upstream in quax.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViuQ59tEfPw6z36i1fkFhg
The pinned `pandoc` 2.4b0 (a beta) imports the removed `pkg_resources`
module in `pandoc/utils.py`, which fails under the project's setuptools
>=83 (setuptools dropped `pkg_resources` in 81). The docs `conf.py`
imports `pandoc` in its `builder-inited` handler to render the changelog,
so `make doctest` aborted with `No module named 'pkg_resources'`. The
stable 2.4 release switched to `importlib.resources` and imports cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViuQ59tEfPw6z36i1fkFhg
@bramathon
bramathon marked this pull request as ready for review July 21, 2026 20:19
@bramathon
bramathon requested a review from a team as a code owner July 21, 2026 20:19

@windsurf-bot windsurf-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 file skipped due to size limits:
  • pyquil/noise/_channels.py

Looks good to me 🤙

💡 To request another review, post a new comment with "/windsurf-review".

@bramathon

Copy link
Copy Markdown
Collaborator Author

I think there's a problem with the jax or quax dependency version.

Confirmed, we need to bump the minimum version of quax

@erichulburd erichulburd 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.

I've reviewed everything up to quilbase and test_noise_model, which I'll shoot to cover tomorrow. This round's comments are all discretionary.

Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_channels.py Outdated
Comment thread pyquil/noise/_legacy_noise.py
Comment thread pyquil/noise/_legacy_noise.py
Comment thread pyquil/noise/_noise_model.py Outdated
Comment thread pyquil/noise/_noise_model.py Outdated
Comment thread pyquil/noise/_noise_model.py

@erichulburd erichulburd 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.

Largely looks good to me. I'm going to go through and react to my comments that I care more about with ❗ . Close everything out at your discretion and then I'll approve.

Comment thread pyquil/quilbase.py
Comment thread test/unit/test_noise_model.py Outdated
Comment thread test/unit/test_noise_model.py Outdated
Comment thread pyquil/noise/_noise_model.py Outdated
Comment thread test/unit/test_noise_model.py Outdated
Comment thread pyquil/noise/_channels.py
Comment thread pyquil/noise/_channels.py
Comment thread pyquil/noise/_channels.py Outdated
Correctness fixes, each with a regression test:

- from_coherence_times: the T2 -> Tphi conversion omitted the factor of two on
  T1 (1/T2 = 1/(2*T1) + 1/Tphi), so the default t2 = 2*t1 produced a negative
  pure-dephasing time, NaN fidelities and a LinAlgError. Every physical
  T1 < T2 <= 2*T1 was affected. Fixed in Channel and ResetChannel, with a
  T2 <= 2*T1 guard.
- to_stochastic_channel: hand-rolled Kronecker products used the opposite
  superoperator convention to quax (kron(conj(U), U)) and an inverse that is
  not the superoperator of any unitary, yielding a non-CP, non-TP channel with
  process fidelity 0.003 instead of 0.996. Now composed through quax.
- pauli_twirl: twirled the full process, gate included, which deletes any gate
  whose transfer matrix is not already diagonal (RX(pi/2): 0.9925 -> 0.4975).
  Now twirls error_process and recomposes the gate, which for Clifford gates is
  exactly the standard twirl (1/4^n) sum_k P'_k^dag E P_k with P'_k = U P_k U^dag.
- SuperopChannel.from_pauli_noise: an explicit identity key overrode the
  implicit normalization, breaking trace preservation. Now rejected.
- Channel.__add__: raise on a gate_time mismatch instead of silently
  reinterpreting the other operand's per-unit-time rates.
- SuperopResetChannel.from_reset_fidelity: the argument was applied as a
  depolarizing shrink factor, so requesting 0.9 gave 0.95. Now inverted through
  F = (1 + (d-1)p)/d so the channel reports back the requested fidelity.
- CycleChannel.average_gate_fidelity: average gate fidelity does not factorize
  over a tensor product; convert once from the exact process-fidelity product.
- Channel.__matmul__: no longer aliased to __add__. `@` is the exact
  superoperator composition (returning SuperopChannel); `+` is the
  generator-level noise sum. The two agree only for commuting generators.
- from_random_coherent_error: solve the rotation angle numerically so the
  requested process fidelity is hit for multi-qubit gates too.
- Raise instead of silently clipping out-of-range depolarizing constants,
  readout fidelities and asymmetries, and imaginary gate parameters.
- Guard the qubit-only Pauli constructors and analyses against qudits with a
  message naming the operation, rather than a raw JAX shape error.
- DefPermutationGate.num_args: read prime-power lengths like DefGate does.
- from_isa: crashed on a complex ISA parameter; skipped perfect gate
  fidelities but not perfect readout fidelities.

Review comments addressed:

- Rename the fidelity properties to match quax: fidelity ->
  average_gate_fidelity, pauli_fidelity -> process_fidelity (and the
  infidelities). `fidelity` previously meant an average gate fidelity on gate
  channels but a process fidelity on reset channels.
- Rename unitary -> ideal_unitary, and as_post_gate_noise() -> the
  error_process property, so the error-only object carries the error name.
- CycleChannel: serialize the cycle instruction and DEFCIRCUIT so an
  externally-built cycle survives a round trip; add __or__ so `a | b | c`
  builds one flat cycle; validate by instruction equality rather than str();
  return expanded_instructions as a tuple.
- NoiseModel.from_isa now takes a qcs_sdk InstructionSetArchitecture; the rpcq
  CompilerISA path is from_compiler_isa, deprecated for pyQuil v5.
- Replace NoiseModel's hand-written __init__ with __post_init__ validation.
- Drop the @deprecated decorator from the legacy Kraus NoiseModel: it fired
  from pyQuil's own internals (get_qc(noisy=True)) while the replacement is
  still private.
- Re-export the remaining private names from the old pyquil/noise.py.
- TypeAlias on the union aliases, TypedDict for the packed JSON payloads,
  @Final on the concrete channels, _DEFAULT_GATE_TIME, _resolve_params ->
  _evaluate_parameter_designators, _as_channel -> _to_superop_channel,
  _qudit_dims -> _operator_dims_from_dimension.
- Say "prime power" consistently in the DefGate dimension errors, and add
  qudit table tests for num_args and _integer_base_and_exponent.
- Memoize DepolarizingNoiseModel's synthesized channels; resolve each gate
  unitary once per construction; use math.prod in estimate_program_fidelity;
  drop an unreachable serialization branch.
- Fix the quax keyword in test_from_depolarizing_constant that was failing CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QB5EGDshZbFtBvyfPPQqU
@erichulburd
erichulburd merged commit 1dbd8e9 into master Aug 18, 2026
14 checks passed
@erichulburd
erichulburd deleted the 1854-qudit-noise-model branch August 18, 2026 23:40
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.

Qudit Noise model

3 participants