ticit 0.2.3

Fast simulation of near-Clifford quantum circuits.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
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
545
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
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
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
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
# ticit v0.1 Python API Reference

The `ticit` package performs exact batch sampling of noisy, adaptive,
Clifford-dominated quantum circuits:

```python
import ticit

compiled_sampler = ticit.Circuit("""
    H 0
    M 0
    OBSERVABLE_INCLUDE(0) rec[-1]
""").compile()
result = compiled_sampler.sample(shots=10_000, seed=42)
print(result.logical_error_rate)
```

Like Clifft, [`SampleResult`](#ticitsampleresult) contains NumPy arrays for
per-shot measurements, detectors, observables, and expectation values. It also
includes aggregate postselection and logical-error counters.

## Index

- [`ticit.Circuit`]#ticitcircuit
  - [`ticit.Circuit.__init__`]#ticitcircuit__init__
  - [`ticit.Circuit.from_text`]#ticitcircuitfrom_text
  - [`ticit.Circuit.from_file`]#ticitcircuitfrom_file
  - [`ticit.Circuit.compile`]#ticitcircuitcompile
  - [`ticit.Circuit.num_qubits`]#ticitcircuitnum_qubits
  - [`ticit.Circuit.num_measurements`]#ticitcircuitnum_measurements
  - [`ticit.Circuit.num_detectors`]#ticitcircuitnum_detectors
  - [`ticit.Circuit.num_observables`]#ticitcircuitnum_observables
  - [`ticit.Circuit.num_exp_vals`]#ticitcircuitnum_exp_vals
- [`ticit.Program`]#ticitprogram
  - [`ticit.Program.sample`]#ticitprogramsample
  - [`ticit.Program.backend`]#ticitprogrambackend
  - [`ticit.Program.num_qubits`]#ticitprogramnum_qubits
  - [`ticit.Program.num_measurements`]#ticitprogramnum_measurements
  - [`ticit.Program.num_detectors`]#ticitprogramnum_detectors
  - [`ticit.Program.num_observables`]#ticitprogramnum_observables
  - [`ticit.Program.num_exp_vals`]#ticitprogramnum_exp_vals
  - [`ticit.Program.observable`]#ticitprogramobservable
  - [`ticit.Program.has_postselection`]#ticitprogramhas_postselection
- [`ticit.SampleResult`]#ticitsampleresult
  - [record arrays]#record-arrays
  - [count fields]#count-fields
  - [rate properties]#rate-properties
  - [timing fields]#timing-fields
- [`ticit.PauliString`]#ticitpaulistring
- [Pauli constructor functions]#pauli-constructor-functions
- [`ticit.MeasureResult`]#ticitmeasureresult
- [`ticit.TableauSimulator`]#ticittableausimulator
- [`ticit.SimulatorError`]#ticitsimulatorerror
- [`ticit.ParseError`]#ticitparseerror
- [`ticit.parse`]#ticitparse
- [`ticit.parse_file`]#ticitparse_file
- [`ticit.compile`]#ticitcompile
- [`ticit.sample`]#ticitsample
- [`ticit.sample_survivors`]#ticitsample_survivors
- [GPU backend]#gpu-backend

## Installation

From the repository, build the mixed Rust/Python package with Maturin:

```sh
cd ticit_py
maturin develop
```

Python 3.10 or newer is required. The wheel uses PyO3's `abi3-py310` stable
ABI. To include CUDA support, build with the `gpu` Cargo feature:

```sh
cd ticit_py
maturin develop --features gpu
```

The checked-in type stub is generated from the PyO3 declarations:

```sh
cargo run -p ticit_py --bin stub_gen
```

## `ticit.Circuit`

```python
class ticit.Circuit(stim_text: str = "")
```

A parsed, flattened circuit. Lowering and planning happen when `compile()` is
called.

### `ticit.Circuit.__init__`

```python
def __init__(self, stim_text: str = "") -> None
```

Parses circuit source. An empty string creates an empty circuit.

```python
import ticit

circuit = ticit.Circuit("H 0\nM 0\nDETECTOR rec[-1]")
assert circuit.num_qubits == 1
assert circuit.num_measurements == 1
assert circuit.num_detectors == 1
```

Raises:

- [`ticit.ParseError`]#ticitparseerror: the input is malformed or cannot be
  lowered.
- `ValueError`: the input uses a valid but unsupported operation.

### `ticit.Circuit.from_text`

```python
@staticmethod
def from_text(stim_text: str) -> ticit.Circuit
```

Equivalent to `ticit.Circuit(stim_text)`.

### `ticit.Circuit.from_file`

```python
@staticmethod
def from_file(path: str) -> ticit.Circuit
```

Reads a UTF-8 circuit file and returns its parsed circuit. Raises `OSError` if
the file cannot be read and [`ticit.ParseError`](#ticitparseerror) if its
contents are invalid.

### `ticit.Circuit.num_qubits`

```python
property num_qubits: int
```

Number of qubits named by the circuit.

### `ticit.Circuit.num_measurements`

```python
property num_measurements: int
```

Number of measurement results written to the measurement record.

### `ticit.Circuit.num_detectors`

```python
property num_detectors: int
```

Number of `DETECTOR` and `DISCARD` declarations.

### `ticit.Circuit.num_observables`

```python
property num_observables: int
```

One more than the largest `OBSERVABLE_INCLUDE` index, or zero when the circuit
has no observable declarations.

### `ticit.Circuit.num_exp_vals`

```python
property num_exp_vals: int
```

Number of expectation values produced by `EXP_VAL` instructions.

### `ticit.Circuit.compile`

```python
def circuit.compile(
    postselection_mask: Sequence[int] | None = None,
    *,
    normalize_syndromes: bool = False,
    expected_detectors: Sequence[int] | None = None,
    expected_observables: Sequence[int] | None = None,
    pin_measurements: Sequence[tuple[Sequence[int], bool]] | None = None,
    backend: str = "cpu",
    observable: int = 0,
    threads: int = 1,
    sample_chunk_shots: int = 0,
    batch_size: int = 0,
    gpu_chunk_shots: int = 1_048_576,
) -> ticit.Program
```

Compiles the parsed circuit into a reusable sampler. `postselection_mask`
contains one zero/nonzero flag per detector. `normalize_syndromes=True`
computes a noiseless reference sample on the CPU during preparation and XORs
detector and observable outcomes against it. Explicit `expected_detectors` and
`expected_observables` may be supplied instead, but cannot be combined with
`normalize_syndromes`. Both CPU and GPU sampling use the prepared vectors.

### Pinned measurement parities

`pin_measurements` takes `(records, value)` pairs, each requiring that the XOR
of those measurement records is `value` in the **noiseless** circuit, in every
shot. Use it when a circuit is one compiled path of an adaptive program and the
path is only valid for one outcome of a logical measurement: without it, half
the shots per branch land on the wrong path and are discarded.

A parity the circuit leaves free is pinned by forcing the last measurement
branch it depends on. A parity the circuit already determines must already
equal `value`, or compilation raises `ValueError`.

Pins act on measurement *branches*, so noise still flips the recorded parity
and a decoder still sees the errors it must correct; what becomes deterministic
is the parity the noiseless circuit would have produced. Each pinned shot is
conditioned on a probability-one-half branch outcome, so sampling every
combination of the pinned parities with equal shots reproduces the
unconditioned distribution exactly. Sampling raises `ValueError` if a pinned
branch turns out not to be a fair coin, because such a shot would carry a
weight this API does not report.

The reference sample `normalize_syndromes=True` computes obeys the same pins,
so a pinned observable normalizes against the value the pinned shots share.

Pinning requires `backend="cpu"`.

### `ticit.Circuit.reference_sample`

```python
def circuit.reference_sample() -> ticit.ReferenceSample
```

Returns the full noiseless detector and observable parity vectors. This is the
same CPU reference used by `normalize_syndromes=True`. Where the circuit leaves
a measurement free, any outcome gives a valid noiseless sample and this picks
one; `pin_measurements` is what constrains that choice.

## `ticit.ReferenceSample`

An immutable result with `detectors: list[bool]` and
`observables: list[bool]` properties.

## `ticit.Program`

A circuit prepared for repeated calls to
[`Program.sample`](#ticitprogramsample). Programs are created by
[`Circuit.compile`](#ticitcircuitcompile); `ticit.Program()` has no public
constructor.

CPU programs retain their planned program, expression plan, worker states, and
buffers between calls. GPU programs retain the parsed circuit, backend
configuration, and CPU-produced reference vectors; current GPU planning and
device allocation occur in each sample call.

### `ticit.Program.backend`

```python
property backend: str
```

Either `"cpu"` or `"gpu"`.

### `ticit.Program.num_qubits`

```python
property num_qubits: int
```

Number of circuit qubits.

### `ticit.Program.num_measurements`

```python
property num_measurements: int
```

Number of circuit measurement records.

### `ticit.Program.num_detectors`

```python
property num_detectors: int
```

Number of circuit detectors.

### `ticit.Program.num_observables`

```python
property num_observables: int
```

Number of circuit observable indices.

### `ticit.Program.num_exp_vals`

```python
property num_exp_vals: int
```

Number of expectation values in each result row.

### `ticit.Program.observable`

```python
property observable: int
```

Observable index whose accepted one outcomes are counted by
`SampleResult.logical_errors`.

### `ticit.Program.has_postselection`

```python
property has_postselection: bool
```

Whether the compiled program rejects shots using at least one detector.

### `ticit.Program.sample`

```python
def program.sample(
    shots: int,
    seed: int | None = None,
    *,
    bit_packed: bool = False,
) -> ticit.SampleResult
```

Samples the compiled circuit. `shots` must be positive. `seed=None` chooses
fresh OS-provided entropy; an integer makes the result reproducible. Calls
release the Python GIL and are serialized around the program's reusable worker
buffers.

`bit_packed=True` returns the three bit arrays with shape
`(rows, ceil(num_bits / 8))`. Bit `k` is stored in byte `k // 8` at
`1 << (k % 8)`, equivalent to `numpy.packbits(..., axis=1, bitorder="little")`.

For postselected programs, the record arrays contain one row per surviving
shot.

## `ticit.SampleResult`

Per-shot records, aggregate counters, and timing from one sampling call.
Instances are immutable and are returned by
[`Program.sample`](#ticitprogramsample), [`ticit.sample`](#ticitsample), and
[`ticit.sample_survivors`](#ticitsample_survivors).

### Record arrays

```python
property measurements: numpy.ndarray  # uint8, (rows, output measurement bytes)
property detectors: numpy.ndarray     # uint8, (rows, output detector bytes)
property observables: numpy.ndarray   # uint8, (rows, output observable bytes)
property exp_vals: numpy.ndarray      # float64, (rows, program.num_exp_vals)
property bit_packed: bool
```

[`Program.sample`](#ticitprogramsample) returns `passed_shots` rows. The three
bit arrays can also be tuple-unpacked as
`measurements, detectors, observables = result`, matching Clifft. Without
packing, each bit occupies one byte. `sample_survivors` retains rows by
default; passing `keep_records=False` explicitly returns zero-row arrays with
the requested packed or unpacked column count.

### Count fields

```python
property total_shots: int
property shots: int
property discards: int
property discarded: int
property passed_shots: int
property accepted: int
property logical_errors: int
property observable_ones: numpy.ndarray  # uint64, (program.num_observables,)
property observable: int
```

The aliases follow both Clifft and ticit terminology:

- `total_shots == shots` is the number of attempted shots.
- `discards == discarded` is the number rejected by detector postselection.
- `passed_shots == accepted` is the number retained.
- `shots == discarded + accepted` always holds.
- `logical_errors` counts accepted shots where the selected observable is one.
- `observable_ones[i]` counts accepted rows where observable `i` is one.
- `observable` identifies that selected observable index.

```python
result = ticit.Circuit("M 0").compile().sample(shots=100, seed=1)
assert result.total_shots == 100
assert result.passed_shots == 100
assert result.discards == 0
```

### Rate properties

```python
property discard_rate: float
property logical_error_rate: float
```

`discard_rate` is `discarded / shots`. `logical_error_rate` is
`logical_errors / accepted`. A zero denominator produces `nan`.

### Timing fields

```python
property compile_s: float
property presample_s: float
property execute_s: float
property sample_s: float
property active_threads: int
```

- `compile_s`: CPU circuit-planning time, or GPU planning/setup/JIT warmup time.
- `presample_s`: exogenous-noise generation and expression evaluation.
- `execute_s`: factored circuit execution and result reduction.
- `sample_s`: wall-clock steady-state sampling time.
- `active_threads`: CPU workers that received work; one for GPU sampling.

With multiple CPU workers, `presample_s` and `execute_s` are sums of worker
time and can exceed `sample_s`.

## `ticit.PauliString`

```python
class ticit.PauliString(nqubits: int = 0)
```

A packed Pauli operator. The represented operator is
`i**phase_exponent * product(X**x * Z**z)`. Constructing by qubit count creates
identity; [`ticit.pauli_string`](#pauli-constructor-functions) parses a dense
literal.

```python
p = ticit.pauli_string("IXYZ")
assert p.nqubits == 4
assert str(p) == "IXYZ"
assert str(-p) == "-IXYZ"
```

Read-only properties:

```python
property nqubits: int
property x: list[int]
property z: list[int]
property phase_exponent: int
```

`x` and `z` are copies of the packed LSB-first 64-bit words. Qubit `q` is bit
`q & 63` of word `q >> 6`.

Methods:

```python
@staticmethod
def from_text(text: str) -> ticit.PauliString

def xbit(self, q: int) -> bool
def zbit(self, q: int) -> bool
def set_xbit(self, q: int, value: bool) -> None
def set_zbit(self, q: int, value: bool) -> None
def set_phase(self, phase_exponent: int) -> None
def phase_shift(self, delta: int) -> None
def has_nonidentity_body(self) -> bool
def same_body(self, other: ticit.PauliString) -> bool
```

Out-of-range qubit indices raise `ValueError`. `set_phase` and `phase_shift`
reduce their inputs modulo four. `same_body` ignores phase.

Operators:

- `str(p)` renders the coefficient and dense body.
- `p * q` performs Pauli multiplication; operands must have equal widths.
- `-p` returns a copy multiplied by -1.
- `p == q` compares width, phase, and packed body structurally.

### Pauli constructor functions

```python
def ticit.pauli_identity(nqubits: int) -> ticit.PauliString
def ticit.pauli_x(nqubits: int, q: int) -> ticit.PauliString
def ticit.pauli_y(nqubits: int, q: int) -> ticit.PauliString
def ticit.pauli_z(nqubits: int, q: int) -> ticit.PauliString
def ticit.pauli_string(text: str) -> ticit.PauliString
def ticit.neg(pauli: ticit.PauliString) -> ticit.PauliString
```

`pauli_string` accepts `I`, `X`, `Y`, and `Z` case-insensitively; `_` aliases
identity. String position is the qubit index. The single-axis constructors
raise `ValueError` when `q >= nqubits`.

## `ticit.MeasureResult`

An immutable result returned by tableau-simulator measurements.

```python
property outcome: bool
property probability: float
property deterministic: bool
```

`outcome=False` represents eigenvalue +1 and `outcome=True` represents -1.
`probability` is the pre-projection branch probability. `deterministic` is true
when the state forced the outcome.

## `ticit.TableauSimulator`

```python
class ticit.TableauSimulator(num_qubits: int, seed: int | None = None)
```

A procedural Clifford+T simulator in Stim's `TableauSimulator` style. It starts
in `|0...0>`. Writing a missing qubit grows the register; read-only `peek_*`
operations reject missing qubits. `seed=None` uses OS entropy.

```python
sim = ticit.TableauSimulator(2, seed=7)
sim.h(0)
sim.cx(0, 1)
assert sim.peek_observable_expectation(ticit.pauli_string("XX")) == 1
assert sim.measure(0).outcome == sim.measure(1).outcome
```

State and RNG:

```python
property num_qubits: int
property rank: int
def reseed_rng(self, seed: int) -> None
def restore_rng_from(self, snapshot: ticit.TableauSimulator) -> None
```

Single-qubit Clifford gates:

```python
def h(self, q: int) -> None
def s(self, q: int) -> None
def s_dag(self, q: int) -> None
def x(self, q: int) -> None
def y(self, q: int) -> None
def z(self, q: int) -> None
def sqrt_x(self, q: int) -> None
def sqrt_x_dag(self, q: int) -> None
def sqrt_y(self, q: int) -> None
def sqrt_y_dag(self, q: int) -> None
def c_xyz(self, q: int) -> None
def c_zyx(self, q: int) -> None
def h_xy(self, q: int) -> None
def h_yz(self, q: int) -> None
```

Two-qubit Clifford gates:

```python
def cx(self, control: int, target: int) -> None
def cnot(self, control: int, target: int) -> None
def cy(self, control: int, target: int) -> None
def cz(self, a: int, b: int) -> None
def swap(self, a: int, b: int) -> None
def iswap(self, a: int, b: int) -> None
def iswap_dag(self, a: int, b: int) -> None
def xcx(self, control: int, target: int) -> None
def xcy(self, control: int, target: int) -> None
def xcz(self, control: int, target: int) -> None
def ycx(self, control: int, target: int) -> None
def ycy(self, control: int, target: int) -> None
def ycz(self, control: int, target: int) -> None
def zcx(self, control: int, target: int) -> None
def zcy(self, control: int, target: int) -> None
def zcz(self, a: int, b: int) -> None
```

Repeated operands raise `ValueError`. `cx`, `cnot`, and `zcx` are aliases;
`cz` and `zcz` are aliases; `cy` and `zcy` are aliases.

Pauli and non-Clifford operations:

```python
def pauli(self, pauli: ticit.PauliString) -> None
def controlled_pauli(
    self,
    control: ticit.PauliString,
    target: ticit.PauliString,
) -> None
def t(self, q: int) -> None
def t_dag(self, q: int) -> None
def t_pauli(self, axis: ticit.PauliString, adjoint: bool) -> None
def ccz(self, a: int, b: int, c: int) -> None
```

Controlled Pauli axes must be positive, Hermitian, and commuting. T rotation
axes must be Hermitian. Argument failures raise `ValueError`; exponential rank
growth beyond the engine cap raises
[`ticit.SimulatorError`](#ticitsimulatorerror).

Measurements and postselection:

```python
def measure(self, q: int) -> ticit.MeasureResult
def measure_observable(self, observable: ticit.PauliString) -> ticit.MeasureResult
def postselect_observable(
    self,
    observable: ticit.PauliString,
    desired_value: bool,
) -> ticit.MeasureResult
def postselect_x(self, q: int, desired_value: bool) -> ticit.MeasureResult
def postselect_y(self, q: int, desired_value: bool) -> ticit.MeasureResult
def postselect_z(self, q: int, desired_value: bool) -> ticit.MeasureResult
```

Forcing an outcome with zero probability raises
[`ticit.SimulatorError`](#ticitsimulatorerror) and leaves the state unchanged.

Non-collapsing expectations and resets:

```python
def peek_observable_expectation(self, observable: ticit.PauliString) -> float
def peek_x(self, q: int) -> float
def peek_y(self, q: int) -> float
def peek_z(self, q: int) -> float
def reset(self, q: int) -> None
def reset_x(self, q: int) -> None
def reset_y(self, q: int) -> None
def reset_z(self, q: int) -> None
```

`reset` and `reset_z` prepare `|0>`; `reset_x` prepares `|+>`; `reset_y`
prepares `|+i>`.

Dense inspection:

```python
def state_vector(self) -> list[complex]
```

Reconstructs a length-`2**num_qubits` state vector. This is intended only for
tests and small registers because both time and memory are exponential.

## `ticit.SimulatorError`

```python
class ticit.SimulatorError(RuntimeError)
```

Raised for live-state failures such as rank overflow, impossible
postselection, or pruning that would erase the state. Invalid axes, repeated
qubits, and out-of-range read-only access instead raise `ValueError`.

## `ticit.ParseError`

```python
class ticit.ParseError(ValueError)
```

Raised when circuit source is malformed or fails during lowering.

## `ticit.parse`

```python
def ticit.parse(text: str) -> ticit.Circuit
```

Parses source text without preparing a sampler.

```python
circuit = ticit.parse("M 0")
assert circuit.num_measurements == 1
```

## `ticit.parse_file`

```python
def ticit.parse_file(path: str) -> ticit.Circuit
```

Parses a UTF-8 circuit file. This is equivalent to
`ticit.Circuit.from_file(path)`.

## `ticit.compile`

```python
def ticit.compile(
    stim_text: str,
    postselection_mask: Sequence[int] | None = None,
    expected_detectors: Sequence[int] | None = None,
    expected_observables: Sequence[int] | None = None,
    normalize_syndromes: bool = False,
    *,
    pin_measurements: Sequence[tuple[Sequence[int], bool]] | None = None,
    backend: str = "cpu",
    observable: int = 0,
    threads: int = 1,
    sample_chunk_shots: int = 0,
    batch_size: int = 0,
    gpu_chunk_shots: int = 1_048_576,
) -> ticit.Program
```

Compatibility wrapper that parses and prepares a circuit. New code should use
[`Circuit.compile`](#ticitcircuitcompile). The positional parameters mirror
Clifft's `compile` function so existing call sites need minimal changes.

Arguments:

- `stim_text`: circuit in ticit's Stim-style text format.
- `postselection_mask`: one zero/nonzero flag per detector. Nonzero flags reject
  a shot when that detector parity is one. Source `DISCARD` declarations are
  unioned with this mask.
- `expected_detectors`: explicit detector reference bits, one per detector.
- `expected_observables`: explicit observable reference bits, one per index.
- `normalize_syndromes`: compute a noiseless reference on the CPU during
  preparation. Mutually exclusive with explicit reference vectors.
- `pin_measurements`: `(records, value)` parities every shot's noiseless circuit
  must produce; see
  [Pinned measurement parities]#pinned-measurement-parities.
- `backend`: `"cpu"` or `"gpu"`.
- `observable`: observable index counted as a logical error.
- `threads`: maximum CPU worker count; must be positive.
- `sample_chunk_shots`: CPU shots assigned to one scheduling chunk. Zero uses
  ticit's automatic value.
- `batch_size`: CPU shots executed together in one bit-packed batch. Zero uses
  a value based on peak active width.
- `gpu_chunk_shots`: maximum shots allocated in one GPU launch group; must be
  positive when `backend="gpu"`.

Returns:

- A reusable [`ticit.Program`]#ticitprogram.

Raises:

- [`ticit.ParseError`]#ticitparseerror: malformed circuit source.
- `ValueError`: invalid options, reference length, mask length, or backend name.
- `RuntimeError`: `backend="gpu"` was requested from a CPU-only build.

```python
program = ticit.compile(
    "H 0\nM 0\nOBSERVABLE_INCLUDE(0) rec[-1]",
    backend="cpu",
    threads=2,
)
assert program.observable == 0
```

## `ticit.sample`

```python
def ticit.sample(
    program: ticit.Program,
    shots: int,
    seed: int | None = None,
    *,
    bit_packed: bool = False,
) -> ticit.SampleResult
```

Compatibility wrapper for [`Program.sample`](#ticitprogramsample). Postselected
programs return survivor rows.

```python
program = ticit.Circuit("M 0").compile()
a = program.sample(shots=64, seed=123)
b = program.sample(shots=64, seed=123)
assert (a.discards, a.logical_errors) == (b.discards, b.logical_errors)
```

## `ticit.sample_survivors`

```python
def ticit.sample_survivors(
    program: ticit.Program,
    shots: int,
    seed: int | None = None,
    keep_records: bool = True,
    *,
    bit_packed: bool = False,
) -> ticit.SampleResult
```

A Clifft-compatible name for postselected sampling. Its counters are identical
to `ticit.sample(program, shots, seed)`. It returns survivor rows by default;
`keep_records=False` is the explicit aggregate-only mode and avoids record
materialization.
`bit_packed` has the same meaning as on
[`Program.sample`](#ticitprogramsample).

## GPU backend

GPU selection is a compile option, not a separate Python module:

```python
program = ticit.Circuit(
    "H 0\nM 0\nOBSERVABLE_INCLUDE(0) rec[-1]"
).compile(backend="gpu", gpu_chunk_shots=1_048_576)
result = ticit.sample_survivors(program, shots=1_000_000, seed=42)
```

Requirements and current limits:

- Build `ticit_py` with Cargo feature `gpu`.
- A working CUDA environment supported by ticit's `cutile` backend is required.
- GPU detector postselection uses the same per-detector `postselection_mask`
  as the CPU and Clifft APIs.
- GPU sampling retains measurement, detector, observable, and expectation rows
  by default, just like CPU sampling. Set `keep_records=False` explicitly when
  only aggregate counters are wanted.
- GPU planning, device allocation, and one-time cuTile JIT warmup currently
  occur during the sampling call and are reported in `compile_s`.