Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/tsim/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,49 @@ def append(
tag = f"{name}(theta={args[0]}*pi)"
name = "I"
arg = None
elif name in ("R_XX", "R_YY", "R_ZZ"):
if arg is None:
raise ValueError(f"For {name} gates, an angle must be provided.")
args = list(arg) if isinstance(arg, Iterable) else [arg]
if len(args) != 1:
raise ValueError(
f"For {name} gates, a single angle must be provided."
)
t_list: list[int] = (
[targets] if isinstance(targets, int) else list(targets) # type: ignore[arg-type]
)
if len(t_list) != 2:
raise ValueError(
f"For {name} gates, exactly two target qubits must be provided."
)
q0, q1 = (
t.value if isinstance(t, stim.GateTarget) else t for t in t_list
)
if q0 == q1:
raise ValueError(
f"Duplicate target qubits in {name}: both targets are qubit {q0}."
)
pauli = name[2] # 'X', 'Y', or 'Z'
target_fn = {
"X": stim.target_x,
"Y": stim.target_y,
"Z": stim.target_z,
}[pauli]
tag = f"{name}(theta={args[0]}*pi)"
targets = [target_fn(q0), stim.target_combiner(), target_fn(q1)]
name = "SPP"
arg = None
elif name == "R_PAULI":
if arg is None:
raise ValueError("For R_PAULI gates, an angle must be provided.")
args = list(arg) if isinstance(arg, Iterable) else [arg]
if len(args) != 1:
raise ValueError(
"For R_PAULI gates, a single angle must be provided."
)
tag = f"R_PAULI(theta={args[0]}*pi)"
name = "SPP"
arg = None
elif name == "U3":
args = list(arg) if isinstance(arg, Iterable) else []
if arg is None or len(args) != 3:
Expand Down Expand Up @@ -806,6 +849,19 @@ def fix_tags(circuit: stim.Circuit) -> stim.Circuit:
result.append("I", targets, args, tag=new_tag)
continue

# Parametric Pauli rotations (R_PAULI, R_XX, R_YY, R_ZZ) are stored
# as SPP[tag]. Stim's inverse() correctly flips SPP <-> SPP_DAG and
# reverses target order. Our parse.py interprets SPP_DAG[tag] with
# dagger=True, which is exactly the inverse we want. So we just pass
# these instructions through unchanged.
if name in ("SPP", "SPP_DAG") and tag and tag != "T":
parsed = parse_parametric_tag(instr)
if parsed is not None:
gate_name, params = parsed
if gate_name in ("R_PAULI", "R_XX", "R_YY", "R_ZZ"):
result.append(instr)
continue

result.append(instr)
return result

Expand Down
38 changes: 38 additions & 0 deletions src/tsim/core/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,44 @@ def _pauli_product_phase(
s(b, qubit)


def r_pauli(
b: GraphRepresentation,
paulis: list[tuple[Literal["X", "Y", "Z"], int]],
theta: Fraction,
dagger: bool = False,
) -> None:
"""Apply ``exp(-i theta pi/2 P)`` for an arbitrary Pauli product ``P``.

Generalises the single-qubit rotations :func:`r_x`, :func:`r_y`, and
:func:`r_z` to multi-qubit Pauli strings. ``R_XX``, ``R_YY``, and
``R_ZZ`` are the two-qubit special cases with ``paulis = [(P, q0), (P, q1)]``.

This reuses the ladder-of-CNOTs decomposition shared by :func:`spp` and
:func:`tpp`: rotate each qubit into the Z basis, accumulate Z-parity onto
the last qubit, apply the parametric Z rotation, then uncompute.

Args:
b: The graph representation to modify.
paulis: List of ``(pauli_type, qubit)`` pairs defining the Pauli
product ``P``. Must be non-empty.
theta: Rotation angle in *half-turns* (units of π), so the unitary
is ``exp(-i theta π/2 P)``. For example, ``theta=1/2`` gives the
S-gate for ``P = Z`` (matching ``SPP``).
dagger: If ``True``, negate the rotation angle, applying
``exp(+i theta π/2 P)`` instead.

"""
if dagger:
theta = -theta
_pauli_product_phase(
b,
paulis,
lambda b_, q: r_z(b_, q, theta),
lambda b_, q: r_z(b_, q, -theta),
dagger=False,
)


def spp(
b: GraphRepresentation,
paulis: list[tuple[Literal["X", "Y", "Z"], int]],
Expand Down
16 changes: 15 additions & 1 deletion src/tsim/core/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
mpad,
mpp,
observable_include,
r_pauli,
r_x,
r_y,
r_z,
Expand All @@ -29,6 +30,10 @@
"R_X": frozenset({"theta"}),
"R_Y": frozenset({"theta"}),
"R_Z": frozenset({"theta"}),
"R_XX": frozenset({"theta"}),
"R_YY": frozenset({"theta"}),
"R_ZZ": frozenset({"theta"}),
"R_PAULI": frozenset({"theta"}),
"U3": frozenset({"theta", "phi", "lambda"}),
}

Expand All @@ -38,7 +43,7 @@ def parse_parametric_tag(
) -> tuple[str, dict[str, Fraction]] | None:
"""Parse the parametric tag on an instruction (e.g. ``I[R_Z(theta=0.3*pi)]``).

Supports gates: R_Z, R_X, R_Y, U3.
Supports gates: R_Z, R_X, R_Y, R_XX, R_YY, R_ZZ, R_PAULI, U3.

Args:
instruction: The stim instruction whose tag will be parsed.
Expand Down Expand Up @@ -225,6 +230,15 @@ def parse_stim_circuit(
for paulis, invert in _iter_pauli_products(instruction):
mpp(b, paulis, invert, p=p)
continue
if name in ("SPP", "SPP_DAG") and instruction.tag and instruction.tag != "T":
parsed = parse_parametric_tag(instruction)
if parsed is not None:
gate_name, params = parsed
if gate_name in ("R_PAULI", "R_XX", "R_YY", "R_ZZ"):
is_dag = name == "SPP_DAG"
for paulis, invert in _iter_pauli_products(instruction):
r_pauli(b, paulis, params["theta"], dagger=is_dag ^ invert)
continue
if name in ("SPP", "SPP_DAG") and instruction.tag == "T":
is_dag = name == "SPP_DAG"
for paulis, invert in _iter_pauli_products(instruction):
Expand Down
15 changes: 14 additions & 1 deletion src/tsim/utils/clifford.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ def is_half_pi_multiple(phase: Fraction) -> bool:
return False
continue

if instr.name in ["S", "S_DAG", "SPP", "SPP_DAG"] and instr.tag == "T":
if instr.name in [
"S",
"S_DAG",
"SPP",
"SPP_DAG",
] and instr.tag == "T":
return False

if instr.name == "I" and instr.tag:
Expand All @@ -137,6 +142,14 @@ def is_half_pi_multiple(phase: Fraction) -> bool:
else:
return False

# SPP with parametric theta tag: Clifford iff theta is a half-π multiple
if instr.name in ("SPP", "SPP_DAG") and instr.tag and instr.tag != "T":
result = parse_parametric_tag(instr)
if result is not None:
gate_name, params = result
if gate_name in ("R_PAULI", "R_XX", "R_YY", "R_ZZ") and not is_half_pi_multiple(params["theta"]):
return False

return True


Expand Down
73 changes: 70 additions & 3 deletions src/tsim/utils/program_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
# Matches valid numeric literals including scientific notation (e.g. 0.5, 4e-4, 1.2e3)
FLOAT_RE = r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?"

_TSIM_GATES = {"R_X", "R_Y", "R_Z", "U3"}
_TSIM_GATES = {"R_X", "R_Y", "R_Z", "R_XX", "R_YY", "R_ZZ", "R_PAULI", "U3"}
_GATE_NOT_FOUND_RE = re.compile(r"Gate not found: '(\w+)'")
_GATE_USAGE_RE = re.compile(r"(?<!\[)\b(R_[A-Z]\([^)]*\)|R_[XYZ]\b|U3\([^)]*\)|U3\b)")
_GATE_USAGE_RE = re.compile(
r"(?<!\[)\b(R_(?:XX|YY|ZZ|PAULI|[XYZ])\([^)]*\)|R_(?:XX|YY|ZZ|PAULI|[XYZ])\b|U3\([^)]*\)|U3\b)"
)


def enriched_stim_error(exc: ValueError, converted_text: str) -> ValueError:
Expand All @@ -30,7 +32,8 @@ def enriched_stim_error(exc: ValueError, converted_text: str) -> ValueError:
def shorthand_to_stim(text: str) -> str:
"""Convert tsim shorthand syntax to valid stim instructions.

Converts:
Converts::

T 0 1 → S[T] 0 1
T_DAG 0 1 → S_DAG[T] 0 1
TPP X0*Y1 → SPP[T] X0*Y1
Expand All @@ -39,6 +42,10 @@ def shorthand_to_stim(text: str) -> str:
R_X(0.25) 0 → I[R_X(theta=0.25*pi)] 0
R_Y(-0.5) 0 → I[R_Y(theta=-0.5*pi)] 0
U3(0.3, 0.24, 0.49) 0 → I[U3(theta=0.3*pi, phi=0.24*pi, lambda=0.49*pi)] 0
R_XX(0.25) 0 1 → SPP[R_XX(theta=0.25*pi)] X0*X1
R_YY(0.25) 0 1 → SPP[R_YY(theta=0.25*pi)] Y0*Y1
R_ZZ(0.25) 0 1 → SPP[R_ZZ(theta=0.25*pi)] Z0*Z1
R_PAULI(0.25) X0*Y1 → SPP[R_PAULI(theta=0.25*pi)] X0*Y1

"""
# TPP_DAG/TPP must come before T_DAG/T to avoid partial matches
Expand All @@ -48,6 +55,35 @@ def shorthand_to_stim(text: str) -> str:
text = re.sub(r"(?<!\[)\bT_DAG\b(?!\[)", "S_DAG[T]", text)
text = re.sub(r"(?<!\[)\bT\b(?!\[)", "S[T]", text)

# R_XX/R_YY/R_ZZ must come before R_X/R_Y/R_Z to avoid partial matches
def replace_r_pp(m: re.Match) -> str:
pauli = m.group(1) # "XX", "YY", or "ZZ"
angle = float(m.group(2))
q0, q1 = m.group(3), m.group(4)
if q0 == q1:
raise ValueError(
f"Duplicate target qubits in R_{pauli}: both targets are qubit {q0}."
)
p = pauli[0]
return f"SPP[R_{pauli}(theta={angle}*pi)] {p}{q0}*{p}{q1}"

text = re.sub(
rf"\bR_(XX|YY|ZZ)\(({FLOAT_RE})\)\s+(\d+)\s+(\d+)",
replace_r_pp,
text,
)

def replace_r_pauli(m: re.Match) -> str:
angle = float(m.group(1))
targets = m.group(2)
return f"SPP[R_PAULI(theta={angle}*pi)] {targets}"

text = re.sub(
rf"\bR_PAULI\(({FLOAT_RE})\)\s+((?:[XYZ]\d+)(?:\*[XYZ]\d+)*)",
replace_r_pauli,
text,
)

def replace_rotation(m: re.Match) -> str:
axis = m.group(1)
return f"I[R_{axis}(theta={float(m.group(2))}*pi)]"
Expand Down Expand Up @@ -86,6 +122,10 @@ def stim_to_shorthand(text: str) -> str:
Rewrites:
- I[U3(theta=θ*pi, phi=φ*pi, lambda=λ*pi)] → U3(θ, φ, λ)
- I[R_X(theta=α*pi)] / I[R_Y(...)] / I[R_Z(...)] → R_X(α) / R_Y(α) / R_Z(α)
- SPP[R_XX(theta=α*pi)] X0*X1 → R_XX(α) 0 1
- SPP[R_YY(theta=α*pi)] Y0*Y1 → R_YY(α) 0 1
- SPP[R_ZZ(theta=α*pi)] Z0*Z1 → R_ZZ(α) 0 1
- SPP[R_PAULI(theta=α*pi)] X0*Y1*Z2 → R_PAULI(α) X0*Y1*Z2
- SPP[T] → TPP
- SPP_DAG[T] → TPP_DAG
- S[T] → T
Expand Down Expand Up @@ -115,6 +155,33 @@ def replace_rotation(m: re.Match) -> str:
text,
)

# Replace SPP[R_XX/R_YY/R_ZZ(...)] P0*P1 with R_XX/R_YY/R_ZZ(α) q0 q1
# Must come before the general R_PAULI rewrite below.
def replace_spp_r_pp(m: re.Match) -> str:
pauli = m.group(1) # "XX", "YY", or "ZZ"
angle = m.group(2)
q0 = m.group(3)
q1 = m.group(4)
return f"R_{pauli}({angle}) {q0} {q1}"

text = re.sub(
rf"\bSPP\[R_(XX|YY|ZZ)\(theta=({FLOAT_RE})\*pi\)\]\s+[XYZ](\d+)\*[XYZ](\d+)",
replace_spp_r_pp,
text,
)

# Replace SPP[R_PAULI(...)] with R_PAULI(α) <pauli-product>
def replace_spp_r_pauli(m: re.Match) -> str:
angle = m.group(1)
targets = m.group(2)
return f"R_PAULI({angle}) {targets}"

text = re.sub(
rf"\bSPP\[R_PAULI\(theta=({FLOAT_RE})\*pi\)\]\s+((?:[XYZ]\d+)(?:\*[XYZ]\d+)*)",
replace_spp_r_pauli,
text,
)

# Replace SPP[T] and SPP_DAG[T] with TPP and TPP_DAG
# Must come before S[T]/S_DAG[T] to avoid partial matches
text = re.sub(r"(?<!\w)SPP_DAG\[T\](?!\w)", "TPP_DAG", text)
Expand Down
Loading