-
Notifications
You must be signed in to change notification settings - Fork 66
Add amplitude damping noise model (#497) #540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c33e65b
2deb23a
cf1e21d
bec57de
096f00a
2dc824d
af09971
dda1110
54cfebe
c3f8c7c
ebad847
6bd72e5
ff8a83b
d22d508
81498f2
1d5b095
9c80bb2
9ff6976
8a0bf99
e503549
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| """Amplitude damping noise model.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import typing_extensions | ||
|
|
||
| from graphix.channels import ( | ||
| KrausChannel, | ||
| amplitude_damping_channel, | ||
| two_qubit_amplitude_damping_channel, | ||
| ) | ||
| from graphix.command import BaseM, CommandKind | ||
| from graphix.measurements import toggle_outcome | ||
| from graphix.noise_models.noise_model import ApplyNoise, Noise, NoiseModel | ||
| from graphix.rng import ensure_rng | ||
| from graphix.utils import Probability | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Iterable | ||
|
|
||
| from numpy.random import Generator | ||
|
|
||
| from graphix.measurements import Outcome | ||
| from graphix.noise_models.noise_model import CommandOrNoise | ||
|
|
||
|
|
||
| class AmplitudeDampingNoise(Noise): | ||
| """One-qubit amplitude damping noise with damping parameter ``prob``.""" | ||
|
|
||
| prob = Probability() | ||
|
|
||
| def __init__(self, prob: float) -> None: | ||
| r"""Initialize one-qubit amplitude damping noise. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| prob : float | ||
| Damping parameter :math:`\\gamma` of the noise, between 0 and 1. | ||
| """ | ||
| self.prob = prob | ||
|
|
||
| @property | ||
| @typing_extensions.override | ||
| def nqubits(self) -> int: | ||
| """Return the number of qubits targetted by the noise element.""" | ||
| return 1 | ||
|
|
||
| @typing_extensions.override | ||
| def to_kraus_channel(self) -> KrausChannel: | ||
| """Return the Kraus channel describing the noise element.""" | ||
| return amplitude_damping_channel(self.prob) | ||
|
|
||
|
|
||
| class TwoQubitAmplitudeDampingNoise(Noise): | ||
| """Two-qubit amplitude damping noise with damping parameter ``prob``.""" | ||
|
|
||
| prob = Probability() | ||
|
|
||
| def __init__(self, prob: float) -> None: | ||
| r"""Initialize two-qubit amplitude damping noise. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| prob : float | ||
| Damping parameter :math:`\\gamma` of the noise, between 0 and 1. | ||
| """ | ||
| self.prob = prob | ||
|
|
||
| @property | ||
| @typing_extensions.override | ||
| def nqubits(self) -> int: | ||
| """Return the number of qubits targetted by the noise element.""" | ||
| return 2 | ||
|
|
||
| @typing_extensions.override | ||
| def to_kraus_channel(self) -> KrausChannel: | ||
| """Return the Kraus channel describing the noise element.""" | ||
| return two_qubit_amplitude_damping_channel(self.prob) | ||
|
|
||
|
|
||
| class AmplitudeDampingNoiseModel(NoiseModel): | ||
| r"""Amplitude damping noise model. | ||
|
|
||
| :param NoiseModel: Parent abstract class class:`NoiseModel` | ||
| :type NoiseModel: class | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| prepare_error_prob: float = 0.0, | ||
| x_error_prob: float = 0.0, | ||
| z_error_prob: float = 0.0, | ||
| entanglement_error_prob: float = 0.0, | ||
| measure_channel_prob: float = 0.0, | ||
| measure_error_prob: float = 0.0, | ||
| ) -> None: | ||
| self.prepare_error_prob = prepare_error_prob | ||
| self.x_error_prob = x_error_prob | ||
| self.z_error_prob = z_error_prob | ||
| self.entanglement_error_prob = entanglement_error_prob | ||
| self.measure_channel_prob = measure_channel_prob | ||
| self.measure_error_prob = measure_error_prob | ||
|
|
||
| @typing_extensions.override | ||
| def input_nodes( | ||
| self, nodes: Iterable[int], rng: Generator | None = None, *, stacklevel: int = 1 | ||
| ) -> list[CommandOrNoise]: | ||
| """Return the noise to apply to input nodes.""" | ||
| return [ApplyNoise(noise=AmplitudeDampingNoise(self.prepare_error_prob), nodes=[node]) for node in nodes] | ||
|
|
||
| @typing_extensions.override | ||
| def command( | ||
| self, cmd: CommandOrNoise, rng: Generator | None = None, *, stacklevel: int = 1 | ||
| ) -> list[CommandOrNoise]: | ||
| """Return the noise to apply to the command ``cmd``.""" | ||
| match cmd.kind: | ||
| case CommandKind.N: | ||
| return [cmd, ApplyNoise(noise=AmplitudeDampingNoise(self.prepare_error_prob), nodes=[cmd.node])] | ||
| case CommandKind.E: | ||
| return [ | ||
| cmd, | ||
| ApplyNoise( | ||
| noise=TwoQubitAmplitudeDampingNoise(self.entanglement_error_prob), nodes=list(cmd.nodes) | ||
| ), | ||
| ] | ||
| case CommandKind.M: | ||
| return [ApplyNoise(noise=AmplitudeDampingNoise(self.measure_channel_prob), nodes=[cmd.node]), cmd] | ||
| case CommandKind.X: | ||
| return [ | ||
| cmd, | ||
| ApplyNoise(noise=AmplitudeDampingNoise(self.x_error_prob), nodes=[cmd.node], domain=cmd.domain), | ||
| ] | ||
| case CommandKind.Z: | ||
| return [ | ||
| cmd, | ||
| ApplyNoise(noise=AmplitudeDampingNoise(self.z_error_prob), nodes=[cmd.node], domain=cmd.domain), | ||
| ] | ||
| case CommandKind.C | CommandKind.T | CommandKind.ApplyNoise: | ||
| return [cmd] | ||
| case CommandKind.S: | ||
| raise ValueError("Unexpected signal!") | ||
| case _: # pragma: no cover | ||
| typing_extensions.assert_never(cmd.kind) | ||
|
|
||
| @typing_extensions.override | ||
| def confuse_result( | ||
| self, cmd: BaseM, result: Outcome, rng: Generator | None = None, *, stacklevel: int = 1 | ||
| ) -> Outcome: | ||
| """Assign wrong measurement result with probability ``measure_error_prob``.""" | ||
| rng = ensure_rng(rng, stacklevel=stacklevel + 1) | ||
| if rng.uniform() < self.measure_error_prob: | ||
| return toggle_outcome(result) | ||
| return result | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,13 @@ | |||||
| import graphix.random_objects as randobj | ||||||
| from graphix import command | ||||||
| from graphix.branch_selector import ConstBranchSelector | ||||||
| from graphix.channels import KrausChannel, dephasing_channel, depolarising_channel | ||||||
| from graphix.channels import ( | ||||||
| KrausChannel, | ||||||
| amplitude_damping_channel, | ||||||
| dephasing_channel, | ||||||
| depolarising_channel, | ||||||
| two_qubit_amplitude_damping_channel, | ||||||
| ) | ||||||
| from graphix.fundamentals import ANGLE_PI, Plane | ||||||
| from graphix.ops import Ops | ||||||
| from graphix.sim.density_matrix import DensityMatrix, DensityMatrixBackend | ||||||
|
|
@@ -569,7 +575,7 @@ def test_apply_dephasing_channel(self, fx_rng: Generator) -> None: | |||||
| dm = DensityMatrix(randobj.rand_dm(2, fx_rng)) | ||||||
|
|
||||||
| # copy of initial dm | ||||||
| rho_test = dm.rho | ||||||
| rho_test = np.asarray(dm.rho, dtype=np.complex128) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to change this?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for that is the plain
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't observe any errors from either mypy or pyright when I revert that line to
Suggested change
Could you check again on your side? Moreover, this code was already in |
||||||
|
|
||||||
| # create dephasing channel | ||||||
| prob = fx_rng.uniform() | ||||||
|
|
@@ -647,7 +653,7 @@ def test_apply_depolarising_channel(self, fx_rng: Generator) -> None: | |||||
| dm = DensityMatrix(randobj.rand_dm(2, fx_rng)) | ||||||
|
|
||||||
| # copy of initial dm | ||||||
| rho_test = dm.rho | ||||||
| rho_test = np.asarray(dm.rho, dtype=np.complex128) | ||||||
|
thierry-martinez marked this conversation as resolved.
|
||||||
|
|
||||||
| # create dephasing channel | ||||||
| prob = fx_rng.uniform() | ||||||
|
|
@@ -736,6 +742,95 @@ def test_apply_depolarising_channel(self, fx_rng: Generator) -> None: | |||||
| assert np.allclose(expected_dm.trace(), 1.0) | ||||||
| assert np.allclose(dm.rho, expected_dm) | ||||||
|
|
||||||
| def test_apply_amplitude_damping_channel(self, fx_rng: Generator) -> None: | ||||||
| # check on single qubit first, against the by-hand Kraus sum | ||||||
| dm = DensityMatrix(randobj.rand_dm(2, fx_rng)) | ||||||
| rho_test = np.asarray(dm.rho, dtype=np.complex128) | ||||||
|
|
||||||
| gamma = fx_rng.uniform() | ||||||
| ad_channel = amplitude_damping_channel(gamma) | ||||||
|
|
||||||
| assert isinstance(ad_channel, KrausChannel) | ||||||
|
|
||||||
| dm.apply_channel(ad_channel, [0]) | ||||||
|
|
||||||
| k1 = np.array([[1.0, 0.0], [0.0, np.sqrt(1 - gamma)]], dtype=np.complex128) | ||||||
| k2 = np.array([[0.0, np.sqrt(gamma)], [0.0, 0.0]], dtype=np.complex128) | ||||||
| expected_dm = k1 @ rho_test @ k1.conj().T + k2 @ rho_test @ k2.conj().T | ||||||
|
|
||||||
| assert np.allclose(expected_dm.trace(), 1.0) | ||||||
| assert np.allclose(dm.rho, expected_dm) | ||||||
|
|
||||||
| # check embedded in a larger random register | ||||||
| nqubits = int(fx_rng.integers(2, 5)) | ||||||
| i = int(fx_rng.integers(0, nqubits)) | ||||||
|
|
||||||
| psi = _randstate_raw(nqubits, fx_rng) | ||||||
| psi /= np.sqrt(np.sum(np.abs(psi) ** 2)) | ||||||
| dm = DensityMatrix(data=np.outer(psi, psi.conj())) | ||||||
|
|
||||||
| gamma = fx_rng.uniform() | ||||||
| ad_channel = amplitude_damping_channel(gamma) | ||||||
| dm.apply_channel(ad_channel, [i]) | ||||||
|
|
||||||
| expected_dm = np.zeros((2**nqubits, 2**nqubits), dtype=np.complex128) | ||||||
| for elem in ad_channel: | ||||||
| psi_evolved = np.tensordot(elem.operator, psi.reshape((2,) * nqubits), (1, i)) | ||||||
| psi_evolved = np.moveaxis(psi_evolved, 0, i).reshape(2**nqubits) | ||||||
| expected_dm += elem.coef * np.conj(elem.coef) * np.outer(psi_evolved, psi_evolved.conj()) | ||||||
|
|
||||||
| assert np.allclose(expected_dm.trace(), 1.0) | ||||||
| assert np.allclose(dm.rho, expected_dm) | ||||||
|
|
||||||
| @pytest.mark.parametrize("gamma", [0.0, 0.2, 0.5, 0.9, 1.0]) | ||||||
| def test_amplitude_damping_ground_state_fixed(self, gamma: float) -> None: | ||||||
| # ``|0><0|`` is a fixed point of amplitude damping for any gamma. | ||||||
| ket0 = np.array([[1.0, 0.0], [0.0, 0.0]], dtype=np.complex128) | ||||||
| dm = DensityMatrix(data=BasicStates.ZERO) | ||||||
| dm.apply_channel(amplitude_damping_channel(gamma), [0]) | ||||||
| assert np.allclose(dm.rho, ket0) | ||||||
|
|
||||||
| @pytest.mark.parametrize("gamma", [0.0, 0.2, 0.5, 0.9, 1.0]) | ||||||
| def test_amplitude_damping_excited_state_decays(self, gamma: float) -> None: | ||||||
| # ``|1><1| -> (1 - gamma)|1><1| + gamma|0><0|`` (directional T1 decay). | ||||||
| ket0 = np.array([[1.0, 0.0], [0.0, 0.0]], dtype=np.complex128) | ||||||
| ket1 = np.array([[0.0, 0.0], [0.0, 1.0]], dtype=np.complex128) | ||||||
| dm = DensityMatrix(data=BasicStates.ONE) | ||||||
| dm.apply_channel(amplitude_damping_channel(gamma), [0]) | ||||||
| assert np.allclose(dm.rho, (1 - gamma) * ket1 + gamma * ket0) | ||||||
|
|
||||||
| @pytest.mark.parametrize("gamma", [0.1, 0.4, 0.8]) | ||||||
| def test_amplitude_damping_coherence_decay(self, gamma: float) -> None: | ||||||
| # Off-diagonal coherences scale by ``sqrt(1 - gamma)`` (distinct from dephasing). | ||||||
| dm = DensityMatrix(data=BasicStates.PLUS) | ||||||
| dm.apply_channel(amplitude_damping_channel(gamma), [0]) | ||||||
| assert np.isclose(dm.rho[0, 1], np.sqrt(1 - gamma) / 2) | ||||||
| assert np.isclose(dm.rho[1, 0], np.sqrt(1 - gamma) / 2) | ||||||
|
|
||||||
| @pytest.mark.parametrize("gamma", [0.0, 0.25, 0.6, 1.0]) | ||||||
| def test_apply_two_qubit_amplitude_damping_channel(self, gamma: float, fx_rng: Generator) -> None: | ||||||
| # The two-qubit channel equals independent damping on each factor. | ||||||
| a = _randstate_raw(1, fx_rng) | ||||||
| a /= np.sqrt(np.sum(np.abs(a) ** 2)) | ||||||
| b = _randstate_raw(1, fx_rng) | ||||||
| b /= np.sqrt(np.sum(np.abs(b) ** 2)) | ||||||
| rho_a = np.outer(a, a.conj()) | ||||||
| rho_b = np.outer(b, b.conj()) | ||||||
|
|
||||||
| dm = DensityMatrix(data=np.kron(rho_a, rho_b)) | ||||||
| dm.apply_channel(two_qubit_amplitude_damping_channel(gamma), [0, 1]) | ||||||
|
|
||||||
| k1 = np.array([[1.0, 0.0], [0.0, np.sqrt(1 - gamma)]], dtype=np.complex128) | ||||||
| k2 = np.array([[0.0, np.sqrt(gamma)], [0.0, 0.0]], dtype=np.complex128) | ||||||
|
|
||||||
| def single(rho: npt.NDArray[np.complex128]) -> npt.NDArray[np.complex128]: | ||||||
| return k1 @ rho @ k1.conj().T + k2 @ rho @ k2.conj().T | ||||||
|
|
||||||
| expected = np.kron(single(rho_a), single(rho_b)) | ||||||
|
|
||||||
| assert np.allclose(expected.trace(), 1.0) | ||||||
| assert np.allclose(dm.rho, expected) | ||||||
|
|
||||||
| def test_apply_random_channel_one_qubit(self, fx_rng: Generator) -> None: | ||||||
| """Test using complex parameters.""" | ||||||
| # check against statevector backend by hand for now. | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.