Source code for vulqano.circuit_tester.operations

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


"""
Operations used in the simultion of the circuit to check the effective fidelity
after the run from the compiler.
The operations have a infidelity > 0, with different values based on the
requirements of the user.

There are two possible classes for handling and generate the operations:

- ``ContinuosOperations``, for parametrized operations;
- ``DiscreteOperations``, for discrete operations.

The available operations are:

- "idle", "RZ", "RX", "CP", "CP_r", "SWAP", SWAP_r", "CPCP_XTALK";
- "I", "T", "Tdg", "S", "Sdg", "Z", "H", "CZ", "CZ_r", "SWAP", SWAP_r", "CZCZ_XTALK"
"""
# pylint: disable=too-few-public-methods


import numpy as np


try:
    import qmatchatea.circuit.operations as qmop
except ImportError:
    qmop = None


__all__ = ["DiscreteOperations", "ContinuosOperations"]


[docs] class DiscreteOperations: """ Class to handle the discrete oprations of a given simulation **Arguments** id_infidelity: float What should be the infidelity of an identity, i.e. of an idle qubit. It is used as scale for the infidelity of all the other operations, which are defined as `id_infidelity*weights_dict["operation"]`. weights_dict: Dictionary[(str, float)] Dictionary with the infidelity weight of each operation. infidelity_method : str | callable, optional Way to implement the infidelity of the gate. If str, you have to choose from the available methods: - "gaussian", add gaussian noise If callable, it should be a function that takes in input the gate name and returns the gate matrix with the error. cross_talk : str | callable Crosstalk model. - If "gaussian", use gaussian noise on the four-qubits gate If callable, it should be a function that takes in input the gate name (with the xtalk configuration) and returns the gate matrix with the error. Default to None. """ def __init__( self, id_infidelity, weights_dict, infidelity_method="gaussian", cross_talk="gaussian", ): if qmop is None: raise ImportError("Please install qmatchatea.") self.id_infidelity = id_infidelity self.weights_dict = weights_dict if isinstance(infidelity_method, str): self._infidelity_method = getattr( self, f"_{infidelity_method.lower()}_infidelity" ) elif callable(infidelity_method): self._infidelity_method = infidelity_method else: raise ValueError( f"Only str or callable can be crosstalk, not {type(infidelity_method)}" ) if isinstance(cross_talk, str): self._cross_talk = getattr(self, f"_{cross_talk.lower()}_crosstalk") elif callable(cross_talk): self._cross_talk = cross_talk else: raise ValueError( f"Only str or callable can be crosstalk, not {type(cross_talk)}" ) # Dictionary of the available operations self._ops = { "idle": QCId, "I": QCId, "T": qmop.QCTgate, "TDG": qmop.QCTadggate, "S": qmop.QCSgate, "SDG": qmop.QCSadggate, "Z": qmop.QCZpauli, "H": qmop.QCHadamard, "CZ": qmop.QCCz, "CZ_r": qmop.QCCz, "SWAP": qmop.QCSwap, "SWAP_r": qmop.QCSwap, } @property def operations(self): """Property giving a list of the available operations""" return list(self._ops.keys()) def __getitem__(self, key): """ Get the operation with name `key` **Arguments** key : Tuple[ str, Tuple[float] ] Name of the operation you are interested in and its parameters **Returns** `QCOperation` Operation with the correct infidelity """ if isinstance(key, str): name = key params = [] else: name, params = key name = name.upper() # This is the default case if name in self._ops: # Apply the noise to the operation operation_mat = self._infidelity_method(name, params) # The only case that is not here is for crosstalk elif name == "XTALK": # First, we create the 4-qubit gate without crosstalk operation_mat = self._cross_talk(params) key = str(key) else: raise KeyError( f"{name} is not available, use only XTALK, {self._ops.keys()}" ) # Create a new operation with the given matrix operation = qmop.QCOperation(name=name, operator=lambda: operation_mat) return operation def _gaussian_infidelity(self, operation_name, params): """ Apply a gaussian noise to the operation **Arguments** operation_name : str The name of the operation to be applied params : Tuple[float] Tuple of the operation parameres **Returns** np.ndarray The matrix of the operation taking into account the infidelity """ infidelity = self.id_infidelity * self.weights_dict[operation_name] op_mat = self._ops[operation_name](*params).operator op_mat = op_mat + np.random.normal(0, infidelity, size=op_mat.shape) return op_mat def _gaussian_crosstalk(self, params): """ Apply a gaussian noise to the operation **Arguments** params : float The weight of this specific type of crosstalk If None, no parameters are used. Default to None. **Returns** np.ndarray The matrix of the operation taking into account the infidelity """ infidelity = self.id_infidelity * params[0] op_mat = np.identity(16, dtype=np.float64) op_mat += np.random.normal(0, infidelity, size=op_mat.shape) return op_mat def _cccp_crosstalk(self, params): """ Apply the crosstalk as a triple-controlled phase with a normally distributed phase **Arguments** params : float The weight of this specific type of crosstalk If None, no parameters are used. Default to None. **Returns** np.ndarray The matrix of the operation taking into account the infidelity """ infidelity = self.id_infidelity * params[0] * 0.0010420522033015207 op_mat = np.identity(16, dtype=np.complex128) op_mat[-1, -1] = np.exp(1j * np.random.normal(0, infidelity)) return op_mat
[docs] class ContinuosOperations(DiscreteOperations): """ Class to handle the discrete oprations of a given simulation **Arguments** id_infidelity: float What should be the infidelity of an identity, i.e. of an idle qubit. It is used as scale for the infidelity of all the other operations, which are defined as `id_infidelity*weights_dict["operation"]`. weights_dict: Dictionary[(str, float)] Dictionary with the infidelity weight of each operation. infidelity_method : str | callable, optional Way to implement the infidelity of the gate. If str, you have to choose from the available methods: - "gaussian", add gaussian noise to the matrix - "gaussian_on_params", add gaussian noise to the gate parameters If callable, it should be a function that takes in input the gate name and gate parameters and returns the gate matrix with the error. cross_talk : str | callable Crosstalk model. - If "gaussian", use gaussian noise on the four-qubits gate If callable, it should be a function that takes in input the gate name (with the xtalk configuration) and returns the gate matrix with the error. Default to None. """ def __init__( self, id_infidelity, weights_dict, infidelity_method="gaussian", cross_talk="gaussian", ): super().__init__( id_infidelity, weights_dict, infidelity_method=infidelity_method, cross_talk=cross_talk, ) # Dictionary of the available operations self._ops.update( { "I": QCId, "RZ": qmop.QCPgate, "RX": QCRX, "RY": QCRY, "CP": qmop.QCCp, "CP_r": qmop.QCCp, "SWAP": qmop.QCSwap, "SWAP_r": qmop.QCSwap, } ) def _gaussian_on_params_infidelity(self, operation_name, operation_params): """ Apply a gaussian noise to the operation **Arguments** operation_name : str The name of the operation to be applied operation_params : Tuple[float] Tuple of the operation parameres Returns ------- np.ndarray The matrix of the operation taking into account the infidelity """ old_operation_name = None # Map for constant gates to parametric form if operation_name == "H": # Decomposition in X + Ry(pi/2) operation_params = (np.pi / 2,) operation_name = "RY" old_operation_name = "H" elif operation_name == "T": # Decomposition in Rz(pi/4) operation_params = (np.pi / 4,) operation_name = "RZ" elif operation_name == "TDG": # Decomposition in Rz(pi/4) operation_params = (-np.pi / 4,) operation_name = "RZ" elif operation_name == "CZ": # Decomposition in Cp operation_params = (np.pi,) operation_name = "CP" elif operation_name == "SWAP": return self._gaussian_on_params_infidelity_swap() if operation_params is None: return self._gaussian_infidelity(operation_name, operation_params) infidelity = self.id_infidelity * self.weights_dict[operation_name] noise = np.random.normal(0, infidelity, size=len(operation_params)) params = (pp + noise[ii] for ii, pp in enumerate(operation_params)) op_mat = self._ops[operation_name](*params).operator if old_operation_name == "H": op_mat = np.array([[0, 1], [1, 0]], dtype=complex) @ op_mat return op_mat def _gaussian_on_params_infidelity_swap(self): """ Generate a SWAP gate with a given infidelity supposing that the swap can be decomposed in the following form, where the CZ has the gaussian error on the parameters ---0-H-0-H-0--- | | | -H-0-0-0-H-0-H- """ infidelity = self.id_infidelity * self.weights_dict["SWAP"] had = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2) hhhh = np.kron(had, had) idhh = np.kron(np.identity(2), had) czs = [ np.array( [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, np.exp(1j * (np.pi + np.random.normal(0, infidelity)))], ] ) for _ in range(3) ] swap = idhh @ czs[0] @ hhhh @ czs[1] @ hhhh @ czs[2] @ idhh return swap
# Definition of operations not avaliable in qmatchatea class QCId(qmop.QCOperation): """ Identity gate **Arguments** conditioned : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ def __init__(self, conditioned=qmop.ClassicalCondition()): super().__init__("id", None, conditioned=conditioned) @property def operator(self): """ Overwriting operator with identity """ return np.identity(2) class QCRX(qmop.QCOperation): """ Rotatio on X axis gate **Arguments** theta: float Rotation angle along X conditioned : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ def __init__(self, theta, conditioned=qmop.ClassicalCondition()): super().__init__("rx", self.matrix, theta, conditioned=conditioned) @staticmethod def matrix(theta): """ Overwriting operator with RX gate """ theta = theta / 2 return np.array( [[np.cos(theta), -1j * np.sin(theta)], [-1j * np.sin(theta), np.cos(theta)]] ) class QCRY(qmop.QCOperation): """ Rotatio on Y axis gate **Arguments** theta: float Rotation angle along Y conditioned : :py:class:`ClassicalCondition`, optional Classical condition to be checked for applying the operation on the simulation. """ def __init__(self, theta, conditioned=qmop.ClassicalCondition()): super().__init__("ry", self.matrix, theta, conditioned=conditioned) @staticmethod def matrix(theta): """ Overwriting operator with RX gate """ theta = theta / 2 return np.array( [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]], dtype=complex, )