Source code for qdecomp.utils.qgate

# Copyright 2024-2025 Olivier Romain, Francis Blais, Vincent Girouard, Marius Trudeau
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.

"""
This module contains the QGate class, which represents a quantum gate. It provides methods to easily
create a gate from different representations (matrix, sequence, tuple) and to stock its information
in an intuitive way. The class also contains methods to stock the decomposition of the gate, its
initial representation and its error. Finally, the class provides a method to calculate the matrix
representation of the gate from its sequence.

Classes:
    - :class:`QGate`: Class representing a quantum gate.
"""

from __future__ import annotations

from typing import Any, Callable

import numpy as np

from qdecomp.utils.gates import get_matrix_from_name

__all__ = ["QGate"]


[docs] class QGate: r""" Class representing a quantum gate. It provides the user the ability to easily create and manipulate quantum gates. The gates can be defined either by their matrix, their sequence or a tuple. A Gate object contains the information about the qubit on which it acts, its control bit, if any, and its error if a sequence is used to approximate it. The class also provides a method to calculate the matrix representation of the gate from its sequence. Class methods to instantiate a QGate object: - from_matrix: Create a QGate object from a matrix. - from_sequence: Create a QGate object from a sequence. - from_tuple: Create a QGate object from a tuple. Parameters: name (str | None): Name of the gate. num_qubits (int): Number of qubits on which the gate applies. sequence (str | None): Sequence associated with the gate decomposition. target (tuple[int]): Qubits on which the gate acts. init_matrix (np.ndarray | None): Matrix used to initialize the gate. sequence_matrix (np.ndarray | None): Approximated matrix representation of the gate. epsilon (float): Tolerance for the gate. Methods to change the QGate representation: - to_tuple: Convert the gate to a tuple representation. - convert: Convert the gate by using a user-defined function. - set_decomposition: Set the decomposition of the gate. The decomposition might be an approximation of the initial gate stored in the init_matrix attribute. - calculate_seq_matrix: Calculate the matrix representation of the gate from its sequence. **Example** .. code-block:: python >>> import numpy as np >>> from qdecomp.utils import QGate # Create a gate from a sequence >>> g1 = QGate.from_sequence(sequence="CNOT", target=(1, 3)) >>> print(g1) Sequence: CNOT Target: (1, 3) # Create a gate from a matrix >>> g2 = QGate.from_matrix(matrix=np.diag([1, -1]), name="My_Z_Gate") >>> print(g2) Gate: My_Z_Gate Target: (0,) Init. matrix: [[ 1 0] [ 0 -1]] # Create a gate from a tuple >>> g3 = QGate.from_tuple(("H", (0, ), 0)) >>> print(g3) Sequence: H Target: (0,) # Get the matrix of a from_sequence() gate >>> gate = QGate.from_sequence(sequence="X Z Y", name="my_gate") >>> print("Name:", gate.name) Name: my_gate >>> print("Sequence:", gate.sequence) Sequence: X Z Y >>> print("Target:", gate.target) Target: (0,) >>> print("Matrix:\n", gate.sequence_matrix, "\n") Matrix: [[ 0.+1.j 0.+0.j] [-0.+0.j -0.+1.j]] # Set the approximation sequence of a gate >>> approx_gate = QGate.from_matrix(matrix=np.diag([1-0.001j, 1.j+0.001]), name="approx_my_gate") >>> approx_gate.set_decomposition("S", epsilon=0.01) >>> print("Name:", approx_gate.name) Name: approx_my_gate >>> print("Sequence:", approx_gate.sequence) Sequence: S >>> print("Initial matrix:\n", approx_gate.init_matrix) Initial matrix: [[1. -0.001j 0. +0.j ] [0. +0.j 0.001+1.j ]] >>> print("Matrix from sequence:\n", approx_gate.sequence_matrix) Matrix from sequence: [[1.+0.j 0.+0.j] [0.+0.j 0.+1.j]] """
[docs] def __init__( self, name: str | None = None, target: tuple[int, ...] = (0,), ) -> None: """ Initialize the QGate object. Args: name (str | None): Name of the gate target (tuple[int, ...]): Qubits on which the gate applies. Raises: ValueError: If the target qubits are not a tuple. ValueError: If the target qubits are not integers. ValueError: If the target qubits are not in ascending order. """ # Check if the target is a tuple of integers if not isinstance(target, tuple): raise TypeError(f"The target qubit must be a tuple. Got {target}.") # Check if that target qubit(s) are integers if not all(isinstance(i, int) for i in target): raise TypeError(f"The target qubit must be a tuple of integers. Got {target}.") # Check if the target qubits are in ascending order if not all(target[i] < target[i + 1] for i in range(len(target) - 1)): raise ValueError(f"The target qubits must be in ascending order. Got {target}.") # Populate the attributes self._name = name self._sequence = None self._target = target self._init_matrix = None self._sequence_matrix = None self._epsilon = None
[docs] @classmethod def from_matrix( cls, matrix: np.ndarray, name: str | None = None, target: tuple[int, ...] = (0,), epsilon: float | None = None, ) -> "QGate": """ Create a QGate object from a matrix. Args: matrix (np.ndarray): Matrix representation of the gate. name (str | None): Name of the gate. target (tuple[int, ...]): Qubits on which the gate applies. epsilon (float | None): Tolerance for the gate. Returns: QGate: The QGate object. Raises: ValueError: If the matrix is not unitary. ValueError: If the number of rows of the matrix is not 2^num_of_qubits. **Example** .. code-block:: python >>> import numpy as np >>> from qdecomp.utils import QGate >>> gate = QGate.from_matrix(np.diag([1, 1j]), name="my_S_gate", target=(1, )) >>> print(gate) Gate: my_S_gate Target: (1,) Init. matrix: [[1.+0.j 0.+0.j] [0.+0.j 0.+1.j]] """ # Convert the matrix to a numpy array matrix = np.asarray(matrix) # 2D matrix if matrix.ndim != 2: raise ValueError(f"The input matrix must be a 2D matrix. Got {matrix.ndim} dimensions.") # Square matrix if matrix.shape[0] != matrix.shape[1]: raise ValueError(f"The input matrix must be a square matrix. Got shape {matrix.shape}.") # Unitary matrix if not np.allclose(np.eye(matrix.shape[0]), matrix @ matrix.conj().T): raise ValueError("The input matrix must be unitary.") # Size of the matrix compared to the number of targets and control if matrix.shape[0] != 2 ** len(target): raise ValueError( "The input matrix must have a size of 2^num_of_qubit. Got shape " + f"{matrix.shape} and {len(target)} qubit(s)." ) # Create the gate gate = cls(name=name, target=target) gate._init_matrix = matrix gate._epsilon = epsilon return gate
[docs] @classmethod def from_sequence( cls, sequence: str, name: str | None = None, target: tuple[int, ...] = (0,), ) -> "QGate": """ Create a QGate object from a sequence. Args: sequence (str): Sequence associated with the gate decomposition. target (tuple[int, ...]): Qubits on which the gate applies. Returns: QGate: The QGate object. **Example** .. code-block:: python >>> from qdecomp.utils import QGate >>> h_gate = QGate.from_sequence("H", name="my_H_gate", target=(2, )) >>> print(h_gate) Gate: my_H_gate Sequence: H Target: (2,) >>> cx_gate = QGate.from_sequence("CNOT", name="my_CNOT_gate", target=(1, 3)) >>> print(cx_gate) Gate: my_CNOT_gate Sequence: CNOT Target: (1, 3) """ # Create the gate gate = cls(name=name, target=target) gate._sequence = sequence return gate
[docs] @classmethod def from_tuple(cls, gate_tuple: tuple, name: str | None = None) -> "QGate": r""" Create a QGate object from a tuple. Two tuple formats are allowed: - (sequence, target, epsilon) - (matrix, target, epsilon) In the first case, the epsilon is discarded. Args: gate_tuple (tuple): Tuple representation of the gate. Returns: QGate: The QGate object. Raises: TypeError: If the first elements of the tuple is not a string or a np.ndarray. ValueError: If the tuple does not contain three elements. **Example** .. code-block:: python >>> from qdecomp.utils import QGate # Create a QGate object from a sequence >>> init_gate = QGate.from_sequence("H", name="my_H_gate", target=(2, )) >>> print(init_gate) Gate: my_H_gate Sequence: H Target: (2,) # Get the tuple representation of the QGate object >>> tup = init_gate.to_tuple() >>> print(tup, "\n") ('H', (2,), 0) # Reconstruct the QGate object from the tuple >>> tup_gate = QGate.from_tuple(tup) >>> print(tup_gate) Sequence: H Target: (2,) """ if len(gate_tuple) != 3: raise ValueError("The tuple must contain three elements.") first = gate_tuple[0] if isinstance(first, str): return cls.from_sequence(sequence=first, name=name, target=gate_tuple[1]) elif isinstance(first, np.ndarray): return cls.from_matrix( matrix=first, name=name, target=gate_tuple[1], epsilon=gate_tuple[2] ) else: raise TypeError( f"The first element of the tuple must be a string or a np.ndarray. Got {type(first)}." )
@property def name(self) -> str | None: """ Get the name of the gate. Returns: str | None: The name of the gate. """ return self._name @property def sequence(self) -> str | None: """ Get the sequence associated with the gate decomposition. Returns: str | None: The sequence associated with the gate decomposition. """ return self._sequence @property def target(self) -> tuple[int]: """ Get the target qubit(s). Returns: tuple[int]: The target qubit(s). """ return self._target @property def init_matrix(self) -> np.ndarray: """ Return the matrix used to initialize the gate. Returns: np.ndarray: Matrix representation of the initialization gate. """ return self._init_matrix @property def sequence_matrix(self) -> np.ndarray: """ Get the matrix representation of the gate given by its sequence. If the gate is initialized with a matrix (obtained with the `init_matrix` property), and then its sequence is specified, the sequence matrix represents the matrix obtained by multiplying the gates in the sequence. Returns: np.ndarray: Approximated matrix representation of the gate. """ # Calculate the matrix if it is not already known if self._sequence_matrix is None: self._calculate_seq_matrix() return self._sequence_matrix @property def matrix(self) -> np.ndarray: """ Get a matrix representation of the gate. If the sequence is known, the matrix associated to the sequence is returned, as the `sequence_matrix` property does. If the sequence is not known, the matrix used to initialize the gate (accessed via the `init_matrix` property) is returned. Returns: np.ndarray: Matrix representation of the gate. """ if self.sequence is not None: return self.sequence_matrix else: return self.init_matrix @property def num_qubits(self) -> int: """ Get the number of qubits on which the gate applies. Returns: int: The number of qubits on which the gate applies. """ return len(self._target) @property def epsilon(self) -> float | None: """ Get the tolerance for the gate. Returns: float | None: The tolerance for the gate. """ return self._epsilon def __str__(self) -> str: """ Convert the gate to a string representation. Returns: str: The string representation of the gate. """ string = "" if self.name is not None: string += "Gate: " + self.name + "\n" if self.sequence is not None: string += "Sequence: " + self.sequence + "\n" string += "Target: " + str(self.target) + "\n" if self.epsilon is not None: string += "Epsilon: " + str(self.epsilon) + "\n" if self._init_matrix is not None: string += "Init. matrix:\n" + str(self.init_matrix) + "\n" if self._sequence_matrix is not None: string += "Seq. matrix:\n" + str(self.sequence_matrix) + "\n" return string
[docs] def to_tuple(self) -> tuple: """ Convert the gate to a tuple representation. Returns: tuple: The tuple representation of the gate. Raises: ValueError: If the sequence is not initialized. """ # Test if the sequence is initialized if self.sequence is None: raise ValueError("The sequence must be initialized to convert the gate to a tuple.") # Convert the gate to a tuple epsilon = self.epsilon if self.epsilon is not None else 0 return (self.sequence, self.target, epsilon)
[docs] def set_decomposition(self, sequence: str, epsilon: float | None = None) -> None: """ Set the decomposition of the gate. This decomposition doesn't need to be exact. The error epsilon specifies the error made by approximating the initial gate by the sequence and the exact matrix representation is stored in the approx_matrix attribute. Args: sequence (str): The decomposition of the gate. epsilon (float | None): The tolerance for the gate. If None, the value of the epsilon attribute is used. Raises: ValueError: If the sequence is already initialized. ValueError: If the epsilon is not already initialized for the gate and not provided as an argument. """ # Reinitialize the _sequence_matrix attribute if the sequence was already specified if self.sequence is not None: self._sequence_matrix # Check if epsilon is defined in the gate or specified as an argument, and set it if necessary if epsilon is None: if self.epsilon is None: # pragma: no branch raise ValueError("The epsilon must be initialized.") else: self._epsilon = epsilon # Set the sequence self._sequence = sequence
def _calculate_seq_matrix(self) -> None: """ Calculate the matrix representation of the gate from its sequence. The result can be accessed using the `sequence_matrix` attribute. Raises: ValueError: If the sequence_matrix is already known. ValueError: If the sequence is not initialized. ValueError: If the sequence contains a gate that applies on the wrong number of qubits. """ # Check if the matrix is not already known if self._sequence_matrix is not None: raise ValueError("The sequence_matrix is already known.") # Check if the sequence is initialized if self.sequence is None: raise ValueError("The sequence must be initialized.") # Calculate the matrix matrix_shape = 2**self.num_qubits matrix = np.eye(matrix_shape, dtype=complex) for name in self.sequence.split(" "): simple_matrix = get_matrix_from_name(name) # If the simple_matrix is a scalar (global phase) if not isinstance(simple_matrix, np.ndarray): matrix = simple_matrix * matrix continue # Check if the simple_matrix has the right shape if simple_matrix.shape[0] != matrix_shape: raise ValueError( f"The sequence contains a gate that applies on the wrong number of qubits: {name}." ) matrix = simple_matrix @ matrix # Store the matrix and the qubits on which the gate applies self._sequence_matrix = matrix