Source code for geqo.simulators.sympy.implementation

import itertools
from abc import abstractmethod

import sympy as sym
from sympy import Mul, MutableDenseMatrix, S, Symbol, pi, re
from sympy.physics.quantum import TensorProduct

from geqo.__logger__ import get_logger
from geqo.algorithms import PermuteQubits
from geqo.algorithms.algorithms import PCCM, QFT, InversePCCM, InverseQFT
from geqo.core import (
    BasicGate,
    InverseBasicGate,
    Sequence,
)
from geqo.core.quantum_operation import QuantumOperation
from geqo.gates import (
    CNOT,
    Hadamard,
    InversePhase,
    InverseRx,
    InverseRy,
    InverseRz,
    InverseRzz,
    InverseSGate,
    PauliX,
    PauliY,
    PauliZ,
    Phase,
    Rx,
    Ry,
    Rz,
    Rzz,
    SGate,
    SwapQubits,
    Toffoli,
)
from geqo.initialization import SetBits, SetDensityMatrix, SetQubits
from geqo.operations import (
    ClassicalControl,
    DropQubits,
    Measure,
    QuantumControl,
)
from geqo.simulators.base import BaseQASM
from geqo.utils import (
    bin2num,
    partial_diag,
    permutationMatrixQubitsSymPy,
)
from geqo.utils._sympy_.helpers import (
    multiQubitsUnitary,
    partialTrace,
    projection,
)

logger = get_logger(__name__)


def getRX(angle: float) -> MutableDenseMatrix:
    """
    Return a SymPy matrix for Rx gate with given angle.

    Parameters
    ----------
    angle : sympy.core.basic.Basic
        The angle for the rotation.

    Returns
    -------
    matrix : sympy.Matrix
        A SymPy array of size 2x2, which corresponds to a rotation matrix.

    """
    return sym.Matrix(
        [
            [sym.cos(angle / 2), -sym.I * sym.sin(angle / 2)],
            [-sym.I * sym.sin(angle / 2), sym.cos(angle / 2)],
        ]
    )


def getRY(angle: float) -> MutableDenseMatrix:
    """
    Return a SymPy matrix for Ry gate with given angle.

    Parameters
    ----------
    angle : sympy.core.basic.Basic
        The angle for the rotation.

    Returns
    -------
    matrix : sympy.Matrix
        A SymPy array of size 2x2, which corresponds to a rotation matrix.

    """
    return sym.Matrix(
        [
            [sym.cos(angle / 2), -sym.sin(angle / 2)],
            [sym.sin(angle / 2), sym.cos(angle / 2)],
        ]
    )


def getUnitarySymPy(gate: QuantumOperation, values: dict) -> dict | MutableDenseMatrix:
    """
    Get the matrix representation for the different quantum operations. If there is a name for a parameter in a gate, then it is retrieved from the values parameter.

    Parameters
    ----------
    gate : geqo.core.quantum_operation.QuantumOperation
        A quantum operation that should be converted to matrix form.
    values : dict([String, sympy.core.basic.Basic])
        A dictionary that assigns a SymPy value to names.

    Returns
    -------
    res : sympy.Matrix
        The matrix corresponding to the provided quantum operation.
    """
    if isinstance(gate, BasicGate):
        if gate.name not in values:
            logger.error("Parameter %s in BasicGate is undefined.", gate.name)
            raise Exception(
                "Parameter " + str(gate.name) + " in BasicGate is undefined."
            )
        return values[gate.name]
    elif type(gate) is InverseBasicGate:
        if gate.name not in values:
            logger.error(
                "Parameter %s in InverseBasicGate is undefined.",
                gate.name,
            )
            raise Exception(
                "Parameter " + str(gate.name) + " in InverseBasicGate is undefined."
            )
        return sym.conjugate(values[gate.name]).T
    elif isinstance(gate, PauliX):
        return sym.Matrix([[0, 1], [1, 0]])
    elif isinstance(gate, PauliY):
        return sym.Matrix([[0, -sym.I], [sym.I, 0]])
    elif isinstance(gate, PauliZ):
        return sym.Matrix([[1, 0], [0, -1]])
    elif isinstance(gate, Phase):
        return sym.Matrix([[1, 0], [0, sym.exp(sym.I * values[gate.name])]])
    elif isinstance(gate, InversePhase):
        return sym.Matrix([[1, 0], [0, sym.exp(-sym.I * values[gate.name])]])
    elif isinstance(gate, Hadamard):
        w2 = 1 / sym.sqrt(2)
        return sym.Matrix([[w2, w2], [w2, -w2]])
    elif isinstance(gate, SGate):
        return sym.Matrix([[1, 0], [0, sym.I]])
    elif isinstance(gate, InverseSGate):
        return sym.Matrix([[1, 0], [0, -sym.I]])
    elif isinstance(gate, SwapQubits):
        return sym.Matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
    elif isinstance(gate, CNOT):
        return sym.Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
    elif isinstance(gate, PermuteQubits):
        qubits = list(range(len(gate.targetOrder)))
        perm = permutationMatrixQubitsSymPy([gate.targetOrder.index(q) for q in qubits])
        return perm
    elif isinstance(gate, QuantumControl):
        u = getUnitarySymPy(gate.qop, values)
        size = u.shape[0]
        res = sym.Matrix([])
        for t in list(itertools.product([0, 1], repeat=len(gate.onoff))):
            if t == tuple(gate.onoff):
                res = sym.diag(res, u)
            else:
                res = sym.diag(res, sym.eye(size))
        return res
    elif isinstance(gate, Rx):
        return getRX(values[gate.name])
    elif isinstance(gate, InverseRx):
        return getRX(-values[gate.name])
    elif isinstance(gate, Ry):
        return getRY(values[gate.name])
    elif isinstance(gate, InverseRy):
        return getRY(-values[gate.name])
    elif isinstance(gate, Rz):
        return sym.Matrix(
            [
                [sym.exp(-sym.I * values[gate.name] / 2), 0],
                [0, sym.exp(sym.I * values[gate.name] / 2)],
            ]
        )
    elif isinstance(gate, InverseRz):
        return sym.Matrix(
            [
                [sym.exp(sym.I * values[gate.name] / 2), 0],
                [0, sym.exp(-sym.I * values[gate.name] / 2)],
            ]
        )
    elif isinstance(gate, Rzz):
        return sym.Matrix(
            [
                [sym.exp(-sym.I * values[gate.name] / 2), 0, 0, 0],
                [0, sym.exp(sym.I * values[gate.name] / 2), 0, 0],
                [0, 0, sym.exp(sym.I * values[gate.name] / 2), 0],
                [0, 0, 0, sym.exp(-sym.I * values[gate.name] / 2)],
            ]
        )
    elif isinstance(gate, InverseRzz):
        return sym.Matrix(
            [
                [sym.exp(sym.I * values[gate.name] / 2), 0, 0, 0],
                [0, sym.exp(-sym.I * values[gate.name] / 2), 0, 0],
                [0, 0, sym.exp(-sym.I * values[gate.name] / 2), 0],
                [0, 0, 0, sym.exp(sym.I * values[gate.name] / 2)],
            ]
        )
    else:
        logger.error("gate %s not implemented yet", gate)
        raise Exception("gate " + str(gate) + " not implemented yet")


class BaseSympySimulator(BaseQASM):
    def __init__(self, numberQubits: int, values: dict):
        """
        The constructor of this simulator takes as parameters the number of classical bits, the number of qubits
        and a list of values, which are needed to run all operations.

        Parameters
        ----------
        numberBits : int
            The number of classical bits of the simulated system.
        numberQubits : int
            The number of qubits of the simulated system.
        values : {key:value}
            A dictionary of keys and values, which are needed for simulating the operations.

        Returns
        -------
        BaseSympySimulator : geqo.simulators.sympy.BaseSympySimulator
            A simulator object, which is based on SymPy.
        """
        self.numberQubits = numberQubits
        super().__init__(values)

    @abstractmethod
    def prepareBackend(self, operations: list[QuantumOperation]):
        """
        For a list of supported operators, this method sets the necessary values for the simulator.

        Parameters
        ----------
        operations : list(geqo.core.quantum_operation.QuantumOperation)
            A list of quantum operations, for which the backend should be prepared.
        """
        pass

    @abstractmethod
    def apply(self, gate: QuantumOperation, targets: list[int], *args, **kwargs):
        """
        Apply an operation to the quantum state, which is currently kept in the simulator.

        Parameters
        ----------
        gate : geqo.core.quantum_operation.QuantumOperation
            The operation that should be applied.
        targets : list(int)
            The list of qubit indexes, which are the target of the provided operation.
        """
        pass

    def setValue(self, name: str, value: list | Symbol | MutableDenseMatrix | float):
        """
        Set a name to a specific value. This has to be done before an operationen, which contains the name, can be applied.

        Parameters
        ----------
        name : String
            The name, which is set to a value.
        value : sympy.Matrix | sympy.core.basic.Basic
            The corresponding value for the name.
        """
        self.values[name] = value

    def _qasm_lines_sympy(self, lines: list[str]) -> list[str]:
        appeared = []
        for key, item in self.values.items():
            if isinstance(item, (Symbol, Mul)):
                if item.has(pi):
                    self.values[key] = float(item)
                elif item not in appeared:
                    lines.append(f"input float {item};")
                    appeared.append(item)
        return lines

    def sequence_to_qasm3(self, sequence: Sequence) -> str:
        """
        Turn a ```Sequence``` to a QASM sequence, which is a character string.

        Parameters
        ----------
        sequence : geqo.core.quantum_circuit.Sequence
            The name, which is set to a value.

        Returns
        -------
        qasm_lines : String
            A character string, which is a QASM representation of the provided ```Sequence```.
        """
        lines = self._qasm_lines_init(sequence)
        lines = self._qasm_lines_sympy(lines)
        qasm_lines = self._qasm_lines_body(sequence, lines)

        return qasm_lines


[docs] class ensembleSimulatorSymPy(BaseSympySimulator): def __init__(self, numberBits: int, numberQubits: int): """ The constructor of this simulator takes as parameters the number of classical bits, the number of qubits and a list of values, which are needed to run all operations. Parameters ---------- numberBits : int The number of classical bits of the simulated system. numberQubits : int The number of qubits of the simulated system. Returns ------- self : geqo.simulators.sympy.ensembleSimulatorSymPy A simulator object, which is based on SymPy. """ values = {} super().__init__(numberQubits, values) self.numberBits = numberBits rho = sym.zeros(2**self.numberQubits, 2**self.numberQubits) rho[0, 0] = 1 self.ensemble = {} bits = (0,) * self.numberBits self.ensemble[bits] = (1, rho) def __repr__(self) -> str: """ Returns ------- string_representation : String Representation of the object as character string. """ return f"{self.__class__.__name__}({self.numberBits}, {self.numberQubits})"
[docs] def prepareBackend(self, operations: list[QuantumOperation]): """ For a list of supported operators, this method sets the necessary values for the simulator. Parameters ---------- operations : list(geqo.core.quantum_operation.QuantumOperation) A list of quantum operations, for which the backend should be prepared. """ for ops in operations: if isinstance(ops, QFT): logger.info("prepare execution of QFT") n = ops.numberQubits for j in range(1, n): self.values[ops.nameSpacePrefix + "Ph" + str(j)] = ( 2 * S.Pi / (2 ** (j + 1)) ) elif isinstance(ops, InverseQFT): self.prepareBackend([ops.qft]) elif isinstance(ops, PCCM): logger.info("prepare execution of PCCM") self.values[ops.nameSpacePrefix + "RX(π/2)"] = ( S.Pi / 2 ) # getRX(S.Pi / 2) self.values[ops.nameSpacePrefix + "RX(-π/2)"] = ( -S.Pi / 2 ) # getRX(-S.Pi / 2) self.values[ops.nameSpacePrefix + "RY(-π/2)"] = ( -S.Pi / 2 ) # getRY(-S.Pi / 2) nameAngle = ops.nameSpacePrefix + ops.name if nameAngle not in self.values: phi = sym.Symbol(nameAngle) self.values[ops.nameSpacePrefix + "RX(" + str(phi) + ")"] = ( phi # getRX( ) # ) # phi else: logger.debug( "found nameAngle %s with value %s ", nameAngle, self.values[nameAngle], ) phi = sym.Symbol(nameAngle) self.values[ops.nameSpacePrefix + "RX(" + str(phi) + ")"] = ( self.values[nameAngle] ) # getRX( # self.values[nameAngle] # ) elif isinstance(ops, InversePCCM): self.prepareBackend([ops.pccm]) elif isinstance(ops, Toffoli): logger.info("prepare execution of Toffoli") self.values[ops.nameSpacePrefix + "S.Pi/4"] = S.Pi / 4 self.values[ops.nameSpacePrefix + "-S.Pi/4"] = -S.Pi / 4 else: logger.error("prepareBackend: operation not supported: %s", ops) raise Exception("prepareBackend: operation not supported:", str(ops))
[docs] def apply( self, gate: QuantumOperation, targets: list[int], classicalTargets: list[int] | None = None, ): """ Apply an operation to the state, which is currently kept in the simulator. Parameters ---------- gate : geqo.core.quantum_operation.QuantumOperation The operation that should be applied. targets : list(int) The list of qubit indexes, which are the target of the provided operation. classicalTargets : list(int) The list of classical bits, which are the target of the provided operation. """ if classicalTargets is None: classicalTargets = [] if isinstance(gate, ClassicalControl): if gate.qop.isUnitary(): controls = targets[: -gate.qop.getNumberQubits()] # control bits target_qubits = targets[-gate.qop.getNumberQubits() :] onoffMapping = [controls.index(t) for t in sorted(controls)] condition = [gate.onoff[i] for i in onoffMapping] newEnsemble = {} for s in self.ensemble: bits = [s[i] for i in range(self.numberBits) if i in controls] if bits == condition: # if the control condition is met dec = gate.qop.getEquivalentSequence() qubitMapping = {} qubits = [*range(len(dec.qubits))] for x in range(len(dec.qubits)): qubitMapping[qubits[x]] = target_qubits[x] unitaries = sym.eye(2**self.numberQubits) for d in dec.gatesAndTargets: # if len(d) == 2: # non measurement op = d[0] subtargets = [qubitMapping[x] for x in d[1]] if ( op.hasDecomposition() ): # i.e. QubitReversal within QFT still has decomposition dec2 = op.getEquivalentSequence() qubitMapping2 = {} qubits2 = [*range(len(dec2.qubits))] for x in range(len(dec2.qubits)): qubitMapping2[qubits2[x]] = subtargets[x] for d2 in dec2.gatesAndTargets: op2 = d2[0] subtargets2 = [qubitMapping2[x] for x in d2[1]] u = multiQubitsUnitary( getUnitarySymPy(op2, self.values), [*range(self.numberQubits)], subtargets2, ) unitaries = u * unitaries else: # no more decomposition after first decomposition u = multiQubitsUnitary( getUnitarySymPy(op, self.values), [*range(self.numberQubits)], subtargets, ) unitaries = u * unitaries # else: # measurement # raise Exception( # "Controlled measurements are not supported." # ) newEnsemble[s] = ( self.ensemble[s][0], unitaries * self.ensemble[s][1] * sym.conjugate(unitaries.T), ) else: # if the control condition is not met newEnsemble[s] = ( self.ensemble[s][0], self.ensemble[s][1], ) self.ensemble = newEnsemble else: # non-unitary target operation logger.error("Controlled operations cannot be non-unitary.") raise Exception("Controlled operations cannot be non-unitary.") # If there is a decomposition into a sequence of other gates, then use it. elif gate.hasDecomposition(): dec = gate.getEquivalentSequence() qubitMapping = {} if dec is not None: for x in range(len(dec.qubits)): qubitMapping[dec.qubits[x]] = targets[x] bitMapping = {} for x in range(len(dec.bits)): bitMapping[dec.bits[x]] = classicalTargets[x] for d in dec.gatesAndTargets: if len(d) == 2: # only qubits if isinstance(d[0], ClassicalControl): ckey_type = type(dec.bits[0]) qkey_type = type(dec.qubits[0]) controls = d[1][: len(d[0].onoff)] qtargets = d[1][len(d[0].onoff) :] control_targets = [ bitMapping[x] if type(x) is ckey_type else bitMapping[dec.bits[x]] for x in controls ] quantum_targets = [ qubitMapping[x] if type(x) is qkey_type else qubitMapping[dec.qubits[x]] for x in qtargets ] apply_targets = control_targets + quantum_targets else: key_type = type(dec.qubits[0]) apply_targets = [ qubitMapping[x] if type(x) is key_type else qubitMapping[dec.qubits[x]] for x in d[1] ] self.apply(d[0], apply_targets) else: # classical and quantum bits; quantum first qkey_type = type(dec.qubits[0]) apply_qtargets = [ qubitMapping[x] if type(x) is qkey_type else qubitMapping[dec.qubits[x]] for x in d[1] ] ckey_type = type(dec.bits[0]) apply_ctargets = [ bitMapping[x] if type(x) is ckey_type else bitMapping[dec.bits[x]] for x in d[2] ] self.apply( d[0], # [qubitMapping[x] for x in d[1]], # [bitMapping[x] for x in d[2]], apply_qtargets, apply_ctargets, ) # If the gate is unitary, then get the matrix and # apply it to the right order of qubits. elif gate.isUnitary(): targetOrder = [q for q in targets] for q in list(range(self.numberQubits)): if q not in targetOrder: targetOrder.append(q) perm = permutationMatrixQubitsSymPy( [targetOrder.index(q) for q in list(range(self.numberQubits))] ) u = getUnitarySymPy(gate, self.values) u2 = TensorProduct(u, sym.eye(2 ** (self.numberQubits - len(targets)))) u3 = perm.T * u2 * perm newEnsemble = {} for s in self.ensemble: densityMatrix = self.ensemble[s][1] densityMatrix2 = u3 * densityMatrix * sym.conjugate(u3.T) newEnsemble[s] = (self.ensemble[s][0], densityMatrix2) self.ensemble = newEnsemble elif isinstance(gate, SetDensityMatrix): if gate.name not in self.values: logger.error("name of parameter %s not known to simulator", gate.name) raise Exception( "name of parameter " + str(gate.name) + " not known to simulator" ) newDensityMatrix = self.values[gate.name] # trace out the target qubits newEnsemble = {} for ens in self.ensemble: prob = self.ensemble[ens][0] rho = self.ensemble[ens][1] rho_part, perm = partialTrace( rho, list(range(self.numberQubits)), targets ) rho2 = (perm.T) * TensorProduct(rho_part, newDensityMatrix) * perm newEnsemble[ens] = (prob, rho2) self.ensemble = newEnsemble elif isinstance(gate, Measure): targetOrder = [q for q in targets] for q in list(range(self.numberQubits)): if q not in targetOrder: targetOrder.append(q) perm = permutationMatrixQubitsSymPy( [targetOrder.index(q) for q in list(range(self.numberQubits))] ) newEnsemble = {} for s in self.ensemble: currentProb = self.ensemble[s][0] currentRho = perm * self.ensemble[s][1] * perm.T # Create all projectors, we assume that the measured # qubits are now at the front. numberMeasuredQubits = len(targets) for t in itertools.product([0, 1], repeat=numberMeasuredQubits): part1 = sym.zeros(2**numberMeasuredQubits, 2**numberMeasuredQubits) part1[bin2num(t), bin2num(t)] = 1 part2 = sym.eye(2 ** (self.numberQubits - numberMeasuredQubits)) proj = TensorProduct(part1, part2) resultProb = re(sym.trace(proj * currentRho)) resultRho = perm.T * proj * currentRho * proj * perm newBits = [x for x in s] # make a copy for ti in range(len(t)): newBits[classicalTargets[ti]] = t[ti] newBits = tuple(newBits) if resultProb > 0: # if True: resultRho = resultRho / resultProb if newBits in newEnsemble: previousProb = newEnsemble[newBits][0] previousRho = newEnsemble[newBits][1] newEnsemble[newBits] = ( resultProb * currentProb + previousProb, ( previousProb * previousRho + resultProb * currentProb * resultRho ) / (resultProb * currentProb + previousProb), ) else: newEnsemble[newBits] = ( resultProb * currentProb, resultRho, ) self.ensemble = newEnsemble elif type(gate) is SetBits: if gate.name not in self.values: logger.error("name of parameter %s not known to simulator", gate.name) raise Exception( "name of parameter " + str(gate.name) + " not known to simulator" ) bitValues = self.values[gate.name] newEnsemble = {} for s in self.ensemble: prob = self.ensemble[s][0] rho = self.ensemble[s][1] # set the bits as defined in SetBits. We must respect a bitMapping s2 = [x for x in s] for ti in range(len(targets)): s2[targets[ti]] = bitValues[ti] s3 = tuple(s2) if s3 not in newEnsemble: newEnsemble[s3] = (prob, rho) else: previousProb = newEnsemble[s3][0] previousRho = newEnsemble[s3][1] newEnsemble[s3] = ( prob + previousProb, (previousProb * previousRho + prob * rho) / (previousProb + prob), ) self.ensemble = newEnsemble elif type(gate) is SetQubits: if gate.name not in self.values: logger.error("name of parameter %s not known to simulator", gate.name) raise Exception( "name of parameter " + str(gate.name) + " not known to simulator" ) qubitValues = self.values[gate.name] newDensityMatrix = sym.zeros(2 ** len(targets), 2 ** len(targets)) index = bin2num(qubitValues) newDensityMatrix[index, index] = 1 # trace out the target qubits newEnsemble = {} for ens in self.ensemble: prob = self.ensemble[ens][0] rho = self.ensemble[ens][1] rho_part, perm = partialTrace( rho, list(range(self.numberQubits)), targets ) rho2 = (perm.T) * TensorProduct(rho_part, newDensityMatrix) * perm newEnsemble[ens] = (prob, rho2) self.ensemble = newEnsemble elif type(gate) is DropQubits: # trace out the dropped qubits newEnsemble = {} for ens in self.ensemble: prob = self.ensemble[ens][0] rho = self.ensemble[ens][1] rho_reduced, _ = partialTrace( rho, list(range(self.numberQubits)), targets ) newEnsemble[ens] = (prob, rho_reduced) self.ensemble = newEnsemble self.numberQubits -= len(targets) else: logger.error( "gate not implemented for %s: %s", self.__class__.__name__, gate ) raise Exception( f"gate not implemented for {self.__class__.__name__}: {gate}" )
[docs] class mixedStateSimulatorSymPy(ensembleSimulatorSymPy): def __init__( self, numberBits: int, numberQubits: int, return_density: bool = False ): """ The constructor of this simulator takes as parameters the number of classical bits, the number of qubits and a list of values, which are needed to run all operations. Parameters ---------- numberBits : int The number of classical bits of the simulated system. numberQubits : int The number of qubits of the simulated system. return_density : Bool Should the full history of density matrices after measurements be preserved? Returns ------- self : geqo.simulators.sympy.mixedStateSimulatorSymPy A simulator object, which is based on SymPy. """ super().__init__(numberBits, numberQubits) rho = sym.zeros(2**self.numberQubits, 2**self.numberQubits) rho[0, 0] = 1 self.densityMatrix = rho self.measureHistory = [] self.return_density = return_density
[docs] def apply( self, gate: QuantumOperation, targets: list[int], classicalTargets: list[int] | None = None, ): """ Apply an operation to the state, which is currently kept in the simulator. Parameters ---------- gate : geqo.core.quantum_operation.QuantumOperation The operation that should be applied. targets : list(int) The list of qubit indexes, which are the target of the provided operation. classicalTargets : list(int) The list of classical bits, which are the target of the provided operation. """ if classicalTargets is None: classicalTargets = [] # If there is a decomposition into a sequence of other gates, then use it. if gate.hasDecomposition() is True: dec = gate.getEquivalentSequence() qubitMapping = {} if dec is not None: for x in range(len(dec.qubits)): qubitMapping[dec.qubits[x]] = targets[x] bitMapping = {} for x in range(len(dec.bits)): bitMapping[dec.bits[x]] = classicalTargets[x] for d in dec.gatesAndTargets: if len(d) == 2: # only qubits key_type = type(dec.qubits[0]) apply_targets = [ qubitMapping[x] if type(x) is key_type else qubitMapping[dec.qubits[x]] for x in d[1] ] self.apply(d[0], apply_targets) else: # classical and quantum bits; quantum first qkey_type = type(dec.qubits[0]) apply_qtargets = [ qubitMapping[x] if type(x) is qkey_type else qubitMapping[dec.qubits[x]] for x in d[1] ] ckey_type = type(dec.bits[0]) apply_ctargets = [ bitMapping[x] if type(x) is ckey_type else bitMapping[dec.bits[x]] for x in d[2] ] self.apply( d[0], # [qubitMapping[x] for x in d[1]], # [bitMapping[x] for x in d[2]], apply_qtargets, apply_ctargets, ) # If the gate is unitary, then get the matrix and # apply it to the right order of qubits. elif gate.isUnitary(): u = getUnitarySymPy(gate, self.values) u_perm = multiQubitsUnitary(u, [*range(self.numberQubits)], targets) self.densityMatrix = u_perm * self.densityMatrix * sym.conjugate(u_perm.T) elif isinstance(gate, SetDensityMatrix): if gate.name not in self.values: logger.error( "name of parameter %s not known to simulator", gate.name, ) raise Exception( "name of parameter " + str(gate.name) + " not known to simulator" ) newDensityMatrix = self.values[gate.name] # trace out the target qubits rho_part, perm = partialTrace( self.densityMatrix, list(range(self.numberQubits)), targets ) self.densityMatrix = ( (perm.T) * TensorProduct(rho_part, newDensityMatrix) * perm ) elif isinstance(gate, Measure): non_target = list(set(list(range(self.numberQubits))) - set(targets)) outcome = partial_diag( self.densityMatrix, list(range(self.numberQubits)), non_target ) # Ensemble now stores only the probability of each measurement outcome and optionally the mixed density matrices # measurement = {(0,0,0):0.5,(1,1,1):0.5,"mixed_state":density_matrix} measurement = {} mixedRho = sym.zeros(2**self.numberQubits, 2**self.numberQubits) for out in outcome: resultProb = out[1] resultRho = projection( self.densityMatrix, self.numberQubits, targets, out[0] ) mixedRho += resultRho measurement[out[0]] = re(resultProb) if self.return_density: measurement["mixed_state"] = mixedRho self.densityMatrix = mixedRho self.measureHistory.append(measurement) elif isinstance(gate, SetBits): logger.error("SetBits not supported by this simulator") raise Exception("SetBits not supported by this simulator") elif isinstance(gate, SetQubits): if gate.name not in self.values: logger.error("name of parameter %s not known to simulator", gate.name) raise Exception( "name of parameter " + str(gate.name) + " not known to simulator" ) qubitValues = self.values[gate.name] newDensityMatrix = sym.zeros(2 ** len(targets), 2 ** len(targets)) index = bin2num(qubitValues) newDensityMatrix[index, index] = 1 # trace out the target qubits and re-combine with newDensityMatrix rho_part, perm = partialTrace( self.densityMatrix, list(range(self.numberQubits)), targets ) self.densityMatrix = ( (perm.T) * TensorProduct(rho_part, newDensityMatrix) * perm ) elif isinstance(gate, DropQubits): # trace out the dropped qubits rho_reduced, _ = partialTrace( self.densityMatrix, list(range(self.numberQubits)), targets ) self.densityMatrix = rho_reduced self.numberQubits -= len(targets) else: logger.error( "gate not implemented for %s: %s", self.__class__.__name__, gate ) raise Exception( f"gate not implemented for {self.__class__.__name__}: {gate}" )
[docs] class simulatorUnitarySymPy(BaseSympySimulator): """ Calculate the unitary matrix corresponding to a sequence of gates. No non-unitary operations like ClassicalControl, Measurement or DropQubits are allowed. """ def __init__(self, numberQubits: int): """ The constructor of this simulator takes as parameters the number of qubits. Parameters ---------- numberQubits : int The number of qubits of the simulated system. Returns ------- simulatorUnitarySymPy : geqo.simulators.sympy.simulatorUnitarySymPy A simulator object, which is based on SymPy. """ values = {} super().__init__(numberQubits, values) self.u = sym.eye(2**self.numberQubits) def __repr__(self) -> str: """ Returns ------- string_representation : String Representation of the object as character string. """ return f"{self.__class__.__name__}({self.numberQubits})"
[docs] def prepareBackend(self, operations: list[QuantumOperation]): """ For a list of supported operators, this method sets the necessary values for the simulator. Parameters ---------- operations : list(geqo.core.quantum_operation.QuantumOperation) A list of quantum operations, for which the backend should be prepared. """ for ops in operations: if isinstance(ops, QFT): logger.info("prepare execution of QFT") n = ops.numberQubits for j in range(1, n): self.values[ops.nameSpacePrefix + "Ph" + str(j)] = ( 2 * S.Pi / (2 ** (j + 1)) ) elif isinstance(ops, InverseQFT): self.prepareBackend([ops.qft]) elif isinstance(ops, PCCM): logger.info("prepare execution of PCCM") self.values[ops.nameSpacePrefix + "RX(π/2)"] = ( S.Pi / 2 ) # getRX(S.Pi / 2) self.values[ops.nameSpacePrefix + "RX(-π/2)"] = ( -S.Pi / 2 ) # getRX(-S.Pi / 2) self.values[ops.nameSpacePrefix + "RY(-π/2)"] = ( -S.Pi / 2 ) # getRY(-S.Pi / 2) nameAngle = ops.nameSpacePrefix + ops.name if nameAngle not in self.values: phi = sym.Symbol(nameAngle) self.values[ops.nameSpacePrefix + "RX(" + str(phi) + ")"] = ( phi # getRX( ) # ) # phi else: logger.debug( "found nameAngle %s with value %s ", nameAngle, self.values[nameAngle], ) phi = sym.Symbol(nameAngle) self.values[ops.nameSpacePrefix + "RX(" + str(phi) + ")"] = ( self.values[nameAngle] ) # getRX( # self.values[nameAngle] # ) elif isinstance(ops, InversePCCM): self.prepareBackend([ops.pccm]) elif isinstance(ops, Toffoli): logger.info("prepare execution of Toffoli") self.values[ops.nameSpacePrefix + "S.Pi/4"] = S.Pi / 4 self.values[ops.nameSpacePrefix + "-S.Pi/4"] = -S.Pi / 4 else: raise Exception("operation not supported:", str(ops))
[docs] def apply(self, gate: QuantumOperation, targets: list[int]): """ Apply an operation to the quantum state, which is currently kept in the simulator. Parameters ---------- gate : geqo.core.quantum_operation.QuantumOperation The operation that should be applied. targets : list(int) The list of qubit indexes, which are the target of the provided operation. """ # If there is a decomposition into a sequence of other gates, then use it. if gate.hasDecomposition() is True: dec = gate.getEquivalentSequence() qubitMapping = {} if dec is not None: for x in range(len(dec.qubits)): qubitMapping[dec.qubits[x]] = targets[x] for d in dec.gatesAndTargets: # self.apply(d[0], [qubitMapping[x] for x in d[1]]) key_type = type(dec.qubits[0]) apply_targets = [ qubitMapping[x] if type(x) is key_type else qubitMapping[dec.qubits[x]] for x in d[1] ] self.apply(d[0], apply_targets) elif gate.isUnitary(): u = getUnitarySymPy(gate, self.values) u_perm = multiQubitsUnitary(u, [*range(self.numberQubits)], targets) self.u = u_perm * self.u else: logger.error( "gate not implemented for %s: %s", self.__class__.__name__, gate ) raise Exception( f"gate not implemented for {self.__class__.__name__}: {gate}" )