# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from __future__ import annotations
import warnings
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Optional, Union
import numpy as np
from braket.circuits.gate import Gate
from braket.circuits.instruction import Instruction
from braket.circuits.moments import Moments
from braket.circuits.noise import Noise
from braket.circuits.quantum_operator_helpers import is_unitary
from braket.registers.qubit_set import QubitSet, QubitSetInput
if TYPE_CHECKING: # pragma: no cover
from braket.circuits.circuit import Circuit
[docs]
def no_noise_applied_warning(noise_applied: bool) -> None:
"""Helper function to give a warning is noise is not applied.
Args:
noise_applied (bool): True if the noise has been applied.
"""
if not noise_applied:
warnings.warn(
"Noise is not applied to any gate, as there is no eligible gate in the circuit"
" with the input criteria or there is no multi-qubit gate to apply"
" the multi-qubit noise.",
stacklevel=1,
)
[docs]
def wrap_with_list(an_item: Any) -> list[Any]:
"""Helper function to make the input parameter a list.
Args:
an_item (Any): The item to wrap.
Returns:
list[Any]: The item wrapped in a list.
"""
if an_item is not None and not isinstance(an_item, list):
an_item = [an_item]
return an_item
[docs]
def check_noise_target_gates(noise: Noise, target_gates: Iterable[type[Gate]]) -> None:
"""Helper function to check
1. whether all the elements in target_gates are a Gate type;
2. if `noise` is multi-qubit noise and `target_gates` contain gates
with the number of qubits is the same as `noise.qubit_count`.
Args:
noise (Noise): A Noise class object to be applied to the circuit.
target_gates (Iterable[type[Gate]]): Gate class or
List of Gate classes which `noise` is applied to.
"""
if not all(isinstance(g, type) and issubclass(g, Gate) for g in target_gates):
raise TypeError("All elements in target_gates must be an instance of the Gate class")
if noise.qubit_count > 1:
for g in target_gates:
fixed_qubit_count = g.fixed_qubit_count()
if fixed_qubit_count is NotImplemented:
raise ValueError(
f"Target gate {g} can be instantiated on a variable number of qubits,"
" but noise can only target gates with fixed qubit counts."
)
if fixed_qubit_count != noise.qubit_count:
raise ValueError(
f"Target gate {g} acts on {fixed_qubit_count} qubits,"
f" but {noise} acts on {noise.qubit_count} qubits."
)
[docs]
def check_noise_target_unitary(noise: Noise, target_unitary: np.ndarray) -> None:
"""Helper function to check
1. whether the input matrix is a np.ndarray type;
2. whether the target_unitary is a unitary;
Args:
noise (Noise): A Noise class object to be applied to the circuit.
target_unitary (ndarray): matrix of the target unitary gates
"""
if not isinstance(target_unitary, np.ndarray):
raise TypeError("target_unitary must be a np.ndarray type")
if not is_unitary(target_unitary):
raise ValueError("target_unitary must be a unitary")
[docs]
def check_noise_target_qubits(
circuit: Circuit, target_qubits: Optional[QubitSetInput] = None
) -> QubitSet:
"""Helper function to check whether all the target_qubits are positive integers.
Args:
circuit (Circuit): A circuit where `noise` is to be checked.
target_qubits (Optional[QubitSetInput]): Index or indices of qubit(s).
Returns:
QubitSet: The target qubits.
"""
if target_qubits is None:
target_qubits = circuit.qubits
else:
target_qubits = wrap_with_list(target_qubits)
if not all(isinstance(q, int) for q in target_qubits):
raise TypeError("target_qubits must be integer(s)")
if any(q < 0 for q in target_qubits):
raise ValueError("target_qubits must contain only non-negative integers.")
target_qubits = QubitSet(target_qubits)
return target_qubits
[docs]
def apply_noise_to_moments(
circuit: Circuit, noise: Iterable[type[Noise]], target_qubits: QubitSet, position: str
) -> Circuit:
"""Apply initialization/readout noise to the circuit.
When `noise.qubit_count` == 1, `noise` is added to all qubits in `target_qubits`.
When `noise.qubit_count` > 1, `noise.qubit_count` must be the same as the length of
`target_qubits`.
Args:
circuit (Circuit): A circuit to `noise` is applied to.
noise (Iterable[type[Noise]]): Noise channel(s) to be applied
to the circuit.
target_qubits (QubitSet): Index or indices of qubits. `noise` is applied to.
position (str): The position to add the noise to. May be 'initialization' or
'readout_noise'.
Returns:
Circuit: modified circuit.
"""
noise_instructions = []
for noise_channel in noise:
if noise_channel.qubit_count == 1:
new = [Instruction(noise_channel, qubit) for qubit in target_qubits]
noise_instructions = noise_instructions + new
else:
noise_instructions.append(Instruction(noise_channel, target_qubits))
new_moments = Moments()
if position == "initialization":
for noise in noise_instructions:
new_moments.add_noise(noise, "initialization_noise")
# add existing instructions
for moment_key in circuit.moments:
instruction = circuit.moments[moment_key]
# if the instruction is noise instruction
if isinstance(instruction.operator, Noise):
new_moments.add_noise(instruction, moment_key.moment_type, moment_key.noise_index)
# if the instruction is a gate instruction
else:
new_moments.add([instruction], moment_key.noise_index)
if position == "readout":
for noise in noise_instructions:
new_moments.add_noise(noise, "readout_noise")
circuit._moments = new_moments
return circuit
def _apply_noise_to_gates_helper(
noise: Iterable[type[Noise]],
target_qubits: QubitSet,
instruction: Instruction,
noise_index: int,
intersection: QubitSet,
noise_applied: bool,
new_noise_instruction: Iterable,
) -> tuple[Iterable[Instruction], int, bool]:
"""Helper function to work out the noise instructions to be attached to a gate.
Args:
noise (Iterable[type[Noise]]): Noise channel(s) to be applied
to the circuit.
target_qubits (QubitSet): Index or indices of qubits which `noise` is applied to.
instruction (Instruction): Instruction of the gate which `noise` is applied to.
noise_index (int): The number of noise channels applied to the gate.
intersection (QubitSet): Intersection of target_qubits and the qubits associated
with the gate.
noise_applied (bool): Whether noise is applied or not.
new_noise_instruction (Iterable): current new noise instructions to be attached
to the circuit.
Returns:
tuple[Iterable[Instruction], int, bool]: A tuple of three values:
new_noise_instruction: A list of noise instructions
noise_index: The number of noise channels applied to the gate
noise_applied: Whether noise is applied or not
"""
for noise_channel in noise:
if noise_channel.qubit_count == 1:
for qubit in intersection:
# apply noise to the qubit if it is in target_qubits
noise_index += 1
new_noise_instruction.append((Instruction(noise_channel, qubit), noise_index))
noise_applied = True
# only apply noise to the gates that have the same qubit_count as the noise.
elif (
instruction.operator.qubit_count == noise_channel.qubit_count
and instruction.target.issubset(target_qubits)
):
noise_index += 1
new_noise_instruction.append(
(Instruction(noise_channel, instruction.target), noise_index)
)
noise_applied = True
return new_noise_instruction, noise_index, noise_applied
[docs]
def apply_noise_to_gates(
circuit: Circuit,
noise: Iterable[type[Noise]],
target_gates: Union[Iterable[type[Gate]], np.ndarray],
target_qubits: QubitSet,
) -> Circuit:
"""Apply noise after target gates in target qubits.
When `noise.qubit_count` == 1, `noise` is applied to target_qubits after `target_gates`.
When `noise.qubit_count` > 1, all elements in `target_gates`, if is given, must have
the same number of qubits as `noise.qubit_count`.
Args:
circuit (Circuit): A circuit where `noise` is applied to.
noise (Iterable[type[Noise]]): Noise channel(s) to be applied
to the circuit.
target_gates (Union[Iterable[type[Gate]], ndarray]): List of gates, or a unitary matrix
which `noise` is applied to.
target_qubits (QubitSet): Index or indices of qubits which `noise` is applied to.
Returns:
Circuit: modified circuit.
Raises:
Warning:
If `noise` is multi-qubit noise while there is no gate with the same
number of qubits in `target_qubits` or in the whole circuit when
`target_qubits` is not given.
If no `target_gates` exist in `target_qubits` or in the whole circuit
when `target_qubits` is not given.
"""
new_moments = Moments()
noise_applied = False
for moment_key in circuit.moments:
instruction = circuit.moments[moment_key]
# add the instruction to new_moments if it is noise instruction
if isinstance(instruction.operator, Noise):
new_moments.add_noise(instruction, moment_key.moment_type, moment_key.noise_index)
# if the instruction is a gate instruction
else:
new_noise_instruction = []
noise_index = moment_key.noise_index
if isinstance(target_gates, np.ndarray):
if (instruction.operator.name == "Unitary") and (
np.array_equiv(instruction.operator._matrix, target_gates)
):
intersection = list(set(instruction.target) & set(target_qubits))
(
new_noise_instruction,
noise_index,
noise_applied,
) = _apply_noise_to_gates_helper(
noise,
target_qubits,
instruction,
noise_index,
intersection,
noise_applied,
new_noise_instruction,
)
elif (target_gates is None) or (
instruction.operator.name in [g.__name__ for g in target_gates]
):
intersection = list(set(instruction.target) & set(target_qubits))
new_noise_instruction, noise_index, noise_applied = _apply_noise_to_gates_helper(
noise,
target_qubits,
instruction,
noise_index,
intersection,
noise_applied,
new_noise_instruction,
)
# add the gate and gate noise instructions to new_moments
new_moments.add([instruction], noise_index=noise_index)
for instruction, noise_index in new_noise_instruction:
new_moments.add_noise(instruction, "gate_noise", noise_index)
no_noise_applied_warning(noise_applied)
circuit._moments = new_moments
return circuit