diff --git a/src/mqt/qudits/quantum_circuit/circuit.py b/src/mqt/qudits/quantum_circuit/circuit.py index f364354..fb83ac3 100644 --- a/src/mqt/qudits/quantum_circuit/circuit.py +++ b/src/mqt/qudits/quantum_circuit/circuit.py @@ -18,6 +18,8 @@ CustomTwo, GellMann, H, + NoiseX, + NoiseY, Perm, R, RandU, @@ -268,6 +270,20 @@ def randu(self, qudits: list[int]) -> RandU: def rz(self, qudit: int, parameters: list[int | float], controls: ControlData | None = None) -> Rz: return Rz(self, "Rz" + str(self.dimensions[qudit]), qudit, parameters, self.dimensions[qudit], controls) + @add_gate_decorator + def noisex(self, qudit: int, parameters: list[int], controls: ControlData | None = None) -> NoiseX: + return NoiseX(self, "NoiseX" + str(self.dimensions[qudit]), qudit, parameters, self.dimensions[qudit], controls) + + @add_gate_decorator + def noisey(self, qudit: int, parameters: list[int], controls: ControlData | None = None) -> NoiseY: + return NoiseY(self, "NoiseY" + str(self.dimensions[qudit]), qudit, parameters, self.dimensions[qudit], controls) + + @add_gate_decorator + def noisez(self, qudit: int, level: int, controls: ControlData | None = None) -> VirtRz: + return VirtRz( + self, "NoiseZ" + str(self.dimensions[qudit]), qudit, [level, np.pi], self.dimensions[qudit], controls + ) + @add_gate_decorator def virtrz(self, qudit: int, parameters: list[int | float], controls: ControlData | None = None) -> VirtRz: return VirtRz(self, "VirtRz" + str(self.dimensions[qudit]), qudit, parameters, self.dimensions[qudit], controls) diff --git a/src/mqt/qudits/quantum_circuit/gates/__init__.py b/src/mqt/qudits/quantum_circuit/gates/__init__.py index 247c5e0..6556d3c 100644 --- a/src/mqt/qudits/quantum_circuit/gates/__init__.py +++ b/src/mqt/qudits/quantum_circuit/gates/__init__.py @@ -13,6 +13,8 @@ from .h import H from .ls import LS from .ms import MS +from .noise_x import NoiseX +from .noise_y import NoiseY from .perm import Perm from .r import R from .randu import RandU @@ -35,6 +37,8 @@ "GateTypes", "GellMann", "H", + "NoiseX", + "NoiseY", "Perm", "R", "RandU", diff --git a/src/mqt/qudits/quantum_circuit/gates/noise_x.py b/src/mqt/qudits/quantum_circuit/gates/noise_x.py new file mode 100644 index 0000000..9cdef08 --- /dev/null +++ b/src/mqt/qudits/quantum_circuit/gates/noise_x.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from ..components.extensions.gate_types import GateTypes +from ..gate import Gate + +if TYPE_CHECKING: + from numpy.typing import NDArray + + from ..circuit import QuantumCircuit + from ..components.extensions.controls import ControlData + from ..gate import Parameter + + +class NoiseX(Gate): + def __init__( + self, + circuit: QuantumCircuit, + name: str, + target_qudits: int, + parameters: list[int], + dimensions: int, + controls: ControlData | None = None, + ) -> None: + super().__init__( + circuit=circuit, + name=name, + gate_type=GateTypes.SINGLE, + target_qudits=target_qudits, + dimensions=dimensions, + control_set=controls, + qasm_tag="noisex", + ) + + if self.validate_parameter(parameters): + self.original_lev_a: int = parameters[0] + self.original_lev_b: int = parameters[1] + self.lev_a, self.lev_b = self.levels_setter(self.original_lev_a, self.original_lev_b) + self._params = parameters + + def __array__(self) -> NDArray: # noqa: PLW3201 + dimension = self.dimensions + matrix = np.identity(dimension, dtype="complex") + + matrix[self.lev_a, self.lev_a] = 0.0 + matrix[self.lev_b, self.lev_b] = 0.0 + matrix[self.lev_a, self.lev_b] = 1.0 + matrix[self.lev_b, self.lev_a] = 1.0 + + return matrix + + @staticmethod + def levels_setter(la: int, lb: int) -> tuple[int, int]: + if la < lb: + return la, lb + return lb, la + + def validate_parameter(self, parameter: Parameter) -> bool: + if parameter is None: + return False + + if isinstance(parameter, list): + assert isinstance(parameter[0], int) + assert isinstance(parameter[1], int) + assert parameter[0] >= 0 + assert parameter[0] < self.dimensions + assert parameter[1] >= 0 + assert parameter[1] < self.dimensions + assert parameter[0] != parameter[1] + # Useful to remember direction of the rotation + self.original_lev_a = parameter[0] + self.original_lev_b = parameter[1] + + return True + + if isinstance(parameter, np.ndarray): + # Add validation for numpy array if needed + return False + + return False + + @property + def dimensions(self) -> int: + assert isinstance(self._dimensions, int), "Dimensions must be an integer" + return self._dimensions diff --git a/src/mqt/qudits/quantum_circuit/gates/noise_y.py b/src/mqt/qudits/quantum_circuit/gates/noise_y.py new file mode 100644 index 0000000..afb67d9 --- /dev/null +++ b/src/mqt/qudits/quantum_circuit/gates/noise_y.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from ..components.extensions.gate_types import GateTypes +from ..gate import Gate + +if TYPE_CHECKING: + from numpy.typing import NDArray + + from ..circuit import QuantumCircuit + from ..components.extensions.controls import ControlData + from ..gate import Parameter + + +class NoiseY(Gate): + def __init__( + self, + circuit: QuantumCircuit, + name: str, + target_qudits: int, + parameters: list[int], + dimensions: int, + controls: ControlData | None = None, + ) -> None: + super().__init__( + circuit=circuit, + name=name, + gate_type=GateTypes.SINGLE, + target_qudits=target_qudits, + dimensions=dimensions, + control_set=controls, + qasm_tag="noisey", + ) + + if self.validate_parameter(parameters): + self.original_lev_a: int = parameters[0] + self.original_lev_b: int = parameters[1] + self.lev_a, self.lev_b = self.levels_setter(self.original_lev_a, self.original_lev_b) + self._params = parameters + + def __array__(self) -> NDArray: # noqa: PLW3201 + dimension = self.dimensions + matrix = np.identity(dimension, dtype="complex") + + matrix[self.lev_a, self.lev_a] = 0.0 + matrix[self.lev_b, self.lev_b] = 0.0 + matrix[self.lev_a, self.lev_b] = -1j + matrix[self.lev_b, self.lev_a] = 1j + + return matrix + + @staticmethod + def levels_setter(la: int, lb: int) -> tuple[int, int]: + if la < lb: + return la, lb + return lb, la + + def validate_parameter(self, parameter: Parameter) -> bool: + if parameter is None: + return False + + if isinstance(parameter, list): + assert isinstance(parameter[0], int) + assert isinstance(parameter[1], int) + assert parameter[0] >= 0 + assert parameter[0] < self.dimensions + assert parameter[1] >= 0 + assert parameter[1] < self.dimensions + assert parameter[0] != parameter[1] + # Useful to remember direction of the rotation + self.original_lev_a = parameter[0] + self.original_lev_b = parameter[1] + + return True + + if isinstance(parameter, np.ndarray): + # Add validation for numpy array if needed + return False + + return False + + @property + def dimensions(self) -> int: + assert isinstance(self._dimensions, int), "Dimensions must be an integer" + return self._dimensions diff --git a/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2six.py b/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2six.py index f97bcd2..1fea81a 100644 --- a/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2six.py +++ b/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2six.py @@ -4,8 +4,10 @@ from typing_extensions import Unpack +from mqt.qudits.simulation.noise_tools.noise import Noise + from ....core import LevelGraph -from ...noise_tools import Noise, NoiseModel +from ...noise_tools import NoiseModel from ..tnsim import TNSim if TYPE_CHECKING: diff --git a/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2three.py b/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2three.py index d377885..7bc8be8 100644 --- a/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2three.py +++ b/src/mqt/qudits/simulation/backends/fake_backends/fake_traps2three.py @@ -4,8 +4,10 @@ from typing_extensions import Unpack +from mqt.qudits.simulation.noise_tools.noise import Noise + from ....core import LevelGraph -from ...noise_tools import Noise, NoiseModel +from ...noise_tools import NoiseModel from ..tnsim import TNSim if TYPE_CHECKING: diff --git a/src/mqt/qudits/simulation/backends/fake_backends/fake_traps3six.py b/src/mqt/qudits/simulation/backends/fake_backends/fake_traps3six.py index b91fa02..b926bdb 100644 --- a/src/mqt/qudits/simulation/backends/fake_backends/fake_traps3six.py +++ b/src/mqt/qudits/simulation/backends/fake_backends/fake_traps3six.py @@ -4,8 +4,10 @@ from typing_extensions import Unpack +from mqt.qudits.simulation.noise_tools.noise import Noise + from ....core import LevelGraph -from ...noise_tools import Noise, NoiseModel +from ...noise_tools import NoiseModel from ..tnsim import TNSim if TYPE_CHECKING: diff --git a/src/mqt/qudits/simulation/noise_tools/__init__.py b/src/mqt/qudits/simulation/noise_tools/__init__.py index 21302e3..b8b764f 100644 --- a/src/mqt/qudits/simulation/noise_tools/__init__.py +++ b/src/mqt/qudits/simulation/noise_tools/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .noise import Noise, NoiseModel +from .noise import Noise, NoiseModel, SubspaceNoise from .noisy_circuit_factory import NoisyCircuitFactory -__all__ = ["Noise", "NoiseModel", "NoisyCircuitFactory"] +__all__ = ["Noise", "NoiseModel", "NoisyCircuitFactory", "SubspaceNoise"] diff --git a/src/mqt/qudits/simulation/noise_tools/noise.py b/src/mqt/qudits/simulation/noise_tools/noise.py index 391efb6..be301c1 100644 --- a/src/mqt/qudits/simulation/noise_tools/noise.py +++ b/src/mqt/qudits/simulation/noise_tools/noise.py @@ -1,52 +1,131 @@ from __future__ import annotations -from dataclasses import dataclass - -@dataclass class Noise: """Represents a noise model with depolarizing and dephasing probabilities.""" - probability_depolarizing: float - probability_dephasing: float + def __init__(self, probability_depolarizing: float, probability_dephasing: float) -> None: + self.probability_depolarizing = probability_depolarizing + self.probability_dephasing = probability_dephasing + + def __str__(self) -> str: + return str(self.probability_depolarizing) + " " + str(self.probability_dephasing) + + +class SubspaceNoise: + """Represents physical noises for each level transitions.""" + + def __init__( + self, + probability_depolarizing: float, + probability_dephasing: float, + levels: tuple[int, int] | list[tuple[int, int]], + ) -> None: + self.subspace_w_probs: dict[tuple[int, int], Noise] = {} + + if isinstance(levels, tuple): + self.add_noise( + levels[0], + levels[1], + Noise(probability_depolarizing, probability_dephasing), + ) + elif len(levels) > 0: + for lev in levels: + self.add_noise( + lev[0], + lev[1], + Noise(probability_depolarizing, probability_dephasing), + ) + else: + # case where you want the subspace noise to be dynamically assigned + # to the two-dimensional subspace of a Givens derived rotation. + # The negative values are not physical and we will check only if they are negative. + self.add_noise( + -2, + -1, + Noise(probability_depolarizing, probability_dephasing), + ) + + def __str__(self) -> str: + result = "" + for levs, noise in self.subspace_w_probs.items(): + result += str(levs[0]) + "<->" + str(levs[1]) + ":" + str(noise) + ", " + return result + + def add_noise(self, lev_a: int, lev_b: int, noise: Noise) -> None: + if lev_b < lev_a: + lev_a, lev_b = lev_b, lev_a + if lev_a == lev_b: + msg = "The levels in the subspace noise should be different!" + raise ValueError(msg) + if (lev_a, lev_b) in self.subspace_w_probs: + msg = "The same level physical noise is defined for multiple times!" + raise ValueError(msg) + if (lev_a < 0 or lev_b < 0) and len(self.subspace_w_probs) > 0: + msg = ( + "Negative keys are for the dynamic assignment of the subspaces, " + "therefore you cannot assignment more subspaces!" + ) + raise ValueError(msg) + if any(levs[0] < 0 and levs[1] < 0 for levs in self.subspace_w_probs): + msg = ( + "Negative keys are already present for the dynamic assignment of the subspaces, " + "therefore you cannot assignment other subspaces!" + ) + raise ValueError(msg) + self.subspace_w_probs[lev_a, lev_b] = noise + + def add_noises(self, noises: dict[tuple[int, int], Noise]) -> None: + for tup, noise in noises.items(): + self.add_noise(tup[0], tup[1], noise) class NoiseModel: """Represents a quantum noise model for various gates and qudit configurations.""" def __init__(self) -> None: - self.quantum_errors: dict[str, dict[str, Noise]] = {} + self.quantum_errors: dict[str, dict[str, Noise | SubspaceNoise]] = {} - def _add_quantum_error(self, noise: Noise, gates: list[str], mode: str) -> None: + def _add_quantum_error(self, noise: Noise | SubspaceNoise, gates: list[str], mode: str) -> None: """Helper method to add quantum errors to the model. Args: - noise (Noise): The noise model to add. + noise (SubspaceNoise | Noise): The subspace noise model to add. gates (List[str]): List of gate names to apply the noise to. mode (Union[Tuple[int, ...], Literal["local", "all", "nonlocal", "target", "control"]]): The mode or qudit configuration for the noise. """ for gate in gates: if gate not in self.quantum_errors: self.quantum_errors[gate] = {} - self.quantum_errors[gate][mode] = noise - - def add_quantum_error_locally(self, noise: Noise, gates: list[str]) -> None: + if mode not in self.quantum_errors[gate]: + self.quantum_errors[gate][mode] = noise # empty case + elif isinstance(noise, Noise): + msg = "Mathematical noise has been defined multiple times!" + raise ValueError(msg) + else: + existing_instance = self.quantum_errors[gate][mode] + assert isinstance(existing_instance, SubspaceNoise) + existing_instance.add_noises( + noise.subspace_w_probs + ) # add the noise info to the existing SubspaceNoise instance + + def add_quantum_error_locally(self, noise: Noise | SubspaceNoise, gates: list[str]) -> None: """Add a quantum error locally to all qudits for specified gates.""" self._add_quantum_error(noise, gates, "local") - def add_all_qudit_quantum_error(self, noise: Noise, gates: list[str]) -> None: + def add_all_qudit_quantum_error(self, noise: Noise | SubspaceNoise, gates: list[str]) -> None: """Add a quantum error to all qudits for specified gates.""" self._add_quantum_error(noise, gates, "all") - def add_nonlocal_quantum_error(self, noise: Noise, gates: list[str]) -> None: + def add_nonlocal_quantum_error(self, noise: Noise | SubspaceNoise, gates: list[str]) -> None: """Add a nonlocal quantum error for specified gates.""" self._add_quantum_error(noise, gates, "nonlocal") - def add_nonlocal_quantum_error_on_target(self, noise: Noise, gates: list[str]) -> None: + def add_nonlocal_quantum_error_on_target(self, noise: Noise | SubspaceNoise, gates: list[str]) -> None: """Add a nonlocal quantum error on target qudits for specified gates.""" self._add_quantum_error(noise, gates, "target") - def add_nonlocal_quantum_error_on_control(self, noise: Noise, gates: list[str]) -> None: + def add_nonlocal_quantum_error_on_control(self, noise: Noise | SubspaceNoise, gates: list[str]) -> None: """Add a nonlocal quantum error on control qudits for specified gates.""" self._add_quantum_error(noise, gates, "control") @@ -59,6 +138,6 @@ def __str__(self) -> str: """Return a string representation of the NoiseModel.""" info_str = "NoiseModel Info:\n" for gate, errors in self.quantum_errors.items(): - for mode, noise in errors.items(): - info_str += f"Gate: {gate}, Mode: {mode}, Noise: {noise}\n" + for mode, subnoise in errors.items(): + info_str += f"Gate: {gate}, Mode: {mode}, SubspaceNoise: {subnoise}\n" return info_str diff --git a/src/mqt/qudits/simulation/noise_tools/noisy_circuit_factory.py b/src/mqt/qudits/simulation/noise_tools/noisy_circuit_factory.py index e9ed227..5334c6d 100644 --- a/src/mqt/qudits/simulation/noise_tools/noisy_circuit_factory.py +++ b/src/mqt/qudits/simulation/noise_tools/noisy_circuit_factory.py @@ -4,12 +4,15 @@ import os import time from functools import partial +from itertools import product from typing import TYPE_CHECKING, cast import numpy as np -from ...quantum_circuit import QuantumCircuit, gates +from ...quantum_circuit import QuantumCircuit from ...quantum_circuit.components.extensions.gate_types import GateTypes +from ...quantum_circuit.gates import CEx, R, Rh, Rz +from .noise import Noise, SubspaceNoise if TYPE_CHECKING: from collections.abc import Callable @@ -17,7 +20,7 @@ from numpy.random import Generator from ...quantum_circuit.gate import Gate - from .noise import Noise, NoiseModel + from .noise import NoiseModel class NoisyCircuitFactory: @@ -45,6 +48,53 @@ def generate_circuit(self) -> QuantumCircuit: return noisy_circuit + def _dynamic_subspace_noise_info_rectification(self, noise_info: SubspaceNoise, instruction: Gate) -> SubspaceNoise: + """Corrects negative subspace levels in SubspaceNoise by matching them to physical two-level rotations. + + This function adapts SubspaceNoise dynamically to align with physical local two-level rotations + (R, Rz, Rh, CEx). If the input noise has negative subspace levels, it maps them to the + instruction's actual levels. + + Args: + noise_info: The original SubspaceNoise object to be corrected + instruction: The Gate instruction containing the target levels + Returns: + SubspaceNoise: Corrected noise information with proper subspace levels + Note: + Only processes single-subspace noise objects with negative indices. + Supported gate types are R, Rz, Rh, and CEx. + """ + # Return original if no correction needed + if not self._needs_correction(noise_info): + return noise_info + + # Return original if instruction type not supported + if not isinstance(instruction, (R, Rz, Rh, CEx)): + return noise_info + + # Get the noise probabilities from the negative-indexed subspace + subspace = next(iter(noise_info.subspace_w_probs.keys())) + noise_probs = noise_info.subspace_w_probs[subspace] + + # Create new noise info with correct levels + return SubspaceNoise( + probability_depolarizing=noise_probs.probability_depolarizing, + probability_dephasing=noise_probs.probability_dephasing, + levels=(instruction.lev_a, instruction.lev_b), + ) + + @staticmethod + def _needs_correction(noise_info: SubspaceNoise) -> bool: + """Determines if the noise info needs level correction. + + Returns True if there is exactly one subspace and it has negative indices. + """ + if len(noise_info.subspace_w_probs) != 1: + return False + + subspace = next(iter(noise_info.subspace_w_probs.keys())) + return subspace[0] < 0 or subspace[1] < 0 + def _apply_noise(self, noisy_circuit: QuantumCircuit, instruction: Gate) -> None: if instruction.qasm_tag not in self.noise_model.quantum_errors: return @@ -54,13 +104,16 @@ def _apply_noise(self, noisy_circuit: QuantumCircuit, instruction: Gate) -> None if qudits is None: continue # type: ignore[unreachable] - self._apply_depolarizing_noise(noisy_circuit, instruction, qudits, noise_info) - self._apply_dephasing_noise(noisy_circuit, instruction, qudits, noise_info) + if isinstance(noise_info, SubspaceNoise): + noise_info = self._dynamic_subspace_noise_info_rectification(noise_info, instruction) # noqa: PLW2901 + + self._apply_depolarizing_noise(noisy_circuit, qudits, noise_info) + self._apply_dephasing_noise(noisy_circuit, qudits, noise_info) def _get_affected_qudits(self, instruction: Gate, mode: str) -> list[int]: if isinstance(mode, str): return self._get_qudits_for_mode(instruction, mode) - msg = "Something broken is constructrion of Noise Model." # type: ignore[unreachable] + msg = "Something broken is construction of Noise Model." # type: ignore[unreachable] raise ValueError(msg) def _get_qudits_for_mode(self, instruction: Gate, mode: str) -> list[int]: @@ -87,47 +140,123 @@ def _get_nonlocal_qudits(instruction: Gate) -> list[int]: return instruction.reference_lines def _get_control_qudits(self, instruction: Gate) -> list[int]: - self._validate_two_qudit_gate(instruction, "Control") + self._validate_two_qudit_gate(instruction) qudits_targeted = cast(list[int], instruction.target_qudits) return qudits_targeted[:1] - def _get_target_qudits(self, instruction: Gate) -> list[int]: - self._validate_two_qudit_gate(instruction, "Target") - qudits_targeted = cast(list[int], instruction.target_qudits) - return qudits_targeted[1:] + @staticmethod + def _get_target_qudits(instruction: Gate) -> list[int]: + """Returns the target qudits for a gate instruction, excluding control qudits. + + For multi-controlled gates (e.g. controlled-R, controlled-exchange), returns all + non-control qudits. For single qudit gates, returns a list with just the target qudit. + + Args: + instruction: The gate instruction to analyze + + Returns: + List of target qudit indices, excluding control qudits + """ + if instruction.gate_type != GateTypes.SINGLE: + qudits_targeted = instruction.reference_lines + return qudits_targeted[1:] + + return [cast(int, instruction.target_qudits)] @staticmethod - def _validate_two_qudit_gate(instruction: Gate, mode: str) -> None: + def _validate_two_qudit_gate(instruction: Gate) -> None: if instruction.gate_type != GateTypes.TWO: - msg = f"{mode} mode only applicable for two-qudit gates, not {instruction.gate_type}" + msg = f"Gate type {instruction.gate_type} is incompatible for the desired operation." raise ValueError(msg) def _apply_depolarizing_noise( - self, noisy_circuit: QuantumCircuit, instruction: Gate, qudits: list[int], noise_info: Noise + self, noisy_circuit: QuantumCircuit, qudits: list[int], noise_info: Noise | SubspaceNoise ) -> None: - if self.rng.random() < noise_info.probability_depolarizing: - self._apply_x_noise(noisy_circuit, instruction, qudits) + if isinstance(noise_info, Noise): # Mathematical Description of Depolarizing noise channel + for dit in qudits: + dim = noisy_circuit.dimensions[dit] + prob_each = noise_info.probability_depolarizing / dim / dim # TODO: ARE WE SURE THIS IS CORRECT? + noise_combinations = list(product(range(dim), repeat=2)) + probabilities = [1 - prob_each * (dim * dim - 1)] + [prob_each] * (dim * dim - 1) + power_noise_x, power_noise_z = self.rng.choice(noise_combinations, p=probabilities) + # TODO: THE FOLLOWING LINES COULD CREATE A LOT OF OVERHEAD IN SIMULATION + for _ in range(power_noise_x): + noisy_circuit.x(dit) + for _ in range(power_noise_z): + noisy_circuit.z(dit) + elif isinstance(noise_info, SubspaceNoise): # Physical Noise + for dit in qudits: + dim = noisy_circuit.dimensions[dit] + possible_levels = list(range(dim)) + + # Validate all subspace levels + for lev_a, lev_b in noise_info.subspace_w_probs: + if lev_a not in possible_levels or lev_b not in possible_levels: + msg = ( + "Subspace levels exceed qudit dimensions. " + f"Got levels ({lev_a}, {lev_b}) but dimension is {dim}. " + "Check noise model compatibility with circuit." + ) + raise IndexError(msg) + + # Calculate probabilities for noise operations + prob_each = noise_info.subspace_w_probs[lev_a, lev_b].probability_depolarizing / 4 + + # Generate possible noise combinations (X,Z gates) + noise_combinations = list(product(range(2), repeat=2)) + probabilities = [1 - 3 * prob_each] + [prob_each] * 3 + + # Choose noise operation based on probability distribution + noise_x, noise_z = self.rng.choice(noise_combinations, p=probabilities) + + # Apply appropriate noise operation + if (noise_x, noise_z) == (1, 0): + noisy_circuit.noisex(dit, [lev_a, lev_b]) + elif (noise_x, noise_z) == (0, 1): + noisy_circuit.noisez(dit, lev_b) + elif (noise_x, noise_z) == (1, 1): + noisy_circuit.noisey(dit, [lev_a, lev_b]) def _apply_dephasing_noise( - self, noisy_circuit: QuantumCircuit, instruction: Gate, qudits: list[int], noise_info: Noise + self, noisy_circuit: QuantumCircuit, qudits: list[int], noise_info: Noise | SubspaceNoise ) -> None: - if self.rng.random() < noise_info.probability_dephasing: - self._apply_z_noise(noisy_circuit, instruction, qudits) + """Applies dephasing noise to specified qudit levels outside the main depolarizing subspace levels. - @staticmethod - def _apply_x_noise(noisy_circuit: QuantumCircuit, instruction: Gate, qudits: list[int]) -> None: - if isinstance(instruction, (gates.R, gates.Rz)): - for dit in qudits: - noisy_circuit.r(dit, [instruction.lev_a, instruction.lev_b, np.pi, np.pi / 2]) - else: - for dit in qudits: - noisy_circuit.x(dit) + Args: + noisy_circuit: Circuit to apply noise to + qudits: List of qudits to apply noise to + noise_info: Noise model information containing subspace probabilities - @staticmethod - def _apply_z_noise(noisy_circuit: QuantumCircuit, instruction: Gate, qudits: list[int]) -> None: - if isinstance(instruction, (gates.R, gates.Rz)): + Raises: + IndexError: If subspace levels are outside qudit dimensions + """ + if isinstance(noise_info, SubspaceNoise): # Physical Noise for dit in qudits: - noisy_circuit.rz(dit, [instruction.lev_a, instruction.lev_b, np.pi]) - else: - for dit in qudits: - noisy_circuit.z(dit) + dim = noisy_circuit.dimensions[dit] + possible_levels = set(range(dim)) # Changed to set for efficient operations + + # Validate all subspace levels + for lev_a, lev_b in noise_info.subspace_w_probs: + if lev_a not in possible_levels or lev_b not in possible_levels: + msg = ( + "Subspace levels exceed qudit dimensions. " + f"Got levels ({lev_a}, {lev_b}) but dimension is {dim}. " + "Check noise model compatibility with circuit." + ) + raise IndexError(msg) + + # Calculate remaining levels for dephasing + subspace_levels = {lev_a, lev_b} + dephasing_levels = list(possible_levels - subspace_levels) + + if not dephasing_levels: # Check if we have levels for dephasing + continue + + # Calculate probability for each level + prob_each = noise_info.subspace_w_probs[lev_a, lev_b].probability_dephasing + probs = [prob_each, 1 - prob_each] # [apply noise, no noise] + + # Apply dephasing to each level outside subspace + for physical_level in dephasing_levels: + if self.rng.choice([True, False], p=probs): + noisy_circuit.noisez(dit, physical_level) diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp index 4eeca8b..89fa626 100644 --- a/src/python/bindings.cpp +++ b/src/python/bindings.cpp @@ -268,6 +268,10 @@ NoiseModel parse_noise_model(const py::dict& noise_model) { noiseSpread = noiseSpreadTuple; } + if (py::isinstance(noiseTypesPair.second)) { + throw std::invalid_argument("Physical noise is not supported yet."); + } + double depo = noiseTypesPair.second.attr("probability_depolarizing").cast(); double deph = diff --git a/test/python/qudits_circuits/gate_set/test_noisex.py b/test/python/qudits_circuits/gate_set/test_noisex.py new file mode 100644 index 0000000..808d99d --- /dev/null +++ b/test/python/qudits_circuits/gate_set/test_noisex.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from unittest import TestCase + +import numpy as np + +from mqt.qudits.quantum_circuit import QuantumCircuit + + +class TestNoiseX(TestCase): + @staticmethod + def test___array__(): + circuit_3 = QuantumCircuit(1, [3], 0) + nx = circuit_3.noisex(0, [1, 2]) + nx_test = np.array([ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], + ]) + + assert np.allclose(nx.to_matrix(identities=0), nx_test) + assert np.allclose(nx.dag().to_matrix(identities=0), nx_test.conj().T) + + circuit_4 = QuantumCircuit(1, [4], 0) + nx_2 = circuit_4.noisex(0, [0, 2]) + + nx_test_2 = np.array([ + [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], + ]) + + assert np.allclose(nx_2.to_matrix(identities=0), nx_test_2) + assert np.allclose(nx_2.dag().to_matrix(identities=0), nx_test_2.conj().T) diff --git a/test/python/qudits_circuits/gate_set/test_noisey.py b/test/python/qudits_circuits/gate_set/test_noisey.py new file mode 100644 index 0000000..c1707a3 --- /dev/null +++ b/test/python/qudits_circuits/gate_set/test_noisey.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from unittest import TestCase + +import numpy as np + +from mqt.qudits.quantum_circuit import QuantumCircuit + + +class TestNoiseY(TestCase): + @staticmethod + def test___array__(): + circuit_3 = QuantumCircuit(1, [3], 0) + ny = circuit_3.noisey(0, [1, 2]) + ny_test = np.array([ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 - 1.0j], + [0.0 + 0.0j, 0.0 + 1.0j, 0.0 + 0.0j], + ]) + + assert np.allclose(ny.to_matrix(identities=0), ny_test) + assert np.allclose(ny.dag().to_matrix(identities=0), ny_test.conj().T) + + circuit_4 = QuantumCircuit(1, [4], 0) + ny_2 = circuit_4.noisey(0, [0, 2]) + + ny_test_2 = np.array([ + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 - 1.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 1.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], + ]) + + assert np.allclose(ny_2.to_matrix(identities=0), ny_test_2) + assert np.allclose(ny_2.dag().to_matrix(identities=0), ny_test_2.conj().T) diff --git a/test/python/simulation/noise_tools/test_noise_tools.py b/test/python/simulation/noise_tools/test_noise_tools_mathematical.py similarity index 86% rename from test/python/simulation/noise_tools/test_noise_tools.py rename to test/python/simulation/noise_tools/test_noise_tools_mathematical.py index 815ff60..1c27b2c 100644 --- a/test/python/simulation/noise_tools/test_noise_tools.py +++ b/test/python/simulation/noise_tools/test_noise_tools_mathematical.py @@ -4,6 +4,7 @@ from unittest import TestCase import numpy as np +import pytest from mqt.qudits.quantum_circuit import QuantumCircuit from mqt.qudits.quantum_circuit.components.quantum_register import QuantumRegister @@ -37,7 +38,7 @@ def setUp(self) -> None: noise_model.add_nonlocal_quantum_error(entangling_error_extra, ["csum"]) # Local Gates noise_model.add_quantum_error_locally(local_error, ["rh", "h", "rxy", "s", "x", "z"]) - noise_model.add_quantum_error_locally(local_error_rz, ["rz", "virtrz"]) + noise_model.add_quantum_error_locally(local_error_rz, ["rz"]) self.noise_model = noise_model def test_generate_circuit(self): @@ -84,15 +85,17 @@ def test_generate_circuit(self): insts_new += 1 tag_counts_list2[gate.qasm_tag] += 1 - keys_to_check = ["x", "z", "rxy", "rz"] + keys_to_check = ["x", "z", "virtrz"] valid_stochasticity = True # Iterate over all keys for key in tag_counts_list1.keys() | tag_counts_list2.keys(): if key in keys_to_check: if tag_counts_list1.get(key, 0) > tag_counts_list2.get(key, 0): valid_stochasticity = False + print(key, "error") elif tag_counts_list1.get(key, 0) != tag_counts_list2.get(key, 0): valid_stochasticity = False + print(key, "error") assert valid_stochasticity assert insts == circ.number_gates @@ -102,14 +105,20 @@ def test_generate_circuit(self): def test_generate_circuit_isolated(self): qreg_example = QuantumRegister("reg", 2, [5, 5]) circ = QuantumCircuit(qreg_example) - x = circ.x(0) - x.control([1], [2]) + circ.x(0) factory = NoisyCircuitFactory(self.noise_model, circ) instructions_og = circ.instructions new_circ = factory.generate_circuit() - - assert circ.number_gates == 1 - assert new_circ.number_gates == 5 + assert circ.number_gates == 1 # original x only assert [i.qasm_tag for i in instructions_og] == ["x"] - assert ["x", "x", "x", "z", "z"] + for tag in [i.qasm_tag for i in new_circ.instructions]: + assert tag in {"x", "z", "virtrz"} + + @staticmethod + def test_error(): + noise_model = NoiseModel() + local_error = Noise(probability_depolarizing=0.999, probability_dephasing=0.999) + noise_model.add_all_qudit_quantum_error(local_error, ["csum"]) + with pytest.raises(ValueError, match="Mathematical noise has been defined multiple times!"): + noise_model.add_all_qudit_quantum_error(local_error, ["csum"]) diff --git a/test/python/simulation/noise_tools/test_noise_tools_physical.py b/test/python/simulation/noise_tools/test_noise_tools_physical.py new file mode 100644 index 0000000..ffad988 --- /dev/null +++ b/test/python/simulation/noise_tools/test_noise_tools_physical.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +from collections import defaultdict +from unittest import TestCase + +import numpy as np +import pytest + +from mqt.qudits.quantum_circuit import QuantumCircuit +from mqt.qudits.quantum_circuit.components.quantum_register import QuantumRegister +from mqt.qudits.simulation.noise_tools import Noise, NoiseModel, NoisyCircuitFactory, SubspaceNoise + + +def rand_0_5() -> int: + rng = np.random.default_rng() + return int(rng.integers(0, 6)) + + +class TestNoisyCircuitFactoryPhysical(TestCase): + def setUp(self) -> None: + sub1 = SubspaceNoise(0.999, 0.999, (0, 1)) + sub2 = SubspaceNoise(0.0, 0.999, [(1, 2), (2, 3)]) + sub3 = SubspaceNoise(0.999, 0.0, [(1, 2), (2, 3)]) + dynamic_assigned = SubspaceNoise(0.999, 0.0, []) + + # Add errors to noise_tools model + + noise_model = NoiseModel() # We know that the architecture is only two qudits + # Very noisy gate_matrix + noise_model.add_all_qudit_quantum_error(sub1, ["csum"]) + # Local Gates + noise_model.add_quantum_error_locally(sub1, ["z", "ms"]) + noise_model.add_quantum_error_locally(sub2, ["x", "h"]) + noise_model.add_quantum_error_locally(dynamic_assigned, ["rh", "rz", "rxy", "cx"]) + noise_model.add_quantum_error_locally(sub3, ["s"]) + self.noise_model = noise_model + + assert set(noise_model.basis_gates) == {"csum", "z", "ms", "rh", "h", "rz", "rxy", "x", "s", "cx"} + + def test_generate_circuit(self): + qreg_example = QuantumRegister("reg", 6, 6 * [5]) + circ = QuantumCircuit(qreg_example) + choice = rand_0_5() + x = circ.x(choice) + x.control([int(np.mod(choice + 1, 5))], [2]) + circ.rz(rand_0_5(), [0, 2, np.pi / 13]) + circ.cx([3, 4], [0, 3, 0, np.pi / 12]) + circ.x(rand_0_5()).dag() + circ.s(rand_0_5()) + circ.z(rand_0_5()) + circ.csum([5, 1]) + circ.virtrz(rand_0_5(), [0, np.pi / 13]).dag() + circ.virtrz(rand_0_5(), [1, -np.pi / 8]) + circ.csum([2, 5]).dag() + circ.x(rand_0_5()).dag() + circ.z(rand_0_5()).dag() + circ.h(rand_0_5()) + circ.rz(rand_0_5(), [3, 4, np.pi / 13]).dag() + circ.h(rand_0_5()).dag() + circ.r(rand_0_5(), [0, 1, np.pi / 5 + np.pi, np.pi / 7]) + circ.rh(rand_0_5(), [1, 3]) + circ.r(rand_0_5(), [0, 4, np.pi, np.pi / 2]).dag() + circ.r(rand_0_5(), [0, 3, np.pi / 5, np.pi / 7]) + circ.cx([1, 2], [0, 1, 1, np.pi / 2]).dag() + circ.csum([0, 1]) + + factory = NoisyCircuitFactory(self.noise_model, circ) + instructions_og = circ.instructions + new_circ = factory.generate_circuit() + instructions_new = new_circ.instructions + + tag_counts_list1: dict[str, int] = defaultdict(int) + tag_counts_list2: dict[str, int] = defaultdict(int) + insts = 0 + insts_new = 0 + for gate in instructions_og: + insts += 1 + tag_counts_list1[gate.qasm_tag] += 1 + + for gate in instructions_new: + insts_new += 1 + tag_counts_list2[gate.qasm_tag] += 1 + + keys_to_check = ["noisex", "noisey", "virtrz"] + valid_stochasticity = True + # Iterate over all keys + for key in tag_counts_list1.keys() | tag_counts_list2.keys(): + if key in keys_to_check: + if tag_counts_list1.get(key, 0) > tag_counts_list2.get(key, 0): + valid_stochasticity = False + print(key, "error") + elif tag_counts_list1.get(key, 0) != tag_counts_list2.get(key, 0): + valid_stochasticity = False + print(key, "error") + + assert valid_stochasticity + assert insts == circ.number_gates + assert insts_new == new_circ.number_gates + assert len(instructions_new) > len(instructions_og) + + def test_generate_circuit_isolated1(self): + qreg_example = QuantumRegister("reg", 4, [5, 5, 5, 5]) + circ = QuantumCircuit(qreg_example) + circ.z(0) + circ.z(1) + + factory = NoisyCircuitFactory(self.noise_model, circ) + instructions_og = circ.instructions + new_circ = factory.generate_circuit() + assert circ.number_gates == 2 # original x only + assert [i.qasm_tag for i in instructions_og] == ["z", "z"] + for tag in [i.qasm_tag for i in new_circ.instructions]: + assert tag in {"z", "virtrz", "noisex", "noisey"} + for inst in new_circ.instructions: + assert inst.target_qudits in {0, 1} + if inst.qasm_tag in {"noisex", "noisey"}: + assert inst.lev_a == 0 + assert inst.lev_b == 1 + if inst.qasm_tag == "virtz": + assert inst.lev_a != 0 + assert inst.lev_b != 1 + + def test_generate_circuit_isolated2(self): + qreg_example = QuantumRegister("reg", 4, [5, 5, 5, 5]) + circ = QuantumCircuit(qreg_example) + circ.x(0) + circ.x(1) + + factory = NoisyCircuitFactory(self.noise_model, circ) + instructions_og = circ.instructions + new_circ = factory.generate_circuit() + assert circ.number_gates == 2 # original x only + assert [i.qasm_tag for i in instructions_og] == ["x", "x"] + for tag in [i.qasm_tag for i in new_circ.instructions]: + assert tag in {"x", "virtrz"} + for inst in new_circ.instructions: + assert inst.target_qudits in {0, 1} + + def test_generate_circuit_isolated3(self): + qreg_example = QuantumRegister("reg", 4, [5, 5, 5, 5]) + circ = QuantumCircuit(qreg_example) + circ.s(1) + circ.s(2) + + factory = NoisyCircuitFactory(self.noise_model, circ) + instructions_og = circ.instructions + new_circ = factory.generate_circuit() + assert circ.number_gates == 2 # original x only + assert [i.qasm_tag for i in instructions_og] == ["s", "s"] + for tag in [i.qasm_tag for i in new_circ.instructions]: + assert tag in {"s", "noisex", "virtrz", "noisey"} + for inst in new_circ.instructions: + assert inst.target_qudits in {1, 2} + if inst.qasm_tag in {"noisex", "noisey"}: + assert (inst.lev_a == 2 and inst.lev_b == 3) or (inst.lev_a == 1 and inst.lev_b == 2) + + @staticmethod + def test_str(): + sub1 = SubspaceNoise(0.999, 0.999, (0, 1)) + noise_model = NoiseModel() + noise_model.add_quantum_error_locally(sub1, ["z"]) + assert "Gate: z, Mode: local, SubspaceNoise: 0<->1:0.999 0.999," in str(noise_model) + + @staticmethod + def test_error(): + noise_model = NoiseModel() + + with pytest.raises(ValueError, match="The levels in the subspace noise should be different!"): + SubspaceNoise(0.999, 0.999, (0, 0)) + + err2 = SubspaceNoise(0.999, 0.999, (0, 1)) + noise_model.add_quantum_error_locally(err2, ["z"]) + with pytest.raises(ValueError, match="The same level physical noise is defined for multiple times!"): + noise_model.add_quantum_error_locally(err2, ["z"]) + + @staticmethod + def test_invalid_level(): + err = SubspaceNoise(0.999, 0.999, (8, 9)) + noise_model = NoiseModel() + noise_model.add_quantum_error_locally(err, ["z"]) + qreg_example = QuantumRegister("reg", 6, 6 * [5]) + circ = QuantumCircuit(qreg_example) + circ.z(0) + factory = NoisyCircuitFactory(noise_model, circ) + with pytest.raises(IndexError, match=r"Subspace levels exceed qudit dimensions.*"): + factory.generate_circuit() + + @staticmethod + def test_no_dephasing(): + err = SubspaceNoise(0.999, 0.999, (0, 1)) + noise_model = NoiseModel() + noise_model.add_quantum_error_locally(err, ["z"]) + qreg_example = QuantumRegister("reg", 2, 2 * [2]) + circ = QuantumCircuit(qreg_example) + circ.z(0) + factory = NoisyCircuitFactory(noise_model, circ) + factory.generate_circuit() + + @staticmethod + def test_invalid_gate(): + err = SubspaceNoise(0.999, 0.999, (0, 1)) + noise_model = NoiseModel() + noise_model.add_nonlocal_quantum_error_on_control(err, ["z"]) + qreg_example = QuantumRegister("reg", 2, 2 * [2]) + circ = QuantumCircuit(qreg_example) + circ.z(0) + factory = NoisyCircuitFactory(noise_model, circ) + with pytest.raises(ValueError, match=r".* is incompatible for the desired operation."): + factory.generate_circuit() + + noise_model = NoiseModel() + noise_model.add_nonlocal_quantum_error(err, ["z"]) + factory = NoisyCircuitFactory(noise_model, circ) + with pytest.raises(ValueError, match=r"Nonlocal mode not applicable for gate type: .*"): + factory.generate_circuit() + + @staticmethod + def test_invalid_mode(): + err = SubspaceNoise(0.999, 0.999, (0, 1)) + noise_model = NoiseModel() + noise_model._add_quantum_error(err, ["z"], "error_mode") # noqa: SLF001 + qreg_example = QuantumRegister("reg", 2, 2 * [2]) + circ = QuantumCircuit(qreg_example) + circ.z(0) + factory = NoisyCircuitFactory(noise_model, circ) + with pytest.raises(ValueError, match="Unknown mode: error_mode"): + factory.generate_circuit() + + def test_generate_circuit_isolated4(self): + qreg_example = QuantumRegister("reg", 4, [5, 5, 5, 5]) + circ = QuantumCircuit(qreg_example) + circ.ms([1, 2], [np.pi / 4]) + circ.ms([1, 2], [np.pi / 3]) + + factory = NoisyCircuitFactory(self.noise_model, circ) + instructions_og = circ.instructions + new_circ = factory.generate_circuit() + assert circ.number_gates == 2 # original x only + assert [i.qasm_tag for i in instructions_og] == ["ms", "ms"] + for tag in [i.qasm_tag for i in new_circ.instructions]: + assert tag in {"ms", "noisex", "virtrz", "noisey"} + for inst in new_circ.instructions: + if isinstance(inst.target_qudits, list): + for t in inst.target_qudits: + assert t in {1, 2} + else: + assert inst.target_qudits in {1, 2} + if inst.qasm_tag in {"noisex", "noisey"}: + assert inst.lev_a == 0 + assert inst.lev_b == 1 + + @staticmethod + def test_generate_circuit_isolated5(): + dynamic_assigned = SubspaceNoise(0.999, 0.0, []) + # Add errors to noise_tools model + noise_model = NoiseModel() # We know that the architecture is only two qudits + noise_model.add_nonlocal_quantum_error_on_target(dynamic_assigned, ["rh", "rz", "rxy", "cx"]) + qreg_example = QuantumRegister("reg", 4, [5, 5, 5, 5]) + circ = QuantumCircuit(qreg_example) + circ.r(1, [0, 1, np.pi, np.pi / 2]).dag() + circ.r(1, [2, 3, np.pi, np.pi / 2]).control([2], [1]) + circ.cx([0, 1]) + + factory = NoisyCircuitFactory(noise_model, circ) + instructions_og = circ.instructions + new_circ = factory.generate_circuit() + assert circ.number_gates == 3 # namely r, r, cx + assert [i.qasm_tag for i in instructions_og] == ["rxy", "rxy", "cx"] + for tag in [i.qasm_tag for i in new_circ.instructions]: + assert tag in {"rxy", "cx", "noisex", "virtrz", "noisey"} + for inst in new_circ.instructions: + if isinstance(inst.target_qudits, list): + for t in inst.target_qudits: + assert t in {0, 1} + else: + assert inst.target_qudits in {0, 1} + if inst.qasm_tag in {"noisex", "noisey"}: + assert (inst.lev_a == 0 and inst.lev_b == 1) or (inst.lev_a == 2 and inst.lev_b == 3) + + @staticmethod + def test_invalid_dynamic_levels_construction(): + err = SubspaceNoise(0.999, 0.999, (0, 1)) + with pytest.raises(ValueError, match="Negative keys are for the dynamic assignment"): + err.add_noise(-2, -1, Noise(0.99, 0.0)) + + err = SubspaceNoise(0.999, 0.999, []) + with pytest.raises(ValueError, match="Negative keys are already present"): + err.add_noise(1, 2, Noise(0.99, 0.0)) diff --git a/test/python/simulation/test_tnsim.py b/test/python/simulation/test_tnsim.py index 200d85e..e48ecf5 100644 --- a/test/python/simulation/test_tnsim.py +++ b/test/python/simulation/test_tnsim.py @@ -8,6 +8,7 @@ from mqt.qudits.quantum_circuit.components.quantum_register import QuantumRegister from mqt.qudits.simulation import MQTQuditProvider from mqt.qudits.simulation.noise_tools import Noise, NoiseModel +from mqt.qudits.simulation.noise_tools.noise import SubspaceNoise from .._qudits.test_pymisim import is_quantum_state @@ -414,6 +415,48 @@ def test_stochastic_simulation(): assert len(state_vector.squeeze()) == 5**3 assert is_quantum_state(state_vector) + @staticmethod + def test_stochastic_simulation_physical(): + provider = MQTQuditProvider() + backend = provider.get_backend("tnsim") + + qreg_example = QuantumRegister("reg", 3, 3 * [5]) + circuit = QuantumCircuit(qreg_example) + circuit.rz(0, [0, 2, np.pi / 13]) + circuit.x(1).dag() + circuit.s(2) + circuit.csum([2, 1]).dag() + circuit.h(2) + circuit.r(2, [0, 1, np.pi / 5 + np.pi, np.pi / 7]) + circuit.rh(1, [1, 3]) + circuit.x(1).control([0], [2]) + circuit.cx([1, 2], [0, 1, 1, np.pi / 2]).dag() + circuit.csum([0, 1]) + + # Define Physical Noise + sub1 = SubspaceNoise(0.999, 0.999, (0, 1)) + sub2 = SubspaceNoise(0.0, 0.999, [(1, 2), (2, 3)]) + sub3 = SubspaceNoise(0.999, 0.0, [(1, 2), (2, 3)]) + + # Add errors to noise_tools model + + noise_model = NoiseModel() # We know that the architecture is only two qudits + # Very noisy gate_matrix + noise_model.add_all_qudit_quantum_error(sub1, ["csum"]) + # Local Gates + noise_model.add_quantum_error_locally(sub1, ["z"]) + noise_model.add_quantum_error_locally(sub2, ["rh", "h", "rxy", "x"]) + noise_model.add_quantum_error_locally(sub3, ["s"]) + + print("Start execution") + job = backend.run(circuit, noise_model=noise_model, shots=100) + result = job.result() + state_vector = result.get_state_vector() + counts = result.get_counts() + assert len(counts) == 100 + assert len(state_vector.squeeze()) == 5**3 + assert is_quantum_state(state_vector) + def test_tn_multi(self): # noqa: PLR6301 # TODO: Implement test currently just a stub assert True