from __future__ import annotations
from abc import ABC, abstractmethod
from copy import deepcopy
from typing import Optional
import numpy as np
from scipy.linalg import fractional_matrix_power
from typeguard import typechecked
from mpqp.core.instruction.gates.gate_definition import UnitaryMatrix
from mpqp.core.instruction.instruction import Instruction
from mpqp.tools.generics import Matrix
from mpqp.tools.maths import matrix_eq
[docs]@typechecked
class Gate(Instruction, ABC):
"""Represent a unitary operator acting on qubit(s).
A gate is an measurement and the main component of a circuit. The semantics
of a gate is defined using
:class:`GateDefinition<mpqp.core.instruction.gates.gate_definition.GateDefinition>`.
Args:
targets: List of indices referring to the qubits on which the gate will
be applied.
label: Label used to identify the gate.
"""
def __init__(
self,
targets: list[int],
label: Optional[str] = None,
):
super().__init__(targets, label=label)
[docs] @abstractmethod
def to_matrix(self) -> Matrix:
"""Return the "base" matricial semantics to this gate. Without
considering potential column and row permutations needed if the targets
of the gate are not sorted.
Returns:
A numpy array representing the unitary matrix of the gate.
Example:
>>> gd = UnitaryMatrix(
... np.array([[0, 0, 0, 1], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0]])
... )
>>> CustomGate(gd, [1, 2]).to_matrix()
array([[0, 0, 0, 1],
[0, 1, 0, 0],
[1, 0, 0, 0],
[0, 0, 1, 0]])
>>> SWAP(0,1).to_matrix()
array([[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 1]])
"""
[docs] def inverse(self) -> Gate:
"""Computing the inverse of this gate.
Returns:
The gate corresponding to the inverse of this gate.
Example:
>>> Z(0).inverse()
Z(0)
>>> CustomGate(UnitaryMatrix(np.diag([1,1j])),[0]).inverse().to_matrix()
array([[1.-0.j, 0.-0.j],
[0.-0.j, 0.-1.j]])
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
return CustomGate(
UnitaryMatrix(self.to_matrix().transpose().conjugate()),
self.targets,
self.label,
)
[docs] def is_equivalent(self, other: Gate) -> bool:
"""Determine if the gate in parameter is equivalent to this gate.
The equivalence of two gate is only determined from their matricial
semantics (and thus ignores all other aspects of the gate such as the
target qubits, the label, etc....)
Args:
other: the gate to test if it is equivalent to this gate
Returns:
``True`` if the two gates' matrix semantics are equal.
Example:
>>> X(0).is_equivalent(CustomGate(UnitaryMatrix(np.array([[0,1],[1,0]])),[1]))
True
"""
return matrix_eq(self.to_matrix(), other.to_matrix())
[docs] def power(self, exponent: float) -> Gate:
"""Compute the exponentiation `G^{exponent}` of this gate G.
Args:
exponent: Number representing the exponent.
Returns:
The gate elevated to the exponent in parameter.
Examples:
>>> swap_gate = SWAP(0,1)
>>> (swap_gate.power(2)).to_matrix()
array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])
>>> (swap_gate.power(-1)).to_matrix()
array([[1., 0., 0., 0.],
[0., 0., 1., 0.],
[0., 1., 0., 0.],
[0., 0., 0., 1.]])
>>> (swap_gate.power(0.75)).to_matrix() # not implemented yet
array([[1. +0.j , 0. +0.j ,
0. +0.j , 0. +0.j ],
[0. +0.j , 0.14644661+0.35355339j,
0.85355339-0.35355339j, 0. +0.j ],
[0. +0.j , 0.85355339-0.35355339j,
0.14644661+0.35355339j, 0. +0.j ],
[0. +0.j , 0. +0.j ,
0. +0.j , 1. +0.j ]])
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
return CustomGate(
definition=UnitaryMatrix(
fractional_matrix_power(self.to_matrix(), exponent)
),
targets=self.targets,
label=None if self.label is None else self.label + f"^{exponent}",
)
def tensor_product(self, other: Gate) -> Gate:
"""Compute the tensor product of the current gate.
Args:
other: Second operand of the tensor product.
Returns:
A Gate representing a tensor product of this gate with the gate in
parameter.
Example:
>>> (X(0).tensor_product(Z(0))).to_matrix()
array([[ 0, 0, 1, 0],
[ 0, 0, 0, -1],
[ 1, 0, 0, 0],
[ 0, -1, 0, 0]])
# 3M-TODO: to be implemented, don't trust the code bellow, it's pure experiments
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
gd = UnitaryMatrix(
np.kron(self.to_matrix(), other.to_matrix())
) # self, gate, type="tensor"
# compute the list of qubits that will be targeted by these gates
...
# instantiate the definition
l1 = "g1" if self.label is None else self.label
l2 = "g2" if self.label is None else self.label
return CustomGate(
definition=gd,
targets=[0],
label=f"{l1}⊗{l2}",
)
def _mandatory_label(self, postfix: str = ""):
return "g" + postfix if self.label is None else self.label
[docs] def product(self, other: Gate, targets: Optional[list[int]] = None) -> Gate:
"""Compute the composition of self and the other gate.
Args:
other: Rhs of the product.
targets: Qubits on which this new gate will operate. If not given,
the targets of the two gates multiplied must be the same and the
resulting gate will have this same targets.
Returns:
The product of the two gates concerned.
Example:
>>> (X(0).product(Z(0))).to_matrix()
array([[ 0, -1],
[ 1, 0]])
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
return CustomGate(
definition=UnitaryMatrix(self.to_matrix().dot(other.to_matrix())), # type: ignore
targets=self._check_targets_compatibility(other, targets),
label=f"{self._mandatory_label('1')}×{other._mandatory_label('2')}",
)
[docs] def scalar_product(self, scalar: complex) -> Gate:
"""Multiply this gate by a scalar. It normalizes the subtraction
to ensure it is unitary.
Args:
scalar: The number to multiply the gate's matrix by.
Returns:
The result of the multiplication, the targets of the resulting gate
will be the same as the ones of the initial gate.
Example:
>>> (X(0).scalar_product(1j)).to_matrix()
array([[0.+0.j, 0.+1.j],
[0.+1.j, 0.+0.j]])
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
return CustomGate(
UnitaryMatrix(self.to_matrix() * scalar / abs(scalar)),
targets=self.targets,
label=f"{scalar}×{self._mandatory_label()}",
)
[docs] def minus(self, other: Gate, targets: Optional[list[int]] = None) -> Gate:
"""Compute the subtraction of two gates. It normalizes the subtraction
to ensure it is unitary.
Args:
other: The gate to subtract to this gate.
targets: Qubits on which this new gate will operate. If not given,
the targets of the two gates multiplied must be the same and the
resulting gate will have this same targets.
Returns:
The subtraction of ``self`` and ``other``.
Example:
>>> (X(0).minus(Z(0))).to_matrix()
array([[-0.70710678, 0.70710678],
[ 0.70710678, 0.70710678]])
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
subtraction = self.to_matrix() - other.to_matrix()
return CustomGate(
definition=UnitaryMatrix(subtraction / np.linalg.norm(subtraction, 2)), # type: ignore
targets=self._check_targets_compatibility(other, targets),
label=f"{self._mandatory_label('1')}-{other._mandatory_label('2')}",
)
[docs] def plus(self, other: Gate, targets: Optional[list[int]] = None) -> Gate:
"""Compute the sum of two gates. It normalizes the subtraction
to ensure it is unitary.
Args:
other: The gate to add to this gate.
targets: Qubits on which this new gate will operate. If not given, the targets of the two gates multiplied
must be the same and the resulting gate will have this same targets.
Returns:
The sum of ``self`` and ``other``.
Example:
>>> (X(0).plus(Z(0))).to_matrix()
array([[ 0.70710678, 0.70710678],
[ 0.70710678, -0.70710678]])
"""
from mpqp.core.instruction.gates.custom_gate import CustomGate
addition = self.to_matrix() + other.to_matrix()
return CustomGate(
definition=UnitaryMatrix(addition / np.linalg.norm(addition, 2)), # type: ignore
targets=self._check_targets_compatibility(other, targets),
label=f"{self._mandatory_label('1')}+{other._mandatory_label('2')}",
)
def _check_targets_compatibility(self, other: Gate, targets: Optional[list[int]]):
if targets is None:
if self.targets != other.targets:
raise ValueError(
"Cannot infer what targets to use, please specify them in the "
"`targets` argument."
)
targets = self.targets
if self.nb_qubits != other.nb_qubits:
raise ValueError(
f"Incompatible shapes for gates: respectively {self.nb_qubits} "
f"and {other.nb_qubits} qubits"
)
if len(targets) != self.nb_qubits:
raise ValueError(
f"Incorrect size for targets: size {len(targets)} while it "
f"should be {self.nb_qubits}"
)
return targets
def __add__(self, other: Gate) -> Gate:
return self.plus(other)
def __sub__(self, other: Gate) -> Gate:
return self.minus(other)
[docs]@typechecked
class InvolutionGate(Gate, ABC):
"""Gate who's inverse is itself.
Args:
targets: List of indices referring to the qubits on which the gate will be applied.
label: Label used to identify the gate.
"""
[docs] def inverse(self) -> Gate:
return deepcopy(self)
[docs]@typechecked
class SingleQubitGate(Gate, ABC):
"""Abstract class for gates operating on a single qubit.
Args:
target: Index or referring to the qubit on which the gate will be applied.
label: Label used to identify the gate.
"""
def __init__(self, target: int, label: Optional[str] = None):
Gate.__init__(self, [target], label)
def __repr__(self) -> str:
return f"{type(self).__name__}({self.targets[0]})"
nb_qubits = ( # pyright: ignore[reportIncompatibleMethodOverride, reportAssignmentType]
1
)