From e08f8482a31f2ed8f72872745eaf20efa075d9ad Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Mar 2026 19:37:14 -0600 Subject: [PATCH] exposing stabilizer seeding for the Python wrappers --- .github/dependabot.yml | 48 +++++++++++++++++ Cargo.lock | 12 ++--- python/pecos-rslib/pecos_rslib.pyi | 6 ++- .../pecos-rslib/src/sparse_stab_bindings.rs | 12 ++++- python/pecos-rslib/src/stab_bindings.rs | 12 ++++- .../tests/test_simulator_seeding.py | 52 +++++++++++++++++++ .../test_stabilizer_simulator_seeding.py | 42 +++++++++++++++ uv.lock | 36 ++++++------- 8 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 python/pecos-rslib/tests/test_simulator_seeding.py create mode 100644 python/quantum-pecos/tests/pecos/test_stabilizer_simulator_seeding.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..7c7821c86 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,48 @@ +version: 2 + +updates: + # Rust (Cargo) - security updates only + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + rust-security: + applies-to: security-updates + patterns: ["*"] + # Only open PRs for security advisories + allow: + - dependency-type: "all" + # Ignore all version updates (non-security) + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"] + + # Python (pip) - security updates only + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + python-security: + applies-to: security-updates + patterns: ["*"] + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"] + + # GitHub Actions - security updates only + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + actions-security: + applies-to: security-updates + patterns: ["*"] + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"] diff --git a/Cargo.lock b/Cargo.lock index d9149eaec..317edcab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,9 +1668,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed" -version = "1.30.0" +version = "1.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c566da967934c6c7ee0458a9773de9b2a685bd2ce26a3b28ddfc740e640182f5" +checksum = "9af2cbf772fa6d1c11358f92ef554cb6b386201210bcf0e91fb7fba8a907fb40" dependencies = [ "az", "bytemuck", @@ -2723,9 +2723,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -5327,9 +5327,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", diff --git a/python/pecos-rslib/pecos_rslib.pyi b/python/pecos-rslib/pecos_rslib.pyi index 98f02a403..5a377bce0 100644 --- a/python/pecos-rslib/pecos_rslib.pyi +++ b/python/pecos-rslib/pecos_rslib.pyi @@ -771,7 +771,8 @@ class GateBindingsDict: class SparseSim: """Sparse stabilizer simulator.""" - def __init__(self, num_qubits: int) -> None: ... + def __init__(self, num_qubits: int, seed: int | None = None) -> None: ... + def set_seed(self, seed: int) -> None: ... def reset(self) -> SparseSim: ... @property def num_qubits(self) -> int: ... @@ -788,7 +789,8 @@ class SparseSim: class Stab: """Generic stabilizer simulator (recommended).""" - def __init__(self, num_qubits: int) -> None: ... + def __init__(self, num_qubits: int, seed: int | None = None) -> None: ... + def set_seed(self, seed: int) -> None: ... def reset(self) -> Stab: ... @property def num_qubits(self) -> int: ... diff --git a/python/pecos-rslib/src/sparse_stab_bindings.rs b/python/pecos-rslib/src/sparse_stab_bindings.rs index 799967994..bf02dfd17 100644 --- a/python/pecos-rslib/src/sparse_stab_bindings.rs +++ b/python/pecos-rslib/src/sparse_stab_bindings.rs @@ -24,12 +24,20 @@ pub struct PySparseSim { #[pymethods] impl PySparseSim { #[new] - fn new(num_qubits: usize) -> Self { + #[pyo3(signature = (num_qubits, seed=None))] + fn new(num_qubits: usize, seed: Option) -> Self { PySparseSim { - inner: SparseStab::new(num_qubits), + inner: match seed { + Some(s) => SparseStab::with_seed(num_qubits, s), + None => SparseStab::new(num_qubits), + }, } } + fn set_seed(&mut self, seed: u64) { + self.inner.set_seed(seed); + } + fn reset(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { slf.inner.reset(); slf diff --git a/python/pecos-rslib/src/stab_bindings.rs b/python/pecos-rslib/src/stab_bindings.rs index f08eddb5c..2aa2f9a74 100644 --- a/python/pecos-rslib/src/stab_bindings.rs +++ b/python/pecos-rslib/src/stab_bindings.rs @@ -24,12 +24,20 @@ pub struct PyStab { #[pymethods] impl PyStab { #[new] - fn new(num_qubits: usize) -> Self { + #[pyo3(signature = (num_qubits, seed=None))] + fn new(num_qubits: usize, seed: Option) -> Self { PyStab { - inner: Stab::new(num_qubits), + inner: match seed { + Some(s) => Stab::with_seed(num_qubits, s), + None => Stab::new(num_qubits), + }, } } + fn set_seed(&mut self, seed: u64) { + self.inner.set_seed(seed); + } + fn reset(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { slf.inner.reset(); slf diff --git a/python/pecos-rslib/tests/test_simulator_seeding.py b/python/pecos-rslib/tests/test_simulator_seeding.py new file mode 100644 index 000000000..2640d5b83 --- /dev/null +++ b/python/pecos-rslib/tests/test_simulator_seeding.py @@ -0,0 +1,52 @@ +# Copyright 2026 The PECOS Developers +# +# 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 +# +# https://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. + +"""Tests for simulator seeding via the pecos_rslib bindings.""" + +import pytest +from pecos_rslib.simulators import SparseSim, Stab + + +def _measure_sequence(sim_cls: type, *, seed: int, rounds: int = 16) -> list: + """Create a seeded simulator, apply H then MZ repeatedly, return outcomes.""" + sim = sim_cls(1, seed=seed) + outcomes = [] + for _ in range(rounds): + sim.reset() + sim.run_1q_gate("H", 0) + outcomes.append(sim.run_1q_gate("MZ", 0)) + return outcomes + + +@pytest.mark.parametrize("sim_cls", [SparseSim, Stab]) +class TestSimulatorSeeding: + """Verify that seeded stabilizer simulators produce reproducible results.""" + + def test_constructor_seed_is_reproducible(self, sim_cls: type) -> None: + """Same seed in constructor gives same measurement sequence.""" + assert _measure_sequence(sim_cls, seed=42) == _measure_sequence(sim_cls, seed=42) + + def test_different_seeds_differ(self, sim_cls: type) -> None: + """Different seeds give different measurement sequences.""" + assert _measure_sequence(sim_cls, seed=42) != _measure_sequence(sim_cls, seed=99) + + def test_set_seed_is_reproducible(self, sim_cls: type) -> None: + """Calling set_seed after construction gives reproducible results.""" + sim_a = sim_cls(1) + sim_a.set_seed(42) + outcomes_a = [sim_a.run_1q_gate("H", 0) for _ in range(8)] + + sim_b = sim_cls(1) + sim_b.set_seed(42) + outcomes_b = [sim_b.run_1q_gate("H", 0) for _ in range(8)] + + assert outcomes_a == outcomes_b diff --git a/python/quantum-pecos/tests/pecos/test_stabilizer_simulator_seeding.py b/python/quantum-pecos/tests/pecos/test_stabilizer_simulator_seeding.py new file mode 100644 index 000000000..d5e55a2d1 --- /dev/null +++ b/python/quantum-pecos/tests/pecos/test_stabilizer_simulator_seeding.py @@ -0,0 +1,42 @@ +# Copyright 2026 The PECOS Developers +# +# 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 +# +# https://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. + +"""High-level tests for seeded stabilizer simulator re-exports.""" + +import pytest +from pecos.simulators import SparseSim, Stab + + +def _measurement_sequence( + sim_cls: type, + *, + seed: int | None = None, + reseed: int | None = None, + rounds: int = 16, +) -> list: + sim = sim_cls(1, seed=seed) if seed is not None else sim_cls(1) + if reseed is not None: + sim.set_seed(reseed) + + outcomes = [] + for _ in range(rounds): + sim.reset() + sim.run_1q_gate("H", 0) + outcomes.append(sim.run_1q_gate("MZ", 0)) + return outcomes + + +@pytest.mark.parametrize("sim_cls", [SparseSim, Stab]) +def test_high_level_simulators_accept_seed_and_set_seed(sim_cls: type) -> None: + """Verify that seeded stabilizer simulators produce reproducible results.""" + assert _measurement_sequence(sim_cls, seed=42) == _measurement_sequence(sim_cls, seed=42) + assert _measurement_sequence(sim_cls, reseed=42) == _measurement_sequence(sim_cls, reseed=42) diff --git a/uv.lock b/uv.lock index bc6b35078..6f7ae8bd7 100644 --- a/uv.lock +++ b/uv.lock @@ -1449,11 +1449,11 @@ wheels = [ [[package]] name = "jsonpointer" -version = "3.0.0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/bf/9ecc036fbc15cf4153ea6ed4dbeed31ef043f762cccc9d44a534be8319b0/jsonpointer-3.1.0.tar.gz", hash = "sha256:f9b39abd59ba8c1de8a4ff16141605d2a8dacc4dd6cf399672cf237bfe47c211", size = 9000, upload-time = "2026-03-20T21:47:09.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/cebb241a435cbf4626b5ea096d8385c04416d7ca3082a15299b746e248fa/jsonpointer-3.1.0-py3-none-any.whl", hash = "sha256:f82aa0f745001f169b96473348370b43c3f581446889c41c807bab1db11c8e7b", size = 7651, upload-time = "2026-03-20T21:47:08.792Z" }, ] [[package]] @@ -3002,30 +3002,30 @@ wheels = [ [[package]] name = "polars" -version = "1.39.2" +version = "1.39.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/33/50d5f19904e48c79e287b825e847bb44ca9d22a010da2c54f011c007c9b4/polars-1.39.2.tar.gz", hash = "sha256:81a522dbc8a90153f8b584db65ada340e02eff35e880b1c8317a4db2972f332e", size = 728976, upload-time = "2026-03-17T17:18:41.405Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/52/306da8d01a324a6ad693baa963dcd180f089882426653605bd418681183b/polars-1.39.2-py3-none-any.whl", hash = "sha256:4c18adebbec09e71021cb54af3b529f5d77b78d0220bb96f251855b418152691", size = 823983, upload-time = "2026-03-17T17:16:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" }, ] [[package]] name = "polars-runtime-32" -version = "1.39.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/27/f152f8ac761f1bd40f8b97795d812a92fca36e2d9a4a48899a29a4b63759/polars_runtime_32-1.39.2.tar.gz", hash = "sha256:cd140b510aeaa929d868f2dde12b1d9163e6df416a30abbabc936f0dc68f58b8", size = 2871745, upload-time = "2026-03-17T17:18:43.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/eb/b207c80c0e34ab9bd63dce98edc33c86e76ad3806b57faa551f9cb38483a/polars_runtime_32-1.39.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f8165a49fd3fdb9526f23448ec62186c387ddb8b3a574b1cbe093e7efbf20b97", size = 45273242, upload-time = "2026-03-17T17:16:38.161Z" }, - { url = "https://files.pythonhosted.org/packages/2b/75/ceca07b2b6553fff591335213d91c944da13ee56b0dd789793d9dd5ef106/polars_runtime_32-1.39.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:897deb4b2c878d65d0f753bb79c95b94902e4fb6c8f55693e059f419cebb54e6", size = 40842750, upload-time = "2026-03-17T17:16:41.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/42/e34654ebbba3f46c9a462b96097c425058fd1734e30c90d57f88785580d6/polars_runtime_32-1.39.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac7180dc99251050c54cc7f8e599167b4f10f0f165f64c593cfc6b6ece11da8", size = 43220738, upload-time = "2026-03-17T17:16:45.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ad/f95110341491949f7137f1c74b988a6fede81b0b145771f3a69d9001f15a/polars_runtime_32-1.39.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5895c531f61cad339a6283410c5b142f51c785263970efdea4d3ec571c1d98ef", size = 46877499, upload-time = "2026-03-17T17:16:49.411Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1a/3eefef3da464e3f5defd957baa971567062ac8d6503a0922b9bae51cc8ce/polars_runtime_32-1.39.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4786a82d300e1005b5d3ee3c42f16c5b457d3c34cb5d3e1d2423a41e53a51292", size = 43380175, upload-time = "2026-03-17T17:16:53.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a4/f50f78fa8349eca7d817fd9f55781ec0bf9dd7de2d41b8fa07559f1a8c26/polars_runtime_32-1.39.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b3aa5295f1fabde44230d65843974c7eed8e1826ddbe6279fb2af5ad43135432", size = 46485946, upload-time = "2026-03-17T17:16:57.511Z" }, - { url = "https://files.pythonhosted.org/packages/ad/38/d39a817f7bcc10b3fac623a862f7747f644a7b25c3fa9471a15676fa38a8/polars_runtime_32-1.39.2-cp310-abi3-win_amd64.whl", hash = "sha256:54681d568a7a33468248406a02d9335e767c838c6a64b0d8ea810ed73561efed", size = 46995460, upload-time = "2026-03-17T17:17:03.078Z" }, - { url = "https://files.pythonhosted.org/packages/61/81/ce97dc3d3ebd43f78dd47100110e586df0a48de9668b79806220f948b422/polars_runtime_32-1.39.2-cp310-abi3-win_arm64.whl", hash = "sha256:d4402c8d35a9dcec2049e6b0bcfd13853ef1a0ce456a32b489b81979d3e6f8a5", size = 41857645, upload-time = "2026-03-17T17:17:08.189Z" }, +version = "1.39.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" }, ] [[package]]