Skip to content

Tsim API

The public API, which follows semantic versioning. Other modules are internal and may change without notice.

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
168
169
170
171
172
173
174
175
176
177
178
179
180
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
418
419
420
421
422
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
404
405
406
407
408
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
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
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
410
411
412
413
414
415
416
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
424
425
426
427
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
400
401
402
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
429
430
431
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
392
393
394
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
433
434
435
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
396
397
398
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.

Besides Stim's gates, this accepts Tsim's gates:

  • T, T_DAG on qubits, and TPP, TPP_DAG on Pauli products.
  • R_X, R_Y, R_Z and U3 on qubits, R_XX, R_YY and R_ZZ on two distinct qubits, and R_PAULI on Pauli products. U3 takes three angles (theta, phi, lambda) as arg, the others one. Angles are in units of π.
  • CCZ and CCX on bare qubit targets, in groups of three. They are appended as their Clifford+T decomposition.

The other Tsim gates are stored as tagged Clifford placeholders.

Gate names are case-insensitive.

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, stim.CircuitRepeatBlock or stim.Circuit, which results in the instruction, block or circuit being appended to the circuit. The other arguments (targets, arg and tag) 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_sweep_bit, 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, PAULI_CHANNEL_1 takes three disjoint probabilities, and R_X takes an angle in units of π.

None
tag str

A customizable string attached to the instruction.

''
Source code in src/tsim/circuit.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
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
321
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
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.

    Besides Stim's gates, this accepts Tsim's gates:

    - ``T``, ``T_DAG`` on qubits, and ``TPP``, ``TPP_DAG`` on Pauli products.
    - ``R_X``, ``R_Y``, ``R_Z`` and ``U3`` on qubits, ``R_XX``, ``R_YY`` and
      ``R_ZZ`` on two distinct qubits, and ``R_PAULI`` on Pauli products.
      ``U3`` takes three angles ``(theta, phi, lambda)`` as ``arg``, the
      others one. Angles are in units of π.
    - ``CCZ`` and ``CCX`` on bare qubit targets, in groups of three. They are
      appended as their Clifford+T decomposition.

    The other Tsim gates are stored as tagged Clifford placeholders.

    Gate names are case-insensitive.

    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`,
            `stim.CircuitRepeatBlock` or `stim.Circuit`, which results in the
            instruction, block or circuit being appended to the circuit. The
            other arguments (targets, arg and tag) 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_sweep_bit`, `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, PAULI_CHANNEL_1 takes three disjoint probabilities, and R_X
            takes an angle in units of π.

        tag: A customizable string attached to the instruction.

    """
    if isinstance(name, str):
        # NOTE: Stim gate names are case-insensitive, and so are Tsim's.
        name = name.upper()
        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 in _PAULI_PRODUCT_GATES:
            targets = _signed_pauli_targets(
                targets, combined=name != "OBSERVABLE_INCLUDE"
            )

        if name == "TPP":
            name = "SPP"
            tag = encode_t_tag(tag)
        elif name == "TPP_DAG":
            name = "SPP_DAG"
            tag = encode_t_tag(tag)
        elif name == "T":
            name = "S"
            tag = encode_t_tag(tag)
        elif name == "T_DAG":
            name = "S_DAG"
            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.")
            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
        elif name in ("R_XX", "R_YY", "R_ZZ"):
            alpha = _single_angle(name, arg)
            pauli = name[2]
            q0, q1 = _two_distinct_qubits(name, targets)
            targets = _pauli_product_targets([(pauli, q0), (pauli, q1)])
            tag = f"R_PAULI(theta={alpha}*pi)"
            name = "SPP"
            arg = None
        elif name == "R_PAULI":
            alpha = _single_angle(name, arg)
            tag = f"R_PAULI(theta={alpha}*pi)"
            name = "SPP"
            arg = None

        self._stim_circ.append(name=name, targets=targets, arg=arg, tag=tag)  # type: ignore
    else:
        no_targets = isinstance(targets, (tuple, list)) and not targets
        if not no_targets or arg is not None or tag:
            raise ValueError(
                "Can't specify targets, arg or tag when appending a "
                f"{type(name).__name__}."
            )
        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
197
198
199
200
201
202
203
204
205
206
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
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
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

compile_detector_sampler

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

Compile circuit into a detector sampler.

Connected components whose single output is deterministically given by one f-variable, or is a constant, 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 SamplingStrategy

"active_statevector" (the default) to simulate each non-direct component on a dense state vector over its active qubits, or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). "active_statevector" samples on the CPU, on numba.get_num_threads() threads. The stabilizer rank strategies are provisional.

'active_statevector'
seed int | None

Random seed for the sampler. If None, a random seed is drawn. A seed reproduces its samples only with the same versions of Tsim and its dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch size nor the thread count. With a stabilizer-rank strategy they also need the same explicit batch_size and the same reference sample settings, since the default batch size depends on the free memory.

None

Returns:

Type Description
CompiledDetectorSampler

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

Source code in src/tsim/circuit.py
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
def compile_detector_sampler(
    self,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
) -> CompiledDetectorSampler:
    """Compile circuit into a detector sampler.

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

    Args:
        strategy: "active_statevector" (the default) to simulate each
            non-direct component on a dense state vector over its active
            qubits, or a stabilizer rank decomposition strategy ("cat5",
            "bss", "cutting"). "active_statevector" samples on the CPU, on
            ``numba.get_num_threads()`` threads. The stabilizer rank
            strategies are provisional.
        seed: Random seed for the sampler. If None, a random seed is drawn. A seed
            reproduces its samples only with the same versions of Tsim and its
            dependencies (such as NumPy and JAX) and the same backend (CPU or GPU).
            With "active_statevector", and under any strategy for a circuit without
            non-direct components, the samples of a seed depend on neither the batch
            size nor the thread count. With a stabilizer-rank strategy they also
            need the same explicit ``batch_size`` and the same reference sample
            settings, since the default batch size depends on the free memory.

    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
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
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: SamplingStrategy = "active_statevector",
    seed: int | None = None
) -> CompiledMeasurementSampler

Compile circuit into a measurement sampler.

Parameters:

Name Type Description Default
strategy SamplingStrategy

"active_statevector" (the default) to simulate each non-direct component on a dense state vector over its active qubits, or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). "active_statevector" samples on the CPU, on numba.get_num_threads() threads. The stabilizer rank strategies are provisional.

'active_statevector'
seed int | None

Random seed for the sampler. If None, a random seed is drawn. A seed reproduces its samples only with the same versions of Tsim and its dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch size nor the thread count. With a stabilizer-rank strategy they also need the same explicit batch_size, since the default batch size depends on the free memory.

None

Returns:

Type Description
CompiledMeasurementSampler

A CompiledMeasurementSampler that can be used to sample measurements.

Source code in src/tsim/circuit.py
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
def compile_sampler(
    self,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
) -> CompiledMeasurementSampler:
    """Compile circuit into a measurement sampler.

    Args:
        strategy: "active_statevector" (the default) to simulate each
            non-direct component on a dense state vector over its active
            qubits, or a stabilizer rank decomposition strategy ("cat5",
            "bss", "cutting"). "active_statevector" samples on the CPU, on
            ``numba.get_num_threads()`` threads. The stabilizer rank
            strategies are provisional.
        seed: Random seed for the sampler. If None, a random seed is drawn. A seed
            reproduces its samples only with the same versions of Tsim and its
            dependencies (such as NumPy and JAX) and the same backend (CPU or GPU).
            With "active_statevector", and under any strategy for a circuit without
            non-direct components, the samples of a seed depend on neither the batch
            size nor the thread count. With a stabilizer-rank strategy they also
            need the same explicit ``batch_size``, since the default batch size
            depends on the free memory.

    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
618
619
620
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 | float = 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.

Warning

By default (decompose_errors=False) the model is not decomposed. Matching decoders such as PyMatching build their graph only from error mechanisms that flip at most two detectors, and silently ignore the rest, which can raise the logical error rate substantially. Set decompose_errors=True when the model is meant for a matching decoder.

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.

Decomposition keeps the logical observables as observables, as Stim does, so it requires them to be deterministic: Stim raises a ValueError otherwise.

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 | float

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
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
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
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
def detector_error_model(
    self,
    *,
    decompose_errors: bool = False,
    flatten_loops: bool = False,
    allow_gauge_detectors: bool = False,
    approximate_disjoint_errors: bool | float = 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.

    Warning:
        By default (`decompose_errors=False`) the model is not decomposed. Matching decoders such as
        PyMatching build their graph only from error mechanisms that flip at most two detectors, and silently
        ignore the rest, which can raise the logical error rate substantially. Set `decompose_errors=True`
        when the model is meant for a matching decoder.

    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.

            Decomposition keeps the logical observables as observables, as Stim does, so it requires them
            to be deterministic: Stim raises a ValueError otherwise.
        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[
        "zx",
        "zx-dets",
        "zx-meas",
        "timeline-svg",
        "timeslice-svg",
        "timeline-text",
    ] = "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['zx', 'zx-dets', 'zx-meas', 'timeline-svg', 'timeslice-svg', 'timeline-text']

The type of diagram. Available types are: "zx": A zxparam D3 drawing of the circuit's quantum/classical ZX graph. "zx-dets": A zxparam D3 drawing of the reduced ZX graph that is used to compute probabilities of detectors and observables. "zx-meas": A zxparam D3 drawing of the reduced ZX graph 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-text": An ASCII text diagram of the operations applied by the circuit over time, with Tsim's custom gates rendered under their logical names rather than the Clifford placeholders used to store them.

'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
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
763
764
765
766
767
768
769
770
771
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
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
def diagram(
    self,
    type: Literal[
        "zx",
        "zx-dets",
        "zx-meas",
        "timeline-svg",
        "timeslice-svg",
        "timeline-text",
    ] = "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:
            "zx": A zxparam D3 drawing of the circuit's quantum/classical
                ZX graph.
            "zx-dets": A zxparam D3 drawing of the reduced ZX graph that is
                used to compute probabilities of detectors and observables.
            "zx-meas": A zxparam D3 drawing of the reduced ZX graph 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-text": An ASCII text diagram of the operations
                applied by the circuit over time, with Tsim's custom
                gates rendered under their logical names rather than
                the Clifford placeholders used to store them.
        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 _REMOVED_DIAGRAM_TYPES:
        replacement = _REMOVED_DIAGRAM_TYPES[type]
        raise ValueError(
            f"Diagram type {type!r} was removed in Tsim 1.0; use {replacement!r}."
        )
    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 == "zx":
        builder = parse_stim_circuit(self._stim_circ)
        g = builder.build(closed=False, outputs="detectors")
        return draw_zx(g, builder.num_error_bits, kwargs)
    elif type in ("zx-dets", "zx-meas"):
        builder = parse_stim_circuit(self._stim_circ)
        outputs = "detectors" if type == "zx-dets" else "records"
        g = builder.build(closed=True, outputs=outputs)
        zx.full_reduce(g)
        g, _ = transform_error_basis(g, builder.num_error_bits)
        g.to_rg()
        return draw_zx(g, builder.num_error_bits, kwargs, layout="outputs")
    elif type == "timeline-text":
        from stim_timeline_text import render_timeline_text

        return render_timeline_text(str(self._stim_circ))
    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
622
623
624
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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
@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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@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() -> Graph

Construct the circuit's quantum/classical ZX graph, with qubit ports.

Error parameters have the error index as their ID. Each detector and observable is a classical XOR spider with its own classical output boundary; their ports follow the qubit outputs (detectors, then observables), so the graph's tensor is the circuit's channel together with the detector and observable bits. Circuit.to_tensor returns the channel alone.

Source code in src/tsim/circuit.py
889
890
891
892
893
894
895
896
897
898
899
900
def get_graph(self) -> Graph:
    """Construct the circuit's quantum/classical ZX graph, with qubit ports.

    Error parameters have the error index as their ID. Each detector and
    observable is a classical XOR spider with its own classical output
    boundary; their ports follow the qubit outputs (detectors, then
    observables), so the graph's tensor is the circuit's channel together
    with the detector and observable bits. ``Circuit.to_tensor`` returns the
    channel alone.
    """
    builder = parse_stim_circuit(self._stim_circ)
    return builder.build(closed=False, outputs="detectors")

get_sampling_graph

get_sampling_graph(sample_detectors: bool = False) -> Graph

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

The graph is not doubled: qubit wires are quantum and measurement records are classical spiders. It is constructed as follows:

  1. Wire starts become |0⟩ and wire ends are discarded.
  2. Add outputs, which are classical boundaries: (a) When sampling measurements (i.e. sample_detectors is False), add an output 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 an output for each detector, then for each observable.

Error parameters have the error index as their ID, so the graph's to_tensor needs a bit for every error index in its bindings (unbound parameters raise); its tensor is then the joint distribution of the outputs.

Parameters:

Name Type Description Default
sample_detectors bool

If True, sample detectors and observables instead of measurements.

False

Returns:

Type Description
Graph

A ZX graph for sampling.

Source code in src/tsim/circuit.py
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
def get_sampling_graph(self, sample_detectors: bool = False) -> Graph:
    """Get a ZX graph that can be used to compute probabilities.

    The graph is not doubled: qubit wires are quantum and measurement records
    are classical spiders. It is constructed as follows:

    1. Wire starts become ``|0⟩`` and wire ends are discarded.
    2. Add outputs, which are classical boundaries:
    (a) When sampling measurements (i.e. `sample_detectors` is False),
        add an output 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 an output for each detector, then for each observable.

    Error parameters have the error index as their ID, so the graph's
    ``to_tensor`` needs a bit for every error index in its bindings (unbound
    parameters raise); its tensor is then the joint distribution of the
    outputs.

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

    Returns:
        A ZX graph for sampling.

    """
    builder = parse_stim_circuit(self._stim_circ)
    return builder.build(
        closed=True, outputs="detectors" if sample_detectors else "records"
    )

inverse

inverse() -> Circuit

Return the inverse of the circuit.

Source code in src/tsim/circuit.py
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
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, tag=instr.tag
                    )
                )
                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
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
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 spiders whose angle is not a multiple of π/2.

Source code in src/tsim/circuit.py
884
885
886
887
def tcount(self) -> int:
    """Count the spiders whose angle is not a multiple of π/2."""
    builder = parse_stim_circuit(self._stim_circ)
    return zx.tcount(builder.build(closed=False, outputs="none"))

to_tensor

to_tensor(
    bindings: Mapping[int, int] | None = None,
) -> np.ndarray

Return the circuit's doubled tensor (its channel, U ⊗ Ū for unitaries).

Axes follow zxparam's Graph.to_tensor: outputs then inputs, by qubit, each with a copy and a conjugate axis. Only qubits that some instruction targets get axes, in increasing index order; qubits that no instruction names are left out, so H 1 gives a one-qubit tensor (add I q to keep qubit q). Error bits default to 0 (the noiseless circuit); bindings maps error indices to bits and raises ValueError for any other key. Detector and observable annotations are left out. Needs zxparam[quimb].

Source code in src/tsim/circuit.py
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
def to_tensor(self, bindings: Mapping[int, int] | None = None) -> np.ndarray:
    """Return the circuit's doubled tensor (its channel, U ⊗ Ū for unitaries).

    Axes follow zxparam's ``Graph.to_tensor``: outputs then inputs, by qubit,
    each with a copy and a conjugate axis. Only qubits that some instruction
    targets get axes, in increasing index order; qubits that no instruction
    names are left out, so ``H 1`` gives a one-qubit tensor (add ``I q`` to
    keep qubit ``q``). Error bits default to 0 (the noiseless circuit);
    ``bindings`` maps error indices to bits and raises ``ValueError`` for any
    other key. Detector and observable annotations are left out. Needs
    ``zxparam[quimb]``.
    """
    builder = parse_stim_circuit(self._stim_circ)
    values: dict[int, Bit] = {j: 0 for j in range(builder.num_error_bits)}
    for j, bit in (bindings or {}).items():
        if j not in values:
            raise ValueError(
                f"{j!r} is not an error index of this circuit, which has "
                f"{builder.num_error_bits} error bit(s)"
            )
        values[j] = 1 if bit else 0
    return builder.build(closed=False, outputs="none").to_tensor(values)

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
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
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, tag=instr.tag
                    )
                )
                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
626
627
628
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())

Samples measurement outcomes from a quantum circuit.

Follows stim.CompiledMeasurementSampler. Each measurement record of the circuit is one output column.

Parameters:

Name Type Description Default
circuit Circuit

The quantum circuit to compile.

required
strategy SamplingStrategy

"active_statevector" (the default), or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). The stabilizer rank strategies are provisional.

'active_statevector'
seed int | None

Random seed for the sampler. If None, a random seed is drawn. A seed reproduces its samples only with the same versions of Tsim and its dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch size nor the thread count. With a stabilizer-rank strategy they also need the same explicit batch_size, since the default batch size depends on the free memory.

None
Source code in src/tsim/sampler.py
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
def __init__(
    self,
    circuit: Circuit,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
):
    """Create a measurement sampler.

    Args:
        circuit: The quantum circuit to compile.
        strategy: "active_statevector" (the default), or a stabilizer rank
            decomposition strategy ("cat5", "bss", "cutting"). The
            stabilizer rank strategies are provisional.
        seed: Random seed for the sampler. If None, a random seed is drawn. A seed
            reproduces its samples only with the same versions of Tsim and its
            dependencies (such as NumPy and JAX) and the same backend (CPU or GPU).
            With "active_statevector", and under any strategy for a circuit without
            non-direct components, the samples of a seed depend on neither the batch
            size nor the thread count. With a stabilizer-rank strategy they also
            need the same explicit ``batch_size``, since the default batch size
            depends on the free memory.

    """
    super().__init__(
        circuit,
        sample_detectors=False,
        mode="sequential",
        seed=seed,
        strategy=strategy,
    )

__repr__

__repr__() -> str

Return a string representation with compilation statistics.

Source code in src/tsim/sampler.py
673
674
675
676
677
678
679
680
681
682
683
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
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def __repr__(self) -> str:
    """Return a string representation with compilation statistics."""
    n_direct = len(self._program.direct_f_indices)

    c_graphs = []
    c_params = []
    c_a_terms = []
    c_b_terms = []
    c_c_terms = []
    c_d_terms = []
    c_e_terms = []
    total_memory_bytes = 0
    num_outputs = []
    statevector_widths: list[int] = []
    statevector_actions = 0

    for component in self._program.components:
        if isinstance(component, StatevectorComponent):
            statevector_widths.append(component.width)
            statevector_actions += component.num_actions
            num_outputs.append(len(component.output_indices))
            total_memory_bytes += _nbytes(component)
            continue
        for circuit in component.compiled_scalar_graphs:
            num_outputs.append(len(component.output_indices))
            c_graphs.append(circuit.num_graphs)
            c_params.append(circuit.n_params)
            c_a_terms.append(circuit.node_phases.phases.size)
            c_b_terms.append(circuit.phase_terms.coeffs.size)
            c_c_terms.append(
                circuit.pi_products.psi_params.shape[0]
                * circuit.pi_products.psi_params.shape[1]
            )
            c_d_terms.append(
                circuit.phase_pairs.alpha.size + circuit.phase_pairs.beta.size
            )
            c_e_terms.append(
                circuit.float_terms.table.shape[0]
                * circuit.float_terms.table.shape[1]
            )

            total_memory_bytes += _nbytes(circuit)

    def _format_bytes(n: int) -> str:
        if n < 1024:
            return f"{n} B"
        if n < 1024**2:
            return f"{n / 1024:.1f} kB"
        return f"{n / (1024**2):.1f} MB"

    total_memory_str = _format_bytes(total_memory_bytes)
    error_channel_bits = sum(
        channel.num_bits for channel in self._channel_sampler.channels
    )

    # NOTE: only the statistics of the backends in use are shown.
    parts = [f"{n_direct} direct"]
    if statevector_widths:
        parts.append(
            f"{len(statevector_widths)} statevector components "
            f"(width ≤ {max(statevector_widths)}, "
            f"{statevector_actions} actions)"
        )
    if c_graphs:
        parts.append(f"{sum(c_graphs)} graphs")
    parts += [
        f"{error_channel_bits} error channel bits",
        f"{max(num_outputs, default=0)} outputs for largest cc",
    ]
    if c_graphs:
        parts += [
            f"≤ {max(c_params)} parameters",
            f"{sum(c_a_terms)} A terms",
            f"{sum(c_b_terms)} B terms",
            f"{sum(c_c_terms)} C terms",
            f"{sum(c_d_terms)} D terms",
            f"{sum(c_e_terms)} E terms",
        ]
    parts.append(total_memory_str)
    return f"{type(self).__name__}({', '.join(parts)})"

sample

sample(
    shots: int,
    *,
    batch_size: int | None = None,
    bit_packed: bool = False
) -> np.ndarray

Sample measurement outcomes from the circuit.

Parameters:

Name Type Description Default
shots int

The number of times to sample every measurement in the circuit.

required
batch_size int | None

The number of samples to process at once. Defaults to None, which chooses it from the available memory. With strategy="active_statevector" it bounds the shots a thread holds and does not change the samples. With a stabilizer-rank strategy on a GPU, setting this explicitly can help fully utilize VRAM; it changes the samples of a seed.

None
bit_packed bool

Defaults to false. When set, results are bit-packed. The packed rows may be a strided view of a wider array.

False

Returns:

Type Description
ndarray

A boolean array of shape (shots, num_measurements), or with

ndarray

bit_packed a uint8 array of shape

ndarray

(shots, ceil(num_measurements / 8)) in little-endian bit order.

Source code in src/tsim/sampler.py
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
def sample(
    self,
    shots: int,
    *,
    batch_size: int | None = None,
    bit_packed: bool = False,
) -> np.ndarray:
    """Sample measurement outcomes from the circuit.

    Args:
        shots: The number of times to sample every measurement in the circuit.
        batch_size: The number of samples to process at once. Defaults to None,
            which chooses it from the available memory. With
            ``strategy="active_statevector"`` it bounds the shots a thread
            holds and does not change the samples.
            With a stabilizer-rank strategy on a GPU, setting this explicitly
            can help fully utilize VRAM; it changes the samples of a seed.
        bit_packed: Defaults to false. When set, results are bit-packed. The
            packed rows may be a strided view of a wider array.

    Returns:
        A boolean array of shape ``(shots, num_measurements)``, or with
        ``bit_packed`` a uint8 array of shape
        ``(shots, ceil(num_measurements / 8))`` in little-endian bit order.

    """
    columns = [np.arange(self._program.num_outputs)]
    return self._sample_outputs(shots, batch_size, columns, bit_packed=bit_packed)[
        0
    ]

Samples detector and observable outcomes from a quantum circuit.

Parameters:

Name Type Description Default
circuit Circuit

The quantum circuit to compile.

required
strategy SamplingStrategy

"active_statevector" (the default), or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). The stabilizer rank strategies are provisional.

'active_statevector'
seed int | None

Random seed for the sampler. If None, a random seed is drawn. A seed reproduces its samples only with the same versions of Tsim and its dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch size nor the thread count. With a stabilizer-rank strategy they also need the same explicit batch_size and the same reference sample settings, since the default batch size depends on the free memory.

None
Source code in src/tsim/sampler.py
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
def __init__(
    self,
    circuit: Circuit,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
):
    """Create a detector sampler.

    Args:
        circuit: The quantum circuit to compile.
        strategy: "active_statevector" (the default), or a stabilizer rank
            decomposition strategy ("cat5", "bss", "cutting"). The
            stabilizer rank strategies are provisional.
        seed: Random seed for the sampler. If None, a random seed is drawn. A seed
            reproduces its samples only with the same versions of Tsim and its
            dependencies (such as NumPy and JAX) and the same backend (CPU or GPU).
            With "active_statevector", and under any strategy for a circuit without
            non-direct components, the samples of a seed depend on neither the batch
            size nor the thread count. With a stabilizer-rank strategy they also
            need the same explicit ``batch_size`` and the same reference sample
            settings, since the default batch size depends on the free memory.

    """
    super().__init__(
        circuit,
        sample_detectors=True,
        mode="sequential",
        seed=seed,
        strategy=strategy,
    )

__repr__

__repr__() -> str

Return a string representation with compilation statistics.

Source code in src/tsim/sampler.py
673
674
675
676
677
678
679
680
681
682
683
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
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def __repr__(self) -> str:
    """Return a string representation with compilation statistics."""
    n_direct = len(self._program.direct_f_indices)

    c_graphs = []
    c_params = []
    c_a_terms = []
    c_b_terms = []
    c_c_terms = []
    c_d_terms = []
    c_e_terms = []
    total_memory_bytes = 0
    num_outputs = []
    statevector_widths: list[int] = []
    statevector_actions = 0

    for component in self._program.components:
        if isinstance(component, StatevectorComponent):
            statevector_widths.append(component.width)
            statevector_actions += component.num_actions
            num_outputs.append(len(component.output_indices))
            total_memory_bytes += _nbytes(component)
            continue
        for circuit in component.compiled_scalar_graphs:
            num_outputs.append(len(component.output_indices))
            c_graphs.append(circuit.num_graphs)
            c_params.append(circuit.n_params)
            c_a_terms.append(circuit.node_phases.phases.size)
            c_b_terms.append(circuit.phase_terms.coeffs.size)
            c_c_terms.append(
                circuit.pi_products.psi_params.shape[0]
                * circuit.pi_products.psi_params.shape[1]
            )
            c_d_terms.append(
                circuit.phase_pairs.alpha.size + circuit.phase_pairs.beta.size
            )
            c_e_terms.append(
                circuit.float_terms.table.shape[0]
                * circuit.float_terms.table.shape[1]
            )

            total_memory_bytes += _nbytes(circuit)

    def _format_bytes(n: int) -> str:
        if n < 1024:
            return f"{n} B"
        if n < 1024**2:
            return f"{n / 1024:.1f} kB"
        return f"{n / (1024**2):.1f} MB"

    total_memory_str = _format_bytes(total_memory_bytes)
    error_channel_bits = sum(
        channel.num_bits for channel in self._channel_sampler.channels
    )

    # NOTE: only the statistics of the backends in use are shown.
    parts = [f"{n_direct} direct"]
    if statevector_widths:
        parts.append(
            f"{len(statevector_widths)} statevector components "
            f"(width ≤ {max(statevector_widths)}, "
            f"{statevector_actions} actions)"
        )
    if c_graphs:
        parts.append(f"{sum(c_graphs)} graphs")
    parts += [
        f"{error_channel_bits} error channel bits",
        f"{max(num_outputs, default=0)} outputs for largest cc",
    ]
    if c_graphs:
        parts += [
            f"≤ {max(c_params)} parameters",
            f"{sum(c_a_terms)} A terms",
            f"{sum(c_b_terms)} B terms",
            f"{sum(c_c_terms)} C terms",
            f"{sum(c_d_terms)} D terms",
            f"{sum(c_e_terms)} E terms",
        ]
    parts.append(total_memory_str)
    return f"{type(self).__name__}({', '.join(parts)})"

sample

sample(
    shots: int,
    *,
    batch_size: int | None = None,
    prepend_observables: bool = False,
    append_observables: bool = False,
    separate_observables: Literal[True],
    bit_packed: bool = False,
    use_detector_reference_sample: bool = False,
    use_observable_reference_sample: bool = False,
    postselection_mask: ndarray | None = None
) -> tuple[np.ndarray, np.ndarray]
sample(
    shots: int,
    *,
    batch_size: int | None = None,
    prepend_observables: bool = False,
    append_observables: bool = False,
    separate_observables: Literal[False] = False,
    bit_packed: bool = False,
    use_detector_reference_sample: bool = False,
    use_observable_reference_sample: bool = False,
    postselection_mask: ndarray | None = None
) -> np.ndarray
sample(
    shots: int,
    *,
    batch_size: int | None = None,
    prepend_observables: bool = False,
    append_observables: bool = False,
    separate_observables: bool = False,
    bit_packed: bool = False,
    use_detector_reference_sample: bool = False,
    use_observable_reference_sample: bool = False,
    postselection_mask: ndarray | None = None
) -> np.ndarray | tuple[np.ndarray, np.ndarray]

Return detector samples from the circuit.

The circuit must define the detectors using DETECTOR instructions. Observables defined by OBSERVABLE_INCLUDE instructions can also be included in the results as honorary detectors.

Parameters:

Name Type Description Default
shots int

The number of times to sample every detector in the circuit.

required
batch_size int | None

The number of samples to process at once. Defaults to None, which chooses it from the available memory. With strategy="active_statevector" it bounds the shots a thread holds and does not change the samples. With a stabilizer-rank strategy on a GPU, setting this explicitly can help fully utilize VRAM; it changes the samples of a seed.

None
separate_observables bool

Defaults to False. When set to True, the return value is a (detection_events, observable_flips) tuple instead of a flat detection_events array.

False
prepend_observables bool

Defaults to false. When set, observables are included with the detectors and are placed at the start of the results.

False
append_observables bool

Defaults to false. When set, observables are included with the detectors and are placed at the end of the results.

False
bit_packed bool

Defaults to false. When set, results are bit-packed. The packed rows may be a strided view of a wider array.

False
use_detector_reference_sample bool

Defaults to False. When True, a noiseless reference sample is computed and XORed with detector outcomes so that results represent deviations from the noiseless baseline. This should only be used when detectors are deterministic. Otherwise, it can unpredictably change the results.

False
use_observable_reference_sample bool

Defaults to False. When True, a noiseless reference sample is computed and XORed with observable outcomes so that results represent deviations from the noiseless baseline. This should only be used when observables are deterministic. Otherwise, it can unpredictably change the results.

False
postselection_mask ndarray | None

Optional boolean array of length num_detectors. When set, only the shots in which every masked detector reads 0 in the returned sample (after the reference sample is XOR-ed in, with use_detector_reference_sample) are returned, so the result can have fewer than shots rows, and len(result) / shots is the acceptance rate. Without a mask, exactly shots rows are returned. A shot that a masked direct detector (an error bit or a constant) discards skips the components; with strategy="active_statevector" a masked component detector also stops a shot's simulation, while stabilizer-rank components are computed in full.

None

Returns:

Type Description
ndarray | tuple[ndarray, ndarray]

A boolean array of shape (rows, num_columns), or with

ndarray | tuple[ndarray, ndarray]

bit_packed a uint8 array of shape (rows, ceil(num_columns / 8))

ndarray | tuple[ndarray, ndarray]

in little-endian bit order. rows is shots, or fewer with a

ndarray | tuple[ndarray, ndarray]

postselection_mask. The columns are the detectors, with the

ndarray | tuple[ndarray, ndarray]

observables prepended and/or appended if requested. With

ndarray | tuple[ndarray, ndarray]

separate_observables, a (detectors, observables) tuple of such

ndarray | tuple[ndarray, ndarray]

arrays holding the same shots.

Warns:

Type Description
UserWarning

If a masked direct detector is a constant 1, without use_detector_reference_sample, so that every shot is discarded.

Raises:

Type Description
ValueError

If separate_observables is combined with prepend_observables or append_observables.

Source code in src/tsim/sampler.py
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
def sample(
    self,
    shots: int,
    *,
    batch_size: int | None = None,
    prepend_observables: bool = False,
    append_observables: bool = False,
    separate_observables: bool = False,
    bit_packed: bool = False,
    use_detector_reference_sample: bool = False,
    use_observable_reference_sample: bool = False,
    postselection_mask: np.ndarray | None = None,
) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
    """Return detector samples from the circuit.

    The circuit must define the detectors using DETECTOR instructions. Observables
    defined by OBSERVABLE_INCLUDE instructions can also be included in the results
    as honorary detectors.

    Args:
        shots: The number of times to sample every detector in the circuit.
        batch_size: The number of samples to process at once. Defaults to None,
            which chooses it from the available memory. With
            ``strategy="active_statevector"`` it bounds the shots a thread
            holds and does not change the samples.
            With a stabilizer-rank strategy on a GPU, setting this explicitly
            can help fully utilize VRAM; it changes the samples of a seed.
        separate_observables: Defaults to False. When set to True, the return value
            is a (detection_events, observable_flips) tuple instead of a flat
            detection_events array.
        prepend_observables: Defaults to false. When set, observables are included
            with the detectors and are placed at the start of the results.
        append_observables: Defaults to false. When set, observables are included
            with the detectors and are placed at the end of the results.
        bit_packed: Defaults to false. When set, results are bit-packed. The
            packed rows may be a strided view of a wider array.
        use_detector_reference_sample: Defaults to False. When True, a noiseless
            reference sample is computed and XORed with detector outcomes so that
            results represent deviations from the noiseless baseline. This should
            only be used when detectors are deterministic. Otherwise, it can
            unpredictably change the results.
        use_observable_reference_sample: Defaults to False. When True, a noiseless
            reference sample is computed and XORed with observable outcomes so that
            results represent deviations from the noiseless baseline. This should
            only be used when observables are deterministic. Otherwise, it can
            unpredictably change the results.
        postselection_mask: Optional boolean array of length ``num_detectors``.
            When set, only the shots in which every masked detector reads 0
            in the returned sample (after the reference sample is XOR-ed
            in, with ``use_detector_reference_sample``) are returned, so the
            result can have fewer than ``shots`` rows, and
            ``len(result) / shots`` is the acceptance rate. Without a mask,
            exactly ``shots`` rows are returned. A shot that a masked direct
            detector (an error bit or a constant) discards skips the
            components; with ``strategy="active_statevector"`` a masked
            component detector also stops a shot's simulation, while
            stabilizer-rank components are computed in full.

    Returns:
        A boolean array of shape ``(rows, num_columns)``, or with
        ``bit_packed`` a uint8 array of shape ``(rows, ceil(num_columns / 8))``
        in little-endian bit order. ``rows`` is ``shots``, or fewer with a
        ``postselection_mask``. The columns are the detectors, with the
        observables prepended and/or appended if requested. With
        ``separate_observables``, a ``(detectors, observables)`` tuple of such
        arrays holding the same shots.

    Warns:
        UserWarning: If a masked direct detector is a constant 1, without
            ``use_detector_reference_sample``, so that every shot is
            discarded.

    Raises:
        ValueError: If ``separate_observables`` is combined with
            ``prepend_observables`` or ``append_observables``.

    """
    if separate_observables and (prepend_observables or append_observables):
        raise ValueError(
            "Can't specify separate_observables=True with "
            "append_observables=True or prepend_observables=True"
        )

    num_detectors = self._num_detectors
    mask = None
    if postselection_mask is not None:
        mask = np.asarray(postselection_mask, dtype=np.bool_)
        if mask.shape != (num_detectors,):
            raise ValueError(
                f"postselection_mask must have shape ({num_detectors},), "
                f"got {mask.shape}"
            )
        if not use_detector_reference_sample:
            indices, flips, direct_columns = self._direct_detectors
            always = direct_columns[(indices < 0) & flips]
            always = always[mask[always]]
            if len(always):
                warnings.warn(
                    f"The postselected detectors {always.tolist()} always "
                    "read 1, so every shot is discarded. Did you mean "
                    "use_detector_reference_sample=True?",
                    stacklevel=2,
                )
    num_outputs = self._program.num_outputs
    referenced = None
    if use_detector_reference_sample or use_observable_reference_sample:
        referenced = np.where(
            np.arange(num_outputs) < num_detectors,
            use_detector_reference_sample,
            use_observable_reference_sample,
        )
    columns = _output_columns(
        num_detectors,
        num_outputs,
        prepend=prepend_observables,
        append=append_observables,
        separate=separate_observables,
    )
    arrays = self._sample_outputs(
        shots,
        batch_size,
        columns,
        bit_packed=bit_packed,
        mask=mask,
        referenced=referenced,
    )
    return (arrays[0], arrays[1]) if separate_observables else arrays[0]