Source code for tqsim.circuit

# This code is part of TQSim.
#
# (C) Copyright Constantine Quantum Technologies, 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

import os
import pickle
from typing import List, Sequence, Tuple

import numpy as np
from matplotlib.figure import Figure 

from .config import STORE_PATH  # For caching the bases and sigmas.
from .lib.basis_generator import generate_basis
from .lib.drawer import Drawer
from .lib.operator_generator import generate_braiding_operator


[docs]class AnyonicCircuit: """This class represents an anyon-based topological quantum circuit. Such a circuit is described by the number of qudits it contains, and the number of anyons each qudit contains. Parameters ---------- nb_qudits : int, optional Number of qudits in the circuit. The default is 1. nb_anyons_per_qudit : int, optional Number of anyons in each qudit. The default is 3. Examples --------- Here is an example of how to create a circuit with 2 qudits and 3 anyons per qudits. The circuit has a total of 6 anyons (2 * 3). >>> circuit = AnyonicCircuit(nb_qudits=2, nb_anyons_per_qudit=3) """ def __init__(self, nb_qudits: int = 1, nb_anyons_per_qudit: int = 3): """ Parameters ---------- nb_qudits : int, optional Number of qudits in the circuit. The default is 1. nb_anyons_per_qudit : int, optional Number of anyons in each qudit. The default is 3. Returns ------- None. """ self.__nb_qudits = nb_qudits self.__nb_anyons_per_qudit = nb_anyons_per_qudit self.__nb_anyons = nb_qudits * nb_anyons_per_qudit self.__nb_braids: int = 0 self.__braids_history: List[Tuple[int, int]] = [] self.__measured: bool = False self.__basis, self.__dim = self.__get_basis() input_state = np.zeros((self.__dim, 1), dtype=np.complex128) input_state[0, 0] = 1 self.__initial_state = input_state self.__sigmas = self.__get_sigmas() self.__unitary = np.eye(self.__dim) self.__drawer = Drawer(nb_qudits, nb_anyons_per_qudit) @property def nb_qudits(self) -> int: """Returns the number of qudits in the circuit. Returns ------- int Number of qudits. """ return self.__nb_qudits @property def nb_anyons_per_qudits(self) -> int: """Returns the number of anyons for each qudit in the circuit. Returns ------- int Number of anyons per qudit. """ return self.__nb_anyons_per_qudit @property def drawer(self) -> Drawer: """Returns the drawer object for the circuit. Returns ------- Drawer The circuit's drawer object. """ return self.__drawer @property def dim(self) -> int: """Returns the dimension of the fusion space. Returns ------- int Dimension of the fusion space. """ return self.__dim @property def basis(self): """Returns a list of all the basis states for the circuit. Returns ------- List List of all the basis states. """ return self.__basis @property def braiding_operators(self): """Returns a list of all the braiding operators. Returns ------- List List of all the braiding operators. """ return self.__sigmas def __get_basis(self) -> Tuple[np.ndarray, int]: folder_path = os.path.join( STORE_PATH, f"{self.__nb_qudits}_{self.__nb_anyons_per_qudit}" ) filename = os.path.join(folder_path, "basis.dat") try: with open(filename, "rb") as f: basis = pickle.load(f) except FileNotFoundError: basis = generate_basis(self.__nb_qudits, self.__nb_anyons_per_qudit) os.makedirs(os.path.dirname(filename), exist_ok=True) with open(filename, "wb") as f: pickle.dump(basis, f) return basis, len(basis) def __get_sigmas(self) -> List[np.ndarray]: folder_path = os.path.join( STORE_PATH, f"{self.__nb_qudits}_{self.__nb_anyons_per_qudit}" ) filename = os.path.join(folder_path, "sigmas.dat") try: with open(filename, "rb") as f: sigmas = pickle.load(f) except FileNotFoundError: sigmas = [] for index in range(1, self.__nb_anyons): sigma = generate_braiding_operator( index, self.__nb_qudits, self.__nb_anyons_per_qudit ) sigmas.append(np.array(sigma)) os.makedirs(os.path.dirname(filename), exist_ok=True) with open(filename, "wb") as f: pickle.dump(sigmas, f) return sigmas
[docs] def initialize(self, input_state: np.ndarray): """Initializes the circuit in the state input_state. Parameters ---------- input_state : np.ndarray A normalized quantum state with the same dimensions as the fusion space. Raises ------ Exception Will be raised if an initialization is attempted after performing braiding operations. ValueError Will be raised if the input state has the wrong dimension or is not normalized. Returns ------- AnyonicCircuit A reference to the same circuit. """ if self.__nb_braids > 0: raise Exception( "Initialization should happen before any braiding operation is performed!" ) input_state = np.array(input_state) if not np.size(input_state) == self.__dim: raise ValueError(f"The state has wrong dimension. Should be {self.__dim}") norm = np.sum(np.real(input_state * input_state.conjugate())) if not np.isclose(norm, 1): raise ValueError("The input state is not normalized correctly!") self.__initial_state = np.reshape(input_state, (self.__dim, 1)) return self
[docs] def braid(self, n: int, m: int): """Braids the two anyons at positions 'n' and 'm'. If n < m, they are braided in a clockwise direction, if n > m, they are braided in a counterclockwise direction. Parameters ---------- n : int The 1st anyon's position. m : int The 2nd anyon's position. Raises ------ Exception Exceptions are raised if a braiding is attempted after a measurement, or if trying to braid two non-adjacent anyons. ValueError Is raised if the parameters are not strictly positive integers, or if incorrect positions are passed. Returns ------- AnyonicCircuit A reference to the same circuit. """ if self.__measured: raise Exception("System already measured! Cannot perform further braiding!") if not isinstance(m, int) or not isinstance(n, int): raise ValueError("n, m must be integers") if m < 1 or n < 1: raise ValueError("n, m must be higher than 0!") if m > self.__nb_anyons or n > self.__nb_anyons: ## Check if correct raise ValueError( f"The system has only {self.__nb_anyons} anyons! n, m are erroneous!" ) if abs(n - m) != 1: raise Exception("You can only braid adjacent anyons!") if n < m: self.__unitary = self.__sigmas[n - 1] @ self.__unitary else: self.__unitary = self.__sigmas[m - 1].T.conjugate() @ self.__unitary self.__braids_history.append((n, m)) self.__nb_braids += 1 self.drawer.braid(m, n) return self
[docs] def braid_sequence(self, braid: Sequence[Sequence[int]]): """Takes a sequence of [index of the sigma operator, power], and applies the successive operators to the 'power'. The first operator in the sequence is the first to be applied. Parameters ---------- braid : Sequence[Sequence[int]] A sequence of pairs of integers representing a braiding operator and an exponent. Raises ------ ValueError Is raised if one of the operators' indices is not an integer greater or equal to 1, or is an incorrect index. Returns ------- AnyonicCircuit A reference to the same circuit. """ for ind, power in braid: if not isinstance(ind, int) or not isinstance(power, int): raise ValueError("Indices and powers must be integers!") if ind < 1: raise ValueError( f"sigma_{ind} is not a valid braiding operator! " f"The operators indices must be >= 1." ) if ind >= self.__nb_anyons: raise ValueError( f"sigma_{ind} is not a valid braiding operator! " f"The operators indices must be < {self.__nb_anyons}." ) # Computing m and n m = n = 0 if power > 0: n = ind m = ind + 1 elif power < 0: m = ind n = ind + 1 else: # if power=0, do nothing (identity) continue for _ in range(abs(power)): self.braid(n, m) return self
[docs] def measure(self): """Performs a measurement on the whole circuit. Raises ------ Exception Is raised if a measurement has already been carried. Returns ------- AnyonicCircuit A reference to the same circuit. """ if self.__measured: raise Exception("Cannot carry the measurements twice!") self.__measured = True self.drawer.measure() return self
[docs] def history(self, output: str = "raw"): """Returns the history of all braiding operations that were performed in the circuit. Its output can either be the raw braiding operations (n, m), a list of braiding operators (sigmas), or a LaTeX string containing the product of all the braiding operators. Parameters ---------- output : str, optional Can either be "raw", "sigmas", or "latex". The default is "raw". Raises ------ ValueError Is raised if an incorrect output format is chosen. Returns ------- List or String Either a list of (n, m) operations, a list of braiding operators, or a LaTeX string. """ if not output in ["raw", "sigmas", "latex"]: raise ValueError('Output should be either: "raw", "sigmas" or "latex"') if output == "raw": return self.__braids_history elif output == "sigmas": ret = [] for (n, m) in self.__braids_history: if m < n: ret.append(f"is{m}") else: ret.append(f"s{n}") return ret else: sigmas = self.history(output="sigmas") # Converting to a sigma notation with powers power_sigmas = [] last_sigma = sigmas[-1] if len(sigmas) else None power = 0 for sigma in reversed(sigmas): if sigma == last_sigma: power += 1 continue # Done counting the powers of the last sigma, adding it power_sigmas.append((last_sigma, power)) # resetting power = 1 last_sigma = sigma # Handling the last sigma if power != 0: power_sigmas.append((last_sigma, power)) # Converting to LaTeX latex = "" for sigma, p in power_sigmas: # Inverses of sigmas (negative powers) if sigma[0] == "i": latex += r"\sigma_{" f"{sigma[2:]}" "}^{" f"{-p}" "}" # Sigmas (positive powers) else: latex += r"\sigma_{" f"{sigma[1:]}" "}" if p > 1: # Only add exponents != 1 latex += "^{" f"{p}" "}" latex = "$ " + latex + "$" return latex
[docs] def draw(self) -> Figure: """Draws the topological quantum circuit. Returns ------- Figure The braid describing the topological quantum circuit. """ return self.drawer.draw()
[docs] def statevector(self) -> np.ndarray: """Computes and returns the current state vector of the circuit. Returns ------- ndarray The state vector of the circuit. """ return self.__unitary @ self.__initial_state
[docs] def unitary(self) -> np.ndarray: """Returns the unitary representation of the quantum circuit. Returns ------- ndarray The unitary matrix for the circuit. """ return self.__unitary
[docs] def run(self, shots: int = 1000): """Simulates the quantum circuit for 'shots' number of times and returns the measurement results. Parameters ---------- shots : int, optional Number of times the circuit is simulated. The default is 1000. Raises ------ Exception Is raised if the circuit is run without a measurement. Returns ------- dict Contains the number of measurements for each measured state. """ # Needs to be measured? if not self.__measured: raise Exception("The system was not measured!") statevector = self.statevector() probs = np.ravel(np.real(statevector * statevector.conjugate())) memory = np.random.choice(np.arange(self.__dim), p=probs, size=shots) idx, counts = np.unique(memory, return_counts=True) counts_dict = {} for i in range(len(idx)): counts_dict[str(idx[i])] = counts[i] return {"counts": counts_dict, "memory": memory}