Source code for braket.circuits.moments

# 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

from collections import OrderedDict
from collections.abc import ItemsView, Iterable, KeysView, Mapping, ValuesView
from enum import Enum
from typing import Any, NamedTuple, Union

from braket.circuits.compiler_directive import CompilerDirective
from braket.circuits.gate import Gate
from braket.circuits.instruction import Instruction
from braket.circuits.measure import Measure
from braket.circuits.noise import Noise
from braket.registers.qubit import Qubit
from braket.registers.qubit_set import QubitSet


[docs] class MomentType(str, Enum): """The type of moments. GATE: a gate NOISE: a noise channel added directly to the circuit GATE_NOISE: a gate-based noise channel INITIALIZATION_NOISE: a initialization noise channel READOUT_NOISE: a readout noise channel COMPILER_DIRECTIVE: an instruction to the compiler, external to the quantum program itself MEASURE: a measurement """ GATE = "gate" NOISE = "noise" GATE_NOISE = "gate_noise" INITIALIZATION_NOISE = "initialization_noise" READOUT_NOISE = "readout_noise" COMPILER_DIRECTIVE = "compiler_directive" GLOBAL_PHASE = "global_phase" MEASURE = "measure"
[docs] class MomentsKey(NamedTuple): """Key of the Moments mapping. Args: time: moment qubits: qubit set moment_type: The type of the moment noise_index: the number of noise channels at the same moment. For gates, this is the number of gate_noise channels associated with that gate. For all other noise types, noise_index starts from 0; but for gate noise, it starts from 1. """ time: int qubits: QubitSet moment_type: MomentType noise_index: int subindex: int = 0
[docs] class Moments(Mapping[MomentsKey, Instruction]): r"""An ordered mapping of `MomentsKey` or `NoiseMomentsKey` to `Instruction`. The core data structure that contains instructions, ordering they are inserted in, and time slices when they occur. `Moments` implements `Mapping` and functions the same as a read-only dictionary. It is mutable only through the `add()` method. This data structure is useful to determine a dependency of instructions, such as printing or optimizing circuit structure, before sending it to a quantum device. The original insertion order is preserved and can be retrieved via the `values()` method. Args: instructions (Iterable[Instruction] | None): Instructions to initialize self. Default = None. Examples: >>> moments = Moments() >>> moments.add([Instruction(Gate.H(), 0), Instruction(Gate.CNot(), [0, 1])]) >>> moments.add([Instruction(Gate.H(), 0), Instruction(Gate.H(), 1)]) >>> for i, item in enumerate(moments.items()): ... print(f"Item {i}") ... print(f"\\tKey: {item[0]}") ... print(f"\\tValue: {item[1]}") ... Item 0 Key: MomentsKey(time=0, qubits=QubitSet([Qubit(0)])) Value: Instruction('operator': H, 'target': QubitSet([Qubit(0)])) Item 1 Key: MomentsKey(time=1, qubits=QubitSet([Qubit(0), Qubit(1)])) Value: Instruction('operator': CNOT, 'target': QubitSet([Qubit(0), Qubit(1)])) Item 2 Key: MomentsKey(time=2, qubits=QubitSet([Qubit(0)])) Value: Instruction('operator': H, 'target': QubitSet([Qubit(0)])) Item 3 Key: MomentsKey(time=2, qubits=QubitSet([Qubit(1)])) Value: Instruction('operator': H, 'target': QubitSet([Qubit(1)])) """ def __init__(self, instructions: Iterable[Instruction] | None = None): self._moments: OrderedDict[MomentsKey, Instruction] = OrderedDict() self._max_times: dict[Qubit, int] = {} self._qubits = QubitSet() self._depth = 0 self._time_all_qubits = -1 self._number_gphase_in_current_moment = 0 self.add(instructions or []) @property def depth(self) -> int: """int: Get the depth (number of slices) of self.""" return self._depth @property def qubit_count(self) -> int: """int: Get the number of qubits used across all of the instructions.""" return len(self._qubits) @property def qubits(self) -> QubitSet: """QubitSet: Get the qubits used across all of the instructions. The order of qubits is based on the order in which the instructions were added. Note: Don't mutate this object, any changes may impact the behavior of this class and / or consumers. If you need to mutate this, then copy it via `QubitSet(moments.qubits())`. """ return self._qubits
[docs] def time_slices(self) -> dict[int, list[Instruction]]: """Get instructions keyed by time. Returns: dict[int, list[Instruction]]: Key is the time and value is a list of instructions that occur at that moment in time. The order of instructions is in no particular order. Note: This is a computed result over self and can be freely mutated. This is re-computed with every call, with a computational runtime O(N) where N is the number of instructions in self. """ time_slices = {} self.sort_moments() for key, instruction in self._moments.items(): instructions = time_slices.get(key.time, []) instructions.append(instruction) time_slices[key.time] = instructions return time_slices
[docs] def add( self, instructions: Union[Iterable[Instruction], Instruction], noise_index: int = 0 ) -> None: """Add one or more instructions to self. Args: instructions (Union[Iterable[Instruction], Instruction]): Instructions to add to self. The instruction is added to the max time slice in which the instruction fits. noise_index (int): the number of noise channels at the same moment. For gates, this is the number of gate_noise channels associated with that gate. For all other noise types, noise_index starts from 0; but for gate noise, it starts from 1. """ if isinstance(instructions, Instruction): instructions = [instructions] for instruction in instructions: self._add(instruction, noise_index)
def _add(self, instruction: Instruction, noise_index: int = 0) -> None: operator = instruction.operator if isinstance(operator, CompilerDirective): time = self._update_qubit_times(self._qubits) self._moments[MomentsKey(time, None, MomentType.COMPILER_DIRECTIVE, 0)] = instruction self._depth = time + 1 self._time_all_qubits = time elif isinstance(operator, Noise): self.add_noise(instruction) elif isinstance(operator, Gate) and operator.name == "GPhase": time = self._get_qubit_times(self._max_times.keys()) + 1 self._number_gphase_in_current_moment += 1 key = MomentsKey( time, QubitSet([]), MomentType.GLOBAL_PHASE, 0, self._number_gphase_in_current_moment, ) self._moments[key] = instruction elif isinstance(operator, Measure): qubit_range = instruction.target.union(instruction.control) time = self._get_qubit_times(self._max_times.keys()) + 1 self._moments[MomentsKey(time, qubit_range, MomentType.MEASURE, noise_index)] = ( instruction ) self._qubits.update(qubit_range) self._depth = max(self._depth, time + 1) else: qubit_range = instruction.target.union(instruction.control) time = self._update_qubit_times(qubit_range) self._moments[MomentsKey(time, qubit_range, MomentType.GATE, noise_index)] = instruction self._qubits.update(qubit_range) self._depth = max(self._depth, time + 1) def _get_qubit_times(self, qubits: QubitSet) -> int: return max([self._max_time_for_qubit(qubit) for qubit in qubits] + [self._time_all_qubits]) def _update_qubit_times(self, qubits: QubitSet) -> int: time = self._get_qubit_times(qubits) + 1 # Update time for all specified qubits for qubit in qubits: self._max_times[qubit] = time self._number_gphase_in_current_moment = 0 return time
[docs] def add_noise( self, instruction: Instruction, input_type: str = "noise", noise_index: int = 0 ) -> None: """Adds noise to a moment. Args: instruction (Instruction): Instruction to add. input_type (str): One of MomentType. noise_index (int): The number of noise channels at the same moment. For gates, this is the number of gate_noise channels associated with that gate. For all other noise types, noise_index starts from 0; but for gate noise, it starts from 1. """ qubit_range = instruction.target time = max(0, *[self._max_time_for_qubit(qubit) for qubit in qubit_range]) if input_type == MomentType.INITIALIZATION_NOISE: time = 0 while MomentsKey(time, qubit_range, input_type, noise_index) in self._moments: noise_index += 1 self._moments[MomentsKey(time, qubit_range, input_type, noise_index)] = instruction self._qubits.update(qubit_range)
[docs] def sort_moments(self) -> None: """Make the disordered moments in order. 1. Make the readout noise in the end 2. Make the initialization noise at the beginning """ # key for NOISE, GATE and GATE_NOISE key_noise = [] # key for INITIALIZATION_NOISE key_initialization_noise = [] # key for READOUT_NOISE key_readout_noise = [] moment_copy = OrderedDict() sorted_moment = OrderedDict() last_measure = self._depth for key, instruction in self._moments.items(): moment_copy[key] = instruction if key.moment_type == MomentType.READOUT_NOISE: key_readout_noise.append(key) elif key.moment_type == MomentType.INITIALIZATION_NOISE: key_initialization_noise.append(key) elif key.moment_type == MomentType.MEASURE: last_measure = key.time key_noise.append(key) else: key_noise.append(key) for key in key_initialization_noise: sorted_moment[key] = moment_copy[key] for key in key_noise: sorted_moment[key] = moment_copy[key] # find the max time in the circuit and make it the time for readout noise max_time = max(last_measure - 1, 0) for key in key_readout_noise: sorted_moment[ MomentsKey(max_time, key.qubits, MomentType.READOUT_NOISE, key.noise_index) ] = moment_copy[key] self._moments = sorted_moment
def _max_time_for_qubit(self, qubit: Qubit) -> int: # -1 if qubit is unoccupied because the first instruction will have an index of 0 return self._max_times.get(qubit, -1) # # Implement abstract methods, default to calling `self`'s underlying dictionary #
[docs] def keys(self) -> KeysView[MomentsKey]: """Return a view of self's keys.""" return self._moments.keys()
[docs] def items(self) -> ItemsView[MomentsKey, Instruction]: """Return a view of self's (key, instruction).""" return self._moments.items()
[docs] def values(self) -> ValuesView[Instruction]: """Return a view of self's instructions. Returns: ValuesView[Instruction]: The (in-order) instructions. """ self.sort_moments() return self._moments.values()
[docs] def get(self, key: MomentsKey, default: Any | None = None) -> Instruction: """Get the instruction in self by key. Args: key (MomentsKey): Key of the instruction to fetch. default (Any | None): Value to return if `key` is not in `moments`. Default = `None`. Returns: Instruction: `moments[key]` if `key` in `moments`, else `default` is returned. """ return self._moments.get(key, default)
def __getitem__(self, key: MomentsKey): return self._moments.__getitem__(key) def __iter__(self): return self._moments.__iter__() def __len__(self): return self._moments.__len__() def __contains__(self, item: MomentsKey): return self._moments.__contains__(item) def __eq__(self, other: Moments): if isinstance(other, Moments): return self._moments == other._moments return NotImplemented def __ne__(self, other: Moments): result = self.__eq__(other) return not result if result is not NotImplemented else NotImplemented def __repr__(self): return self._moments.__repr__() def __str__(self): return self._moments.__str__()