Source code for qmatchatea.circuit.circuit

# This code is part of qmatchatea.
#
# 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.

"""
Circuit class used for the simulations.

The circuit is defined as a list of layers, and each layer is a list of
**Instructions**, i.e. a list [QCOperation, sites]. This structure ensures
a simplified analysis of which operations could be executed in parallel.
Furthermore, we point out a couple of further important elements of the
class:

- qregisters: the quantum registers are a way of keeping track of
    "temporary" qubits, that can be added and removed at will. With
    the class methods :func:`add_qregister` and :func:`remove_qregister`
    it is possible to apply such operations. Then, when you apply an operation
    you can always specify to which register you are referring, without having
    to worry about the indexing. In particular, qubits in the same register
    are naturally ordered from left to right. So, if you want to apply a not
    gate to the left-most qubit of the register a1 you will have to call
    `qcirc.x(0, 'a1')`, since the left-most has index 0 for the register 'a1';
- cregisters: the classical registers, where you store the information of
    projective mesurements happened previously on the system and their probability.
    So, the position `idx` of the cregister will hold the tuple
    ``(meas_result, meas_prob``. Each operation
    can be conditioned on the value of a classical register. By calling the
    method :func:`inspect_cregister(creg, idx)` where `creg` is the name of your
    classical register, you can have access to the value of the bit at the
    index idx. If idx=None instead, the full register is treated as the binary
    representation of a decimal number, and that decimal number is given by
    the function. Remember to add the classical registers you need beforehand,
    using :func:`add_cregister`: the quantum circuit start without any of them!

To add operations to the circuit there are three main ways:

- the :func:`add`, to append the operation at the end of the circuit
- some methods to apply widely-used operations to the circuit with a simplified
    notation.
- the :func:`insert` method, to insert an operation at any given layer of the circuit.
    However, this method is dangerous and is not recommended.

The :func:`str_repr` is meant to give a way to visualize the circuit, but is not needed
for the functioning of the class, and it is still under development.
"""

# pylint: disable=too-many-lines

from copy import deepcopy

import numpy as np
import scipy.sparse as sp
from qiskit import QuantumCircuit

from qmatchatea.utils.tn_utils import QCOperators

from .observables import QCObservableStep

# Gates
# Operations
from .operations import (
    ClassicalCondition,
    QCAddSite,
    QCCnot,
    QCCp,
    QCCz,
    QCHadamard,
    QCMeasureProjective,
    QCOperation,
    QCPgate,
    QCRemoveSite,
    QCRenormalize,
    QCSadggate,
    QCSgate,
    QCSwap,
    QCTadggate,
    QCTgate,
    QCXpauli,
    QCYpauli,
    QCZpauli,
)

__all__ = ["Qcircuit"]


# pylint: disable-next=too-many-public-methods, too-many-instance-attributes
[docs] class Qcircuit: """ Class used to represent a quantum circuit in the qmatchatea library. Each layer of the circuit is a list containing instructions, i.e. lists [QCOperation, sites]. - When iterating over the class the iterators returns the layers. - Iterating over the layer returns the instructions, with all permutations of indexes already taken care of, based on the quantum registers. This means that, once you obtain the operation, you simply need to apply it on the defined sites of the mps, without further complexity. However, you should not run it by yourself, it is preferred to use the :func:`QcMps.run_from_qcirc()`. Parameters ---------- num_sites: int Number of sites in the circuit local_dim: int, optional Local dimension of the single site. Default to 2. """ def __init__(self, num_sites, local_dim=2) -> None: self._num_sites = num_sites self._local_dim = local_dim # Registers are a way of keeping track of ancilla sites, # i.e. sites that get added and removed in runtime. # The structure is a name connected to a np.ndarray. The # ndarray has as values the true position of the flattened # chain. Sites in a register are always ordered in # increasing order from left to right self._qregisters = {"default": np.arange(num_sites)} # Classical registers keep tracks of the result of projective # measurement and their probability along the simulation. # They are represented as a two-dimensional vector, with the # row being the index of the register and the column either # the measurement outcome (index=0) or their probability (index=1) # They are all initialized to 0, and when retrieved through # the cregister function returns the decimal value of their # binary representation self._cregisters = {} self._cl_occupation_map = {} # Circuit list. The outer list contains the full circuit, # and then each list is a layer. If no operation is applied # on a qubit a "wait" operation is applied. To take into # account multiple gates and make the wait management easier # we encode a logical ndarray where True means occupied and # False empty (wait) self._data = [] self._occupation_map = [] self._add_layer() # Number of one-site operations in the circuit self._num_one_site_operations = 0 # Number of two-site operations in the circuit self._num_two_site_operations = 0 # Used in the writing operation for the 1-1 mapping self._namelist = [] def __len__(self): """ Provide number of operations in the circuit """ return self._num_one_site_operations + self._num_two_site_operations def __getitem__(self, key): """Overwrite the call for lists, you can access the circuit layers using .. code-block:: circ[0] >>> [QCOperation, [0]], [QCOperation, [1, 2]] Parameters ---------- key : int index of the circuit layer you are interested in Returns ------- list list of instructions [Operation, sites] """ return self._data[key] def __setitem__(self, *args): """ Raise a Not-implemented error, since this is not the way operations should be modified. """ raise NotImplementedError( "Elements of the circuit cannot be substituted this way" ) def __iter__(self): """Iterator protocol""" return iter(self._data) @property def num_sites(self): """Number of sites property""" return self._num_sites @property def num_qubits(self): """Number of sites property, with another name for simplicity of interfaces""" return self._num_sites @property def local_dim(self): """Local dimension property""" return self._local_dim @property def num_layers(self): """Number of layers in the circuit""" return len(self._data) @property def num_classical_sites(self): """Number of classical sites, i.e. bits""" num_cl = 0 for _, item in self._cregisters.items(): num_cl += len(item) return num_cl @property def cregisters(self): """Return the full list of classical registers""" return self._cregisters def _add_layer(self): """ Add an empty layer to the circuit, so filled with wait operations """ if len(self._occupation_map) == 0: self._occupation_map = np.zeros((self._num_sites, 1), dtype=bool) else: self._occupation_map = np.hstack( (self._occupation_map, np.zeros((self._num_sites, 1), dtype=bool)) ) self._data.append([])
[docs] def inspect_cregister(self, register, idx=None): """ Retrieve the decimal value of the binary number stored in the classical register `register` if idx=None, otherwise it returns the value of the bit at position idx Parameters ---------- register : str Name of the classical register idx : int, optional Index of the bit you want to recover. If None, you will recover the decimal representation of the full register. Default to None. """ if idx is None: binary_str = np.array2string( self._cregisters[register][:, 0][::-1].astype(int), separator="" ) binary_str = binary_str.replace("[", "").replace("]", "") out = int(binary_str, 2) else: out = int(self._cregisters[register][idx, 0]) return out
[docs] def add_cregister(self, name, num_bits): """ Add a classical register with name `name` and length `num_bits` Parameters ---------- name : str Name of the classical register num_bits : int Number of bits the register can store """ self._cregisters[name] = np.zeros((num_bits, 2), dtype=float) self._cl_occupation_map[name] = np.zeros(num_bits, dtype=int)
[docs] def modify_cregister(self, value, name, idx): """ Modify the value of a measured bit in the classical register Parameters ---------- value : tuple of (int, float) Value of the measured qubit to be saved in the classical register and of the probability of that outcome name : str Name of the classical register idx : int Index of the bit where to save value """ self._cregisters[name][idx, 0] = value[0] self._cregisters[name][idx, 1] = np.real(value[1])
[docs] def add_site(self, new_register, position, reference_register="default"): """ Add a site to the circuit, in a specific position. The position is the index of the link where the site will be added, so 0 is attached to the left, num_sites is attached to the right. An intermediate number attaches it in the middle. Parameters ---------- new_register : str Name of the register that will track the index of the new site position : int Index of the link where the new site should be added in the register reference_register: str Register with respect to which the position is taken. Default to 'default'. """ if not 0 <= position <= len(self._qregisters[reference_register]): raise ValueError( f"Site cannot be inserted in position {position}" + f" of register {reference_register} with" + f" num_sites={len(self._qregisters[reference_register])}" ) # Recover true position on the MPS chain from the position in the register if position < len(self._qregisters[reference_register]): true_pos = self._qregisters[reference_register][position] else: true_pos = self._qregisters[reference_register][position - 1] + 1 # Update all the values after that one, adding one to their # relative position for idxs in self._qregisters.values(): mask = idxs >= true_pos idxs[mask] += 1 # Insert the new site in the register if new_register in self._qregisters: idx = np.nonzero(self._qregisters[new_register] > true_pos)[0] if len(idx) == 0: self._qregisters[new_register] = np.append( self._qregisters[new_register], true_pos ) else: idx = idx[0] self._qregisters[new_register] = np.insert( self._qregisters[new_register], idx[0], true_pos ) # Add new register with the new index if it is next else: self._qregisters[new_register] = np.array([true_pos]) # Insert a new row in the occupation map and update num_sites om_shape = self._occupation_map.shape[1] self._occupation_map = np.insert( self._occupation_map, true_pos, np.zeros(om_shape), axis=0 ) self._num_sites += 1 # Put the information in the data self.add(QCAddSite(position), np.arange(self.num_sites), None)
[docs] def remove_site(self, register, position): """ Remove a site from the circuit. The site to be removed is identified by its register and the position in the register. Parameters ---------- register : str Name of the register where the site to remove is position : int Index of the site to remove """ if not 0 <= position < len(self._qregisters[register]): raise ValueError( f"Site cannot be removed in position {position}" + f" of register {register} with" + f" num_sites={len(self._qregisters[register])}" ) # Put the information in the data self.add(QCRemoveSite(position), np.arange(self.num_sites), None) # Recover true position on the MPS chain from the position in the register true_pos = self._qregisters[register][position] # Update all the values after that one, subtracting one to their # relative position for idxs in self._qregisters.values(): mask = idxs > true_pos idxs[mask] -= 1 # Remove the site from the register self._qregisters[register] = np.delete(self._qregisters[register], position) # Remove row from the occupation map and update num_sites self._num_sites -= 1 self._occupation_map = np.delete(self._occupation_map, true_pos, axis=0)
[docs] def add_qregister(self, new_register, positions, reference_register="default"): """ Add a new register to the circuit, in a specific positions. The positions is the index of the link where the site will be added, so 0 is attached to the left, num_sites is attached to the right. An intermediate number attaches it in the middle. This link number is taken with respect to the reference register. This function is a simple loop over add_site, but we expect it to be a very useful function in runtime Parameters ---------- new_register : str Name of the register that will track the index of the new site positions : array-like of ints Index of the link where the new site should be added in the register reference_register: str Register with respect to which the position is taken. Default to 'default'. """ for position in positions: self.add_site(new_register, position, reference_register)
[docs] def remove_qregister(self, register): """ Remove a register from the circuit. Parameters ---------- register : str Name of the register to be removed """ num_reps = len(self._qregisters[register]) for _ in range(num_reps): self.remove_site(register, 0) # Remove the key from the dictionary del self._qregisters[register]
# pylint: disable-next=too-many-branches
[docs] def add(self, operation, sites, register="default"): """ Add an operation to the circuit Parameters ---------- operation : :class:`QCOperation` Operation to be added to the sites : integer of array-like of integers Qubits to which the operation is applied register : str or array of str Register to which the operations are applied. Default are both to default. """ if not issubclass(type(operation), QCOperation): raise TypeError( f"operation must be of type QCOperation, not {type(operation)}" ) if isinstance(sites, int): # Trasnsfor integer in array-like list sites = [sites] self._num_one_site_operations += 1 else: self._num_two_site_operations += 1 if isinstance(register, str): register = [register] * len(sites) if register is not None: for idx, site in enumerate(sites): sites[idx] = self._qregisters[register[idx]][site] sites = list(sites) last_occupied = -1 for site in sites: temp = np.where(self._occupation_map[site, :])[0] temp = [-1] if len(temp) == 0 else temp last_occupied = max(last_occupied, temp[-1]) # Check for classical dependencies if operation.is_conditioned: if operation.c_if.idx is None: for occ in self._cl_occupation_map[operation.c_if.creg]: last_occupied = max(last_occupied, occ) self._cl_occupation_map[operation.c_if.creg] = last_occupied + 1 else: last_occupied = max( last_occupied, self._cl_occupation_map[operation.c_if.creg][operation.c_if.idx], ) self._cl_occupation_map[operation.c_if.creg][operation.c_if.idx] = ( last_occupied + 1 ) # Check for measures if hasattr(operation, "cregister"): occ = self._cl_occupation_map[operation.cregister][operation.cl_idx] last_occupied = max(last_occupied, occ) self._cl_occupation_map[operation.cregister][operation.cl_idx] = ( last_occupied + 1 ) # Check if the last occupied is on the last layer, # and add a layer if True if last_occupied == self._occupation_map.shape[1] - 1: self._add_layer() # Add the operation on data and occupation map instruction = [operation, sites] self._data[last_occupied + 1].append(instruction) self._occupation_map[sites, last_occupied + 1] = True
# print(self._occupation_map) # pylint: disable-next=too-many-arguments
[docs] def insert(self, operation, sites, layer, register="default", inlayer=True): """ Insert an operation after a specific layer on the sites If inlayer is True simply add it into the layer without checking if an operation is already there, otherwise check for the operation and insert a new layer Parameters ---------- operation : :class:`QCOperation` Operation to be added to the sites : int of array-like of integers Qubits to which the operation is applied layer: int integer identifying the layer after which we want to insert the operation register : str or array of str Register to which the operations are applied. Default are both to default. inlayer: bool, optional If False, and the position in the layer is already occupied, add a new layer. Otherwise add it to that layer nevertheless. Default to True. """ if not issubclass(type(operation), QCOperation): raise TypeError( f"operation must be of type QCOperation, not {type(operation)}" ) if np.isscalar(sites): # Trasnsfor integer in array-like list sites = [sites] if isinstance(register, str): register = [register] * len(sites) for idx, site in enumerate(sites): sites[idx] = self._qregisters[register[idx]][site] instruction = [operation, sites] # Check if the positions sites in layer is occupied is_occupied = self._occupation_map[sites, layer] if not np.isscalar(is_occupied): is_occupied = any(is_occupied) if (not inlayer) and is_occupied: # Insert new layer in occupation map and data self._occupation_map = np.insert( self._occupation_map, layer + 1, False, axis=1 ) self._data.insert(layer + 1, []) self._occupation_map[sites, layer + 1] = True layer += 1 elif not inlayer and not is_occupied: self._occupation_map[sites, layer] = True # Insert instruction self._data[layer].append(instruction)
[docs] @classmethod def from_qiskit(cls, qc): """ Initialize the Qcircuit class from a qiskit quantum circuit Parameters ---------- qc : :class:`QuantumCircuit` qiskit QuantumCircuit that will initialize the Qcircuit """ if not isinstance(qc, QuantumCircuit): raise TypeError(f"qc must be of type QuantumCircuit, not {type(qc)}") # Reinitialize values qcirc = cls(qc.num_qubits) for instance in qc.data: gate = instance[0] if gate.name == "barrier": pass elif gate.name == "measure": qcirc.measure_projective( [qub.index for qub in instance[1]], [cl_idx.index for cl_idx in instance[2]], ) else: operation = QCOperation(gate.name, gate.to_matrix) qcirc.add(operation, sites=[qub.index for qub in instance[1]]) return qcirc
[docs] def fill_qcoperators(self, operators): """ Fill the QCoperators dictionary with all the operators in the Qcircuit Parameters ---------- operators: :class:`QCOperators` operators class to fill """ if not isinstance(operators, QCOperators): raise TypeError(f"qc must be of type QuantumCircuit, not {type(operators)}") for layer in self._data: for instruction in layer: operation = instruction[0] sites = instruction[1] name = operation.name matrix = operation.operator search = True while search: # If the operator has already been written, even if parametric, # don't add it again. Instead, if it has sufficiently different # parameters, add it with a new name if name in operators: ii = 0 if np.allclose(matrix, operators[name]): search = False else: name = operation.name + str(ii) ii += 1 else: search = False operators[name] = matrix.reshape([self.local_dim] * len(sites)) self._namelist.append(name)
[docs] def write(self, dest, id_mapping): """ Write on file (originally written for the Fortran simulator), with the correct formatting Parameters ---------- dest: str or filehandle If str, write on that file. Otherwise, write using that filehandle id_mapping: dict Mapping from the operator name to the operator id """ if isinstance(dest, str): # pylint: disable=consider-using-with fh = open(dest, "w+") elif hasattr(dest, "write"): fh = dest else: raise TypeError("`dest` for `write` neither str nor writeable.") fh.write(str(self.num_sites) + "\n") fh.write(str(self.num_classical_sites) + "\n") fh.write(str(len(self) + len(self._data)) + "\n") ii = 0 for layer in self._data: for instruction in layer: operation = instruction[0] sites = instruction[1] fh.write( operation.name, id_mapping(self._namelist[ii]), len(sites), 0, "\n", ) fh.write("\t") for site in sites: fh.write(str(site + 1) + " ") # Qubit numbers for clbit in range(0): fh.write(str(clbit + 1) + " ") # Classical bit numbers fh.write("\n") ii += 1 fh.write("barrier", "1", "1", "1", "\n", "\n")
# pylint: disable-next=too-many-branches
[docs] def to_matrix(self, sparse=False, max_qubit_equivalent=20, qiskit_order=True): """ Return the matrix representation of the circuit. It is a unitary matrix if no noise/non-unitary matrix has been applied. Notice that this computation scales as :math:`d^{2n}`, where :math:`d` is the local dimension of the single degree of freedom and :math:`n` the number of sites. Furthermore, the function is not optimized and might take long. Parameters ---------- sparse : bool, optional If True, return a csr sparse matrix instead of a numpy array. Default to False. max_qubit_equivalent: int, optional Maximum number of qubit sites the MPS can have and still be transformed into a matrix. If the number of sites is greater, it will throw an exception. Default to 20. qiskit_order : bool, optional If True, return the matrix using qiskit ordering. Default to True. Returns ------- matrix : np.ndarray or sp.csr_matrix Numpy array or sparse matrix representing the system unitary """ if self.local_dim**self.num_sites > 2**max_qubit_equivalent: raise RuntimeError( "Maximum number of sites for the matrix is " + f"fixed to the equivalent of {max_qubit_equivalent} qubit sites" ) # Define the identity call based on the sparse flag if sparse: iden = sp.identity kron = sp.kron swap = sp.csr_matrix(QCSwap().operator) else: iden = np.identity kron = np.kron swap = QCSwap().operator matrix = iden(self.local_dim**self.num_sites) for layer in self._data: for instruction in layer: op_mat = instruction[0].operator if sparse: op_mat = sp.csr_matrix(op_mat) sites = instruction[1] diff = max(sites) - min(sites) if len(sites) == 2: # Create matrix if non adjacent if diff > 1: circ = Qcircuit(diff + 1, self.local_dim) for ii in range(diff - 1): circ.add(QCSwap(), [ii, ii + 1]) if sites[0] < sites[1]: circ.add(instruction[0], [diff - 1, diff]) else: circ.add(instruction[0], [diff, diff - 1]) for ii in range(diff - 1, 0, -1): circ.add(QCSwap(), [ii, ii - 1]) op_mat = circ.to_matrix(sparse=sparse) # Revert the direction if target is above control elif sites[0] < sites[1]: op_mat = swap @ op_mat @ swap # Reverse all the indexing if the qiskit order is required if qiskit_order: sites = self.num_sites - 1 - np.array(sites) if op_mat.shape == (4, 4): op_mat = swap @ op_mat @ swap if min(sites) > 0: op_mat = kron(iden(2 ** (min(sites))), op_mat) if max(sites) < self.num_sites - 1: op_mat = kron(op_mat, iden(2 ** (self.num_sites - max(sites) - 1))) matrix = op_mat @ matrix return matrix
# pylint: disable-next=too-many-locals
[docs] def str_repr(self): """ Get the string representation of the circuit """ # Initialization str_repr = [] biggest_idx_len = len(f"q_{self.num_sites-1} ") for idx in range(self.num_sites): str_idx = f"q_{idx} " str_idx += " " * (biggest_idx_len - len(str_idx)) void = " " * biggest_idx_len str_repr.append([void, str_idx, void]) for layer in self._data: used_sites = [] max_len = 0 for instruction in layer: op_str = instruction[0].string_rep.split("\n") sites = instruction[1] used_sites += list(sites) for idx, site in enumerate(sites): current_str = op_str[idx * 3 : (idx + 1) * 3] if len(current_str) == 0: current_str = op_str[:3] for idx_str, str1 in enumerate(current_str): str_repr[site][idx_str] += str1 max_len = max(max_len, len(str1)) empty_idxs = np.setdiff1d(np.arange(self.num_sites), np.array(used_sites)) for site in empty_idxs: str_repr[site][0] += " " * max_len str_repr[site][1] += "─" * max_len str_repr[site][2] += " " * max_len circ_rep = "" for site in str_repr: circ_rep += "\n".join(site) return circ_rep
################################################################ ## Dummy methods for the most used operations to simplify the ## ## life of the programmer. You could still use the add method ## ## since this is simply a dummy rewriting. ## ################################################################
[docs] def x(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a x gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCXpauli(c_if), pos, qreg)
[docs] def y(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a y gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCYpauli(c_if), pos, qreg)
[docs] def z(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a z gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCZpauli(c_if), pos, qreg)
[docs] def h(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a hadamard gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCHadamard(c_if), pos, qreg)
[docs] def s(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a S gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCSgate(c_if), pos, qreg)
[docs] def sadg(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply the adjoint of an S gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCSadggate(c_if), pos, qreg)
[docs] def t(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a T gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCTgate(c_if), pos, qreg)
[docs] def tadg(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply the adjoint of an T gate in the position pos in the qregister qreg Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCTadggate(c_if), pos, qreg)
[docs] def p(self, theta, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a Phase gate in the position pos in the qregister qreg with a phase theta Parameters ---------- theta : float Rotation angle for the phase gate pos : int Index, relative to the quantum register, where you want to apply the operation qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCPgate(theta, c_if), pos, qreg)
[docs] def cx(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a controlled not gate in the position pos in the qregisters qreg. pos should be an array, qreg should be an array if the qubits are in different qregisters Parameters ---------- pos : list of two ints Index, relative to the quantum register, where you want to apply the operation qreg : list of str, str or None, optional Quantum registers where you want to apply the operation. If only a str is provided, the program assumes you want to apply the gate inside the same quantum register. If a list of two strings is provided, the gate is applied between different quantum registers. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default'. c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCCnot(c_if), pos, qreg)
[docs] def cz(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a controlled z gate in the position pos in the qregisters qreg. pos should be an array, qreg should be an array if the qubits are in different qregisters Parameters ---------- pos : list of two ints Index, relative to the quantum register, where you want to apply the operation qreg : list of str, str or None, optional Quantum registers where you want to apply the operation. If only a str is provided, the program assumes you want to apply the gate inside the same quantum register. If a list of two strings is provided, the gate is applied between different quantum registers. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default'. c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCCz(c_if), pos, qreg)
[docs] def cp(self, theta, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a controlled phase gate in the position pos in the qregisters qreg. pos should be an array, qreg should be an array if the qubits are in different qregisters Parameters ---------- theta : float Rotation angle for controlled phase gate pos : list of two ints Index, relative to the quantum register, where you want to apply the operation qreg : list of str, str or None, optional Quantum registers where you want to apply the operation. If only a str is provided, the program assumes you want to apply the gate inside the same quantum register. If a list of two strings is provided, the gate is applied between different quantum registers. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default'. c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCCp(theta, c_if), pos, qreg)
[docs] def swap(self, pos, qreg="default", c_if=ClassicalCondition()): """ Apply a swap gate in the position pos in the qregisters qreg. pos should be an array, qreg should be an array if the qubits are in different qregisters Parameters ---------- pos : list of two ints Index, relative to the quantum register, where you want to apply the operation qreg : list of str, str or None, optional Quantum registers where you want to apply the operation. If only a str is provided, the program assumes you want to apply the gate inside the same quantum register. If a list of two strings is provided, the gate is applied between different quantum registers. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default'. c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCSwap(c_if), pos, qreg)
# pylint: disable-next=too-many-arguments
[docs] def gate(self, gate, pos, name="op", qreg="default", c_if=ClassicalCondition()): """ Apply a matrix to the circuit. The dimensions of the matrix should be consistent with the number of qubits to which it should be applied Parameters ---------- gate : np.ndarray gate matrix pos : int or array-like of ints Indexes of the sites to which it should be applied name : str, optional Unique identifier of the gate. Default to 'op' qreg : str or array-like of str, optional qregister to which the gate should be applied """ if isinstance(pos, int): pos = [pos] if gate.shape[1] != self.local_dim ** len(pos): raise ValueError( f"Gate shape {gate.shape[1]} is incompatible " + "with the local dimension and the number of sites to which " + f"it is applied. Should be {self.local_dim**len(pos)}" ) op = QCOperation(name, lambda: gate, conditioned=c_if) self.add(op, pos, qreg)
# pylint: disable-next=too-many-arguments
[docs] def measure_projective( self, pos, cl_idx, qreg="default", creg="default", selected_output=None, c_if=ClassicalCondition(), ): """ Apply a projective measurement in the position pos in the qregister qreg. You can choose the output of the measure. The result is stored in the classical qregister creg at index cl_idx Parameters ---------- pos : int Index, relative to the quantum register, where you want to apply the operation cl_idx : int Index of the classical register where to store the result of the measurement qreg : str or None, optional Quantum register where you want to apply the operation. If None, the "raw index" is considered, i.e. without taking into account the quantum register reindexing. Default to 'default' cregister : str, optional Classical register where the result of the measurement is stored. Default to 'default' selected_output : int, optional Output selected a priori. Default to None. c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ self.add(QCMeasureProjective(cl_idx, selected_output, creg, c_if), pos, qreg)
[docs] def renormalize(self, c_if=ClassicalCondition()): """ Renormalize the state Parameters ---------- c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation """ self.insert(QCRenormalize(c_if), 0, -1)
# pylint: disable-next=too-many-arguments
[docs] def measure_observables( self, name, observables, operators, qreg=None, c_if=ClassicalCondition() ): """ Perform a measurement step of all the observables defined in the observable parameter. Parameters ---------- name : str Name of the observables step observables: :class:`TNObservables` Observables to be measured at this step operators: :class:`QCOperators` Class containing the operators present in the Observables qreg : str or None, optional Quantum register where the observables should be measured. If None, they are measured on the FULL circuit, regardless of the registers c_if : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation """ obs = QCObservableStep(name, observables, operators, c_if) obs.num_sites = self.num_sites # We want to apply the measurement to the whole circuit regardless # of the register if qreg is None: obs.snap_current_qregisters(self._qregisters) self.add(obs, np.arange(self.num_sites - 1), register=qreg) # We want to apply the measurements to a single part of the circuit else: qreg_val = deepcopy(self._qregisters[qreg]) obs.snap_current_qregisters({qreg: qreg_val}) # Adjust the index of the observables based on specific sites obs.adjust_obs_indexing() # Add occupying the whole interested quantum register self.add(obs, np.arange(self.num_sites - 1), register=None)