Skip to content

circuit

Quantum circuit wrapper around stim.Circuit with non-Clifford gate support.

Circuit

Circuit(stim_program_text: str = '')

Quantum circuit as a thin wrapper around stim.Circuit.

Circuits are constructed like stim circuits:

    >>> circuit = Circuit('''
    ...     H 0
    ...     T 0
    ...     CNOT 0 1
    ...     M 0 1
    ... ''')

Parameters:

Name Type Description Default
stim_program_text str

Stim program text to parse. If empty, creates an empty circuit.

''
Source code in src/tsim/circuit.py
49
50
51
52
53
54
55
56
57
58
59
60
61
def __init__(self, stim_program_text: str = ""):
    """Initialize circuit from stim program text.

    Args:
        stim_program_text: Stim program text to parse. If empty, creates an
            empty circuit.

    """
    converted = shorthand_to_stim(stim_program_text)
    try:
        self._stim_circ = stim.Circuit(converted)
    except ValueError as exc:
        raise enriched_stim_error(exc, converted) from None

is_clifford property

is_clifford: bool

Check if the circuit is a Clifford circuit.

A circuit is a Clifford circuit if it only contains Clifford gates (i.e. half-pi multiples of the rotation angles).

Returns:

Type Description
bool

True if the circuit is a Clifford circuit, otherwise False.

num_detectors property

num_detectors: int

Counts the number of bits produced when sampling the circuit's detectors.

num_measurements property

num_measurements: int

Counts the number of bits produced when sampling the circuit's measurements.

num_observables property

num_observables: int

Counts the number of bits produced when sampling the circuit's logical observables.

This is one more than the largest observable index given to OBSERVABLE_INCLUDE.

num_qubits property

num_qubits: int

Counts the number of qubits used when simulating the circuit.

This is always one more than the largest qubit index used by the circuit.

num_ticks property

num_ticks: int

Counts the number of TICK instructions executed when running the circuit.

TICKs in loops are counted once per iteration.

Returns:

Type Description
int

The number of ticks executed by the circuit.

stim_circuit property

stim_circuit: Circuit

Return the underlying stim circuit.

Parametric rotation instructions whose angles are all half-π multiples are expanded into their equivalent Clifford gates. REPEAT blocks are preserved structurally; their bodies are expanded recursively.

__add__

__add__(other: Circuit | Circuit) -> Circuit

Return a new circuit with another circuit appended.

Source code in src/tsim/circuit.py
237
238
239
240
241
def __add__(self, other: Circuit | stim.Circuit) -> Circuit:
    """Return a new circuit with another circuit appended."""
    result = Circuit.from_stim_program(self._stim_circ.copy())
    result += other
    return result

__eq__

__eq__(other: object) -> bool

Check equality with another circuit.

Source code in src/tsim/circuit.py
223
224
225
226
227
def __eq__(self, other: object) -> bool:
    """Check equality with another circuit."""
    if isinstance(other, Circuit):
        return self._stim_circ == other._stim_circ
    return NotImplemented

__getitem__

__getitem__(
    index_or_slice: int,
) -> stim.CircuitInstruction | stim.CircuitRepeatBlock
__getitem__(index_or_slice: slice) -> Circuit
__getitem__(index_or_slice: object) -> object

Return copies of instructions from the circuit.

Parameters:

Name Type Description Default
index_or_slice object

An integer index picking out an instruction to return, or a slice picking out a range of instructions to return as a circuit.

required

Returns:

Type Description
object

If the index was an integer, then an instruction from the circuit.

object

If the index was a slice, then a circuit made up of the instructions in that

object

slice.

Source code in src/tsim/circuit.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def __getitem__(
    self,
    index_or_slice: object,
) -> object:
    """Return copies of instructions from the circuit.

    Args:
        index_or_slice: An integer index picking out an instruction to return, or a
            slice picking out a range of instructions to return as a circuit.

    Returns:
        If the index was an integer, then an instruction from the circuit.
        If the index was a slice, then a circuit made up of the instructions in that
        slice.

    """
    if isinstance(index_or_slice, int):
        return self._stim_circ[index_or_slice]
    elif isinstance(index_or_slice, slice):
        return Circuit.from_stim_program(self._stim_circ[index_or_slice])
    else:
        raise TypeError(f"Invalid index or slice: {index_or_slice}")

__iadd__

__iadd__(other: Circuit | Circuit) -> Circuit

Append another circuit to this circuit in-place.

Source code in src/tsim/circuit.py
229
230
231
232
233
234
235
def __iadd__(self, other: Circuit | stim.Circuit) -> Circuit:
    """Append another circuit to this circuit in-place."""
    if isinstance(other, Circuit):
        self._stim_circ += other._stim_circ
    else:
        self._stim_circ += other
    return self

__imul__

__imul__(repetitions: int) -> Circuit

Repeat this circuit in-place.

Source code in src/tsim/circuit.py
243
244
245
246
def __imul__(self, repetitions: int) -> Circuit:
    """Repeat this circuit in-place."""
    self._stim_circ *= repetitions
    return self

__len__

__len__() -> int

Return the number of instructions in the circuit.

Source code in src/tsim/circuit.py
219
220
221
def __len__(self) -> int:
    """Return the number of instructions in the circuit."""
    return len(self._stim_circ)

__mul__

__mul__(repetitions: int) -> Circuit

Return a new circuit repeated the given number of times.

Source code in src/tsim/circuit.py
248
249
250
def __mul__(self, repetitions: int) -> Circuit:
    """Return a new circuit repeated the given number of times."""
    return Circuit.from_stim_program(self._stim_circ * repetitions)

__repr__

__repr__() -> str

Return a string representation that can recreate the circuit.

Source code in src/tsim/circuit.py
211
212
213
def __repr__(self) -> str:
    """Return a string representation that can recreate the circuit."""
    return f"tsim.Circuit('''\n{self!s}\n''')"

__rmul__

__rmul__(repetitions: int) -> Circuit

Return a new circuit repeated the given number of times.

Source code in src/tsim/circuit.py
252
253
254
def __rmul__(self, repetitions: int) -> Circuit:
    """Return a new circuit repeated the given number of times."""
    return self * repetitions

__str__

__str__() -> str

Return the circuit as program text.

Source code in src/tsim/circuit.py
215
216
217
def __str__(self) -> str:
    """Return the circuit as program text."""
    return stim_to_shorthand(str(self._stim_circ))

append

append(
    name: str,
    targets: (
        int
        | GateTarget
        | PauliString
        | Iterable[int | GateTarget | PauliString]
    ) = (),
    arg: float | Iterable[float] | None = None,
    *,
    tag: str = ""
) -> None
append(
    name: CircuitInstruction | CircuitRepeatBlock | Circuit,
) -> None
append(
    name: (
        str
        | CircuitInstruction
        | CircuitRepeatBlock
        | Circuit
    ),
    targets: (
        int
        | GateTarget
        | PauliString
        | Iterable[int | GateTarget | PauliString]
    ) = (),
    arg: float | Iterable[float] | None = None,
    *,
    tag: str = ""
) -> None

Append an operation into the circuit.

Parameters:

Name Type Description Default
name str | CircuitInstruction | CircuitRepeatBlock | Circuit

The name of the operation's gate (e.g. "H" or "M" or "CNOT").

This argument can also be set to a stim.CircuitInstruction or stim.CircuitInstructionBlock, which results in the instruction or block being appended to the circuit. The other arguments (targets and arg) can't be specified when doing so.

required
targets int | GateTarget | PauliString | Iterable[int | GateTarget | PauliString]

The objects operated on by the gate. This can be either a single target or an iterable of multiple targets.

Each target can be: An int: The index of a targeted qubit. A stim.GateTarget: Could be a variety of things. Methods like stim.target_rec, stim.target_sweet, stim.target_x, and stim.CircuitInstruction.__getitem__ all return this type. A stim.PauliString: This will automatically be expanded into a product of pauli targets like X1*Y2*Z3.

()
arg float | Iterable[float] | None

The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, X_ERROR takes a probability, OBSERVABLE_INCLUDE takes an observable index, and PAULI_CHANNEL_1 takes three disjoint probabilities.

None
tag str

A customizable string attached to the instruction.

''
Source code in src/tsim/circuit.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def append(
    self,
    name: str | stim.CircuitInstruction | stim.CircuitRepeatBlock | stim.Circuit,
    targets: (
        int
        | stim.GateTarget
        | stim.PauliString
        | Iterable[int | stim.GateTarget | stim.PauliString]
    ) = (),
    arg: float | Iterable[float] | None = None,
    *,
    tag: str = "",
) -> None:
    """Append an operation into the circuit.

    Args:
        name: The name of the operation's gate (e.g. "H" or "M" or "CNOT").

            This argument can also be set to a `stim.CircuitInstruction` or
            `stim.CircuitInstructionBlock`, which results in the instruction or
            block being appended to the circuit. The other arguments (targets
            and arg) can't be specified when doing so.

        targets: The objects operated on by the gate. This can be either a
            single target or an iterable of multiple targets.

            Each target can be:
                An int: The index of a targeted qubit.
                A `stim.GateTarget`: Could be a variety of things. Methods like
                    `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and
                    `stim.CircuitInstruction.__getitem__` all return this type.
                A `stim.PauliString`: This will automatically be expanded into
                    a product of pauli targets like `X1*Y2*Z3`.
        arg: The "parens arguments" for the gate, such as the probability for a
            noise operation. A double or list of doubles parameterizing the
            gate. Different gates take different parens arguments. For example,
            X_ERROR takes a probability, OBSERVABLE_INCLUDE takes an observable
            index, and PAULI_CHANNEL_1 takes three disjoint probabilities.

        tag: A customizable string attached to the instruction.

    """
    if isinstance(name, str):
        if name == "TPP":
            name = "SPP"
            tag = "T"
        elif name == "TPP_DAG":
            name = "SPP_DAG"
            tag = "T"
        elif name == "T":
            name = "S"
            tag = "T"
        elif name == "T_DAG":
            name = "S_DAG"
            tag = "T"
        elif name in ("R_X", "R_Y", "R_Z"):
            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."
                )
            tag = f"{name}(theta={args[0]}*pi)"
            name = "I"
            arg = None
        elif name == "U3":
            args = list(arg) if isinstance(arg, Iterable) else []
            if arg is None or len(args) != 3:
                raise ValueError(
                    "For U3 gates, three rotation angles must be provided."
                )
            theta, phi, lam = args
            tag = f"U3(theta={theta}*pi, phi={phi}*pi, lambda={lam}*pi)"
            name = "I"
            arg = None

        self._stim_circ.append(name=name, targets=targets, arg=arg, tag=tag)  # type: ignore
    else:
        self._stim_circ.append(name=name)

append_from_stim_program_text

append_from_stim_program_text(
    stim_program_text: str,
) -> None

Append operations described by a Stim format program into the circuit.

Supports the same shorthand syntax as the constructor.

Source code in src/tsim/circuit.py
78
79
80
81
82
83
84
85
86
87
def append_from_stim_program_text(self, stim_program_text: str) -> None:
    """Append operations described by a Stim format program into the circuit.

    Supports the same shorthand syntax as the constructor.
    """
    converted = shorthand_to_stim(stim_program_text)
    try:
        self._stim_circ.append_from_stim_program_text(converted)
    except ValueError as exc:
        raise enriched_stim_error(exc, converted) from None

approx_equals

approx_equals(other: object, *, atol: float) -> bool

Check if a circuit is approximately equal to another circuit.

Two circuits are approximately equal if they are equal up to slight perturbations of instruction arguments such as probabilities. For example, X_ERROR(0.100) 0 is approximately equal to X_ERROR(0.099) within an absolute tolerance of 0.002. All other details of the circuits (such as the ordering of instructions and targets) must be exactly the same.

Parameters:

Name Type Description Default
other object

The circuit, or other object, to compare to this one.

required
atol float

The absolute error tolerance. The maximum amount each probability may have been perturbed by.

required

Returns:

Type Description
bool

True if the given object is a circuit approximately equal up to the

bool

receiving circuit up to the given tolerance, otherwise False.

Source code in src/tsim/circuit.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def approx_equals(
    self,
    other: object,
    *,
    atol: float,
) -> bool:
    """Check if a circuit is approximately equal to another circuit.

    Two circuits are approximately equal if they are equal up to slight
    perturbations of instruction arguments such as probabilities. For example,
    `X_ERROR(0.100) 0` is approximately equal to `X_ERROR(0.099)` within an absolute
    tolerance of 0.002. All other details of the circuits (such as the ordering of
    instructions and targets) must be exactly the same.

    Args:
        other: The circuit, or other object, to compare to this one.
        atol: The absolute error tolerance. The maximum amount each probability may
            have been perturbed by.

    Returns:
        True if the given object is a circuit approximately equal up to the
        receiving circuit up to the given tolerance, otherwise False.

    """
    if isinstance(other, Circuit):
        return self._stim_circ.approx_equals(other._stim_circ, atol=atol)
    elif isinstance(other, stim.Circuit):
        return self._stim_circ.approx_equals(other, atol=atol)
    else:
        return False

cast_to_stim

cast_to_stim() -> stim.Circuit

Return self with type cast to stim.Circuit.

This is useful for passing the circuit to functions that expect a stim.Circuit.

Source code in src/tsim/circuit.py
764
765
766
767
768
769
770
def cast_to_stim(self) -> stim.Circuit:
    """Return self with type cast to `stim.Circuit`.

    This is useful for passing the circuit to functions that expect a
    `stim.Circuit`.
    """
    return cast(stim.Circuit, self)  # pragma: no cover

compile_detector_sampler

compile_detector_sampler(
    *,
    strategy: DecompositionStrategy = "cat5",
    seed: int | None = None
) -> CompiledDetectorSampler

Compile circuit into a detector sampler.

Connected components whose single output is deterministically given by one f-variable are handled via a fast direct path (no compilation or autoregressive sampling). Remaining components go through the full compilation pipeline.

Parameters:

Name Type Description Default
strategy DecompositionStrategy

Stabilizer rank decomposition strategy. Must be one of "cat5", "bss", "cutting".

'cat5'
seed int | None

Random seed for the sampler. IMPORTANT: Currently, the sampler will only produce deterministic samples for fixed batch size. If deterministic samples are needed, the batch size should be set manually.

None

Returns:

Type Description
CompiledDetectorSampler

A CompiledDetectorSampler that can be used to sample detectors and observables.

Source code in src/tsim/circuit.py
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
def compile_detector_sampler(
    self,
    *,
    strategy: DecompositionStrategy = "cat5",
    seed: int | None = None,
) -> CompiledDetectorSampler:
    """Compile circuit into a detector sampler.

    Connected components whose single output is deterministically given by
    one f-variable are handled via a fast direct path (no compilation or
    autoregressive sampling).  Remaining components go through the full
    compilation pipeline.

    Args:
        strategy: Stabilizer rank decomposition strategy.
            Must be one of "cat5", "bss", "cutting".
        seed: Random seed for the sampler. IMPORTANT: Currently, the sampler will
            only produce deterministic samples for fixed batch size. If
            deterministic samples are needed, the batch size should be set
            manually.

    Returns:
        A CompiledDetectorSampler that can be used to sample detectors and observables.

    """
    from tsim.sampler import CompiledDetectorSampler

    return CompiledDetectorSampler(self, seed=seed, strategy=strategy)

compile_m2d_converter

compile_m2d_converter(
    *, skip_reference_sample: bool = False
) -> stim.CompiledMeasurementsToDetectionEventsConverter

Create a measurement-to-detection-event converter for the given circuit.

The converter can efficiently compute detection events and observable flips from raw measurement data.

The converter uses a noiseless reference sample, collected from the circuit using stim's Tableau simulator during initialization of the converter, as a baseline for determining what the expected value of a detector is.

Note that the expected behavior of gauge detectors (detectors that are not actually deterministic under noiseless execution) can vary depending on the reference sample. Stim mitigates this by always generating the same reference sample for a given circuit.

Parameters:

Name Type Description Default
skip_reference_sample bool

Defaults to False. When set to True, the reference sample used by the converter is initialized to all-zeroes instead of being collected from the circuit. This should only be used if it's known that the all-zeroes sample is actually a possible result from the circuit (under noiseless execution).

False

Returns:

Type Description
CompiledMeasurementsToDetectionEventsConverter

An initialized stim.CompiledMeasurementsToDetectionEventsConverter.

Source code in src/tsim/circuit.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def compile_m2d_converter(
    self,
    *,
    skip_reference_sample: bool = False,
) -> stim.CompiledMeasurementsToDetectionEventsConverter:
    """Create a measurement-to-detection-event converter for the given circuit.

    The converter can efficiently compute detection events and observable flips
    from raw measurement data.

    The converter uses a noiseless reference sample, collected from the circuit
    using stim's Tableau simulator during initialization of the converter, as a
    baseline for determining what the expected value of a detector is.

    Note that the expected behavior of gauge detectors (detectors that are not
    actually deterministic under noiseless execution) can vary depending on the
    reference sample. Stim mitigates this by always generating the same reference
    sample for a given circuit.

    Args:
        skip_reference_sample: Defaults to False. When set to True, the reference
            sample used by the converter is initialized to all-zeroes instead of
            being collected from the circuit. This should only be used if it's known
            that the all-zeroes sample is actually a possible result from the
            circuit (under noiseless execution).

    Returns:
        An initialized stim.CompiledMeasurementsToDetectionEventsConverter.

    """
    return self._stim_circ.compile_m2d_converter(
        skip_reference_sample=skip_reference_sample
    )

compile_sampler

compile_sampler(
    *,
    strategy: DecompositionStrategy = "cat5",
    seed: int | None = None
) -> CompiledMeasurementSampler

Compile circuit into a measurement sampler.

Parameters:

Name Type Description Default
strategy DecompositionStrategy

Stabilizer rank decomposition strategy. Must be one of "cat5", "bss", "cutting".

'cat5'
seed int | None

Random seed for the sampler. IMPORTANT: Currently, the sampler will only produce deterministic samples for fixed batch size. If deterministic samples are needed, the batch size should be set manually.

None

Returns:

Type Description
CompiledMeasurementSampler

A CompiledMeasurementSampler that can be used to sample measurements.

Source code in src/tsim/circuit.py
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
def compile_sampler(
    self,
    *,
    strategy: DecompositionStrategy = "cat5",
    seed: int | None = None,
) -> CompiledMeasurementSampler:
    """Compile circuit into a measurement sampler.

    Args:
        strategy: Stabilizer rank decomposition strategy.
            Must be one of "cat5", "bss", "cutting".
        seed: Random seed for the sampler. IMPORTANT: Currently, the sampler will
            only produce deterministic samples for fixed batch size. If
            deterministic samples are needed, the batch size should be set
            manually.

    Returns:
        A CompiledMeasurementSampler that can be used to sample measurements.

    """
    from tsim.sampler import CompiledMeasurementSampler

    return CompiledMeasurementSampler(self, seed=seed, strategy=strategy)

copy

copy() -> Circuit

Create a copy of this circuit.

Source code in src/tsim/circuit.py
437
438
439
def copy(self) -> Circuit:
    """Create a copy of this circuit."""
    return Circuit.from_stim_program(self._stim_circ.copy())

detector_error_model

detector_error_model(
    *,
    decompose_errors: bool = False,
    flatten_loops: bool = False,
    allow_gauge_detectors: bool = False,
    approximate_disjoint_errors: bool = False,
    ignore_decomposition_failures: bool = False,
    block_decomposition_from_introducing_remnant_edges: bool = False
) -> stim.DetectorErrorModel

Return a stim.DetectorErrorModel describing the error processes in the circuit.

Unlike the stim.Circuit.detector_error_model() method, this method allows for non-deterministic observables when allow_gauge_detectors is set to true.

Parameters:

Name Type Description Default
decompose_errors bool

Defaults to false. When set to true, the error analysis attempts to decompose the components of composite error mechanisms (such as depolarization errors) into simpler errors, and suggest this decomposition via stim.target_separator() between the components. For example, in an XZ surface code, single qubit depolarization has a Y error term which can be decomposed into simpler X and Z error terms. Decomposition fails (causing this method to throw) if it's not possible to decompose large errors into simple errors that affect at most two detectors.

This is not supported by tsim and setting it to true will raise an error. The argument is present for compatibility with stim.

False
flatten_loops bool

Defaults to false. When set to true, the output will not contain any repeat blocks. When set to false, the error analysis watches for loops in the circuit reaching a periodic steady state with respect to the detectors being introduced, the error mechanisms that affect them, and the locations of the logical observables. When it identifies such a steady state, it outputs a repeat block. This is massively more efficient than flattening for circuits that contain loops, but creates a more complex output.

Irrelevant unless allow_gauge_detectors=False.

False
allow_gauge_detectors bool

Defaults to false. When set to false, the error analysis verifies that detectors in the circuit are actually deterministic under noiseless execution of the circuit.

Note that, unlike in stim, logical observables are also allowed to be non-deterministic.

False
approximate_disjoint_errors bool

Defaults to false. When set to false, composite error mechanisms with disjoint components (such as PAULI_CHANNEL_1(0.1, 0.2, 0.0)) can cause the error analysis to throw exceptions (because detector error models can only contain independent error mechanisms). When set to true, the probabilities of the disjoint cases are instead assumed to be independent probabilities. For example, a `PAULI_CHANNEL_1(0.1, 0.2, 0.0) becomes equivalent to an X_ERROR(0.1) followed by a Z_ERROR(0.2). This assumption is an approximation, but it is a good approximation for small probabilities.

This argument can also be set to a probability between 0 and 1, setting a threshold below which the approximation is acceptable. Any error mechanisms that have a component probability above the threshold will cause an exception to be thrown.

False
ignore_decomposition_failures bool

Defaults to False. When this is set to True, circuit errors that fail to decompose into graphlike detector error model errors no longer cause the conversion process to abort. Instead, the undecomposed error is inserted into the output. Whatever tool the detector error model is then given to is responsible for dealing with the undecomposed errors (e.g. a tool may choose to simply ignore them).

Irrelevant unless decompose_errors=True.

False
block_decomposition_from_introducing_remnant_edges bool

Defaults to False. Requires that both A B and C D be present elsewhere in the detector error model in order to decompose A B C D into A B ^ C D. Normally, only one of A B or C D needs to appear to allow this decomposition.

Remnant edges can be a useful feature for ensuring decomposition succeeds, but they can also reduce the effective code distance by giving the decoder single edges that actually represent multiple errors in the circuit (resulting in the decoder making misinformed choices when decoding).

Irrelevant unless decompose_errors=True.

False
Source code in src/tsim/circuit.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
def detector_error_model(
    self,
    *,
    decompose_errors: bool = False,
    flatten_loops: bool = False,
    allow_gauge_detectors: bool = False,
    approximate_disjoint_errors: bool = False,
    ignore_decomposition_failures: bool = False,
    block_decomposition_from_introducing_remnant_edges: bool = False,
) -> stim.DetectorErrorModel:
    """Return a stim.DetectorErrorModel describing the error processes in the circuit.

    Unlike the stim.Circuit.detector_error_model() method, this method allows for non-deterministic observables
    when `allow_gauge_detectors` is set to true.

    Args:
        decompose_errors: Defaults to false. When set to true, the error analysis attempts to decompose the
            components of composite error mechanisms (such as depolarization errors) into simpler errors, and
            suggest this decomposition via `stim.target_separator()` between the components. For example, in an
            XZ surface code, single qubit depolarization has a Y error term which can be decomposed into simpler
            X and Z error terms. Decomposition fails (causing this method to throw) if it's not possible to
            decompose large errors into simple errors that affect at most two detectors.

            This is not supported by tsim and setting it to true will raise an error. The argument is present
            for compatibility with stim.
        flatten_loops: Defaults to false. When set to true, the output will not contain any `repeat` blocks.
            When set to false, the error analysis watches for loops in the circuit reaching a periodic steady
            state with respect to the detectors being introduced, the error mechanisms that affect them, and the
            locations of the logical observables. When it identifies such a steady state, it outputs a repeat
            block. This is massively more efficient than flattening for circuits that contain loops, but creates
            a more complex output.

            Irrelevant unless allow_gauge_detectors=False.
        allow_gauge_detectors: Defaults to false. When set to false, the error analysis verifies that detectors
            in the circuit are actually deterministic under noiseless execution of the circuit.

            Note that, unlike in stim, logical observables are also allowed to be non-deterministic.
        approximate_disjoint_errors: Defaults to false. When set to false, composite error mechanisms with
            disjoint components (such as `PAULI_CHANNEL_1(0.1, 0.2, 0.0)`) can cause the error analysis to throw
            exceptions (because detector error models can only contain independent error mechanisms). When set
            to true, the probabilities of the disjoint cases are instead assumed to be independent
            probabilities. For example, a ``PAULI_CHANNEL_1(0.1, 0.2, 0.0)` becomes equivalent to an
            `X_ERROR(0.1)` followed by a `Z_ERROR(0.2)`. This assumption is an approximation, but it is a good
            approximation for small probabilities.

            This argument can also be set to a probability between 0 and 1, setting a threshold below which the
            approximation is acceptable. Any error mechanisms that have a component probability above the
            threshold will cause an exception to be thrown.
        ignore_decomposition_failures: Defaults to False.
            When this is set to True, circuit errors that fail to decompose into graphlike
            detector error model errors no longer cause the conversion process to abort.
            Instead, the undecomposed error is inserted into the output. Whatever tool
            the detector error model is then given to is responsible for dealing with the
            undecomposed errors (e.g. a tool may choose to simply ignore them).

            Irrelevant unless decompose_errors=True.
        block_decomposition_from_introducing_remnant_edges: Defaults to False.
            Requires that both A B and C D be present elsewhere in the detector error model
            in order to decompose A B C D into A B ^ C D. Normally, only one of A B or C D
            needs to appear to allow this decomposition.

            Remnant edges can be a useful feature for ensuring decomposition succeeds, but
            they can also reduce the effective code distance by giving the decoder single
            edges that actually represent multiple errors in the circuit (resulting in the
            decoder making misinformed choices when decoding).

            Irrelevant unless decompose_errors=True.

    """
    return get_detector_error_model(
        self._stim_circ,
        allow_non_deterministic_observables=True,
        decompose_errors=decompose_errors,
        flatten_loops=flatten_loops,
        allow_gauge_detectors=allow_gauge_detectors,
        approximate_disjoint_errors=approximate_disjoint_errors,
        ignore_decomposition_failures=ignore_decomposition_failures,
        block_decomposition_from_introducing_remnant_edges=block_decomposition_from_introducing_remnant_edges,
    )

diagram

diagram(
    type: Literal[
        "pyzx",
        "pyzx-dets",
        "pyzx-meas",
        "timeline-svg",
        "timeslice-svg",
    ] = "timeline-svg",
    tick: int | range | None = None,
    filter_coords: Iterable[Iterable[float] | DemTarget] = (
        (),
    ),
    rows: int | None = None,
    height: float | None = None,
    width: float | None = None,
    zoomable: bool = True,
    **kwargs: Any
) -> Any

Return a diagram of the circuit, from a variety of options.

Parameters:

Name Type Description Default
type Literal['pyzx', 'pyzx-dets', 'pyzx-meas', 'timeline-svg', 'timeslice-svg']

The type of diagram. Available types are: "pyzx": A pyzx SVG of the ZX diagram of the circuit. "pyzx-dets": A pyzx SVG of the ZX diagram that is used to compute probabilities of detectors and observables. "pyzx-meas": A pyzx SVG of the ZX diagram that is used to compute probabilities of measurements. "timeline-svg": An SVG image of the operations applied by the circuit over time. Includes annotations showing the measurement record index that each measurement writes to, and the measurements used by detectors. "timeslice-svg": An SVG image of the operations applied between two TICK instructions in the circuit, with the operations laid out in 2d.

'timeline-svg'
tick int | range | None

Required for time slice diagrams. Specifies which TICK instruction, or range of TICK instructions, to slice at. Note that the first TICK instruction in the circuit corresponds tick=1. The value tick=0 refers to the very start of the circuit.

Passing range(A, B) for a detector slice will show the slices for ticks A through B including A but excluding B.

Passing range(A, B) for a time slice will show the operations between tick A and tick B.

None
rows int | None

In diagrams that have multiple separate pieces, such as timeslice diagrams and detslice diagrams, this controls how many rows of pieces there will be. If not specified, a number of rows that creates a roughly square layout will be chosen.

None
filter_coords Iterable[Iterable[float] | DemTarget]

A list of things to include in the diagram. Different effects depending on the diagram.

For detslice diagrams, the filter defaults to showing all detectors and no observables. When specified, each list entry can be a collection of floats (detectors whose coordinates start with the same numbers will be included), a stim.DemTarget (specifying a detector or observable to include), a string like "D5" or "L0" specifying a detector or observable to include.

((),)
height float | None

Optional height for the rendered diagram in pixels. Only applied to timeline-svg and timeslice-svg diagram types. For timeline-svg, when both height and width are None, the height is automatically determined based on the number of qubits. When only one dimension is given, the other is computed from the SVG aspect ratio.

None
width float | None

Optional width for the rendered diagram in pixels. Only applied to timeline-svg and timeslice-svg diagram types.

None
zoomable bool

If True (default), wraps SVG diagrams in an interactive container with pan and Ctrl/Cmd+wheel zoom. Only applies to timeline-svg and timeslice-svg diagram types.

True
**kwargs Any

Additional keyword arguments passed to the underlying diagram renderer.

{}

Returns:

Type Description
Any

An object whose __str__ method returns the diagram, so that

Any

writing the diagram to a file works correctly. The returned

Any

object may also define methods such as _repr_html_, so that

Any

ipython notebooks recognize it can be shown using a specialized

Any

viewer instead of as raw text.

Source code in src/tsim/circuit.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
def diagram(
    self,
    type: Literal[
        "pyzx",
        "pyzx-dets",
        "pyzx-meas",
        "timeline-svg",
        "timeslice-svg",
    ] = "timeline-svg",
    tick: int | range | None = None,
    filter_coords: Iterable[Iterable[float] | stim.DemTarget] = ((),),
    rows: int | None = None,
    height: float | None = None,
    width: float | None = None,
    zoomable: bool = True,
    **kwargs: Any,
) -> Any:
    """Return a diagram of the circuit, from a variety of options.

    Args:
        type: The type of diagram. Available types are:
            "pyzx": A pyzx SVG of the ZX diagram of the circuit.
            "pyzx-dets": A pyzx SVG of the ZX diagram that is used to compute
                probabilities of detectors and observables.
            "pyzx-meas": A pyzx SVG of the ZX diagram that is used to compute
                probabilities of measurements.
            "timeline-svg": An SVG image of the operations applied by
                the circuit over time. Includes annotations showing the
                measurement record index that each measurement writes
                to, and the measurements used by detectors.
            "timeslice-svg": An SVG image of the operations applied
                between two TICK instructions in the circuit, with the
                operations laid out in 2d.
        tick: Required for time slice diagrams. Specifies
            which TICK instruction, or range of TICK instructions, to
            slice at. Note that the first TICK instruction in the
            circuit corresponds tick=1. The value tick=0 refers to the
            very start of the circuit.

            Passing `range(A, B)` for a detector slice will show the
            slices for ticks A through B including A but excluding B.

            Passing `range(A, B)` for a time slice will show the
            operations between tick A and tick B.
        rows: In diagrams that have multiple separate pieces, such as timeslice
            diagrams and detslice diagrams, this controls how many rows of
            pieces there will be. If not specified, a number of rows that creates
            a roughly square layout will be chosen.
        filter_coords: A list of things to include in the diagram. Different
            effects depending on the diagram.

            For detslice diagrams, the filter defaults to showing all detectors
            and no observables. When specified, each list entry can be a collection
            of floats (detectors whose coordinates start with the same numbers will
            be included), a stim.DemTarget (specifying a detector or observable
            to include), a string like "D5" or "L0" specifying a detector or
            observable to include.
        height: Optional height for the rendered diagram in pixels. Only applied
            to timeline-svg and timeslice-svg diagram types. For
            timeline-svg, when both height and width are None, the height is
            automatically determined based on the number of qubits. When only
            one dimension is given, the other is computed from the SVG aspect
            ratio.
        width: Optional width for the rendered diagram in pixels. Only applied
            to timeline-svg and timeslice-svg diagram types.
        zoomable: If True (default), wraps SVG diagrams in an interactive
            container with pan and Ctrl/Cmd+wheel zoom. Only applies to
            timeline-svg and timeslice-svg diagram types.
        **kwargs: Additional keyword arguments passed to the underlying diagram renderer.

    Returns:
        An object whose `__str__` method returns the diagram, so that
        writing the diagram to a file works correctly. The returned
        object may also define methods such as `_repr_html_`, so that
        ipython notebooks recognize it can be shown using a specialized
        viewer instead of as raw text.

    """
    if type in [
        "timeline-svg",
        "timeslice-svg",
    ]:
        if height is None and width is None and type == "timeline-svg":
            height = min(30 * self.num_qubits + 50, 700)
        return render_svg(
            self._stim_circ,
            type,
            tick=tick,
            filter_coords=filter_coords,
            rows=rows,
            width=width,
            height=height,
            zoomable=zoomable,
        )
    elif type == "pyzx":
        return render_pyzx_d3(self._stim_circ, kwargs)
    elif type in ["pyzx-dets", "pyzx-meas"]:
        from tsim.core.graph import (
            scale_horizontally,
            squash_graph,
            transform_error_basis,
        )

        g = self.get_sampling_graph(sample_detectors=type == "pyzx-dets")
        zx.full_reduce(g, paramSafe=True)
        g, _ = transform_error_basis(g)
        squash_graph(g)
        if kwargs.get("scale_horizontally", False):
            scale_horizontally(g, kwargs.pop("scale_horizontally", 1.0))
        zx.draw(g, **kwargs)
        return g
    else:
        return self._stim_circ.diagram(type=type, **kwargs)  # pragma: no cover

flattened

flattened() -> Circuit

Return a copy of the circuit with all repeat blocks expanded.

Source code in src/tsim/circuit.py
441
442
443
def flattened(self) -> Circuit:
    """Return a copy of the circuit with all repeat blocks expanded."""
    return Circuit.from_stim_program(self._stim_circ.flattened())

from_file classmethod

from_file(filename: str) -> Circuit

Create a Circuit from a file.

Parameters:

Name Type Description Default
filename str

The filename to read the circuit from.

required

Returns:

Type Description
Circuit

A new Circuit instance.

Source code in src/tsim/circuit.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
@classmethod
def from_file(cls, filename: str) -> Circuit:
    """Create a Circuit from a file.

    Args:
        filename: The filename to read the circuit from.

    Returns:
        A new Circuit instance.

    """
    with open(filename, encoding="utf-8") as f:
        stim_program_text = f.read()
    converted = shorthand_to_stim(stim_program_text)
    try:
        stim_circ = stim.Circuit(converted)
    except ValueError as exc:
        raise enriched_stim_error(exc, converted) from None
    return cls.from_stim_program(stim_circ)

from_stim_program classmethod

from_stim_program(stim_circuit: Circuit) -> Circuit

Create a Circuit from a stim.Circuit object.

Parameters:

Name Type Description Default
stim_circuit Circuit

The stim circuit to wrap.

required

Returns:

Type Description
Circuit

A new Circuit instance.

Source code in src/tsim/circuit.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@classmethod
def from_stim_program(cls, stim_circuit: stim.Circuit) -> Circuit:
    """Create a Circuit from a stim.Circuit object.

    Args:
        stim_circuit: The stim circuit to wrap.

    Returns:
        A new Circuit instance.

    """
    c = cls.__new__(cls)
    c._stim_circ = stim_circuit.copy()
    return c

get_graph

get_graph() -> BaseGraph

Construct the ZX graph.

Source code in src/tsim/circuit.py
679
680
681
682
def get_graph(self) -> BaseGraph:
    """Construct the ZX graph."""
    built = parse_stim_circuit(self._stim_circ)
    return built.graph

get_sampling_graph

get_sampling_graph(
    sample_detectors: bool = False,
) -> BaseGraph

Get a ZX graph that can be used to compute probabilities.

This graph will be constructed as follows:

  1. Double the ZX-diagram by composing it with its adjoint.
  2. Connect all rec[i] vertices to their corresponding adjoint rec[i] vertices.
  3. Add outputs: (a) When sampling measurements (i.e. sample_detectors is False), add output nodes for each measurement. Detectors and observables are removed since they are ignored when sampling measurements. (b) When sampling detectors and observables (i.e. sample_detectors is True), add output nodes for each detector and observable. Only one set of detector and observable nodes is kept, i.e., detectors and observables are not composed with their adjoints.

Parameters:

Name Type Description Default
sample_detectors bool

If True, sample detectors and observables instead of measurements.

False

Returns:

Type Description
BaseGraph

A ZX graph for sampling.

Source code in src/tsim/circuit.py
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
def get_sampling_graph(self, sample_detectors: bool = False) -> BaseGraph:
    """Get a ZX graph that can be used to compute probabilities.

    This graph will be constructed as follows:

    1. Double the ZX-diagram by composing it with its adjoint.
    2. Connect all rec[i] vertices to their corresponding adjoint rec[i] vertices.
    3. Add outputs:
    (a) When sampling measurements (i.e. `sample_detectors` is False),
        add output nodes for each measurement. Detectors and observables are
        removed since they are ignored when sampling measurements.
    (b) When sampling detectors and observables (i.e. `sample_detectors` is True),
        add output nodes for each detector and observable. Only one set of detector
        and observable nodes is kept, i.e., detectors and observables are not
        composed with their adjoints.

    Args:
        sample_detectors: If True, sample detectors and observables instead of
            measurements.

    Returns:
        A ZX graph for sampling.

    """
    built = parse_stim_circuit(self._stim_circ)
    return build_sampling_graph(built, sample_detectors=sample_detectors)

inverse

inverse() -> Circuit

Return the inverse of the circuit.

Source code in src/tsim/circuit.py
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
def inverse(self) -> Circuit:
    """Return the inverse of the circuit."""

    def fix_tags(circuit: stim.Circuit) -> stim.Circuit:
        # Stim only inverts Clifford gates (and S[T] / S_DAG[T]).
        # Non-Clifford rotations stored as I[tag] need their parameters
        # negated (and reordered for U3).
        result = stim.Circuit()
        for instr in circuit:
            if isinstance(instr, stim.CircuitRepeatBlock):
                fixed = fix_tags(instr.body_copy())
                result.append(stim.CircuitRepeatBlock(instr.repeat_count, fixed))
                continue

            name = instr.name
            tag = instr.tag
            targets = [t.value for t in instr.targets_copy()]
            args = instr.gate_args_copy()

            if name == "I" and tag:
                parsed = parse_parametric_tag(instr)
                if parsed is not None:
                    gate_name, params = parsed
                    if gate_name == "U3":
                        # U3(θ, φ, λ)⁻¹ = U3(-θ, -λ, -φ)
                        theta = float(-params["theta"])
                        phi = float(-params["lambda"])
                        lam = float(-params["phi"])
                        new_tag = (
                            f"U3(theta={theta}*pi, phi={phi}*pi, lambda={lam}*pi)"
                        )
                    else:
                        theta = float(-params["theta"])
                        new_tag = f"{gate_name}(theta={theta}*pi)"
                    result.append("I", targets, args, tag=new_tag)
                    continue

            result.append(instr)
        return result

    inv_stim = fix_tags(self._stim_circ.inverse())
    return Circuit.from_stim_program(inv_stim)

pop

pop(
    index: int = -1,
) -> stim.CircuitInstruction | stim.CircuitRepeatBlock

Pops an operation from the end of the circuit, or at the given index.

Parameters:

Name Type Description Default
index int

Defaults to -1 (end of circuit). The index to pop from.

-1

Returns:

Type Description
CircuitInstruction | CircuitRepeatBlock

The popped instruction or repeat block.

Raises:

Type Description
IndexError

The given index is outside the bounds of the circuit.

Source code in src/tsim/circuit.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def pop(
    self,
    index: int = -1,
) -> stim.CircuitInstruction | stim.CircuitRepeatBlock:
    """Pops an operation from the end of the circuit, or at the given index.

    Args:
        index: Defaults to -1 (end of circuit). The index to pop from.

    Returns:
        The popped instruction or repeat block.

    Raises:
        IndexError: The given index is outside the bounds of the circuit.

    """
    return self._stim_circ.pop(index)

tcount

tcount() -> int

Count the number of T gates in the circuit.

Source code in src/tsim/circuit.py
674
675
676
677
def tcount(self) -> int:
    """Count the number of T gates in the circuit."""
    built = parse_stim_circuit(self._stim_circ)
    return zx.tcount(built.graph)

to_matrix

to_matrix() -> Any

Convert circuit to matrix representation.

Source code in src/tsim/circuit.py
667
668
669
670
671
672
def to_matrix(self) -> Any:
    """Convert circuit to matrix representation."""
    built = parse_stim_circuit(self._stim_circ)
    g = built.graph.copy()
    g.normalize()
    return g.to_matrix()

to_tensor

to_tensor() -> Any

Convert circuit to tensor representation.

Source code in src/tsim/circuit.py
660
661
662
663
664
665
def to_tensor(self) -> Any:
    """Convert circuit to tensor representation."""
    built = parse_stim_circuit(self._stim_circ)
    g = built.graph.copy()
    g.normalize()
    return g.to_tensor()

without_annotations

without_annotations() -> Circuit

Return a copy of the circuit with all detector and observable annotations removed.

Source code in src/tsim/circuit.py
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
def without_annotations(self) -> Circuit:
    """Return a copy of the circuit with all detector and observable annotations removed."""

    def strip(circuit: stim.Circuit) -> stim.Circuit:
        result = stim.Circuit()
        for instr in circuit:
            if isinstance(instr, stim.CircuitRepeatBlock):
                stripped = strip(instr.body_copy())
                result.append(stim.CircuitRepeatBlock(instr.repeat_count, stripped))
                continue
            if instr.name in ["OBSERVABLE_INCLUDE", "DETECTOR"]:
                continue
            result.append(instr)
        return result

    return Circuit.from_stim_program(strip(self._stim_circ))

without_noise

without_noise() -> Circuit

Return a copy of the circuit with all noise removed.

Source code in src/tsim/circuit.py
445
446
447
def without_noise(self) -> Circuit:
    """Return a copy of the circuit with all noise removed."""
    return Circuit.from_stim_program(self._stim_circ.without_noise())