"""Approximate Rz → Clifford+T synthesis (Ross–Selinger, via pygridsynth).
Deliberately OUTSIDE the transpiler: DAG/rebase passes are unitary-exact by
contract (qarp_conventions §16 EQ-2), so an ε-approximation can be neither a
decomposition rule nor an optimization pass. It runs as an explicit
post-rebase stage with a declared tolerance — per rotation, not a global
budget, the usual convention of gridsynth-based compilation stacks.
Input must be Clifford+T+Rz format (Clifford + T + Rz + meta,
``qx.clifford_t_rz_gateset()``) — the intermediate that rebase already emits.
Output is phase-exact (§13): each Rz folds to ``T^j · Rz(θ_r)`` with
``θ_r = remainder(θ, π/4)``; zero residual is free, otherwise gridsynth
approximates ``Rz(θ_r)`` over {H, S, T, X} within ``epsilon`` in operator
norm. The fold's ``e^{-ijπ/8}``, gridsynth's W gates (= ``e^{iπ/4}``) and
its circuit phase accumulate into one trailing GPhase.
The fold is π/4, not π/2: ``Rz(π/4)`` **is** ``T`` up to phase, so folding
only to Clifford would hand gridsynth angles with exact 1-gate forms and
pay O(log 1/ε) T gates plus ε error for them. Multi-controlled lowering
emits exactly those angles (``decompose_cp`` halves down from π), so the
distinction is not academic.
Needs the optional ``pygridsynth`` dependency (``pip install
"openqarp[cliffordt]"``) — the reference Python implementation of
Ross–Selinger gridsynth.
"""
import math
from functools import lru_cache
from typing import Sequence
import qarpx as qx
_QUARTER_PI = math.pi / 4
_FOLD_TOL = 1e-12 # residual below this is an exact T power, not a rotation
_LETTER = {"H": qx.GateType.H, "S": qx.GateType.S, "T": qx.GateType.T, "X": qx.GateType.X}
# clifford_t_rz_gateset minus Rz: everything that may pass through unsynthesized.
_PASSTHROUGH = frozenset(
{
qx.GateType.X,
qx.GateType.Y,
qx.GateType.Z,
qx.GateType.H,
qx.GateType.S,
qx.GateType.Sdg,
qx.GateType.T,
qx.GateType.Tdg,
qx.GateType.CX,
qx.GateType.CZ,
qx.GateType.Measure,
qx.GateType.Barrier,
qx.GateType.GPhase,
}
)
def _gphase_command(gamma: float) -> "qx.Command":
# Command has no qubit-less constructor binding; mint through a scratch
# block. Lazy import: qarp.engines imports qarp.resources at import
# time, and qarp.blocks must stay out of that chain.
from qarp.blocks import SimpleBlock
b = SimpleBlock(1)
b.gphase(gamma)
return next(iter(b.commands()))
@lru_cache(maxsize=None)
def _rz_sequence(theta_r: float, epsilon: float) -> "tuple[str, float]":
"""Gridsynth letters (W stripped into the phase) for one folded residual.
Cached: a QPE ladder repeats a handful of distinct angles thousands of
times, so synthesis cost is per distinct (θ_r, ε), not per gate.
"""
try:
import mpmath
from pygridsynth.gridsynth import gridsynth_circuit
except ImportError as exc: # pragma: no cover - exercised without the extra
raise ImportError(
'Rz → Clifford+T synthesis needs pygridsynth: pip install "openqarp[cliffordt]"'
) from exc
# mpf(float) is exact — the double IS the target angle; bare floats only
# trigger pygridsynth's decimal-literal precision warning.
circ = gridsynth_circuit(mpmath.mpf(theta_r), mpmath.mpf(epsilon)) # phase-exact mode
letters = circ.to_simple_str()
phase = float(circ.phase) + (math.pi / 4) * letters.count("W")
return letters.replace("W", ""), phase
[docs]
def synthesize_clifford_t(
commands: "Sequence[qx.Command]", *, epsilon: float
) -> "list[qx.Command]":
"""Replace every Rz in a Clifford+T+Rz-format stream by a Clifford+T sequence.
Each output sequence is within ``epsilon`` of its exact rotation in
operator norm, global phase included; all other gates of the set pass through
untouched, so the stream's unitary is preserved to ``m·ε`` for ``m``
synthesized rotations. Raises ``ValueError`` on symbolic angles or on
gates outside the Clifford+T+Rz gate set (rebase first).
"""
if not 0.0 < epsilon < 1.0:
raise ValueError(f"epsilon must be in (0, 1), got {epsilon}")
out: list[qx.Command] = []
gamma = 0.0
for cmd in commands:
if cmd.gate != qx.GateType.Rz:
if cmd.gate not in _PASSTHROUGH:
raise ValueError(
"synthesize_clifford_t expects Clifford+T+Rz-format input (rebase "
f"to qx.clifford_t_rz_gateset() first); got {qx.gate_name(cmd.gate)}"
)
out.append(cmd)
continue
param = cmd.params[0]
if not param.is_concrete():
raise ValueError(
"synthesize_clifford_t needs bound angles: substitute "
"parameters before synthesizing."
)
theta = param.value()
qubit = cmd.qubits[0]
# Exact Clifford+T fold: Rz(θ) = e^{-ijπ/8} · T^j · Rz(θ_r), j ∈ ℤ.
# T^8 = I and T^2 = S, so T^(j mod 8) is emitted as S^a·T^b — at most
# four gates, and never an approximation.
residual = math.remainder(theta, _QUARTER_PI)
j = round((theta - residual) / _QUARTER_PI)
turns = j % 8
out.extend(qx.Command(qx.GateType.S, qubit) for _ in range(turns // 2))
if turns % 2:
out.append(qx.Command(qx.GateType.T, qubit))
gamma -= j * math.pi / 8
if abs(residual) >= _FOLD_TOL:
letters, phase = _rz_sequence(residual, epsilon)
out.extend(qx.Command(_LETTER[ch], qubit) for ch in letters)
gamma += phase
gamma = math.remainder(gamma, 2 * math.pi)
if abs(gamma) > _FOLD_TOL:
out.append(_gphase_command(gamma))
return out