Source code for qclight.circuit.circuit

"""QCLCircuit class"""
import re
from typing import Iterable
import numpy as np
import numpy.typing as npt
from qclight.utils import extract_bits, range_fixed_bits_switch
from qclight.gate import HGate, XGate, IGate
from qclight.error import BinaryStringError, PositiveValueError, PowerOfTwoLengthError

FloatNDArray = npt.NDArray[np.float64]


[docs]class QCLCircuit: """Quantum circuit used for a generic computation""" def __init__(self, state: "str | list[float] | int") -> "None": """QCLCircuit constructor. Args: state: initial state of the circuit """ self._state: "FloatNDArray" self._n: "int" self.gates: "list[FloatNDArray]" self._result: "FloatNDArray | None" self._identity: "FloatNDArray" self._initialize_state(state) @property def n(self) -> "int": """Number of qubits in the circuit.""" return self._n @n.setter def n(self, n: "int") -> "None": self._n = n self._identity = IGate().matrix_of_size(n) @property def state(self) -> "FloatNDArray": """Initial state of the circuit.""" return self._state @property def result(self) -> "FloatNDArray": """Result of the last simulation or the initial state if no simulation has been run yet. """ return self._result if self._result is not None else self._state def __normalize(self, vector: "FloatNDArray") -> "FloatNDArray": """Normalize an array so that the sum of all the elements is 1. Args: vector: state to be normalized Raises: ValueError: the current norm of the vector is 0 Returns: normalized vector """ norm = np.linalg.norm(vector) if norm == 0: raise ValueError("Cannot normalize a zero vector") if np.isclose(norm, 1): return vector return vector / norm def _initialize_state(self, state: "str | list[float] | int") -> "FloatNDArray": """Initializes the state of the circuit. The state is initialized so that the probability of measuring state is 100%. The circuit is effectively reset, so no gates are applied. Args: state: set the initial state of the circuit so that there is 100% probability of measuring the provided state """ self.gates = [] self._result = None if isinstance(state, str): if re.match(r"^[01]+$", state) is None: raise BinaryStringError(state) self.n = len(state) self._state = np.zeros(2**self.n) pos = int(state, 2) self._state[pos] = 1 elif isinstance(state, list): length = len(state) if (length & (length - 1) != 0) or length == 0: raise PowerOfTwoLengthError(length) self._state = self.__normalize(np.array(state)) self.n = length.bit_length() - 1 elif isinstance(state, int): if state <= 0: raise PositiveValueError(state) self._state = np.zeros(2**state) self._state[0] = 1 self.n = state return self._state
[docs] def initialize_circuit(self, state: "str") -> "None": """Builds a circuit that output the provided state starting from the all-zero state. Args: state: state to be outputted """ if not isinstance(state, str) or re.match(r"^[01h]+$", state) is None: raise ValueError("state must be a string that verifies the regex ^[01h]+$") if len(state) != self.n: raise ValueError(f"state must be a string of length n={self.n}") self.gates = [] self._result = None self._state = np.zeros(2**self.n) self._state[0] = 1 x_list = [i for i, digit in enumerate(state) if digit == "1"] h_list = [i for i, digit in enumerate(state) if digit == "h"] self.x(x_list) self.h(h_list)
[docs] def x(self, i: "int | list[int]") -> "None": """Applies a :class:`~qclight.gate.x_gate.XGate` gate to the qubit in position i. | i | X | |---|---| | 0 | 1 | | 1 | 0 | .. table:: Truth table of the X gate. === === i X === === 0 1 1 0 === === Args: i: position of the qubit to be affected by the gate """ self.gates.append(XGate().matrix_of_size(self.n, i))
[docs] def h(self, i: "int | list[int]") -> "None": """Applies a :class:`~qclight.gate.h_gate.HGate` gate to the qubit in position i. Args: i: position of the qubit to be affected by the gate """ self.gates.append(HGate().matrix_of_size(self.n, i))
[docs] def cx(self, c: "int", t: "int") -> "None": """Applies a cx gate controlled by the qubit in position c. The qubit in position t will be negated if the control qubit is 1. | c | t | CX | |:-:|:-:|:--:| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 | .. table:: Truth table of the CX gate. === === ==== c t CX === === ==== 0 0 0 0 1 1 1 0 1 1 1 0 === === ==== Args: c: position of the qubit controlling the gate t: position of the qubit to be affected by the gate """ self.mcx((c,), t)
[docs] def ccx(self, c1: "int", c2: "int", t: "int") -> "None": """Applies a ccx gate controlled by both qubits in position c1 and c2. The qubit in position t will be negated if both control qubits are 1. | c1 | c2 | t | CCX | |:--:|:--:|:-:|:---:| | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 0 | | 1 | 0 | 0 | 0 | | 1 | 1 | 0 | 1 | | 0 | 0 | 1 | 1 | | 0 | 1 | 1 | 1 | | 1 | 0 | 1 | 1 | | 1 | 1 | 1 | 0 | .. table:: Truth table of the CCX gate. ==== ==== ==== ==== c1 c2 t CCX ==== ==== ==== ==== 0 0 0 0 0 1 0 0 1 0 0 0 1 1 0 1 0 0 1 1 0 1 1 1 1 0 1 1 1 1 1 0 ==== ==== ==== ==== Args: c1: position of the first qubit controlling the gate c2: position of the second qubit controlling the gate t: position of the qubit to be affected by the gate """ self.mcx((c1, c2), t)
[docs] def mcx(self, c_bits: "Iterable[int]", t: "int") -> "None": """Applies a mcx gate controlled by all the qubits in position c_bits. The qubit in position t will be negated if all control qubits are 1. .. math:: mcx(c_1, c_2, ..., c_n, t) = \\begin{cases} \\lnot t & \\text{if } c_1 \\land c_2 \\land \\dots \\land c_n \\\\ t & \\text{otherwise} \\\\ \\end{cases} Args: c_bits: positions of the qubits controlling the gate t: position of the qubit to be affected by the gate """ identity = self._identity.copy() for idx, swap_idx in range_fixed_bits_switch(self.n, set(c_bits), t): identity[idx], identity[swap_idx] = ( identity[swap_idx].copy(), identity[idx].copy(), ) self.gates.append(identity)
[docs] def or_(self, q1: "int", q2: "int", t: "int") -> "None": """Applies an OR gate to the qubits in position q1 and q2. The result will be stored in the qubit in position t. | q1 | q2 | OR | |:--:|:--:|:--:| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 1 | .. table:: Truth table of the OR gate. ==== ==== ==== q1 q2 OR ==== ==== ==== 0 0 0 0 1 1 1 0 1 1 1 1 ==== ==== ==== """ self.cx(q1, t) self.cx(q2, t) self.ccx(q1, q2, t)
[docs] def swap(self, i: "int", j: "int") -> "None": """Swaps the qubit in position i with the qubit in position j. Args: i: position of the first qubit to be swapped j: position of the second qubit to be swapped """ self.cx(i, j) self.cx(j, i) self.cx(i, j)
[docs] def run(self) -> "FloatNDArray": """Runs the simulator and returns the result. Returns: results of the computation """ result = self._state for gate in self.gates: result = np.matmul(result, gate) self._result = result return self.result
[docs] def counts( self, auto_run: "bool" = True, msr_list: "list[int] | None" = None ) -> "None": """Shows the result of the computation and the probability for each to happen. If auto_run is True, the circuit is run before showing the result. If an msr_list is provided, the result is shown only for the qubits with that index. Args: auto_run: whether to run the circuit before showing the result msr_list: list of indices of the qubits to be considered """ if auto_run: self.run() print("RESULTS:") # Show the results for all qubits if msr_list is None or len(msr_list) == 0: for i, digit in enumerate(self.result): if digit > 0: print(f"{i:0{self.n}b} - {np.square(digit) * 100:.2f}%") return # Show the results only considering the state of the qubits in msr_list msr: "dict[int, np.float16]" = {} for i, digit in enumerate(self.result): idx = extract_bits(i, msr_list) prb = np.square(digit) if idx in msr: msr[idx] += prb else: msr[idx] = prb for i, prb in msr.items(): if prb > 0: print(f"{i:0{len(msr_list)}b} - {prb:.2f}%")
[docs] def measure( self, auto_run: "bool" = True, msr_list: "list[int] | None" = None ) -> "None": """Collapses the qubits and show their value as a classical bit. If auto_run is True, the circuit is run before showing the result. If an msr_list is provided, the result is shown only for the qubits with that index. Args: auto_run: whether to run the circuit before showing the result msr_list: list of indices of the qubits to be measured """ if msr_list is None: msr_list = list(range(self.n)) if auto_run: self.run()