Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
db05351
Initial commit for stimside, a stim wrapper that enables simulations …
mliqai Jan 27, 2026
954254e
Fix sinter collect issue and detector collection in tableside
mliqai Feb 4, 2026
1a01246
Speed up Stim Sampling with Faster Ref Sample (#1036)
qec-pconner Feb 4, 2026
1cdb38f
Add plot_custom to sinter python API (#1035)
m-mcewen Feb 4, 2026
d2c38fa
Fix keyboard handling for Escape and Ctrl+Backspace in crumble (#1038)
Strilanc Feb 4, 2026
99fc61e
Add single-measure key option for Cirq converison (#1043)
chriseclectic Mar 11, 2026
87e7325
Optimize backtrack_path to use unordered_map (#1041)
DeDuckProject Mar 31, 2026
ca0ce4c
Fix some undefined behaviors (#1054)
Strilanc Apr 13, 2026
a083e9c
Bump minimatch from 3.1.2 to 3.1.5 (#1044)
dependabot[bot] Apr 15, 2026
97ec43b
Add basic support for feedback operations to stimcirq (#1055)
Strilanc Apr 16, 2026
2a3693b
Add pymatching correlated to sinter (#1046)
m-mcewen Apr 16, 2026
924ece6
Remove runtime type checks from sinter.AnonTaskStats (#1056)
Strilanc Apr 16, 2026
5560218
Add stimcirq support for feedback controlled by sympy Xor expressions…
Strilanc Apr 18, 2026
667b051
Adding the LEAKAGE_SWAP tag
mliqai Apr 18, 2026
e5ce090
Allowing LEAKAGE_DETECTOR tag to be attached to DETECTORs
mliqai Apr 18, 2026
ad7ac95
Merge branch 'stimside' into stimside-dev
mliqai Apr 18, 2026
bd4a649
Adding checks that LEAKAGE_DETECTOR only applies to DETECTORs
mliqai Apr 18, 2026
f20861c
Fixing seed propagation in sampler and simulator
mliqai Apr 24, 2026
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
65 changes: 65 additions & 0 deletions doc/sinter_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi
- [`sinter.iter_collect`](#sinter.iter_collect)
- [`sinter.log_binomial`](#sinter.log_binomial)
- [`sinter.log_factorial`](#sinter.log_factorial)
- [`sinter.plot_custom`](#sinter.plot_custom)
- [`sinter.plot_discard_rate`](#sinter.plot_discard_rate)
- [`sinter.plot_error_rate`](#sinter.plot_error_rate)
- [`sinter.post_selection_mask_from_4th_coord`](#sinter.post_selection_mask_from_4th_coord)
Expand Down Expand Up @@ -1452,6 +1453,70 @@ def log_factorial(
"""
```

<a name="sinter.plot_custom"></a>
```python
# sinter.plot_custom

# (at top-level in the sinter module)
def plot_custom(
*,
ax: 'plt.Axes',
stats: 'Iterable[sinter.TaskStats]',
x_func: Callable[[sinter.TaskStats], Any],
y_func: Callable[[sinter.TaskStats], Union[sinter.Fit, float, int]],
group_func: Callable[[sinter.TaskStats], ~TCurveId] = lambda _: None,
point_label_func: Callable[[sinter.TaskStats], Any] = lambda _: None,
filter_func: Callable[[sinter.TaskStats], Any] = lambda _: True,
plot_args_func: Callable[[int, ~TCurveId, List[sinter.TaskStats]], Dict[str, Any]] = lambda index, group_key, group_stats: dict(),
line_fits: Optional[Tuple[Literal['linear', 'log', 'sqrt'], Literal['linear', 'log', 'sqrt']]] = None,
) -> None:
"""Plots error rates in curves with uncertainty highlights.

Args:
ax: The plt.Axes to plot onto. For example, the `ax` value from `fig, ax = plt.subplots(1, 1)`.
stats: The collected statistics to plot.
x_func: The X coordinate to use for each stat's data point. For example, this could be
`x_func=lambda stat: stat.json_metadata['physical_error_rate']`.
y_func: The Y value to use for each stat's data point. This can be a float or it can be a
sinter.Fit value, in which case the curve will follow the fit.best value and a
highlighted area will be shown from fit.low to fit.high.
group_func: Optional. When specified, multiple curves will be plotted instead of one curve.
The statistics are grouped into curves based on whether or not they get the same result
out of this function. For example, this could be `group_func=lambda stat: stat.decoder`.
If the result of the function is a dictionary, then optional keys in the dictionary will
also control the plotting of each curve. Available keys are:
'label': the label added to the legend for the curve
'color': the color used for plotting the curve
'marker': the marker used for the curve
'linestyle': the linestyle used for the curve
'sort': the order in which the curves will be plotted and added to the legend
e.g. if two curves (with different resulting dictionaries from group_func) share the same
value for key 'marker', they will be plotted with the same marker.
Colors, markers and linestyles are assigned in order, sorted by the values for those keys.
point_label_func: Optional. Specifies text to draw next to data points.
filter_func: Optional. When specified, some curves will not be plotted.
The statistics are filtered and only plotted if filter_func(stat) returns True.
For example, `filter_func=lambda s: s.json_metadata['basis'] == 'x'` would plot only stats
where the saved metadata indicates the basis was 'x'.
plot_args_func: Optional. Specifies additional arguments to give the underlying calls to
`plot` and `fill_between` used to do the actual plotting. For example, this can be used
to specify markers and colors. Takes the index of the curve in sorted order and also a
curve_id (these will be 0 and None respectively if group_func is not specified). For example,
this could be:

plot_args_func=lambda index, group_key, group_stats: {
'color': (
'red'
if group_key == 'decoder=pymatching p=0.001'
else 'blue'
),
}
line_fits: Defaults to None. Set this to a tuple (x_scale, y_scale) to include a dashed line
fit to every curve. The scales determine how to transform the coordinates before
performing the fit, and can be set to 'linear', 'sqrt', or 'log'.
"""
```

<a name="sinter.plot_discard_rate"></a>
```python
# sinter.plot_discard_rate
Expand Down
25 changes: 25 additions & 0 deletions doc/usage_command_line.md
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@ SYNOPSIS
[--out_format 01|b8|r8|ptb64|hits|dets] \
[--seed int] \
[--shots int] \
[--skip_loop_folding] \
[--skip_reference_sample]

DESCRIPTION
Expand Down Expand Up @@ -1762,6 +1763,30 @@ OPTIONS
Must be an integer between 0 and a quintillion (10^18).


--skip_loop_folding
Skips loop folding logic on the reference sample calculation.

When this argument is specified, the reference sample (that is used
to convert measurement flip data from frame simulations into actual
measurement data) is generated by iterating through the entire
flattened circuit with no loop detection.

Loop folding can enormously improve performance for circuits
containing REPEAT blocks with large repeat counts, by detecting
periodicity in loops and fast-forwarding across them when computing
the reference sample for the circuit. However, in some cases the
analysis is not able to detect the periodicity that is present. For
example, this has been observed in honeycomb code circuits. When
this happens, the folding-capable analysis is slower than simply
analyzing the flattened circuit without any specialized loop logic.
The `--skip_loop_folding` flag can be used to just analyze the
flattened circuit, bypassing this slowdown for circuits such as
honeycomb code circuits.

By default, loop detection is enabled. Pass this flag to disable
it (when appropriate by use case).


--skip_reference_sample
Asserts the circuit can produce a noiseless sample that is just 0s.

Expand Down
2 changes: 2 additions & 0 deletions glue/cirq/stimcirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ._cx_swap_gate import CXSwapGate
from ._cz_swap_gate import CZSwapGate
from ._det_annotation import DetAnnotation
from ._feedback_pauli import FeedbackPauli
from ._obs_annotation import CumulativeObservableAnnotation
from ._shift_coords_annotation import ShiftCoordsAnnotation
from ._stim_sampler import StimSampler
Expand All @@ -19,6 +20,7 @@
JSON_RESOLVERS_DICT = {
"CumulativeObservableAnnotation": CumulativeObservableAnnotation,
"DetAnnotation": DetAnnotation,
"FeedbackPauli": FeedbackPauli,
"MeasureAndOrResetGate": MeasureAndOrResetGate,
"ShiftCoordsAnnotation": ShiftCoordsAnnotation,
"SweepPauli": SweepPauli,
Expand Down
73 changes: 69 additions & 4 deletions glue/cirq/stimcirq/_cirq_to_stim.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

import cirq
import stim
import sympy

from ._i_error_gate import IErrorGate
from ._ii_error_gate import IIErrorGate
from ._ii_gate import IIGate


Expand Down Expand Up @@ -142,6 +141,58 @@ def cirq_circuit_to_stim_data(


StimTypeHandler = Callable[[stim.Circuit, cirq.Gate, List[int], str], None]
StimOpTypeHandler = Callable[[stim.Circuit, cirq.Operation, List[int], str, List[Tuple[str, int]]], None]


def _stim_append_classically_controlled_gate(
circuit: stim.Circuit,
op: cirq.ClassicallyControlledOperation,
targets: List[int],
tag: str,
measurement_key_lengths: List[Tuple[str, int]]):

if len(op.classical_controls) != 1:
raise NotImplementedError(f'Stim only supports single-control Pauli feedback, but got {op=}')
controls: list[cirq.KeyCondition] = []
single_control, = op.classical_controls
if isinstance(single_control, cirq.KeyCondition):
controls.append(single_control)
elif isinstance(single_control, cirq.SympyCondition) and isinstance(single_control.expr, sympy.Xor) and all(isinstance(e, sympy.Symbol) for e in single_control.expr.args):
for symbol in single_control.expr.args:
controls.append(cirq.KeyCondition(key=cirq.MeasurementKey(str(symbol)), index=-1))
else:
raise NotImplementedError(f'Stim only supports single-control Pauli feedback (i.e. a `cirq.KeyCondition` control), but got {single_control=}')
gate = op.without_classical_controls().gate

if gate == cirq.X:
stim_gate = 'X'
elif gate == cirq.Y:
stim_gate = 'Y'
elif gate == cirq.Z:
stim_gate = 'Z'
else:
raise NotImplementedError(f'Stim only supports Pauli feedback, but got {op=}')
assert len(targets) == 1

for control in controls:
skips_left = control.index
for offset in range(len(measurement_key_lengths)):
m_key, m_len = measurement_key_lengths[-1 - offset]
if m_len != 1:
raise NotImplementedError(f"multi-qubit measurement {m_key!r}")
if m_key == control.key:
if skips_left > 0:
skips_left -= 1
else:
rec_target = stim.target_rec(-1 - offset)
break
else:
raise ValueError(
f"{control!r} was processed before the measurement it referenced."
f" Make sure the referenced measurements keys are actually in the circuit, and come"
f" in an earlier moment (or earlier in the same moment's operation order)."
)
circuit.append(f"C{stim_gate}", [rec_target, targets[0]], tag=tag)


@functools.lru_cache(maxsize=1)
Expand Down Expand Up @@ -278,6 +329,14 @@ def gate_type_to_stim_append_func() -> Dict[Type[cirq.Gate], StimTypeHandler]:
}


@functools.lru_cache()
def op_type_to_stim_append_func() -> Dict[Type[cirq.Operation], StimOpTypeHandler]:
"""A dictionary mapping specific gate types to stim circuit appending functions."""
return {
cirq.ClassicallyControlledOperation: _stim_append_classically_controlled_gate,
}


def _stim_append_measurement_gate(
circuit: stim.Circuit, gate: cirq.MeasurementGate, targets: List[int], tag: str
):
Expand Down Expand Up @@ -454,7 +513,8 @@ def process_circuit_operation_into_repeat_block(self, op: cirq.CircuitOperation,

def process_operations(self, operations: Iterable[cirq.Operation]) -> None:
g2f = gate_to_stim_append_func()
t2f = gate_type_to_stim_append_func()
tg2f = gate_type_to_stim_append_func()
to2f = op_type_to_stim_append_func()
for op in operations:
assert isinstance(op, cirq.Operation)
tag = self.tag_func(op)
Expand Down Expand Up @@ -500,11 +560,16 @@ def process_operations(self, operations: Iterable[cirq.Operation]) -> None:
continue

# Look for recognized gate types like cirq.DepolarizingChannel.
type_append_func = t2f.get(type(gate))
type_append_func = tg2f.get(type(gate))
if type_append_func is not None:
type_append_func(self.out, gate, targets, tag=tag)
continue

op_type_append_func = to2f.get(type(op))
if op_type_append_func is not None:
op_type_append_func(self.out, op, targets, tag, self.key_out)
continue

# Ask unrecognized operations to decompose themselves into simpler operations.
try:
self.process_operations(cirq.decompose_once(op))
Expand Down
28 changes: 28 additions & 0 deletions glue/cirq/stimcirq/_cirq_to_stim_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
import stim
import stimcirq
import sympy
from stimcirq._cirq_to_stim import cirq_circuit_to_stim_data, gate_to_stim_append_func


Expand Down Expand Up @@ -425,3 +426,30 @@ def test_round_trip_example_circuit():
cirq_circuit = stimcirq.stim_circuit_to_cirq_circuit(stim_circuit.flattened())
circuit_back = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit)
assert len(circuit_back.shortest_graphlike_error()) == 3


def test_xor_feedback():
a, b, c, d, e = cirq.LineQubit.range(5)
cirq_circuit = cirq.Circuit([
cirq.Moment(
cirq.measure(a, key='a'),
cirq.measure(b, key='b'),
cirq.measure(c, key='c'),
cirq.measure(d, key='d'),
),
cirq.Moment(
cirq.X(e).with_classical_controls(cirq.SympyCondition(sympy.Xor(
sympy.Symbol('a'),
sympy.Symbol('b'),
sympy.Symbol('c'),
sympy.Symbol('d'),
))),
),
])
stim_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit)
assert stim_circuit == stim.Circuit("""
M 0 1 2 3
TICK
CX rec[-4] 4 rec[-3] 4 rec[-2] 4 rec[-1] 4
TICK
""")
67 changes: 67 additions & 0 deletions glue/cirq/stimcirq/_feedback_pauli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import Any, Dict, List, Tuple, Optional

import cirq
import stim


@cirq.value_equality
class FeedbackPauli(cirq.Gate):
"""A Pauli gate conditioned on a prior measurement."""

def __init__(
self,
*,
relative_measurement_index: Optional[int] = None,
pauli: cirq.Pauli,
):
r"""

Args:
relative_measurement_index: A negative integer identifying how many measurements ago is the measurement that
controls the Pauli operation.
pauli: The cirq Pauli operation to apply when the bit is True.
"""
if relative_measurement_index is not None and (relative_measurement_index >= 0 or not isinstance(relative_measurement_index, int)):
raise ValueError(f"{relative_measurement_index=} isn't a negative int (note {type(relative_measurement_index)=})")
self.relative_measurement_index = relative_measurement_index
self.pauli = pauli

def _is_parameterized_(self) -> bool:
return False

def _num_qubits_(self) -> int:
return 1

def _value_equality_values_(self) -> Any:
return self.pauli, self.relative_measurement_index

def _circuit_diagram_info_(self, args: Any) -> str:
return f"{self.pauli}^rec[{self.relative_measurement_index}]"

@staticmethod
def _json_namespace_() -> str:
return ''

def _json_dict_(self) -> Dict[str, Any]:
return {
'pauli': self.pauli,
'relative_measurement_index': self.relative_measurement_index,
}

def __repr__(self) -> str:
return (
f'stimcirq.FeedbackPauli('
f'relative_measurement_index={self.relative_measurement_index!r}, '
f'pauli={self.pauli!r})'
)

def _stim_conversion_(
self,
*,
edit_circuit: stim.Circuit,
tag: str,
targets: List[int],
**kwargs,
):
rec_target = stim.target_rec(self.relative_measurement_index)
edit_circuit.append(f"C{self.pauli}", [rec_target, targets[0]], tag=tag)
Loading