import torch
import torch.nn as nn
import torchquantum as tq
import torchquantum.functional.functionals as tqf
import numpy as np
from abc import ABCMeta
from ..macro import C_DTYPE, F_DTYPE
from typing import Iterable, Union, List
from enum import IntEnum
__all__ = [
"Operator",
"Operation",
"DiagonalOperation",
"Observable",
"WiresEnum",
"NParamsEnum",
"AnyNParams",
"AllWires",
"AnyWires",
]
[docs]class WiresEnum(IntEnum):
"""Integer enumeration class
to represent the number of wires
an operation acts on."""
AnyWires = -1
AllWires = 0
[docs]class NParamsEnum(IntEnum):
"""Integer enumeration class
to represent the number of wires
an operation acts on"""
AnyNParams = -1
AnyNParams = NParamsEnum.AnyNParams
AllWires = WiresEnum.AllWires
"""IntEnum: An enumeration which represents all wires in the
subsystem. It is equivalent to an integer with value 0."""
AnyWires = WiresEnum.AnyWires
"""IntEnum: An enumeration which represents any wires in the
subsystem. It is equivalent to an integer with value -1."""
[docs]class Operator(tq.QuantumModule):
"""The class for quantum operators."""
@property
def name(self):
"""String for the name of the operator."""
return self._name
@name.setter
def name(self, value):
"""Set the name of the operator.
Args:
value (str): operator name.
"""
self._name = value
[docs] def __init__(
self,
has_params: bool = False,
trainable: bool = False,
init_params=None,
n_wires=None,
wires=None,
inverse=False,
):
"""__init__ function for Operator.
Args:
has_params (bool, optional): Whether the operations has parameters.
Defaults to False.
trainable (bool, optional): Whether the parameters are trainable
(if contains parameters). Defaults to False.
init_params (torch.Tensor, optional): Initial parameters.
Defaults to None.
n_wires (int, optional): Number of qubits. Defaults to None.
wires (Union[int, List[int]], optional): Which qubit the operation
is applied to. Defaults to None.
"""
super().__init__()
self.params = None
# number of wires of the operator
# n_wires is used in gates that can be applied to arbitrary number
# of qubits such as MultiRZ
self.n_wires = n_wires
# wires that the operator applies to
self.wires = wires
self._name = self.__class__.__name__
# for static mode
self.static_matrix = None
self.inverse = inverse
self.clifford_quantization = False
try:
assert not (trainable and not has_params)
except AssertionError:
has_params = True
logger.warning(
f"Module must have parameters to be trainable; "
f"Switched 'has_params' to True."
)
self.has_params = has_params
self.trainable = trainable
if self.has_params:
self.params = self.build_params(trainable=self.trainable)
self.reset_params(init_params)
@classmethod
def _matrix(cls, params):
"""The unitary matrix of the operator.
Args:
params (torch.Tensor, optional): The parameters for parameterized
operators.
Returns: None.
"""
raise NotImplementedError
@property
def matrix(self):
"""The unitary matrix of the operator."""
return self._matrix(self.params)
@classmethod
def _eigvals(cls, params):
"""The eigenvalues of the unitary matrix of the operator.
Args:
params (torch.Tensor, optional): The parameters for parameterized
operators.
Returns: None.
"""
# Warning: The eigenvalues of the operator {cls.__name__} are not defined.
return None
@property
def eigvals(self):
"""The eigenvalues of the unitary matrix of the operator.
Returns: Eigenvalues.
"""
return self._eigvals(self.params)
def _get_unitary_matrix(self):
"""Obtain the unitary matrix of the operator.
Returns: Unitary matrix.
"""
return self.matrix
[docs] def set_wires(self, wires):
"""Set which qubits the operator is applied to.
Args:
wires (Union[int, List[int]]): Qubits the operator is applied to.
Returns: None.
"""
self.wires = [wires] if isinstance(wires, int) else wires
[docs] def forward(
self, q_device: tq.QuantumDevice, wires=None, params=None, inverse=None
):
"""Apply the operator to the quantum device states.
Args:
q_device (torchquantum.QuantumDevice): Quantum Device that the
operator is applied to.
wires (Union[int, List[int]]): Qubits that the operator is
applied to.
params (torch.Tensor): Parameters of the operator
inverse (bool): Whether inverse the unitary matrix of the operator.
Returns:
"""
if inverse is not None:
logger.warning("replace the inverse flag with the input")
self.inverse = inverse
# try:
# assert self.name in self.fixed_ops or \
# self.has_params ^ (params is not None)
# except AssertionError as err:
# logger.exception(f"Parameterized gate either has its "
# f"own parameters or has input as parameters")
# raise err
# try:
# assert not (self.wires is None and wires is None)
# except AssertionError as err:
# logger.exception(f"Need to specify the wires either when "
# f"initialize or when forward")
# raise err
if params is not None:
self.params = params
if self.params is not None:
self.params = (
self.params.unsqueeze(-1) if self.params.dim() == 1 else self.params
)
if wires is not None:
# update the wires
wires = [wires] if isinstance(wires, int) else wires
self.wires = wires
# self.inverse = inverse
if self.static_mode:
self.parent_graph.add_op(self)
return
# non-parameterized gate
if self.params is None:
if self.n_wires is None:
self.func(q_device, self.wires, inverse=self.inverse) # type: ignore
else:
self.func(q_device, self.wires, n_wires=self.n_wires, inverse=self.inverse) # type: ignore
else:
if isinstance(self.noise_model_tq, tq.NoiseModelTQPhase):
params = self.noise_model_tq.add_noise(self.params)
else:
params = self.params
if self.clifford_quantization:
params = CliffordQuantizer.quantize_sse(params)
if self.n_wires is None:
self.func(q_device, self.wires, params=params, inverse=self.inverse)
else:
self.func(
q_device,
self.wires,
params=params,
n_wires=self.n_wires,
inverse=self.inverse,
)
if self.noise_model_tq is not None and self.noise_model_tq.is_add_noise:
noise_ops = self.noise_model_tq.sample_noise_op(self)
if len(noise_ops):
for noise_op in noise_ops:
noise_op(q_device)
def __repr__(self):
return f" class: {self.name} \n parameters: {self.params} \n wires: {self.wires} \n inverse: {self.inverse}"
[docs]class Observable(Operator, metaclass=ABCMeta):
"""Class for Observables."""
[docs] def __init__(
self,
has_params: bool = False,
trainable: bool = False,
init_params=None,
n_wires=None,
wires=None,
inverse=False,
):
"""Init function of the Observable class
Args:
has_params (bool, optional): Whether the operations has parameters.
Defaults to False.
trainable (bool, optional): Whether the parameters are trainable
(if contains parameters). Defaults to False.
init_params (torch.Tensor, optional): Initial parameters.
Defaults to None.
n_wires (int, optional): Number of qubits. Defaults to None.
wires (Union[int, List[int]], optional): Which qubit the operation
is applied to. Defaults to None.
"""
super().__init__(
has_params=has_params,
trainable=trainable,
init_params=init_params,
n_wires=n_wires,
wires=wires,
inverse=inverse,
)
self.return_type = None
[docs] def diagonalizing_gates(self):
"""The diagonalizing gates when perform measurements.
Returns: None.
"""
raise NotImplementedError
[docs]class Operation(Operator, metaclass=ABCMeta):
"""_summary_"""
[docs] def __init__(
self,
has_params: bool = False,
trainable: bool = False,
init_params=None,
n_wires=None,
wires=None,
inverse=False,
):
"""_summary_
Args:
has_params (bool, optional): Whether the operations has parameters.
Defaults to False.
trainable (bool, optional): Whether the parameters are trainable
(if contains parameters). Defaults to False.
init_params (torch.Tensor, optional): Initial parameters.
Defaults to None.
n_wires (int, optional): Number of qubits. Defaults to None.
wires (Union[int, List[int]], optional): Which qubit the operation is applied to.
Defaults to None.
"""
super().__init__(
has_params=has_params,
trainable=trainable,
init_params=init_params,
n_wires=n_wires,
wires=wires,
inverse=inverse,
)
if type(self.num_wires) == int:
self.n_wires = self.num_wires
@property
def matrix(self):
"""The unitary matrix of the operator."""
op_matrix = self._matrix(self.params)
return op_matrix
@property
def eigvals(self):
""" "The eigenvalues of the unitary matrix of the operator.
Returns:
torch.Tensor: Eigenvalues.
"""
op_eigvals = self._eigvals(self.params)
return op_eigvals
[docs] def init_params(self):
"""Initialize the parameters.
Raises:
NotImplementedError: The init param function is not implemented.
"""
raise NotImplementedError
[docs] def build_params(self, trainable):
"""Build parameters.
Args:
trainable (bool): Whether the parameters are trainable.
Returns:
torch.Tensor: Built parameters.
"""
parameters = nn.Parameter(torch.empty([1, self.num_params], dtype=F_DTYPE))
parameters.requires_grad = True if trainable else False
# self.register_parameter(f"{self.name}_params", parameters)
return parameters
[docs] def reset_params(self, init_params=None):
"""Reset parameters.
Args:
init_params (torch.Tensor, optional): Input the initialization
parameters. Defaults to None.
"""
if init_params is not None:
if isinstance(init_params, Iterable):
for k, init_param in enumerate(init_params):
torch.nn.init.constant_(self.params[:, k], init_param)
else:
torch.nn.init.constant_(self.params, init_params)
else:
torch.nn.init.uniform_(self.params, -np.pi, np.pi)
[docs]class DiagonalOperation(Operation, metaclass=ABCMeta):
"""Class for Diagonal Operation."""
@classmethod
def _eigvals(cls, params):
"""The eigenvalues of the unitary matrix of the operator.
Args:
params (torch.Tensor, optional): The parameters for parameterized
operators.
Returns: None.
raise NotImplementedError
"""
@property
def eigvals(self):
"""The eigenvalues of the unitary matrix of the operator.
Returns: Eigenvalues.
"""
return super().eigvals
@classmethod
def _matrix(cls, params):
"""The unitary matrix of the operator.
Args:
params (torch.Tensor, optional): The parameters for parameterized
operators.
Returns: None.
"""
return torch.diag(cls._eigvals(params))