Source code for pprop.gates.controlled

"""
This submodule defines :class:`ControlledGate`, the base class for two-qubit
non-parametrised controlled gates, and the concrete gates :class:`CNOT`,
:class:`CY`, and :class:`CZ`.
"""
from typing import Dict, List, Optional, Tuple

from pennylane import CNOT as qmlCNOT
from pennylane import CY as qmlCY
from pennylane import CZ as qmlCZ

from ..pauli.op import PauliOp
from ..pauli.sentence import CoeffTerms, PauliDict
from .base import Gate

# Rule type: maps a two-character Pauli string (control + target) to
# ((output_control, output_target), sign).
# e.g. "IY" -> (("Z", "Y"), +1)
EvolutionRule = Dict[str, Tuple[Tuple[str, str], int]]


[docs] class ControlledGate(Gate): """ Base class for two-qubit non-parametrised controlled gates. The Heisenberg evolution rule is encoded as a dict keyed by two-character strings ``"PQ"`` where ``P`` is the Pauli at the control wire and ``Q`` is the Pauli at the target wire. Each entry maps to an ``((output_control, output_target), sign)`` tuple. Two-qubit Pauli combinations absent from the dict commute with the gate and pass through unchanged. Parameters ---------- wires : list[int] ``[control, target]`` qubit indices. qml_gate : pennylane.operation.Operator Corresponding PennyLane gate class, used for circuit drawing. parameter : int or None Index into the parameter vector. Controlled gates are non-parametrised, so this is always ``None``. rule : EvolutionRule Dict mapping a two-character Pauli string (e.g. ``"IY"``) to a ``((output_control, output_target), sign)`` pair. Attributes ---------- rule : EvolutionRule The Heisenberg evolution rule for this gate. """ def __init__( self, wires, qml_gate, parameter, rule: EvolutionRule, ) -> None: super().__init__(wires=wires, qml_gate=qml_gate, parameter=parameter) self.rule = rule
[docs] def evolve(self, word: Tuple[PauliOp, CoeffTerms], k1, k2) -> PauliDict: """ Heisenberg-evolve a Pauli word through this controlled gate. Looks up the two-character Pauli string ``op[control] + op[target]`` in ``self.rule``. If absent the word commutes with the gate and is returned unchanged. Otherwise both qubits are updated and all scalar coefficients are multiplied by the sign. After applying the rule, the evolved word is checked against the Pauli weight cutoff ``k1``. If its weight exceeds ``k1`` it is discarded entirely (returning an empty :class:`~pprop.pauli.sentence.PauliDict`). Parameters ---------- word : tuple[PauliOp, CoeffTerms] ``(pauliop, coeff_terms)`` pair to evolve. k1 : int or None Pauli weight cutoff. Evolved words with weight exceeding ``k1`` are discarded. ``None`` disables truncation. k2 : int or None Frequency cutoff (unused, controlled gates do not change frequency). Returns ------- PauliDict Empty if truncated by ``k1``; one entry otherwise. """ op, coeff_terms = word wire0, wire1 = self.wires # Look up the two-qubit Pauli combination at (control, target). rule = self.rule.get(op[wire0] + op[wire1], None) # If no rule exists this Pauli commutes with the gate, pass through unchanged. if rule is None: return PauliDict({op: coeff_terms}) (out0, out1), sign = rule new_op = op.copy() new_op.set(wire0, out0) new_op.set(wire1, out1) # Discard if the evolved word exceeds the Pauli weight cutoff. if k1 is not None and new_op.weight() > k1: return PauliDict() # Scale coefficients by the sign; avoid unnecessary list comprehension for +1. if sign == 1: new_terms = list(coeff_terms) else: new_terms: CoeffTerms = [(sign * c, s, cc) for c, s, cc in coeff_terms] return PauliDict({new_op: new_terms})
[docs] class CNOT(ControlledGate): r""" The Controlled-NOT (CX) gate. .. math:: \text{CNOT} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{bmatrix} The Heisenberg evolution maps each two-qubit Pauli string ``control ⊗ target`` according to the rule dict. All other combinations commute with the gate. Parameters ---------- wires : list[int] ``[control, target]`` qubit indices. parameter : float, int, optional Unused. Defaults to ``None``. """ def __init__(self, wires: List[int], parameter: Optional[int] = None) -> None: rule: EvolutionRule = { "IY": (("Z", "Y"), +1), "IZ": (("Z", "Z"), +1), "XI": (("X", "X"), +1), "XX": (("X", "I"), +1), "XY": (("Y", "Z"), +1), "XZ": (("Y", "Y"), -1), "YI": (("Y", "X"), +1), "YX": (("Y", "I"), +1), "YY": (("X", "Z"), -1), "YZ": (("X", "Y"), +1), "ZY": (("I", "Y"), +1), "ZZ": (("I", "Z"), +1), } super().__init__(wires, qmlCNOT, parameter, rule)
[docs] class CY(ControlledGate): r""" The Controlled-Y gate. .. math:: \text{CY} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & -i \\ 0 & 0 & i & 0 \end{bmatrix} Parameters ---------- wires : list[int] ``[control, target]`` qubit indices. parameter : float, int, optional Unused. Defaults to ``None``. """ def __init__(self, wires: List[int], parameter: Optional[int] = None) -> None: rule: EvolutionRule = { "IX": (("Z", "X"), +1), "IZ": (("Z", "Z"), +1), "XI": (("X", "Y"), +1), "XX": (("Y", "Z"), -1), "XY": (("X", "I"), +1), "XZ": (("Y", "X"), +1), "YI": (("Y", "Y"), +1), "YX": (("X", "Z"), +1), "YY": (("Y", "I"), +1), "YZ": (("X", "X"), -1), "ZX": (("I", "X"), +1), "ZZ": (("I", "Z"), +1), } super().__init__(wires, qmlCY, parameter, rule)
[docs] class CZ(ControlledGate): r""" The Controlled-Z gate. .. math:: \text{CZ} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & -1 \end{bmatrix} Parameters ---------- wires : list[int] ``[control, target]`` qubit indices. parameter : float, int, optional Unused. Defaults to ``None``. """ def __init__(self, wires: List[int], parameter: Optional[int] = None) -> None: rule: EvolutionRule = { "IX": (("Z", "X"), +1), "IY": (("Z", "Y"), +1), "XI": (("X", "Z"), +1), "XX": (("Y", "Y"), +1), "XY": (("Y", "X"), -1), "XZ": (("X", "I"), +1), "YI": (("Y", "Z"), +1), "YX": (("X", "Y"), -1), "YY": (("X", "X"), +1), "YZ": (("Y", "I"), +1), "ZX": (("I", "X"), +1), "ZY": (("I", "Y"), +1), } super().__init__(wires, qmlCZ, parameter, rule)