def __init__(self, target: QubitSetInput = None): """ Args: target (QubitSetInput): The target qubits of the reduced density matrix. Default is `None`, and the full density matrix is returned. Examples: >>> ResultType.DensityMatrix(target=[0, 1]) """ self._target = QubitSet(target) ascii_symbols = ["DensityMatrix"] * len(self._target) if self._target else ["DensityMatrix"] super().__init__(ascii_symbols=ascii_symbols)
def __init__(self, target: QubitSetInput = None): """ Args: target (int, Qubit, or iterable of int / Qubit, optional): The target qubits that the result type is requested for. Default is `None`, which means all qubits for the circuit. Examples: >>> ResultType.Probability(target=[0, 1]) """ self._target = QubitSet(target) ascii_symbols = ["Probability"] * len(self._target) if self._target else ["Probability"] super().__init__(ascii_symbols=ascii_symbols)
def qubit_intersection(self, qubits: QubitSetInput) -> QubitSetInput: """ Returns subset of passed qubits that match the criteria. Args: qubits (QubitSetInput): A qubit or set of qubits that may match the criteria. Returns: QubitSetInput: The subset of passed qubits that match the criteria. """ target_qubit = QubitSet(qubits) if self._qubits is None: return target_qubit return self._qubits.intersection(target_qubit)
def i(target: QubitSetInput) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit, int, or iterable of Qubit / int): Target qubit(s) Returns: Iterable[Instruction]: `Iterable` of I instructions. Examples: >>> circ = Circuit().i(0) >>> circ = Circuit().i([0, 1, 2]) """ return [Instruction(Gate.I(), target=qubit) for qubit in QubitSet(target)]
def phaseshift(target: QubitInput, angle: float) -> Instruction: """Registers this function into the circuit class. Args: target (Qubit or int): Target qubit index. angle (float): Angle in radians. Returns: Instruction: PhaseShift instruction. Examples: >>> circ = Circuit().phaseshift(0, 0.15) """ return [Instruction(Gate.PhaseShift(angle), target=qubit) for qubit in QubitSet(target)]
def rz(target: QubitInput, angle: float) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit or int): Target qubit index. angle (float): Angle in radians. Returns: Iterable[Instruction]: Rz instruction. Examples: >>> circ = Circuit().rz(0, 0.15) """ return [ Instruction(Rz(angle), target=qubit) for qubit in QubitSet(target) ]
def phase_damping(target: QubitSetInput, gamma: float) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit, int, or iterable of Qubit / int): Target qubit(s) gamma (float): Probability of phase damping. Returns: Iterable[Instruction]: `Iterable` of PhaseDamping instructions. Examples: >>> circ = Circuit().phase_damping(0, gamma=0.1) """ return [ Instruction(Noise.PhaseDamping(gamma=gamma), target=qubit) for qubit in QubitSet(target) ]
def _ascii_group_items( circuit_qubits: QubitSet, items: List[Union[Instruction, ResultType]], ) -> List[Tuple[QubitSet, List[Instruction]]]: """ Group instructions in a moment for ASCII diagram Args: circuit_qubits (QubitSet): set of qubits in circuit items (List[Union[Instruction, ResultType]]): list of instructions or result types Returns: List[(QubitSet, List[Union[Instruction, ResultType]])]: list of grouped instructions or result types """ groupings = [] for item in items: # Can only print Gate and Noise operators for instructions at the moment if isinstance(item, Instruction) and not isinstance( item.operator, (Gate, Noise, CompilerDirective)): continue if (isinstance(item, ResultType) and not item.target) or ( isinstance(item, Instruction) and isinstance(item.operator, CompilerDirective)): qubit_range = circuit_qubits else: qubit_range = QubitSet( range(min(item.target), max(item.target) + 1)) found_grouping = False for group in groupings: qubits_added = group[0] instr_group = group[1] # Take into account overlapping multi-qubit gates if not qubits_added.intersection(set(qubit_range)): instr_group.append(item) qubits_added.update(qubit_range) found_grouping = True break if not found_grouping: groupings.append((qubit_range, [item])) return groupings
def bit_flip(target: QubitSetInput, probability: float) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit, int, or iterable of Qubit / int): Target qubit(s) probability (float): Probability of bit flipping. Returns: Iterable[Instruction]: `Iterable` of BitFlip instructions. Examples: >>> circ = Circuit().bit_flip(0, probability=0.1) """ return [ Instruction(Noise.BitFlip(probability=probability), target=qubit) for qubit in QubitSet(target) ]
def amplitude_damping(target: QubitSetInput, gamma: float) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit, int, or iterable of Qubit / int): Target qubit(s). gamma (float): decaying rate of the amplitude damping channel. Returns: Iterable[Instruction]: `Iterable` of AmplitudeDamping instructions. Examples: >>> circ = Circuit().amplitude_damping(0, gamma=0.1) """ return [ Instruction(Noise.AmplitudeDamping(gamma=gamma), target=qubit) for qubit in QubitSet(target) ]
def test_apply_noise_to_moments_initialization_2QubitNoise_1( circuit_2qubit, noise_2qubit): circ = apply_noise_to_moments( circuit_2qubit, [noise_2qubit], target_qubits=QubitSet([0, 1]), position="initialization", ) expected = (Circuit().add_instruction(Instruction( noise_2qubit, [0, 1])).add_instruction(Instruction(Gate.X(), 0)).add_instruction( Instruction(Gate.Y(), 1)).add_instruction(Instruction( Gate.X(), 0)).add_instruction(Instruction(Gate.X(), 1)).add_instruction( Instruction(Gate.CNot(), [0, 1]))) assert circ == expected
def test_apply_noise_to_gates_1QubitNoise_2(circuit_2qubit, noise_1qubit): circ = apply_noise_to_gates( circuit_2qubit, [noise_1qubit], target_gates=[Gate.X], target_qubits=QubitSet(0), ) expected = (Circuit().add_instruction(Instruction( Gate.X(), 0)).add_instruction(Instruction(noise_1qubit, 0)).add_instruction( Instruction(Gate.Y(), 1)).add_instruction(Instruction( Gate.X(), 0)).add_instruction(Instruction( noise_1qubit, 0)).add_instruction(Instruction( Gate.X(), 1)).add_instruction(Instruction(Gate.CNot(), [0, 1]))) assert circ == expected
def depolarizing(target: QubitSetInput, probability: float) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (QubitSetInput): Target qubit(s) probability (float): Probability of depolarizing. Returns: Iterable[Instruction]: `Iterable` of Depolarizing instructions. Examples: >>> circ = Circuit().depolarizing(0, probability=0.1) """ return [ Instruction(Noise.Depolarizing(probability=probability), target=qubit) for qubit in QubitSet(target) ]
def test_apply_noise_to_moments_readout_1QubitNoise_2(circuit_2qubit, noise_1qubit): circ = apply_noise_to_moments( circuit_2qubit, [noise_1qubit], target_qubits=QubitSet(1), position="readout", ) expected = (Circuit().add_instruction(Instruction( Gate.X(), 0)).add_instruction(Instruction(Gate.Y(), 1)).add_instruction( Instruction(Gate.X(), 0)).add_instruction(Instruction( Gate.X(), 1)).add_instruction(Instruction( Gate.CNot(), [0, 1])).add_instruction(Instruction(noise_1qubit, 1))) assert circ == expected
def pauli_channel( target: QubitSetInput, probX: float, probY: float, probZ: float ) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit, int, or iterable of Qubit / int): Target qubit(s) probability List[float]: Probabilities for the Pauli X, Y and Z noise happening in the Kraus channel. Returns: Iterable[Instruction]: `Iterable` of PauliChannel instructions. Examples: >>> circ = Circuit().pauli_channel(0,probX=0.1,probY=0.2,probZ=0.3) """ return [ Instruction(Noise.PauliChannel(probX=probX, probY=probY, probZ=probZ), target=qubit) for qubit in QubitSet(target) ]
def test_apply_noise_to_gates_1QubitNoise_not_dense(circuit_2qubit_not_dense, noise_1qubit): circ = apply_noise_to_gates( circuit_2qubit_not_dense, [noise_1qubit], target_qubits=QubitSet([0, 1]), target_gates=None, ) expected_moments = Moments() expected_moments._add(Instruction(Gate.X(), 0), noise_index=1) expected_moments.add_noise(Instruction(noise_1qubit, 0), "gate_noise", 1) expected_moments._add(Instruction(Gate.Y(), 1), noise_index=1) expected_moments.add_noise(Instruction(noise_1qubit, 1), "gate_noise", 1) expected_moments._add(Instruction(Gate.X(), 0), noise_index=1) expected_moments.add_noise(Instruction(noise_1qubit, 0), "gate_noise", 1) expected_moments._add(Instruction(Gate.CNot(), [0, 1]), noise_index=2) expected_moments.add_noise(Instruction(noise_1qubit, 0), "gate_noise", 1) expected_moments.add_noise(Instruction(noise_1qubit, 1), "gate_noise", 2) assert circ.moments == expected_moments
def test_apply_noise_to_gates_2QubitNoise_2(circuit_3qubit, noise_2qubit, noise_1qubit, noise_1qubit_2): circ = apply_noise_to_gates( circuit_3qubit, [noise_1qubit, noise_2qubit, noise_1qubit_2], target_gates=[Gate.CZ], target_qubits=QubitSet([1, 2]), ) expected = (Circuit().add_instruction(Instruction( Gate.X(), 0)).add_instruction(Instruction(Gate.Y(), 1)).add_instruction( Instruction(Gate.CNot(), [0, 1])).add_instruction( Instruction(Gate.Z(), 2)).add_instruction( Instruction(Gate.CZ(), [2, 1])).add_instruction( Instruction(noise_1qubit, 1)).add_instruction( Instruction(noise_1qubit, 2)).add_instruction( Instruction(noise_2qubit, [2, 1])).add_instruction( Instruction(noise_1qubit_2, 1)). add_instruction(Instruction( noise_1qubit_2, 2)).add_instruction(Instruction( Gate.CNot(), [0, 2])).add_instruction( Instruction(Gate.CZ(), [1, 2])).add_instruction( Instruction(noise_1qubit, 1)).add_instruction( Instruction( noise_1qubit, 2)).add_instruction( Instruction( noise_2qubit, [1, 2])).add_instruction( Instruction( noise_1qubit_2, 1)).add_instruction( Instruction( noise_1qubit_2, 2))) assert circ == expected
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: target_qubits (Optional[QubitSetInput] = None): Index or indices of qubit(s). Returns: target_qubits: QubitSet """ 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 not all(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
def __init__(self, operator: InstructionOperator, target: QubitSetInput = None): """ InstructionOperator includes objects of type `Gate` only. Args: operator (InstructionOperator): Operator for the instruction. target (int, Qubit, or iterable of int / Qubit): Target qubits that the operator is applied to. Raises: ValueError: If `operator` is empty or any integer in `target` does not meet the `Qubit` or `QubitSet` class requirements. Also, if operator qubit count does not equal the size of the target qubit set. TypeError: If a `Qubit` class can't be constructed from `target` due to an incorrect `typing`. Examples: >>> Instruction(Gate.CNot(), [0, 1]) Instruction('operator': CNOT, 'target': QubitSet(Qubit(0), Qubit(1))) >>> instr = Instruction(Gate.CNot()), QubitSet([0, 1])]) Instruction('operator': CNOT, 'target': QubitSet(Qubit(0), Qubit(1))) >>> instr = Instruction(Gate.H(), 0) Instruction('operator': H, 'target': QubitSet(Qubit(0),)) >>> instr = Instruction(Gate.Rx(0.12), 0) Instruction('operator': Rx, 'target': QubitSet(Qubit(0),)) """ if not operator: raise ValueError("Operator cannot be empty") target_set = QubitSet(target) if isinstance( operator, QuantumOperator) and len(target_set) != operator.qubit_count: raise ValueError( f"Operator qubit count {operator.qubit_count} must be equal to" f" size of target qubit set {target_set}") self._operator = operator self._target = target_set
def generalized_amplitude_damping( target: QubitSetInput, gamma: float, probability: float ) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target (Qubit, int, or iterable of Qubit / int): Target qubit(s). p(float): Probability of the system being excited by the environment. gamma (float): The damping rate of the amplitude damping channel. Returns: Iterable[Instruction]: `Iterable` of GeneralizedAmplitudeDamping instructions. Examples: >>> circ = Circuit().generalized_amplitude_damping(0, probability = 0.9, gamma=0.1) """ return [ Instruction( Noise.GeneralizedAmplitudeDamping(gamma=gamma, probability=probability), target=qubit, ) for qubit in QubitSet(target) ]
def __init__( self, ascii_symbols: List[str], observable: Observable, target: QubitSetInput = None ): """ Args: ascii_symbols (List[str]): ASCII string symbols for the result type. This is used when printing a diagram of circuits. observable (Observable): the observable for the result type target (QubitSetInput): Target qubits that the result type is requested for. Default is `None`, which means the observable must only operate on 1 qubit and it will be applied to all qubits in parallel Raises: ValueError: if target=None and the observable's qubit count is not 1. Or, if `target!=None` and the observable's qubit count and the number of target qubits are not equal. Or, if `target!=None` and the observable's qubit count and the number of `ascii_symbols` are not equal. """ super().__init__(ascii_symbols) self._observable = observable self._target = QubitSet(target) if not self._target: if self._observable.qubit_count != 1: raise ValueError( f"Observable {self._observable} must only operate on 1 qubit for target=None" ) else: if self._observable.qubit_count != len(self._target): raise ValueError( f"Observable's qubit count {self._observable.qubit_count} and " f"the size of the target qubit set {self._target} must be equal" ) if self._observable.qubit_count != len(self.ascii_symbols): raise ValueError( "Observable's qubit count and the number of ASCII symbols must be equal" )
def _ascii_diagram_column( circuit_qubits: QubitSet, items: List[Union[Instruction, ResultType]]) -> str: """ Return a column in the ASCII string diagram of the circuit for a given list of items. Args: circuit_qubits (QubitSet): qubits in circuit items (List[Union[Instruction, ResultType]]): list of instructions or result types Returns: str: An ASCII string diagram for the specified moment in time for a column. """ symbols = {qubit: "-" for qubit in circuit_qubits} margins = {qubit: " " for qubit in circuit_qubits} for item in items: if isinstance(item, ResultType) and not item.target: target_qubits = circuit_qubits qubits = circuit_qubits ascii_symbols = [item.ascii_symbols[0]] * len(circuit_qubits) elif isinstance(item, Instruction) and isinstance( item.operator, CompilerDirective): target_qubits = circuit_qubits qubits = circuit_qubits ascii_symbol = item.ascii_symbols[0] marker = "*" * len(ascii_symbol) num_after = len(circuit_qubits) - 1 after = ["|"] * (num_after - 1) + ([marker] if num_after else []) ascii_symbols = [ascii_symbol] + after else: target_qubits = item.target qubits = circuit_qubits.intersection( set(range(min(item.target), max(item.target) + 1))) ascii_symbols = item.ascii_symbols for qubit in qubits: # Determine if the qubit is part of the item or in the middle of a # multi qubit item. if qubit in target_qubits: item_qubit_index = [ index for index, q in enumerate(target_qubits) if q == qubit ][0] symbols[qubit] = ascii_symbols[item_qubit_index] else: symbols[qubit] = "|" # Set the margin to be a connector if not on the first qubit if qubit != min(target_qubits): margins[qubit] = "|" symbols_width = max([len(symbol) for symbol in symbols.values()]) output = "" for qubit in circuit_qubits: output += "{0:{width}}\n".format(margins[qubit], width=symbols_width + 1) output += "{0:{fill}{align}{width}}\n".format(symbols[qubit], fill="-", align="<", width=symbols_width + 1) return output
def qubits(self) -> QubitSet: """QubitSet: Get a copy of the qubits for this circuit.""" return QubitSet(self._moments.qubits)
def apply_readout_noise( self, noise: Union[Type[Noise], Iterable[Type[Noise]]], target_qubits: Optional[QubitSetInput] = None, ) -> Circuit: """Apply `noise` right before measurement in every qubit (default) or target_qubits`. Only when `target_qubits` is given can the noise be applied to an empty circuit. When `noise.qubit_count` > 1, the number of qubits in target_qubits must be equal to `noise.qubit_count`. Args: noise (Union[Type[Noise], Iterable[Type[Noise]]]): Noise channel(s) to be applied to the circuit. target_qubits (Union[QubitSetInput, optional]): Index or indices of qubit(s). Default=None. Returns: Circuit: self Raises: TypeError: If `noise` is not Noise type. If `target_qubits` has non-integers. IndexError: If applying noise to an empty circuit. ValueError: If `target_qubits` has negative integers. If `noise.qubit_count` > 1 and the number of qubits in target_qubits is not the same as `noise.qubit_count`. Examples: >>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1) >>> print(circ) >>> noise = Noise.Depolarizing(probability=0.1) >>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1) >>> print(circ.apply_initialization_noise(noise)) >>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1) >>> print(circ.apply_initialization_noise(noise, target_qubits = 1)) >>> circ = Circuit() >>> print(circ.apply_initialization_noise(noise, target_qubits = [0, 1])) """ if (len(self.qubits) == 0) and (target_qubits is None): raise IndexError( "target_qubits must be provided in order to apply the readout noise \ to an empty circuit.") if target_qubits is None: target_qubits = self.qubits else: if not isinstance(target_qubits, list): target_qubits = [target_qubits] if not all(isinstance(q, int) for q in target_qubits): raise TypeError("target_qubits must be integer(s)") if not all(q >= 0 for q in target_qubits): raise ValueError( "target_qubits must contain only non-negative integers.") target_qubits = QubitSet(target_qubits) # make noise a list noise = wrap_with_list(noise) for noise_channel in noise: if not isinstance(noise_channel, Noise): raise TypeError("Noise must be an instance of the Noise class") if noise_channel.qubit_count > 1 and noise_channel.qubit_count != len( target_qubits): raise ValueError( "target_qubits needs to be provided for this multi-qubit noise channel, and \ the number of qubits in target_qubits must be the same as defined by the multi-qubit noise channel." ) return apply_noise_to_moments(self, noise, target_qubits, "readout")
class Moments(Mapping[MomentsKey, Instruction]): """ An ordered mapping of `MomentsKey` 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], optional): Instructions to initialize self. Default = []. 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] = []): self._moments: OrderedDict[MomentsKey, Instruction] = OrderedDict() self._max_times: Dict[Qubit, int] = {} self._qubits = QubitSet() self._depth = 0 self.add(instructions) @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 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 = {} for key, instruction in self._moments.items(): instructions = time_slices.get(key.time, []) instructions.append(instruction) time_slices[key.time] = instructions return time_slices def add(self, instructions: Iterable[Instruction]) -> None: """ Add instructions to self. Args: instructions (Iterable[Instruction]): Instructions to add to self. The instruction is added to the max time slice in which the instruction fits. """ for instruction in instructions: self._add(instruction) def _add(self, instruction: Instruction) -> None: qubit_range = instruction.target time = max([self._max_time_for_qubit(qubit) for qubit in qubit_range]) + 1 # Mark all qubits in qubit_range with max_time for qubit in qubit_range: self._max_times[qubit] = max(time, self._max_time_for_qubit(qubit)) self._moments[MomentsKey(time, instruction.target)] = instruction self._qubits.update(instruction.target) self._depth = max(self._depth, time + 1) def _max_time_for_qubit(self, qubit: Qubit) -> int: return self._max_times.get(qubit, -1) # # Implement abstract methods, default to calling selfs underlying dictionary # def keys(self) -> KeysView[MomentsKey]: """Return a view of self's keys.""" return self._moments.keys() def items(self) -> ItemsView[MomentsKey, Instruction]: """Return a view of self's (key, instruction).""" return self._moments.items() def values(self) -> ValuesView[Instruction]: """Return a view of self's instructions.""" return self._moments.values() def get(self, key: MomentsKey, default=None) -> Instruction: """ Get the instruction in self by key. Args: key (MomentsKey): Key of the instruction to fetch. default (Any, optional): 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): return self._moments.__getitem__(key) def __iter__(self): return self._moments.__iter__() def __len__(self): return self._moments.__len__() def __contains__(self, item): return self._moments.__contains__(item) def __eq__(self, other): if isinstance(other, Moments): return (self._moments) == (other._moments) return NotImplemented def __ne__(self, other): result = self.__eq__(other) if result is not NotImplemented: return not result return NotImplemented def __repr__(self): return self._moments.__repr__() def __str__(self): return self._moments.__str__()
class Moments(Mapping[MomentsKey, Instruction]): """ 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], optional): 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): self._moments: OrderedDict[MomentsKey, Instruction] = OrderedDict() self._max_times: Dict[Qubit, int] = {} self._qubits = QubitSet() self._depth = 0 self._time_all_qubits = -1 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 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 def add(self, instructions: Iterable[Instruction], noise_index: int = 0) -> None: """ Add instructions to self. Args: instructions (Iterable[Instruction]): Instructions to add to self. The instruction is added to the max time slice in which the instruction fits. """ 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) else: qubit_range = instruction.target time = self._update_qubit_times(qubit_range) self._moments[ MomentsKey(time, instruction.target, MomentType.GATE, noise_index) ] = instruction self._qubits.update(instruction.target) self._depth = max(self._depth, time + 1) def _update_qubit_times(self, qubits): qubit_max_times = [self._max_time_for_qubit(qubit) for qubit in qubits] + [ self._time_all_qubits ] time = max(qubit_max_times) + 1 # Update time for all specified qubits for qubit in qubits: self._max_times[qubit] = time return time def add_noise( self, instruction: Instruction, input_type: str = "noise", noise_index: int = 0 ) -> None: 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 = noise_index + 1 self._moments[MomentsKey(time, qubit_range, input_type, noise_index)] = instruction self._qubits.update(qubit_range) 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() 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) 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(self._depth - 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 selfs underlying dictionary # def keys(self) -> KeysView[MomentsKey]: """Return a view of self's keys.""" return self._moments.keys() def items(self) -> ItemsView[MomentsKey, Instruction]: """Return a view of self's (key, instruction).""" return self._moments.items() def values(self) -> ValuesView[Instruction]: """Return a view of self's instructions.""" self.sort_moments() return self._moments.values() def get(self, key: MomentsKey, default=None) -> Instruction: """ Get the instruction in self by key. Args: key (MomentsKey): Key of the instruction to fetch. default (Any, optional): 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): return self._moments.__getitem__(key) def __iter__(self): return self._moments.__iter__() def __len__(self): return self._moments.__len__() def __contains__(self, item): return self._moments.__contains__(item) def __eq__(self, other): if isinstance(other, Moments): return self._moments == other._moments return NotImplemented def __ne__(self, other): result = self.__eq__(other) if result is not NotImplemented: return not result return NotImplemented def __repr__(self): return self._moments.__repr__() def __str__(self): return self._moments.__str__()
def qubits(self) -> QubitSet: """QubitSet: Get a copy of the qubits for this circuit.""" return QubitSet(self._moments.qubits.union(self._qubit_observable_set))
def target(self, target: QubitSetInput) -> None: """Sets the target. Args: target (QubitSetInput): The new target. """ self._target = QubitSet(target)
def target(self, target: QubitSetInput) -> None: self._target = QubitSet(target)
class Instruction: """ An instruction is a quantum directive that describes the task to perform on a quantum device. """ def __init__(self, operator: InstructionOperator, target: QubitSetInput): """ InstructionOperator includes objects of type `Gate` only. Args: operator (InstructionOperator): Operator for the instruction. target (int, Qubit, or iterable of int / Qubit): Target qubits that the operator is applied to. Raises: ValueError: If `operator` is empty or any integer in `target` does not meet the `Qubit` or `QubitSet` class requirements. TypeError: If a `Qubit` class can't be constructed from `target` due to an incorrect `typing`. Examples: >>> Instruction(Gate.CNot(), [0, 1]) Instruction('operator': CNOT, 'target': QubitSet(Qubit(0), Qubit(1))) >>> instr = Instruction(Gate.CNot()), QubitSet([0, 1])]) Instruction('operator': CNOT, 'target': QubitSet(Qubit(0), Qubit(1))) >>> instr = Instruction(Gate.H(), 0) Instruction('operator': H, 'target': QubitSet(Qubit(0),)) >>> instr = Instruction(Gate.Rx(0.12), 0) Instruction('operator': Rx, 'target': QubitSet(Qubit(0),)) """ if not operator: raise ValueError("Operator cannot be empty") self._operator = operator self._target = QubitSet(target) @property def operator(self) -> InstructionOperator: """Operator: The operator for the instruction, for example, `Gate`.""" return self._operator @property def target(self) -> QubitSet: """ QubitSet: Target qubits that the operator is applied to. Note: Don't mutate this property, any mutations can have unexpected consequences. """ return self._target def to_ir(self): """ Converts the operator into the canonical intermediate representation. If the operator is passed in a request, this method is called before it is passed. """ return self._operator.to_ir([int(qubit) for qubit in self._target]) def copy( self, target_mapping: Dict[QubitInput, QubitInput] = {}, target: QubitSetInput = None ) -> Instruction: """ Return a shallow copy of the instruction. Note: If `target_mapping` is specified, then `self.target` is mapped to the specified qubits. This is useful apply an instruction to a circuit and change the target qubits. Args: target_mapping (dictionary[int or Qubit, int or Qubit], optional): A dictionary of qubit mappings to apply to the target. Key is the qubit in this `target` and the value is what the key is changed to. Default = `{}`. target (int, Qubit, or iterable of int / Qubit, optional): Target qubits for the new instruction. Returns: Instruction: A shallow copy of the instruction. Raises: TypeError: If both `target_mapping` and `target` are supplied. Examples: >>> instr = Instruction(Gate.H(), 0) >>> new_instr = instr.copy() >>> new_instr.target QubitSet(Qubit(0)) >>> new_instr = instr.copy(target_mapping={0: 5}) >>> new_instr.target QubitSet(Qubit(5)) >>> new_instr = instr.copy(target=[5]) >>> new_instr.target QubitSet(Qubit(5)) """ if target_mapping and target is not None: raise TypeError("Only 'target_mapping' or 'target' can be supplied, but not both.") elif target is not None: return Instruction(self._operator, target) else: return Instruction(self._operator, self._target.map(target_mapping)) def __repr__(self): return f"Instruction('operator': {self._operator}, 'target': {self._target})" def __eq__(self, other): if isinstance(other, Instruction): return (self._operator, self._target) == (other._operator, other._target) return NotImplemented