"""Quantum gates"""
from copy import copy
from typing import Any, TYPE_CHECKING
import torch
from torch import nn
from torch.autograd.functional import jacobian
from .distributed import dist_many_ctrl_one_targ_gate, dist_one_targ_gate, dist_swap_gate
from .operation import Gate
from .qmath import evolve_state, inverse_permutation, is_unitary, multi_kron, svd
from .state import DistributedQubitState
from .utils import apply_complex_fix
if TYPE_CHECKING:
from .qpd import MoveQPD
[docs]
class SingleGate(Gate):
r"""A base class for single-qubit gates.
Args:
name: The name of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
name: str | None = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
assert len(self.wires) == 1
# MBQC
self.nancilla = 2
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
matrix = self.update_matrix()
identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device)
if self.controls == []:
lst = [identity] * self.nqubit
lst[self.wires[0]] = matrix
return multi_kron(lst)
else:
oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device)
lst1 = [identity] * self.nqubit
lst2 = [identity] * self.nqubit
lst3 = [identity] * self.nqubit
for i in self.controls:
lst2[i] = oneone
lst3[i] = oneone
lst3[self.wires[0]] = matrix
return multi_kron(lst1) - multi_kron(lst2) + multi_kron(lst3)
[docs]
def op_dist_state(self, x: DistributedQubitState) -> DistributedQubitState:
"""Perform a forward pass of a gate for a distributed state vector."""
target = self.nqubit - self.wires[0] - 1
matrix = self.update_matrix()
if len(self.controls) > 0:
controls = [self.nqubit - control - 1 for control in self.controls]
return dist_many_ctrl_one_targ_gate(x, controls, target, matrix)
else:
return dist_one_targ_gate(x, target, matrix)
[docs]
class DoubleGate(Gate):
r"""A base class for two-qubit gates.
Args:
name: The name of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
name: str | None = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
if wires is None:
wires = [0, 1]
assert len(wires) == 2
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
matrix = self.update_matrix()
identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device)
zerozero = torch.tensor([[1, 0], [0, 0]], dtype=matrix.dtype, device=matrix.device)
zeroone = torch.tensor([[0, 1], [0, 0]], dtype=matrix.dtype, device=matrix.device)
onezero = torch.tensor([[0, 0], [1, 0]], dtype=matrix.dtype, device=matrix.device)
oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device)
if self.controls == []:
lst1 = [identity] * self.nqubit
lst2 = [identity] * self.nqubit
lst3 = [identity] * self.nqubit
lst4 = [identity] * self.nqubit
lst1[self.wires[0]] = zerozero
lst1[self.wires[1]] = matrix[0:2, 0:2]
lst2[self.wires[0]] = zeroone
lst2[self.wires[1]] = matrix[0:2, 2:4]
lst3[self.wires[0]] = onezero
lst3[self.wires[1]] = matrix[2:4, 0:2]
lst4[self.wires[0]] = oneone
lst4[self.wires[1]] = matrix[2:4, 2:4]
return multi_kron(lst1) + multi_kron(lst2) + multi_kron(lst3) + multi_kron(lst4)
else:
lst1 = [identity] * self.nqubit
lst2 = [identity] * self.nqubit
lst3 = [identity] * self.nqubit
lst4 = [identity] * self.nqubit
lst5 = [identity] * self.nqubit
lst6 = [identity] * self.nqubit
for i in self.controls:
lst2[i] = oneone
lst3[i] = oneone
lst4[i] = oneone
lst5[i] = oneone
lst6[i] = oneone
lst3[self.wires[0]] = zerozero
lst3[self.wires[1]] = matrix[0:2, 0:2]
lst4[self.wires[0]] = zeroone
lst4[self.wires[1]] = matrix[0:2, 2:4]
lst5[self.wires[0]] = onezero
lst5[self.wires[1]] = matrix[2:4, 0:2]
lst6[self.wires[0]] = oneone
lst6[self.wires[1]] = matrix[2:4, 2:4]
return (
multi_kron(lst1)
- multi_kron(lst2)
+ multi_kron(lst3)
+ multi_kron(lst4)
+ multi_kron(lst5)
+ multi_kron(lst6)
)
[docs]
class DoubleControlGate(DoubleGate):
r"""A base class for two-qubit controlled gates.
Args:
name: The name of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
name: str | None = None,
nqubit: int = 2,
wires: list[int] | None = None,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name=name, nqubit=nqubit, wires=wires, controls=None, condition=False, den_mat=den_mat, tsr_mode=tsr_mode
)
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
matrix = self.update_matrix()
identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device)
zerozero = torch.tensor([[1, 0], [0, 0]], dtype=matrix.dtype, device=matrix.device)
oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device)
lst1 = [identity] * self.nqubit
lst2 = [identity] * self.nqubit
lst1[self.wires[0]] = zerozero
lst2[self.wires[0]] = oneone
lst2[self.wires[1]] = matrix[2:4, 2:4]
return multi_kron(lst1) + multi_kron(lst2)
[docs]
class TripleGate(Gate):
r"""A base class for three-qubit gates.
Args:
name: The name of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 3
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
name: str | None = None,
nqubit: int = 3,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
if wires is None:
wires = [0, 1, 2]
assert len(wires) == 3
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
[docs]
class ArbitraryGate(Gate):
r"""A base class for customized gates.
Args:
name: The name of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
minmax: The minimum and maximum indices of the qubits that the quantum operation acts on.
Only valid when ``wires`` is ``None``. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
name: str | None = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
minmax: list[int] | None = None,
controls: int | list[int] | None = None,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
self.nqubit = nqubit
if wires is None:
if minmax is None:
minmax = [0, nqubit - 1]
self._check_minmax(minmax)
wires = list(range(minmax[0], minmax[1] + 1))
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=False,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.minmax = [min(self.wires), max(self.wires)]
# whether the wires are consecutive integers
self.local = True
for i in range(len(self.wires) - 1):
if self.wires[i + 1] - self.wires[i] != 1:
self.local = False
break
self.inv_mode = False
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
if self.local:
matrix = self.update_matrix()
identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device)
lst = [identity] * (self.nqubit - len(self.wires) + 1)
lst[self.wires[0]] = matrix
return multi_kron(lst)
else:
matrix = self.update_matrix()
identity = torch.eye(2**self.nqubit, dtype=matrix.dtype, device=matrix.device)
identity = identity.reshape([2**self.nqubit] + [2] * self.nqubit)
return self.op_state_base(identity, matrix).reshape(2**self.nqubit, 2**self.nqubit).T
[docs]
def inverse(self) -> 'ArbitraryGate':
"""Get the inversed gate."""
name = self.name + '_dagger' if isinstance(self.name, str) else self.name
gate = copy(self)
gate.inv_mode = not self.inv_mode
gate.name = name
return gate
[docs]
class ParametricSingleGate(SingleGate):
r"""A base class for single-qubit gates with parameters.
Args:
name: The name of the gate. Default: ``None``
inputs: The parameters of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameters are ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
name: str | None = None,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.npara = 1
self.requires_grad = requires_grad
self.inv_mode = False
self.init_para(inputs)
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
theta = -self.theta if self.inv_mode else self.theta
matrix = self.get_matrix(theta)
self.matrix = matrix.detach()
return matrix
[docs]
def get_derivative(self, theta: Any) -> torch.Tensor:
"""Get the derivative of the local unitary matrix."""
theta = self.inputs_to_tensor(theta).squeeze()
du_dx = jacobian(self._real_wrapper, theta)
return du_dx[..., 0] + du_dx[..., 1] * 1j
[docs]
def init_para(self, inputs: Any = None) -> None:
"""Initialize the parameters."""
theta = self.inputs_to_tensor(inputs)
if self.requires_grad:
self.theta = nn.Parameter(theta)
else:
self.register_buffer('theta', theta)
self.update_matrix()
[docs]
def inverse(self) -> 'ParametricSingleGate':
"""Get the inversed gate."""
gate = copy(self)
gate.inv_mode = not self.inv_mode
return gate
[docs]
class ParametricDoubleGate(DoubleGate):
r"""A base class for two-qubit gates with parameters.
Args:
name: The name of the gate. Default: ``None``
inputs: The parameters of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameters are ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
name: str | None = None,
inputs: Any = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.npara = 1
self.requires_grad = requires_grad
self.inv_mode = False
self.init_para(inputs)
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
theta = -self.theta if self.inv_mode else self.theta
matrix = self.get_matrix(theta)
self.matrix = matrix.detach()
return matrix
[docs]
def get_derivative(self, theta: Any) -> torch.Tensor:
"""Get the derivative of the local unitary matrix."""
theta = self.inputs_to_tensor(theta).squeeze()
du_dx = jacobian(self._real_wrapper, theta)
return du_dx[..., 0] + du_dx[..., 1] * 1j
[docs]
def init_para(self, inputs: Any = None) -> None:
"""Initialize the parameters."""
theta = self.inputs_to_tensor(inputs)
if self.requires_grad:
self.theta = nn.Parameter(theta)
else:
self.register_buffer('theta', theta)
self.update_matrix()
[docs]
def inverse(self) -> 'ParametricDoubleGate':
"""Get the inversed gate."""
gate = copy(self)
gate.inv_mode = not self.inv_mode
return gate
[docs]
class U3Gate(ParametricSingleGate):
r"""U3 gate, a generic single-qubit rotation gate with 3 angles.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
U_3(\theta, \phi, \lambda) =
\begin{pmatrix}
\cos\left(\th\right) & -e^{i\lambda}\sin\left(\th\right) \\
e^{i\phi}\sin\left(\th\right) & e^{i(\phi+\lambda)}\cos\left(\th\right)
\end{pmatrix}
Args:
inputs: The parameters of the gate (:math:`\theta`, :math:`\phi` and :math:`\lambda`). Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameters are ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='U3Gate',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
self.npara = 3
[docs]
def get_matrix(self, theta: Any, phi: Any, lambd: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta, phi, lambd = self.inputs_to_tensor([theta, phi, lambd])
cos_t = torch.cos(theta / 2) + 0j
sin_t = torch.sin(theta / 2) + 0j
e_il = torch.exp(1j * lambd)
e_ip = torch.exp(1j * phi)
e_ipl = torch.exp(1j * (phi + lambd))
return torch.stack([cos_t, -e_il * sin_t, e_ip * sin_t, e_ipl * cos_t]).reshape(2, 2)
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
if self.inv_mode:
theta = -self.theta
phi = -self.lambd
lambd = -self.phi
else:
theta = self.theta
phi = self.phi
lambd = self.lambd
matrix = self.get_matrix(theta, phi, lambd)
self.matrix = matrix.detach()
return matrix
def _real_wrapper(self, x: torch.Tensor) -> torch.Tensor:
mat = self.get_matrix(x[0], x[1], x[2])
return torch.view_as_real(mat)
[docs]
def get_derivative(self, inputs: Any) -> torch.Tensor:
"""Get the derivatives of the local unitary matrix."""
if not isinstance(inputs, torch.Tensor):
inputs = torch.tensor(inputs, dtype=torch.float)
inputs = inputs.reshape(self.npara)
du_dx = jacobian(self._real_wrapper, inputs).permute(3, 0, 1, 2)
return du_dx[..., 0] + du_dx[..., 1] * 1j
[docs]
def init_para(self, inputs: Any = None) -> None:
"""Initialize the parameters."""
theta, phi, lambd = self.inputs_to_tensor(inputs)
if self.requires_grad:
self.theta = nn.Parameter(theta)
self.phi = nn.Parameter(phi)
self.lambd = nn.Parameter(lambd)
else:
self.register_buffer('theta', theta)
self.register_buffer('phi', phi)
self.register_buffer('lambd', lambd)
self.update_matrix()
def _qasm(self) -> str:
if self.inv_mode:
theta = -self.theta
phi = -self.lambd
lambd = -self.phi
else:
theta = self.theta
phi = self.phi
lambd = self.lambd
if self.condition:
return self._qasm_cond_measure() + f'u({theta.item()},{phi.item()},{lambd.item()}) q{self.wires};\n'
if self.controls == []:
return f'u({theta.item()},{phi.item()},{lambd.item()}) q{self.wires};\n'
elif len(self.controls) == 1:
return f'cu({theta.item()},{phi.item()},{lambd.item()},0.0) q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('u')
[docs]
class PhaseShift(ParametricSingleGate):
r"""Phase shift gate.
**Matrix Representation:**
.. math::
P(\theta) =
\begin{pmatrix}
1 & 0 \\
0 & e^{i\theta}
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='PhaseShift',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
m1 = torch.eye(1, dtype=theta.dtype, device=theta.device)
e_it = torch.exp(1j * theta)
return torch.block_diag(m1, e_it).reshape(2, 2)
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
if self.condition:
return self._qasm_cond_measure() + f'p({theta.item()}) q{self.wires};\n'
if self.controls == []:
return f'p({theta.item()}) q{self.wires};\n'
elif len(self.controls) == 1:
return f'cp({theta.item()}) q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('p')
[docs]
class Identity(Gate):
r"""Identity gate.
**Matrix Representation:**
.. math::
I = \begin{pmatrix}
1 & 0 \\
0 & 1
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self, nqubit: int = 1, wires: int | list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False
) -> None:
super().__init__(
name='Identity',
nqubit=nqubit,
wires=wires,
controls=None,
condition=False,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.eye(2**self.nqubit, dtype=torch.cfloat))
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
return self.matrix
[docs]
def forward(self, x: Any) -> Any:
"""Perform a forward pass."""
return x
[docs]
class PauliX(SingleGate):
r"""PauliX gate.
**Matrix Representation:**
.. math::
X = \begin{pmatrix}
0 & 1 \\
1 & 0
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='PauliX',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[0, 1], [1, 0]], dtype=torch.cfloat))
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'x q{self.wires};\n'
if self.controls == []:
return f'x q{self.wires};\n'
elif len(self.controls) == 1:
return f'cx q{self.controls},q{self.wires};\n'
elif len(self.controls) == 2:
return f'ccx q[{self.controls[0]}],q[{self.controls[1]}],q{self.wires};\n'
else:
return self._qasm_customized('x')
[docs]
def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Measurement(nodes))
cmds.append(Measurement(ancilla[0], angle=-torch.pi))
cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0]))
cmds.append(Correction(ancilla[1], basis='z', domain=nodes))
self.nodes = [ancilla[1]]
return cmds
[docs]
class PauliY(SingleGate):
r"""PauliY gate.
**Matrix Representation:**
.. math::
Y = \begin{pmatrix}
0 & -i \\
i & 0
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='PauliY',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[0, -1j], [1j, 0]]))
# MBQC
self.nancilla = 4
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'y q{self.wires};\n'
if self.controls == []:
return f'y q{self.wires};\n'
elif len(self.controls) == 1:
return f'cy q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('y')
[docs]
def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Entanglement(ancilla[1], ancilla[2]))
cmds.append(Entanglement(ancilla[2], ancilla[3]))
cmds.append(Measurement(nodes, angle=torch.pi / 2))
cmds.append(Measurement(ancilla[0], angle=torch.pi, s_domain=nodes))
cmds.append(Measurement(ancilla[1], angle=-torch.pi / 2, s_domain=nodes))
cmds.append(Measurement(ancilla[2]))
cmds.append(Correction(ancilla[3], basis='x', domain=[ancilla[0], ancilla[2]]))
cmds.append(Correction(ancilla[3], basis='z', domain=[ancilla[0], ancilla[1]]))
self.nodes = [ancilla[3]]
return cmds
[docs]
class PauliZ(SingleGate):
r"""PauliZ gate.
**Matrix Representation:**
.. math::
Z = \begin{pmatrix}
1 & 0 \\
0 & -1
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='PauliZ',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0], [0, -1]], dtype=torch.cfloat))
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'z q{self.wires};\n'
if self.controls == []:
return f'z q{self.wires};\n'
elif len(self.controls) == 1:
return f'cz q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('z')
[docs]
def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Measurement(nodes, angle=-torch.pi))
cmds.append(Measurement(ancilla[0]))
cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0]))
cmds.append(Correction(ancilla[1], basis='z', domain=nodes))
self.nodes = [ancilla[1]]
return cmds
[docs]
class Hadamard(SingleGate):
r"""Hadamard gate.
**Matrix Representation:**
.. math::
H = \frac{1}{\sqrt{2}}
\begin{pmatrix}
1 & 1 \\
1 & -1
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='Hadamard',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 1], [1, -1]], dtype=torch.cfloat) / 2**0.5)
# MBQC
self.nancilla = 1
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'h q{self.wires};\n'
if self.controls == []:
return f'h q{self.wires};\n'
elif len(self.controls) == 1:
return f'ch q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('h')
[docs]
def pattern(self, nodes: int | list[int], ancilla: int | list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
if isinstance(ancilla, list):
assert len(ancilla) == self.nancilla
ancilla = ancilla[0]
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla))
cmds.append(Measurement(nodes))
cmds.append(Correction(ancilla, basis='x', domain=nodes))
self.nodes = [ancilla]
return cmds
[docs]
class SGate(SingleGate):
r"""S gate.
**Matrix Representation:**
.. math::
S = \begin{pmatrix}
1 & 0 \\
0 & i
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='SGate',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0], [0, 1j]]))
[docs]
def inverse(self) -> 'SDaggerGate':
"""Get the inversed gate."""
return SDaggerGate(
nqubit=self.nqubit,
wires=self.wires,
controls=self.controls,
condition=self.condition,
den_mat=self.den_mat,
tsr_mode=self.tsr_mode,
)
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f's q{self.wires};\n'
if self.controls == []:
return f's q{self.wires};\n'
elif len(self.controls) == 1:
qasm_str1 = ''
qasm_str2 = f'cs q{self.controls},q{self.wires};\n'
if 'cs' not in Gate._qasm_new_gate:
qasm_str1 = 'gate cs q0,q1 { p(pi/4) q0; cx q0,q1; p(-pi/4) q1; cx q0,q1; p(pi/4) q1; }\n'
Gate._qasm_new_gate.append('cs')
return qasm_str1 + qasm_str2
else:
return self._qasm_customized('s')
[docs]
def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Measurement(nodes, angle=-torch.pi / 2))
cmds.append(Measurement(ancilla[0]))
cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0]))
cmds.append(Correction(ancilla[1], basis='z', domain=nodes))
self.nodes = [ancilla[1]]
return cmds
[docs]
class SDaggerGate(SingleGate):
r"""S dagger gate.
**Matrix Representation:**
.. math::
S^{\dagger} =
\begin{pmatrix}
1 & 0 \\
0 & -i
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='SDaggerGate',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0], [0, -1j]]))
[docs]
def inverse(self) -> SGate:
"""Get the inversed gate."""
return SGate(
nqubit=self.nqubit,
wires=self.wires,
controls=self.controls,
condition=self.condition,
den_mat=self.den_mat,
tsr_mode=self.tsr_mode,
)
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'sdg q{self.wires};\n'
if self.controls == []:
return f'sdg q{self.wires};\n'
elif len(self.controls) == 1:
qasm_str1 = ''
qasm_str2 = f'csdg q{self.controls},q{self.wires};\n'
if 'csdg' not in Gate._qasm_new_gate:
qasm_str1 = 'gate csdg q0,q1 { p(-pi/4) q0; cx q0,q1; p(pi/4) q1; cx q0,q1; p(-pi/4) q1; }\n'
Gate._qasm_new_gate.append('csdg')
return qasm_str1 + qasm_str2
else:
return self._qasm_customized('sdg')
[docs]
class TGate(SingleGate):
r"""T gate.
**Matrix Representation:**
.. math::
T = \begin{pmatrix}
1 & 0 \\
0 & e^{i\pi/4}
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='TGate',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0], [0, (1 + 1j) / 2**0.5]]))
[docs]
def inverse(self) -> 'TDaggerGate':
"""Get the inversed gate."""
return TDaggerGate(
nqubit=self.nqubit,
wires=self.wires,
controls=self.controls,
condition=self.condition,
den_mat=self.den_mat,
tsr_mode=self.tsr_mode,
)
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f't q{self.wires};\n'
if self.controls == []:
return f't q{self.wires};\n'
else:
return self._qasm_customized('t')
[docs]
class TDaggerGate(SingleGate):
r"""T dagger gate.
**Matrix Representation:**
.. math::
T^{\dagger} =
\begin{pmatrix}
1 & 0 \\
0 & e^{-i\pi/4}
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='TDaggerGate',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0], [0, (1 - 1j) / 2**0.5]]))
[docs]
def inverse(self) -> TGate:
"""Get the inversed gate."""
return TGate(
nqubit=self.nqubit,
wires=self.wires,
controls=self.controls,
condition=self.condition,
den_mat=self.den_mat,
tsr_mode=self.tsr_mode,
)
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'tdg q{self.wires};\n'
if self.controls == []:
return f'tdg q{self.wires};\n'
else:
return self._qasm_customized('tdg')
[docs]
class Rx(ParametricSingleGate):
r"""Rx gate, rotation around x-axis.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
R_x(\theta) =
\begin{pmatrix}
\cos\left(\th\right) & -i\sin\left(\th\right) \\
-i\sin\left(\th\right) & \cos\left(\th\right)
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Rx',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
# MBQC
self.idx_enc = [4] # index of the commands to encode
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
cos = torch.cos(theta / 2) + 0j
isin = torch.sin(theta / 2) * 1j
return torch.stack([cos, -isin, -isin, cos]).reshape(2, 2)
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
if self.condition:
return self._qasm_cond_measure() + f'rx({theta.item()}) q{self.wires};\n'
if self.controls == []:
return f'rx({theta.item()}) q{self.wires};\n'
elif len(self.controls) == 1:
return f'crx({theta.item()}) q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('rx')
[docs]
def pattern(
self, nodes: int | list[int], ancilla: list[int], angle: Any, requires_grad: bool = False
) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Measurement(nodes))
cmds.append(Measurement(ancilla[0], angle=-angle, s_domain=nodes, requires_grad=requires_grad))
cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0]))
cmds.append(Correction(ancilla[1], basis='z', domain=nodes))
self.nodes = [ancilla[1]]
return cmds
[docs]
class Ry(ParametricSingleGate):
r"""Ry gate, rotation around y-axis.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
R_y(\theta) =
\begin{pmatrix}
\cos\left(\th\right) & -\sin\left(\th\right) \\
\sin\left(\th\right) & \cos\left(\th\right)
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Ry',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
# MBQC
self.nancilla = 4
self.idx_enc = [6] # index of the commands to encode
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
cos = torch.cos(theta / 2)
sin = torch.sin(theta / 2)
return torch.stack([cos, -sin, sin, cos]).reshape(2, 2) + 0j
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
if self.condition:
return self._qasm_cond_measure() + f'ry({theta.item()}) q{self.wires};\n'
if self.controls == []:
return f'ry({theta.item()}) q{self.wires};\n'
elif len(self.controls) == 1:
return f'cry({theta.item()}) q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('ry')
[docs]
def pattern(
self, nodes: int | list[int], ancilla: list[int], angle: Any, requires_grad: bool = False
) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Entanglement(ancilla[1], ancilla[2]))
cmds.append(Entanglement(ancilla[2], ancilla[3]))
cmds.append(Measurement(nodes, angle=torch.pi / 2))
cmds.append(Measurement(ancilla[0], angle=-angle, s_domain=nodes, requires_grad=requires_grad))
cmds.append(Measurement(ancilla[1], angle=-torch.pi / 2, s_domain=nodes))
cmds.append(Measurement(ancilla[2]))
cmds.append(Correction(ancilla[3], basis='x', domain=[ancilla[0], ancilla[2]]))
cmds.append(Correction(ancilla[3], basis='z', domain=[ancilla[0], ancilla[1]]))
self.nodes = [ancilla[3]]
return cmds
[docs]
class Rz(ParametricSingleGate):
r"""Rz gate, rotation around z-axis.
**Matrix Representation:**
.. math::
R_z(\theta) =
\begin{pmatrix}
e^{-i\frac{\theta}{2}} & 0 \\
0 & e^{i\frac{\theta}{2}}
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Rz',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
# MBQC
self.idx_enc = [3] # index of the commands to encode
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
e_m_it = torch.exp(-1j * theta / 2)
e_it = torch.exp(1j * theta / 2)
return torch.stack([e_m_it, e_it]).reshape(-1).diag_embed().reshape(2, 2)
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
if self.condition:
return self._qasm_cond_measure() + f'rz({theta.item()}) q{self.wires};\n'
if self.controls == []:
return f'rz({theta.item()}) q{self.wires};\n'
elif len(self.controls) == 1:
return f'crz({theta.item()}) q{self.controls},q{self.wires};\n'
else:
return self._qasm_customized('rz')
[docs]
def pattern(
self, nodes: int | list[int], ancilla: list[int], angle: Any, requires_grad: bool = False
) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
if isinstance(nodes, list):
assert len(nodes) == len(self.wires)
nodes = nodes[0]
assert len(ancilla) == self.nancilla
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(nodes, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Measurement(nodes, angle=-angle, requires_grad=requires_grad))
cmds.append(Measurement(ancilla[0]))
cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0]))
cmds.append(Correction(ancilla[1], basis='z', domain=nodes))
self.nodes = [ancilla[1]]
return cmds
[docs]
class ProjectionJ(ParametricSingleGate):
r"""Matrix J to project the measurement basis.
The matrix :math:`J(\theta)` transforms the measurement in the XY, YZ, or ZX plane to Z measurement.
The transformation means :math:`M(\theta)|\psi\rangle = M_z J(\theta)|\psi\rangle`,
where :math:`M_z` is the Z-basis measurement.
**Matrix Representation:**
.. math::
J_{\text{XY}}(\theta) = \frac{1}{\sqrt{2}}
\begin{pmatrix}
1 & e^{-i\theta} \\
1 & -e^{-i\theta}
\end{pmatrix}
.. math::
\newcommand{\th}{\frac{\theta}{2}}
J_{\text{YZ}}(\theta) = \frac{1}{\sqrt{2}}
\begin{pmatrix}
\cos(\th) + \sin(\th) & -i(\cos(\th) - \sin(\th)) \\
\cos(\th) - \sin(\th) & i(\cos(\th) + \sin(\th))
\end{pmatrix}
.. math::
\newcommand{\th}{\frac{\theta}{2}}
J_{\text{ZX}}(\theta) =
\begin{pmatrix}
\cos(\th) & \sin(\th) \\
\sin(\th) & -\cos(\th)
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
plane: The measurement plane (``'xy'``, ``'yz'``, or ``'zx'``). Default: ``'xy'``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
plane: str = 'xy',
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
self.plane = plane.lower()
super().__init__(
name='ProjectionJ',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
if self.plane in ['xy', 'yx']:
one = torch.ones_like(theta) + 0j
e_m_theta = torch.exp(-1j * theta)
return torch.stack([one, e_m_theta, one, -e_m_theta]).reshape(2, 2) / 2**0.5
elif self.plane in ['yz', 'zy']:
c_p_s = torch.cos(theta / 2) + torch.sin(theta / 2) + 0j
c_m_s = torch.cos(theta / 2) - torch.sin(theta / 2) + 0j
return torch.stack([c_p_s, -1j * c_m_s, c_m_s, 1j * c_p_s]).reshape(2, 2) / 2**0.5
elif self.plane in ['zx', 'xz']:
cos = torch.cos(theta / 2)
sin = torch.sin(theta / 2)
return torch.stack([cos, sin, sin, -cos]).reshape(2, 2) + 0j
else:
raise ValueError(f'Unsupported measurement plane: {self.plane}')
def _qasm(self) -> str:
if self.condition:
name = 'j'
qasm_lst1 = [f'opaque {name} ']
qasm_lst2 = [f'{name} ']
for i, wire in enumerate(self.wires):
qasm_lst1.append(f'q{i},')
qasm_lst2.append(f'q[{wire}],')
qasm_str1 = ''.join(qasm_lst1)[:-1] + ';\n'
qasm_str2 = ''.join(qasm_lst2)[:-1] + ';\n'
if name not in Gate._qasm_new_gate:
Gate._qasm_new_gate.append(name)
return qasm_str1 + self._qasm_cond_measure() + qasm_str2
else:
return self._qasm_cond_measure() + qasm_str2
return self._qasm_customized('j')
[docs]
class CombinedSingleGate(SingleGate):
r"""Combined single-qubit gate.
Args:
gates: The list of single-qubit gates.
name: The name of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
gates: list[SingleGate],
name: str | None = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name=name,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
for gate in gates:
gate.nqubit = self.nqubit
gate.wires = self.wires
gate.controls = self.controls
gate.condition = self.condition
gate.den_mat = self.den_mat
gate.tsr_mode = self.tsr_mode
self.gates = nn.ModuleList(gates)
self.update_npara()
self.update_matrix()
[docs]
def get_matrix(self) -> torch.Tensor:
"""Get the local unitary matrix."""
matrix = None
for gate in self.gates:
matrix = gate.update_matrix() if matrix is None else gate.update_matrix() @ matrix
return matrix
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
matrix = self.get_matrix()
self.matrix = matrix.detach()
return matrix
[docs]
def get_derivative(self, inputs: Any) -> torch.Tensor:
"""Get the derivatives of the local unitary matrix."""
if not isinstance(inputs, torch.Tensor):
inputs = torch.tensor(inputs, dtype=torch.float)
inputs = inputs.reshape(self.npara)
derivatives = []
count = 0
for gate in self.gates:
du_dx = gate.get_derivative(inputs[count : count + gate.npara])
if du_dx.ndim == 2:
du_dx = du_dx.unsqueeze(0)
derivatives.append(du_dx)
count += gate.npara
return torch.cat(derivatives)
[docs]
def update_npara(self) -> None:
"""Update the number of parameters."""
self.npara = 0
for gate in self.gates:
self.npara += gate.npara
[docs]
def add(self, gate: SingleGate) -> None:
"""Add a single-qubit gate to the list and update the local unitary matrix."""
gate.nqubit = self.nqubit
gate.wires = self.wires
gate.controls = self.controls
gate.condition = self.condition
gate.den_mat = self.den_mat
gate.tsr_mode = self.tsr_mode
self.gates.append(gate)
self.matrix = gate.matrix @ self.matrix
self.npara += gate.npara
[docs]
def inverse(self) -> 'CombinedSingleGate':
"""Get the inversed gate."""
gates = nn.ModuleList()
for gate in reversed(self.gates):
gates.append(gate.inverse())
return CombinedSingleGate(
gates=gates,
name=self.name,
nqubit=self.nqubit,
wires=self.wires,
controls=self.controls,
condition=self.condition,
den_mat=self.den_mat,
tsr_mode=self.tsr_mode,
)
def _qasm(self) -> str:
lst = []
for gate in self.gates:
lst.append(gate._qasm())
return ''.join(lst)
[docs]
class CNOT(DoubleControlGate):
r"""CNOT gate.
**Matrix Representation:**
.. math::
\text{CNOT} =
\begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 0 & 1 \\
0 & 0 & 1 & 0
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self, nqubit: int = 2, wires: list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False
) -> None:
super().__init__(name='CNOT', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode)
self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + 0j)
# MBQC
self.nancilla = 2
def _qasm(self) -> str:
return f'cx q[{self.wires[0]}],q[{self.wires[1]}];\n'
[docs]
def pattern(self, nodes: list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
assert len(nodes) == len(self.wires)
assert len(ancilla) == self.nancilla
control = nodes[0]
target = nodes[1]
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(target, ancilla[0]))
cmds.append(Entanglement(control, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Measurement(target))
cmds.append(Measurement(ancilla[0]))
cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0]))
cmds.append(Correction(ancilla[1], basis='z', domain=target))
cmds.append(Correction(control, basis='z', domain=target))
self.nodes = [control, ancilla[1]]
return cmds
[docs]
class Swap(DoubleGate):
r"""Swap gate.
**Matrix Representation:**
.. math::
\text{SWAP} =
\begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='Swap',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + 0j)
[docs]
def op_dist_state(self, x: DistributedQubitState) -> DistributedQubitState:
"""Perform a forward pass of a gate for a distributed state vector."""
if len(self.controls) > 0:
return super().op_dist_state(x)
else:
return dist_swap_gate(x, self.nqubit - self.wires[0] - 1, self.nqubit - self.wires[1] - 1)
def _qasm(self) -> str:
if self.condition:
return self._qasm_cond_measure() + f'swap q[{self.wires[0]}],q[{self.wires[1]}];\n'
if self.controls == []:
return f'swap q[{self.wires[0]}],q[{self.wires[1]}];\n'
elif len(self.controls) == 1:
return f'cswap q{self.controls},q[{self.wires[0]}],q[{self.wires[1]}];\n'
else:
return self._qasm_customized('swap')
[docs]
class ImaginarySwap(DoubleGate):
r"""Imaginary Swap gate.
**Matrix Representation:**
.. math::
\text{iSWAP} =
\begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & i & 0 \\
0 & i & 0 & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name='ImaginarySwap',
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]]))
def _qasm(self) -> str:
qasm_str1 = ''
qasm_str2 = f'iswap q[{self.wires[0]}],q[{self.wires[1]}];\n'
if 'iswap' not in Gate._qasm_new_gate:
qasm_str1 = 'gate iswap q0,q1 { s q0; s q1; h q0; cx q0,q1; cx q1,q0; h q1; }\n'
Gate._qasm_new_gate.append('iswap')
if self.condition:
return qasm_str1 + self._qasm_cond_measure() + qasm_str2
if self.controls == []:
return qasm_str1 + qasm_str2
else:
return self._qasm_customized('iswap')
[docs]
class Rxx(ParametricDoubleGate):
r"""Rxx gate.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
R_{xx}(\theta) = \exp\left(-i \th X{\otimes}X\right) =
\begin{pmatrix}
\cos\left(\th\right) & 0 & 0 & -i\sin\left(\th\right) \\
0 & \cos\left(\th\right) & -i\sin\left(\th\right) & 0 \\
0 & -i\sin\left(\th\right) & \cos\left(\th\right) & 0 \\
-i\sin\left(\th\right) & 0 & 0 & \cos\left(\th\right)
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Rxx',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
cos = torch.cos(theta / 2)
isin = torch.sin(theta / 2) * 1j
m1 = torch.stack([cos, cos, cos, cos]).reshape(-1).diag_embed().reshape(4, 4)
m2 = torch.stack([-isin, -isin, -isin, -isin]).reshape(-1).diag_embed().fliplr().reshape(4, 4)
return m1 + m2
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
if self.condition:
return self._qasm_cond_measure() + f'rxx({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n'
if self.controls == []:
return f'rxx({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n'
else:
return self._qasm_customized('rxx')
[docs]
class Ryy(ParametricDoubleGate):
r"""Ryy gate.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
R_{yy}(\theta) = \exp\left(-i \th Y{\otimes}Y\right) =
\begin{pmatrix}
\cos\left(\th\right) & 0 & 0 & i\sin\left(\th\right) \\
0 & \cos\left(\th\right) & -i\sin\left(\th\right) & 0 \\
0 & -i\sin\left(\th\right) & \cos\left(\th\right) & 0 \\
i\sin\left(\th\right) & 0 & 0 & \cos\left(\th\right)
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Ryy',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
cos = torch.cos(theta / 2)
isin = torch.sin(theta / 2) * 1j
m1 = torch.stack([cos, cos, cos, cos]).reshape(-1).diag_embed().reshape(4, 4)
m2 = torch.stack([isin, -isin, -isin, isin]).reshape(-1).diag_embed().fliplr().reshape(4, 4)
return m1 + m2
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
qasm_str1 = ''
qasm_str2 = f'ryy({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n'
if 'ryy' not in Gate._qasm_new_gate:
qasm_str1 = (
'gate ryy(param0) q0,q1 { '
'rx(pi/2) q0; rx(pi/2) q1; cx q0,q1; '
'rz(param0) q1; cx q0,q1; '
'rx(-pi/2) q0; rx(-pi/2) q1; }\n'
)
Gate._qasm_new_gate.append('ryy')
if self.condition:
return qasm_str1 + self._qasm_cond_measure() + qasm_str2
if self.controls == []:
return qasm_str1 + qasm_str2
else:
return self._qasm_customized('ryy')
[docs]
class Rzz(ParametricDoubleGate):
r"""Rzz gate.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
R_{zz}(\theta) = \exp\left(-i \th Z{\otimes}Z\right) =
\begin{pmatrix}
e^{-i \th} & 0 & 0 & 0 \\
0 & e^{i \th} & 0 & 0 \\
0 & 0 & e^{i \th} & 0 \\
0 & 0 & 0 & e^{-i \th}
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Rzz',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
e_m_it = torch.exp(-1j * theta / 2)
e_it = torch.exp(1j * theta / 2)
return torch.stack([e_m_it, e_it, e_it, e_m_it]).reshape(-1).diag_embed().reshape(4, 4)
def _qasm(self) -> str:
theta = -self.theta if self.inv_mode else self.theta
if self.condition:
return self._qasm_cond_measure() + f'rzz({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n'
if self.controls == []:
return f'rzz({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n'
else:
return self._qasm_customized('rzz')
[docs]
class Rxy(ParametricDoubleGate):
r"""Rxy gate.
**Matrix Representation:**
.. math::
\newcommand{\th}{\frac{\theta}{2}}
R_{xy}(\theta) = \exp\left(-i \th X{\otimes}Y\right) =
\begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & \cos\left(\th\right) & -i\sin\left(\th\right) & 0 \\
0 & -i\sin\left(\th\right) & \cos\left(\th\right) & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='Rxy',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
cos = torch.cos(theta / 2) + 0j
isin = torch.sin(theta / 2) * 1j
m1 = torch.eye(1, dtype=theta.dtype, device=theta.device)
m2 = torch.stack([cos, -isin, -isin, cos]).reshape(2, 2)
return torch.block_diag(m1, m2, m1)
def _qasm(self) -> str:
if self.condition:
name = 'rxy'
qasm_lst1 = [f'opaque {name} ']
qasm_lst2 = [f'{name} ']
for i, wire in enumerate(self.wires):
qasm_lst1.append(f'q{i},')
qasm_lst2.append(f'q[{wire}],')
qasm_str1 = ''.join(qasm_lst1)[:-1] + ';\n'
qasm_str2 = ''.join(qasm_lst2)[:-1] + ';\n'
if name not in Gate._qasm_new_gate:
Gate._qasm_new_gate.append(name)
return qasm_str1 + self._qasm_cond_measure() + qasm_str2
else:
return self._qasm_cond_measure() + qasm_str2
return self._qasm_customized('rxy')
[docs]
class ReconfigurableBeamSplitter(ParametricDoubleGate):
r"""Reconfigurable Beam Splitter gate.
**Matrix Representation:**
.. math::
\text{RBS}(\theta) =
\begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & \cos\left(\theta\right) & \sin\left(\theta\right) & 0 \\
0 & -\sin\left(\theta\right) & \cos\left(\theta\right) & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}
Args:
inputs: The parameter of the gate. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 2
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
condition: Whether to use ``controls`` as conditional measurement. Default: ``False``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 2,
wires: list[int] | None = None,
controls: int | list[int] | None = None,
condition: bool = False,
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name='ReconfigurableBeamSplitter',
inputs=inputs,
nqubit=nqubit,
wires=wires,
controls=controls,
condition=condition,
den_mat=den_mat,
tsr_mode=tsr_mode,
requires_grad=requires_grad,
)
[docs]
def get_matrix(self, theta: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
theta = self.inputs_to_tensor(theta)
cos = torch.cos(theta)
sin = torch.sin(theta)
m1 = torch.eye(1, dtype=theta.dtype, device=theta.device)
m2 = torch.stack([cos, sin, -sin, cos]).reshape(2, 2) + 0j
return torch.block_diag(m1, m2, m1)
def _qasm(self) -> str:
if self.condition:
name = 'rbs'
qasm_lst1 = [f'opaque {name} ']
qasm_lst2 = [f'{name} ']
for i, wire in enumerate(self.wires):
qasm_lst1.append(f'q{i},')
qasm_lst2.append(f'q[{wire}],')
qasm_str1 = ''.join(qasm_lst1)[:-1] + ';\n'
qasm_str2 = ''.join(qasm_lst2)[:-1] + ';\n'
if name not in Gate._qasm_new_gate:
Gate._qasm_new_gate.append(name)
return qasm_str1 + self._qasm_cond_measure() + qasm_str2
else:
return self._qasm_cond_measure() + qasm_str2
return self._qasm_customized('rbs')
[docs]
class Toffoli(TripleGate):
r"""Toffoli gate.
**Matrix Representation:**
.. math::
\text{Toffoli} =
\begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 3
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self, nqubit: int = 3, wires: list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False
) -> None:
super().__init__(
name='Toffoli',
nqubit=nqubit,
wires=wires,
controls=None,
condition=False,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer(
'matrix',
torch.tensor(
[
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1, 0],
]
)
+ 0j,
)
# MBQC
self.nancilla = 18
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
matrix = self.update_matrix()
identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device)
oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device)
lst1 = [identity] * self.nqubit
lst2 = [identity] * self.nqubit
lst3 = [identity] * self.nqubit
lst2[self.wires[0]] = oneone
lst2[self.wires[1]] = oneone
lst3[self.wires[0]] = oneone
lst3[self.wires[1]] = oneone
lst3[self.wires[2]] = matrix[-2:, -2:]
return multi_kron(lst1) - multi_kron(lst2) + multi_kron(lst3)
def _qasm(self) -> str:
return f'ccx q[{self.wires[0]}],q[{self.wires[1]}],q[{self.wires[2]}];\n'
[docs]
def pattern(self, nodes: list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
from .mbqc import Correction, Entanglement, Measurement, Node
assert len(nodes) == len(self.wires)
assert len(ancilla) == self.nancilla
control1 = nodes[0]
control2 = nodes[1]
target = nodes[2]
cmds = nn.Sequential()
cmds.append(Node(ancilla))
cmds.append(Entanglement(target, ancilla[0]))
cmds.append(Entanglement(ancilla[0], ancilla[1]))
cmds.append(Entanglement(ancilla[1], ancilla[2]))
cmds.append(Entanglement(ancilla[1], control2))
cmds.append(Entanglement(control1, ancilla[14]))
cmds.append(Entanglement(ancilla[2], ancilla[3]))
cmds.append(Entanglement(ancilla[14], ancilla[4]))
cmds.append(Entanglement(ancilla[3], ancilla[5]))
cmds.append(Entanglement(ancilla[3], ancilla[4]))
cmds.append(Entanglement(ancilla[5], ancilla[6]))
cmds.append(Entanglement(control2, ancilla[6]))
cmds.append(Entanglement(control2, ancilla[9]))
cmds.append(Entanglement(ancilla[6], ancilla[7]))
cmds.append(Entanglement(ancilla[9], ancilla[4]))
cmds.append(Entanglement(ancilla[9], ancilla[10]))
cmds.append(Entanglement(ancilla[7], ancilla[8]))
cmds.append(Entanglement(ancilla[10], ancilla[11]))
cmds.append(Entanglement(ancilla[4], ancilla[8]))
cmds.append(Entanglement(ancilla[4], ancilla[11]))
cmds.append(Entanglement(ancilla[4], ancilla[16]))
cmds.append(Entanglement(ancilla[8], ancilla[12]))
cmds.append(Entanglement(ancilla[11], ancilla[15]))
cmds.append(Entanglement(ancilla[12], ancilla[13]))
cmds.append(Entanglement(ancilla[16], ancilla[17]))
cmds.append(Measurement(target))
cmds.append(Measurement(ancilla[0], s_domain=target))
cmds.append(Measurement(ancilla[1], s_domain=ancilla[0]))
cmds.append(Measurement(control1))
cmds.append(Measurement(ancilla[2], angle=-torch.pi * 7 / 4, s_domain=[ancilla[1], target]))
cmds.append(Measurement(ancilla[14], s_domain=control1))
cmds.append(Measurement(ancilla[3], s_domain=[ancilla[2], ancilla[0]]))
cmds.append(
Measurement(ancilla[5], angle=-torch.pi / 4, s_domain=[ancilla[3], ancilla[1], ancilla[14], target])
)
cmds.append(Measurement(control2, angle=-torch.pi / 4))
cmds.append(Measurement(ancilla[6], s_domain=[ancilla[5], ancilla[2], ancilla[0]]))
cmds.append(Measurement(ancilla[9], s_domain=[control2, ancilla[5], ancilla[2]]))
cmds.append(
Measurement(
ancilla[7], angle=-torch.pi * 7 / 4, s_domain=[ancilla[6], ancilla[3], ancilla[1], ancilla[14], target]
)
)
cmds.append(Measurement(ancilla[10], angle=-torch.pi * 7 / 4, s_domain=[ancilla[9], ancilla[14]]))
cmds.append(Measurement(ancilla[4], angle=-torch.pi / 4, s_domain=ancilla[14]))
cmds.append(Measurement(ancilla[8], s_domain=[ancilla[7], ancilla[5], ancilla[2], ancilla[0]]))
cmds.append(Measurement(ancilla[11], s_domain=[ancilla[10], control2, ancilla[5], ancilla[2]]))
cmds.append(
Measurement(
ancilla[12], angle=-torch.pi / 4, s_domain=[ancilla[8], ancilla[6], ancilla[3], ancilla[1], target]
)
)
cmds.append(
Measurement(
ancilla[16],
s_domain=[
ancilla[4],
control1,
ancilla[2],
control2,
ancilla[7],
ancilla[10],
ancilla[2],
control2,
ancilla[5],
],
)
)
cmds.append(Correction(ancilla[17], basis='x', domain=[ancilla[14], ancilla[16]]))
cmds.append(Correction(ancilla[15], basis='x', domain=[ancilla[9], ancilla[11]]))
cmds.append(
Correction(ancilla[13], basis='x', domain=[ancilla[0], ancilla[2], ancilla[5], ancilla[7], ancilla[12]])
)
cmds.append(
Correction(ancilla[17], basis='z', domain=[ancilla[4], ancilla[5], ancilla[7], ancilla[10], control1])
)
cmds.append(Correction(ancilla[15], basis='z', domain=[control2, ancilla[2], ancilla[5], ancilla[10]]))
cmds.append(Correction(ancilla[13], basis='z', domain=[ancilla[1], ancilla[3], ancilla[6], ancilla[8], target]))
self.nodes = [ancilla[17], ancilla[15], ancilla[13]]
return cmds
[docs]
class Fredkin(TripleGate):
r"""Fredkin gate.
**Matrix Representation:**
.. math::
\text{Fredkin} =
\begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1
\end{pmatrix}
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 3
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self, nqubit: int = 3, wires: list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False
) -> None:
super().__init__(
name='Fredkin',
nqubit=nqubit,
wires=wires,
controls=None,
condition=False,
den_mat=den_mat,
tsr_mode=tsr_mode,
)
self.register_buffer(
'matrix',
torch.tensor(
[
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1],
]
)
+ 0j,
)
[docs]
def get_unitary(self) -> torch.Tensor:
"""Get the global unitary matrix."""
matrix = self.update_matrix()
identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device)
zerozero = torch.tensor([[1, 0], [0, 0]], dtype=matrix.dtype, device=matrix.device)
zeroone = torch.tensor([[0, 1], [0, 0]], dtype=matrix.dtype, device=matrix.device)
onezero = torch.tensor([[0, 0], [1, 0]], dtype=matrix.dtype, device=matrix.device)
oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device)
lst1 = [identity] * self.nqubit
lst2 = [identity] * self.nqubit
lst3 = [identity] * self.nqubit
lst4 = [identity] * self.nqubit
lst5 = [identity] * self.nqubit
lst1[self.wires[0]] = zerozero
lst2[self.wires[0]] = oneone
lst2[self.wires[1]] = zerozero
lst2[self.wires[2]] = matrix[-4:-2, -4:-2]
lst3[self.wires[0]] = oneone
lst3[self.wires[1]] = zeroone
lst3[self.wires[2]] = matrix[-4:-2, -2:]
lst4[self.wires[0]] = oneone
lst4[self.wires[1]] = onezero
lst4[self.wires[2]] = matrix[-2:, -4:-2]
lst5[self.wires[0]] = oneone
lst5[self.wires[1]] = oneone
lst5[self.wires[2]] = matrix[-2:, -2:]
return multi_kron(lst1) + multi_kron(lst2) + multi_kron(lst3) + multi_kron(lst4) + multi_kron(lst5)
def _qasm(self) -> str:
return f'cswap q[{self.wires[0]}],q[{self.wires[1]}],q[{self.wires[2]}];\n'
[docs]
class UAnyGate(ArbitraryGate):
r"""Arbitrary unitary gate.
Args:
unitary: Any given unitary matrix.
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
minmax: The minimum and maximum indices of the qubits that the quantum operation acts on.
Only valid when ``wires`` is ``None``. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
name: The name of the gate. Default: ``'UAnyGate'``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self,
unitary: Any,
nqubit: int = 1,
wires: int | list[int] | None = None,
minmax: list[int] | None = None,
controls: int | list[int] | None = None,
name: str = 'UAnyGate',
den_mat: bool = False,
tsr_mode: bool = False,
) -> None:
super().__init__(
name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, den_mat=den_mat, tsr_mode=tsr_mode
)
if not isinstance(unitary, torch.Tensor):
unitary = torch.tensor(unitary, dtype=torch.cfloat).reshape(-1, 2 ** len(self.wires))
assert unitary.dtype in (torch.cfloat, torch.cdouble)
assert unitary.shape[-1] == unitary.shape[-2] == 2 ** len(self.wires)
assert is_unitary(unitary)
self.register_buffer('matrix', unitary)
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
if self.inv_mode:
return self.matrix.mH
else:
return self.matrix
[docs]
class LatentGate(ArbitraryGate):
r"""Latent gate.
Args:
inputs: Any given real matrix.
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
minmax: The minimum and maximum indices of the qubits that the quantum operation acts on.
Only valid when ``wires`` is ``None``. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
name: The name of the gate. Default: ``'LatentGate'``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameters are ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
inputs: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
minmax: list[int] | None = None,
controls: int | list[int] | None = None,
name: str = 'LatentGate',
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
super().__init__(
name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, den_mat=den_mat, tsr_mode=tsr_mode
)
self.requires_grad = requires_grad
self.init_para(inputs)
[docs]
def get_matrix(self, inputs: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
latent = self.inputs_to_tensor(inputs) + 0j
u, _, vh = svd(latent)
return u @ vh
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
latent = self.latent.mH if self.inv_mode else self.latent
matrix = self.get_matrix(latent)
assert matrix.shape[-1] == matrix.shape[-2] == 2 ** len(self.wires)
self.matrix = matrix.detach()
return matrix
[docs]
def get_derivative(self, latent: Any) -> torch.Tensor:
"""Get the derivatives of the local unitary matrix."""
latent = self.inputs_to_tensor(latent)
du_dx = jacobian(self._real_wrapper, latent).permute(3, 4, 0, 1, 2)
return du_dx[..., 0] + du_dx[..., 1] * 1j
[docs]
def init_para(self, inputs: Any = None) -> None:
"""Initialize the parameters."""
latent = self.inputs_to_tensor(inputs)
if self.requires_grad:
self.latent = nn.Parameter(latent)
else:
self.register_buffer('latent', latent)
self.update_matrix()
self.npara = self.latent.numel()
[docs]
class HamiltonianGate(ArbitraryGate):
r"""Hamiltonian gate.
Args:
hamiltonian: The Hamiltonian. It can be a list, e.g., ``[[0.5, 'x0y1'], [-1, 'z3y1']]`` for
:math:`0.5 * \sigma^x_0 \otimes \sigma^y_1 - \sigma^y_1 \otimes \sigma^z_3`. It can also be
a torch.Tensor when ``wires`` or ``minmax`` is specified.
t: The evolution time. Default: ``None``
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on.
Only valid when ``hamiltonian`` is not a list. Default: ``None``
minmax: The minimum and maximum indices of the qubits that the quantum operation acts on.
Only valid when ``hamiltonian`` is not a list and ``wires`` is ``None``. Default: ``None``
controls: The indices of the control qubits. Default: ``None``
name: The name of the gate. Default: ``'HamiltonianGate'``
den_mat: Whether the quantum operation acts on density matrices or state vectors.
Default: ``False`` (which means state vectors)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
requires_grad: Whether the parameter is ``nn.Parameter`` or ``buffer``.
Default: ``False`` (which means ``buffer``)
"""
def __init__(
self,
hamiltonian: Any,
t: Any = None,
nqubit: int = 1,
wires: int | list[int] | None = None,
minmax: list[int] | None = None,
controls: int | list[int] | None = None,
name: str = 'HamiltonianGate',
den_mat: bool = False,
tsr_mode: bool = False,
requires_grad: bool = False,
) -> None:
self.nqubit = nqubit
self.ham_lst = None
if isinstance(hamiltonian, list):
self.ham_lst = hamiltonian
wires = None
minmax = self.get_minmax(hamiltonian)
super().__init__(
name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, den_mat=den_mat, tsr_mode=tsr_mode
)
self.npara = 1
self.requires_grad = requires_grad
self.register_buffer('x', PauliX().matrix)
self.register_buffer('y', PauliY().matrix)
self.register_buffer('z', PauliZ().matrix)
self.init_para([hamiltonian, t])
def _apply(self, fn: Any) -> 'HamiltonianGate':
tensors_dict = {}
names = ['x', 'y', 'z', 'ham_tsr']
tensors_dict = {name: tensor for name in names if (tensor := self._buffers.pop(name)) is not None}
super()._apply(fn)
corrected = apply_complex_fix(fn, tensors_dict)
for key, value in corrected.items():
self.register_buffer(key, value)
return self
def _convert_hamiltonian(self, hamiltonian: list) -> list[list]:
"""Convert and check the list representation of the Hamiltonian."""
if len(hamiltonian) == 2 and isinstance(hamiltonian[1], str):
hamiltonian = [hamiltonian]
assert all(isinstance(i, list) for i in hamiltonian), 'Invalid input type'
for pair in hamiltonian:
assert isinstance(pair[1], str), 'Invalid input type'
return hamiltonian
[docs]
def get_minmax(self, hamiltonian: list) -> list[int]:
"""Get ``minmax`` according to the Hamiltonian."""
hamiltonian = self._convert_hamiltonian(hamiltonian)
minmax = [self.nqubit - 1, 0]
for pair in hamiltonian:
wires = pair[1][1::2]
for i in wires:
i = int(i)
if i < minmax[0]:
minmax[0] = i
if i > minmax[1]:
minmax[1] = i
return minmax
[docs]
def get_matrix(self, hamiltonian: Any, t: Any) -> torch.Tensor:
"""Get the local unitary matrix."""
ham, t = self.inputs_to_tensor([hamiltonian, t])
matrix = torch.linalg.matrix_exp(-1j * ham * t)
return matrix
[docs]
def update_matrix(self) -> torch.Tensor:
"""Update the local unitary matrix."""
t = -self.t if self.inv_mode else self.t
matrix = self.get_matrix(self.ham_tsr, t)
assert matrix.shape[-1] == matrix.shape[-2] == 2 ** len(self.wires)
self.matrix = matrix.detach()
return matrix
def _real_wrapper(self, x: Any) -> torch.Tensor:
mat = self.get_matrix(self.ham_tsr, x)
return torch.view_as_real(mat)
[docs]
def get_derivative(self, t: Any) -> torch.Tensor:
"""Get the derivative of the local unitary matrix."""
if not isinstance(t, torch.Tensor):
t = torch.tensor(t, dtype=torch.float)
t = t.squeeze()
du_dx = jacobian(self._real_wrapper, t)
return du_dx[..., 0] + du_dx[..., 1] * 1j
[docs]
def init_para(self, inputs: list | None = None) -> None:
"""Initialize the parameters."""
ham, t = self.inputs_to_tensor(inputs)
self.register_buffer('ham_tsr', ham)
if self.requires_grad:
self.t = nn.Parameter(t)
else:
self.register_buffer('t', t)
self.update_matrix()
[docs]
class Reset(Gate):
r"""Reset.
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
postselect: The postselected value. Default: 0
(``None`` means no postselection, which is not compatible with vmap)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self, nqubit: int = 1, wires: int | list[int] | None = None, postselect: int | None = 0, tsr_mode: bool = False
) -> None:
if wires is None:
wires = list(range(nqubit))
super().__init__(name='Reset', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode)
self.postselect = postselect
[docs]
def op_state(self, x: torch.Tensor) -> torch.Tensor:
"""Perform a forward pass for state vectors."""
if len(self.wires) == self.nqubit:
x = torch.zeros_like(self.vector_rep(x))
x[:, 0] = 1
x = self.tensor_rep(x)
else:
if self.postselect in (0, 1): # compatible with vmap
for wire in self.wires:
pm_shape = list(range(1, self.nqubit + 1))
pm_shape.remove(wire + 1)
pm_shape = [wire + 1] + pm_shape + [0]
x = x.permute(pm_shape) # (2, ..., 2, batch)
probs = (x.abs() ** 2).sum(list(range(1, self.nqubit))) # (2, batch)
mask = 1 - torch.sign(probs[self.postselect]) # (batch)
norm = torch.sqrt(probs[self.postselect] + mask)
state0 = ((1 - mask) * x[self.postselect] + mask * x[1 - self.postselect]) / norm
state1 = torch.zeros_like(state0)
x = torch.stack([state0, state1])
x = x.permute(inverse_permutation(pm_shape))
elif self.postselect is None: # NOT compatible with vmap
wires = sorted(self.wires)
idx_sum = list(range(1, self.nqubit + 1))
for i in wires:
idx_sum.remove(i + 1)
probs = (x.abs() ** 2).sum(idx_sum)
out = []
for i, prob in enumerate(probs):
prob = prob.reshape(-1)
sample = torch.multinomial(prob, 1)[0]
proj = torch.zeros_like(prob)
proj[sample] = 1 / prob[sample] ** 0.5
reset = torch.ones_like(prob).diag_embed()
reset[0, 0] = 0
reset[sample, sample] = 0
reset[0, sample] = 1
mat = reset @ proj.diag_embed() + 0j
out.append(evolve_state(x[i : i + 1], mat, self.nqubit, wires))
x = torch.cat(out)
if not self.tsr_mode:
x = self.vector_rep(x).squeeze(0)
return x
def _qasm(self) -> str:
qasm_lst = []
for wire in self.wires:
qasm_lst.append(f'reset q[{wire}];\n')
return ''.join(qasm_lst)
[docs]
class Barrier(Gate):
"""Barrier.
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
name: The name of the gate. Default: ``'Barrier'``
"""
def __init__(self, nqubit: int = 1, wires: int | list[int] | None = None, name: str = 'Barrier') -> None:
if wires is None:
wires = list(range(nqubit))
super().__init__(name=name, nqubit=nqubit, wires=wires)
self.nancilla = 0
[docs]
def forward(self, x: Any) -> Any:
"""Perform a forward pass."""
return x
def _qasm(self) -> str:
qasm_lst = ['barrier ']
for wire in self.wires:
qasm_lst.append(f'q[{wire}],')
return ''.join(qasm_lst)[:-1] + ';\n'
[docs]
def pattern(self, nodes: list[int], ancilla: list[int]) -> nn.Sequential:
"""Get the MBQC pattern."""
assert len(ancilla) == self.nancilla
self.nodes = nodes
return nn.Sequential()
[docs]
class WireCut(Barrier):
"""Wire Cut.
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: 0
"""
def __init__(self, nqubit: int = 1, wires: int | list[int] = 0) -> None:
super().__init__(name='WireCut', nqubit=nqubit, wires=wires)
[docs]
class Move(DoubleGate):
r"""Move, a two-qubit operation representing a reset of the second qubit followed by a swap.
Args:
nqubit: The number of qubits that the quantum operation acts on. Default: 1
wires: The indices of the qubits that the quantum operation acts on. Default: ``None``
postselect: The postselected value. Default: 0
(``None`` means no postselection, which is not compatible with vmap)
tsr_mode: Whether the quantum operation is in tensor mode, which means the input and output are represented by
a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False``
"""
def __init__(
self, nqubit: int = 2, wires: list[int] | None = None, postselect: int | None = 0, tsr_mode: bool = False
) -> None:
super().__init__(name='Move', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode)
reset = Reset(nqubit=nqubit, wires=self.wires[1], postselect=postselect, tsr_mode=True)
swap = Swap(nqubit=nqubit, wires=self.wires, tsr_mode=True)
self.gates = nn.Sequential(reset, swap)
[docs]
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Perform a forward pass."""
if not self.tsr_mode:
x = self.tensor_rep(x)
x = self.gates(x)
if not self.tsr_mode:
return self.vector_rep(x).squeeze(0)
return x
[docs]
def qpd(self, label: int | None = None) -> 'MoveQPD':
"""Get the quasiprobability-decomposition representation."""
from .qpd import MoveQPD
return MoveQPD(nqubit=self.nqubit, wires=self.wires, label=label, tsr_mode=self.tsr_mode)