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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 50 additions & 4 deletions src/tsim/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.")
Expand Down
7 changes: 4 additions & 3 deletions src/tsim/core/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}),
Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions src/tsim/core/tags.py
Original file line number Diff line number Diff line change
@@ -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}")
5 changes: 3 additions & 2 deletions src/tsim/external/vec_sim/vec_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/tsim/utils/clifford.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions src/tsim/utils/diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand All @@ -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 = '<tspan baseline-shift="super" font-size="14">†</tspan>'
Expand Down
Loading