Source code for qhronology.quantum.gates

# Project: Qhronology (https://github.com/lgbishop/qhronology)
# Author: lgbishop <lachlanbishop@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.com.

"""
Classes for the creation of quantum gates.
"""

# https://peps.python.org/pep-0649/
# https://peps.python.org/pep-0749/
from __future__ import annotations

import itertools
from typing import Any

import sympy as sp
from sympy.physics.quantum import TensorProduct
from sympy.physics.quantum.dagger import Dagger

from qhronology.utilities.classification import num, sym, mat, arr, Forms
from qhronology.utilities.diagrams import Families
from qhronology.utilities.helpers import (
    flatten_list,
    check_systems_conflicts,
    symbolize_expression,
    recursively_simplify,
    default_arguments,
    fix_arguments,
    count_systems,
    arrange,
    symbolize_tuples,
    extract_matrix,
    stringify,
)
from qhronology.utilities.objects import QuantumObject

from qhronology.mechanics.matrices import ket, bra
from qhronology.mechanics.operations import densify


[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 (or "slice") 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 conditions, 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 | sym | str]] The specification of the quantum gate's matrix representation in a standard ``dim``-dimensional basis. Can be one of: - a SymPy matrix (``mat``) - a NumPy array (``arr``) - a list of lists of numerical, symbolic, or string expressions that collectively specify a matrix (``list[list[num | sym | str]]``) Defaults to the single-system ``dim``-dimensional Identity operator. targets : list[int] The numerical indices of the subsystems on which the gate elements reside. Defaults to ``[0]`` (if ``num_systems`` is ``None``) or ``[i for i in range(num_systems)]`` (if ``num_systems`` is not ``None``). controls : list[int] The numerical indices of the subsystems on which control nodes reside. Defaults to ``[]``. anticontrols : list[int] The numerical indices of the subsystems on which anticontrol nodes reside. Defaults to ``[]``. num_systems : int The (total) number of systems which the gate spans. Must be a non-negative integer. Defaults to ``max(targets + controls + anticontrols + [count_systems(sp.Matrix(spec), dim)]) + 1``. dim : int The dimensionality of the quantum gate's Hilbert space. Must be a non-negative integer. Defaults to ``2``. symbols : dict[sym | str, dict[str, Any]] A dictionary in which the keys are individual symbols (usually found within the gate specification ``spec``) and the values are dictionaries of their respective SymPy keyword-argument ``assumptions``. Defaults to ``{}``. conditions : list[tuple[num | sym | str, num | sym | str]] A list of :math:`2`-tuples of conditions 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 ``subs()`` method. The order in which they are applied is simply their order in the list. Defaults to ``[]``. conjugate : bool Whether to perform Hermitian conjugation on the gate when it is called. Defaults to ``False``. exponent : num | sym | str A numerical or string representation of a scalar value to which gate's operator (residing on ``targets``) is exponentiated. Must be a non-negative integer. Useful for computing powers of gates (such as PSWAP), but is only guaranteed to return a valid power of a gate if its corresponding matrix representation (e.g., :math:`\\op{A}`) is involutory (i.e., :math:`\\op{A}^2 = \\Identity`). Defaults to ``1``. coefficient : num | sym | str A numerical or string representation of a scalar value by which the gate's matrix (occupying ``targets``) is multiplied. Performed after exponentiation. Useful for multiplying the gate by a phase factor. Defaults to ``1``. label : str The unformatted string used to represent the gate in mathematical expressions. Defaults to ``"U"``. notation : str The formatted string used to represent the gate in mathematical expressions. When not ``None``, overrides the value passed to ``label``. Not intended to be set by the user in most cases. Defaults to ``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 ``"GATE"``. Note ---- The indices specified in ``targets``, ``controls``, and ``anticontrols`` must be distinct. """ def __init__( self, spec: mat | arr | list[list[num | sym | 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, symbols: dict | None = None, conditions: list[tuple[num | sym | str, num | sym | str]] | None = None, conjugate: bool | None = None, exponent: num | sym | str | None = None, coefficient: num | sym | str | None = None, label: str | None = None, notation: str | None = None, family: str | None = None, ): targets = [0] if targets is None else targets controls = [] if controls is None else controls anticontrols = [] if anticontrols is None else anticontrols dim = 2 if dim is None else dim spec_num_systems = 0 if spec is None: spec = sp.eye(dim) else: spec_num_systems = count_systems(sp.Matrix(spec), dim) num_systems = ( (max(spec_num_systems, max(targets + controls + anticontrols) + 1)) if num_systems is None else num_systems ) if ( any(len(indices) != 0 for indices in [targets, controls, anticontrols]) is False ): targets = [n for n in range(0, num_systems)] 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])) QuantumObject.__init__( self, form=Forms.MATRIX.value, dim=dim, num_systems=num_systems, symbols=symbols, conditions=conditions, conjugate=conjugate, label=label, notation=notation, family=family, debug=False, ) self.spec = spec self.targets = targets self.controls = controls self.anticontrols = anticontrols self.exponent = exponent self.coefficient = coefficient @property def spec(self) -> mat | arr | list[list[num | sym | str]]: """The matrix representation of the quantum gate's operator. Provides a complete description of the operator in a standard ``dim``-dimensional basis. """ return self._spec @spec.setter def spec(self, spec: mat | arr | list[list[num | sym | str]]): self._spec = spec @property def matrix(self) -> mat: """The matrix representation of the total gate across all of its systems.""" operator = sp.Matrix(self.spec) identity = sp.eye(self.dim) ordered = [] for i in self.systems: if i not in self.targets: ordered.append(identity) if i == min(self.targets): ordered.append(operator) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass @property def targets(self) -> list[int]: """The numerical indices of the subsystems on which the gate elements reside.""" return list(set(self._targets)) @targets.setter def targets(self, targets: list[int]): 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^{0} \\Unitary^{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^{0} \\Unitary^{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 boundaries(self) -> list[int]: """An ordered list of indices of the object's boundaries corresponding to its ``labels``. Used exclusively by the visualization engine.""" return [max(flatten_list([self.targets, self.controls, self.anticontrols]))] @property def exponent(self) -> num | sym | str: """A numerical or string representation of a scalar value specifying the value to which the gate's matrix representation is exponentiated. Is guaranteed to produce valid powers only for involutory matrices. For an involutory matrix :math:`\\op{A}`, that is :math:`\\op{A}^2 = \\Identity` (where :math:`\\Identity` is the identity matrix), we have the identity, .. math:: \\exp[\\eye x \\op{A}] = \\cos(x)\\Identity + \\eye\\sin(x)\\op{A}, for any :math:`x \\in \\Complexes`. In the case of :math:`x = -\\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 | sym | str): self._exponent = exponent @property def coefficient(self) -> num | sym | str: """A numerical or string representation of a scalar value by which the gate's matrix (occupying ``targets``) is multiplied.""" return self._coefficient @coefficient.setter def coefficient(self, coefficient: num | sym | str): self._coefficient = coefficient
[docs] def output( self, conditions: list[tuple[num | sym | str, num | sym | str]] | None = None, simplify: bool | None = None, conjugate: bool | None = None, exponent: bool | num | sym | str | None = None, coefficient: bool | num | sym | str | None = None, ) -> mat: """Construct the gate and return its matrix representation. Arguments --------- conditions : list[tuple[num | sym | str, num | sym | str]] Algebraic conditions to be applied to the gate. Defaults to the value of ``self.conditions``. simplify : bool Whether to perform algebraic simplification on the gate. Defaults to ``False``. conjugate : bool Whether to perform Hermitian conjugation on the gate. If ``False``, does not conjugate. Defaults to the value of ``self.conjugate``. exponent : bool | num | sym | str The scalar value by which the gate's matrix representation is exponentiated. If ``False``, does not exponentiate. Defaults to the value of ``self.exponent``. coefficient : num | sym | str The scalar value by which the gate's matrix representation is multiplied. If ``False``, does not multiply the gate by the coefficient. Defaults to the value of ``self.coefficient``. Returns ------- mat The constructed quantum gate. """ gate = self.matrix # 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) gate = ((1 + sp.exp(sp.I * sp.pi * exponent)) / 2) * sp.eye( self.dim**self.num_systems ) + ((1 - sp.exp(sp.I * sp.pi * exponent)) / 2) * gate # Coefficient if coefficient is None or coefficient is True: coefficient = self.coefficient if coefficient is not False: coefficient = symbolize_expression(self.coefficient, self.symbols_list) gate *= coefficient gate = symbolize_expression(gate, self.symbols_list) controllers = self.controls + self.anticontrols if len(controllers) > 0: operator = gate identity = sp.eye(self.dim) for n in controllers: controller_compliment = list(set(self.systems) ^ set([n])) matrix = sp.zeros(self.dim**self.num_systems) for k in range(0, self.dim): controller = identity if n in self.controls: controller = ket(k, self.dim) * bra(k, self.dim) if n in self.anticontrols: controller = ket(self.dim - 1 - k, self.dim) * bra( self.dim - 1 - k, self.dim ) ordered = arrange( [controller_compliment, [n]], [identity] + [controller] ) controlling = sp.Matrix(TensorProduct(*ordered)) matrix += controlling * operator**k operator = matrix gate = matrix # Conditions conditions = self.conditions if conditions is None else conditions conditions = symbolize_tuples(conditions, self.symbols_list) gate = gate.subs(conditions) # Simplification simplify = False if simplify is None else simplify if simplify is True: gate = recursively_simplify(gate, conditions) # Conjugation conjugate = self.conjugate if conjugate is None else conjugate if conjugate is True: gate = Dagger(gate) return gate
[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. This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. index : int The index of the desired Pauli matrix. Can take the following values: - ``0`` (:math:`2`-dimensional identity matrix :math:`\\Identity`) - ``1`` (Pauli-X :math:`\\Pauli_x`) - ``2`` (Pauli-Y :math:`\\Pauli_y`) - ``3`` (Pauli-Z :math:`\\Pauli_z`) **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__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 ``dim`` as an argument, nor can the associated property be set. """ 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", 2), ("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 @property def matrix(self) -> mat: operator = Pauli.MATRICES[self.index] identity = sp.eye(self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange([targets_compliment, self.targets], [identity] + [operator]) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
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. This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__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: - ``0`` (:math:`3`-dimensional identity matrix :math:`\\Identity`) - ``1`` (:math:`\\GellMann_1`) - ``2`` (:math:`\\GellMann_2`) - ``3`` (:math:`\\GellMann_3`) - ``4`` (:math:`\\GellMann_4`) - ``5`` (:math:`\\GellMann_5`) - ``6`` (:math:`\\GellMann_6`) - ``7`` (:math:`\\GellMann_7`) - ``8`` (:math:`\\GellMann_8`) **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__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 ``dim`` as an argument, nor can the associated property be set.""" 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", 3), ("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 @property def matrix(self) -> mat: operator = GellMann.MATRICES[self.index] identity = sp.eye(self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange([targets_compliment, self.targets], [identity] + [operator]) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
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 matrices :math:`\\Rotation_i` are a set of three :math:`2 \\times 2` matrices, .. math:: \\begin{aligned} \\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_y &= \\e^{-\\eye\\Pauli_{y}\\theta/2} = \\begin{bmatrix} \\cos(\\theta/2) & -\\sin(\\theta/2) \\\\ \\sin(\\theta/2) & \\cos(\\theta/2) \\end{bmatrix} \\\\ \\Rotation_z &= \\e^{-\\eye\\Pauli_{z}\\theta/2} = \\begin{bmatrix} \\e^{-\\eye\\theta/2} & 0 \\\\ 0 & \\e^{\\eye\\theta/2} \\end{bmatrix} \\end{aligned} where :math:`\\theta` is the rotation angle (``angle``). 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 ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__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: - ``1`` (:math:`x`-rotation :math:`\\Rotation_x`) - ``2`` (:math:`y`-rotation :math:`\\Rotation_y`) - ``3`` (:math:`z`-rotation :math:`\\Rotation_z`) angle : num | sym | str The scalar value to be used as the rotation angle. Defaults to ``0``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. Note ---- The rotation gates are defined only for :math:`2`-dimensional (i.e., binary/qubit) systems. This means that the constructor does not take ``dim`` as an argument, nor can the associated property be set. """ DIM = 2 def __init__( self, *args, axis: int, angle: num | sym | 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", 2), ("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 | sym | str: """The scalar value to be used as the rotation angle.""" return self._angle @angle.setter def angle(self, angle: num | sym | str): self._angle = angle @property def matrix(self) -> mat: operator = sp.eye(self.dim) 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)]] ) identity = sp.eye(self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange([targets_compliment, self.targets], [identity] + [operator]) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
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. In :math:`\\Dimension` dimensions, a phase operator :math:`\\Phase` may be represented as a :math:`\\Dimension \\times \\Dimension` diagonal matrix .. math:: \\Phase(\\omega) = \\sum\\limits_{k=0}^{\\Dimension - 1} \\omega^k \\ket{k}\\bra{k} where :math:`\\omega` is the *phase* factor. This is fundamentally a single-system gate, and so a copy is placed on each of the subsystems corresponding to the indices in the ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. phase : num | sym | str The phase value. Defaults to the unit root given by ``sp.exp(2 * sp.pi * sp.I / self.dim)``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. """ def __init__( self, *args, phase: num | sym | 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 | sym | str: """The phase value.""" return self._phase @phase.setter def phase(self, phase: num | sym | str): self._phase = phase @property def matrix(self) -> mat: identity = sp.eye(self.dim) operator = sp.eye(self.dim) phase = symbolize_expression(self.phase, self.symbols_list) operator = sp.zeros(self.dim) for k in range(0, self.dim): operator += phase**k * ket(k, self.dim) * bra(k, self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange([targets_compliment, self.targets], [identity] + [operator]) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
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. In :math:`\\Dimension` dimensions, a diagonal operator :math:`\\Diagonal` may be represented as a :math:`\\Dimension \\times \\Dimension` diagonal matrix .. math:: \\Diagonal(\\lambda_0, \\lambda_1, \\ldots, \\lambda_{\\Dimension - 1}) = \\sum\\limits_{k=0}^{\\Dimension - 1} \\lambda_k\\ket{k}\\bra{k}, \\quad \\lambda_k \\in \\Complexes, \\; \\abs{\\lambda_k} = 1 where :math:`\\{\\lambda_k\\}_{k=0}^{\\Dimension - 1}` are the main diagonal *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 ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. entries : dict[int | list[int], num | sym | str] A dictionary in which the keys are level specifications (integer or list of integers) and the values are scalars. exponentiation : bool Whether to exponentiate (with imaginary unit) the values given in ``entries``. Defaults to ``False``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. Note ---- Levels that are unspecified in the ``entries`` argument all have a corresponding matrix element of ``1``, regardless of the value of ``exponentiation``. """ def __init__( self, *args, entries: dict[int | list[int], num | sym | 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 | sym | str]: """A dictionary in which the keys are level 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 | sym | str]): self._entries = entries @property def exponentiation(self) -> bool: """Whether to exponentiate (with imaginary unit) the values given in ``entries``.""" return self._exponentiation @exponentiation.setter def exponentiation(self, exponentiation: bool): self._exponentiation = exponentiation @property def matrix(self) -> mat: identity = sp.eye(self.dim) operator = sp.eye(self.dim) 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) projector = ket(key, self.dim) * bra(key, self.dim) operator = operator + (coefficient - 1) * projector targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange([targets_compliment, self.targets], [identity] + [operator]) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
DIAG = Diagonal class Permutation(QuantumGate): """A subclass for creating permutation 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. TODO ---- Change this gate such that permutations can be defined on subsets of the total system space and so do not have to contain the index of every subsystem. This means that the gates should also be controllable. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. permutation : int A list of system indices representing the positional arrangement of the systems as a result of the transformation. Must contain all of the system indices. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. Note ---- When specifying a value for the ``permutation`` argument at instantiation, a value for the ``targets`` argument need not be supplied as the associated property will automatically be set. """ def __init__(self, *args, permutation: list[int], **kwargs): self.permutation = permutation args, kwargs = default_arguments(args, kwargs, QuantumGate, [("label", "P")]) args, kwargs = fix_arguments(args, kwargs, QuantumGate, [("spec", None)]) super().__init__(*args, **kwargs) @property def permutation(self) -> list[int]: return self._permutation @permutation.setter def permutation(self, permutation: list[int]): self._permutation = permutation self.targets = list(set(flatten_list(permutation))) @property def targets(self) -> list[int]: return list(set(flatten_list(self.permutation))) @targets.setter def targets(self, targets: list[int]): pass @property def matrix(self) -> mat: possibility = [k for k in range(0, self.dim)] possibilities = [possibility for k in self.systems] combinations = list(itertools.product(*possibilities)) matrix = sp.zeros(self.dim**self.num_systems) for n in range(0, self.dim**self.num_systems): level = list(combinations[n]) permuted = [level[self.permutation[k]] for k in self.targets] matrix = matrix + ket(permuted, self.dim) * bra(level, self.dim) return matrix @matrix.setter def matrix(self, matrix: mat): pass PERM = Permutation
[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. In :math:`\\Dimension` dimensions, a SWAP operator :math:`\\Swap` between two (neighbouring) systems :math:`A` and :math:`B` may be represented as a :math:`\\Dimension^2 \\times \\Dimension^2` matrix .. math:: \\Swap^{A,B} = \\sum\\limits_{j,k=0}^{\\Dimension - 1} {\\ket{j}\\bra{k}}^A \\otimes {\\ket{k}\\bra{j}}^B, where the identity operator acts on all other systems. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__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 ``*args``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.""" def __init__(self, *args, **kwargs): args, kwargs = default_arguments( args, kwargs, QuantumGate, [("label", "S"), ("family", "SWAP")] ) 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." ) @property def matrix(self) -> mat: 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 = sp.zeros(self.dim**self.num_systems) 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, self.dim) * bra(level, self.dim) return matrix @matrix.setter def matrix(self, matrix: mat): pass
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. In :math:`\\Dimension` dimensions, it is defined as the operator .. math:: \\SUM(n) = \\sum\\limits_{k=0}^{\\Dimension - 1} \\ket{k \\oplus n}\\bra{k} where :math:`n \\in \\Integers_{\\geq 0}` (``shift``) is the *shift* parameter, 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-X :math:`\\Pauli_x` operator 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 ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. shift : int The summation shift parameter. Must be a non-negative integer. Defaults to ``1``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.""" 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 summation shift parameter.""" return self._shift @shift.setter def shift(self, shift: int): self._shift = shift @property def matrix(self) -> mat: identity = sp.eye(self.dim) summation = sp.zeros(self.dim) for k in range(0, self.dim): oplus = (k + self.shift) % self.dim summation = summation + ket(oplus, self.dim) * bra(k, self.dim) matrix = sp.Matrix([1]) for m in range(0, self.num_systems): if m in list(self.targets): matrix = sp.Matrix(TensorProduct(matrix, summation)) else: matrix = sp.Matrix(TensorProduct(matrix, identity)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
SUM = Summation
[docs] class Not(Summation): """A subclass for creating NOT (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 (i.e., binary/qubit) systems, and is exactly equivalent to the Pauli-X gate. 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 ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__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 ``dim`` as an argument, nor can the associated property be set.""" DIM = 2 def __init__(self, *args, **kwargs): args, kwargs = default_arguments( args, kwargs, QuantumGate, [("label", "X"), ("family", "TARG")] ) args, kwargs = fix_arguments( args, kwargs, QuantumGate, [("dim", 2), ("spec", None)] ) super().__init__(*args, shift=1, **kwargs) @property def dim(self) -> int: return Not.DIM @dim.setter def dim(self, dim: 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. The elementary Hadamard gate :math:`\\Hadamard` (for qubits) may be represented as the :math:`2 \\times 2` operator .. 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} This can be generalized to the following :math:`\\Dimension`-dimensional form for qudits, .. 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^{\\frac{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 ``targets`` property. Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. """ 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) @property def matrix(self) -> mat: # operator = (1 / sp.sqrt(2)) * sp.Matrix([[1, 1], [1, -1]]) omega = sp.exp(2 * sp.pi * sp.I / self.dim) operator = sp.zeros(self.dim) for i in range(0, self.dim): for j in range(0, self.dim): operator += ( omega ** (j * (self.dim - i)) * ket(i, dim=self.dim) * bra(j, dim=self.dim) ) operator *= 1 / sp.sqrt(self.dim) identity = sp.eye(self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange([targets_compliment, self.targets], [identity] + [operator]) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
HAD = Hadamard
[docs] class Fourier(QuantumGate): """A subclass for creating Fourier (quantum discrete Fourier transform [QDFT]) 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 Fourier operator :math:`\\QFT` for a single :math:`\\Dimension`-dimensional qudit may be represented as 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_{\\Dimension} = \\e^{\\frac{2\\pi\\eye}{\\Dimension}} = \\omega`. In the case of :math:`N` qudits, the action of the multipartite Fourier operator :math:`\\QFT_N` 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 ``composite`` is ``True``, a copy of the elementary form :math:`\\QFT` is placed on each of the subsystems corresponding to the indices in the ``targets`` property. If ``composite`` is ``False``, the composite form :math:`\\QFT_N` is applied to the subsystems specified by ``targets`` in: - *ascending* order if ``reverse`` is ``False`` - *descending* order if ``reverse`` is ``True`` Arguments --------- *args Variable-length argument list, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. composite : bool Whether the composite (multipartite) Fourier gate is to be used. If ``False``, copies of the elementary Fourier gate are placed on each index specified in ``targets``. Defaults to ``True``. reverse : bool Whether to reverse the order in which the composite (multipartite) Fourier gate is applied. Only applies when ``composite`` is ``False``. Defaults to ``False``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. """ 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 ``self.composite`` is ``False``. """ return self._reverse @reverse.setter def reverse(self, reverse: bool): self._reverse = reverse @property def matrix(self) -> mat: 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], dim=self.dim, num_systems=self.num_systems ) ) else: QFT.append( Phase( targets=[targets[i + j]], controls=[t], exponent=sp.Rational(1, (self.dim**j)), dim=self.dim, num_systems=self.num_systems, label=f"1 / {self.dim**j}", family="GATE", ) ) matrix = sp.eye(self.dim**self.num_systems) for gate in QFT: matrix = gate.output() * matrix else: omega = sp.exp(2 * sp.pi * sp.I / self.dim) operator = sp.zeros(self.dim) for i in range(0, self.dim): for j in range(0, self.dim): operator += ( omega ** (j * i) * ket(i, dim=self.dim) * bra(j, dim=self.dim) ) operator *= 1 / sp.sqrt(self.dim) identity = sp.eye(self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) ordered = arrange( [targets_compliment, self.targets], [identity] + [operator] ) matrix = sp.Matrix(TensorProduct(*ordered)) return matrix @matrix.setter def matrix(self, matrix: mat): pass
QDFT = 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 ``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 ``observable`` is ``False`` (``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 ``observable`` is ``True`` (``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 ``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 measurements. 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 Variable-length argument list, passed directly to the constructor ``__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 ``operators`` as observables (as opposed to Kraus operators or projectors). Defaults to ``False``. **kwargs Arbitrary keyword arguments, passed directly to the constructor ``__init__`` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`. Note ---- Measurement operations in quantum physics are, in general, non-linear and non-unitary operations on (normalized) state vectors and density operators. As such, they cannot be represented by matrices, and so the ``matrix`` property therefore does not return a valid representation of the measurement operation. Instead, it returns an identity matrix of the appropriate size for its number of dimensions and systems. Note ---- The ``targets`` argument must be specified as a list of numerical indices of the subsystem(s) to be measured. These indices must be consecutive, and their number 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 ``operators`` property as observables (as opposed to Kraus operators or projectors).""" return self._observable @observable.setter def observable(self, observable: bool): self._observable = observable @property def matrices(self) -> list[mat]: """A list of matrix representations of all operators in the ``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 ``Measurement`` gate instances in their ``gates`` property. """ matrices = [] identity = sp.eye(self.dim) targets_compliment = list(set(self.systems) ^ set(self.targets)) for operator in self.operators: operator = densify(extract_matrix(operator)) ordered = arrange( [targets_compliment, self.targets], [identity] + [operator] ) matrix = sp.Matrix(TensorProduct(*ordered)) matrices.append(matrix) return matrices @property def matrix(self) -> mat: return sp.eye(self.dim**self.num_systems) @matrix.setter def matrix(self, matrix: mat): pass @property def form(self) -> str: return Forms.MATRIX.value @form.setter def form(self, form: str): pass
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 matrix representations of the gates together. 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 ``gates`` property. Arguments --------- *gates : QuantumGate Variable-length argument list of :py:class:`~qhronology.quantum.gates.QuantumGate` instances to be interleaved. merge : bool Whether to merge the gates together diagrammatically. Defaults to ``False``. conjugate : bool Whether to perform Hermitian conjugation on the composite gate when it is called. Defaults to ``False``. exponent : num | sym | str A numerical or string representation of a scalar value to which composite gate's total matrix representation is exponentiated. Must be a non-negative integer. Defaults to ``1``. coefficient : num | sym | str A numerical or string representation of a scalar value by which the composite gate's matrix representation is multiplied. Performed after exponentiation. Defaults to ``1``. label : str The unformatted string used to represent the gate in mathematical expressions. Defaults to ``"⊗".join([gate.label for gate in [*gates]])``. notation : str The formatted string used to represent the gate in mathematical expressions. When not ``None``, overrides the value passed to ``label``. Not intended to be set by the user in most cases. Defaults to ``None``. Note ---- Care should be taken to ensure that gates passed to this class all have the same ``num_systems`` value and do not have overlapping ``targets``, ``controls``, and ``anticontrols`` properties. Note ---- The resulting visualization (using the ``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, conjugate: bool | None = None, exponent: num | sym | str | None = None, coefficient: num | sym | str | None = None, label: str | None = None, notation: str | None = None, ): self.gates = [*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 super().__init__( spec=None, conjugate=conjugate, exponent=exponent, coefficient=coefficient, label=label, notation=notation, ) def __str__(self) -> str: return str(self.notation) + " = " + stringify(self.output(), self.dim) def __repr__(self) -> str: return repr(self.output()) @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: num_systems = list(set(flatten_list([gate.num_systems for gate in self.gates]))) if len(num_systems) != 1: raise ValueError("Mismatch between one or more of the number of systems.") return num_systems[0] @num_systems.setter def num_systems(self, num_systems: int): pass @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 conditions(self) -> list[tuple[num | sym | str, num | sym | str]]: conditions = [] for gate in self.gates: conditions += gate.conditions return conditions @conditions.setter def conditions(self, conditions): pass @property def matrix(self) -> mat: spec = sp.eye(self.dim**self.num_systems) for gate in self.gates: spec = ( gate.output(conditions=gate.conditions, exponent=gate.exponent) * spec ) return spec @matrix.setter def matrix(self, matrix: mat): pass def output( self, conditions: list[tuple[num | sym | str, num | sym | str]] | None = None, simplify: bool | None = None, conjugate: bool | None = None, exponent: bool | num | sym | str | None = None, coefficient: bool | num | sym | str | None = None, ) -> mat: """Construct the composite gate and return its matrix representation. Arguments --------- conditions : list[tuple[num | sym | str, num | sym | str]] Algebraic conditions to be applied to the gate. Defaults to the value of ``self.conditions``. simplify : bool Whether to perform algebraic simplification on the gate. Defaults to ``False``. conjugate : bool Whether to perform Hermitian conjugation on the gate. If ``False``, does not conjugate. Defaults to the value of ``self.conjugate``. exponent : bool | num | sym | str The scalar value by which the gate's matrix representation is exponentiated. If ``False``, does not exponentiate. Defaults to the value of ``self.exponent``. coefficient : num | sym | str The scalar value by which the gate's matrix representation is multiplied. If ``False``, does not multiply the gate by the coefficient. Defaults to the value of ``self.coefficient``. Returns ------- mat The constructed quantum gate. """ gate = self.matrix # 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) gate = ((1 + sp.exp(-sp.I * sp.pi * exponent)) / 2) * sp.eye( self.dim**self.num_systems ) + ((1 - sp.exp(-sp.I * sp.pi * exponent)) / 2) * gate # Coefficient if coefficient is None or coefficient is True: coefficient = self.coefficient if coefficient is not False: coefficient = symbolize_expression(self.coefficient, self.symbols_list) gate *= coefficient gate = symbolize_expression(gate, self.symbols_list) # Conditions conditions = self.conditions if conditions is None else conditions conditions = symbolize_tuples(conditions, self.symbols_list) gate = gate.subs(conditions) # Simplification simplify = False if simplify is None else simplify if simplify is True: gate = recursively_simplify(gate, conditions) # Conjugation conjugate = self.conjugate if conjugate is None else conjugate if conjugate is True: gate = Dagger(gate) return gate
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 matrix representations of the gates together. 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 Variable-length argument list of :py:class:`~qhronology.quantum.gates.QuantumGate` instances to be stacked. merge : bool Whether to merge the gates together diagrammatically. Defaults to ``False``. conjugate : bool Whether to perform Hermitian conjugation on the composite gate when it is called. Defaults to ``False``. exponent : num | sym | str A numerical or string representation of a scalar value to which composite gate's total matrix representation is exponentiated. Defaults to ``1``. coefficient : num | sym | str A numerical or string representation of a scalar value by which the composite gate's matrix representation is multiplied. Performed after exponentiation. Defaults to ``1``. label : str The unformatted string used to represent the gate in mathematical expressions. Defaults to ``"⊗".join([gate.label for gate in [*gates]])``. notation : str The formatted string used to represent the gate in mathematical expressions. When not ``None``, overrides the value passed to ``label``. Not intended to be set by the user in most cases. Defaults to ``None``.""" def __init__( self, *gates: QuantumGate, merge: bool | None = None, conjugate: bool | None = None, exponent: num | sym | str | None = None, coefficient: num | sym | str | None = None, label: str | None = None, notation: str | None = None, ): super().__init__( *gates, merge=merge, conjugate=conjugate, exponent=exponent, coefficient=coefficient, 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 sum([gate.num_systems for gate in self.gates]) @num_systems.setter def num_systems(self, num_systems: int): pass @property def matrix(self) -> mat: matrices = [ gate.output(conditions=gate.conditions, exponent=gate.exponent) for gate in self.gates ] return sp.Matrix(TensorProduct(*matrices)) @matrix.setter def matrix(self, matrix: mat): pass
STACK = GateStack class _Single(QuantumGate): """A :py:class:`~qhronology.quantum.gates.QuantumGate` subclass for creating single-cell abstract quantum gates. Used internally exclusively 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, ) @property def matrix(self) -> mat: return sp.eye(self.dim**self.num_systems) @matrix.setter def matrix(self, matrix: mat): pass class _Empty(QuantumGate): """A :py:class:`~qhronology.quantum.gates.QuantumGate` subclass for creating single-cell empty quantum gates. Used internally exclusively 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 ) @property def matrix(self) -> mat: return sp.eye(self.dim**self.num_systems) @matrix.setter def matrix(self, matrix: mat): pass class _Wormhole(QuantumGate): """A :py:class:`~qhronology.quantum.gates.QuantumGate` subclass for creating single-cell wormhole (mouth) quantum gates. Used internally exclusively 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 ) @property def matrix(self) -> mat: return sp.eye(self.dim**self.num_systems) @matrix.setter def matrix(self, matrix: mat): pass