diff --git a/README.md b/README.md index e57cb47e..cc5530dd 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ An introductory tutorial is available [here](https://tsim.mintlify.app/tutorials For many existing scripts, replacing `stim` with `tsim` should just work. Tsim mirrors the Stim API and currently supports all [Stim instructions](https://github.com/quantumlib/Stim/wiki/Stim-v1.13-Gate-Reference). -Additionally, Tsim supports the instructions `T`, `T_DAG`, `R_Z`, `R_X`, `R_Y`, `U3`, `TPP`, and `TPP_DAG`. +Additionally, Tsim supports the instructions `T`, `T_DAG`, `R_Z`, `R_X`, `R_Y`, `U3`, `TPP`, `TPP_DAG`, `CCZ`, and `CCX`. ```python import tsim @@ -144,6 +144,15 @@ TPP X0*Y1 # Apply exp(-i π/8 · X0⊗Y1) (up to global phase) TPP_DAG Z0 # Apply exp(+i π/8 · Z) = T_DAG (up to global phase) ``` +### `CCZ` and `CCX` Gates + +`CCZ` applies a controlled-controlled Z gate, and `CCX` applies the controlled-controlled X gate (Toffoli). Both gates are expanded internally into a Clifford+T decomposition: + +``` +CCZ 0 1 2 # Apply CCZ with controls 0 and 1, target 2 +CCX 0 1 2 # Apply Toffoli/CCX with controls 0 and 1, target 2 +``` + ## Publications Using Tsim - [Simulating magic state cultivation with few Clifford terms](https://arxiv.org/abs/2509.08658) by Kwok Ho Wan and Zhenghao Zhong (2025). diff --git a/docs/index.md b/docs/index.md index f648655b..b819c89c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ A detailed description of Tsim is given in [arXiv:2604.01059](https://arxiv.org/ An introductory tutorial is available [here](demos/encoding_demo.ipynb). For many existing scripts, replacing `stim` with `tsim` should just work. Tsim mirrors the Stim API and supports all [Stim instructions](https://github.com/quantumlib/Stim/wiki/Stim-v1.13-Gate-Reference). -Additionally, Tsim supports the instructions `T`, `T_DAG`, `R_Z`, `R_X`, `R_Y`, and `U3`. +Additionally, Tsim supports the instructions `T`, `T_DAG`, `R_Z`, `R_X`, `R_Y`, `U3`, `CCZ`, and `CCX`. ```python import tsim diff --git a/src/tsim/circuit.py b/src/tsim/circuit.py index 09210567..bf28c753 100644 --- a/src/tsim/circuit.py +++ b/src/tsim/circuit.py @@ -21,16 +21,43 @@ from tsim.core.graph import build_sampling_graph from tsim.core.parse import parse_parametric_tag, parse_stim_circuit +from tsim.core.tags import encode_t_tag from tsim.noise.dem import get_detector_error_model from tsim.utils.clifford import expand_clifford_rotations, is_clifford from tsim.utils.diagram import render_pyzx_d3, render_svg from tsim.utils.program_text import ( + controlled_gate_decomposition_lines, enriched_stim_error, shorthand_to_stim, stim_to_shorthand, ) +def _bare_qubit_targets( + gate_name: str, + targets: ( + int + | stim.GateTarget + | stim.PauliString + | Iterable[int | stim.GateTarget | stim.PauliString] + ), +) -> list[int]: + if isinstance(targets, int | stim.GateTarget | stim.PauliString): + target_items: list[int | stim.GateTarget | stim.PauliString] = [targets] + else: + target_items = list(targets) + + qubits: list[int] = [] + for target in target_items: + if isinstance(target, int): + qubits.append(target) + elif isinstance(target, stim.GateTarget) and target.is_qubit_target: + qubits.append(target.value) + else: + raise ValueError(f"{gate_name} only supports bare qubit targets.") + return qubits + + class Circuit: """Quantum circuit as a thin wrapper around stim.Circuit. @@ -150,18 +177,37 @@ def append( """ if isinstance(name, str): + if name in ("CCZ", "CCX"): + if arg is not None: + raise ValueError(f"For {name} gates, no arguments are accepted.") + qubits = _bare_qubit_targets(name, targets) + if len(qubits) % 3 != 0: + raise ValueError( + f"{name} expects qubit targets in groups of three." + ) + self.append_from_stim_program_text( + "\n".join( + line + for i in range(0, len(qubits), 3) + for line in controlled_gate_decomposition_lines( + name, qubits[i], qubits[i + 1], qubits[i + 2], tag=tag + ) + ) + ) + return + if name == "TPP": name = "SPP" - tag = "T" + tag = encode_t_tag(tag) elif name == "TPP_DAG": name = "SPP_DAG" - tag = "T" + tag = encode_t_tag(tag) elif name == "T": name = "S" - tag = "T" + tag = encode_t_tag(tag) elif name == "T_DAG": name = "S_DAG" - tag = "T" + tag = encode_t_tag(tag) elif name in ("R_X", "R_Y", "R_Z"): if arg is None: raise ValueError(f"For {name} gates, an angle must be provided.") diff --git a/src/tsim/core/parse.py b/src/tsim/core/parse.py index 0e74c712..2b919db4 100644 --- a/src/tsim/core/parse.py +++ b/src/tsim/core/parse.py @@ -24,6 +24,7 @@ tpp, u3, ) +from tsim.core.tags import is_t_tag _PARAMETRIC_GATE_PARAMS: dict[str, frozenset[str]] = { "R_X": frozenset({"theta"}), @@ -192,9 +193,9 @@ def parse_stim_circuit( f"in instruction {str(instruction)!r}" ) - if name == "S" and instruction.tag == "T": + if name == "S" and is_t_tag(instruction.tag): name = "T" - elif name == "S_DAG" and instruction.tag == "T": + elif name == "S_DAG" and is_t_tag(instruction.tag): name = "T_DAG" # Handle parametric gates via tags (e.g., I with tag "R_Z(theta=0.3*pi)") @@ -225,7 +226,7 @@ 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 == "T": + if name in ("SPP", "SPP_DAG") and is_t_tag(instruction.tag): is_dag = name == "SPP_DAG" for paulis, invert in _iter_pauli_products(instruction): tpp(b, paulis, dagger=is_dag ^ invert) diff --git a/src/tsim/core/tags.py b/src/tsim/core/tags.py new file mode 100644 index 00000000..7c7cd9df --- /dev/null +++ b/src/tsim/core/tags.py @@ -0,0 +1,25 @@ +"""Helpers for tags that encode tsim-specific gate metadata.""" + +T_TAG = "T" +_T_USER_TAG_PREFIX = f"{T_TAG}:" + + +def encode_t_tag(user_tag: str = "") -> str: + """Encode a T-family gate tag while preserving an optional user tag.""" + if not user_tag: + return T_TAG + return f"{_T_USER_TAG_PREFIX}{user_tag}" + + +def is_t_tag(tag: str) -> bool: + """Return whether a Stim tag encodes a tsim T-family gate.""" + return tag == T_TAG or tag.startswith(_T_USER_TAG_PREFIX) + + +def decode_t_user_tag(tag: str) -> str: + """Return the user tag attached to an encoded T-family gate tag.""" + if tag == T_TAG: + return "" + if tag.startswith(_T_USER_TAG_PREFIX): + return tag[len(_T_USER_TAG_PREFIX) :] + raise ValueError(f"Tag does not encode a T-family gate: {tag!r}") diff --git a/src/tsim/external/vec_sim/vec_sampler.py b/src/tsim/external/vec_sim/vec_sampler.py index d32ff79e..99696ac7 100644 --- a/src/tsim/external/vec_sim/vec_sampler.py +++ b/src/tsim/external/vec_sim/vec_sampler.py @@ -18,6 +18,7 @@ import stim from tsim.core.parse import parse_parametric_tag +from tsim.core.tags import is_t_tag from tsim.external.vec_sim import VecSim @@ -74,10 +75,10 @@ def sample_circuit_with_vec_sim_return_data( sim.do_qalloc_z(q) for inst in circuit: assert not isinstance(inst, stim.CircuitRepeatBlock) - if inst.name == "S" and inst.tag == "T": + if inst.name == "S" and is_t_tag(inst.tag): for q in inst.targets_copy(): sim.do_t(q.qubit_value) - elif inst.name == "S_DAG" and inst.tag == "T": + elif inst.name == "S_DAG" and is_t_tag(inst.tag): for q in inst.targets_copy(): sim.do_t_dag(q.qubit_value) elif inst.name == "I" and inst.tag: diff --git a/src/tsim/utils/clifford.py b/src/tsim/utils/clifford.py index f27cc06f..4c921e65 100644 --- a/src/tsim/utils/clifford.py +++ b/src/tsim/utils/clifford.py @@ -7,6 +7,7 @@ import stim from tsim.core.parse import parse_parametric_tag +from tsim.core.tags import is_t_tag # Clifford decompositions for U3(θ, φ, λ) = R_Z(φ) · R_Y(θ) · R_Z(λ). # Keys: (θ_idx, φ_idx, λ_idx) where each index ∈ {0,1,2,3} is the angle in half-pi units. @@ -116,7 +117,7 @@ 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 is_t_tag(instr.tag): return False if instr.name == "I" and instr.tag: diff --git a/src/tsim/utils/diagram.py b/src/tsim/utils/diagram.py index 3920438f..166c7bba 100644 --- a/src/tsim/utils/diagram.py +++ b/src/tsim/utils/diagram.py @@ -14,6 +14,7 @@ from tsim.core.graph import scale_horizontally from tsim.core.parse import parse_stim_circuit +from tsim.core.tags import is_t_tag from tsim.utils.program_text import FLOAT_RE @@ -390,7 +391,7 @@ def _replace_tagged_gates( # Double each Pauli target so the SVG contains duplicate rect+text # pairs at the same position, which _deduplicate_doubled_spp() later # detects and renames SPP → TPP. - if instr.tag == "T" and instr.name in ["SPP", "SPP_DAG"]: + if is_t_tag(instr.tag) and instr.name in ["SPP", "SPP_DAG"]: targets = instr.targets_copy() doubled: list[stim.GateTarget] = [] for target in targets: @@ -405,7 +406,7 @@ def _replace_tagged_gates( continue # Handle T gates (S[T] and S_DAG[T]) - if instr.tag == "T" and instr.name in ["S", "S_DAG"]: + if is_t_tag(instr.tag) and instr.name in ["S", "S_DAG"]: for target in instr.targets_copy(): identifier = np.round(np.random.rand(), 6) DAG = '' diff --git a/src/tsim/utils/program_text.py b/src/tsim/utils/program_text.py index ed8f6f41..cb7b2a5f 100644 --- a/src/tsim/utils/program_text.py +++ b/src/tsim/utils/program_text.py @@ -1,13 +1,98 @@ """Conversion utilities between tsim shorthand and stim program text.""" import re +from collections.abc import Callable + +from tsim.core.tags import decode_t_user_tag, encode_t_tag # 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 = {"CCZ", "CCX", "R_X", "R_Y", "R_Z", "U3"} _GATE_NOT_FOUND_RE = re.compile(r"Gate not found: '(\w+)'") -_GATE_USAGE_RE = re.compile(r"(? list[str]: + """Return a Clifford+T decomposition for a controlled-controlled gate.""" + if gate not in ("CCZ", "CCX"): + raise ValueError(f"Unsupported controlled-controlled gate: {gate!r}") + + def tagged(name: str) -> str: + return f"{name}[{tag}]" if tag else name + + a, b, c = str(control1), str(control2), str(target) + ccz_lines = [ + f"{tagged('CNOT')} {b} {c}", + f"{tagged('T_DAG')} {c}", + f"{tagged('CNOT')} {a} {c}", + f"{tagged('T')} {c}", + f"{tagged('CNOT')} {b} {c}", + f"{tagged('T_DAG')} {c}", + f"{tagged('CNOT')} {a} {c}", + f"{tagged('T')} {b}", + f"{tagged('T')} {c}", + f"{tagged('CNOT')} {a} {b}", + f"{tagged('T')} {a}", + f"{tagged('T_DAG')} {b}", + f"{tagged('CNOT')} {a} {b}", + ] + if gate == "CCZ": + return ccz_lines + return [f"{tagged('H')} {c}", *ccz_lines, f"{tagged('H')} {c}"] + + +def _expand_controlled_gates(text: str) -> str: + lines: list[str] = [] + for line in text.splitlines(): + body, sep, comment = line.partition("#") + match = re.match(r"^(\s*)(CCZ|CCX)(?:\[([^\]\n]*)\])?\s+(.+?)\s*$", body) + if not match: + lines.append(line) + continue + + indent, gate, tag, targets_text = match.groups() + targets = targets_text.split() + if len(targets) % 3 != 0 or not all(target.isdecimal() for target in targets): + raise ValueError( + f"{gate} expects bare qubit integer targets in groups of three." + ) + + if sep: + lines.append(f"{indent}{sep}{comment}") + for i in range(0, len(targets), 3): + lines.extend( + f"{indent}{decomp_line}" + for decomp_line in controlled_gate_decomposition_lines( + gate, targets[i], targets[i + 1], targets[i + 2], tag=tag or "" + ) + ) + return "\n".join(lines) + + +def _replace_t_family(stim_gate: str) -> Callable[[re.Match[str]], str]: + def replace(m: re.Match[str]) -> str: + return f"{stim_gate}[{encode_t_tag(m.group(1) or '')}]" + + return replace + + +def _replace_t_family_shorthand(tsim_gate: str) -> Callable[[re.Match[str]], str]: + def replace(m: re.Match[str]) -> str: + user_tag = decode_t_user_tag(m.group(1)) + tag_suffix = f"[{user_tag}]" if user_tag else "" + return f"{tsim_gate}{tag_suffix}" + + return replace def enriched_stim_error(exc: ValueError, converted_text: str) -> ValueError: @@ -31,6 +116,8 @@ def shorthand_to_stim(text: str) -> str: """Convert tsim shorthand syntax to valid stim instructions. Converts: + CCZ 0 1 2 → Clifford+T decomposition of CCZ + CCX 0 1 2 → Clifford+T decomposition of CCX/Toffoli T 0 1 → S[T] 0 1 T_DAG 0 1 → S_DAG[T] 0 1 TPP X0*Y1 → SPP[T] X0*Y1 @@ -41,12 +128,30 @@ def shorthand_to_stim(text: str) -> str: U3(0.3, 0.24, 0.49) 0 → I[U3(theta=0.3*pi, phi=0.24*pi, lambda=0.49*pi)] 0 """ + text = _expand_controlled_gates(text) + # TPP_DAG/TPP must come before T_DAG/T to avoid partial matches # (? str: axis = m.group(1) @@ -117,12 +222,28 @@ def replace_rotation(m: re.Match) -> str: # 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"(?