Source code for seemps.register.circuit

from __future__ import annotations
import numpy as np
from math import sqrt
from typing import Optional, Union
from ..typing import Operator, Vector
from ..state import MPS, CanonicalMPS, Strategy, DEFAULT_STRATEGY
from ..state._contractions import _contract_nrjl_ijk_klm
from abc import abstractmethod

σx = np.array([[0.0, 1.0], [1.0, 0.0]])
σz = np.array([[1.0, 0.0], [0.0, -1.0]])
σy = -1j * σz @ σx
id2 = np.eye(2)

known_operators = {
    "SX": σx / 2.0,
    "SY": σy / 2.0,
    "SZ": σz / 2.0,
    "σX": σx,
    "σY": σy,
    "σZ": σz,
    "CNOT": 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],
        ]
    ),
    "CX": "CNOT",
    "CZ": np.diag([1.0, 1.0, 1.0, -1.0]),
}


def interpret_operator(op: Union[str, Operator]) -> Operator:
    O: Operator
    if isinstance(op, str):
        O = known_operators.get(op.upper(), None)
        if O is None:
            raise Exception(f"Unknown qubit operator '{op}'")
        if isinstance(O, str):
            return interpret_operator(O)
    elif not isinstance(op, np.ndarray) or op.ndim != 2 or op.shape[0] != op.shape[1]:
        raise Exception(f"Invalid qubit operator of type '{type(op)}")
    else:
        O = op
    return O


class UnitaryCircuit:
    register_size: int
    stategy: Strategy

    def __init__(
        self,
        register_size: int,
        strategy: Strategy = DEFAULT_STRATEGY,
    ):
        self.strategy = strategy
        self.register_size = register_size

    @abstractmethod
    def apply_inplace(
        self, state: MPS, parameters: Optional[Vector] = None
    ) -> CanonicalMPS: ...

    def __matmul__(self, state: MPS) -> CanonicalMPS:
        return self.apply(state)

    def apply(self, state: MPS, parameters: Optional[Vector] = None) -> CanonicalMPS:
        return self.apply_inplace(
            state.copy() if isinstance(state, CanonicalMPS) else state, parameters
        )


class ParameterizedCircuit(UnitaryCircuit):
    parameters: Vector
    parameters_size: int

    def __init__(
        self,
        register_size: int,
        parameters_size: Optional[int] = None,
        default_parameters: Optional[Vector] = None,
        strategy: Strategy = DEFAULT_STRATEGY,
    ):
        super().__init__(register_size, strategy)
        if default_parameters is None:
            if parameters_size is None:
                raise Exception(
                    "In ParameterizedUnitaries, either parameter_size or default_parameters must be provided"
                )
            default_parameters = np.zeros(parameters_size)
        elif parameters_size is None:
            parameters_size = len(default_parameters)
        elif parameters_size != len(default_parameters):
            raise IndexError(
                f"'default_parameters' length {len(default_parameters)} does not match size 'parameters_size' {parameters_size}"
            )
        self.parameters = np.asarray(default_parameters)
        self.parameters_size = parameters_size


[docs] class LocalRotationsLayer(ParameterizedCircuit): """Layer of local rotations acting on the each qubit with the same generator and possibly different angles. Parameters ---------- register_size : int Number of qubits on which to operate. operator : str | Operator Either the name of a generator ("Sx", "Sy", "Sz") or a 2x2 matrix. same_parameter : bool If `True`, the same angle is reused by all gates and `self.parameters_size=1`. Otherwise, the user must provide one value for each rotation. default_parameters : Optional[Vector] A vector of angles to use if no other one is provided. strategy : Strategy Truncation and simplification strategy (Defaults to `DEFAULT_STRATEGY`) Examples -------- >>> state = random_uniform_mps(2, 3) >>> U = LocalRotationsLayer(register_size=state.size, operator="Sz") >>> Ustate = U @ state """ factor: float = 1.0 operator: Operator def __init__( self, register_size: int, operator: Union[str, Operator], same_parameter: bool = False, default_parameters: Optional[Vector] = None, strategy: Strategy = DEFAULT_STRATEGY, ): if same_parameter: parameters_size = 1 if default_parameters is not None: if len(default_parameters) > 1: raise Exception( "Cannot provide more than one parameter if same_parameter is True" ) else: parameters_size = register_size super().__init__( register_size, parameters_size, default_parameters, strategy, ) O = interpret_operator(operator) if O.shape != (2, 2): raise Exception("Not a valid one-qubit operator") # # self.operator is a Pauli operator with det = 1. We # extract the original determinant into a prefactor for the # rotation angles. # self.factor = sqrt(abs(np.linalg.det(O))) self.operator = O / self.factor
[docs] def apply_inplace( self, state: MPS, parameters: Optional[Vector] = None ) -> CanonicalMPS: assert self.register_size == state.size if parameters is None: parameters = self.parameters if not isinstance(state, CanonicalMPS): state = CanonicalMPS(state, center=0, strategy=self.strategy) if len(parameters) == 1: parameters = np.full(self.register_size, parameters[0]) else: parameters = np.asarray(parameters) angle = parameters.reshape(-1, 1, 1) * self.factor ops = np.cos(angle) * id2 - 1j * np.sin(angle) * self.operator for i, (opi, A) in enumerate(zip(ops, state)): # np.einsum('ij,ajb->aib', opi, A) state[i] = np.matmul(opi, A) return state
[docs] class TwoQubitGatesLayer(UnitaryCircuit): """Layer of CNOT/CZ gates, acting on qubits 0 to N-1, from left to right or right to left, depending on the direction. Parameters ---------- register_size : int Number of qubits on which to operate. operator : str | Operator A two-qubit gate, either denoted by a string ("CNOT", "CZ") or by a 4x4 two-qubit matrix. direction : int | None Direction in which gates are applied. If 'None', direction will be chosen based on the orthogonalitiy center of the state. strategy : Strategy Truncation strategy (Defaults to `DEFAULT_STRATEGY`) """ operator: Operator direction: Optional[int] def __init__( self, register_size: int, operator: Union[str, Operator], direction: Optional[int] = None, strategy: Strategy = DEFAULT_STRATEGY, ): super().__init__(register_size, strategy) O = interpret_operator(operator) if O.shape != (4, 4): raise Exception("Not a valid two-qubit operator") self.operator = O self.direction = direction
[docs] def apply_inplace( self, state: MPS, parameters: Optional[Vector] = None ) -> CanonicalMPS: assert self.register_size == state.size if parameters is not None and len(parameters) > 0: raise Exception("{self.cls} does not accept parameters") if not isinstance(state, CanonicalMPS): state = CanonicalMPS(state, center=0, strategy=self.strategy) L = self.register_size op = self.operator center = state.center strategy = self.strategy direction = self.direction if direction is None: direction = +1 if (center < L // 2) else -1 if direction >= 0: if center > 1: state.recenter(1) for j in range(L - 1): state.update_2site_right( _contract_nrjl_ijk_klm(op, state[j], state[j + 1]), j, strategy ) else: if center < L - 2: state.recenter(L - 2) for j in range(L - 2, -1, -1): # AA = np.einsum("ijk,klm,nrjl -> inrm", state[j], state[j + 1], U[j]) state.update_2site_left( _contract_nrjl_ijk_klm(op, state[j], state[j + 1]), j, strategy ) return state
[docs] class ParameterizedLayeredCircuit(ParameterizedCircuit): """Variational quantum circuit with Ry rotations and CNOTs. Constructs a unitary circuit with variable parameters, composed of operations such as :class:`LocalRotationsLayer`, :class:`TwoQubitGatesLayer` or similar gates. This is the basis for more useful algorithms such as the :class:`VariationalQuantumEigensolver` Parameters ---------- register_size : int Number of qubits on which to operate layers : list[UnitaryCircuit] List of constant or parameterized unitary layers. default_parameters : Vector Default angles for the rotations (Defaults to zeros). strategy : Strategy Truncation and simplification strategy (Defaults to `DEFAULT_STRATEGY`) """ layers: list[tuple[UnitaryCircuit, int, int]] def __init__( self, register_size: int, layers: list[UnitaryCircuit], default_parameters: Optional[Vector] = None, strategy: Strategy = DEFAULT_STRATEGY, ): parameters_size = 0 segments: list[tuple[UnitaryCircuit, int, int]] = [] for circuit in layers: if isinstance(circuit, ParameterizedCircuit): l = circuit.parameters_size segments.append((circuit, parameters_size, parameters_size + l)) parameters_size += l else: segments.append((circuit, 0, 0)) super().__init__(register_size, parameters_size, default_parameters, strategy) self.layers = segments
[docs] def apply_inplace( self, state: MPS, parameters: Optional[Vector] = None ) -> CanonicalMPS: if parameters is None: parameters = self.parameters for circuit, start, end in self.layers: state = circuit.apply_inplace(state, parameters[start:end]) return state # type: ignore
[docs] class VQECircuit(ParameterizedLayeredCircuit): """Variational quantum circuit with Ry rotations and CNOTs. Parameters ---------- register_size : int Number of qubits on which to operate layers : int Number of local rotation layers default_parameters : Vector Default angles for the rotations (Defaults to zeros). Must have size `layers * register_size`. strategy : Strategy Truncation and simplification strategy (Defaults to `DEFAULT_STRATEGY`) """ def __init__( self, register_size: int, layers: int, default_parameters: Optional[Vector] = None, strategy: Strategy = DEFAULT_STRATEGY, ): if default_parameters is not None: parameters_array = np.reshape(default_parameters, (-1, register_size)) def get_default_parameters(layer: int): if default_parameters is None: return None return parameters_array[layer // 2, :] super().__init__( register_size, [ LocalRotationsLayer( register_size, operator="Sy", same_parameter=False, default_parameters=get_default_parameters(layer), strategy=strategy, ) if (layer % 2 == 0) else TwoQubitGatesLayer( register_size, operator="CNOT", direction=+1 if (layer % 4) == 1 else -1, ) for layer in range(2 * layers) ], default_parameters, strategy, )