Skip to content

sampler

Compiled samplers for measurements and detectors.

CompiledDetectorSampler

CompiledDetectorSampler(
    circuit: Circuit,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
    trajectory_sharing: bool | None = None
)

Bases: _CompiledSamplerBase


              flowchart TD
              tsim.sampler.CompiledDetectorSampler[CompiledDetectorSampler]
              tsim.sampler._CompiledSamplerBase[_CompiledSamplerBase]

                              tsim.sampler._CompiledSamplerBase --> tsim.sampler.CompiledDetectorSampler
                


              click tsim.sampler.CompiledDetectorSampler href "" "tsim.sampler.CompiledDetectorSampler"
              click tsim.sampler._CompiledSamplerBase href "" "tsim.sampler._CompiledSamplerBase"
            

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").

'active_statevector'
trajectory_sharing bool | None

With strategy="active_statevector", simulate shots with equal histories on one lane. Defaults to True for "active_statevector" and False otherwise.

None
seed int | None

Random seed for the sampler. With "active_statevector" the samples of a seed do not depend on the batch size; with a stabilizer-rank strategy they are only deterministic for a fixed batch size and fixed reference sample settings.

None
Source code in src/tsim/sampler.py
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
def __init__(
    self,
    circuit: Circuit,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
    trajectory_sharing: bool | 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").
        trajectory_sharing: With ``strategy="active_statevector"``, simulate shots
            with equal histories on one lane. Defaults to True for
            ``"active_statevector"`` and False otherwise.
        seed: Random seed for the sampler. With "active_statevector" the samples
            of a seed do not depend on the batch size; with a stabilizer-rank
            strategy they are only deterministic for a fixed batch size and
            fixed reference sample settings.

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

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, shots where any masked direct detector (an error bit or a constant) fires are not sampled further. All shots rows are still returned: survivors contain the full sample, while discarded rows retain direct detector columns and fill component columns with False; with strategy="active_statevector", a shot discarded by a masked component detector also reads False in the outputs set after it. Re-apply the mask to the returned detector columns to recover surviving shots.

None

Returns:

Type Description
ndarray | tuple[ndarray, ndarray]

A numpy array or tuple of numpy arrays containing the samples.

Raises:

Type Description
ValueError

If separate_observables is combined with prepend_observables or append_observables.

Source code in src/tsim/sampler.py
849
850
851
852
853
854
855
856
857
858
859
860
861
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
893
894
895
896
897
898
899
900
901
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
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
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, shots where any masked direct detector (an error bit
            or a constant) fires are not sampled further. All ``shots`` rows
            are still returned: survivors
            contain the full sample, while discarded rows retain direct detector
            columns and fill component columns with ``False``; with
            ``strategy="active_statevector"``, a shot discarded by a masked
            component detector also reads ``False`` in the outputs set after
            it. Re-apply the mask to the returned detector columns to recover
            surviving shots.

    Returns:
        A numpy array or tuple of numpy arrays containing the samples.

    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"
        )

    compute_reference = (
        use_detector_reference_sample or use_observable_reference_sample
    )
    num_detectors = self._num_detectors

    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}"
            )
        # NOTE: shot words discard and prune with any mask; with a
        # stabilizer-rank strategy only direct detectors that can fire skip work.
        if self._shots is not None:
            postselection_mask = mask if self._program.components else None
        else:
            skips = mask & self._direct_detector_mask
            indices, flips, columns = self._direct_detectors
            constant = indices < 0
            skips[columns[constant]] &= flips[constant] & (
                not use_detector_reference_sample
            )
            postselection_mask = mask if skips.any() else None

    num_outputs = self._program.num_outputs
    is_detector = np.arange(num_outputs) < num_detectors
    referenced = np.where(
        is_detector, use_detector_reference_sample, use_observable_reference_sample
    )
    columns = _output_columns(
        num_detectors,
        num_outputs,
        prepend=prepend_observables,
        append=append_observables,
        separate=separate_observables,
    )
    if self._shots is not None:
        # NOTE: the reference bits are XOR-ed in before the postselection tests.
        flip = self._reference_sample() & referenced if compute_reference else None
        if self._program.components or bit_packed:
            arrays = self._sample_shot_outputs(
                shots,
                batch_size,
                columns,
                bit_packed=bit_packed,
                mask=postselection_mask,
                flip=flip,
            )
            return (arrays[0], arrays[1]) if separate_observables else arrays[0]
        _check_sizes(shots, batch_size)
        samples = self._shots.direct_bools(shots, flip)
    elif postselection_mask is not None:
        if not compute_reference:
            referenced = None
        samples = self._sample_batches_with_postselection(
            shots,
            batch_size,
            postselection_mask=postselection_mask,
            referenced=referenced,
        )
    elif compute_reference:
        samples, reference = self._sample_batches(
            shots, batch_size, compute_reference=True
        )
        if use_detector_reference_sample:
            samples[:, :num_detectors] ^= reference[:num_detectors]
        if use_observable_reference_sample:
            samples[:, num_detectors:] ^= reference[num_detectors:]
    else:
        samples = self._sample_batches(shots, batch_size)
    arrays = [
        _maybe_bit_pack(_take_columns(samples, c), bit_packed=bit_packed)
        for c in columns
    ]
    return (arrays[0], arrays[1]) if separate_observables else arrays[0]

CompiledMeasurementSampler

CompiledMeasurementSampler(
    circuit: Circuit,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
    trajectory_sharing: bool | None = None
)

Bases: _CompiledSamplerBase


              flowchart TD
              tsim.sampler.CompiledMeasurementSampler[CompiledMeasurementSampler]
              tsim.sampler._CompiledSamplerBase[_CompiledSamplerBase]

                              tsim.sampler._CompiledSamplerBase --> tsim.sampler.CompiledMeasurementSampler
                


              click tsim.sampler.CompiledMeasurementSampler href "" "tsim.sampler.CompiledMeasurementSampler"
              click tsim.sampler._CompiledSamplerBase href "" "tsim.sampler._CompiledSamplerBase"
            

Samples measurement outcomes from a quantum circuit.

Uses sequential decomposition [0, 1, 2, ..., n] where: - compiled_scalar_graphs[0]: normalization (0 outputs plugged) - compiled_scalar_graphs[i]: cumulative probability up to bit i

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").

'active_statevector'
trajectory_sharing bool | None

With strategy="active_statevector", simulate shots with equal histories on one lane. Defaults to True for "active_statevector" and False otherwise.

None
seed int | None

Random seed for the sampler. With "active_statevector" the samples of a seed do not depend on the batch size; with a stabilizer-rank strategy they are only deterministic for a fixed batch size.

None
Source code in src/tsim/sampler.py
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
def __init__(
    self,
    circuit: Circuit,
    *,
    strategy: SamplingStrategy = "active_statevector",
    seed: int | None = None,
    trajectory_sharing: bool | 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").
        trajectory_sharing: With ``strategy="active_statevector"``, simulate shots
            with equal histories on one lane. Defaults to True for
            ``"active_statevector"`` and False otherwise.
        seed: Random seed for the sampler. With "active_statevector" the samples
            of a seed do not depend on the batch size; with a stabilizer-rank
            strategy they are only deterministic for a fixed batch size.

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

sample

sample(
    shots: int, *, batch_size: int | None = None
) -> 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

Returns:

Type Description
ndarray

A numpy array containing the measurement samples.

Source code in src/tsim/sampler.py
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
def sample(self, shots: int, *, batch_size: int | None = None) -> 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.

    Returns:
        A numpy array containing the measurement samples.

    """
    if self._shots is not None:
        columns = [np.arange(self._program.num_outputs)]
        if not self._program.components:
            _check_sizes(shots, batch_size)
            return self._shots.direct_bools(shots)
        return self._sample_shot_outputs(
            shots, batch_size, columns, bit_packed=False
        )[0]
    return self._sample_batches(shots, batch_size)

CompiledStateProbs

CompiledStateProbs(
    circuit: Circuit,
    *,
    sample_detectors: bool = False,
    strategy: SamplingStrategy = "cat5",
    seed: int | None = None
)

Bases: _CompiledSamplerBase


              flowchart TD
              tsim.sampler.CompiledStateProbs[CompiledStateProbs]
              tsim.sampler._CompiledSamplerBase[_CompiledSamplerBase]

                              tsim.sampler._CompiledSamplerBase --> tsim.sampler.CompiledStateProbs
                


              click tsim.sampler.CompiledStateProbs href "" "tsim.sampler.CompiledStateProbs"
              click tsim.sampler._CompiledSamplerBase href "" "tsim.sampler._CompiledSamplerBase"
            

Computes measurement probabilities for a given state.

Uses joint decomposition [0, n] where: - compiled_scalar_graphs[0]: normalization (0 outputs plugged) - compiled_scalar_graphs[1]: full joint probability (all outputs plugged)

Parameters:

Name Type Description Default
circuit Circuit

The quantum circuit to compile.

required
sample_detectors bool

If True, compute detector/observable probabilities.

False
strategy SamplingStrategy

Stabilizer rank decomposition strategy ("cat5", "bss", "cutting"); "active_statevector" cannot compute probabilities.

'cat5'
seed int | None

Random seed. If None, a random seed is generated. Note that deterministic results are only guaranteed for a fixed batch size.

None
Source code in src/tsim/sampler.py
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
def __init__(
    self,
    circuit: Circuit,
    *,
    sample_detectors: bool = False,
    strategy: SamplingStrategy = "cat5",
    seed: int | None = None,
):
    """Create a probability estimator.

    Args:
        circuit: The quantum circuit to compile.
        sample_detectors: If True, compute detector/observable probabilities.
        strategy: Stabilizer rank decomposition strategy ("cat5", "bss",
            "cutting"); "active_statevector" cannot compute probabilities.
        seed: Random seed. If None, a random seed is generated. Note that
            deterministic results are only guaranteed for a fixed batch size.

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

probability_of

probability_of(
    state: ndarray, *, batch_size: int
) -> np.ndarray

Compute probabilities for a batch of error samples given a measurement state.

Parameters:

Name Type Description Default
state ndarray

The measurement outcome state to compute probability for.

required
batch_size int

Number of error samples to use for estimation.

required

Returns:

Type Description
ndarray

Array of probabilities P(state | error_sample) for each error sample.

Source code in src/tsim/sampler.py
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
def probability_of(self, state: np.ndarray, *, batch_size: int) -> np.ndarray:
    """Compute probabilities for a batch of error samples given a measurement state.

    Args:
        state: The measurement outcome state to compute probability for.
        batch_size: Number of error samples to use for estimation.

    Returns:
        Array of probabilities P(state | error_sample) for each error sample.

    """
    if batch_size < 1:
        raise ValueError(f"batch_size must be at least 1, got {batch_size}")
    expected_outputs = self._program.num_outputs
    if state.shape != (expected_outputs,):
        raise ValueError(
            f"state must have shape ({expected_outputs},), got {state.shape}"
        )
    f_samples = jnp.asarray(self._channel_sampler.sample(batch_size))
    p_norm = jnp.ones(batch_size)
    p_joint = jnp.ones(batch_size)

    if len(self._program.direct_f_indices) > 0:
        direct_bits = _direct_bits(self._program, f_samples)
        n_direct = len(self._program.direct_f_indices)
        targets = state[self._program.output_order[:n_direct]]
        p_joint = p_joint * (direct_bits == targets).all(axis=1)

    for component in self._program.components:
        assert isinstance(component, CompiledComponent)
        assert len(component.compiled_scalar_graphs) == 2

        f_selected = f_samples[:, component.f_selection]

        norm_circuit, joint_circuit = component.compiled_scalar_graphs

        # Normalization: only f-params
        p_norm = p_norm * jnp.abs(evaluate(norm_circuit, f_selected))

        # Joint probability: f-params + state
        component_state = state[list(component.output_indices)]
        tiled_state = jnp.tile(component_state, (batch_size, 1))
        joint_params = jnp.hstack([f_selected, tiled_state])
        p_joint = p_joint * jnp.abs(evaluate(joint_circuit, joint_params))

    return np.asarray(p_joint / p_norm)

sample_component

sample_component(
    component: CompiledComponent,
    f_params: Array,
    key: Array,
) -> tuple[jax.Array, PRNGKey, jax.Array]

Sample outputs from a single component.

Parameters:

Name Type Description Default
component CompiledComponent

The compiled component to sample from.

required
f_params Array

Error parameters, shape (batch_size, num_f_params).

required
key Array

JAX random key.

required

Returns:

Type Description
Array

Tuple of (samples, next_key, max_norm_deviation) where samples has shape

Array

(batch_size, num_outputs_for_component).

Source code in src/tsim/sampler.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def sample_component(
    component: CompiledComponent,
    f_params: jax.Array,
    key: PRNGKey,
) -> tuple[jax.Array, PRNGKey, jax.Array]:
    """Sample outputs from a single component.

    Args:
        component: The compiled component to sample from.
        f_params: Error parameters, shape (batch_size, num_f_params).
        key: JAX random key.

    Returns:
        Tuple of (samples, next_key, max_norm_deviation) where samples has shape
        (batch_size, num_outputs_for_component).

    """
    # Skip JIT for small components (overhead not worth it)
    if len(component.output_indices) <= 1:
        return _sample_component(component, f_params, key)
    return _sample_component_jit(component, f_params, key)

sample_program

sample_program(
    program: CompiledProgram, f_params: Array, key: Array
) -> jax.Array

Sample all outputs of a program of stabilizer-rank components with JAX.

Parameters:

Name Type Description Default
program CompiledProgram

The compiled program to sample from.

required
f_params Array

Error parameters, shape (batch_size, num_f_params).

required
key Array

JAX random key.

required

Returns:

Type Description
Array

Samples array of shape (batch_size, num_outputs), reordered to

Array

match the original output indices.

Source code in src/tsim/sampler.py
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
def sample_program(
    program: CompiledProgram, f_params: jax.Array, key: PRNGKey
) -> jax.Array:
    """Sample all outputs of a program of stabilizer-rank components with JAX.

    Args:
        program: The compiled program to sample from.
        f_params: Error parameters, shape (batch_size, num_f_params).
        key: JAX random key.

    Returns:
        Samples array of shape (batch_size, num_outputs), reordered to
        match the original output indices.

    """
    if program.num_outputs == 0:
        return jnp.zeros((f_params.shape[0], 0), dtype=jnp.bool_)
    results = []
    if len(program.direct_f_indices) > 0:
        results.append(_direct_bits(program, f_params))
    for component in program.components:
        assert isinstance(component, CompiledComponent)
        samples, key, max_norm_deviation = sample_component(component, f_params, key)
        _check_norm(max_norm_deviation, component.norm_warning_threshold)
        results.append(samples)
    combined = jnp.concatenate(results, axis=1)
    if program.output_reindex is not None:
        combined = combined[:, program.output_reindex]
    return combined