diff --git a/graphix/__init__.py b/graphix/__init__.py index 12f8da89c..d3d28a818 100644 --- a/graphix/__init__.py +++ b/graphix/__init__.py @@ -13,7 +13,7 @@ from graphix.graphsim import GraphState from graphix.instruction import Instruction from graphix.measurements import BlochMeasurement, Measurement, PauliMeasurement -from graphix.noise_models import DepolarisingNoiseModel, NoiseModel +from graphix.noise_models import AmplitudeDampingNoiseModel, DepolarisingNoiseModel, NoiseModel from graphix.opengraph import OpenGraph from graphix.optimization import StandardizedPattern from graphix.parameter import Placeholder @@ -27,6 +27,7 @@ __all__ = [ "ANGLE_PI", + "AmplitudeDampingNoiseModel", "Axis", "BasicStates", "BlochMeasurement", diff --git a/graphix/channels.py b/graphix/channels.py index 8bb0ebc1b..41321a80f 100644 --- a/graphix/channels.py +++ b/graphix/channels.py @@ -178,6 +178,63 @@ def dephasing_channel(prob: float) -> KrausChannel: ) +def amplitude_damping_channel(gamma: float) -> KrausChannel: + r"""Single-qubit amplitude damping channel. + + .. math:: + K_1 = \begin{pmatrix} + 1 & 0 \\ + 0 & \sqrt{1-\gamma} + \end{pmatrix},\quad + K_2 = \begin{pmatrix} + 0 & \sqrt{\gamma} \\ + 0 & 0 + \end{pmatrix} + + Parameters + ---------- + gamma : float + The damping probability, between 0 and 1. + + Returns + ------- + :class:`graphix.channels.KrausChannel` object + containing the corresponding Kraus operators + """ + return KrausChannel( + [ + KrausData(1.0, np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=np.complex128)), + KrausData(1.0, np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=np.complex128)), + ] + ) + + +def two_qubit_amplitude_damping_channel(gamma: float) -> KrausChannel: + r"""Two-qubit amplitude damping channel (independent tensor product). + + The two-qubit channel is formed by the tensor product of two single-qubit + amplitude damping channels, yielding 4 Kraus operators: + + .. math:: + \{K_1 \otimes K_1,\; K_1 \otimes K_2,\; K_2 \otimes K_1,\; K_2 \otimes K_2\} + + Parameters + ---------- + gamma : float + The damping probability, between 0 and 1. + + Returns + ------- + :class:`graphix.channels.KrausChannel` object + containing the corresponding Kraus operators + """ + single_ops = [ + np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=np.complex128), + np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=np.complex128), + ] + return KrausChannel([KrausData(1.0, np.kron(left, right)) for left in single_ops for right in single_ops]) + + def depolarising_channel(prob: float) -> KrausChannel: r"""Single-qubit depolarizing channel. diff --git a/graphix/noise_models/__init__.py b/graphix/noise_models/__init__.py index 1d74beaac..fb7a375e3 100644 --- a/graphix/noise_models/__init__.py +++ b/graphix/noise_models/__init__.py @@ -4,6 +4,11 @@ from typing import TYPE_CHECKING +from graphix.noise_models.amplitude_damping import ( + AmplitudeDampingNoise, + AmplitudeDampingNoiseModel, + TwoQubitAmplitudeDampingNoise, +) from graphix.noise_models.depolarising import DepolarisingNoise, DepolarisingNoiseModel, TwoQubitDepolarisingNoise from graphix.noise_models.noise_model import ( ApplyNoise, @@ -16,11 +21,14 @@ from graphix.noise_models.noise_model import CommandOrNoise as CommandOrNoise __all__ = [ + "AmplitudeDampingNoise", + "AmplitudeDampingNoiseModel", "ApplyNoise", "ComposeNoiseModel", "DepolarisingNoise", "DepolarisingNoiseModel", "Noise", "NoiseModel", + "TwoQubitAmplitudeDampingNoise", "TwoQubitDepolarisingNoise", ] diff --git a/graphix/noise_models/amplitude_damping.py b/graphix/noise_models/amplitude_damping.py new file mode 100644 index 000000000..dd2dddad3 --- /dev/null +++ b/graphix/noise_models/amplitude_damping.py @@ -0,0 +1,153 @@ +"""Amplitude damping noise model.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import typing_extensions + +from graphix.channels import 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.channels import KrausChannel + from graphix.measurements import Outcome + from graphix.noise_models.noise_model import CommandOrNoise + + +class AmplitudeDampingNoise(Noise): + """One-qubit amplitude damping noise with damping parameter ``gamma``.""" + + gamma = Probability() + + def __init__(self, gamma: float) -> None: + """Initialize one-qubit amplitude damping noise. + + Parameters + ---------- + gamma : float + Damping parameter of the noise, between 0 and 1. + """ + self.gamma = gamma + + @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.gamma) + + +class TwoQubitAmplitudeDampingNoise(Noise): + """Two-qubit amplitude damping noise with damping parameter ``gamma``.""" + + gamma = Probability() + + def __init__(self, gamma: float) -> None: + """Initialize two-qubit amplitude damping noise. + + Parameters + ---------- + gamma : float + Damping parameter of the noise, between 0 and 1. + """ + self.gamma = gamma + + @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.gamma) + + +class AmplitudeDampingNoiseModel(NoiseModel): + """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_error_prob = measure_error_prob + self.measure_channel_prob = measure_channel_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 _: + 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 cmd = "M".""" + rng = ensure_rng(rng, stacklevel=stacklevel + 1) + if rng.uniform() < self.measure_error_prob: + return toggle_outcome(result) + return result diff --git a/tests/test_kraus.py b/tests/test_kraus.py index f7e4983f0..b7e55881f 100644 --- a/tests/test_kraus.py +++ b/tests/test_kraus.py @@ -9,8 +9,10 @@ from graphix.channels import ( KrausChannel, KrausData, + amplitude_damping_channel, dephasing_channel, depolarising_channel, + two_qubit_amplitude_damping_channel, two_qubit_depolarising_channel, two_qubit_depolarising_tensor_channel, ) @@ -180,3 +182,45 @@ def test_2_qubit_depolarising_tensor_channel(self, fx_rng: Generator) -> None: for i in range(len(depol_tensor_channel_2_qubit)): assert np.allclose(depol_tensor_channel_2_qubit[i].coef, data[i].coef) assert np.allclose(depol_tensor_channel_2_qubit[i].operator, data[i].operator) + + def test_amplitude_damping_channel(self, fx_rng: Generator) -> None: + gamma = fx_rng.uniform() + data = [ + KrausData(1.0, np.array([[1.0, 0.0], [0.0, np.sqrt(1 - gamma)]], dtype=np.complex128)), + KrausData(1.0, np.array([[0.0, np.sqrt(gamma)], [0.0, 0.0]], dtype=np.complex128)), + ] + + channel = amplitude_damping_channel(gamma) + + assert channel.nqubit == 1 + assert len(channel) == 2 + + for i in range(len(channel)): + assert np.allclose(channel[i].coef, data[i].coef) + assert np.allclose(channel[i].operator, data[i].operator) + + @pytest.mark.parametrize("gamma", [-0.1, 1.1]) + def test_amplitude_damping_channel_invalid_gamma(self, gamma: float) -> None: + with pytest.raises(ValueError, match="The specified channel is not normalized"): + amplitude_damping_channel(gamma) + with pytest.raises(ValueError, match="The specified channel is not normalized"): + two_qubit_amplitude_damping_channel(gamma) + + def test_2_qubit_amplitude_damping_channel(self, fx_rng: Generator) -> None: + gamma = fx_rng.uniform() + one_qubit_channel = amplitude_damping_channel(gamma) + data = [ + KrausData(left.coef * right.coef, np.kron(left.operator, right.operator)) + for left in one_qubit_channel + for right in one_qubit_channel + ] + + channel = two_qubit_amplitude_damping_channel(gamma) + + assert isinstance(channel, KrausChannel) + assert channel.nqubit == 2 + assert len(channel) == 4 + + for i in range(len(channel)): + assert np.allclose(channel[i].coef, data[i].coef) + assert np.allclose(channel[i].operator, data[i].operator) diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index 3babd2a4c..ace29967e 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -4,17 +4,21 @@ import numpy as np import pytest +import typing_extensions from graphix import Pattern -from graphix.command import CommandKind, M, N +from graphix.command import CommandKind, M, N, S, T from graphix.noise_models import ( + AmplitudeDampingNoise, + AmplitudeDampingNoiseModel, ApplyNoise, ComposeNoiseModel, DepolarisingNoise, DepolarisingNoiseModel, + TwoQubitAmplitudeDampingNoise, TwoQubitDepolarisingNoise, ) -from graphix.noise_models.noise_model import NoiselessNoiseModel +from graphix.noise_models.noise_model import NoiselessNoiseModel, NoiseModel from graphix.random_objects import rand_circuit from graphix.sim.density_matrix import DensityMatrix from graphix.simulator import DefaultMeasureMethod @@ -100,7 +104,90 @@ def test_compose_noise_model_simulation(fx_rng: Generator) -> None: assert np.abs(np.dot(state_mbqc.flatten().conjugate(), DensityMatrix(state).rho.flatten())) == pytest.approx(1) -def test_confuse_result(fx_rng: Generator) -> None: +def test_amplitude_damping_noise_model_transpile(fx_rng: Generator) -> None: + noise_model = AmplitudeDampingNoiseModel( + prepare_error_prob=0.1, + entanglement_error_prob=0.2, + measure_channel_prob=0.3, + x_error_prob=0.4, + z_error_prob=0.5, + ) + nqubits = 5 + depth = 5 + pattern = rand_circuit(nqubits, depth, rng=fx_rng).transpile().pattern + noisy_pattern = noise_model.transpile(pattern, rng=fx_rng) + iterator = iter(noisy_pattern) + + for cmd in pattern: + if cmd.kind == CommandKind.M: + assert_apply_noise(next(iterator), AmplitudeDampingNoise, 0.3, [cmd.node]) + assert next(iterator) == cmd + match cmd.kind: + case CommandKind.N: + assert_apply_noise(next(iterator), AmplitudeDampingNoise, 0.1, [cmd.node]) + case CommandKind.E: + assert_apply_noise(next(iterator), TwoQubitAmplitudeDampingNoise, 0.2, list(cmd.nodes)) + case CommandKind.X: + assert_apply_noise(next(iterator), AmplitudeDampingNoise, 0.4, [cmd.node], cmd.domain) + case CommandKind.Z: + assert_apply_noise(next(iterator), AmplitudeDampingNoise, 0.5, [cmd.node], cmd.domain) + case CommandKind.C | CommandKind.M | CommandKind.T: + pass + case CommandKind.S: + raise AssertionError("Unexpected signal in pattern") + case _: + typing_extensions.assert_never(cmd.kind) + + +def test_amplitude_damping_noise_model_command_edge_cases() -> None: + noise_model = AmplitudeDampingNoiseModel() + apply_noise = ApplyNoise(noise=AmplitudeDampingNoise(0.2), nodes=[0]) + tick = T() + + assert noise_model.command(tick) == [tick] + assert noise_model.command(apply_noise) == [apply_noise] + with pytest.raises(ValueError, match="Unexpected signal"): + noise_model.command(S(0)) + + +def test_amplitude_damping_noise_model_input_nodes() -> None: + noise_model = AmplitudeDampingNoiseModel(prepare_error_prob=0.25) + cmds = noise_model.input_nodes([0, 1]) + assert len(cmds) == 2 + assert_apply_noise(cmds[0], AmplitudeDampingNoise, 0.25, [0]) + assert_apply_noise(cmds[1], AmplitudeDampingNoise, 0.25, [1]) + + +def test_amplitude_damping_noise_nqubits() -> None: + assert AmplitudeDampingNoise(0.3).nqubits == 1 + assert TwoQubitAmplitudeDampingNoise(0.3).nqubits == 2 + + +def test_amplitude_damping_noise_to_kraus_channel(fx_rng: Generator) -> None: + gamma = fx_rng.uniform() + channel = AmplitudeDampingNoise(gamma).to_kraus_channel() + assert channel.nqubit == 1 + assert len(channel) == 2 + + +def test_two_qubit_amplitude_damping_noise_to_kraus_channel(fx_rng: Generator) -> None: + gamma = fx_rng.uniform() + channel = TwoQubitAmplitudeDampingNoise(gamma).to_kraus_channel() + assert channel.nqubit == 2 + assert len(channel) == 4 + + +def test_amplitude_damping_confuse_result_unchanged(fx_rng: Generator) -> None: + noise_model = AmplitudeDampingNoiseModel(measure_error_prob=0.0) + assert noise_model.confuse_result(M(0), 0, rng=fx_rng) == 0 + assert noise_model.confuse_result(M(0), 1, rng=fx_rng) == 1 + + +@pytest.mark.parametrize( + "noise_model", + [DepolarisingNoiseModel(measure_error_prob=1), AmplitudeDampingNoiseModel(measure_error_prob=1)], +) +def test_confuse_result(fx_rng: Generator, noise_model: NoiseModel) -> None: # Pattern that measures 0 on qubit 0 with probability 1. pattern = Pattern(cmds=[N(0), M(0)]) measure_method = DefaultMeasureMethod() @@ -108,9 +195,22 @@ def test_confuse_result(fx_rng: Generator) -> None: backend="densitymatrix", noise_model=NoiselessNoiseModel(), rng=fx_rng, measure_method=measure_method ) assert measure_method.results[0] == 0 - noise_model = DepolarisingNoiseModel(measure_error_prob=1) measure_method = DefaultMeasureMethod() pattern.simulate_pattern( backend="densitymatrix", noise_model=noise_model, rng=fx_rng, measure_method=measure_method ) assert measure_method.results[0] == 1 + + +def assert_apply_noise( + cmd: CommandOrNoise, + noise_type: type[AmplitudeDampingNoise | TwoQubitAmplitudeDampingNoise], + gamma: float, + nodes: list[int], + domain: set[int] | None = None, +) -> None: + assert isinstance(cmd, ApplyNoise) + assert isinstance(cmd.noise, noise_type) + assert cmd.noise.gamma == gamma + assert cmd.nodes == nodes + assert cmd.domain == domain diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 104923265..5d9006c05 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -7,12 +7,16 @@ import pytest from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector +from graphix.channels import amplitude_damping_channel, two_qubit_amplitude_damping_channel from graphix.command import CommandKind from graphix.fundamentals import angle_to_rad -from graphix.noise_models import DepolarisingNoiseModel +from graphix.measurements import Measurement +from graphix.noise_models import AmplitudeDampingNoiseModel, DepolarisingNoiseModel from graphix.noise_models.noise_model import NoiselessNoiseModel from graphix.ops import Ops +from graphix.sim.base_backend import _outcome_to_operator_matrix from graphix.sim.density_matrix import DensityMatrix +from graphix.states import BasicStates from graphix.transpiler import Circuit if TYPE_CHECKING: @@ -28,6 +32,24 @@ def rz_exact_res(alpha: Angle) -> npt.NDArray[np.float64]: return 0.5 * np.array([[1, np.exp(-1j * rad)], [np.exp(1j * rad), 1]]) +def single_qubit_amplitude_damping_exact( + rho: npt.NDArray[np.complex128 | np.float64], gamma: float +) -> npt.NDArray[np.complex128]: + """Apply the single-qubit amplitude damping channel to a 2x2 density matrix.""" + return np.array( + [ + [rho[0, 0] + gamma * rho[1, 1], np.sqrt(1 - gamma) * rho[0, 1]], + [np.sqrt(1 - gamma) * rho[1, 0], (1 - gamma) * rho[1, 1]], + ], + dtype=np.complex128, + ) + + +def rz_measurement_results(rzpattern: Pattern, z_outcome: Outcome, x_outcome: Outcome) -> dict[int, Outcome]: + m_nodes = (cmd.node for cmd in rzpattern if cmd.kind == CommandKind.M) + return {next(m_nodes): z_outcome, next(m_nodes): x_outcome} + + def hpat() -> Pattern: circ = Circuit(1) circ.h(0) @@ -524,3 +546,291 @@ def test_noisy_measure_confuse_rz_arbitrary( or np.allclose(res.rho, Ops.Z @ exact @ Ops.Z) or np.allclose(res.rho, Ops.Z @ Ops.X @ exact @ Ops.X @ Ops.Z) ) + + +class TestAmplitudeDampingAnalytic: + """Analytic per-step verification of amplitude damping noise.""" + + @staticmethod + def _kraus_operators(gamma: float) -> list[npt.NDArray[np.complex128]]: + return [ + np.array([[1.0, 0.0], [0.0, np.sqrt(1 - gamma)]], dtype=np.complex128), + np.array([[0.0, np.sqrt(gamma)], [0.0, 0.0]], dtype=np.complex128), + ] + + @staticmethod + def apply_amplitude_damping_single_qubit( + rho: npt.NDArray[np.complex128], gamma: float + ) -> npt.NDArray[np.complex128]: + return sum( + (k @ rho @ k.conj().T for k in TestAmplitudeDampingAnalytic._kraus_operators(gamma)), + np.zeros((2, 2), dtype=np.complex128), + ) + + @staticmethod + def apply_amplitude_damping_on_qubit( + rho: npt.NDArray[np.complex128], qubit: int, gamma: float + ) -> npt.NDArray[np.complex128]: + eye = np.eye(2, dtype=np.complex128) + out = np.zeros(rho.shape, dtype=np.complex128) + for kraus in TestAmplitudeDampingAnalytic._kraus_operators(gamma): + full = np.kron(kraus, eye) if qubit == 0 else np.kron(eye, kraus) + out += full @ rho @ full.conj().T + return out + + @staticmethod + def apply_amplitude_damping_two_qubit(rho: npt.NDArray[np.complex128], gamma: float) -> npt.NDArray[np.complex128]: + out = np.zeros(rho.shape, dtype=np.complex128) + for left in TestAmplitudeDampingAnalytic._kraus_operators(gamma): + for right in TestAmplitudeDampingAnalytic._kraus_operators(gamma): + full = np.kron(left, right) + out += full @ rho @ full.conj().T + return out + + @staticmethod + def _apply_measurement_and_trace_out( + dm: DensityMatrix, qubit: int, measurement: Measurement, outcome: Outcome + ) -> None: + bloch = measurement.to_bloch() + vec = bloch.plane.polar(bloch.angle) + op_mat = _outcome_to_operator_matrix(vec, outcome) + dm.evolve_single(op_mat, qubit) + dm.remove_qubit(qubit) + + def _expected_hadamard_pattern(self, step: str, gamma: float, outcome: Outcome) -> npt.NDArray[np.complex128]: + eye = np.eye(2, dtype=np.complex128) + rho: npt.NDArray[np.complex128] = np.asarray( + DensityMatrix(data=[BasicStates.PLUS, BasicStates.PLUS]).rho, + dtype=np.complex128, + ) + + if step == "prep": + rho = self.apply_amplitude_damping_on_qubit(rho, 0, gamma) + rho = self.apply_amplitude_damping_on_qubit(rho, 1, gamma) + cz = np.asarray(Ops.CZ, dtype=np.complex128) + rho = cz @ rho @ cz.conj().T + if step == "entangle": + rho = self.apply_amplitude_damping_two_qubit(rho, gamma) + if step == "measure": + rho = self.apply_amplitude_damping_on_qubit(rho, 0, gamma) + x_projector = np.kron( + np.asarray( + DensityMatrix(data=BasicStates.PLUS if outcome == 0 else BasicStates.MINUS).rho, + dtype=np.complex128, + ), + eye, + ) + rho = x_projector @ rho @ x_projector.conj().T + rho = rho / np.trace(rho) + reduced: npt.NDArray[np.complex128] = np.einsum("ijik->jk", rho.reshape(2, 2, 2, 2)) + x_op = np.asarray(Ops.X, dtype=np.complex128) + if outcome == 1: + reduced = x_op @ reduced @ x_op.conj().T + if step == "xcorr" and outcome == 1: + reduced = self.apply_amplitude_damping_single_qubit(reduced, gamma) + return reduced + + def _expected_rz_pattern( + self, + alpha: Angle, + step: str, + gamma: float, + z_outcome: Outcome, + x_outcome: Outcome, + ) -> npt.NDArray[np.complex128]: + dm = DensityMatrix(data=[BasicStates.PLUS, BasicStates.PLUS, BasicStates.PLUS]) + + if step == "prep": + for qubit in (0, 1, 2): + dm.apply_channel(amplitude_damping_channel(gamma), [qubit]) + + dm.entangle((0, 1)) + if step == "entangle": + dm.apply_channel(two_qubit_amplitude_damping_channel(gamma), [0, 1]) + dm.entangle((1, 2)) + if step == "entangle": + dm.apply_channel(two_qubit_amplitude_damping_channel(gamma), [1, 2]) + + if step == "measure": + dm.apply_channel(amplitude_damping_channel(gamma), [0]) + self._apply_measurement_and_trace_out(dm, 0, Measurement.XY(-alpha), z_outcome) + + if step == "measure": + dm.apply_channel(amplitude_damping_channel(gamma), [0]) + self._apply_measurement_and_trace_out(dm, 0, Measurement.X, x_outcome) + + if x_outcome == 1: + dm.evolve_single(np.asarray(Ops.X, dtype=np.complex128), 0) + if step == "xcorr" and x_outcome == 1: + dm.apply_channel(amplitude_damping_channel(gamma), [0]) + + if z_outcome == 1: + dm.evolve_single(np.asarray(Ops.Z, dtype=np.complex128), 0) + if step == "zcorr" and z_outcome == 1: + dm.apply_channel(amplitude_damping_channel(gamma), [0]) + + return np.asarray(dm.rho, dtype=np.complex128) + + @pytest.mark.filterwarnings("ignore:Simulating using densitymatrix backend with no noise.") + def test_noiseless_amplitude_damping_hadamard(self, fx_rng: Generator) -> None: + hadamardpattern = hpat() + noiselessres = hadamardpattern.simulate_pattern(backend="densitymatrix", rng=fx_rng) + noisynoiselessres = hadamardpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(), + rng=fx_rng, + ) + assert isinstance(noiselessres, DensityMatrix) + assert isinstance(noisynoiselessres, DensityMatrix) + assert np.allclose(noiselessres.rho, np.array([[1.0, 0.0], [0.0, 0.0]])) + assert np.allclose(noisynoiselessres.rho, np.array([[1.0, 0.0], [0.0, 0.0]])) + + @pytest.mark.filterwarnings("ignore:Simulating using densitymatrix backend with no noise.") + def test_noiseless_amplitude_damping_rz(self, fx_rng: Generator) -> None: + alpha = fx_rng.random() + rzpattern = rzpat(alpha) + noiselessres = rzpattern.simulate_pattern(backend="densitymatrix", rng=fx_rng) + noisynoiselessres = rzpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(), + rng=fx_rng, + ) + assert isinstance(noiselessres, DensityMatrix) + assert isinstance(noisynoiselessres, DensityMatrix) + assert np.allclose(noiselessres.rho, rz_exact_res(alpha)) + assert np.allclose(noisynoiselessres.rho, rz_exact_res(alpha)) + + def test_noisy_measure_confuse_hadamard(self, fx_rng: Generator) -> None: + hadamardpattern = hpat() + res = hadamardpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(measure_error_prob=1.0), + rng=fx_rng, + ) + assert isinstance(res, DensityMatrix) + assert np.allclose(res.rho, np.array([[0.0, 0.0], [0.0, 1.0]])) + + @pytest.mark.parametrize("outcome", [0, 1]) + @pytest.mark.parametrize( + ("step", "param"), + [ + ("prep", "prepare_error_prob"), + ("entangle", "entanglement_error_prob"), + ("measure", "measure_channel_prob"), + ("xcorr", "x_error_prob"), + ], + ) + def test_amplitude_damping_hadamard_step_matches_analytic( + self, step: str, param: str, outcome: Outcome, fx_rng: Generator + ) -> None: + gamma = fx_rng.random() + res = hpat().simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(**{param: gamma}), + branch_selector=ConstBranchSelector(outcome), + rng=fx_rng, + ) + assert isinstance(res, DensityMatrix) + assert np.allclose(res.rho, self._expected_hadamard_pattern(step, gamma, outcome)) + + if step == "measure": + sqrt_term = np.sqrt(1 - gamma) + closed_form = np.array([[(1 + sqrt_term) / 2, 0.0], [0.0, (1 - sqrt_term) / 2]], dtype=np.complex128) + assert np.allclose(res.rho, closed_form) + + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) + @pytest.mark.parametrize( + ("step", "param"), + [ + ("prep", "prepare_error_prob"), + ("entangle", "entanglement_error_prob"), + ("measure", "measure_channel_prob"), + ("xcorr", "x_error_prob"), + ("zcorr", "z_error_prob"), + ], + ) + def test_amplitude_damping_rz_step_matches_analytic( + self, + step: str, + param: str, + z_outcome: Outcome, + x_outcome: Outcome, + fx_rng: Generator, + ) -> None: + alpha = fx_rng.random() + gamma = fx_rng.random() + rzpattern = rzpat(alpha) + results = rz_measurement_results(rzpattern, z_outcome, x_outcome) + + res = rzpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(**{param: gamma}), + branch_selector=FixedBranchSelector(results), + rng=fx_rng, + ) + assert isinstance(res, DensityMatrix) + assert np.allclose( + res.rho, + self._expected_rz_pattern(alpha, step, gamma, z_outcome, x_outcome), + ) + + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) + def test_amplitude_damping_x_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: + alpha = fx_rng.random() + rzpattern = rzpat(alpha) + gamma = fx_rng.random() + results = rz_measurement_results(rzpattern, z_outcome, x_outcome) + + res = rzpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(x_error_prob=gamma), + branch_selector=FixedBranchSelector(results), + rng=fx_rng, + ) + + exact = rz_exact_res(alpha).astype(np.complex128) + expected = single_qubit_amplitude_damping_exact(exact, gamma) if x_outcome == 1 else exact + assert isinstance(res, DensityMatrix) + assert np.allclose(res.rho, expected) + + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) + def test_amplitude_damping_z_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: + alpha = fx_rng.random() + rzpattern = rzpat(alpha) + gamma = fx_rng.random() + results = rz_measurement_results(rzpattern, z_outcome, x_outcome) + + res = rzpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(z_error_prob=gamma), + branch_selector=FixedBranchSelector(results), + rng=fx_rng, + ) + + exact = rz_exact_res(alpha).astype(np.complex128) + expected = single_qubit_amplitude_damping_exact(exact, gamma) if z_outcome == 1 else exact + assert isinstance(res, DensityMatrix) + assert np.allclose(res.rho, expected) + + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) + def test_amplitude_damping_measure_confuse_rz( + self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome + ) -> None: + alpha = fx_rng.random() + rzpattern = rzpat(alpha) + results = rz_measurement_results(rzpattern, z_outcome, x_outcome) + + res = rzpattern.simulate_pattern( + backend="densitymatrix", + noise_model=AmplitudeDampingNoiseModel(measure_error_prob=1.0), + branch_selector=FixedBranchSelector(results), + rng=fx_rng, + ) + + exact = rz_exact_res(alpha) + assert isinstance(res, DensityMatrix) + assert np.allclose(res.rho, Ops.Z @ Ops.X @ exact @ Ops.X @ Ops.Z)