From dbeec0e6cae545ed1c3cc5d4b46eaf833f28a86e Mon Sep 17 00:00:00 2001 From: Xiyuan Guo Date: Fri, 31 Jul 2026 20:18:20 -0400 Subject: [PATCH] Add stateful TableauSampler with deterministic seeding (#974) Implements a stateful C++ TableauSampler wrapped with Pybind11 to enable reproducible generation of Clifford tableaus via a seedable mt19937_64 random number generator. --- doc/stim.pyi | 67 +++++++++ file_lists/pybind_files | 1 + glue/python/src/stim/__init__.pyi | 67 +++++++++ src/stim/py/stim.pybind.cc | 3 + .../stabilizers/tableau_sampler.pybind.cc | 129 ++++++++++++++++++ src/stim/stabilizers/tableau_sampler.pybind.h | 41 ++++++ .../tableau_sampler_pybind_test.py | 61 +++++++++ 7 files changed, 369 insertions(+) create mode 100644 src/stim/stabilizers/tableau_sampler.pybind.cc create mode 100644 src/stim/stabilizers/tableau_sampler.pybind.h create mode 100644 src/stim/stabilizers/tableau_sampler_pybind_test.py diff --git a/doc/stim.pyi b/doc/stim.pyi index d71851394..b6f3e4d05 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -11970,6 +11970,73 @@ class TableauIterator: ) -> stim.Tableau: """Returns the next iterated tableau. """ +class TableauSampler: + """A tool for pseudo-random tableau sampling. + + Seeds the random number generator once at initialization, then + produces a reproducible sequence of random tableaus via repeated + calls to `next_tableau()`. + + Examples: + >>> import stim + >>> s = stim.TableauSampler(5, seed=42) + >>> t1 = s.next_tableau() + >>> t2 = s.next_tableau() + """ + def __init__( + self, + num_qubits: int, + *, + seed: int | None = None, + ) -> None: + """Creates a tableau sampler. + + Args: + num_qubits: The number of qubits each sampled tableau acts on. + seed: PARTIALLY determines the sequence of sampled tableaus by + deterministically seeding the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng is seeded from system + entropy. + + When set to an integer, making the exact same series of calls + on the exact same machine with the exact same version of Stim + will produce the exact same sequence of tableaus. + + CAUTION: the sequence produced by a specific seed *WILL NOT* + be consistent between versions of Stim. This restriction is + present to make it possible to have future optimizations to + the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version + to version. + + CAUTION: the sequence produced by a specific seed *MAY NOT* + be consistent across machines that differ in the width of + supported SIMD instructions. For example, using the same seed + on a machine that supports AVX instructions and one that only + supports SSE instructions may produce different sequences. + + Examples: + >>> import stim + >>> sampler = stim.TableauSampler(4, seed=12345) + >>> t = sampler.next_tableau() + """ + def next_tableau( + self, + ) -> stim.Tableau: + """Samples a uniformly random tableau. + + Returns: + A uniformly random `stim.Tableau` over the sampler's `num_qubits`. + + Examples: + >>> import stim + >>> sampler = stim.TableauSampler(2, seed=42) + >>> t1 = sampler.next_tableau() + >>> t2 = sampler.next_tableau() + """ class TableauSimulator: """A stabilizer circuit simulator that tracks an inverse stabilizer tableau. diff --git a/file_lists/pybind_files b/file_lists/pybind_files index a0cb48efb..7979e9462 100644 --- a/file_lists/pybind_files +++ b/file_lists/pybind_files @@ -27,3 +27,4 @@ src/stim/stabilizers/pauli_string.pybind.cc src/stim/stabilizers/pauli_string_iter.pybind.cc src/stim/stabilizers/tableau.pybind.cc src/stim/stabilizers/tableau_iter.pybind.cc +src/stim/stabilizers/tableau_sampler.pybind.cc diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index d71851394..b6f3e4d05 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -11970,6 +11970,73 @@ class TableauIterator: ) -> stim.Tableau: """Returns the next iterated tableau. """ +class TableauSampler: + """A tool for pseudo-random tableau sampling. + + Seeds the random number generator once at initialization, then + produces a reproducible sequence of random tableaus via repeated + calls to `next_tableau()`. + + Examples: + >>> import stim + >>> s = stim.TableauSampler(5, seed=42) + >>> t1 = s.next_tableau() + >>> t2 = s.next_tableau() + """ + def __init__( + self, + num_qubits: int, + *, + seed: int | None = None, + ) -> None: + """Creates a tableau sampler. + + Args: + num_qubits: The number of qubits each sampled tableau acts on. + seed: PARTIALLY determines the sequence of sampled tableaus by + deterministically seeding the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng is seeded from system + entropy. + + When set to an integer, making the exact same series of calls + on the exact same machine with the exact same version of Stim + will produce the exact same sequence of tableaus. + + CAUTION: the sequence produced by a specific seed *WILL NOT* + be consistent between versions of Stim. This restriction is + present to make it possible to have future optimizations to + the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version + to version. + + CAUTION: the sequence produced by a specific seed *MAY NOT* + be consistent across machines that differ in the width of + supported SIMD instructions. For example, using the same seed + on a machine that supports AVX instructions and one that only + supports SSE instructions may produce different sequences. + + Examples: + >>> import stim + >>> sampler = stim.TableauSampler(4, seed=12345) + >>> t = sampler.next_tableau() + """ + def next_tableau( + self, + ) -> stim.Tableau: + """Samples a uniformly random tableau. + + Returns: + A uniformly random `stim.Tableau` over the sampler's `num_qubits`. + + Examples: + >>> import stim + >>> sampler = stim.TableauSampler(2, seed=42) + >>> t1 = sampler.next_tableau() + >>> t2 = sampler.next_tableau() + """ class TableauSimulator: """A stabilizer circuit simulator that tracks an inverse stabilizer tableau. diff --git a/src/stim/py/stim.pybind.cc b/src/stim/py/stim.pybind.cc index 6fec31ab1..5932e1829 100644 --- a/src/stim/py/stim.pybind.cc +++ b/src/stim/py/stim.pybind.cc @@ -43,6 +43,7 @@ #include "stim/stabilizers/tableau.h" #include "stim/stabilizers/tableau.pybind.h" #include "stim/stabilizers/tableau_iter.pybind.h" +#include "stim/stabilizers/tableau_sampler.pybind.h" #define xstr_literal(s) str_literal(s) #define str_literal(s) #s @@ -594,6 +595,7 @@ PYBIND11_MODULE(STIM_PYBIND11_MODULE_NAME, m) { auto c_pauli_string_iter = pybind_pauli_string_iter(m); auto c_tableau = pybind_tableau(m); auto c_tableau_iter = pybind_tableau_iter(m); + auto c_tableau_sampler = pybind_tableau_sampler(m); auto c_circuit_gate_target = pybind_circuit_gate_target(m); auto c_gate_data = pybind_gate_data(m); @@ -633,6 +635,7 @@ PYBIND11_MODULE(STIM_PYBIND11_MODULE_NAME, m) { pybind_circuit_methods_extra(m, c_circuit); pybind_tableau_iter_methods(m, c_tableau_iter); + pybind_tableau_sampler_methods(m, c_tableau_sampler); pybind_dem_sampler_methods(m, c_dem_sampler); pybind_detector_error_model_instruction_methods(m, c_detector_error_model_instruction); diff --git a/src/stim/stabilizers/tableau_sampler.pybind.cc b/src/stim/stabilizers/tableau_sampler.pybind.cc new file mode 100644 index 000000000..37f9c64a5 --- /dev/null +++ b/src/stim/stabilizers/tableau_sampler.pybind.cc @@ -0,0 +1,129 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "stim/stabilizers/tableau_sampler.pybind.h" + +#include "stim/py/base.pybind.h" + +using namespace stim; +using namespace stim_pybind; + +TableauSampler::TableauSampler(size_t num_qubits, std::mt19937_64 &&rng) + : num_qubits(num_qubits), rng(std::move(rng)) { +} + +Tableau TableauSampler::next_tableau() { + return Tableau::random(num_qubits, rng); +} + +std::string TableauSampler::repr() const { + std::stringstream result; + result << "stim.TableauSampler(num_qubits="; + result << num_qubits; + result << ")"; + return result.str(); +} + +pybind11::class_ stim_pybind::pybind_tableau_sampler(pybind11::module &m) { + return pybind11::class_( + m, + "TableauSampler", + clean_doc_string(R"DOC( + A tool for pseudo-random tableau sampling. + + Seeds the random number generator once at initialization, then + produces a reproducible sequence of random tableaus via repeated + calls to `next_tableau()`. + + Examples: + >>> import stim + >>> s = stim.TableauSampler(5, seed=42) + >>> t1 = s.next_tableau() + >>> t2 = s.next_tableau() + )DOC") + .data()); +} + +TableauSampler stim_pybind::py_init_tableau_sampler(size_t num_qubits, const pybind11::object &seed) { + return TableauSampler(num_qubits, make_py_seeded_rng(seed)); +} + +void stim_pybind::pybind_tableau_sampler_methods( + pybind11::module &m, pybind11::class_ &c) { + c.def( + pybind11::init(&py_init_tableau_sampler), + pybind11::arg("num_qubits"), + pybind11::kw_only(), + pybind11::arg("seed") = pybind11::none(), + clean_doc_string(R"DOC( + Creates a tableau sampler. + + Args: + num_qubits: The number of qubits each sampled tableau acts on. + seed: PARTIALLY determines the sequence of sampled tableaus by + deterministically seeding the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng is seeded from system + entropy. + + When set to an integer, making the exact same series of calls + on the exact same machine with the exact same version of Stim + will produce the exact same sequence of tableaus. + + CAUTION: the sequence produced by a specific seed *WILL NOT* + be consistent between versions of Stim. This restriction is + present to make it possible to have future optimizations to + the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version + to version. + + CAUTION: the sequence produced by a specific seed *MAY NOT* + be consistent across machines that differ in the width of + supported SIMD instructions. For example, using the same seed + on a machine that supports AVX instructions and one that only + supports SSE instructions may produce different sequences. + + Examples: + >>> import stim + >>> sampler = stim.TableauSampler(4, seed=12345) + >>> t = sampler.next_tableau() + )DOC") + .data()); + + c.def( + "next_tableau", + [](TableauSampler &self) { + return self.next_tableau(); + }, + clean_doc_string(R"DOC( + Samples a uniformly random tableau. + + Returns: + A uniformly random `stim.Tableau` over the sampler's `num_qubits`. + + Examples: + >>> import stim + >>> sampler = stim.TableauSampler(2, seed=42) + >>> t1 = sampler.next_tableau() + >>> t2 = sampler.next_tableau() + )DOC") + .data()); + + c.def( + "__repr__", + &TableauSampler::repr, + "Returns a string representation of the `stim.TableauSampler`."); +} diff --git a/src/stim/stabilizers/tableau_sampler.pybind.h b/src/stim/stabilizers/tableau_sampler.pybind.h new file mode 100644 index 000000000..b3a15c7bc --- /dev/null +++ b/src/stim/stabilizers/tableau_sampler.pybind.h @@ -0,0 +1,41 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef _STIM_STABILIZERS_TABLEAU_SAMPLER_PYBIND_H +#define _STIM_STABILIZERS_TABLEAU_SAMPLER_PYBIND_H + +#include + +#include "stim/stabilizers/tableau.h" + +namespace stim_pybind { + +struct TableauSampler { + size_t num_qubits; + std::mt19937_64 rng; + TableauSampler() = delete; + TableauSampler(const TableauSampler &) = delete; + TableauSampler(TableauSampler &&) = default; + TableauSampler(size_t num_qubits, std::mt19937_64 &&rng); + stim::Tableau next_tableau(); + std::string repr() const; +}; + +pybind11::class_ pybind_tableau_sampler(pybind11::module &m); +void pybind_tableau_sampler_methods(pybind11::module &m, pybind11::class_ &c); +TableauSampler py_init_tableau_sampler(size_t num_qubits, const pybind11::object &seed); + +} // namespace stim_pybind + +#endif diff --git a/src/stim/stabilizers/tableau_sampler_pybind_test.py b/src/stim/stabilizers/tableau_sampler_pybind_test.py new file mode 100644 index 000000000..c2a8d073f --- /dev/null +++ b/src/stim/stabilizers/tableau_sampler_pybind_test.py @@ -0,0 +1,61 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import stim +import pytest + + +def test_tableau_sampler_basic(): + sampler = stim.TableauSampler(4, seed=12345) + t = sampler.next_tableau() + assert isinstance(t, stim.Tableau) + assert len(t) == 4 + + +def test_tableau_sampler_seed_deterministic(): + s1 = stim.TableauSampler(4, seed=42) + s2 = stim.TableauSampler(4, seed=42) + for _ in range(10): + assert s1.next_tableau() == s2.next_tableau() + + +def test_tableau_sampler_different_seeds_diverge(): + s1 = stim.TableauSampler(4, seed=42) + s2 = stim.TableauSampler(4, seed=99) + assert s1.next_tableau() != s2.next_tableau() + + +def test_tableau_sampler_successive_calls_differ(): + sampler = stim.TableauSampler(5, seed=1) + t1 = sampler.next_tableau() + t2 = sampler.next_tableau() + assert t1 != t2 + + +def test_tableau_sampler_no_seed(): + s1 = stim.TableauSampler(4) + s2 = stim.TableauSampler(4) + assert s1.next_tableau() != s2.next_tableau() + + +def test_tableau_sampler_repr(): + sampler = stim.TableauSampler(3, seed=7) + r = repr(sampler) + assert "stim.TableauSampler" in r + assert "3" in r + + +def test_tableau_sampler_num_qubits_consistent(): + sampler = stim.TableauSampler(3, seed=123) + for _ in range(5): + assert len(sampler.next_tableau()) == 3