Skip to content

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

Open
bramathon wants to merge 10 commits into
masterfrom
1854-qudit-noise-model
Open

feat: qudit noise model (2/5) (#1854)#1859
bramathon wants to merge 10 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
8.21 s
test/benchmarks/test_program.py::test_instructions📈 view plot
⚠️ NO THRESHOLD
2.59 s
test/benchmarks/test_program.py::test_iteration📈 view plot
⚠️ NO THRESHOLD
2.60 s
🐰 View full continuous benchmarking report in Bencher

@asaites asaites self-assigned this Jul 13, 2026
@bramathon
bramathon force-pushed the 1852-drop-python-39 branch from f1884bc to 414f175 Compare July 15, 2026 18:08
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 on lines +770 to +776
if rows < 2:
return 0
for base in range(2, rows + 1):
k = int(round(math.log(rows, base)))
if base**k == rows:
return k
return 1

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.

This might not matter much for the expected values of rows/reasonable values of len(self.matrix), but it is very slow for large numbers. As a much faster alternative, you can calculate it by iteratively checking if $\sqrt[p]{r} \in \mathbb{Z}$ for $p \in \text{PRIME}$, stopping when $\sqrt[p]{r} &lt; 2$. If so, it's a factor of k, and you can reduce the problem to finding the solution for that new base, then multiply its factors by the one you've found.

Concretely:

def _int_n_root(x: int, n: int) -> int | None:
    """Return the largest integer less than or equal to n-th root of x."""
    # This helper quickly checks if `x` is a perfect `n-th` root,
    # and will do so without the numerical instability
    # of the floating point check.
    # There are faster algorithms for very large numbers,
    # but this is already O(log(x)), and simple to implement.

    low, high = 1, x
    while low <= high:
        mid = (low + high) // 2
        if (mid_pow_n := mid ** n) == x:
            return mid
        elif mid_pow_n < x:
            low = mid + 1
        else:
            high = mid - 1
    return low - 1

SMALL_PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61]

def find_k(r: int) -> tuple[int, int]:
    """For positive integer `r`, return integers `(b, k)` such that `r = b**k`
    and `k` is the largest integer for any integer `b >= 1` that satisfies this relation.

    If `r` is not a perfect power, this function returns `(r, 1)`.
    """
    if r < 2:
        return r, 1

    # Check if `r` is a perfect p-th power of a small prime `p`.
    # When it is, reduce `r` to that p-th root and build up the exponent `k`.

    base, k = r, 1
    primes = iter(SMALL_PRIMES)
    p = next(primes, None)
    while p:
        if (b := _int_n_root(base, p)) < 2:
            return base, k
        elif b ** p != base:
            p = next(primes, None)
        else:
            k *= p
            base = b

    raise ValueError("r is too large to efficiently find k")

This approach is $O(\log^{2}{n})$ versus $O(n^2)$.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

On further thought, this entire concept has a huge problem - it doesn't support quarts. Besides qubits and qutrits this the most likely qudit size. I think I'll need to add a option to specify the qudit dims

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No good solution to this right now. I think we will just have to accept only prime qudit dims

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
For a matrix of dimension d^k, returns k where d is the smallest
integer base >= 2 such that rows = d^k.
"""
decomposition = _integer_base_and_exponent(len(self.matrix))

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.

This doesn't work if the exponent is composite.

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

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

2 participants