# Project: Qhronology (https://github.com/lgbishop/qhronology)
# Author: lgbishop <lgbishop@protonmail.com>
# Copyright: Lachlan G. Bishop 2025
# License: AGPLv3 (non-commercial use), proprietary (commercial use)
# For more details, see the README in the project repository:
# https://github.com/lgbishop/qhronology,
# or visit the website:
# https://qhronology.org.
"""
Classes for the creation of quantum gates.
"""
# https://peps.python.org/pep-0649/
# https://peps.python.org/pep-0749/
from __future__ import annotations
import copy
import itertools
from typing import Any
import numpy as np
import sympy as sp
from qhronology.mechanics.matrices import bra, ket, quantum_object
from qhronology.mechanics.operations import densify
from qhronology.utilities.classification import Forms, Kinds, arr, expr, mat, num, sym
from qhronology.utilities.diagrams import Families
from qhronology.utilities.helpers import (
apply_substitutions,
arrange,
cast,
check_systems_conflicts,
conjugate_transpose,
count_systems,
default_arguments,
extract_representation,
fix_arguments,
flatten_list,
generate_identity,
generate_zeros,
matrix_multiplication,
permute_tensor_product,
recursively_simplify,
stringify,
symbolize_substitutions,
symbolize_expression,
tensor_product,
to_array,
to_matrix,
to_numerical,
)
from qhronology.utilities.objects import QuantumObject
[docs]
class QuantumGate(QuantumObject):
"""A class for creating quantum gates and storing their metadata.
This class forms the base upon which all quantum gates are built.
Instances of this base class and its derivatives (subclasses) provide complete descriptions of quantum gates.
This means that they describe a complete vertical column (sometimes called a "slice", "layer", or "cycle") in the quantum circuitry picturalism, including control nodes, anticontrol nodes, empty wires, and the (unitary) gate operator itself.
The details of any algebraic symbols, mathematical substitutions, and visualization labels are also recorded.
Note that, unlike the internal matrix representations contained within instances of the :py:class:`~qhronology.quantum.states.QuantumState` class (and its derivatives), the matrix representations of subclass instances of :py:class:`~qhronology.quantum.gates.QuantumGate` are *not* mutable.
Arguments
---------
spec : mat | arr | list[list[num | expr | str]]
The specification of the quantum gate's matrix representation in a standard :python:`dim`-dimensional basis.
Can be one of:
- a SymPy matrix (:python:`mat`)
- a NumPy array (:python:`arr`)
- a list of lists of numerical, symbolic, or string expressions that collectively describe a matrix (:python:`list[list[num | expr | str]]`)
Defaults to the single-system :python:`dim`-dimensional identity operator.
targets : list[int]
The numerical indices of the subsystems on which the gate's core operator resides.
Defaults to :python:`list(range(0, count_systems(to_matrix(spec), dim)))`.
controls : list[int]
The numerical indices of the subsystems on which control nodes reside.
Defaults to :python:`[]`.
anticontrols : list[int]
The numerical indices of the subsystems on which anticontrol nodes reside.
Defaults to :python:`[]`.
num_systems : int
The (total) number of systems which the gate spans.
Must be a non-negative integer.
Defaults to :python:`max(targets + controls + anticontrols + [count_systems(to_matrix(spec), dim)]) + 1`.
dim : int
The dimensionality of the quantum gate's Hilbert space.
Must be a non-negative integer.
Defaults to :python:`2`.
numerical : bool
Whether to cast the gate's matrix elements as floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to :python:`False`.
array : bool
Whether to cast the gate's matrix as a NumPy array (:python:`True`) or SymPy matrix (:python:`False`).
Defaults to :python:`False`.
symbols : dict[sym | str, dict[str, Any]]
A dictionary in which the keys are individual symbols (usually found within the gate specification :python:`spec`) and the values are dictionaries of their respective SymPy keyword-argument :python:`assumptions`.
Defaults to :python:`{}`.
substitutions : list[tuple[num | expr | str, num | expr | str]]
A list of 2-tuples of substitutions to be applied to the gate.
All instances of the expression in each tuple's first element are replaced by the expression in the respective second element.
This uses the same format as the SymPy :python:`subs()` method.
Defaults to :python:`[]`.
simplification : bool
Whether to perform mathematical simplification on the gate when it is called.
If :python:`False`, does not simplify.
Defaults to :python:`False`.
conjugation : bool
Whether to perform Hermitian conjugation on the gate when it is called.
If :python:`False`, does not conjugate.
Defaults to :python:`False`.
exponent : num | expr | str
A numerical or string representation of a scalar value to which gate's operator (residing on :python:`targets`) is exponentiated.
Must be a non-negative integer.
Defaults to :python:`1`.
coefficient : num | expr | str
A numerical or string representation of a scalar value by which the gate's matrix (occupying :python:`targets`) is multiplied.
Performed after exponentiation.
Useful for multiplying the gate by a phase factor.
Defaults to :python:`1`.
label : str
The unformatted string used to represent the gate in mathematical expressions.
Defaults to :python:`"U"`.
notation : str
The formatted string used to represent the gate in mathematical expressions.
When not :python:`None`, overrides the value passed to :python:`label`.
Not intended to be set by the user in most cases.
Defaults to :python:`None`.
family : str
A string expressing the kind of block element for which the gate is to be visualized.
Not intended to be set by the user.
Defaults to :python:`"GATE"`.
Note
----
The indices specified in :python:`targets`, :python:`controls`, and :python:`anticontrols` must be distinct.
"""
INVOLUTORY = False
def __init__(
self,
spec: mat | arr | list[list[num | expr | str]] | None,
targets: list[int] | None = None,
controls: list[int] | None = None,
anticontrols: list[int] | None = None,
num_systems: int | None = None,
dim: int | None = None,
numerical: bool | None = None,
array: bool | None = None,
symbols: dict | None = None,
substitutions: list[tuple[num | expr | str, num | expr | str]] | None = None,
simplification: bool | None = None,
conjugation: bool | None = None,
exponent: num | expr | str | None = None,
coefficient: num | expr | str | None = None,
label: str | None = None,
notation: str | None = None,
family: str | None = None,
):
dim = 2 if dim is None else dim
targets = [] if targets is None else targets
controls = [] if controls is None else controls
anticontrols = [] if anticontrols is None else anticontrols
num_systems_spec = 0
if spec is None:
spec = sp.eye(dim)
if isinstance(spec, list) is True:
num_systems_spec = count_systems(to_matrix(spec), dim)
else:
num_systems_spec = count_systems(spec, dim)
if num_systems is None:
try:
num_systems = max(num_systems_spec, max(targets + controls + anticontrols) + 1)
except:
num_systems = max(num_systems_spec, 1)
targets = list(range(0, num_systems_spec)) if len(targets) == 0 else targets
exponent = 1 if exponent is None else exponent
coefficient = 1 if coefficient is None else coefficient
label = "U" if label is None else label
family = Families.GATE.value if family is None else family
# Automatically resize.
num_systems = max(flatten_list([num_systems, targets, controls, anticontrols]))
self.targets = targets
self.controls = controls
self.anticontrols = anticontrols
self.exponent = exponent
self.coefficient = coefficient
QuantumObject.__init__(
self,
spec=spec,
form=Forms.MATRIX.value,
kind=Kinds.MIXED.value,
dim=dim,
numerical=numerical,
num_systems=num_systems,
array=array,
symbols=symbols,
substitutions=substitutions,
simplification=simplification,
conjugation=conjugation,
label=label,
notation=notation,
family=family,
debug=False,
)
# Repeat property assignments to check for incompatibilities
self.targets = targets
self.controls = controls
self.anticontrols = anticontrols
@property
def is_vector(self) -> bool:
return False
@property
def form(self) -> str:
return Forms.MATRIX.value
@form.setter
def form(self, form: str):
pass
@property
def current(self) -> mat | arr:
"""The current (unprocessed) matrix representation of the quantum gate."""
return quantum_object(
spec=self.spec,
form=self.form,
kind=self.kind,
dim=self.dim,
numerical=self.numerical,
array=self.array,
)
@current.setter
def current(self, current: mat | arr):
pass
@property
def targets(self) -> list[int]:
"""The numerical indices of the subsystems on which the gate's core operator resides."""
return list(set(self._targets))
@targets.setter
def targets(self, targets: list[int]):
if hasattr(self, "_spec") is True and type(self) == QuantumGate:
if hasattr(self, "_targets") is True and len(self.targets) != self.num_systems_spec:
raise ValueError(
f"""The number of systems of the gate's specification ({self.num_systems_spec}) does not match the number of systems of its targets ({len(self.targets)})."""
)
if (
hasattr(self, "_controls") is True
and hasattr(self, "_anticontrols") is True
):
if (
check_systems_conflicts(targets, self.controls, self.anticontrols)
is True
):
raise ValueError(
"""The `targets`, `controls`, and `anticontrols` lists cannot have any elements in common."""
)
self._targets = targets
@property
def controls(self) -> list[int]:
"""The numerical indices of the subsystems on which control nodes reside.
For example, a controlled-:math:`\\Unitary` gate in :math:`\\Dimension` dimensions takes the form
.. math::
\\begin{aligned}
\\Control^{\\indices{0}} \\Unitary^{\\indices{1}} &= \\sum\\limits_{k=0}^{\\Dimension - 1} \\ket{k}\\bra{k}\\otimes\\Unitary^{k} \\\\
&= \\ket{0}\\bra{0}\\otimes\\Identity + \\ket{1}\\bra{1}\\otimes\\Unitary
+ \\ket{2}\\bra{2}\\otimes\\Unitary^{2} + \\ldots
+ \\ket{\\Dimension - 1}\\bra{\\Dimension - 1}\\otimes\\Unitary^{\\Dimension - 1}
\\end{aligned}
"""
return list(set(self._controls))
@controls.setter
def controls(self, controls: list[int]):
controls = flatten_list(list(controls))
if hasattr(self, "_controls") is False:
self._controls = []
if hasattr(self, "_anticontrols") is False:
self._anticontrols = []
if check_systems_conflicts(self.targets, controls, self.anticontrols) is True:
raise ValueError(
"""The `targets`, `controls`, and `anticontrols` lists cannot have any elements in common."""
)
self._controls = sorted(list(set(controls)))
@property
def anticontrols(self) -> list[int]:
"""The numerical indices of the subsystems on which anticontrol nodes reside.
For example, an anticontrolled-:math:`\\Unitary` gate in :math:`\\Dimension` dimensions takes the form
.. math::
\\begin{aligned}
\\Anticontrol^{\\indices{0}} \\Unitary^{\\indices{1}} &= \\sum\\limits_{k=0}^{\\Dimension - 1} \\ket{k}\\bra{k}\\otimes\\Unitary^{\\Dimension - 1 - k} \\\\
&= \\ket{0}\\bra{0}\\otimes\\Unitary^{\\Dimension - 1} + \\ket{1}\\bra{1}\\otimes\\Unitary^{\\Dimension - 2}
+ \\ket{2}\\bra{2}\\otimes\\Unitary^{\\Dimension - 3} + \\ldots
+ \\ket{\\Dimension - 1}\\bra{\\Dimension - 1}\\otimes\\Identity
\\end{aligned}
"""
return list(set(self._anticontrols))
@anticontrols.setter
def anticontrols(self, anticontrols: list[int]):
anticontrols = flatten_list(list(anticontrols))
if hasattr(self, "_controls") is False:
self._controls = []
if hasattr(self, "_anticontrols") is False:
self._anticontrols = []
if check_systems_conflicts(self.targets, self.controls, anticontrols) is True:
raise ValueError(
"""The `targets`, `controls`, and `anticontrols` lists cannot have any elements in common."""
)
self._anticontrols = sorted(list(set(anticontrols)))
@property
def num_systems(self) -> int:
"""The number of systems that the gate spans.
Must be a non-negative integer."""
return max(self._num_systems, max(self.targets + self.controls + self.anticontrols) + 1)
@num_systems.setter
def num_systems(self, num_systems: int):
if num_systems < max(self.targets + self.controls + self.anticontrols) + 1:
raise ValueError(
f"""The specified number of systems ({num_systems}) is smaller than the largest index given in `targets`, `controls`, and `anticontrols`."""
)
self._num_systems = num_systems
@property
def boundaries(self) -> list[int]:
"""An ordered list of indices of the object's boundaries corresponding to its :python:`labels`.
Used exclusively by the visualization engine."""
return [max(flatten_list([self.targets, self.controls, self.anticontrols]))]
@property
def exponent(self) -> num | expr | str:
"""A numerical or string representation of a scalar value specifying the value to which the gate's matrix representation is exponentiated.
For any gate with a matrix representation :math:`\\op{A}` that is involutory, i.e., :math:`\\op{A}^2 = \\Identity` (where :math:`\\Identity` is the identity matrix), its exponentiation is performed as described below. First, we have the identity,
.. math::
\\exp[\\eye z \\op{A}] = \\cos(z)\\Identity + \\eye\\sin(z)\\op{A},
for any :math:`z \\in \\Complexes`. In the case of :math:`z = -\\frac{\\pi}{2}`, this becomes
.. math::
\\exp\\Bigl[-\\eye\\frac{\\pi}{2}\\op{A}\\Bigr] = -\\eye\\op{A},
which can be rearranged to give
.. math::
\\begin{aligned}
\\op{A} &= \\eye \\exp\\Bigl[-\\eye\\frac{\\pi}{2}\\op{A}\\Bigr] \\\\
&= \\exp\\Bigl[\\eye\\frac{\\pi}{2}\\Bigr] \\cdot
\\exp\\Bigl[-\\eye\\frac{\\pi}{2}\\op{A}\\Bigr].
\\end{aligned}
Simply taking this expression to an arbitrary power :math:`p \\in \\mathbb{C}` thus yields the identity
.. math::
\\begin{aligned}
\\op{A}^p &= \\exp\\Bigl[\\eye\\frac{\\pi}{2} p\\Bigr] \\cdot
\\exp\\Bigl[-\\eye\\frac{\\pi}{2} p \\op{A}\\Bigr] \\\\
&= \\exp\\Bigl[\\eye\\frac{\\pi}{2} p\\Bigr]
\\Bigl[\\cos\\Bigl(\\frac{\\pi}{2} p\\Bigr) \\Identity
- \\eye \\sin\\Bigl(\\frac{\\pi}{2} p\\Bigr) \\op{A}\\Bigr] \\\\
&= \\frac{1 + \\e^{\\eye \\pi p}}{2} \\Identity +
\\frac{1 - \\e^{\\eye \\pi p}}{2} \\op{A}.
\\end{aligned}
"""
return self._exponent
@exponent.setter
def exponent(self, exponent: num | expr | str):
self._exponent = exponent
@property
def coefficient(self) -> num | expr | str:
"""A numerical or string representation of a scalar value by which the gate's matrix (occupying :python:`targets`) is multiplied."""
return self._coefficient
@coefficient.setter
def coefficient(self, coefficient: num | expr | str):
self._coefficient = coefficient
[docs]
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
"""Compute the unprocessed matrix representation of the gate.
Arguments
---------
numerical : bool
Whether to cast the matrix elements as floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to the value of :python:`self.numerical`.
array : bool
Whether to cast the matrix as a NumPy array (:python:`True`) or SymPy matrix (:python:`False`).
Defaults to the value of :python:`self.array`.
Returns
-------
mat | arr
The unprocessed matrix representation of the gate.
"""
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = cast(self.current, numerical=numerical, array=array_intermediate)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
matrix = tensor_product(operator, *[identity] * (self.num_systems - len(self.targets)))
permutation = self.systems
for i, t in enumerate(self.targets):
permutation[i], permutation[t] = permutation[t], permutation[i]
matrix = permute_tensor_product(matrix, permutation, self.dim)
return cast(matrix, numerical=numerical, array=array)
[docs]
def output(
self,
numerical: bool | None = None,
array: bool | None = None,
substitutions: list[tuple[num | expr | str, num | expr | str]] | None = None,
simplification: bool | None = None,
conjugation: bool | None = None,
exponent: bool | num | expr | str | None = None,
coefficient: bool | num | expr | str | None = None,
) -> mat | arr:
"""Compute the processed matrix representation of the constructed gate (including any controls, anticontrols, and empty systems).
Arguments
---------
numerical : bool
Whether to cast the matrix elements as floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to the value of :python:`self.numerical`.
array : bool
Whether to cast the matrix as a NumPy array (:python:`True`) or SymPy matrix (:python:`False`).
Defaults to the value of :python:`self.array`.
substitutions : list[tuple[num | expr | str, num | expr | str]]
Algebraic substitutions to be applied to the gate.
Defaults to the value of :python:`self.substitutions`.
simplification : bool
Whether to perform mathematical simplification on the gate.
If :python:`False`, does not simplify.
Defaults to the value of :python:`self.simplification`.
conjugation : bool
Whether to perform Hermitian conjugation on the gate.
If :python:`False`, does not conjugate.
Defaults to the value of :python:`self.conjugation`.
exponent : bool | num | expr | str
The scalar value by which the gate's matrix representation is exponentiated.
If :python:`False`, does not exponentiate.
Defaults to the value of :python:`self.exponent`.
coefficient : bool | num | expr | str
The scalar value by which the gate's matrix representation is multiplied.
If :python:`False`, does not multiply the gate by the coefficient.
Defaults to the value of :python:`self.coefficient`.
Returns
-------
mat | arr
The processed matrix representation of the constructed gate.
"""
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
gate = self.matrix(numerical=numerical, array=array_intermediate)
# Exponentiate
if exponent is None or exponent is True:
exponent = self.exponent
if exponent != 1 and exponent is not False:
exponent = symbolize_expression(exponent, self.symbols_list)
if self.INVOLUTORY is True:
plus = to_numerical(
(1 + sp.exp(sp.I * sp.pi * exponent)) / 2, numerical=numerical
)
minus = to_numerical(
(1 - sp.exp(sp.I * sp.pi * exponent)) / 2, numerical=numerical
)
identity = generate_identity(
self.dim**self.num_systems,
numerical=numerical,
array=array_intermediate,
)
gate = plus * identity + minus * gate
else:
exponent = to_numerical(exponent, numerical=numerical)
try:
gate = gate**exponent
except:
gate = to_matrix(gate)
gate = gate**exponent
gate = cast(gate, numerical=numerical, array=array_intermediate)
# Coefficient
if coefficient is None or coefficient is True:
coefficient = self.coefficient
if coefficient is not False:
coefficient = symbolize_expression(coefficient, self.symbols_list)
else:
coefficient = 1
if coefficient != 1 and coefficient is not False:
coefficient = to_numerical(coefficient, numerical=numerical)
gate = coefficient * gate
gate = symbolize_expression(gate, self.symbols_list)
controllers = self.controls + self.anticontrols
if len(controllers) > 0:
operator = gate
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
for n in controllers:
controller_compliment = list(set(self.systems) ^ set([n]))
matrix = generate_zeros(
self.dim**self.num_systems,
numerical=numerical,
array=array_intermediate,
)
for k in range(0, self.dim):
controller = identity
if n in self.controls:
controller = ket(
k,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
) * bra(
k,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
)
if n in self.anticontrols:
controller = ket(
self.dim - 1 - k,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
) * bra(
self.dim - 1 - k,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
)
ordered = arrange(
[controller_compliment, [n]], [identity] + [controller]
)
controlling = tensor_product(*ordered)
if numerical is False:
operator_power = cast(
to_matrix(operator) ** k,
numerical=numerical,
array=array_intermediate,
)
else:
operator_power = np.linalg.matrix_power(operator, k)
controlling = cast(
controlling, numerical=numerical, array=array_intermediate
)
operator_power = cast(
operator_power, numerical=numerical, array=array_intermediate
)
matrix = matrix + matrix_multiplication(controlling, operator_power)
operator = matrix
gate = matrix
# Substitutions
substitutions = self.substitutions if substitutions is None else substitutions
substitutions = symbolize_substitutions(substitutions, self.symbols_list)
gate = apply_substitutions(gate, substitutions)
# Simplification
simplification = self.simplification if simplification is None else simplification
if simplification is True:
gate = recursively_simplify(gate, substitutions)
# Conjugation
conjugation = self.conjugation if conjugation is None else conjugation
if conjugation is True:
gate = conjugate_transpose(gate)
return cast(gate, numerical=numerical, array=array)
[docs]
def print(
self,
delimiter: str | None = None,
product: bool | None = None,
return_string: bool | None = None,
numerical: bool | None = None,
substitutions: list[tuple[num | expr | str, num | expr | str]] | None = None,
simplification: bool | None = None,
conjugation: bool | None = None,
exponent: bool | num | expr | str | None = None,
coefficient: bool | num | expr | str | None = None,
) -> None | str:
"""Print or return a mathematical expression of the quantum gate as a string.
Note that this method is essentially a wrapper on the :py:meth:`~qhronology.quantum.gates.QuantumGate.output` method, and so includes its arguments.
Arguments
---------
delimiter : str
A string containing the character(s) with which to delimit (i.e., separate) the values in the ket and/or bra terms in the mathematical expression.
Defaults to :python:`","`.
product : bool
Whether to represent the mathematical expression using tensor products.
Only applies if the gate is a multipartite composition.
Defaults to :python:`False`.
return_string : bool
Whether to return the mathematical expression as a string.
Defaults to :python:`False`.
numerical : bool
Whether to cast the matrix elements as floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to the value of :python:`self.numerical`.
substitutions : list[tuple[num | expr | str, num | expr | str]]
Algebraic substitutions to be applied to the gate.
Defaults to the value of :python:`self.substitutions`.
simplification : bool
Whether to perform mathematical simplification on the gate.
If :python:`False`, does not simplify.
Defaults to the value of :python:`self.simplification`.
conjugation : bool
Whether to perform Hermitian conjugation on the gate.
If :python:`False`, does not conjugate.
Defaults to the value of :python:`self.conjugation`.
exponent : bool | num | expr | str
The scalar value by which the gate is exponentiated.
If :python:`False`, does not exponentiate.
Defaults to the value of :python:`self.exponent`.
coefficient : bool | num | expr | str
The scalar value by which the gate is multiplied.
If :python:`False`, does not multiply the gate by the coefficient.
Defaults to the value of :python:`self.coefficient`.
Returns
-------
None
Returned if :python:`return_string` is :python:`False`.
str
The constructed mathematical expression. Returned if :python:`return_string` is :python:`True`.
"""
expression = (
str(self.notation)
+ " = "
+ stringify(
self.output(
numerical=numerical,
substitutions=substitutions,
simplification=simplification,
conjugation=conjugation,
exponent=exponent,
coefficient=coefficient,
),
dim=self.dim,
delimiter=delimiter,
product=product,
)
)
if return_string is True:
return expression
else:
print(expression)
[docs]
def decompose(
self,
gates: list[QuantumGate],
additional_nodes: tuple[int, int, int] | None = None,
depth: int | list[int] | None = None,
num_find: int | None = None,
only_targets: bool | None = None,
include_empty: bool | None = None,
preserve_structure: bool | None = None,
numerical: bool | None = None,
array: bool | None = None,
) -> list[list[QuantumGate]]:
"""Decompose the gate into one or more sequences of gates of a specified set. By default (:python:`only_targets = True`), only the component of the gate that lies on the its target systems, i.e., the operator returned by the :py:meth:`~qhronology.quantum.gates.QuantumGate.matrix` method, is decomposed. If decomposing either :py:class:`~qhronology.quantum.gates.GateStack` or :py:class:`~qhronology.quantum.gates.GateInterleave`, the :python:`only_targets` argument is forced to :python:`False`.
Arguments
---------
gates : list[QuantumGate]
A list of :py:class:`~qhronology.quantum.gates.QuantumGate` instances with which the gate is to be decomposed.
Can include any subclass of :py:class:`~qhronology.quantum.gates.QuantumGate`, including user-defined gates.
The order in which the gates are given in the list can (in the case of multiple possible decompositions) change the order in which the sequences are found.
Note that not every gate given in this list will necessarily appear in any found sequence.
additional_nodes : tuple[int, int, int]
A 3-tuple of non-negative integers specifying the maximum number of additional target, control, and anticontrol nodes to endow to each gate in :python:`gates`.
Defaults to :python:`(0, 0, 0)`.
depth : int | list[int]
The circuit depth up to which the decomposition is performed.
Must be a positive integer or a list of positive integers.
Use the type :python:`list[int]` to search for decompositions at specific circuit depths.
If :python:`0`, begins the search at a depth of 1 and increases indefinitely.
Defaults to :python:`0`.
num_find : int
The (maximum) number of sequences to find. If :python:`0`, exhaustively searches all possible sequences within the search space.
Defaults to :python:`1`.
only_targets : bool
Whether to decompose only the component of the gate on the targets (:python:`True`), or alternatively the entire gate (:python:`False`), including all control and anticontrol nodes.
Forced to :python:`False` if decomposing :py:class:`~qhronology.quantum.gates.GateStack` or :py:class:`~qhronology.quantum.gates.GateInterleave`, otherwise defaults to :python:`True`.
include_empty : bool
Whether to include empty systems (empty wires) between the first and last target systems in the decomposition.
Defaults to :python:`False`.
preserve_structure : bool
Whether to preserve the node structure (i.e., the relative positions of the targets, controls, and anticontrols) of the gates given in :python:`gates`.
Defaults to :python:`False`.
numerical : bool
Whether to perform the decomposition numerically with floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to the value of :python:`self.numerical`.
array : bool
Whether to perform the decomposition using NumPy arrays (:python:`True`) or SymPy matrices (:python:`False`).
Defaults to the value of :python:`self.array`.
Returns
-------
list[list[QuantumGate]]
A list of found decomposition seqeuences. Will be empty if no sequences are found.
Note
----
Currently, this method decomposes any given gate using brute force, which is *extremely* inefficient. The speed at which decompositions are found depends entirely on the size of the search space, which increases exponentially with:
- The number of gates in :python:`gates`
- The number of additional target, control, and anticontrol nodes specified in :python:`additional_nodes`
- The search depth
- The number of systems
- The gate dimensionality
Future work on this method could involve the implementation of heuristics with which the decomposition can be performed more efficiently.
"""
additional_nodes = (0, 0, 0) if additional_nodes is None else additional_nodes
depth = 0 if depth is None else depth
num_find = 1 if num_find is None else num_find
only_targets = True if only_targets is None else only_targets
only_targets = False if isinstance(self, GateStack | GateInterleave) is True else only_targets
include_empty = False if include_empty is None else include_empty
preserve_structure = False if preserve_structure is None else preserve_structure
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
if only_targets is True:
total_gate = copy.deepcopy(self)
total_gate_targets = total_gate.targets
total_gate_controls = total_gate.controls
total_gate_anticontrols = total_gate.anticontrols
total_gate.controls = []
total_gate.anticontrols = []
total_gate_targets_adjusted = [
target - min(total_gate_targets) for target in total_gate_targets
]
total_gate_controls_adjusted = [
control - min(total_gate_targets) for control in total_gate_controls
]
total_gate_anticontrols_adjusted = [
anticontrol - min(total_gate_targets) for anticontrol in total_gate_anticontrols
]
total_gate.targets = total_gate_targets_adjusted
total_gate.num_systems = max(total_gate.targets) + 1
operator = total_gate.output(numerical=numerical, array=array)
systems = total_gate.targets
if include_empty is True:
systems = list(
range(min(total_gate_targets_adjusted), max(total_gate_targets_adjusted) + 1)
)
num_systems = max(systems) - min(systems) + 1
else:
operator = self.output(numerical=numerical, array=array)
num_systems = self.num_systems
systems = list(range(0, num_systems))
total_gate_targets_adjusted = systems
total_gate_controls_adjusted = []
total_gate_anticontrols_adjusted = []
if any(gate.num_systems > num_systems for gate in gates) is True:
raise AssertionError(
"""The gate cannot be decomposed using gates which possess more systems than it."""
)
subsets = list(itertools.chain.from_iterable(
itertools.combinations(systems, r) for r in range(1, num_systems + 1)
)) # Compute powerset
targets = subsets
controls = [tuple()] + subsets
anticontrols = [tuple()] + subsets
# Reverse the order of inputs to the product so the order of combinations (and therefore instances) is better
combinations = list(itertools.product(anticontrols, controls, targets))
instances = [[] for _ in gates]
for g, gate in enumerate(gates):
current_gate = copy.deepcopy(gate)
min_allowed_targets = len(current_gate.targets)
min_allowed_controls = len(current_gate.controls)
min_allowed_anticontrols = len(current_gate.anticontrols)
max_allowed_targets = min_allowed_targets + additional_nodes[0]
max_allowed_controls = min_allowed_controls + additional_nodes[1]
max_allowed_anticontrols = min_allowed_anticontrols + additional_nodes[2]
kwargs = copy.deepcopy(current_gate.__dict__)
kwargs = {key[1:]:value for key, value in kwargs.items()}
# Remove all unnecessary, conflicting, or incorrect keyword arguments
# (There should be a cleverer way to do this...)
kwargs.pop('targets')
kwargs.pop('controls')
kwargs.pop('anticontrols')
kwargs.pop('num_systems')
kwargs.pop('kind')
kwargs.pop('numerical')
kwargs.pop('array')
kwargs.pop('debug')
current_targets = current_gate.targets
current_controls = current_gate.controls
current_anticontrols = current_gate.anticontrols
current_num_nodes = (
len(current_gate.targets)
+ len(current_gate.controls)
+ len(current_gate.anticontrols)
)
shifted_targets = [
[(target + k) for target in current_targets]
for k in range(-num_systems, num_systems)
]
shifted_controls = [
[(control + k) for control in current_controls]
for k in range(-num_systems, num_systems)
]
shifted_anticontrols = [
[(anticontrol + k) for anticontrol in current_anticontrols]
for k in range(-num_systems, num_systems)
]
for combination in combinations:
combination = list(combination)
combination.reverse()
proposed_targets = list(combination[0])
proposed_controls = list(combination[1])
proposed_anticontrols = list(combination[2])
proposed_combined = list(
set(proposed_targets + proposed_controls + proposed_anticontrols)
)
intersections = [
(
set(shifted_targets[k]) & set(proposed_targets),
set(shifted_controls[k]) & set(proposed_controls),
set(shifted_anticontrols[k]) & set(proposed_anticontrols),
)
for k in range(0, 2 * num_systems)
]
if (
min_allowed_targets <= len(proposed_targets) <= max_allowed_targets
and min_allowed_controls <= len(proposed_controls) <= max_allowed_controls
and min_allowed_anticontrols <= len(proposed_anticontrols) <= max_allowed_anticontrols
and len(
set(proposed_combined)
& set(total_gate_controls_adjusted + total_gate_anticontrols_adjusted)
) == 0 and (
preserve_structure is False or (
preserve_structure is True and
any(sum([len(node_group) for node_group in intersections[k]]) == current_num_nodes
for k in range(0, 2*num_systems)) is True
)
)
):
try: # Automatically filter out undesired target, control, and anticontrol combinations
gate_variant = type(current_gate)(
targets=proposed_targets,
controls=proposed_controls,
anticontrols=proposed_anticontrols,
num_systems=num_systems,
numerical=numerical,
array=array,
**kwargs,
)
instances[g].append(gate_variant)
# gate_variant.diagram() # For debugging
except:
pass
instances = flatten_list(instances)
if len(instances) == 0:
raise Exception("""A list of gate instances has failed to be constructed.""")
found_sequences = []
def depths(depth):
if depth == 0:
d = 1
while True:
yield d
d += 1
else:
depth = list(range(1, depth + 1)) if isinstance(depth, int) is True else depth
for d in depth:
yield d
counter = 0
num_found = 0
for length in depths(depth):
sequences = itertools.product(instances, repeat=length)
for sequence in sequences:
sequence_operators = [
gate.output(numerical=numerical, array=array) for gate in sequence
]
sequence_operators.reverse()
sequence_operator = matrix_multiplication(*sequence_operators)
if (
numerical is False
and np.array_equal(operator, sequence_operator) is True
) or (
numerical is True
and np.allclose(
to_array(operator, numerical=numerical),
to_array(sequence_operator, numerical=numerical),
rtol=10 ** (-12),
atol=0,
)
is True
):
found_sequences.append(list(sequence))
num_found += 1
# print(counter)
counter += 1
if num_find != 0 and num_found >= num_find:
break
if num_find != 0 and num_found >= num_find:
break
if num_found > 0:
# Resize the gates in each sequence and add control and anticontrol nodes
for s, sequence in enumerate(found_sequences):
for g, gate in enumerate(sequence):
found_sequences[s][g] = copy.deepcopy(found_sequences[s][g])
if only_targets is True:
found_sequences[s][g].num_systems = self.num_systems
unshifted_targets = copy.deepcopy(found_sequences[s][g].targets)
unshifted_controls = copy.deepcopy(found_sequences[s][g].controls)
unshifted_anticontrols = copy.deepcopy(found_sequences[s][g].anticontrols)
shifted_targets = [
target + min(total_gate_targets)
for target in unshifted_targets
]
shifted_controls = [
control + min(total_gate_targets)
for control in unshifted_controls
]
shifted_anticontrols = [
anticontrol + min(total_gate_targets)
for anticontrol in unshifted_anticontrols
]
found_sequences[s][g].targets = []
found_sequences[s][g].controls = []
found_sequences[s][g].anticontrols = []
found_sequences[s][g].targets = shifted_targets
found_sequences[s][g].controls = shifted_controls
found_sequences[s][g].anticontrols = shifted_anticontrols
found_sequences[s][g].controls += self.controls
found_sequences[s][g].anticontrols += self.anticontrols
# print(f"Total number of decompositions attempted: {counter}")
# print(f"Total number of decompositions found: {len(found_sequences)}")
return found_sequences
[docs]
class Unitary(QuantumGate):
"""A subclass for creating :math:`2`-dimensional unitary gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
A square matrix :math:`\\Unitary` is said to be *unitary* if its matrix inverse is equal to its conjugate tranpose (Hermitian conjugate). Mathematically, this is the condition
.. math:: \\Unitary^\\dagger = \\Unitary^{-1},
which equivalently means that any unitary matrix satisfies
.. math:: \\Unitary^\\dagger \\Unitary = \\Unitary \\Unitary^\\dagger = \\Identity,
where :math:`\\Identity` is the identity matrix. Evidently, the inverse of a unitary matrix is another unitary matrix. Additionally, the product of any two unitary matrices is unitary, and so the set of all unitary matrices consitutes a group, called the *unitary group* (written :math:`\\GroupUnitary(n)` for the group of :math:`n \\times n` unitary matrices), with the group operation being matrix multiplication.
Unitary gates for qubits correspond to :math:`2 \\times 2` unitary matrices, with one parametrization of such matrices being
.. math::
\\Unitary(\\theta,\\phi,\\lambda) =
\\begin{bmatrix} \\cos(\\theta/2) & -\\e^{\\eye\\lambda}\\sin(\\theta/2) \\\\
\\e^{\\eye\\phi}\\sin(\\theta/2) & \\e^{\\eye(\\phi + \\lambda)}\\cos(\\theta/2) \\end{bmatrix},
where the *parameters* (:python:`parameters`) :math:`\\theta, \\phi, \\lambda \\in \\Reals` can be interpreted as angles. Note that this form is :math:`2\\pi`-periodic in each of its three parameters, and specifies any element of :math:`\\GroupUnitary(2)` (up to a global phase).
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
parameters : tuple[num | expr | str, num | expr | str, num | expr | str]
A 3-tuple of scalar values corresponding to the parameters :math:`(\\theta, \\phi, \\lambda)` in the given definition.
Defaults to :python:`(0, 0, 0)`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
Note
----
The unitary gate is defined only for :math:`2`-dimensional (i.e., binary/qubit) systems.
This means that the constructor does not take :python:`dim` as an argument, nor can the associated property be set.
"""
INVOLUTORY = False
DIM = 2
def __init__(
self,
*args,
parameters: (
tuple[num | expr | str, num | expr | str, num | expr | str] | None
) = None,
**kwargs,
):
parameters = (0, 0, 0) if parameters is None else parameters
self.parameters = parameters
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "U")])
args, kwargs = fix_arguments(
args, kwargs, QuantumGate, [("dim", Unitary.DIM), ("spec", None)]
)
super().__init__(*args, **kwargs)
@property
def dim(self) -> int:
return Unitary.DIM
@dim.setter
def dim(self, dim: int):
pass
@property
def parameters(self) -> tuple[num | expr | str, num | expr | str, num | expr | str]:
"""The 3-tuple of parameter values."""
return self._parameters
@parameters.setter
def parameters(
self, parameters: tuple[num | expr | str, num | expr | str, num | expr | str]
):
self._parameters = parameters
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
param_theta = symbolize_expression(self.parameters[0], self.symbols_list)
param_phi = symbolize_expression(self.parameters[1], self.symbols_list)
param_lambda = symbolize_expression(self.parameters[2], self.symbols_list)
operator = sp.Matrix(
[
[
sp.cos(param_theta / 2),
-sp.exp(sp.I * param_lambda) * sp.sin(param_theta / 2),
],
[
sp.exp(sp.I * param_phi) * sp.sin(param_theta / 2),
sp.exp(sp.I * (param_phi + param_lambda)) * sp.cos(param_theta / 2),
],
]
)
operator = cast(operator, numerical=numerical, array=array_intermediate)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
UNI = Unitary
[docs]
class Pauli(QuantumGate):
"""A subclass for creating Pauli gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Pauli gates correspond to the *Pauli operators* :math:`\\Pauli_i`, which is a set of three unitary operators that have the following representations as :math:`2 \\times 2` matrices:
.. math::
\\begin{aligned}
\\Pauli_1 &= \\Pauli_x \\equiv \\ket{0}\\bra{1} + \\ket{1}\\bra{0}
= \\begin{bmatrix} 0 & 1 \\\\ 1 & 0 \\end{bmatrix}, \\\\
\\Pauli_2 &= \\Pauli_y \\equiv -\\eye \\ket{0}\\bra{1} + \\eye \\ket{1}\\bra{0}
= \\begin{bmatrix} 0 & -\\eye \\\\ \\eye & 0 \\end{bmatrix}, \\\\
\\Pauli_3 &= \\Pauli_z \\equiv \\ket{0}\\bra{0} - \\ket{1}\\bra{1}
= \\begin{bmatrix} 1 & 0 \\\\ 0 & -1 \\end{bmatrix}.
\\end{aligned}
Indexed by :math:`i` (:python:`index`), this set is often extended to include the :math:`2`-dimensional identity matrix for :math:`i=0`.
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
index : int
The index of the desired Pauli matrix. Can take the following values:
- :python:`0` (:math:`2`-dimensional identity matrix :math:`\\Identity`)
- :python:`1` (Pauli-:math:`X` :math:`\\Pauli_x`)
- :python:`2` (Pauli-:math:`Y` :math:`\\Pauli_y`)
- :python:`3` (Pauli-:math:`Z` :math:`\\Pauli_z`)
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
Note
----
The Pauli gates are defined only for :math:`2`-dimensional (i.e., binary/qubit) systems.
This means that the constructor does not take :python:`dim` as an argument, nor can the associated property be set.
"""
INVOLUTORY = True
DIM = 2
MATRICES = {
0: sp.Matrix([[1, 0], [0, 1]]),
1: sp.Matrix([[0, 1], [1, 0]]),
2: sp.Matrix([[0, -sp.I], [sp.I, 0]]),
3: sp.Matrix([[1, 0], [0, -1]]),
}
LABELS = {0: "I", 1: "X", 2: "Y", 3: "Z"}
def __init__(self, *args, index: int, **kwargs):
self.index = index
args, kwargs = default_arguments(
args, kwargs, QuantumGate, [("label", Pauli.LABELS[index])]
)
args, kwargs = fix_arguments(
args, kwargs, QuantumGate, [("dim", Pauli.DIM), ("spec", None)]
)
super().__init__(*args, **kwargs)
@property
def dim(self) -> int:
return Pauli.DIM
@dim.setter
def dim(self, dim: int):
pass
@property
def index(self) -> int:
"""The index of the desired Pauli matrix."""
return self._index
@index.setter
def index(self, index: int):
self._index = index
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = cast(
Pauli.MATRICES[self.index], numerical=numerical, array=array_intermediate
)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
PAULI = Pauli
[docs]
class GellMann(QuantumGate):
"""A subclass for creating Gell-Mann gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Gell-Mann gates correspond to the *Gell-Mann operators* :math:`\\GellMann_i`, which is a set of eight non-unitary operators that have the following representations as :math:`3 \\times 3` matrices:
.. raw:: latex
\\vspace*{-1.5\\baselineskip}
.. raw:: latex
\\begin{adjustwidth}{-2.5em}{0cm}
.. math::
\\begin{aligned}
&\\GellMann_1 \\equiv \\ket{0}\\bra{1} + \\ket{1}\\bra{0}
= \\begin{bmatrix} 0 & 1 & 0 \\\\ 1 & 0 & 0 \\\\ 0 & 0 & 0 \\end{bmatrix}, \\\\
&\\GellMann_3 \\equiv \\ket{0}\\bra{0} - \\ket{1}\\bra{1}
= \\begin{bmatrix} 1 & 0 & 0 \\\\ 0 & -1 & 0 \\\\ 0 & 0 & 0 \\end{bmatrix}, \\\\
&\\GellMann_5 \\equiv -\\eye\\ket{0}\\bra{2} + \\eye\\ket{2}\\bra{0}
= \\begin{bmatrix} 0 & 0 & -\\eye \\\\ 0 & 0 & 0 \\\\ \\eye & 0 & 0 \\end{bmatrix}, \\\\
&\\GellMann_7 \\equiv -\\eye\\ket{2}\\bra{3} + \\eye\\ket{3}\\bra{2}
= \\begin{bmatrix} 0 & 0 & 0 \\\\ 0 & 0 & -\\eye \\\\ 0 & \\eye & 0 \\end{bmatrix},
\\end{aligned}
\\quad
\\begin{aligned}
&\\GellMann_2 \\equiv -\\eye\\ket{0}\\bra{1} + \\eye \\ket{1}\\bra{0}
= \\begin{bmatrix} 0 & -\\eye & 0 \\\\ \\eye & 0 & 0 \\\\ 0 & 0 & 0 \\end{bmatrix}, \\\\
&\\GellMann_4 \\equiv \\ket{0}\\bra{2} + \\ket{2}\\bra{0}
= \\begin{bmatrix} 0 & 0 & 1 \\\\ 0 & 0 & 0 \\\\ 1 & 0 & 0 \\end{bmatrix}, \\\\
&\\GellMann_6 \\equiv \\ket{2}\\bra{3} + \\ket{3}\\bra{2}
= \\begin{bmatrix} 0 & 0 & 0 \\\\ 0 & 0 & 1 \\\\ 0 & 1 & 0 \\end{bmatrix}, \\\\
&\\GellMann_8 \\equiv \\frac{1}{\\sqrt{3}}\\bigl(\\ket{0}\\bra{0} + \\ket{1}\\bra{1} - 2\\ket{2}\\bra{2}\\bigr)
= \\frac{1}{\\sqrt{3}}\\begin{bmatrix} 1 & 0 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & -2 \\end{bmatrix}.
\\end{aligned}
.. raw:: latex
\\end{adjustwidth}
Indexed by :math:`i` (:python:`index`), this set is often extended to include the :math:`3`-dimensional identity matrix for :math:`i=0`.
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
index : int
The index of the desired Gell-Mann matrix. Can take the following values:
- :python:`0` (:math:`3`-dimensional identity matrix :math:`\\Identity`)
- :python:`1` (:math:`\\GellMann_1`)
- :python:`2` (:math:`\\GellMann_2`)
- :python:`3` (:math:`\\GellMann_3`)
- :python:`4` (:math:`\\GellMann_4`)
- :python:`5` (:math:`\\GellMann_5`)
- :python:`6` (:math:`\\GellMann_6`)
- :python:`7` (:math:`\\GellMann_7`)
- :python:`8` (:math:`\\GellMann_8`)
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
Note
----
The Gell-Mann gates are defined only for :math:`3`-dimensional (i.e., ternary/qutrit) systems.
This means that the constructor does not take :python:`dim` as an argument, nor can the associated property be set.
"""
INVOLUTORY = False
DIM = 3
MATRICES = {
0: sp.Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]),
1: sp.Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
2: sp.Matrix([[0, -sp.I, 0], [sp.I, 0, 0], [0, 0, 0]]),
3: sp.Matrix([[1, 0, 0], [0, -1, 0], [0, 0, 0]]),
4: sp.Matrix([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
5: sp.Matrix([[0, 0, -sp.I], [0, 0, 0], [sp.I, 0, 0]]),
6: sp.Matrix([[0, 0, 0], [0, 0, 1], [0, 1, 0]]),
7: sp.Matrix([[0, 0, 0], [0, 0, -sp.I], [0, sp.I, 0]]),
8: (1 / sp.sqrt(3)) * sp.Matrix([[1, 0, 0], [0, 1, 0], [0, 0, -2]]),
}
LABELS = {
0: "λ_0",
1: "λ_1",
2: "λ_2",
3: "λ_3",
4: "λ_4",
5: "λ_5",
6: "λ_6",
7: "λ_7",
8: "λ_8",
}
def __init__(self, *args, index: int, **kwargs):
self.index = index
args, kwargs = default_arguments(
args, kwargs, QuantumGate, [("label", GellMann.LABELS[index])]
)
args, kwargs = fix_arguments(
args, kwargs, QuantumGate, [("dim", GellMann.DIM), ("spec", None)]
)
super().__init__(*args, **kwargs)
@property
def dim(self) -> int:
return GellMann.DIM
@dim.setter
def dim(self, dim: int):
pass
@property
def index(self) -> int:
"""The index of the desired Gell-Mann matrix."""
return self._index
@index.setter
def index(self, index: int):
self._index = index
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = cast(
GellMann.MATRICES[self.index], numerical=numerical, array=array_intermediate
)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
GM = GellMann
[docs]
class Rotation(QuantumGate):
"""A subclass for creating rotation gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
The elementary rotation gates correspond to the *rotation operators* :math:`\\Rotation_i`, which is a set of three unitary operators that have the following representations as :math:`2 \\times 2` matrices:
.. math::
\\begin{aligned}
\\Rotation_1 &= \\Rotation_x = \\e^{-\\eye\\Pauli_{x}\\theta/2} =
\\begin{bmatrix} \\cos(\\theta/2) & -\\eye\\sin(\\theta/2) \\\\
-\\eye\\sin(\\theta/2) & \\cos(\\theta/2) \\end{bmatrix}, \\\\
\\Rotation_2 &= \\Rotation_y = \\e^{-\\eye\\Pauli_{y}\\theta/2} =
\\begin{bmatrix} \\cos(\\theta/2) & -\\sin(\\theta/2) \\\\
\\sin(\\theta/2) & \\cos(\\theta/2) \\end{bmatrix}, \\\\
\\Rotation_3 &= \\Rotation_z = \\e^{-\\eye\\Pauli_{z}\\theta/2} =
\\begin{bmatrix} \\e^{-\\eye\\theta/2} & 0 \\\\
0 & \\e^{\\eye\\theta/2} \\end{bmatrix}.
\\end{aligned}
Indexed by :math:`i` (:python:`index`), each matrix in this set is parameterized by the *rotation angle* (:python:`angle`) :math:`\\theta \\in \\Reals`.
These are fundamentally single-system gates, and so a copy of the specified gate is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
axis : int
The index corresponding to the axis of the desired rotation matrix.
Can take the following values:
- :python:`1` (:math:`x`-rotation :math:`\\Rotation_x`)
- :python:`2` (:math:`y`-rotation :math:`\\Rotation_y`)
- :python:`3` (:math:`z`-rotation :math:`\\Rotation_z`)
angle : num | expr | str
The rotation angle.
Defaults to :python:`0`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
.. raw:: latex
\\vspace*{-0.5\\baselineskip}
Note
----
The rotation gates are defined only for :math:`2`-dimensional (i.e., binary/qubit) systems.
This means that the constructor does not take :python:`dim` as an argument, nor can the associated property be set.
"""
INVOLUTORY = False
DIM = 2
def __init__(
self, *args, axis: int, angle: num | expr | str | None = None, **kwargs
):
angle = 0 if angle is None else angle
self.axis = axis
self.angle = angle
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "R")])
args, kwargs = fix_arguments(
args, kwargs, QuantumGate, [("dim", Rotation.DIM), ("spec", None)]
)
super().__init__(*args, **kwargs)
@property
def dim(self) -> int:
return Rotation.DIM
@dim.setter
def dim(self, dim: int):
pass
@property
def axis(self) -> int:
"""The index corresponding to the axis of the desired rotation matrix."""
return self._axis
@axis.setter
def axis(self, axis: int):
self._axis = axis
@property
def angle(self) -> num | expr | str:
"""The rotation angle."""
return self._angle
@angle.setter
def angle(self, angle: num | expr | str):
self._angle = angle
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
angle = symbolize_expression(self.angle, self.symbols_list)
if self.axis == 1:
operator = sp.Matrix(
[
[sp.cos(angle / 2), -sp.I * sp.sin(angle / 2)],
[-sp.I * sp.sin(angle / 2), sp.cos(angle / 2)],
]
)
if self.axis == 2:
operator = sp.Matrix(
[
[sp.cos(angle / 2), -sp.sin(angle / 2)],
[sp.sin(angle / 2), sp.cos(angle / 2)],
]
)
if self.axis == 3:
operator = sp.Matrix(
[
[sp.exp(-sp.I * angle / 2), 0],
[0, sp.exp(sp.I * angle / 2)],
]
)
operator = cast(operator, numerical=numerical, array=array_intermediate)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
ROT = Rotation
[docs]
class Phase(QuantumGate):
"""A subclass for creating phase gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Phase gates correspond to *phase operators* :math:`\\Phase`, which, in :math:`\\Dimension` dimensions, can be represented by :math:`\\Dimension \\times \\Dimension` diagonal matrices of the form
.. math::
\\begin{aligned}
\\Phase(\\omega) &= \\sum\\limits_{k=0}^{\\Dimension - 1} \\omega^k \\ket{k}\\bra{k} \\\\
&=
\\begin{bmatrix}
1 & 0 & 0 & \\ldots & 0 \\\\
0 & \\omega & 0 & \\ldots & 0 \\\\
0 & 0 & \\omega^2 & \\ldots & 0 \\\\
\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\
0 & 0 & 0 & \\ldots & \\omega^{\\Dimension - 1}
\\end{bmatrix}
\\end{aligned}
where :math:`\\omega \\in \\Complexes` is the *phase* parameter (:python:`phase`).
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
phase : num | expr | str
The phase parameter.
Defaults to the unit root given by :python:`sp.exp(2 * sp.pi * sp.I / self.dim)`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
"""
INVOLUTORY = False
def __init__(
self,
*args,
phase: num | expr | str | None = None,
**kwargs,
):
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "P")])
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
super().__init__(*args, **kwargs)
phase = sp.exp(2 * sp.pi * sp.I / self.dim) if phase is None else phase
self.phase = phase
@property
def phase(self) -> num | expr | str:
"""The phase parameter."""
return self._phase
@phase.setter
def phase(self, phase: num | expr | str):
self._phase = phase
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = generate_zeros(
self.dim, numerical=numerical, array=array_intermediate
)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
phase = to_numerical(
symbolize_expression(self.phase, self.symbols_list), numerical=numerical
)
for k in range(0, self.dim):
operator = operator + phase**k * ket(
k, dim=self.dim, numerical=numerical, array=array_intermediate
) * bra(k, dim=self.dim, numerical=numerical, array=array_intermediate)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
PHS = Phase
[docs]
class Diagonal(QuantumGate):
"""A subclass for creating diagonal gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Diagonal gates correspond to *diagonal operators* :math:`\\Diagonal`, which, in :math:`\\Dimension` dimensions, can be represented by :math:`\\Dimension \\times \\Dimension` diagonal matrices of the form
.. math::
\\begin{aligned}
\\Diagonal(\\lambda_0, \\lambda_1, \\ldots, \\lambda_{\\Dimension - 1})
&= \\sum\\limits_{k=0}^{\\Dimension - 1} \\lambda_k\\ket{k}\\bra{k} \\\\
&=
\\begin{bmatrix}
\\lambda_0 & 0 & 0 & \\ldots & 0 \\\\
0 & \\lambda_1 & 0 & \\ldots & 0 \\\\
0 & 0 & \\lambda_2 & \\ldots & 0 \\\\
\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\
0 & 0 & 0 & \\ldots & \\lambda_{\\Dimension - 1}
\\end{bmatrix}
\\end{aligned}
where :math:`\\{\\lambda_k : \\lambda_k \\in \\Complexes, \\; \\abs{\\lambda_k} = 1\\}_{k=0}^{\\Dimension - 1}` are the main diagonal *entries* (:python:`entries`).
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
entries : dict[int | list[int], num | expr | str]
A dictionary in which the keys are number-basis specifications (integer or list of integers) and the values are scalars.
exponentiation : bool
Whether to exponentiate (with imaginary unit) the values given in :python:`entries`.
Defaults to :python:`False`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
Note
----
Basis elements that are unspecified in the :python:`entries` argument all have a corresponding matrix element of :python:`1`, regardless of the value of :python:`exponentiation`.
"""
INVOLUTORY = False
def __init__(
self,
*args,
entries: dict[int | list[int], num | expr | str],
exponentiation: bool | None = None,
**kwargs,
):
self.entries = entries
exponentiation = False if exponentiation is None else exponentiation
self.exponentiation = exponentiation
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "D")])
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
super().__init__(*args, **kwargs)
@property
def entries(self) -> dict[int | list[int], num | expr | str]:
"""A dictionary in which the keys are number-basis specifications (integer or list of integers) and the values are scalars."""
return self._entries
@entries.setter
def entries(self, entries: dict[int | list[int], num | expr | str]):
self._entries = entries
@property
def exponentiation(self) -> bool:
"""Whether to exponentiate (with imaginary unit) the values given in :python:`entries`."""
return self._exponentiation
@exponentiation.setter
def exponentiation(self, exponentiation: bool):
self._exponentiation = exponentiation
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
operator = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
for key, value in self.entries.items():
if self.exponentiation is True:
coefficient = symbolize_expression(
"exp(I*(" + str(value) + "))", self.symbols_list
)
else:
coefficient = symbolize_expression(str(value), self.symbols_list)
coefficient = to_numerical(coefficient, numerical=numerical)
projector = ket(
key, dim=self.dim, numerical=numerical, array=array_intermediate
) * bra(key, dim=self.dim, numerical=numerical, array=array_intermediate)
operator = operator + (coefficient - 1) * projector
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
DIAG = Diagonal
[docs]
class Swap(QuantumGate):
"""A subclass for creating SWAP (exchange) gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
SWAP gates correspond to *SWAP operators* :math:`\\Swap`, which, between two systems :math:`A` and :math:`B` (each of :math:`\\Dimension` dimensions), can be represented by the :math:`\\Dimension^2 \\times \\Dimension^2` matrix
.. math::
\\Swap^{\\indices{A,B}} =
\\sum\\limits_{j,k=0}^{\\Dimension - 1}
{\\ket{j}\\bra{k}}^{\\indices{A}} \\otimes {\\ket{k}\\bra{j}}^{\\indices{B}}.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
targets : list[int, int]
A list of exactly two indices corresponding to the systems to be swapped.
Is an argument of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`, so can be specified positionally in :python:`*args`.
Defaults to :python:`[0, 1]`
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
"""
INVOLUTORY = True
def __init__(self, *args, **kwargs):
args, kwargs = default_arguments(
args, kwargs, QuantumGate, [("label", "S"), ("family", "SWAP"), ("targets", [0, 1])]
)
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
super().__init__(*args, **kwargs)
if len(self.targets) != 2:
raise ValueError(
"""A `targets` list of exactly two (2) system indices must be provided."""
)
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
permutation = [k for k in range(0, self.num_systems)]
permutation[self.targets[0]], permutation[self.targets[1]] = (
permutation[self.targets[1]],
permutation[self.targets[0]],
)
possibility = [k for k in range(0, self.dim)]
possibilities = [possibility for _ in range(0, self.num_systems)]
combinations = list(itertools.product(*possibilities))
matrix = generate_zeros(
self.dim**self.num_systems, numerical=numerical, array=array_intermediate
)
for n in range(0, self.dim**self.num_systems):
level = list(combinations[n])
permuted = [level[permutation[k]] for k in range(0, self.num_systems)]
matrix = matrix + ket(
permuted, dim=self.dim, numerical=numerical, array=array_intermediate
) * bra(level, dim=self.dim, numerical=numerical, array=array_intermediate)
return cast(matrix, numerical=numerical, array=array)
SWAP = Swap
[docs]
class Summation(QuantumGate):
"""A subclass for creating SUM (summation) gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
The SUM gate is essentially a generalization of the NOT gate to :math:`\\Dimension`-dimensional systems. It corresponds to the *SUM operator* :math:`\\SUM`, which can be represented by :math:`\\Dimension \\times \\Dimension` matrices of the form
.. math:: \\SUM(n) = \\sum\\limits_{k=0}^{\\Dimension - 1} \\ket{k \\oplus n}\\bra{k},
where :math:`n \\in \\Integers_{\\geq 0}` is the *shift* parameter (:python:`shift`), and :math:`k \\oplus n \\equiv k + n \\mathrel{\\mathrm{mod}} \\Dimension`.
The case of :math:`n = 1` is known as the *shift operator*, and represents a (non-Hermitian) generalization of the Pauli-:math:`X` operator :math:`\\Pauli_x` to :math:`\\Dimension` dimensions.
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
shift : int
The shift parameter.
Must be a non-negative integer.
Defaults to :python:`1`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
"""
INVOLUTORY = False
def __init__(self, *args, shift: int | None = None, **kwargs):
shift = 1 if shift is None else shift
self.shift = shift
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "Σ")])
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
super().__init__(*args, **kwargs)
@property
def shift(self) -> int:
"""The shift parameter."""
return self._shift
@shift.setter
def shift(self, shift: int):
self._shift = shift
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
summation = generate_zeros(
self.dim, numerical=numerical, array=array_intermediate
)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
for k in range(0, self.dim):
oplus = (k + self.shift) % self.dim
summation = summation + ket(
oplus, dim=self.dim, numerical=numerical, array=array_intermediate
) * bra(k, dim=self.dim, numerical=numerical, array=array_intermediate)
matrix = generate_identity(1, numerical=numerical, array=array_intermediate)
for m in range(0, self.num_systems):
if m in list(self.targets):
matrix = tensor_product(matrix, summation)
else:
matrix = tensor_product(matrix, identity)
return cast(matrix, numerical=numerical, array=array)
SUM = Summation
[docs]
class Not(Summation):
"""A subclass for creating NOT (logical negation or "bit-flip") gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
The NOT gate is essentially a specialization of the SUM gate to :math:`2`-dimensional systems. In the computational basis, it corresponds to the *NOT operator* :math:`\\NOT`, which is exactly equivalent to the Pauli-:math:`X` operator :math:`\\Pauli_x`, and so has the matrix representation
.. math::
\\begin{aligned}
\\NOT &= \\ket{0}\\bra{1} + \\ket{1}\\bra{0} \\\\
&= \\begin{bmatrix} 0 & 1 \\\\ 1 & 0 \\end{bmatrix}.
\\end{aligned}
As such, this class exists purely to simplify access to this operation.
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
Note
----
NOT gates are defined only for :math:`2`-dimensional (i.e., binary/qubit) systems.
This means that the constructor does not take :python:`dim` as an argument, nor can the associated property be set.
"""
INVOLUTORY = True
DIM = 2
SHIFT = 1
def __init__(self, *args, **kwargs):
args, kwargs = default_arguments(
args, kwargs, QuantumGate, [("label", "X"), ("family", "TARG")]
)
args, kwargs = fix_arguments(
args, kwargs, QuantumGate, [("dim", Not.DIM), ("spec", None)]
)
super().__init__(*args, **kwargs)
@property
def dim(self) -> int:
return Not.DIM
@dim.setter
def dim(self, dim: int):
pass
@property
def shift(self) -> int:
return Not.SHIFT
@shift.setter
def shift(self, shift: int):
pass
NOT = Not
[docs]
class Hadamard(QuantumGate):
"""A subclass for creating Hadamard gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Hadamard gates correspond to *Hadamard operators* :math:`\\Hadamard`, which, for qubits (:math:`2` dimensions), may be represented by the :math:`2 \\times 2` matrix
.. math::
\\begin{aligned}
\\Hadamard &= \\frac{1}{\\sqrt{2}}\\sum\\limits_{j,k=0}^{1} (-1)^{jk} \\ket{j}\\bra{k} \\\\
&= \\frac{1}{\\sqrt{2}}\\begin{bmatrix} 1 & 1 \\\\ 1 & -1 \\end{bmatrix}.
\\end{aligned}
For qudits (:math:`\\Dimension` dimensions), this can be generalized to the following :math:`\\Dimension \\times \\Dimension` form,
.. math::
\\begin{aligned}
\\Hadamard_\\Dimension &= \\frac{1}{\\sqrt{\\Dimension}}\\sum\\limits_{j,k=0}^{\\Dimension - 1}
\\omega_\\Dimension^{k(\\Dimension - j)} \\ket{j}\\bra{k} \\\\
&=
\\begin{bmatrix}
1 & 1 & 1 & \\ldots & 1 \\\\
1 & \\omega^{\\Dimension - 1} & \\omega^{2(\\Dimension - 1)} & \\ldots & \\omega^{(\\Dimension - 1)^2} \\\\
1 & \\omega^{\\Dimension - 2} & \\omega^{2(\\Dimension - 2)} & \\ldots & \\omega^{(\\Dimension - 1)(\\Dimension - 2)} \\\\
\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\
1 & \\omega & \\omega^{2} & \\ldots & \\omega^{\\Dimension - 1}
\\end{bmatrix}
\\end{aligned}
where :math:`\\omega_\\Dimension \\equiv \\e^{2\\pi\\eye/\\Dimension}`.
This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
"""
INVOLUTORY = False
def __init__(self, *args, **kwargs):
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "H")])
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
super().__init__(*args, **kwargs)
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
omega = to_numerical(sp.exp(2 * sp.pi * sp.I / self.dim), numerical=numerical)
operator = generate_zeros(
self.dim, numerical=numerical, array=array_intermediate
)
for i in range(0, self.dim):
for j in range(0, self.dim):
operator = operator + (
omega ** (j * (self.dim - i))
* ket(
i, dim=self.dim, numerical=numerical, array=array_intermediate
)
* bra(
j, dim=self.dim, numerical=numerical, array=array_intermediate
)
)
operator = operator * to_numerical(1 / sp.sqrt(self.dim), numerical=numerical)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange([targets_compliment, self.targets], [identity] + [operator])
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
HAD = Hadamard
[docs]
class Fourier(QuantumGate):
"""A subclass for creating Fourier (quantum [discrete] Fourier transform [QFT]) gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Fourier gates correspond to *Fourier operators* :math:`\\QFT`, which, for a single system in :math:`\\Dimension` dimensions, can be represented by the :math:`\\Dimension \\times \\Dimension` matrix
.. math::
\\begin{aligned}
\\QFT &= \\frac{1}{\\sqrt{\\Dimension}} \\sum\\limits_{j,k=0}^{\\Dimension - 1}
\\omega_{\\Dimension}^{jk} \\ket{j}\\bra{k} \\\\
&= \\frac{1}{\\sqrt{\\Dimension}}
\\begin{bmatrix}
1 & 1 & 1 & 1 & \\ldots & 1 \\\\
1 & \\omega & \\omega^2 & \\omega^3 & \\ldots & \\omega^{\\Dimension - 1} \\\\
1 & \\omega^2 & \\omega^4 & \\omega^6 & \\ldots & \\omega^{2(\\Dimension - 1)} \\\\
1 & \\omega^3 & \\omega^6 & \\omega^9 & \\ldots & \\omega^{3(\\Dimension - 1)} \\\\
\\vdots & \\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\
1 & \\omega^{\\Dimension - 1} & \\omega^{2(\\Dimension - 1)} & \\omega^{3(\\Dimension - 1)} & \\ldots & \\omega^{(\\Dimension - 1)(\\Dimension - 1)} \\\\
\\end{bmatrix}
\\end{aligned}
where :math:`\\omega \\equiv \\omega_{\\Dimension} \\equiv \\e^{2\\pi\\eye/\\Dimension}`.
In the case of :math:`N` systems, it is easier to characterize the *multipartite Fourier operator* :math:`\\QFT_N` not by its matrix form but by the transformation it imposes, to which its action on the basis state :math:`\\bigotimes\\limits_{\\ell=1}^{N} \\ket{j_\\ell} \\equiv \\ket{j_1, \\ldots, j_N}` (where :math:`j_\\ell \\in \\Integers_{0}^{\\Dimension - 1}`) is
.. math::
\\ket{j_1, \\ldots, j_N} \\stackrel{\\QFT_N}{\\longrightarrow}
\\frac{1}{\\sqrt{\\Dimension^N}}
\\bigotimes\\limits_{\\ell=1}^{N}
\\sum\\limits_{k_\\ell=0}^{\\Dimension - 1}
\\e^{2\\pi\\eye j k_\\ell \\Dimension^{-\\ell}} \\ket{k_\\ell}
where :math:`j \\equiv \\sum\\limits_{\\ell=1}^{N} j_\\ell \\Dimension^{N - \\ell}`.
If :python:`composite` is :python:`True`, the composite form :math:`\\QFT_N` is applied to the subsystems specified by :python:`targets` in:
- *ascending* order if :python:`reverse` is :python:`False`
- *descending* order if :python:`reverse` is :python:`True`
If :python:`composite` is :python:`False`, a copy of the elementary form :math:`\\QFT` is placed on each of the subsystems corresponding to the indices in the :python:`targets` property.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
composite : bool
Whether the composite (multipartite) Fourier gate is to be used.
If :python:`False`, copies of the elementary Fourier gate are placed on each index specified in :python:`targets`.
Defaults to :python:`True`.
reverse : bool
Whether to reverse the order in which the composite (multipartite) Fourier gate is applied.
Has no effect when :python:`composite` is :python:`False`.
Defaults to :python:`False`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
"""
INVOLUTORY = False
def __init__(
self,
*args,
composite: bool | None = None,
reverse: bool | None = None,
**kwargs,
):
args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "F")])
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
composite = True if composite is None else composite
reverse = False if reverse is None else reverse
super().__init__(*args, **kwargs)
self.composite = composite
self.reverse = reverse
@property
def composite(self) -> bool:
"""Whether the composite (multipartite) Fourier gate is to be used."""
return self._composite
@composite.setter
def composite(self, composite: bool):
self._composite = composite
@property
def reverse(self) -> bool:
"""Whether to reverse the order in which the composite (multipartite) Fourier gate is applied.
Has no effect when :python:`self.composite` is :python:`False`.
"""
return self._reverse
@reverse.setter
def reverse(self, reverse: bool):
self._reverse = reverse
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
if self.composite is True:
# Easy way: use decomposition instead of QFT definition.
targets = sorted(self.targets, reverse=self.reverse)
size = len(targets)
QFT = []
for i, t in enumerate(targets):
count = size - i
for j in range(0, count):
if j == 0:
QFT.append(
Hadamard(
targets=[t],
num_systems=self.num_systems,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
)
)
else:
QFT.append(
Phase(
targets=[t],
controls=[targets[i + j]],
num_systems=self.num_systems,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
exponent=sp.Rational(1, (self.dim**j)),
label=f"1 / {self.dim**j}",
family="GATE",
)
)
matrix = generate_identity(
self.dim**self.num_systems,
numerical=numerical,
array=array_intermediate,
)
for gate in QFT:
matrix = matrix_multiplication(gate.output(), matrix)
else:
omega = to_numerical(
sp.exp(2 * sp.pi * sp.I / self.dim), numerical=numerical
)
operator = generate_zeros(
self.dim, numerical=numerical, array=array_intermediate
)
for i in range(0, self.dim):
for j in range(0, self.dim):
operator = operator + (
omega ** (j * i)
* ket(
i,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
)
* bra(
j,
dim=self.dim,
numerical=numerical,
array=array_intermediate,
)
)
operator = operator * to_numerical(
1 / sp.sqrt(self.dim), numerical=numerical
)
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
targets_compliment = list(set(self.systems) ^ set(self.targets))
ordered = arrange(
[targets_compliment, self.targets], [identity] + [operator]
)
matrix = tensor_product(*ordered)
return cast(matrix, numerical=numerical, array=array)
QFT = Fourier
[docs]
class Measurement(QuantumGate):
"""A subclass for creating measurement gates and storing their metadata.
This is built upon the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so inherits all of its attributes, properties, and methods.
Instances of this class each describe a (non-linear) operation in which the input state (:math:`\\op{\\rho}`) is quantum-mechanically *measured* (against the forms in specified in :python:`operators`) and subsequently mutated according to its predicted post-measurement form (i.e., the sum of all possible measurement outcomes). This yields the transformed states:
- When :python:`observable` is :python:`False`
(:python:`operators` is a list of Kraus operators or projectors :math:`\\Kraus_i`):
.. math:: \\op{\\rho}^\\prime = \\sum_i \\Kraus_i \\op{\\rho} \\Kraus_i^\\dagger
- When :python:`observable` is :python:`True`
(:python:`operators` is a list of observables :math:`\\Observable_i`):
.. math:: \\op{\\rho}^\\prime = \\sum_i \\trace[\\Observable_i \\op{\\rho}] \\Observable_i
The items in the list :python:`operators` can also be vectors (e.g., :math:`\\ket{\\xi_i}`), in which case each is converted into its corresponding matrix representation (e.g., :math:`\\Kraus_i = \\ket{\\xi_i}\\bra{\\xi_i}`) prior to any measurement(s).
Note also that this method does not check for validity of supplied POVMs or the completeness of sets of observables, nor does it renormalize the post-measurement state.
Arguments
---------
*args
Positional arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
operators : list[mat | arr | QuantumObject]
The operator(s) with which to perform the measurement.
These would typically be a (complete) set of Kraus operators forming a POVM,
a (complete) set of (orthogonal) projectors forming a PVM,
or a set of observables constituting a complete basis for the relevant state space.
observable : bool
Whether to treat the items in :python:`operators` as observables (as opposed to Kraus operators or projectors).
Defaults to :python:`False`.
**kwargs
Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
Note
----
In quantum mechanics, measurement operations constitute (in general) non-linear and non-unitary transformations of (normalized) state vectors and density operators. As such, they cannot be represented by matrices, and so the :python:`matrix` property therefore does not return a valid representation of the measurement operation.
Note
----
The :python:`targets` argument must be specified as a list of numerical indices of the subsystem(s) to be measured. The number of indices must match the number of systems spanned by all given operators.
"""
def __init__(
self,
*args,
operators: list[mat | arr | QuantumObject],
observable: bool | None = None,
**kwargs,
):
self.operators = operators
observable = False if observable is None else observable
self.observable = observable
args, kwargs = default_arguments(
args,
kwargs,
QuantumGate,
[("label", "M"), ("family", Families.METER.value)],
)
args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)])
super().__init__(*args, **kwargs)
@property
def operators(self) -> list[mat | arr | QuantumObject]:
"""The operator(s) with which to perform the measurement."""
return self._operators
@operators.setter
def operators(self, operators: list[mat | arr | QuantumObject]):
self._operators = operators
@property
def observable(self) -> bool:
"""Whether to treat the items in the :python:`operators` property as observables (as opposed to Kraus operators or projectors)."""
return self._observable
@observable.setter
def observable(self, observable: bool):
self._observable = observable
def matrices(
self, numerical: bool | None = None, array: bool | None = None
) -> list[mat | arr]:
"""A list of matrix representations of all operators in the :python:`operators` property.
Is a read-only property.
This is used specifically in the :py:class:`~qhronology.quantum.circuits.QuantumCircuit` class when instances of it contain :python:`Measurement` gate instances in their :python:`gates` property.
"""
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
matrices = []
identity = generate_identity(
self.dim, numerical=numerical, array=array_intermediate
)
for operator in self.operators:
operator = densify(extract_representation(operator))
operator = cast(operator, numerical=numerical, array=array_intermediate)
matrix = tensor_product(operator, *[identity] * (self.num_systems - len(self.targets)))
permutation = self.systems
for i, t in enumerate(self.targets):
permutation[i], permutation[t] = permutation[t], permutation[i]
matrix = permute_tensor_product(matrix, permutation, self.dim)
matrices.append(cast(matrix, numerical=numerical, array=array))
return matrices
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
return generate_identity(
self.dim**self.num_systems, numerical=numerical, array=array
)
METER = Measurement
[docs]
class GateInterleave(QuantumGate):
"""Compose two or more :py:class:`~qhronology.quantum.gates.QuantumGate` instances together by interleaving them.
This is achieved by multiplying the gates' matrix representations. For example, for gates described by the multipartite operators :math:`\\op{A} \\otimes \\Identity` and :math:`\\Identity \\otimes \\op{B}`, their interleaved composition is
.. math::
(\\op{A} \\otimes \\Identity) \\cdot (\\Identity \\otimes \\op{B}) = \\op{A} \\otimes \\op{B}.
While this is a subclass of :py:class:`~qhronology.quantum.gates.QuantumGate`, all of its inherited properties, except for those corresponding to arguments in its constructor, are read-only. This is because they are calculated from their corresponding properties in the individual instances contained within the :python:`gates` property.
Arguments
---------
*gates : QuantumGate
:py:class:`~qhronology.quantum.gates.QuantumGate` instances to be interleaved.
merge : bool
Whether to merge the gates together diagrammatically.
Defaults to :python:`False`.
num_systems : int
The (total) number of systems which the gate spans.
Must be a non-negative integer.
Defaults to :python:`max([gate.num_systems for gate in gates])`.
numerical : bool
Whether to cast the gate's matrix elements as floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to :python:`False`.
array : bool
Whether to cast the gate's matrix as a NumPy array (:python:`True`) or SymPy matrix (:python:`False`).
Defaults to :python:`False`.
label : str
The unformatted string used to represent the gate in mathematical expressions.
Defaults to :python:`"⊗".join([gate.label for gate in gates])`.
notation : str
The formatted string used to represent the gate in mathematical expressions.
When not :python:`None`, overrides the value passed to :python:`label`.
Not intended to be set by the user in most cases.
Defaults to :python:`None`.
Note
----
Care should be taken to ensure that gates passed to this class all have the same :python:`num_systems` value and do not have overlapping :python:`targets`, :python:`controls`, and :python:`anticontrols` properties.
Note
----
The resulting visualization (using the inherited :py:meth:`~qhronology.quantum.gates.QuantumGate.diagram` method or in circuit diagrams) may not be accurate in every case. However, the composed matrix should still be correct.
"""
def __init__(
self,
*gates: QuantumGate,
merge: bool | None = None,
num_systems: int | None = None,
numerical: bool | None = None,
array: bool | None = None,
label: str | None = None,
notation: str | None = None,
):
self.gates = [copy.deepcopy(gate) for gate in gates]
merge = False if merge is None else merge
self.merge = merge
label = "⊗".join([gate.label for gate in gates]) if label is None else label
num_systems = max([gate.num_systems for gate in gates]) if num_systems is None else num_systems
super().__init__(
spec=None,
num_systems=num_systems,
numerical=numerical,
array=array,
simplification=None,
conjugation=False,
exponent=1,
coefficient=1,
label=label,
notation=notation,
)
@property
def merge(self) -> bool:
"""Whether to merge the gates together diagrammatically."""
return self._merge
@merge.setter
def merge(self, merge: bool):
self._merge = merge
@property
def labels(self) -> list[str]:
labels = [gate.label for gate in self.gates]
if self.merge is True:
labels = self.label
return labels
@property
def notations(self) -> str | list[str]:
notations = [gate.notation for gate in self.gates]
if self.merge is True:
notations = self.notation
return notations
@property
def gates(self) -> list[QuantumGate]:
"""Variable-length list of :py:class:`~qhronology.quantum.gates.QuantumGate` instances to be composited."""
return self._gates
@gates.setter
def gates(self, gates: list[QuantumGate]):
self._gates = gates
@property
def boundaries(self) -> list[int]:
boundaries = flatten_list([max(gate.boundaries) for gate in self.gates])
if self.merge is True:
boundaries = [self.num_systems]
return boundaries
@property
def family(self) -> str | list[str]:
family = [gate.family for gate in self.gates]
if self.merge is True:
family = Families.GATE.value
return family
@family.setter
def family(self, family: str | list[str]):
pass
@property
def targets(self) -> list[int]:
return list(set(flatten_list([gate.targets for gate in self.gates])))
@targets.setter
def targets(self, targets: list[int]):
pass
@property
def controls(self) -> list[int]:
return list(set(flatten_list([gate.controls for gate in self.gates])))
@controls.setter
def controls(self, controls: list[int]):
pass
@property
def anticontrols(self) -> list[int]:
return list(set(flatten_list([gate.anticontrols for gate in self.gates])))
@anticontrols.setter
def anticontrols(self, anticontrols: list[int]):
pass
@property
def num_systems(self) -> int:
return self._num_systems
@num_systems.setter
def num_systems(self, num_systems: int):
num_systems_min = max(
[
max(gate.targets + gate.controls + gate.anticontrols) + 1
for gate in self.gates
]
)
if num_systems < num_systems_min:
raise ValueError(
f"""One or more of the interleaved gates cannot have its number of systems set to {num_systems}."""
)
else:
for index, gate in enumerate(self.gates):
self.gates[index].num_systems = num_systems
self._num_systems = num_systems
@property
def symbols(self) -> dict[sym | str, dict[str, Any]]:
symbols_collection = [gate.symbols for gate in self.gates]
symbols_merged = {}
for symbols in symbols_collection:
symbols_merged.update(symbols)
return symbols_merged
@symbols.setter
def symbols(self, symbols):
pass
@property
def dim(self) -> int:
dim = list(set(flatten_list([gate.dim for gate in self.gates])))
if len(dim) != 1:
raise ValueError("""Mismatch between one or more of the dimensions.""")
return dim[0]
@dim.setter
def dim(self, dim: int):
pass
@property
def substitutions(self) -> list[tuple[num | expr | str, num | expr | str]]:
substitutions = []
for gate in self.gates:
substitutions += gate.substitutions
return substitutions
@substitutions.setter
def substitutions(self, substitutions):
pass
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
matrix = generate_identity(
self.dim**self.num_systems, numerical=numerical, array=array_intermediate
)
for gate in self.gates:
matrix = matrix_multiplication(
gate.output(numerical=numerical, array=array_intermediate), matrix
)
return cast(matrix, numerical=numerical, array=array)
def output(
self,
numerical: bool | None = None,
array: bool | None = None,
substitutions: list[tuple[num | expr | str, num | expr | str]] | None = None,
simplification: bool | None = None,
conjugation: bool | None = None,
exponent: bool | num | expr | str | None = None,
coefficient: bool | num | expr | str | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
gate = self.matrix(numerical=numerical, array=array_intermediate)
# Exponentiate
if exponent is None or exponent is True:
exponent = self.exponent
if exponent != 1 and exponent is not False:
exponent = symbolize_expression(exponent, self.symbols_list)
plus = to_numerical(
(1 + sp.exp(sp.I * sp.pi * exponent)) / 2, numerical=numerical
)
minus = to_numerical(
(1 - sp.exp(sp.I * sp.pi * exponent)) / 2, numerical=numerical
)
identity = generate_identity(
self.dim**self.num_systems,
numerical=numerical,
array=array_intermediate,
)
gate = plus * identity + minus * gate
# Coefficient
if coefficient is None or coefficient is True:
coefficient = self.coefficient
if coefficient is not False:
coefficient = symbolize_expression(coefficient, self.symbols_list)
else:
coefficient = 1
if coefficient != 1 and coefficient is not False:
coefficient = to_numerical(coefficient, numerical=numerical)
gate = coefficient * gate
gate = symbolize_expression(gate, self.symbols_list)
# Substitutions
substitutions = self.substitutions if substitutions is None else substitutions
substitutions = symbolize_substitutions(substitutions, self.symbols_list)
gate = apply_substitutions(gate, substitutions)
# Simplification
simplification = self.simplification if simplification is None else simplification
if simplification is True:
gate = recursively_simplify(gate, substitutions)
# Conjugation
conjugation = self.conjugation if conjugation is None else conjugation
if conjugation is True:
gate = conjugate_transpose(gate)
return cast(gate, numerical=numerical, array=array)
INTERLEAVE = GateInterleave
[docs]
class GateStack(GateInterleave):
"""Compose two or more :py:class:`~qhronology.quantum.gates.QuantumGate` instances together by "stacking" them vertically.
This is achieved by computing the tensor product of the gates' matrix representations.
For example, for gates described by the multipartite operators :math:`\\op{A} \\otimes \\Identity` and :math:`\\Identity \\otimes \\op{B}`, their stacked composition is
.. math::
(\\op{A} \\otimes \\Identity) \\otimes (\\Identity \\otimes \\op{B})
= \\op{A} \\otimes \\Identity \\otimes \\Identity \\otimes \\op{B}.
This class is derived from the :py:class:`~qhronology.quantum.gates.QuantumGate` class, and so should be used in much the same way.
Arguments
---------
*gates : QuantumGate
:py:class:`~qhronology.quantum.gates.QuantumGate` instances to be stacked.
merge : bool
Whether to merge the gates together diagrammatically.
Defaults to :python:`False`.
num_systems : int
The (total) number of systems which the gate spans.
Must be a non-negative integer.
Defaults to :python:`sum([gate.num_systems for gate in gates])`.
numerical : bool
Whether to cast the gate's matrix elements as floating-point values (:python:`True`) (if possible) or exact values (:python:`False`).
Defaults to :python:`False`.
array : bool
Whether to cast the gate's matrix as a NumPy array (:python:`True`) or SymPy matrix (:python:`False`).
Defaults to :python:`False`.
label : str
The unformatted string used to represent the gate in mathematical expressions.
Defaults to :python:`"⊗".join([gate.label for gate in gates])`.
notation : str
The formatted string used to represent the gate in mathematical expressions.
When not :python:`None`, overrides the value passed to :python:`label`.
Not intended to be set by the user in most cases.
Defaults to :python:`None`.
"""
def __init__(
self,
*gates: QuantumGate,
merge: bool | None = None,
num_systems: int | None = None,
numerical: bool | None = None,
array: bool | None = None,
label: str | None = None,
notation: str | None = None,
):
num_systems = sum([gate.num_systems for gate in gates]) if num_systems is None else num_systems
super().__init__(
*gates,
merge=merge,
num_systems=num_systems,
numerical=numerical,
array=array,
label=label,
notation=notation,
)
@property
def boundaries(self) -> list[int]:
num_systems = [gate.num_systems for gate in self.gates]
boundaries = [
max(gate.boundaries) + sum(num_systems[:n])
for n, gate in enumerate(self.gates)
]
if self.merge is True:
boundaries = [self.num_systems]
return boundaries
@property
def targets(self) -> list[int]:
targets = []
num_systems = [gate.num_systems for gate in self.gates]
for n, gate in enumerate(self.gates):
targets_current = [target + sum(num_systems[:n]) for target in gate.targets]
targets.append(targets_current)
return list(set(flatten_list(targets)))
@targets.setter
def targets(self, targets: list[int]):
pass
@property
def controls(self) -> list[int]:
controls = []
num_systems = [gate.num_systems for gate in self.gates]
for n, gate in enumerate(self.gates):
controls_current = [
control + sum(num_systems[:n]) for control in gate.controls
]
controls.append(controls_current)
return list(set(flatten_list(controls)))
@controls.setter
def controls(self, controls: list[int]):
pass
@property
def anticontrols(self) -> list[int]:
anticontrols = []
num_systems = [gate.num_systems for gate in self.gates]
for n, gate in enumerate(self.gates):
anticontrols_current = [
anticontrol + sum(num_systems[:n]) for anticontrol in gate.anticontrols
]
anticontrols.append(anticontrols_current)
return list(set(flatten_list(anticontrols)))
@anticontrols.setter
def anticontrols(self, anticontrols: list[int]):
pass
@property
def num_systems(self) -> int:
return self._num_systems
@num_systems.setter
def num_systems(self, num_systems: int):
gate_last = self.gates[-1]
num_systems_last_min = (
max(gate_last.targets + gate_last.controls + gate_last.anticontrols) + 1
)
num_systems_except_last = max(
0, sum([gate.num_systems for gate in self.gates[0:-1]])
)
num_systems_total_min = num_systems_last_min + num_systems_except_last
if num_systems < num_systems_total_min:
raise ValueError(
f"""The number of systems of the stack of gates ({num_systems_total_min}) cannot be less than the number of systems specified ({num_systems})."""
)
else:
self.gates[-1].num_systems = num_systems_last_min + (
num_systems - num_systems_total_min
)
self._num_systems = num_systems
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
array_intermediate = True if numerical is True else False
matrices = [
gate.output(numerical=numerical, array=array_intermediate)
for gate in self.gates
]
matrix = tensor_product(*matrices)
return cast(matrix, numerical=numerical, array=array)
STACK = GateStack
class _Single(QuantumGate):
"""A :py:class:`~qhronology.quantum.gates.QuantumGate` subclass for creating single-cell abstract quantum gates.
Used exclusively internally (for visualization purposes).
"""
def __init__(
self, *args, family: str | None = None, label: str | None = None, **kwargs
):
family = Families.TERM.value if family is None else family
label = " " if label is None else label
super().__init__(
*args,
spec=None,
targets=[0],
num_systems=1,
family=family,
label=label,
**kwargs,
)
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
return generate_identity(
self.dim**self.num_systems, numerical=numerical, array=array
)
class _Empty(QuantumGate):
"""A :py:class:`~qhronology.quantum.gates.QuantumGate` subclass for creating single-cell empty quantum gates.
Used exclusively internally (for visualization purposes).
"""
def __init__(self, *args, family: str | None = None, **kwargs):
family = Families.TERM.value if family is None else family
super().__init__(
*args, spec=None, targets=[0], num_systems=1, family=family, **kwargs
)
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
return generate_identity(
self.dim**self.num_systems, numerical=numerical, array=array
)
class _Wormhole(QuantumGate):
"""A :py:class:`~qhronology.quantum.gates.QuantumGate` subclass for creating single-cell wormhole (mouth) quantum gates.
Used exclusively internally (for visualization purposes).
"""
def __init__(self, *args, family: str | None = None, **kwargs):
family = Families.WORMHOLE.value if family is None else family
super().__init__(
*args, spec=None, targets=[0], num_systems=1, family=family, **kwargs
)
def matrix(
self,
numerical: bool | None = None,
array: bool | None = None,
) -> mat | arr:
numerical = self.numerical if numerical is None else numerical
array = self.array if array is None else array
return generate_identity(
self.dim**self.num_systems, numerical=numerical, array=array
)