"""
This submodule defines :class:`ControlledRotationGate`, the base class for
single-parameter controlled rotation gates, and the concrete gates
:class:`CRX`, :class:`CRY`, and :class:`CRZ`.
Coefficient encoding
--------------------
Controlled rotation gates produce factors of the form
:math:`\\cos(\\theta/2)`, :math:`\\sin(\\theta/2)`,
:math:`\\cos^2(\\theta/2)`, :math:`\\sin^2(\\theta/2)`, and
:math:`\\sin(\\theta/2)\\cos(\\theta/2)`. Setting :math:`p` =
``parameter``, these map directly onto :data:`~pprop.pauli.sentence.CoeffTerm`
tuples with repeated indices:
.. list-table::
:header-rows: 1
* - Factor
- CoeffTerm multiplier
* - :math:`\\cos(\\theta/2)`
- ``(1.0, [], [p])``
* - :math:`\\sin(\\theta/2)`
- ``(1.0, [p], [])``
* - :math:`\\cos^2(\\theta/2) = (1+\\cos\\theta)/2`
- ``(1.0, [], [p, p])``
* - :math:`\\sin^2(\\theta/2) = (1-\\cos\\theta)/2`
- ``(1.0, [p, p], [])``
* - :math:`\\sin(\\theta/2)\\cos(\\theta/2) = \\sin(\\theta)/2`
- ``(1.0, [p], [p])``
.. warning::
**Half-angle convention for** ``cos(θ/2)`` **and** ``sin(θ/2)`` **terms.**
Rules involving ``cos(θ/2)`` or ``sin(θ/2)`` (e.g. the ``"XI"``, ``"YI"``
entries) cannot be represented exactly as :data:`~pprop.pauli.sentence.CoeffTerm`
tuples in ``θ``, only in ``θ/2``. To match PennyLane's output exactly,
**the user must pass** ``θ/2`` **as the parameter** for any ``CRX``,
``CRY``, or ``CRZ`` gate in the ansatz:
.. code-block:: python
# Correct: pass theta/2 so that pprop and PennyLane agree
qml.CRX(params[i] / 2, wires=[0, 1])
# Wrong: pprop will NOT match PennyLane
qml.CRX(params[i], wires=[0, 1])
The ``(1 \\pm \\cos\\theta)/2`` and ``\\sin(\\theta)/2`` factors (e.g.
``"IY"``, ``"ZZ"`` entries) are represented exactly in ``θ`` and require
no rescaling.
"""
from __future__ import annotations
from typing import Dict, List, Tuple
from numpy import cos, integer, intp, prod, sin
from pennylane import CRX as qmlCRX
from pennylane import CRY as qmlCRY
from pennylane import CRZ as qmlCRZ
from ..pauli.op import PauliOp
from ..pauli.sentence import CoeffTerms, PauliDict
from .base import Gate
from .utils import get_frequency
# ---------------------------------------------------------------------------
# Type alias
# ---------------------------------------------------------------------------
# Each rule entry maps a two-character Pauli string to a list of
# (output_label_pair, CoeffTerm_multiplier) pairs.
# The multiplier is a single CoeffTerm (c, sin_idx, cos_idx) expressed in
# terms of the gate's parameter index p; the actual index is substituted
# at evolve-time.
# We store the sin/cos index lists as relative placeholders (using -1) and
# replace -1 with self.parameter inside evolve().
_RuleEntry = List[Tuple[str, Tuple[float, List[int], List[int]]]]
EvolutionRule = Dict[str, _RuleEntry]
# Sentinel value used as a placeholder for parameter in the rule dicts.
_P = -1
[docs]
class ControlledRotationGate(Gate):
"""
Base class for single-parameter two-qubit controlled rotation gates.
Unlike :class:`~pprop.gates.rotation_gate.RotationGate`, controlled
rotations act non-trivially only when the control qubit is in the
:math:`|1\\rangle` state. This produces factors of
:math:`\\cos(\\theta/2)`, :math:`\\sin(\\theta/2)`, and their squares,
each encoded as a :data:`~pprop.pauli.sentence.CoeffTerm` with repeated
``parameter`` entries (see module docstring for the full table).
Each rule entry maps a two-character Pauli string ``"PQ"``
(control ⊗ target) to a list of ``(output_label, multiplier)`` pairs,
where ``multiplier`` is a :data:`~pprop.pauli.sentence.CoeffTerm` with
``-1`` as a placeholder for ``parameter``.
Parameters
----------
wires : list[int]
``[control, target]`` qubit indices.
qml_gate : pennylane.operation.Operation
Corresponding PennyLane gate class.
parameter : int, float
Index of :math:`\\theta` in the global parameter vector if int.
Actual value of the rotation if float.
rule : EvolutionRule
Heisenberg evolution rule dict.
Attributes
----------
rule : EvolutionRule
The 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 rotation gate.
For each matching rule entry the existing :data:`CoeffTerms` are
scaled by the rule's multiplier: the multiplier's ``sin_idx`` and
``cos_idx`` (which use ``-1`` as a placeholder) are substituted with
``self.parameter`` and then appended to every existing term's
index lists.
The weight cutoff ``k1`` is checked on the output Pauli word.
The frequency cutoff ``k2`` is checked on each existing term before
appending new trig factors.
Parameters
----------
word : tuple[PauliOp, CoeffTerms]
``(pauliop, coeff_terms)`` pair to evolve.
k1 : int or None
Pauli weight cutoff.
k2 : int or None
Frequency cutoff.
Returns
-------
PauliDict
Evolved Pauli words with updated :data:`CoeffTerms`.
"""
op, coeff_terms = word
wire0, wire1 = self.wires
p = self.parameter
rule = self.rule.get(op[wire0] + op[wire1], None)
# Word commutes with the gate, pass through unchanged.
if rule is None:
return PauliDict({op: coeff_terms})
evolved = PauliDict()
for output_label, (m_coeff, m_sin, m_cos) in rule:
# Build the output Pauli word.
new_op = op.copy()
new_op.set(wire0, output_label[0])
new_op.set(wire1, output_label[1])
# Discard if evolved word exceeds Pauli weight cutoff.
if k1 is not None and new_op.weight() > k1:
continue
# Substitute the placeholder -1 with the actual parameter index.
sin_ext = [p if i == _P else i for i in m_sin]
cos_ext = [p if i == _P else i for i in m_cos]
# Scale each existing term by the multiplier and extend index lists.
new_terms: CoeffTerms = []
for c, s, cc in coeff_terms:
if isinstance(self.parameter, (integer, intp)):
# Discard if adding new trig factors would exceed frequency cutoff.
if k2 is not None and get_frequency((c, s, cc)) + len(sin_ext) + len(cos_ext) > k2:
continue
new_terms.append((
m_coeff * c,
list(s) + sin_ext,
list(cc) + cos_ext,
))
else:
new_terms.append((
m_coeff * c * prod(sin(sin_ext)) * prod(cos(cos_ext)),
list(s),
list(cc),
))
if new_terms:
evolved.add_terms(new_op, new_terms)
return evolved
# ---------------------------------------------------------------------------
# Concrete gates
# ---------------------------------------------------------------------------
[docs]
class CRX(ControlledRotationGate):
r"""
The controlled-:math:`R_x` gate.
.. math::
CR_x(\theta) = \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & \cos(\theta) & -i\sin(\theta) \\
0 & 0 & -i\sin(\theta) & \cos(\theta)
\end{bmatrix}
.. note::
The parameter ``θ`` here corresponds to ``θ/2`` in PennyLane's convention.
Pass ``params[i] / 2`` to ``qml.CRX`` to match.
Parameters
----------
wires : list[int]
``[control, target]`` qubit indices.
parameter : int
Index of :math:`\\theta` in the global parameter vector.
"""
def __init__(self, wires: List[int], parameter: int) -> None:
# Multiplier encoding (using _P as placeholder for parameter):
# cos(t/2) -> (1.0, [], [_P])
# sin(t/2) -> (1.0, [_P], [])
# -sin(t/2) -> (-1.0,[_P], [])
# cos²(t/2) -> (1.0, [], [_P, _P])
# sin²(t/2) -> (1.0, [_P, _P], [])
# sin(t/2)cos(t/2) -> (1.0,[_P], [_P])
# -sin(t/2)cos(t/2) -> (-1.0,[_P], [_P])
rule: EvolutionRule = {
"IY": [("IY", (1.0, [], [_P, _P])),
("IZ", (-1.0, [_P], [_P])),
("ZY", (1.0, [_P, _P], [])),
("ZZ", (1.0, [_P], [_P]))],
"IZ": [("IZ", (1.0, [], [_P, _P])),
("IY", (1.0, [_P], [_P])),
("ZZ", (1.0, [_P, _P], [])),
("ZY", (-1.0, [_P], [_P]))],
"XI": [("XI", (1.0, [], [_P])),
("YX", (1.0, [_P], []))],
"XX": [("XX", (1.0, [], [_P])),
("YI", (1.0, [_P], []))],
"XY": [("XY", (1.0, [], [_P])),
("XZ", (-1.0, [_P], []))],
"XZ": [("XZ", (1.0, [], [_P])),
("XY", (1.0, [_P], []))],
"YI": [("YI", (1.0, [], [_P])),
("XX", (-1.0, [_P], []))],
"YX": [("YX", (1.0, [], [_P])),
("XI", (-1.0, [_P], []))],
"YY": [("YY", (1.0, [], [_P])),
("YZ", (-1.0, [_P], []))],
"YZ": [("YZ", (1.0, [], [_P])),
("YY", (1.0, [_P], []))],
"ZY": [("ZY", (1.0, [], [_P, _P])),
("ZZ", (-1.0, [_P], [_P])),
("IY", (1.0, [_P, _P], [])),
("IZ", (1.0, [_P], [_P]))],
"ZZ": [("ZZ", (1.0, [], [_P, _P])),
("ZY", (1.0, [_P], [_P])),
("IZ", (1.0, [_P, _P], [])),
("IY", (-1.0, [_P], [_P]))],
}
super().__init__(wires, qmlCRX, parameter, rule)
[docs]
class CRY(ControlledRotationGate):
r"""
The controlled-:math:`R_y` gate.
.. math::
CR_y(\theta) = \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & \cos(\theta) & -\sin(\theta) \\
0 & 0 & \sin(\theta) & \cos(\theta)
\end{bmatrix}
.. note::
The parameter ``θ`` here corresponds to ``θ/2`` in PennyLane's convention.
Pass ``params[i] / 2`` to ``qml.CRY`` to match.
Parameters
----------
wires : list[int]
``[control, target]`` qubit indices.
parameter : int
Index of :math:`\\theta` in the global parameter vector.
"""
def __init__(self, wires: List[int], parameter: int) -> None:
rule: EvolutionRule = {
"IX": [("IX", (1.0, [], [_P, _P])),
("IZ", (1.0, [_P], [_P])),
("ZX", (1.0, [_P, _P], [])),
("ZZ", (-1.0, [_P], [_P]))],
"IZ": [("IZ", (1.0, [], [_P, _P])),
("IX", (-1.0, [_P], [_P])),
("ZZ", (1.0, [_P, _P], [])),
("ZX", (1.0, [_P], [_P]))],
"XI": [("XI", (1.0, [], [_P])),
("YY", (1.0, [_P], []))],
"XX": [("XX", (1.0, [], [_P])),
("XZ", (1.0, [_P], []))],
"XY": [("XY", (1.0, [], [_P])),
("YI", (1.0, [_P], []))],
"XZ": [("XZ", (1.0, [], [_P])),
("XX", (-1.0, [_P], []))],
"YI": [("YI", (1.0, [], [_P])),
("XY", (-1.0, [_P], []))],
"YX": [("YX", (1.0, [], [_P])),
("YZ", (1.0, [_P], []))],
"YY": [("YY", (1.0, [], [_P])),
("XI", (-1.0, [_P], []))],
"YZ": [("YZ", (1.0, [], [_P])),
("YX", (-1.0, [_P], []))],
"ZX": [("ZX", (1.0, [], [_P, _P])),
("ZZ", (1.0, [_P], [_P])),
("IX", (1.0, [_P, _P], [])),
("IZ", (-1.0, [_P], [_P]))],
"ZZ": [("ZZ", (1.0, [], [_P, _P])),
("ZX", (-1.0, [_P], [_P])),
("IZ", (1.0, [_P, _P], [])),
("IX", (1.0, [_P], [_P]))],
}
super().__init__(wires, qmlCRY, parameter, rule)
[docs]
class CRZ(ControlledRotationGate):
r"""
The controlled-:math:`R_z` gate.
.. math::
CR_z(\theta) = \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & e^{-i\theta} & 0 \\
0 & 0 & 0 & e^{i\theta}
\end{bmatrix}
.. note::
The parameter ``θ`` here corresponds to ``θ/2`` in PennyLane's convention.
Pass ``params[i] / 2`` to ``qml.CRZ`` to match.
Parameters
----------
wires : list[int]
``[control, target]`` qubit indices.
parameter : int
Index of :math:`\\theta` in the global parameter vector.
"""
def __init__(self, wires: List[int], parameter: int) -> None:
rule: EvolutionRule = {
"IX": [("IX", (1.0, [], [_P, _P])),
("IY", (-1.0, [_P], [_P])),
("ZX", (1.0, [_P, _P], [])),
("ZY", (1.0, [_P], [_P]))],
"IY": [("IY", (1.0, [], [_P, _P])),
("IX", (1.0, [_P], [_P])),
("ZY", (1.0, [_P, _P], [])),
("ZX", (-1.0, [_P], [_P]))],
"XI": [("XI", (1.0, [], [_P])),
("YZ", (1.0, [_P], []))],
"XX": [("XX", (1.0, [], [_P])),
("XY", (-1.0, [_P], []))],
"XY": [("XY", (1.0, [], [_P])),
("XX", (1.0, [_P], []))],
"XZ": [("XZ", (1.0, [], [_P])),
("YI", (1.0, [_P], []))],
"YI": [("YI", (1.0, [], [_P])),
("XZ", (-1.0, [_P], []))],
"YX": [("YX", (1.0, [], [_P])),
("YY", (-1.0, [_P], []))],
"YY": [("YY", (1.0, [], [_P])),
("YX", (1.0, [_P], []))],
"YZ": [("YZ", (1.0, [], [_P])),
("XI", (-1.0, [_P], []))],
"ZX": [("ZX", (1.0, [], [_P, _P])),
("ZY", (-1.0, [_P], [_P])),
("IX", (1.0, [_P, _P], [])),
("IY", (1.0, [_P], [_P]))],
"ZY": [("ZY", (1.0, [], [_P, _P])),
("ZX", (1.0, [_P], [_P])),
("IY", (1.0, [_P, _P], [])),
("IX", (-1.0, [_P], [_P]))],
}
super().__init__(wires, qmlCRZ, parameter, rule)