Skip to content

Add amplitude damping noise model (#497)#540

Open
NandanPatel24 wants to merge 11 commits into
TeamGraphix:masterfrom
NandanPatel24:amplitude-damping-noise
Open

Add amplitude damping noise model (#497)#540
NandanPatel24 wants to merge 11 commits into
TeamGraphix:masterfrom
NandanPatel24:amplitude-damping-noise

Conversation

@NandanPatel24

Copy link
Copy Markdown

Summary

  • Implements amplitude damping noise (issue Implement amplitude damping noise model #497), modeling energy relaxation (T₁ decay) where population flows from the excited state |1⟩ to the ground state |0⟩ with probability γ. Adds the single- and two-qubit channels, the Noise element classes, and an AmplitudeDampingNoiseModel, following DepolarisingNoiseModel as the reference.

Closes #497.

Changes

  • graphix/channels.py — adds amplitude_damping_channel(prob) and two_qubit_amplitude_damping_channel(prob), returning KrausChannel objects. Single-qubit operators are K₁ = diag(1, √(1−γ)) and K₂ = [[0, √γ], [0, 0]]; the two-qubit version is the tensor product of two independent single-qubit channels (four operators {Kᵢ⊗Kⱼ}). Unlike the existing Pauli-based channels these operators aren't scalar multiples of Pauli matrices, so they're built explicitly with coef=1.0.

  • graphix/noise_models/amplitude_damping.py (new) — defines AmplitudeDampingNoise, TwoQubitAmplitudeDampingNoise (both deriving from Noise), and AmplitudeDampingNoiseModel (deriving from NoiseModel). The model mirrors DepolarisingNoiseModel's command-injection logic. confuse_result returns the result unchanged, since amplitude damping is a purely quantum channel with no classical readout-error component; readout error can be added by composing with another model via ComposeNoiseModel. (This is why the model omits the measure_error_prob parameter that the depolarising model carries.)

  • graphix/noise_models/init.py — registers the three new classes for top-level import, alongside the depolarising exports.

A convention note

  • The backend (density_matrix.py) applies channels as ρ′ = Σᵢ KᵢρKᵢ† (verified in evolve/evolve_single). The KrausChannel docstring states the daggered-left form Σᵢ Kᵢ†ρKᵢ — for the existing Hermitian Pauli channels this makes no difference, but amplitude damping's K₂ is non-Hermitian, so the operators here are written for the backend's actual KρK† convention (downward |1⟩ → |0⟩ decay). Happy to also fix the docstring in this PR if maintainers prefer.

Tests and soundness

The suite is split between proving the channel matches the Kraus formula (random-state and by-hand-sum tests) and proving it is amplitude damping specifically (deterministic basis-state tests exploiting the channel's asymmetry — these are the soundness argument the issue asks for).

tests/test_kraus.py

  • test_amplitude_damping_channel — single-qubit channel builds the correct K₁, K₂ operators.
  • test_2_qubit_amplitude_damping_channel — two-qubit channel builds the correct four tensor-product operators.

tests/test_density_matrix.py(https://github.com/TeamGraphix/graphix/blob/master/tests/test_density_matrix.py)

  • test_apply_amplitude_damping_channel — channel application matches the by-hand Σ KᵢρKᵢ† sum, on a single qubit and embedded in a larger random register.
  • test_amplitude_damping_ground_state_fixed — |0⟩⟨0| is unchanged for any γ (ground state can't decay).
  • test_amplitude_damping_excited_state_decays — |1⟩⟨1| → (1−γ)|1⟩⟨1| + γ|0⟩⟨0|, the directional T₁ signature.
  • test_amplitude_damping_coherence_decay — off-diagonal coherences scale by √(1−γ), distinguishing amplitude damping from dephasing.
  • test_apply_two_qubit_amplitude_damping_channel — the two-qubit channel equals independent damping on each factor.

tests/test_noise_model.py((https://github.com/TeamGraphix/graphix/blob/master/tests/test_noise_model.py)

  • test_amplitude_damping_command_injection — noise is injected at the correct positions (after N, after E with two-qubit noise, before M, domain-conditioned on X/Z).
  • test_amplitude_damping_confuse_result_is_identity — the model introduces no classical readout error.
  • test_compose_amplitude_damping_depolarising_transpile — composing with a depolarising model injects both noises with the correct type and probability at every command position.
  • test_compose_amplitude_damping_depolarising_simulation — a composed model with both at default (zero) parameters reproduces the ideal statevector.

All checks pass locally (ruff, ruff-format, mypy, pyright, pytest).

Per unitaryHACK's AI-use guidelines: AI assistance was used while developing this contribution, especially to format this PR content, and for debugging. All ideas remain my own

@NandanPatel24

Copy link
Copy Markdown
Author

Hi, kindly review my PR, I have added a few other tests, testing things that the other PRs didn't seem to address, primarily, evaluating the contribution of each of the noise sources(dephasing and damping) together by running them in the same circuit. I am open to any comments and reviews on this PR

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.96%. Comparing base (6edaaef) to head (c3f8c7c).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #540      +/-   ##
==========================================
+ Coverage   88.85%   88.96%   +0.11%     
==========================================
  Files          49       50       +1     
  Lines        7135     7207      +72     
==========================================
+ Hits         6340     6412      +72     
  Misses        795      795              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@pranav97nair

pranav97nair commented Jun 15, 2026

Copy link
Copy Markdown

Hi @NandanPatel24 , thanks for your contribution to Graphix !

Please take a look at the failing ruff and typing checks in CI. Also, Codecov has detected that some lines in your code are not tested. Please include these in your test suite as well.

Your implementation looks correct, and I like the new tests you've added compared to previous PRs. However, something we would really like to do in the test suite is directly comparing the result of a pattern simulation with a random noise probability gamma at a given step against the expected analytical density matrix depending on gamma. This is what we currently do for the depolarizing channel using the Hadamard and RZ patterns in tests/test_noisy_density_matrix.py.

Similarly, we would like to test the resulting density matrix from applying amplitude damping noise at the preparation, entanglement, measurement, and correction steps. This requires computing the state analytically at each step of the pattern. Do you think you could add this to your tests?

@NandanPatel24

Copy link
Copy Markdown
Author

Hi @NandanPatel24 , thanks for your contribution to Graphix !

Please take a look at the failing ruff and typing checks in CI. Also, Codecov has detected that some lines in your code are not tested. Please include these in your test suite as well.

Your implementation looks correct, and I like the new tests you've added compared to previous PRs. However, something we would really like to do in the test suite is directly comparing the result of a pattern simulation with a random noise probability gamma at a given step against the expected analytical density matrix depending on gamma. This is what we currently do for the depolarizing channel using the Hadamard and RZ patterns in tests/test_noisy_density_matrix.py.

Similarly, we would like to test the resulting density matrix from applying amplitude damping noise at the preparation, entanglement, measurement, and correction steps. This requires computing the state analytically at each step of the pattern. Do you think you could add this to your tests?

Hi, I have added the tests that you gave, for both the H and the RZ gates, as well as added the damping at 4 different steps and compared it with the analytical solution

@pranav97nair

Copy link
Copy Markdown

Thanks for the very quick response! Please take a look at the CI as ruff and typecheck are still failing. I will submit a review of your additions as soon as possible.

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

Hi @NandanPatel24 , nice job with this PR! It looks like the CI is still failing due to some ruff issues on tests/test_noise_model.py. Specifically, I believe you need to add an empty line after line 233 and sort the imports at the top of the file.

In addition, please take a look at my specific feedback below. Once you have addressed the mentioned points, you would be on track to be assigned this issue.

Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment on lines +614 to +620
pauli_x = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128)
cz = np.diag([1.0, 1.0, 1.0, -1.0]).astype(np.complex128)
plus = np.array([1.0, 1.0], dtype=np.complex128) / np.sqrt(2)

rho: npt.NDArray[np.complex128] = np.kron(np.outer(plus, plus.conj()), np.outer(plus, plus.conj())).astype(
np.complex128
)

@pranav97nair pranav97nair Jun 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You can reuse existing Graphix primitives here! Instead of redefining the plus state vector and density matrix, use this:

Suggested change
pauli_x = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128)
cz = np.diag([1.0, 1.0, 1.0, -1.0]).astype(np.complex128)
plus = np.array([1.0, 1.0], dtype=np.complex128) / np.sqrt(2)
rho: npt.NDArray[np.complex128] = np.kron(np.outer(plus, plus.conj()), np.outer(plus, plus.conj())).astype(
np.complex128
)
rho = DensityMatrix(data=[BasicStates.PLUS, BasicStates.PLUS]).rho

And you may also replace pauli_x and cz with Ops.X and Ops.CZ, respectively. Remember to handle any typing issues with numpy arrays.

Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread graphix/noise_models/amplitude_damping.py
@NandanPatel24

Copy link
Copy Markdown
Author

Hi @NandanPatel24 , nice job with this PR! It looks like the CI is still failing due to some ruff issues on tests/test_noise_model.py. Specifically, I believe you need to add an empty line after line 233 and sort the imports at the top of the file.

In addition, please take a look at my specific feedback below. Once you have addressed the mentioned points, you would be on track to be assigned this issue.

Hi, I have implemented the changes that you recommended, have a look at those once, and the ruff and CI issues should now be fixed

@NandanPatel24

Copy link
Copy Markdown
Author

Hi @pranav97nair, please provide your reviews

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

Hi @NandanPatel24 , thanks for your response! I am overall quite satisfied with your code, and would like to go ahead and assign you the issue. To become an eligible assignee, please first comment on Issue #497 with a reference to your PR, preferably today.

I have however requested some minor changes, so please take a look at them below. To ensure you are replicating the CI type-checks properly in your local environment, take a look at the configuration in .github/workflows/typecheck.yml.

Also, note that we have a two‑approval rule for non‑trivial PRs: therefore, once I or another maintainer approves this PR, we'll need a second approval before merging.

Comment thread graphix/channels.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
self, rho: npt.NDArray[np.complex128], qubit: int, nqubit: int, gamma: float
) -> npt.NDArray[np.complex128]:
"""Apply amplitude damping to a single ``qubit`` of an ``nqubit`` register."""
eye = np.eye(2, dtype=np.complex128)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you use the in-built Ops.I to represent the single-qubit identity everywhere?

Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
Comment thread tests/test_noisy_density_matrix.py Outdated
@pranav97nair

Copy link
Copy Markdown

Congratulations on being assigned Issue #497 for UnitaryHack, @NandanPatel24 ! You should receive the bounty once we merge this PR and close the issue.

@NandanPatel24

NandanPatel24 commented Jun 17, 2026

Copy link
Copy Markdown
Author

Hi @pranav97nair , I have made the required changes in commit c3f8c7c

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

Hi @NandanPatel24 , please take a look at the below comments. In addition to addressing these, can you also update the CHANGELOG.md with an entry at the top briefly summarizing what is being added in this PR ?

Comment thread tests/test_noisy_density_matrix.py Outdated
self, rho: npt.NDArray[np.complex128], qubit: int, nqubit: int, gamma: float
) -> npt.NDArray[np.complex128]:
"""Apply amplitude damping to a single ``qubit`` of an ``nqubit`` register."""
eye = np.asarray(Ops.I, dtype=np.complex128)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I believe this wrapper is unnecessary as Ops.I is of type NDArray[complex128]. Please use Ops.I directly everywhere you currently have eye.

Comment thread tests/test_noisy_density_matrix.py Outdated

# measured qubit 0 in the X basis (XY plane, angle 0)
proj = np.kron(self._xy_projector(outcome, 0.0), eye)
rho = proj @ rho @ proj.conj().T

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A projective operator should always be Hermitian, so you can safely remove the .conj().T. Same for _rz_expected.

Comment thread tests/test_noise_model.py Outdated
Comment on lines +120 to +122
assert isinstance(out[1], ApplyNoise)
assert isinstance(out[1].noise, AmplitudeDampingNoise)
assert out[1].nodes == [0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could you factorize all the assert statements related to the noise command into a helper function? To this function you can pass the expected noise class, node, gamma, and domain for each command type (N, E, M, X, Z).

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.

Implement amplitude damping noise model

2 participants