Source code for geqo.simulators.numpy.implementation

from abc import abstractmethod
from itertools import product

import numpy as np

from geqo.__logger__ import get_logger
from geqo.algorithms import PCCM, QFT, InversePCCM, InverseQFT, PermuteQubits
from geqo.core import (
    BasicGate,
    InverseBasicGate,
    Sequence,
)
from geqo.core.quantum_operation import QuantumOperation
from geqo.gates import (
    CNOT,
    Hadamard,
    InversePhase,
    InverseRx,
    InverseRy,
    InverseRz,
    InverseSGate,
    PauliX,
    PauliY,
    PauliZ,
    Phase,
    Rx,
    Ry,
    Rz,
    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._numpy_.helpers import bin2num, permutationMatrixQubitsNumPy

logger = get_logger(__name__)


def getRX(angle: float) -> np.ndarray:
    """
    Return NumPy matrix for Rx gate with given angle.

    Parameters
    ----------
    angle : numpy.float64
        The angle for the rotation.

    Returns
    -------
    matrix : numpy.ndarray
        A NumPy array of size 2x2, which corresponds to a rotation matrix.

    """
    return np.array(
        [
            [np.cos(angle / 2), -1j * np.sin(angle / 2)],
            [-1j * np.sin(angle / 2), np.cos(angle / 2)],
        ]
    )


def getRY(angle: float) -> np.ndarray:
    """
    Return NumPy matrix for Ry gate with given angle.

    Parameters
    ----------
    angle : numpy.float64
        The angle for the rotation.

    Returns
    -------
    matrix : numpy.ndarray
        A NumPy array of size 2x2, which corresponds to a rotation matrix.
    """
    return np.array(
        [
            [np.cos(angle / 2), -np.sin(angle / 2)],
            [np.sin(angle / 2), np.cos(angle / 2)],
        ]
    )


def getUnitaryNumpy(gate: QuantumOperation, values: dict) -> np.ndarray:
    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 values[gate.name].conj().T
    elif isinstance(gate, PauliX):
        return np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128)
    elif isinstance(gate, PauliY):
        return np.array([[0.0, -1j], [1j, 0.0]], dtype=np.complex128)
    elif isinstance(gate, PauliZ):
        return np.array([[1.0, 0.0], [0.0, -1.0]], dtype=np.complex128)
    elif isinstance(gate, Phase):
        return np.array(
            [[1.0, 0.0], [0.0, np.exp(1j * values[gate.name])]], dtype=np.complex128
        )
    elif isinstance(gate, InversePhase):
        return np.array(
            [[1.0, 0.0], [0.0, np.exp(-1j * values[gate.name])]], dtype=np.complex128
        )
    elif isinstance(gate, Hadamard):
        w2 = 1 / np.sqrt(2)
        return np.array([[w2, w2], [w2, -w2]], dtype=np.complex128)
    elif isinstance(gate, SGate):
        return np.array([[1.0, 0.0], [0.0, 1j]], dtype=np.complex128)
    elif isinstance(gate, InverseSGate):
        return np.array([[1.0, 0.0], [0.0, -1j]], dtype=np.complex128)
    elif isinstance(gate, SwapQubits):
        return np.array(
            [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
            dtype=np.complex128,
        )
    elif isinstance(gate, CNOT):
        return np.array(
            [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
                [0.0, 0.0, 1.0, 0.0],
            ],
            dtype=np.complex128,
        )
    elif isinstance(gate, PermuteQubits):
        qubits = list(range(len(gate.targetOrder)))
        perm = permutationMatrixQubitsNumPy([gate.targetOrder.index(q) for q in qubits])
        return perm
    elif isinstance(gate, QuantumControl):
        u = getUnitaryNumpy(gate.qop, values)
        size = u.shape[0]
        dim = 2 ** len(gate.onoff) * size
        target_state_num = bin2num(gate.onoff)
        res = np.eye(dim, dtype=u.dtype)
        start = target_state_num * size
        end = start + size
        res[start:end, start:end] = u
        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 np.array(
            [
                [np.exp(-1j * values[gate.name] / 2), 0.0],
                [0.0, np.exp(1j * values[gate.name] / 2)],
            ],
            dtype=np.complex128,
        )
    elif isinstance(gate, InverseRz):
        return np.array(
            [
                [np.exp(1j * values[gate.name] / 2), 0.0],
                [0.0, np.exp(-1j * values[gate.name] / 2)],
            ],
            dtype=np.complex128,
        )
    else:
        logger.error("gate %s not implemented yet", gate)
        raise Exception("gate " + str(gate) + " not implemented yet")


class BaseSimulatorNumpy(BaseQASM):
    def __init__(self, numberBits: int, 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
        -------
        BaseSimulatorNumpy : geqo.simulators.numpy.BaseSimulatorNumpy
            A simulator object, which is based on NumPy.
        """
        self.numberBits = numberBits
        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 applyUmatrixNumpy(
        self,
        u: np.ndarray,
        targets: list[int],
        extraControls: dict | list[int] | None = None,
    ):
        """
        Apply a unitary matrix to the state, which is currently kept in the simulator. For recursion, a list of extra control qubits can be defined.

        Parameters
        ----------
        u : numpy.ndarray
            A unitary matrix.
        targets : list(int)
            The list of qubit indexes, which are the target of the provided unitary matrix.
        extraControls : list(int)
            A list of indexes, which are taken as control qubits.
        """
        pass

    @abstractmethod
    def apply(self, gate: QuantumOperation, targets: list[int], *args, **kwargs):
        """
        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.
        """
        pass

    def setValue(self, name: str, value: float | list[int] | np.ndarray | np.float64):
        """
        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 : numpy.float64 | numpy.ndarray
            The corresponding value for the name.
        """
        self.values[name] = value

    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)
        qasm_lines = self._qasm_lines_body(sequence, lines)

        return qasm_lines


[docs] class simulatorStatevectorNumpy(BaseSimulatorNumpy): 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.numpy.simulatorStatevectorNumpy A simulator object, which is based on NumPy. """ values = {} super().__init__(numberBits, numberQubits, values) if self.numberQubits > 32: raise NotImplementedError("Too many qubits, limit is 32") self.state = np.zeros(shape=(2**self.numberQubits, 1), dtype=np.complex128) self.state[0, 0] = 1.0 + 0 * 1j self.measurementResult = {} self.measurementHappened = False self.classicalBits = [0] * numberBits 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, PCCM): logger.info("prepare execution of PCCM") self.values[ops.nameSpacePrefix + "RX(π/2)"] = np.pi / 2 self.values[ops.nameSpacePrefix + "RX(-π/2)"] = -np.pi / 2 self.values[ops.nameSpacePrefix + "RY(-π/2)"] = -np.pi / 2 nameAngle = ops.nameSpacePrefix + ops.name if nameAngle not in self.values: logger.warning("%s not specified in the backend.", nameAngle) else: logger.debug( "found nameAngle %s with value %s", nameAngle, self.values[nameAngle], ) self.values[ops.nameSpacePrefix + "RX(" + str(nameAngle) + ")"] = ( self.values[nameAngle] ) elif 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 * np.pi / (2 ** (j + 1)) ) elif isinstance(ops, InverseQFT): self.prepareBackend([ops.qft]) 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"] = np.pi / 4 self.values[ops.nameSpacePrefix + "-S.Pi/4"] = -np.pi / 4 else: logger.error("prepareBackend: operation not supported: %s", str(ops)) raise Exception("prepareBackend: operation not supported:", str(ops))
[docs] def applyUmatrixNumpy( self, u: np.ndarray, targets: list[int], extraControls: dict | list[int] | None = None, ): """ Apply a unitary matrix to the state, which is currently kept in the simulator. For recursion, a list of extra control qubits can be defined. Parameters ---------- u : numpy.ndarray A unitary matrix. targets : list(int) The list of qubit indexes, which are the target of the provided unitary matrix. extraControls : list(int) A list of indexes, which are taken as control qubits. """ non_targets = list(set(range(self.numberQubits)) - set(targets)) for nt in product([0, 1], repeat=len(non_targets)): # check that the inherited controls are all set isControlled = True if isinstance(extraControls, list): for x in extraControls: if not (nt[non_targets.index(x)] == extraControls[x]): isControlled = False if isControlled: affected_components = [] for t in product([0, 1], repeat=len(targets)): bitstring = np.zeros(shape=self.numberQubits, dtype=int) bitstring[non_targets] = nt bitstring[targets] = t affected_components.append(int("".join(map(str, bitstring)), 2)) self.state[affected_components] = u @ self.state[affected_components]
[docs] def apply( self, gate: QuantumOperation, targets: list[int], classicalTargets=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, e.g. a measurement. extraControls : list(int) A list of qubit indices. The qubits are used as control qubits. Note that this is only for internal use. """ if classicalTargets is None: classicalTargets = [] if self.measurementHappened: logger.error("no more operation allowed after measurement") raise Exception("no more operation allowed after measurement") if isinstance(gate, ClassicalControl): logger.error("cannot perform ClassicalControl in statevector simulations") raise Exception( "cannot perform ClassicalControl in statevector simulations" ) elif gate.hasDecomposition(): dec = gate.getEquivalentSequence() qubitMapping = {} 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: 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], apply_qtargets, apply_ctargets, ) elif gate.isUnitary(): u = getUnitaryNumpy(gate, self.values) non_targets = list(set(range(self.numberQubits)) - set(targets)) dim = len(self.state) new_state = np.zeros((dim, 1), dtype=np.complex128) for n in product([0, 1], repeat=len(non_targets)): applied_indices = [] for t in product([0, 1], repeat=len(targets)): power_t = 2 ** np.array( [self.numberQubits - 1 - tar for tar in targets] ) power_n = 2 ** np.array( [self.numberQubits - 1 - non for non in non_targets] ) index = np.dot(np.array(t), power_t) + np.dot(np.array(n), power_n) applied_indices.append(index) # only apply u to the subspace of the target qubits applied_indices = np.array(applied_indices).astype(np.int64) new_state[applied_indices] = u @ self.state[applied_indices] self.state = new_state elif isinstance(gate, Measure): if len(targets) != gate.numberQubits: raise ValueError( "Number of targets does not match number of measured qubits" ) self.measurementHappened = True probabilities = dict() non_targets = list(set(range(self.numberQubits)) - set(targets)) for t in product([0, 1], repeat=gate.numberQubits): probability = 0.0 bitstring = np.zeros(shape=self.numberQubits, dtype=int) bitstring[targets] = t for nt in product([0, 1], repeat=len(non_targets)): bitstring[non_targets] = nt coeff = self.state[int("".join(map(str, bitstring)), 2), 0] probability += coeff * np.conj(coeff) newClassicalBits = self.classicalBits for i in range(len(classicalTargets)): newClassicalBits[classicalTargets[i]] = t[i] probabilities[tuple(newClassicalBits)] = np.real(probability) self.measurementResult = probabilities elif isinstance(gate, SetBits): bits = self.values.get(gate.name, None) if bits is None: logger.error('Bit values "%s" not found', gate.name) raise ValueError(f'Bit values "{gate.name}" not found') if len(classicalTargets) != len(bits): logger.error("wrong number of bits in definition for SetBits") raise ValueError("wrong number of bits in definition for SetBits") for i in range(len(classicalTargets)): self.classicalBits[classicalTargets[i]] = bits[i] elif isinstance(gate, SetQubits): logger.error("cannot set qubits in statevector simulations") raise Exception("cannot set qubits in statevector simulations") elif isinstance(gate, DropQubits): logger.error("cannot drop qubits in statevector simulations") raise Exception("cannot drop qubits in statevector simulations") elif isinstance(gate, SetDensityMatrix): logger.error("cannot set density matrix in statevector simulations") raise Exception("cannot set density matrix in statevector simulations") else: logger.error("gate not supported: %s", gate) raise Exception("gate not supported:", str(gate))