"""In some cases, we need to manipulate unitary operations that are not defined
using native gates (by the corresponding unitary matrix for instance). For those
cases, you can use :class:`mpqp.core.instruction.gates.custom_gate.CustomGate`
to add your custom unitary operation to the circuit, which will be decomposed
and executed transparently."""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
from typeguard import typechecked
from mpqp.tools import Matrix
if TYPE_CHECKING:
from qiskit.circuit import Parameter
from mpqp.core.circuit import QCircuit
from mpqp.core.instruction.gates.gate import Gate
from mpqp.core.instruction.gates.gate_definition import UnitaryMatrix
from mpqp.core.languages import Language
[docs]@typechecked
class CustomGate(Gate):
"""Custom gates allow you to define your own unitary gates.
Args:
matrix: The matrix describing the gate.
targets: The qubits on which the gate operates.
label: The label of the gate. Defaults to None.
Raises:
ValueError: the target qubits must be contiguous and in order, and must
match the size of the matrix
ValueError: Target qubits must be ordered and contiguous for a CustomGate.
Example:
>>> u = np.array([[0,-1],[1,0]])
>>> cg = CustomGate(u, [0])
>>> print(run(QCircuit([X(0), cg]), IBMDevice.AER_SIMULATOR))
Result: IBMDevice, AER_SIMULATOR
State vector: [-1, 0]
Probabilities: [1, 0]
Number of qubits: 1
"""
def __init__(
self,
matrix: Matrix,
targets: Union[list[int], int],
label: Optional[str] = None,
):
definition = UnitaryMatrix(matrix)
if isinstance(targets, int):
targets = [targets]
self.definition = definition
"""See parameter description."""
if definition.nb_qubits != len(targets):
raise ValueError(
f"Size of the targets ({len(targets)}) must match the number of qubits of the "
f"UnitaryMatrix ({definition.nb_qubits})"
)
# 3M-TODO: add later the possibility to give non-contiguous and/or non-ordered target qubits for CustomGate,
# use the to_matrix() method inherited from Gate, maybe
super().__init__(targets, label)
@property
def matrix(self) -> Matrix:
# TODO: move this to `to_canonical_matrix` and check for the usages
return self.definition.matrix
[docs] def to_matrix(self, desired_gate_size: int = 0):
return self.matrix
[docs] def to_canonical_matrix(self):
return self.matrix
[docs] def to_other_language(
self,
language: Language = Language.QISKIT,
qiskit_parameters: Optional[set["Parameter"]] = None,
printing: bool = False,
):
if language == Language.QISKIT:
from qiskit.quantum_info.operators import Operator as QiskitOperator
from sympy import Expr
if qiskit_parameters is None:
qiskit_parameters = set()
gate_symbols = set().union(
*(
elt.free_symbols
for elt in self.matrix.flatten()
if isinstance(elt, Expr)
)
)
from mpqp.core.instruction.gates.native_gates import (
_qiskit_parameter_adder, # pyright: ignore[reportPrivateUsage]
)
for symbol in gate_symbols:
if TYPE_CHECKING:
assert isinstance(symbol, Expr)
_qiskit_parameter_adder(symbol, qiskit_parameters)
if len(gate_symbols) > 0:
if not printing:
raise ValueError(
"Custom gates defined with symbolic variables cannot be"
" exported to qiskit."
)
from qiskit import QuantumCircuit
dummy_circuit = QuantumCircuit(self.nb_qubits)
for param in qiskit_parameters:
# Rx is just a random choice so to have the parameter in the
# list of inputs
dummy_circuit.rx(param, 0)
return dummy_circuit.to_gate(label="CustomGate")
return QiskitOperator(self.matrix)
elif language == Language.QASM2:
from qiskit import QuantumCircuit, qasm2
from mpqp.tools.circuit import replace_custom_gate
nb_qubits = max(self.targets) + 1
qiskit_circ = QuantumCircuit(nb_qubits)
instr = self.to_other_language(Language.QISKIT)
if TYPE_CHECKING:
from qiskit.quantum_info.operators import Operator as QiskitOperator
assert isinstance(instr, QiskitOperator)
qiskit_circ.unitary(
instr,
list(reversed(self.targets)), # dang qiskit qubits order
self.label,
)
circuit, gphase = replace_custom_gate(qiskit_circ.data[0], nb_qubits)
qasm_str = qasm2.dumps(circuit)
qasm_lines = qasm_str.splitlines()
instructions_only = [
line
for line in qasm_lines
if not (
line.startswith("qreg")
or line.startswith("include")
or line.startswith("creg")
or line.startswith("OPENQASM")
)
]
return "\n".join(instructions_only), gphase
else:
raise NotImplementedError(f"Error: {language} is not supported")
def __repr__(self) -> str:
label = f", \"{self.label}\"" if self.label else ""
return f"CustomGate({repr(self.matrix)}, {self.targets}{label})"
[docs] def decompose(self) -> "QCircuit":
"""Returns the circuit made of native gates equivalent to this gate.
The circuit follows the quantum Shannon decomposition which decomposes any unitary matrix into Ry,Rz and CNOT gates.
Example:
>>> U = np.array([[0,1], [1,0]])
>>> gate = CustomGate(U, [0])
>>> print(gate.decompose()) # doctest: +NORMALIZE_WHITESPACE
┌─────────┐┌───────┐┌──────────┐
q: ┤ Rz(π/2) ├┤ Ry(π) ├┤ Rz(-π/2) ├
└─────────┘└───────┘└──────────┘
"""
from mpqp.tools.unitary_decomposition import quantum_shannon_decomposition
return quantum_shannon_decomposition(self.matrix)