From e00dded39979f32b13b09229d4730145a60917c1 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 18 May 2026 12:47:52 +0000 Subject: [PATCH 01/37] Add new noise model and simulators --- poetry.lock | 3767 ++++++++++++--------------- pyproject.toml | 3 +- pyquil/noise.py | 54 +- pyquil/noise/__init__.py | 78 + pyquil/noise/_channels.py | 1503 +++++++++++ pyquil/noise/_legacy_noise.py | 809 ++++++ pyquil/noise/_noise_model.py | 476 ++++ pyquil/quilbase.py | 45 +- pyquil/simulation/_resolver.py | 696 +++++ pyquil/simulation/_simulator.py | 530 ++++ pyquil/transform.py | 213 ++ test/unit/conftest.py | 4 + test/unit/test_legacy_noise.py | 371 +++ test/unit/test_noise_model.py | 310 +++ test/unit/test_qutrit_simulation.py | 564 ++++ test/unit/test_state_vector.py | 1094 ++++++++ 16 files changed, 8408 insertions(+), 2109 deletions(-) create mode 100644 pyquil/noise/__init__.py create mode 100644 pyquil/noise/_channels.py create mode 100644 pyquil/noise/_legacy_noise.py create mode 100644 pyquil/noise/_noise_model.py create mode 100644 pyquil/simulation/_resolver.py create mode 100644 pyquil/simulation/_simulator.py create mode 100644 pyquil/transform.py create mode 100644 test/unit/test_legacy_noise.py create mode 100644 test/unit/test_noise_model.py create mode 100644 test/unit/test_qutrit_simulation.py create mode 100644 test/unit/test_state_vector.py diff --git a/poetry.lock b/poetry.lock index fb05d37c1..5b459f407 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. [[package]] name = "alabaster" @@ -15,66 +15,64 @@ files = [ [[package]] name = "anyio" -version = "4.11.0" +version = "4.13.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, - {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0)"] +trio = ["trio (>=0.32.0)"] [[package]] name = "asttokens" -version = "3.0.0" +version = "3.0.1" description = "Annotate AST trees with source code positions" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"latex\"" files = [ - {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, - {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, + {file = "asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a"}, + {file = "asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7"}, ] [package.extras] -astroid = ["astroid (>=2,<4)"] -test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] +astroid = ["astroid (>=2,<5)"] +test = ["astroid (>=2,<5)", "pytest (<9.0)", "pytest-cov", "pytest-xdist"] [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] @@ -82,19 +80,19 @@ dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)" [[package]] name = "beautifulsoup4" -version = "4.14.2" +version = "4.14.3" description = "Screen-scraping library" optional = true python-versions = ">=3.7.0" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"}, - {file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"}, + {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, + {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, ] [package.dependencies] -soupsieve = ">1.2" +soupsieve = ">=1.6.1" typing-extensions = ">=4.0.0" [package.extras] @@ -106,15 +104,15 @@ lxml = ["lxml"] [[package]] name = "bleach" -version = "6.2.0" +version = "6.3.0" description = "An easy safelist-based HTML-sanitizing tool." optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, - {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, + {file = "bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6"}, + {file = "bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22"}, ] [package.dependencies] @@ -126,14 +124,14 @@ css = ["tinycss2 (>=1.1.0,<1.5)"] [[package]] name = "certifi" -version = "2025.10.5" +version = "2026.4.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, + {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, ] [[package]] @@ -236,92 +234,142 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, - {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, - {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, ] [[package]] @@ -335,93 +383,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "(extra == \"latex\" or extra == \"docs\") and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} - -[[package]] -name = "contourpy" -version = "1.3.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"docs\"" -files = [ - {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, - {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"}, - {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"}, - {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"}, - {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"}, - {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"}, - {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"}, - {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"}, - {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"}, - {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"}, - {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"}, - {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"}, - {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"}, - {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"}, - {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"}, - {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"}, - {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"}, - {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"}, - {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"}, - {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"}, - {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"}, - {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"}, - {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"}, - {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"}, - {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"}, - {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"}, - {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"}, - {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"}, - {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"}, - {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"}, - {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"}, - {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"}, - {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"}, - {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"}, - {file = "contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4"}, -] - -[package.dependencies] -numpy = ">=1.23" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] +markers = {main = "sys_platform == \"win32\" and (extra == \"latex\" or extra == \"docs\")", dev = "sys_platform == \"win32\""} [[package]] name = "contourpy" @@ -430,7 +392,7 @@ description = "Python library for calculating contours of 2D quadrilateral grids optional = true python-versions = ">=3.11" groups = ["main"] -markers = "python_version >= \"3.11\" and extra == \"docs\"" +markers = "extra == \"docs\"" files = [ {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, @@ -518,121 +480,120 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.10.7" +version = "7.13.5" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -681,18 +642,18 @@ files = [ [[package]] name = "deprecated" -version = "1.2.18" +version = "1.3.1" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" groups = ["main"] files = [ - {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, - {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, + {file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"}, + {file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"}, ] [package.dependencies] -wrapt = ">=1.10,<2" +wrapt = ">=1.10,<3" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] @@ -710,35 +671,16 @@ files = [ {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, ] -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "execnet" -version = "2.1.1" +version = "2.1.2" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, ] [package.extras] @@ -778,84 +720,76 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "fonttools" -version = "4.60.1" +version = "4.62.1" description = "Tools to manipulate font files" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28"}, - {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15"}, - {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c"}, - {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea"}, - {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652"}, - {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a"}, - {file = "fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce"}, - {file = "fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038"}, - {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f"}, - {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2"}, - {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914"}, - {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1"}, - {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d"}, - {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa"}, - {file = "fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258"}, - {file = "fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf"}, - {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc"}, - {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877"}, - {file = "fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c"}, - {file = "fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401"}, - {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903"}, - {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed"}, - {file = "fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6"}, - {file = "fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383"}, - {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb"}, - {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4"}, - {file = "fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c"}, - {file = "fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77"}, - {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199"}, - {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c"}, - {file = "fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272"}, - {file = "fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac"}, - {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3"}, - {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85"}, - {file = "fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537"}, - {file = "fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003"}, - {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08"}, - {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99"}, - {file = "fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6"}, - {file = "fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987"}, - {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299"}, - {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01"}, - {file = "fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801"}, - {file = "fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc"}, - {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc"}, - {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed"}, - {file = "fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259"}, - {file = "fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c"}, - {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2"}, - {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036"}, - {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856"}, - {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7"}, - {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854"}, - {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da"}, - {file = "fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a"}, - {file = "fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217"}, - {file = "fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb"}, - {file = "fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9"}, + {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c"}, + {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a"}, + {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3"}, + {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23"}, + {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d"}, + {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae"}, + {file = "fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed"}, + {file = "fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9"}, + {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7"}, + {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14"}, + {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7"}, + {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b"}, + {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1"}, + {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416"}, + {file = "fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53"}, + {file = "fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2"}, + {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974"}, + {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9"}, + {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936"}, + {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392"}, + {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04"}, + {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d"}, + {file = "fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c"}, + {file = "fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42"}, + {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79"}, + {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe"}, + {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68"}, + {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1"}, + {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069"}, + {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9"}, + {file = "fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24"}, + {file = "fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056"}, + {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"}, + {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"}, + {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"}, + {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"}, + {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"}, + {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"}, + {file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"}, + {file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"}, + {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"}, + {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"}, + {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"}, + {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"}, + {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"}, + {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"}, + {file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"}, + {file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"}, + {file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"}, + {file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"}, ] [package.extras] -all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] +repacker = ["uharfbuzz (>=0.45.0)"] symfont = ["sympy"] type1 = ["xattr ; sys_platform == \"darwin\""] -unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""] woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] [[package]] @@ -878,80 +812,80 @@ testing = ["protobuf (>=4.21.9)"] [[package]] name = "grpcio" -version = "1.75.1" +version = "1.80.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "grpcio-1.75.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:1712b5890b22547dd29f3215c5788d8fc759ce6dd0b85a6ba6e2731f2d04c088"}, - {file = "grpcio-1.75.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8d04e101bba4b55cea9954e4aa71c24153ba6182481b487ff376da28d4ba46cf"}, - {file = "grpcio-1.75.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:683cfc70be0c1383449097cba637317e4737a357cfc185d887fd984206380403"}, - {file = "grpcio-1.75.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:491444c081a54dcd5e6ada57314321ae526377f498d4aa09d975c3241c5b9e1c"}, - {file = "grpcio-1.75.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce08d4e112d0d38487c2b631ec8723deac9bc404e9c7b1011426af50a79999e4"}, - {file = "grpcio-1.75.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5a2acda37fc926ccc4547977ac3e56b1df48fe200de968e8c8421f6e3093df6c"}, - {file = "grpcio-1.75.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:745c5fe6bf05df6a04bf2d11552c7d867a2690759e7ab6b05c318a772739bd75"}, - {file = "grpcio-1.75.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:259526a7159d39e2db40d566fe3e8f8e034d0fb2db5bf9c00e09aace655a4c2b"}, - {file = "grpcio-1.75.1-cp310-cp310-win32.whl", hash = "sha256:f4b29b9aabe33fed5df0a85e5f13b09ff25e2c05bd5946d25270a8bd5682dac9"}, - {file = "grpcio-1.75.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf2e760978dcce7ff7d465cbc7e276c3157eedc4c27aa6de7b594c7a295d3d61"}, - {file = "grpcio-1.75.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:573855ca2e58e35032aff30bfbd1ee103fbcf4472e4b28d4010757700918e326"}, - {file = "grpcio-1.75.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:6a4996a2c8accc37976dc142d5991adf60733e223e5c9a2219e157dc6a8fd3a2"}, - {file = "grpcio-1.75.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b1ea1bbe77ecbc1be00af2769f4ae4a88ce93be57a4f3eebd91087898ed749f9"}, - {file = "grpcio-1.75.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e5b425aee54cc5e3e3c58f00731e8a33f5567965d478d516d35ef99fd648ab68"}, - {file = "grpcio-1.75.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0049a7bf547dafaeeb1db17079ce79596c298bfe308fc084d023c8907a845b9a"}, - {file = "grpcio-1.75.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b8ea230c7f77c0a1a3208a04a1eda164633fb0767b4cefd65a01079b65e5b1f"}, - {file = "grpcio-1.75.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:36990d629c3c9fb41e546414e5af52d0a7af37ce7113d9682c46d7e2919e4cca"}, - {file = "grpcio-1.75.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b10ad908118d38c2453ade7ff790e5bce36580c3742919007a2a78e3a1e521ca"}, - {file = "grpcio-1.75.1-cp311-cp311-win32.whl", hash = "sha256:d6be2b5ee7bea656c954dcf6aa8093c6f0e6a3ef9945c99d99fcbfc88c5c0bfe"}, - {file = "grpcio-1.75.1-cp311-cp311-win_amd64.whl", hash = "sha256:61c692fb05956b17dd6d1ab480f7f10ad0536dba3bc8fd4e3c7263dc244ed772"}, - {file = "grpcio-1.75.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:7b888b33cd14085d86176b1628ad2fcbff94cfbbe7809465097aa0132e58b018"}, - {file = "grpcio-1.75.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8775036efe4ad2085975531d221535329f5dac99b6c2a854a995456098f99546"}, - {file = "grpcio-1.75.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb658f703468d7fbb5dcc4037c65391b7dc34f808ac46ed9136c24fc5eeb041d"}, - {file = "grpcio-1.75.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4b7177a1cdb3c51b02b0c0a256b0a72fdab719600a693e0e9037949efffb200b"}, - {file = "grpcio-1.75.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d4fa6ccc3ec2e68a04f7b883d354d7fea22a34c44ce535a2f0c0049cf626ddf"}, - {file = "grpcio-1.75.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d86880ecaeb5b2f0a8afa63824de93adb8ebe4e49d0e51442532f4e08add7d6"}, - {file = "grpcio-1.75.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a8041d2f9e8a742aeae96f4b047ee44e73619f4f9d24565e84d5446c623673b6"}, - {file = "grpcio-1.75.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3652516048bf4c314ce12be37423c79829f46efffb390ad64149a10c6071e8de"}, - {file = "grpcio-1.75.1-cp312-cp312-win32.whl", hash = "sha256:44b62345d8403975513af88da2f3d5cc76f73ca538ba46596f92a127c2aea945"}, - {file = "grpcio-1.75.1-cp312-cp312-win_amd64.whl", hash = "sha256:b1e191c5c465fa777d4cafbaacf0c01e0d5278022082c0abbd2ee1d6454ed94d"}, - {file = "grpcio-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3bed22e750d91d53d9e31e0af35a7b0b51367e974e14a4ff229db5b207647884"}, - {file = "grpcio-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5b8f381eadcd6ecaa143a21e9e80a26424c76a0a9b3d546febe6648f3a36a5ac"}, - {file = "grpcio-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5bf4001d3293e3414d0cf99ff9b1139106e57c3a66dfff0c5f60b2a6286ec133"}, - {file = "grpcio-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f82ff474103e26351dacfe8d50214e7c9322960d8d07ba7fa1d05ff981c8b2d"}, - {file = "grpcio-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ee119f4f88d9f75414217823d21d75bfe0e6ed40135b0cbbfc6376bc9f7757d"}, - {file = "grpcio-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:664eecc3abe6d916fa6cf8dd6b778e62fb264a70f3430a3180995bf2da935446"}, - {file = "grpcio-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c32193fa08b2fbebf08fe08e84f8a0aad32d87c3ad42999c65e9449871b1c66e"}, - {file = "grpcio-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5cebe13088b9254f6e615bcf1da9131d46cfa4e88039454aca9cb65f639bd3bc"}, - {file = "grpcio-1.75.1-cp313-cp313-win32.whl", hash = "sha256:4b4c678e7ed50f8ae8b8dbad15a865ee73ce12668b6aaf411bf3258b5bc3f970"}, - {file = "grpcio-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:5573f51e3f296a1bcf71e7a690c092845fb223072120f4bdb7a5b48e111def66"}, - {file = "grpcio-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:c05da79068dd96723793bffc8d0e64c45f316248417515f28d22204d9dae51c7"}, - {file = "grpcio-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06373a94fd16ec287116a825161dca179a0402d0c60674ceeec8c9fba344fe66"}, - {file = "grpcio-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4484f4b7287bdaa7a5b3980f3c7224c3c622669405d20f69549f5fb956ad0421"}, - {file = "grpcio-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2720c239c1180eee69f7883c1d4c83fc1a495a2535b5fa322887c70bf02b16e8"}, - {file = "grpcio-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:07a554fa31c668cf0e7a188678ceeca3cb8fead29bbe455352e712ec33ca701c"}, - {file = "grpcio-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3e71a2105210366bfc398eef7f57a664df99194f3520edb88b9c3a7e46ee0d64"}, - {file = "grpcio-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8679aa8a5b67976776d3c6b0521e99d1c34db8a312a12bcfd78a7085cb9b604e"}, - {file = "grpcio-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aad1c774f4ebf0696a7f148a56d39a3432550612597331792528895258966dc0"}, - {file = "grpcio-1.75.1-cp314-cp314-win32.whl", hash = "sha256:62ce42d9994446b307649cb2a23335fa8e927f7ab2cbf5fcb844d6acb4d85f9c"}, - {file = "grpcio-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:f86e92275710bea3000cb79feca1762dc0ad3b27830dd1a74e82ab321d4ee464"}, - {file = "grpcio-1.75.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:c09fba33327c3ac11b5c33dbdd8218eef8990d78f83b1656d628831812a8c0fb"}, - {file = "grpcio-1.75.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:7e21400b037be29545704889e72e586c238e346dcb2d08d8a7288d16c883a9ec"}, - {file = "grpcio-1.75.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c12121e509b9f8b0914d10054d24120237d19e870b1cd82acbb8a9b9ddd198a3"}, - {file = "grpcio-1.75.1-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:73577a93e692b3474b1bfe84285d098de36705dbd838bb4d6a056d326e4dc880"}, - {file = "grpcio-1.75.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e19e7dfa0d7ca7dea22be464339e18ac608fd75d88c56770c646cdabe54bc724"}, - {file = "grpcio-1.75.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e1c28f51c1cf67eccdfc1065e8e866c9ed622f09773ca60947089c117f848a1"}, - {file = "grpcio-1.75.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:030a6164bc2ca726052778c0cf8e3249617a34e368354f9e6107c27ad4af8c28"}, - {file = "grpcio-1.75.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:67697efef5a98d46d5db7b1720fa4043536f8b8e5072a5d61cfca762f287e939"}, - {file = "grpcio-1.75.1-cp39-cp39-win32.whl", hash = "sha256:52015cf73eb5d76f6404e0ce0505a69b51fd1f35810b3a01233b34b10baafb41"}, - {file = "grpcio-1.75.1-cp39-cp39-win_amd64.whl", hash = "sha256:9fe51e4a1f896ea84ac750900eae34d9e9b896b5b1e4a30b02dc31ad29f36383"}, - {file = "grpcio-1.75.1.tar.gz", hash = "sha256:3e81d89ece99b9ace23a6916880baca613c03a799925afb2857887efa8b1b3d2"}, + {file = "grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c"}, + {file = "grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388"}, + {file = "grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02"}, + {file = "grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc"}, + {file = "grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a"}, + {file = "grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9"}, + {file = "grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199"}, + {file = "grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81"}, + {file = "grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069"}, + {file = "grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58"}, + {file = "grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a"}, + {file = "grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060"}, + {file = "grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2"}, + {file = "grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21"}, + {file = "grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab"}, + {file = "grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1"}, + {file = "grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106"}, + {file = "grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6"}, + {file = "grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440"}, + {file = "grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9"}, + {file = "grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0"}, + {file = "grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2"}, + {file = "grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de"}, + {file = "grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921"}, + {file = "grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411"}, + {file = "grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd"}, + {file = "grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f"}, + {file = "grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f"}, + {file = "grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193"}, + {file = "grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff"}, + {file = "grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad"}, + {file = "grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0"}, + {file = "grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f"}, + {file = "grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6"}, + {file = "grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140"}, + {file = "grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d"}, + {file = "grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7"}, + {file = "grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7"}, + {file = "grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294"}, + {file = "grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50"}, + {file = "grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e"}, + {file = "grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f"}, + {file = "grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9"}, + {file = "grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14"}, + {file = "grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05"}, + {file = "grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1"}, + {file = "grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f"}, + {file = "grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e"}, + {file = "grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae"}, + {file = "grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f"}, + {file = "grpcio-1.80.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:aacdfb4ed3eb919ca997504d27e03d5dba403c85130b8ed450308590a738f7a4"}, + {file = "grpcio-1.80.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:a361c20ec1ccd3c3953d20fb6d7b4125093bdd10dff44c5e2bbb39e58917cedc"}, + {file = "grpcio-1.80.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43168871f170d1e4ed16ae03d10cd21efa29f190e710a624cee7e5ae07da6f4f"}, + {file = "grpcio-1.80.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1b97cd29a8eda100b559b455331c487a80915b6ea6bd91cf3e89836c4ee8d957"}, + {file = "grpcio-1.80.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bac1d573dfa84ce59a5547073e28fa7326d53352adda6912e362da0b917fcef4"}, + {file = "grpcio-1.80.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4560cf0e86514595dbbd330cd65b7afad4b5c4b8c4905c041cfffa138d45e6fd"}, + {file = "grpcio-1.80.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec0a592e926071b4abad50c1495cd0d0d513324b3ff5e7267067c33ba27506e4"}, + {file = "grpcio-1.80.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:deb10a1528473c11f72a0939eed36d83e847d7cbb63e8cc5611fb7a912d38614"}, + {file = "grpcio-1.80.0-cp39-cp39-win32.whl", hash = "sha256:627fb7312171cdc52828bd6fac8d7028ff2a64b89f1957b6f3416caa2218d141"}, + {file = "grpcio-1.80.0-cp39-cp39-win_amd64.whl", hash = "sha256:05d55e1798756282cddd52d56c896b3e7d673e3a8798c2f1cd05ba249a3bb4de"}, + {file = "grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257"}, ] [package.dependencies] typing-extensions = ">=4.12,<5.0" [package.extras] -protobuf = ["grpcio-tools (>=1.75.1)"] +protobuf = ["grpcio-tools (>=1.80.0)"] [[package]] name = "h11" @@ -1014,143 +948,55 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.10" +version = "3.13" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"}, + {file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" +version = "2.0.0" +description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "<3.15,>=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -description = "Read resources from Python packages" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" -files = [ - {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, - {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, + {file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"}, + {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}, ] -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "ipython" -version = "8.18.1" -description = "IPython: Productive Interactive Computing" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"latex\"" -files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] - [[package]] name = "ipython" -version = "8.37.0" +version = "8.39.0" description = "IPython: Productive Interactive Computing" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.11\" and extra == \"latex\"" +markers = "extra == \"latex\"" files = [ - {file = "ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2"}, - {file = "ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216"}, + {file = "ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f"}, + {file = "ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624"}, ] [package.dependencies] @@ -1179,6 +1025,75 @@ qtconsole = ["qtconsole"] test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] +[[package]] +name = "jax" +version = "0.10.0" +description = "Differentiate, compile, and transform Numpy code." +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "jax-0.10.0-py3-none-any.whl", hash = "sha256:76c42ba163c8db3dc2e449e225b888c0edfb623ded31efdc96d85e0fda1d26e8"}, + {file = "jax-0.10.0.tar.gz", hash = "sha256:0119c767de1645f407df72345d28a3837dc904f1d698911c121d8f2b396fdece"}, +] + +[package.dependencies] +jaxlib = "0.10.0" +ml_dtypes = ">=0.5.0" +numpy = ">=2.0" +opt_einsum = "*" +scipy = ">=1.14" + +[package.extras] +ci = ["jaxlib (==0.9.2)"] +cuda = ["jax-cuda12-plugin[with-cuda] (==0.10.0)", "jaxlib (==0.10.0)"] +cuda12 = ["jax-cuda12-plugin[with-cuda] (==0.10.0)", "jaxlib (==0.10.0)"] +cuda12-local = ["jax-cuda12-plugin (==0.10.0)", "jaxlib (==0.10.0)"] +cuda13 = ["jax-cuda13-plugin[with-cuda] (==0.10.0)", "jaxlib (==0.10.0)"] +cuda13-local = ["jax-cuda13-plugin (==0.10.0)", "jaxlib (==0.10.0)"] +k8s = ["kubernetes"] +minimum-jaxlib = ["jaxlib (==0.10.0)"] +rocm7-local = ["jax-rocm7-plugin (==0.10.0.*)", "jaxlib (==0.10.0)"] +tpu = ["jaxlib (==0.10.0)", "libtpu (==0.0.40.*)", "requests"] +xprof = ["xprof"] + +[[package]] +name = "jaxlib" +version = "0.10.0" +description = "XLA library for JAX" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "jaxlib-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:277032e9f074c3fd5ffd1e0cb03d4fe66e272de472667cdbc418ad99b21b646a"}, + {file = "jaxlib-0.10.0-cp311-cp311-manylinux_2_27_aarch64.whl", hash = "sha256:3db94ebc859375d955de3504182add7ce1733ce3d30c15e0ef031602cb51a559"}, + {file = "jaxlib-0.10.0-cp311-cp311-manylinux_2_27_x86_64.whl", hash = "sha256:9be229993a41e5b2b84f234ecc19a5de02f35eddb1195cf027bd539e1601e15d"}, + {file = "jaxlib-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:421cdf3a4a5c2ee41471035e586954c8dc599d677ce9b11b063c3926a82a7850"}, + {file = "jaxlib-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c1d9b463327c7a2333f210114ecb04f28fefc51ba8233a85a2280cce75bdb42"}, + {file = "jaxlib-0.10.0-cp312-cp312-manylinux_2_27_aarch64.whl", hash = "sha256:aa1d70f1a4e27eb403654e71e2fb28d5786d3e9b77fc1847e8c5389880927ca4"}, + {file = "jaxlib-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl", hash = "sha256:b0bfb865a07df2e6d7418c0b0c292dd294b5500523b1dd5872b180db2aa480d4"}, + {file = "jaxlib-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:25bf167e0d8b594e0ec50783ff4892c0b7ec37236c88b2b425a7c252823f8680"}, + {file = "jaxlib-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:384635fff55899a295bbc82ee6c6f773a300e787dc472ca92bbe79abfaac8369"}, + {file = "jaxlib-0.10.0-cp313-cp313-manylinux_2_27_aarch64.whl", hash = "sha256:6d8d78b7070b34e4c5bba5f7e10927e7f4aac9b69be17e9b0a5898553a4338f3"}, + {file = "jaxlib-0.10.0-cp313-cp313-manylinux_2_27_x86_64.whl", hash = "sha256:d303dc31b65e8b793d5600f81b1583be03dc9b876a4c10b3e259b6609a1cbe3b"}, + {file = "jaxlib-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:3869be623c2f3391be2ee86f8b412372b102492e67cac0a5f0ab1037bbc3a5cc"}, + {file = "jaxlib-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9050ce2ae7eeca62b1a235065056cad62cac590ddc035486faa4472a47eed9f6"}, + {file = "jaxlib-0.10.0-cp313-cp313t-manylinux_2_27_aarch64.whl", hash = "sha256:59e07aab3bdfaad9bdd3cf32e0d3d4f228837b9b231c53f5ae1c0fc284481094"}, + {file = "jaxlib-0.10.0-cp313-cp313t-manylinux_2_27_x86_64.whl", hash = "sha256:3088503812cfe49f34a3083d3b7ef5cb3aaf33d89ceb1b3f647fa52713aee59d"}, + {file = "jaxlib-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98b26672943672742873f65bc03216819fc55325c99f146590d007c0172bff30"}, + {file = "jaxlib-0.10.0-cp314-cp314-manylinux_2_27_aarch64.whl", hash = "sha256:ad47e072430979ec21637aa487d4dc464028b8e9be27268f37de69536c76e341"}, + {file = "jaxlib-0.10.0-cp314-cp314-manylinux_2_27_x86_64.whl", hash = "sha256:2a42cf04c0f88bc03b150a17fa7ddbb2f40e096667ec8a1b840ed87913e6e735"}, + {file = "jaxlib-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:450b771c01b3662c3497e2dceada3f6fc893112ae637ef85ef1dcc7dc68892a8"}, + {file = "jaxlib-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f62026c9fb1f05998592082a6dcb62f70b466342bc139f711802a9b184ba9a46"}, + {file = "jaxlib-0.10.0-cp314-cp314t-manylinux_2_27_aarch64.whl", hash = "sha256:e66bdc0b57ed5649950799d3f0d67a6bb67f03d06b49ea3fced0bdd6140a9943"}, + {file = "jaxlib-0.10.0-cp314-cp314t-manylinux_2_27_x86_64.whl", hash = "sha256:4dccd9065b30954879869641472d5d12fe4d7914175a5cad56293af8429ce7e0"}, +] + +[package.dependencies] +ml_dtypes = ">=0.5.0" +numpy = ">=2.0" +scipy = ">=1.14" + [[package]] name = "jedi" version = "0.19.2" @@ -1211,6 +1126,7 @@ files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] MarkupSafe = ">=2.0" @@ -1220,22 +1136,22 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, - {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" -rpds-py = ">=0.7.1" +rpds-py = ">=0.25.0" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] @@ -1259,45 +1175,44 @@ referencing = ">=0.31.0" [[package]] name = "jupyter-client" -version = "8.6.3" +version = "8.8.0" description = "Jupyter protocol implementation and client libraries" optional = true -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, - {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, + {file = "jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a"}, + {file = "jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=5.1" python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" +pyzmq = ">=25.0" +tornado = ">=6.4.1" traitlets = ">=5.3" [package.extras] docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] +orjson = ["orjson"] +test = ["anyio", "coverage", "ipykernel (>=6.14)", "msgpack", "mypy ; platform_python_implementation != \"PyPy\"", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.6.2)", "pytest-timeout"] [[package]] name = "jupyter-core" -version = "5.8.1" +version = "5.9.1" description = "Jupyter core package. A base package on which Jupyter projects rely." optional = true -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0"}, - {file = "jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941"}, + {file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"}, + {file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"}, ] [package.dependencies] platformdirs = ">=2.5" -pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} traitlets = ">=5.3" [package.extras] @@ -1319,239 +1234,231 @@ files = [ [[package]] name = "kiwisolver" -version = "1.4.7" -description = "A fast implementation of the Cassowary constraint solver" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"docs\"" -files = [ - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, - {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, -] - -[[package]] -name = "kiwisolver" -version = "1.4.9" +version = "1.5.0" description = "A fast implementation of the Cassowary constraint solver" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.11\" and extra == \"docs\"" -files = [ - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1"}, - {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, +markers = "extra == \"docs\"" +files = [ + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1"}, + {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"}, +] + +[[package]] +name = "librt" +version = "0.9.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443"}, + {file = "librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c"}, + {file = "librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e"}, + {file = "librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285"}, + {file = "librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2"}, + {file = "librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b"}, + {file = "librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774"}, + {file = "librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8"}, + {file = "librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671"}, + {file = "librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d"}, + {file = "librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6"}, + {file = "librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1"}, + {file = "librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882"}, + {file = "librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a"}, + {file = "librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6"}, + {file = "librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8"}, + {file = "librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a"}, + {file = "librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4"}, + {file = "librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d"}, + {file = "librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f"}, + {file = "librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27"}, + {file = "librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2"}, + {file = "librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f"}, + {file = "librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f"}, + {file = "librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745"}, + {file = "librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9"}, + {file = "librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e"}, + {file = "librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22"}, + {file = "librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a"}, + {file = "librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5"}, + {file = "librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11"}, + {file = "librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d"}, + {file = "librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd"}, + {file = "librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519"}, + {file = "librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5"}, + {file = "librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb"}, + {file = "librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499"}, + {file = "librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f"}, + {file = "librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1"}, + {file = "librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f"}, + {file = "librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b"}, + {file = "librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9"}, + {file = "librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e"}, + {file = "librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f"}, + {file = "librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4"}, + {file = "librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15"}, + {file = "librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40"}, + {file = "librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118"}, + {file = "librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61"}, + {file = "librt-0.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4"}, + {file = "librt-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a"}, + {file = "librt-0.9.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927"}, + {file = "librt-0.9.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f"}, + {file = "librt-0.9.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7"}, + {file = "librt-0.9.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6"}, + {file = "librt-0.9.0-cp39-cp39-win32.whl", hash = "sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15"}, + {file = "librt-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1"}, + {file = "librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d"}, ] [[package]] @@ -1678,138 +1585,72 @@ files = [ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "matplotlib" -version = "3.9.4" -description = "Python plotting package" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"docs\"" -files = [ - {file = "matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50"}, - {file = "matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff"}, - {file = "matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26"}, - {file = "matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50"}, - {file = "matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5"}, - {file = "matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d"}, - {file = "matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c"}, - {file = "matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099"}, - {file = "matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249"}, - {file = "matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423"}, - {file = "matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e"}, - {file = "matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3"}, - {file = "matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70"}, - {file = "matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483"}, - {file = "matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f"}, - {file = "matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00"}, - {file = "matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0"}, - {file = "matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b"}, - {file = "matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6"}, - {file = "matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45"}, - {file = "matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858"}, - {file = "matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64"}, - {file = "matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df"}, - {file = "matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799"}, - {file = "matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb"}, - {file = "matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a"}, - {file = "matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c"}, - {file = "matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764"}, - {file = "matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041"}, - {file = "matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965"}, - {file = "matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c"}, - {file = "matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7"}, - {file = "matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e"}, - {file = "matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c"}, - {file = "matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb"}, - {file = "matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865"}, - {file = "matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.3.1" -numpy = ">=1.23" -packaging = ">=20.0" -pillow = ">=8" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" - -[package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "numpy (>=1.25)", "pybind11 (>=2.6,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] - -[[package]] -name = "matplotlib" -version = "3.10.6" +version = "3.10.9" description = "Python plotting package" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.11\" and extra == \"docs\"" -files = [ - {file = "matplotlib-3.10.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bc7316c306d97463a9866b89d5cc217824e799fa0de346c8f68f4f3d27c8693d"}, - {file = "matplotlib-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d00932b0d160ef03f59f9c0e16d1e3ac89646f7785165ce6ad40c842db16cc2e"}, - {file = "matplotlib-3.10.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fa4c43d6bfdbfec09c733bca8667de11bfa4970e8324c471f3a3632a0301c15"}, - {file = "matplotlib-3.10.6-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea117a9c1627acaa04dbf36265691921b999cbf515a015298e54e1a12c3af837"}, - {file = "matplotlib-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08fc803293b4e1694ee325896030de97f74c141ccff0be886bb5915269247676"}, - {file = "matplotlib-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:2adf92d9b7527fbfb8818e050260f0ebaa460f79d61546374ce73506c9421d09"}, - {file = "matplotlib-3.10.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:905b60d1cb0ee604ce65b297b61cf8be9f4e6cfecf95a3fe1c388b5266bc8f4f"}, - {file = "matplotlib-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bac38d816637343e53d7185d0c66677ff30ffb131044a81898b5792c956ba76"}, - {file = "matplotlib-3.10.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:942a8de2b5bfff1de31d95722f702e2966b8a7e31f4e68f7cd963c7cd8861cf6"}, - {file = "matplotlib-3.10.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3276c85370bc0dfca051ec65c5817d1e0f8f5ce1b7787528ec8ed2d524bbc2f"}, - {file = "matplotlib-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9df5851b219225731f564e4b9e7f2ac1e13c9e6481f941b5631a0f8e2d9387ce"}, - {file = "matplotlib-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:abb5d9478625dd9c9eb51a06d39aae71eda749ae9b3138afb23eb38824026c7e"}, - {file = "matplotlib-3.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:886f989ccfae63659183173bb3fced7fd65e9eb793c3cc21c273add368536951"}, - {file = "matplotlib-3.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ca662df6a80bd426f871105fdd69db7543e28e73a9f2afe80de7e531eb2347"}, - {file = "matplotlib-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1678bb61d897bb4ac4757b5ecfb02bfb3fddf7f808000fb81e09c510712fda75"}, - {file = "matplotlib-3.10.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:56cd2d20842f58c03d2d6e6c1f1cf5548ad6f66b91e1e48f814e4fb5abd1cb95"}, - {file = "matplotlib-3.10.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:662df55604a2f9a45435566d6e2660e41efe83cd94f4288dfbf1e6d1eae4b0bb"}, - {file = "matplotlib-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08f141d55148cd1fc870c3387d70ca4df16dee10e909b3b038782bd4bda6ea07"}, - {file = "matplotlib-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:590f5925c2d650b5c9d813c5b3b5fc53f2929c3f8ef463e4ecfa7e052044fb2b"}, - {file = "matplotlib-3.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:f44c8d264a71609c79a78d50349e724f5d5fc3684ead7c2a473665ee63d868aa"}, - {file = "matplotlib-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:819e409653c1106c8deaf62e6de6b8611449c2cd9939acb0d7d4e57a3d95cc7a"}, - {file = "matplotlib-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59c8ac8382fefb9cb71308dde16a7c487432f5255d8f1fd32473523abecfecdf"}, - {file = "matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84e82d9e0fd70c70bc55739defbd8055c54300750cbacf4740c9673a24d6933a"}, - {file = "matplotlib-3.10.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25f7a3eb42d6c1c56e89eacd495661fc815ffc08d9da750bca766771c0fd9110"}, - {file = "matplotlib-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9c862d91ec0b7842920a4cfdaaec29662195301914ea54c33e01f1a28d014b2"}, - {file = "matplotlib-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:1b53bd6337eba483e2e7d29c5ab10eee644bc3a2491ec67cc55f7b44583ffb18"}, - {file = "matplotlib-3.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:cbd5eb50b7058b2892ce45c2f4e92557f395c9991f5c886d1bb74a1582e70fd6"}, - {file = "matplotlib-3.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:acc86dd6e0e695c095001a7fccff158c49e45e0758fdf5dcdbb0103318b59c9f"}, - {file = "matplotlib-3.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e228cd2ffb8f88b7d0b29e37f68ca9aaf83e33821f24a5ccc4f082dd8396bc27"}, - {file = "matplotlib-3.10.6-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:658bc91894adeab669cf4bb4a186d049948262987e80f0857216387d7435d833"}, - {file = "matplotlib-3.10.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8913b7474f6dd83ac444c9459c91f7f0f2859e839f41d642691b104e0af056aa"}, - {file = "matplotlib-3.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:091cea22e059b89f6d7d1a18e2c33a7376c26eee60e401d92a4d6726c4e12706"}, - {file = "matplotlib-3.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:491e25e02a23d7207629d942c666924a6b61e007a48177fdd231a0097b7f507e"}, - {file = "matplotlib-3.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3d80d60d4e54cda462e2cd9a086d85cd9f20943ead92f575ce86885a43a565d5"}, - {file = "matplotlib-3.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:70aaf890ce1d0efd482df969b28a5b30ea0b891224bb315810a3940f67182899"}, - {file = "matplotlib-3.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1565aae810ab79cb72e402b22facfa6501365e73ebab70a0fdfb98488d2c3c0c"}, - {file = "matplotlib-3.10.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3b23315a01981689aa4e1a179dbf6ef9fbd17143c3eea77548c2ecfb0499438"}, - {file = "matplotlib-3.10.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30fdd37edf41a4e6785f9b37969de57aea770696cb637d9946eb37470c94a453"}, - {file = "matplotlib-3.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc31e693da1c08012c764b053e702c1855378e04102238e6a5ee6a7117c53a47"}, - {file = "matplotlib-3.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:05be9bdaa8b242bc6ff96330d18c52f1fc59c6fb3a4dd411d953d67e7e1baf98"}, - {file = "matplotlib-3.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:f56a0d1ab05d34c628592435781d185cd99630bdfd76822cd686fb5a0aecd43a"}, - {file = "matplotlib-3.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:94f0b4cacb23763b64b5dace50d5b7bfe98710fed5f0cef5c08135a03399d98b"}, - {file = "matplotlib-3.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cc332891306b9fb39462673d8225d1b824c89783fee82840a709f96714f17a5c"}, - {file = "matplotlib-3.10.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee1d607b3fb1590deb04b69f02ea1d53ed0b0bf75b2b1a5745f269afcbd3cdd3"}, - {file = "matplotlib-3.10.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:376a624a218116461696b27b2bbf7a8945053e6d799f6502fc03226d077807bf"}, - {file = "matplotlib-3.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:83847b47f6524c34b4f2d3ce726bb0541c48c8e7692729865c3df75bfa0f495a"}, - {file = "matplotlib-3.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c7e0518e0d223683532a07f4b512e2e0729b62674f1b3a1a69869f98e6b1c7e3"}, - {file = "matplotlib-3.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:4dd83e029f5b4801eeb87c64efd80e732452781c16a9cf7415b7b63ec8f374d7"}, - {file = "matplotlib-3.10.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:13fcd07ccf17e354398358e0307a1f53f5325dca22982556ddb9c52837b5af41"}, - {file = "matplotlib-3.10.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:470fc846d59d1406e34fa4c32ba371039cd12c2fe86801159a965956f2575bd1"}, - {file = "matplotlib-3.10.6-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7173f8551b88f4ef810a94adae3128c2530e0d07529f7141be7f8d8c365f051"}, - {file = "matplotlib-3.10.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f2d684c3204fa62421bbf770ddfebc6b50130f9cad65531eeba19236d73bb488"}, - {file = "matplotlib-3.10.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f4a69196e663a41d12a728fab8751177215357906436804217d6d9cf0d4d6cf"}, - {file = "matplotlib-3.10.6-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d6ca6ef03dfd269f4ead566ec6f3fb9becf8dab146fb999022ed85ee9f6b3eb"}, - {file = "matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c"}, +markers = "extra == \"docs\"" +files = [ + {file = "matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217"}, + {file = "matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b"}, + {file = "matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37"}, + {file = "matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294"}, + {file = "matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65"}, + {file = "matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda"}, + {file = "matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb"}, + {file = "matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb"}, + {file = "matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb"}, + {file = "matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9"}, + {file = "matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb"}, + {file = "matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f"}, + {file = "matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80"}, + {file = "matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1"}, + {file = "matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320"}, + {file = "matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285"}, + {file = "matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2"}, + {file = "matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf"}, + {file = "matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6"}, + {file = "matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42"}, + {file = "matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f"}, + {file = "matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e"}, + {file = "matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f"}, + {file = "matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838"}, + {file = "matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2"}, + {file = "matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921"}, + {file = "matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8"}, + {file = "matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9"}, + {file = "matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4"}, + {file = "matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc"}, + {file = "matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99"}, + {file = "matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d"}, + {file = "matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8"}, + {file = "matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38"}, + {file = "matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d"}, + {file = "matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f"}, + {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b"}, + {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2"}, + {file = "matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716"}, + {file = "matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f"}, + {file = "matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456"}, + {file = "matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe"}, + {file = "matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6"}, + {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c"}, + {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4"}, + {file = "matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf"}, + {file = "matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39"}, + {file = "matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4"}, + {file = "matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358"}, ] [package.dependencies] @@ -1820,11 +1661,11 @@ kiwisolver = ">=1.3.1" numpy = ">=1.23" packaging = ">=20.0" pillow = ">=8" -pyparsing = ">=2.3.1" +pyparsing = ">=3" python-dateutil = ">=2.7" [package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7,<10)"] [[package]] name = "matplotlib-inline" @@ -1841,27 +1682,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "mdit-py-plugins" -version = "0.4.2" -description = "Collection of plugins for markdown-it-py" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"docs\"" -files = [ - {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, - {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, -] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<4.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -1869,7 +1689,7 @@ description = "Collection of plugins for markdown-it-py" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.11\" and extra == \"docs\"" +markers = "extra == \"docs\"" files = [ {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, @@ -1898,19 +1718,74 @@ files = [ [[package]] name = "mistune" -version = "3.1.4" +version = "3.2.0" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d"}, - {file = "mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164"}, + {file = "mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1"}, + {file = "mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a"}, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +description = "ml_dtypes is a stand-alone implementation of several NumPy dtype extensions used in machine learning." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c"}, + {file = "ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a"}, + {file = "ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270"}, + {file = "ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2"}, + {file = "ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90"}, + {file = "ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040"}, + {file = "ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483"}, + {file = "ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb"}, + {file = "ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de"}, + {file = "ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac"}, + {file = "ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900"}, + {file = "ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff"}, + {file = "ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7"}, + {file = "ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460"}, + {file = "ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48"}, + {file = "ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b"}, + {file = "ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d"}, + {file = "ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328"}, + {file = "ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175"}, + {file = "ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6"}, + {file = "ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d"}, + {file = "ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298"}, + {file = "ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6"}, + {file = "ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1"}, + {file = "ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22"}, + {file = "ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465"}, + {file = "ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f"}, + {file = "ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56"}, + {file = "ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049"}, + {file = "ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9"}, + {file = "ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7"}, + {file = "ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf"}, + {file = "ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1"}, + {file = "ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d"}, + {file = "ml_dtypes-0.5.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0"}, + {file = "ml_dtypes-0.5.4-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24"}, + {file = "ml_dtypes-0.5.4-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef"}, + {file = "ml_dtypes-0.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2"}, + {file = "ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453"}, ] [package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.11\""} +numpy = [ + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.3", markers = "python_version == \"3.11\""}, +] + +[package.extras] +dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"] [[package]] name = "msgpack" @@ -1986,63 +1861,70 @@ files = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.20.2" description = "Optional static typing for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, - {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, - {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, - {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, - {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, - {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, - {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, - {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, - {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, - {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, - {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, - {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, - {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, - {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, - {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, - {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, - {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, - {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, - {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, - {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, - {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, - {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, - {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, - {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, - {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, - {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, - {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, - {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, - {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, - {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, - {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, - {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, - {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, - {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, - {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, - {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, - {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, - {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, + {file = "mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4"}, + {file = "mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997"}, + {file = "mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14"}, + {file = "mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99"}, + {file = "mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c"}, + {file = "mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd"}, + {file = "mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2"}, + {file = "mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c"}, + {file = "mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3"}, + {file = "mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254"}, + {file = "mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98"}, + {file = "mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac"}, + {file = "mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67"}, + {file = "mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100"}, + {file = "mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b"}, + {file = "mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4"}, + {file = "mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6"}, + {file = "mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066"}, + {file = "mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102"}, + {file = "mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9"}, + {file = "mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58"}, + {file = "mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026"}, + {file = "mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943"}, + {file = "mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517"}, + {file = "mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15"}, + {file = "mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee"}, + {file = "mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f"}, + {file = "mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330"}, + {file = "mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30"}, + {file = "mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924"}, + {file = "mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb"}, + {file = "mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc"}, + {file = "mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558"}, + {file = "mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8"}, + {file = "mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3"}, + {file = "mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609"}, + {file = "mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2"}, + {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c"}, + {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744"}, + {file = "mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6"}, + {file = "mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec"}, + {file = "mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382"}, + {file = "mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563"}, + {file = "mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665"}, ] [package.dependencies] +librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" +pathspec = ">=1.0.0" +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.15\""} [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] +native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] reports = ["lxml"] [[package]] @@ -2087,46 +1969,45 @@ testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0, [[package]] name = "nbclient" -version = "0.10.2" +version = "0.10.4" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = true -python-versions = ">=3.9.0" +python-versions = ">=3.10.0" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, - {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, + {file = "nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440"}, + {file = "nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9"}, ] [package.dependencies] jupyter-client = ">=6.1.12" jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbformat = ">=5.1" +nbformat = ">=5.1.3" traitlets = ">=5.4" [package.extras] dev = ["pre-commit"] -docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] +docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.1.0)", "pytest (>=9.0.1,<10)", "pytest-asyncio (>=1.3.0)", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.1.0)", "pytest (>=9.0.1,<10)", "pytest-asyncio (>=1.3.0)", "pytest-cov (>=4.0)", "testpath", "xmltodict"] [[package]] name = "nbconvert" -version = "7.16.6" -description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." +version = "7.17.1" +description = "Convert Jupyter Notebooks (.ipynb files) to other formats." optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b"}, - {file = "nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582"}, + {file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"}, + {file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"}, ] [package.dependencies] beautifulsoup4 = "*" bleach = {version = "!=5.0.0", extras = ["css"]} defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} jinja2 = ">=3.0" jupyter-core = ">=4.7" jupyterlab-pygments = "*" @@ -2140,8 +2021,8 @@ pygments = ">=2.4.1" traitlets = ">=5.1" [package.extras] -all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] -docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +all = ["flaky", "intersphinx-registry", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (>=5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] +docs = ["intersphinx-registry", "ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (>=5.0.2)", "sphinxcontrib-spelling"] qtpdf = ["pyqtwebengine (>=5.15)"] qtpng = ["pyqtwebengine (>=5.15)"] serve = ["tornado (>=6.1)"] @@ -2173,15 +2054,15 @@ test = ["pep440", "pre-commit", "pytest", "testpath"] [[package]] name = "nbsphinx" -version = "0.9.7" +version = "0.9.8" description = "Jupyter Notebook Tools for Sphinx" optional = true -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "nbsphinx-0.9.7-py3-none-any.whl", hash = "sha256:7292c3767fea29e405c60743eee5393682a83982ab202ff98f5eb2db02629da8"}, - {file = "nbsphinx-0.9.7.tar.gz", hash = "sha256:abd298a686d55fa894ef697c51d44f24e53aa312dadae38e82920f250a5456fe"}, + {file = "nbsphinx-0.9.8-py3-none-any.whl", hash = "sha256:92d95ee91784e56bc633b60b767a6b6f23a0445f891e24641ce3c3f004759ccf"}, + {file = "nbsphinx-0.9.8.tar.gz", hash = "sha256:d0765908399a8ee2b57be7ae881cf2ea58d66db3af7bbf33e6eb48f83bea5495"}, ] [package.dependencies] @@ -2189,304 +2070,227 @@ docutils = ">=0.18.1" jinja2 = "*" nbconvert = ">=5.3,<5.4 || >5.4" nbformat = "*" -sphinx = ">=1.8,<8.2" +sphinx = ">=1.8,<8.2.0 || >8.2.0,<8.2.1 || >8.2.1" traitlets = ">=5" [[package]] name = "networkx" -version = "3.2.1" +version = "3.6.1" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\"" -files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, -] - -[package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "networkx" -version = "3.5" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.11" +python-versions = "!=3.14.1,>=3.11" groups = ["main"] -markers = "python_version >= \"3.11\"" files = [ - {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, - {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, + {file = "networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762"}, + {file = "networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509"}, ] [package.extras] +benchmarking = ["asv", "virtualenv"] default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "iplotx (>=0.9.0)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] +release = ["build (>=0.10)", "changelist (==0.5)", "twine (>=4.0)", "wheel (>=0.40)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] test-extras = ["pytest-mpl", "pytest-randomly"] [[package]] name = "numpy" -version = "2.0.2" +version = "2.4.4" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.11" groups = ["main"] -markers = "python_version < \"3.11\"" -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - -[[package]] -name = "numpy" -version = "2.3.3" -description = "Fundamental package for array computing in Python" +files = [ + {file = "numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db"}, + {file = "numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0"}, + {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015"}, + {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40"}, + {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d"}, + {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502"}, + {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd"}, + {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5"}, + {file = "numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e"}, + {file = "numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e"}, + {file = "numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8"}, + {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121"}, + {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e"}, + {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44"}, + {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d"}, + {file = "numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827"}, + {file = "numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a"}, + {file = "numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c"}, + {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103"}, + {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83"}, + {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed"}, + {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959"}, + {file = "numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed"}, + {file = "numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf"}, + {file = "numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d"}, + {file = "numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5"}, + {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7"}, + {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93"}, + {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e"}, + {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40"}, + {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e"}, + {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392"}, + {file = "numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008"}, + {file = "numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8"}, + {file = "numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"}, + {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"}, + {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"}, + {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"}, + {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"}, + {file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"}, + {file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"}, + {file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"}, + {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"}, + {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"}, + {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"}, + {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"}, + {file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"}, + {file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"}, + {file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119"}, + {file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"}, +] + +[[package]] +name = "opt-einsum" +version = "3.4.0" +description = "Path optimization of einsum functions." optional = false -python-versions = ">=3.11" +python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d"}, - {file = "numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569"}, - {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f"}, - {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125"}, - {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48"}, - {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6"}, - {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa"}, - {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30"}, - {file = "numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57"}, - {file = "numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa"}, - {file = "numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7"}, - {file = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}, - {file = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}, - {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}, - {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}, - {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}, - {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}, - {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}, - {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}, - {file = "numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}, - {file = "numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}, - {file = "numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}, - {file = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}, - {file = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}, - {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}, - {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}, - {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}, - {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}, - {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}, - {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}, - {file = "numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}, - {file = "numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}, - {file = "numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}, - {file = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}, - {file = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}, - {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}, - {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}, - {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}, - {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}, - {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}, - {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}, - {file = "numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}, - {file = "numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}, - {file = "numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}, - {file = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}, - {file = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}, - {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}, - {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}, - {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}, - {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}, - {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}, - {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}, - {file = "numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}, - {file = "numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}, - {file = "numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}, - {file = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}, - {file = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}, - {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}, - {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}, - {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}, - {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}, - {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}, - {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}, - {file = "numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}, - {file = "numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}, - {file = "numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db"}, - {file = "numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc"}, - {file = "numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}, +files = [ + {file = "opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd"}, + {file = "opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac"}, ] [[package]] name = "packaging" -version = "25.0" +version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.2" description = "Powerful data structures for data analysis, time series, and statistics" optional = true -python-versions = ">=3.9" +python-versions = ">=3.11" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, - {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, - {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, - {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, - {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, - {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, - {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, - {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, - {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, - {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, - {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, - {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, - {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, - {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, - {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, - {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, - {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, - {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, - {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, - {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, - {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, - {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, - {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, - {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, - {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, - {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, - {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, - {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, - {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, - {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, - {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, - {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, - {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, - {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, - {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, - {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, - {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, - {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, - {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, - {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, - {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, - {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, - {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, - {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, - {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, - {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, - {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, - {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, - {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, - {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, - {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, - {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, - {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, - {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, - {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, + {file = "pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0"}, + {file = "pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c"}, + {file = "pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb"}, + {file = "pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76"}, + {file = "pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e"}, + {file = "pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa"}, + {file = "pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df"}, + {file = "pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f"}, + {file = "pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18"}, + {file = "pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14"}, + {file = "pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d"}, + {file = "pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f"}, + {file = "pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab"}, + {file = "pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d"}, + {file = "pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4"}, + {file = "pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd"}, + {file = "pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3"}, + {file = "pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668"}, + {file = "pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9"}, + {file = "pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e"}, + {file = "pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d"}, + {file = "pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39"}, + {file = "pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991"}, + {file = "pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84"}, + {file = "pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235"}, + {file = "pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d"}, + {file = "pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7"}, + {file = "pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677"}, + {file = "pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172"}, + {file = "pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1"}, + {file = "pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0"}, + {file = "pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b"}, + {file = "pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288"}, + {file = "pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c"}, + {file = "pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535"}, + {file = "pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db"}, + {file = "pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53"}, + {file = "pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf"}, + {file = "pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb"}, + {file = "pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d"}, + {file = "pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8"}, + {file = "pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd"}, + {file = "pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d"}, + {file = "pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660"}, + {file = "pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702"}, + {file = "pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276"}, + {file = "pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482"}, + {file = "pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043"}, ] [package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, -] +numpy = {version = ">=1.26.0", markers = "python_version < \"3.14\""} python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" +tzdata = {version = "*", markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\""} [package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)", "beautifulsoup4 (>=4.12.3)", "bottleneck (>=1.4.2)", "fastparquet (>=2024.11.0)", "fsspec (>=2024.10.0)", "gcsfs (>=2024.10.0)", "html5lib (>=1.1)", "hypothesis (>=6.116.0)", "jinja2 (>=3.1.5)", "lxml (>=5.3.0)", "matplotlib (>=3.9.3)", "numba (>=0.60.0)", "numexpr (>=2.10.2)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "psycopg2 (>=2.9.10)", "pyarrow (>=13.0.0)", "pyiceberg (>=0.8.1)", "pymysql (>=1.1.1)", "pyreadstat (>=1.2.8)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)", "python-calamine (>=0.3.0)", "pytz (>=2024.2)", "pyxlsb (>=1.0.10)", "qtpy (>=2.4.2)", "s3fs (>=2024.10.0)", "scipy (>=1.14.1)", "tables (>=3.10.1)", "tabulate (>=0.9.0)", "xarray (>=2024.10.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)", "zstandard (>=0.23.0)"] +aws = ["s3fs (>=2024.10.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.4.2)"] +compression = ["zstandard (>=0.23.0)"] +computation = ["scipy (>=1.14.1)", "xarray (>=2024.10.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "python-calamine (>=0.3.0)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)"] +feather = ["pyarrow (>=13.0.0)"] +fss = ["fsspec (>=2024.10.0)"] +gcp = ["gcsfs (>=2024.10.0)"] +hdf5 = ["tables (>=3.10.1)"] +html = ["beautifulsoup4 (>=4.12.3)", "html5lib (>=1.1)", "lxml (>=5.3.0)"] +iceberg = ["pyiceberg (>=0.8.1)"] +mysql = ["SQLAlchemy (>=2.0.36)", "pymysql (>=1.1.1)"] +output-formatting = ["jinja2 (>=3.1.5)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=13.0.0)"] +performance = ["bottleneck (>=1.4.2)", "numba (>=0.60.0)", "numexpr (>=2.10.2)"] +plot = ["matplotlib (>=3.9.3)"] +postgresql = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "psycopg2 (>=2.9.10)"] +pyarrow = ["pyarrow (>=13.0.0)"] +spss = ["pyreadstat (>=1.2.8)"] +sql-other = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)"] +test = ["hypothesis (>=6.116.0)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)"] +timezone = ["pytz (>=2024.2)"] +xml = ["lxml (>=5.3.0)"] [[package]] name = "pandoc" @@ -2520,33 +2324,38 @@ files = [ [[package]] name = "parso" -version = "0.8.5" +version = "0.8.6" description = "A Python Parser" optional = true python-versions = ">=3.6" groups = ["main"] markers = "extra == \"latex\"" files = [ - {file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"}, - {file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"}, + {file = "parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff"}, + {file = "parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd"}, ] [package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +qa = ["flake8 (==5.0.4)", "types-setuptools (==67.2.0.1)", "zuban (==0.5.1)"] testing = ["docopt", "pytest"] [[package]] name = "pathspec" -version = "0.12.1" +version = "1.1.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, ] +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] + [[package]] name = "pexpect" version = "4.9.0" @@ -2554,7 +2363,7 @@ description = "Pexpect allows easy control of interactive console applications." optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"latex\" and sys_platform != \"win32\" and (sys_platform != \"win32\" and sys_platform != \"emscripten\" or python_version < \"3.11\")" +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" and extra == \"latex\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -2565,148 +2374,127 @@ ptyprocess = ">=0.5" [[package]] name = "pillow" -version = "11.3.0" -description = "Python Imaging Library (Fork)" +version = "12.2.0" +description = "Python Imaging Library (fork)" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, - {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}, - {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}, - {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}, - {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}, - {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}, - {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}, - {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}, - {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}, - {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}, - {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}, - {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}, - {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}, - {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}, - {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}, - {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}, - {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}, - {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}, - {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}, - {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}, - {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}, - {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}, - {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"}, - {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"}, - {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"}, - {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}, - {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}, + {file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"}, + {file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c"}, + {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3"}, + {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa"}, + {file = "pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032"}, + {file = "pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5"}, + {file = "pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024"}, + {file = "pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab"}, + {file = "pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176"}, + {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b"}, + {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909"}, + {file = "pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808"}, + {file = "pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60"}, + {file = "pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe"}, + {file = "pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5"}, + {file = "pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780"}, + {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5"}, + {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"}, + {file = "pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940"}, + {file = "pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5"}, + {file = "pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414"}, + {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c"}, + {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2"}, + {file = "pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c"}, + {file = "pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795"}, + {file = "pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3"}, + {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9"}, + {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795"}, + {file = "pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e"}, + {file = "pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b"}, + {file = "pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06"}, + {file = "pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b"}, + {file = "pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4"}, + {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4"}, + {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea"}, + {file = "pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24"}, + {file = "pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98"}, + {file = "pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"}, + {file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"}, + {file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"}, + {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"}, + {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"}, + {file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"}, + {file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"}, + {file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"}, + {file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"}, + {file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"}, + {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"}, + {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"}, + {file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"}, + {file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"}, + {file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"}, + {file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -test-arrow = ["pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.9.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, + {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, + {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, ] -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - [[package]] name = "pluggy" version = "1.6.0" @@ -2725,25 +2513,22 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "plumbum" -version = "1.9.0" +version = "1.10.0" description = "Plumbum: shell combinators library" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5"}, - {file = "plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219"}, + {file = "plumbum-1.10.0-py3-none-any.whl", hash = "sha256:9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8"}, + {file = "plumbum-1.10.0.tar.gz", hash = "sha256:f8cbf0ecec0b73ff4e349398b65112a9e3f9300e7dc019001217dcc148d5c97c"}, ] [package.dependencies] pywin32 = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} [package.extras] -dev = ["coverage[toml]", "paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] -docs = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=1.0.0)"] ssh = ["paramiko"] -test = ["coverage[toml]", "paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] [[package]] name = "ply" @@ -2781,7 +2566,7 @@ description = "Run a subprocess in a pseudo terminal" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"latex\" and sys_platform != \"win32\" and (sys_platform != \"win32\" and sys_platform != \"emscripten\" or python_version < \"3.11\")" +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" and extra == \"latex\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -2817,27 +2602,27 @@ files = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] markers = "implementation_name == \"pypy\"" files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] markers = {main = "extra == \"latex\" or extra == \"docs\""} @@ -2846,15 +2631,15 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.2" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e"}, - {file = "pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6"}, + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, ] [package.extras] @@ -2874,12 +2659,10 @@ files = [ [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1" packaging = ">=20" pluggy = ">=1.5,<2" pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] @@ -3012,76 +2795,83 @@ six = ">=1.5" [[package]] name = "python-rapidjson" -version = "1.21" +version = "1.23" description = "Python wrapper around rapidjson" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "python_rapidjson-1.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e1c914b2c7952dadc69d1de54a3f147e27e00d45c70c72fe5a35fb58d0c3437"}, - {file = "python_rapidjson-1.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127935afaf90ffaa721836ad64e58a7cd6838ef9d9a3c670f2769e045824aac1"}, - {file = "python_rapidjson-1.21-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e6d0266549811757523245bcd7ce046cbedf653383f28f412103c759feaf3ef"}, - {file = "python_rapidjson-1.21-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b81bc8189a354de509dc1ca353c847571013dc46dc212660b327dd5303315e5"}, - {file = "python_rapidjson-1.21-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9f90fe043904efeec69a74787c87db26df657f4c722c19db7bfd0964c352f41"}, - {file = "python_rapidjson-1.21-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:150177509c71e9e782930e88e79078323dcca09acf01440f670f0b2f9c2958d4"}, - {file = "python_rapidjson-1.21-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7958912ccd14c2e18d53b6ee766c273f8ee2a75a2f2df47c31e16b9b5fe826e9"}, - {file = "python_rapidjson-1.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ca49342dd54b23ffd5a2b150a43cdc8b2bb10e2c88935e9f30fc58ef6346fd9"}, - {file = "python_rapidjson-1.21-cp310-cp310-win32.whl", hash = "sha256:673c545dd5f8e87ccab51f2af83f30772ec6349295a13aef24b5b3e828fc086c"}, - {file = "python_rapidjson-1.21-cp310-cp310-win_amd64.whl", hash = "sha256:8e5322d8c11d461da1cd2074ee2bce9613b29e5873f46c2b486115d7894e32cd"}, - {file = "python_rapidjson-1.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78103db8a58fbcff54ee475426fc9d3aab5257f6c2eb3ffe1c769b9ad9e0878f"}, - {file = "python_rapidjson-1.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7eafd20dfb972d16f7284e1f0a02fc7603b657f847dab3308c129fb41f214030"}, - {file = "python_rapidjson-1.21-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08433801ddd96f2f2a03d17aeab8121adecb4e8cc119ae5dc05a1c441fb1ef58"}, - {file = "python_rapidjson-1.21-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54ab944647aaf1d0c3fd26a0756edc87904b640a635fd40980b85faa80bdba70"}, - {file = "python_rapidjson-1.21-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f47e5e126a2052fbb8a2f3e6c40b7ab52334e57ed8665733c277027f6caa6fa"}, - {file = "python_rapidjson-1.21-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06fc84a13300be41dc075eded0d7bab7d024e8feb0685f43e958892ad4f6e5ad"}, - {file = "python_rapidjson-1.21-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8be8d08c33e9e8a6b83a4b34777b900f85ce5e6a951c562cb8c6e0966f2f604"}, - {file = "python_rapidjson-1.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:925cc765c2a4ab9aec92a163eca1d0572a4bf5ef7ef2e3d44e3808424454c5ec"}, - {file = "python_rapidjson-1.21-cp311-cp311-win32.whl", hash = "sha256:47a9fc6b1c76946f9af875cfcceb32f08c5a6b1677500174397f042a9bbd5d1e"}, - {file = "python_rapidjson-1.21-cp311-cp311-win_amd64.whl", hash = "sha256:a12c6b412f0b77589912159eea1a3385710ed1dc0d01259f2f467d5ec6b2413b"}, - {file = "python_rapidjson-1.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:354ac34828a242eec1896901fabe59d14adab7a5054d09a8bdaed4c6ee510b22"}, - {file = "python_rapidjson-1.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ddd846b095fad357e4e4c5071e874f9c100c2e5b9765abcc7d71254820251c20"}, - {file = "python_rapidjson-1.21-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7faf4211354c1db9b21934d723f3c7db1d611ec3013458dc2fe1a732aa100d3e"}, - {file = "python_rapidjson-1.21-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46ea95e9292260fd8174966eca05fad08b43ca0f5df1ccfab9796edb0868d8eb"}, - {file = "python_rapidjson-1.21-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1cac86399cfbaf24e72aef6eed1114180a72ba0242c7153f89d874a81fb83c6"}, - {file = "python_rapidjson-1.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a4afbf54eb60dc56d4c2b6fac5d24709e8a5928baaeff0669c00950961c7a65"}, - {file = "python_rapidjson-1.21-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e8d4d1285c697325ad1b5300866176d190625bbdd85dce963300fad5bfaf166"}, - {file = "python_rapidjson-1.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1525778bc9b9cee9b8f3058e0435f0e4ff03e2d8c3883893ebcaf58680316964"}, - {file = "python_rapidjson-1.21-cp312-cp312-win32.whl", hash = "sha256:b5717ddb9788ca00e0f97546cbdd3cfd0c25e52ab3bfed0951c7898e7f37cc60"}, - {file = "python_rapidjson-1.21-cp312-cp312-win_amd64.whl", hash = "sha256:409704e52ad25df661265dbcb3a512a2146c98f72f362adc157e9b595704e1be"}, - {file = "python_rapidjson-1.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:863208f50b848b8e10aefcf0da2a67ce38a1044a2ebf83d6355f16f0c7865d4f"}, - {file = "python_rapidjson-1.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:608ade2fff2788821f25e70af4b1bfbf9ab2b2dc8ad47c91616935c5d31e3b72"}, - {file = "python_rapidjson-1.21-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf6377c4e9636e20b403776585903d1ff52613a419c5f8453032146f0422634"}, - {file = "python_rapidjson-1.21-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74cba817cd9b16a3a54e7ddb3b59980c338c9a28900124502215dcba5db5a276"}, - {file = "python_rapidjson-1.21-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3194bde069c3f03f3c425de7037cca37d337b6c7ac30f42cd2f17e15f1c4da3a"}, - {file = "python_rapidjson-1.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b663932dc7548567028fb735bf4092b50dc373822dc9756a9ade6513541013de"}, - {file = "python_rapidjson-1.21-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fcb094ecca3d892f1dbd318251579ef66e2b2f06a2a24414ad71c31931d7ffff"}, - {file = "python_rapidjson-1.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f4cdb4f77b08b7ae121b7ca27ed2d57dcce17f8cd9f1d4593c954c982aba18ba"}, - {file = "python_rapidjson-1.21-cp313-cp313-win32.whl", hash = "sha256:be63c0ef87bf26059ee77f5de21d84a1be3659cb6baa8484c237ffe17a8beb56"}, - {file = "python_rapidjson-1.21-cp313-cp313-win_amd64.whl", hash = "sha256:38307a2ef4fddbdc18806a796201fe668c30e60de7cd795943a3f7fd39535c8b"}, - {file = "python_rapidjson-1.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c629e9ad36c6b37850ccef73ef99e06b6f1622ceb7c1f8e2f316529e5fb664b"}, - {file = "python_rapidjson-1.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d4f99a48ba81e463176384d2b82de3f3480fb6acaf6dc283e5cb27bde4f5bb1"}, - {file = "python_rapidjson-1.21-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f9d9d4d97deef3b9bdc88b661691a56f4aa87f5be2624338534ec939a4c7dec"}, - {file = "python_rapidjson-1.21-cp39-cp39-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18baeaf6e6a8923e3967030a187791a7c2191a41bf0183d7e56fdc9bdbafc707"}, - {file = "python_rapidjson-1.21-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a9a51d4b4c25eb0f4b2395bdbac937b0b76ec6997c85407ef63f88d700e7ae3"}, - {file = "python_rapidjson-1.21-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9f03088e7c400465ecca19bf32df253b07ec5def8f3cb2b8f75b47827b231258"}, - {file = "python_rapidjson-1.21-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f2c1c1c799138ce924da3e7808304905bacd8ab4ee08e3bafbbd71c83e57839a"}, - {file = "python_rapidjson-1.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fd953da0e05cdf42ed5fc6fcfa7f4900b224ad70ba2ff9cce84f0d60bdabd45c"}, - {file = "python_rapidjson-1.21-cp39-cp39-win32.whl", hash = "sha256:97a02836c859bb71eb6e791ddd35c40d84f809948c7ee212c12842e62887c321"}, - {file = "python_rapidjson-1.21-cp39-cp39-win_amd64.whl", hash = "sha256:af85273105309df8b3fcb71c1919c648e21c8aed4315ea5bae238c7c8cae2281"}, - {file = "python_rapidjson-1.21.tar.gz", hash = "sha256:4d0dd9cf1fcb6f4bf79ee606e6e9be4cfa598f273b91338a6974b6a99309a1e6"}, -] - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"docs\"" -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, + {file = "python_rapidjson-1.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbb0958c5132d3def9d2be178dee45a2c41071573558348091bc539fe2671cd2"}, + {file = "python_rapidjson-1.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f9314d155f342bc95912a0db9c1087a8d043ecceccaff33c21d257de4376f20"}, + {file = "python_rapidjson-1.23-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:332c972ceaaa7faff559d370702682ed20bbe36fdab477679b157a0f3c0391d5"}, + {file = "python_rapidjson-1.23-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f92eed9120b83acd7be9df932318de1c0d6e40d01594e9d855fef41cdb55c7"}, + {file = "python_rapidjson-1.23-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b79f9a4af92188098008623b17743bf8a300abeacb383d0424d070b4fbfbf83e"}, + {file = "python_rapidjson-1.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e60f8aa275d403a407abeb31a6bf319826c9f6b565a569a8dfb930fe0a2cca1a"}, + {file = "python_rapidjson-1.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4c871e878c30f87077249d65ed65c0d5c24dd93c412f487cd95f648bec209909"}, + {file = "python_rapidjson-1.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fc2b0c06b8e25528bbdeef45fda1ea373f62c277eb8f77518646e82ecc9b0718"}, + {file = "python_rapidjson-1.23-cp310-cp310-win32.whl", hash = "sha256:027c2bd096ac505c52a7ff1a7f2860b0ee451bc75e69777b49fd3d842efff544"}, + {file = "python_rapidjson-1.23-cp310-cp310-win_amd64.whl", hash = "sha256:6516b8538b2081bbaf737326dba472b354d1bf873adbb155fc7c735916761afb"}, + {file = "python_rapidjson-1.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6da3f5726c4ca30595b9ccab0e0bcfc18b624c61af5f6e3877b80e00bef62995"}, + {file = "python_rapidjson-1.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:316e1541c98af3af4bc0908cb9baaa44019dcedcad69a9944e2237607deb3ce5"}, + {file = "python_rapidjson-1.23-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e771315f7abd9c345c95f82dc013b6d60f041b6d708b26585a84e011dd5e092d"}, + {file = "python_rapidjson-1.23-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ea4f914033440f4474931838b93890567f9214e26dd89d741b4260d1e243e56"}, + {file = "python_rapidjson-1.23-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0702eae42704948320df851a35371db3e7a9494c123896cbb069cc8b56e3c4e"}, + {file = "python_rapidjson-1.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0cdc820cf2c7b5f4ac5ba4d697f049e0b93d2e9776b53b02a77051472d38cede"}, + {file = "python_rapidjson-1.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:016a35f2d93ee6be13c938ab4bc30dcf525c4ee6e4c3fbd5c75c2320fccea875"}, + {file = "python_rapidjson-1.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40990adbf47bcb7f80b96b586e4ce114b497c19516a8e6a0700d61447ca8d28b"}, + {file = "python_rapidjson-1.23-cp311-cp311-win32.whl", hash = "sha256:8a2dc5faba744b643901489e82f037cef099be92b9d4d0eea597c1d5aea910be"}, + {file = "python_rapidjson-1.23-cp311-cp311-win_amd64.whl", hash = "sha256:6d2db055ed9728071117a8395ebb1552b937c3d5bbcf6f610420f9b6f926c654"}, + {file = "python_rapidjson-1.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56e557fb6a7d7babfeb8ebaa4d096d4ce127477ecf46fe7de7f1edf2e1d8e4d6"}, + {file = "python_rapidjson-1.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d8e107121f5c1e98cb4f0e5fde443e0f66b45eadc3269bc2416e31261535f444"}, + {file = "python_rapidjson-1.23-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc45ef1f725b3a9a27cdedcf9997f1f8c5a523ac03882d3925c6f764b33e5e1b"}, + {file = "python_rapidjson-1.23-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f87de7b994d65da2327fffdc5d3d7166782e3ca99c76c0560c8a7f1e109a5b54"}, + {file = "python_rapidjson-1.23-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6067810f0fd57713ec733b0b6ae265ef169e13b2ce04a4938b1807cddd8b4db4"}, + {file = "python_rapidjson-1.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83306643cf31c0833b226d4317e8738b1b5ed4371e310f3c552be994c01a3df0"}, + {file = "python_rapidjson-1.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:13797fdcd43e558b81d3344c637bf878878fd6dede84409769d6910f8f6a9024"}, + {file = "python_rapidjson-1.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad674edb9dfe8181fb704a14149e5eb30ae179a92021484ebe8935b8d0f88495"}, + {file = "python_rapidjson-1.23-cp312-cp312-win32.whl", hash = "sha256:0c64958048ce714ccc42c659ef954812ed6de79fe4800322b3926ca46f60ffd9"}, + {file = "python_rapidjson-1.23-cp312-cp312-win_amd64.whl", hash = "sha256:cbb0a67a5330d28279a5c3b68068e901deedcd21ade0ec23be1bcc250948ae62"}, + {file = "python_rapidjson-1.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f13fd870fad2758c59a64e7b7efb2f037d447f31913a02569cf3e1e0efda1e29"}, + {file = "python_rapidjson-1.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c8e16a0be2cb9736e92e61604bea62db8575da2946732bec63980b990503bfe"}, + {file = "python_rapidjson-1.23-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7a333587073d576eef6bb685c0d25ea32316ca1dbf44adf6724596dad347658"}, + {file = "python_rapidjson-1.23-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82e8be36193fc2c7769a6ce0678f8c494480f76dc84c8f387a24703f32b4b29b"}, + {file = "python_rapidjson-1.23-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b918f1672bfc08a93680b5bc70f2d60a4c2c001d29d105b3013865235c1d88fa"}, + {file = "python_rapidjson-1.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171b173bfc1ce4896472ebc78c8caf1c77403cd7db5709370f1d49014a64dce4"}, + {file = "python_rapidjson-1.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1ad6f00c3eb7c0b8f7a01efc7d9cc2bb7ee7a50029052950006cf4990f6a1869"}, + {file = "python_rapidjson-1.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3981a92f1557fbaa0e6ae301fd0e27ddc1582a14316575d349a1354b67ff3668"}, + {file = "python_rapidjson-1.23-cp313-cp313-win32.whl", hash = "sha256:0c9d1b98f2def374bc2f29a94230dbb0c17a84b66187ad5183b3d43c68296b2f"}, + {file = "python_rapidjson-1.23-cp313-cp313-win_amd64.whl", hash = "sha256:0ae97758fd48da3ffbb631a4769dafe5a64e0e7091a64be0b8bc081b66a447e3"}, + {file = "python_rapidjson-1.23-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8c0a23345d2a505e0ba3378b2568ba803eb41e2a78b7847221ab924f16e0cd17"}, + {file = "python_rapidjson-1.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3dcea6b19b3eee6a9dc5d8c1a20c828fa18d9d265893ce4c1858e8a85af944aa"}, + {file = "python_rapidjson-1.23-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8272a1e89afa7b5be0d191c024d832262b5d1b479585253b4ebdec8181f54083"}, + {file = "python_rapidjson-1.23-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b0d4f24dc252edb73438241719020a57fc367a5607dcb776e80d43e0a22a779"}, + {file = "python_rapidjson-1.23-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bffe4bb5bd0a51a9d937cad299530aaa82b6fe102aab095504963e98b7714a47"}, + {file = "python_rapidjson-1.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:950eaa5f54d73b1963b7562888b251ec772812a3be8af157adde927462889fbb"}, + {file = "python_rapidjson-1.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb8e40cd40ecccd8d36e795eef8f197fee688ba1611f14c30f9b0ae64576a844"}, + {file = "python_rapidjson-1.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1432c363920e0fd5763cd802eb7797bddb990fc67578bbfd7d0c0b7b4837b2b"}, + {file = "python_rapidjson-1.23-cp314-cp314-win32.whl", hash = "sha256:882e67c03366a5d0a71531cf8e8f89a43fd774081080ce18b9880604696b10aa"}, + {file = "python_rapidjson-1.23-cp314-cp314-win_amd64.whl", hash = "sha256:11fa5f12404220e3bf9931add698ae599083924fb49406a77a59cec62e39378e"}, + {file = "python_rapidjson-1.23-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8fd85ff2f1f18a310f7f257a3e995d18c2238ee3a637eec4a556fc692c4f8cf7"}, + {file = "python_rapidjson-1.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:26a2e3aa6fc2ebc7e587cc571f1c794b1bc58faaebf235d41987cd098ea63e7b"}, + {file = "python_rapidjson-1.23-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20532558f71b834b971985517a1f63372a5824ca9ff0da421a1a66de8f91bcf4"}, + {file = "python_rapidjson-1.23-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9ebd5aca46a9b53f1a074bcfcc0f9ecc37dace55d328ea6e5e5ff08daf3c710"}, + {file = "python_rapidjson-1.23-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c28b4a8e5e8ea8a9fa7325e5b5130ccb88f78e17b1cbf59a7a2edf6f6b4c55f9"}, + {file = "python_rapidjson-1.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:58a5a448ca3131433f8141a5da132ae9ae41629bc38a32709a10c56bba69689e"}, + {file = "python_rapidjson-1.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0b9e3e58fc2ea7e1a06aa8856b0163a60b60e8b12f10678e5fcf8ba982c9de64"}, + {file = "python_rapidjson-1.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:965bbe74a1dd930a4e912b5064cf7cf646e2eefbbfba7c3d80dce397b3899db0"}, + {file = "python_rapidjson-1.23-cp314-cp314t-win32.whl", hash = "sha256:f0c520c81d53d7fc0e8d1e9213e2585b0c657513218575fad63421f0166c4aa3"}, + {file = "python_rapidjson-1.23-cp314-cp314t-win_amd64.whl", hash = "sha256:cac6c1166831c7984e27f42048c27befe4ebb3185554f5692e4812c899d94113"}, + {file = "python_rapidjson-1.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:938d1bd7448151732aa7c70b251dbe9b7b3246a1b8b80c4110e86903e450a5f9"}, + {file = "python_rapidjson-1.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:023ad77d11fd8023c03a038a4dbf734d6d54b74d9fa41414d93d0f696a77437d"}, + {file = "python_rapidjson-1.23-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe29411bbb63a15ea8c0003cbc82c185a4cf9602527d0fb026ccda10e440893"}, + {file = "python_rapidjson-1.23-cp39-cp39-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b90c2e51b3050062df91992c5fcdaf89def37450c41b067639a19d9aaa5fc85e"}, + {file = "python_rapidjson-1.23-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8956da1444a45ee5c4596b1574c50d6dc09a0874f388b975b726ddd0c4068c16"}, + {file = "python_rapidjson-1.23-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29270878291beb42b6ac9abead70323780a44494d3451f820ffd81b3812f603c"}, + {file = "python_rapidjson-1.23-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:63c072656336a0046a4176dd2a961dc26fe81a80c39c5908561820763fe3e632"}, + {file = "python_rapidjson-1.23-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71b0d9eb4529610dec6b23d21056be5a6d3fe143bef177f8434df3a647774716"}, + {file = "python_rapidjson-1.23-cp39-cp39-win32.whl", hash = "sha256:f0ceef3cd234754cc224ef73fcd1e9faa88ea278c5c1a578301ab31816345243"}, + {file = "python_rapidjson-1.23-cp39-cp39-win_amd64.whl", hash = "sha256:d50ba0791f5b2d6525cc6d2bbc9f4dca5d9991271ca57c6d1a82d782ce67e31b"}, + {file = "python_rapidjson-1.23.tar.gz", hash = "sha256:0f845daeb26be147f5720a8c410308235092bb4fbb81ea408aa77203e26296fb"}, ] [[package]] @@ -3091,7 +2881,7 @@ description = "Python for Window Extensions" optional = true python-versions = "*" groups = ["main"] -markers = "(sys_platform == \"win32\" or platform_system == \"Windows\") and platform_python_implementation != \"PyPy\" and extra == \"docs\"" +markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\" and extra == \"docs\"" files = [ {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, @@ -3306,33 +3096,29 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "qcs-api-client-common" -version = "0.11.8" +version = "0.17.5" description = "Contains core QCS client functionality and middleware implementations." optional = false -python-versions = "*" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "qcs_api_client_common-0.11.8-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52be55375a650d88d16ff9b0fb9a93bdf4272c0ecaba3d0b75ecdfd8a8adec88"}, - {file = "qcs_api_client_common-0.11.8-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:176d06a1758afbf78a4896ebeecfdeee41dce63950b869fdcb0c56b6665bb16f"}, - {file = "qcs_api_client_common-0.11.8-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:72f7d1154778350b64f9a0b5a5fba078d51b8f55fa8401c2a04d8bf470b7c42f"}, - {file = "qcs_api_client_common-0.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:4173ec105b91cd5ecb37bec629334e5696c80382441729b1932f5840ff492209"}, - {file = "qcs_api_client_common-0.11.8-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cb7efcd32cee06c874a70da0de857c0647c6f5a3e8815834ef433caeb4ebdc04"}, - {file = "qcs_api_client_common-0.11.8-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a013c240d1e0f47805d346cd88294a1cf4a806deea84509e8cb0953ca20fa073"}, - {file = "qcs_api_client_common-0.11.8-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bafaf69c8f0d4bd5957a38a92fa99d9af2145787d6233eda13015869991507e1"}, - {file = "qcs_api_client_common-0.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:ddadfc820f1647b964bd7b487ef8cce182f835570ec2e80666be947c265a5fc2"}, - {file = "qcs_api_client_common-0.11.8-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:580061d2906c8966524a22e7cd68db04fa529103a6e027603830ea00202bdf87"}, - {file = "qcs_api_client_common-0.11.8-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b64b685fa8bced69a84f143a82b01ddd10e7a64a9ef2320b375558014d30414e"}, - {file = "qcs_api_client_common-0.11.8-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:41a1c3c1ec3b0f0180d7b18fd3a74d8e09a7a816572fbfcd08ceb0b57efab328"}, - {file = "qcs_api_client_common-0.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:b076e621a3e1294523f8e99dff9a48f06ff3ec3cafb45c04d90197d9e5391bc9"}, - {file = "qcs_api_client_common-0.11.8-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3a6d96a76c40c2b5d866d934edce20adaa7ab98e2342aa67fb1ba772acd2dd00"}, - {file = "qcs_api_client_common-0.11.8-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:37491fadbe62a30e032d117cbf011d423beead81c44f5355634b2070f5dfcd15"}, - {file = "qcs_api_client_common-0.11.8-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f78ac5066070845e541c6e27cb22865bb0d0405cceb0b8e39e4b6e2826e2a6fb"}, - {file = "qcs_api_client_common-0.11.8-cp38-cp38-win_amd64.whl", hash = "sha256:0c4c4c9ea687120256ad79acc007a16ec24afb6513b7e7c023d1e92f6674cee9"}, - {file = "qcs_api_client_common-0.11.8-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c40adfdf1535c8620e5baa71fcc0f0d1c5127c4322bc5b16c7d78f7b72b9c42"}, - {file = "qcs_api_client_common-0.11.8-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:facbc40de88e23c5775b2d11f34fb4762e463d9a43f1dddddd449fb6957c0a28"}, - {file = "qcs_api_client_common-0.11.8-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:132ba3111893dceca926122c6e58edb6294cedfd809b7e01789d87a64279372c"}, - {file = "qcs_api_client_common-0.11.8-cp39-cp39-win_amd64.whl", hash = "sha256:96ebf74879b707ee5b5d2567a16d1c668ba80327ac8e7116e73b682b9e293cec"}, - {file = "qcs_api_client_common-0.11.8.tar.gz", hash = "sha256:cd255c3759caedf7c641b5ba7b62aa25bdb3b32df040d66014315460dbc33309"}, + {file = "qcs_api_client_common-0.17.5-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4b4fd6f8495eaea3c305ed9708d6d6cc15dd92ec227e1bca43521aa855d92239"}, + {file = "qcs_api_client_common-0.17.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:45f17f51344c10ee4dac3486fc6d1753fca0716f79e6de47ccfa76592c11b9a4"}, + {file = "qcs_api_client_common-0.17.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8e9650ed50faed7e3cfeebf4374b27f0f1409acfd022b1fdf525162031eb4983"}, + {file = "qcs_api_client_common-0.17.5-cp310-cp310-win_amd64.whl", hash = "sha256:6a9573dd6ac9a342d8c189386fa2377407d338f25257969911d2136d782d520c"}, + {file = "qcs_api_client_common-0.17.5-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:212d41c6a70584c91b46b970a0de3cbd3a1900aba44a92648813a8053615f28d"}, + {file = "qcs_api_client_common-0.17.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:101951a8eff305869ebd7575cbf3b02909e8d1bf2da0580f7eb49ed9edf3bfa0"}, + {file = "qcs_api_client_common-0.17.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:37aa7f6c76e917890158ba57ef2600b0069aeceefa7d3c74f2c45cfabd6f2fed"}, + {file = "qcs_api_client_common-0.17.5-cp311-cp311-win_amd64.whl", hash = "sha256:deba7add73c9926c5d3589a581f983c05b6a03a65db711c2270342a56d635aac"}, + {file = "qcs_api_client_common-0.17.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2f7f563fbb3498b03faef1517358e8c7f56c43bafbc73fa66c4325fd8e6c6ec7"}, + {file = "qcs_api_client_common-0.17.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3103e61a2302d5dfa688315d29ef0f4f7f234aab4f70cfbc75c73318c2eead62"}, + {file = "qcs_api_client_common-0.17.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6136f786bc5262eaa510e0683a7ade1b8bfc6b2b850e7333a45a80d397c03099"}, + {file = "qcs_api_client_common-0.17.5-cp312-cp312-win_amd64.whl", hash = "sha256:92ef06de9948ffa978231a0412daecdde70b26450d1f51618015887675e2e84e"}, + {file = "qcs_api_client_common-0.17.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e2f63c4bbad2936a54478c2f4f026fca8d6c76462bee34a01854cb9b1bf1e5da"}, + {file = "qcs_api_client_common-0.17.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c9252fcb6ab258c1878e5f978a6983af7941a12defd38304d4d09383641ee9b9"}, + {file = "qcs_api_client_common-0.17.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9480cc4e5d64b16831383608f6251fda94886a65be17da37fa50a41fc57a51c5"}, + {file = "qcs_api_client_common-0.17.5-cp313-cp313-win_amd64.whl", hash = "sha256:c7aaa84479f326ce13728cba1c0f3c3bcf314c6267b3d45e05f63a51b703b239"}, + {file = "qcs_api_client_common-0.17.5.tar.gz", hash = "sha256:6d39b53cc83f11ffebb5f80a1618c1b3ed5a286dbfedb6a0bc6ed8534f32de47"}, ] [package.dependencies] @@ -3340,43 +3126,40 @@ grpc-interceptor = ">=0.15.0" grpcio = ">=1.63.0" httpx = ">=0.27.0" -[package.extras] -dev = ["furo (>=2024.5.6)", "grpcio-testing (>=1.63.0)", "maturin (>=1.5.1)", "mypy (>=1.4.1)", "pytest (>=8.1.1)", "pytest-asyncio (>=0.23.6)", "pytest-clarity (>=1.0.1)", "pytest-mock (>=3.14.0)", "pytest-sugar (>=1.0.0)", "ruff (>=0.3.5)", "sphinx (>=7.3.7)", "sphinx-autoapi (>=3.0.0)", "syrupy (>=4.0.0)", "watchdog[watchmedo] (>=4.0.0)"] - [[package]] name = "qcs-sdk-python" -version = "0.21.19" +version = "0.21.22" description = "Python interface for the QCS Rust SDK" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "qcs_sdk_python-0.21.19-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:37e9b582f251844bc4c118f80eaf91c2dc449edc519c98493d4af3338ded831a"}, - {file = "qcs_sdk_python-0.21.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c696e97db02eac75a946edd01341fb32658fb4e1c05c72d7149f45b50cd2837"}, - {file = "qcs_sdk_python-0.21.19-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:60c9225ba058bdf2bab8f4bb0016a85d248f8dc974d717c2d36e231693054db4"}, - {file = "qcs_sdk_python-0.21.19-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:506d8cf0447f6e71b3a468bbe2bc60be54eb31bcc8f5165c7792c5df7962d67a"}, - {file = "qcs_sdk_python-0.21.19-cp310-cp310-win_amd64.whl", hash = "sha256:e1dc4d4011323c55acb1b8e37409457f7fdb1e51643c6b33b922588b4b9ca2a8"}, - {file = "qcs_sdk_python-0.21.19-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9fb1c2d8fdda64dcab431e0c5c4da3d92d8a0ef0cf3c5e366c23ddc5d60b70e0"}, - {file = "qcs_sdk_python-0.21.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ff06a2addac593d54759192d03ac30ef4818f4c3ef3e08db0eba547fafe78b1"}, - {file = "qcs_sdk_python-0.21.19-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:13764abef25716ccc03d0a2499b761eecc8e310110a6dd0d9cd475e45549b2f5"}, - {file = "qcs_sdk_python-0.21.19-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:70674a0fc61e83f00e794972cbf5386b161622497d7609ef0e131aa25c17df8e"}, - {file = "qcs_sdk_python-0.21.19-cp311-cp311-win_amd64.whl", hash = "sha256:f2844c2892d75599f1fb8ffff7e7ee38f7f357dfca57f178be8f5418af5f1ba4"}, - {file = "qcs_sdk_python-0.21.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e604f7559362235ec209d109324a57fe4d3b84e566ec524282d584d2681f6cb7"}, - {file = "qcs_sdk_python-0.21.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a2919fb3869d3eca07a4e61dd638ce81e4c5eacaa100c855ec6f13b2c90e552c"}, - {file = "qcs_sdk_python-0.21.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b74ce5b676fb9f84b8a6320e240d5f8afda0e0bd4a8c8ed7b00358f588c3b08b"}, - {file = "qcs_sdk_python-0.21.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:36978e53ea4fa3120c8363bf1f81f1915aa095f117c7348c7636ccff1a905aed"}, - {file = "qcs_sdk_python-0.21.19-cp312-cp312-win_amd64.whl", hash = "sha256:5df8a32c4c6f6c042c64e27df964bcfba8b212084e0bcabad28e62e58e8be07e"}, - {file = "qcs_sdk_python-0.21.19-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b66d25013c2b5fea6eb4d5e04b9d9f78384434ae1ecdc80ba7c33d6b3714665e"}, - {file = "qcs_sdk_python-0.21.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7603b4200a3d773c313a85a54d5cfab35f00c3bf43dc0fcfc8a5786262e472bf"}, - {file = "qcs_sdk_python-0.21.19-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:a7814e874dbfd70b4c748e489a88c97b2540ffd09cc4e68427d86bbc943027b3"}, - {file = "qcs_sdk_python-0.21.19-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:5738f6cbf7d3c23938cb4dd98640566819b6c2eda54e58d78158cba9dc81dd5c"}, - {file = "qcs_sdk_python-0.21.19-cp38-cp38-win_amd64.whl", hash = "sha256:f0f907e13dc6afc6ac1ebfd960595fce91e9d94ca0308f1a3d6b4d5a48cb9cef"}, - {file = "qcs_sdk_python-0.21.19-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:39cb2bfb3be9e7a58f138c5d78b15c7d2f4196f62db896d43a8c50bd5079551d"}, - {file = "qcs_sdk_python-0.21.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0febee697e78dc0a84aee62cc7b378b2afeaa1dc3bc0c5c5964266831afc910c"}, - {file = "qcs_sdk_python-0.21.19-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:eec825100236f6d4a903cf165d7aca74b86c9b970e2097679990caee626151fb"}, - {file = "qcs_sdk_python-0.21.19-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:6e2830129efb2a71143bf853c4a8411bfbfd4873e1d89cb39fba91abb7257b56"}, - {file = "qcs_sdk_python-0.21.19-cp39-cp39-win_amd64.whl", hash = "sha256:53fb8137b0defeb508a68ebb0d755eeca9101c2c5b877eba1d408b34be7a1f77"}, - {file = "qcs_sdk_python-0.21.19.tar.gz", hash = "sha256:8a1d6a3c13a4becabd0b0c8b063191bfed7e1d5c7de87db7518b79400c2ec601"}, + {file = "qcs_sdk_python-0.21.22-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cb91c482b0fa9305f54bed35acfc7e75b62b9826d0686dfe21b2db9e96c2535"}, + {file = "qcs_sdk_python-0.21.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6096bda4a7901d58505bbf44603b70a95b00d995ef2ddfd4d445f3ce0c746e37"}, + {file = "qcs_sdk_python-0.21.22-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:62aafe936bf60f053f2ad2ecef8399eed6e8a7d49e7f2cb19af0c9e308205cc6"}, + {file = "qcs_sdk_python-0.21.22-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ca85831da9f25ed837c606b63f9b6ced5cb7b9241bd596451607643766acf158"}, + {file = "qcs_sdk_python-0.21.22-cp310-cp310-win_amd64.whl", hash = "sha256:70f385b3329a6ab621a5aac56e02ed836657816edf8a2082ed04fff45b0f5870"}, + {file = "qcs_sdk_python-0.21.22-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:35e1aef030083c1b58f4b95ad7da89ab939617da3ad98093d87298dbffea8994"}, + {file = "qcs_sdk_python-0.21.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:121cd94a8e09ce125bcc287efe56555eee350365be719961d05c51a456af4d00"}, + {file = "qcs_sdk_python-0.21.22-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1113f957445f1663e5c0431fa1ee1cb6b08ce7574dc519203acb3c39b8a41678"}, + {file = "qcs_sdk_python-0.21.22-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cf1a6578b1b4382175f0db25d86e21e13c5b7b229396aceb2f5b264dc8b9b317"}, + {file = "qcs_sdk_python-0.21.22-cp311-cp311-win_amd64.whl", hash = "sha256:7a37b473f52bc4006369108e37ccc82357a727566faa7c494859f0a615cc3c4c"}, + {file = "qcs_sdk_python-0.21.22-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bd3998d7d8a9c09703667e65a72f2994b6e01aeed7f95bf42e828d22d734cdab"}, + {file = "qcs_sdk_python-0.21.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c1a08550a67570b3965025228489e4400b123593442c677a1d0d8ea0ecb7ed"}, + {file = "qcs_sdk_python-0.21.22-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e0e55b477d5d053334aede3dd7f1613fb9c547d09d560ae7f43a67cdfe0d09a8"}, + {file = "qcs_sdk_python-0.21.22-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d8c75597e8b3e87477f48a1554b3a34b8c289133853a2bec5db0c9561e96332c"}, + {file = "qcs_sdk_python-0.21.22-cp312-cp312-win_amd64.whl", hash = "sha256:becdb70a11345a0e13b072b57f5de1630be4495c5972d11d5d5d037500180fdd"}, + {file = "qcs_sdk_python-0.21.22-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9c37818ca9b511ddbbf80be2ed1c8eb3d0f59f5e3a3e9dc5c4ee017295ca1a80"}, + {file = "qcs_sdk_python-0.21.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bae9af7b1271ae32fccecca6c3dc2b8239d9b9703cc23567a9177ab5650c2010"}, + {file = "qcs_sdk_python-0.21.22-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:54a80a96a3a2f83296aef2766829b9aea4c80eb185e925721711a9dc27873447"}, + {file = "qcs_sdk_python-0.21.22-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:98a8ebb898d2d2ae84ec1ab960afe91af7b08e744c260930782455dad1da9485"}, + {file = "qcs_sdk_python-0.21.22-cp38-cp38-win_amd64.whl", hash = "sha256:22c1d8692eef9c207a50f4c1a66f8b15e87d56adc6ad07d8a8db3388ceb5c818"}, + {file = "qcs_sdk_python-0.21.22-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:133548d3ceaaca71fc93161bb5213b5d843fbb79ff5dfaf2e06eacbd5b79abed"}, + {file = "qcs_sdk_python-0.21.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6d4f2585756aec307d461d5465eee8fba3fb6636bfe55b48c1876d9ec075522f"}, + {file = "qcs_sdk_python-0.21.22-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad0ee18426c368a960b519d390fe4ca82cf32c3f7be747d41568e4e9fe298623"}, + {file = "qcs_sdk_python-0.21.22-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:353f7a485e40df974d2df19ff30b9b753f466469f611a1e46de9324e8ed90be6"}, + {file = "qcs_sdk_python-0.21.22-cp39-cp39-win_amd64.whl", hash = "sha256:882fb9600f0993027de7de49128bdd25d579acc2faaf4a6b4d7c09eab2806467"}, + {file = "qcs_sdk_python-0.21.22.tar.gz", hash = "sha256:cdedde65abe084f2aa049cbf214dbe45a0f35505147a26f3c9373b598c2ea918"}, ] [package.dependencies] @@ -3431,15 +3214,15 @@ dev = ["maturin (>=1.2.3)", "mypy (>=1.13.0)", "pdoc (>=14.1.0)", "pytest (>=7.2 [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" description = "JSON Referencing + Python" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, ] [package.dependencies] @@ -3449,26 +3232,26 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" description = "Python HTTP for Humans." optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "respx" @@ -3485,6 +3268,24 @@ files = [ [package.dependencies] httpx = ">=0.21.0" +[[package]] +name = "rigetti-quax" +version = "0.5.3" +description = "A high-performance library for quantum information science built on top of JAX" +optional = false +python-versions = "<4.0,>=3.11" +groups = ["main"] +files = [ + {file = "rigetti_quax-0.5.3-py3-none-any.whl", hash = "sha256:f53ae6f5d3182deb94f7eeefc93c19597d1954b8befb7731b4ee90ef9a048e8f"}, + {file = "rigetti_quax-0.5.3.tar.gz", hash = "sha256:df758507afd5e72fecea2209f1846bbbd4b89eec066d27084a667ecc16cc7cec"}, +] + +[package.dependencies] +jax = ">=0.8.2" + +[package.extras] +plot = ["plotly (>=5.20)"] + [[package]] name = "rpcq" version = "3.11.0" @@ -3504,254 +3305,147 @@ pyzmq = ">=17" [[package]] name = "rpds-py" -version = "0.27.1" +version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, - {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9"}, - {file = "rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4"}, - {file = "rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1"}, - {file = "rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881"}, - {file = "rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948"}, - {file = "rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39"}, - {file = "rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15"}, - {file = "rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746"}, - {file = "rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90"}, - {file = "rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998"}, - {file = "rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39"}, - {file = "rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594"}, - {file = "rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502"}, - {file = "rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b"}, - {file = "rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002"}, - {file = "rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3"}, - {file = "rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83"}, - {file = "rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d"}, - {file = "rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228"}, - {file = "rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7"}, - {file = "rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688"}, - {file = "rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797"}, - {file = "rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334"}, - {file = "rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675"}, - {file = "rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3"}, - {file = "rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456"}, - {file = "rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3"}, - {file = "rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2"}, - {file = "rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0"}, - {file = "rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a"}, - {file = "rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772"}, - {file = "rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527"}, - {file = "rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b"}, - {file = "rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52"}, - {file = "rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859"}, - {file = "rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] [[package]] name = "ruamel-yaml" -version = "0.18.15" +version = "0.19.1" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701"}, - {file = "ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700"}, + {file = "ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93"}, + {file = "ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993"}, ] -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} - [package.extras] docs = ["mercurial (>5.7)", "ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.14" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\"" -files = [ - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f8b2acb0ffdd2ce8208accbec2dca4a06937d556fdcaefd6473ba1b5daa7e3c4"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:aef953f3b8bd0b50bd52a2e52fb54a6a2171a1889d8dea4a5959d46c6624c451"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a0ac90efbc7a77b0d796c03c8cc4e62fd710b3f1e4c32947713ef2ef52e09543"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf6b699223afe6c7fe9f2ef76e0bfa6dd892c21e94ce8c957478987ade76cd8"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d73a0187718f6eec5b2f729b0f98e4603f7bd9c48aa65d01227d1a5dcdfbe9e8"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81f6d3b19bc703679a5705c6a16dabdc79823c71d791d73c65949be7f3012c02"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b28caeaf3e670c08cb7e8de221266df8494c169bd6ed8875493fab45be9607a4"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94f3efb718f8f49b031f2071ec7a27dd20cbfe511b4dfd54ecee54c956da2b31"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-win32.whl", hash = "sha256:27c070cf3888e90d992be75dd47292ff9aa17dafd36492812a6a304a1aedc182"}, - {file = "ruamel.yaml.clib-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:4f4a150a737fccae13fb51234d41304ff2222e3b7d4c8e9428ed1a6ab48389b8"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5bae1a073ca4244620425cd3d3aa9746bde590992b98ee8c7c8be8c597ca0d4e"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:0a54e5e40a7a691a426c2703b09b0d61a14294d25cfacc00631aa6f9c964df0d"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:10d9595b6a19778f3269399eff6bab642608e5966183abc2adbe558a42d4efc9"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba72975485f2b87b786075e18a6e5d07dc2b4d8973beb2732b9b2816f1bad70"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29757bdb7c142f9595cc1b62ec49a3d1c83fab9cef92db52b0ccebaad4eafb98"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:557df28dbccf79b152fe2d1b935f6063d9cc431199ea2b0e84892f35c03bb0ee"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:26a8de280ab0d22b6e3ec745b4a5a07151a0f74aad92dd76ab9c8d8d7087720d"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e501c096aa3889133d674605ebd018471bc404a59cbc17da3c5924421c54d97c"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-win32.whl", hash = "sha256:915748cfc25b8cfd81b14d00f4bfdb2ab227a30d6d43459034533f4d1c207a2a"}, - {file = "ruamel.yaml.clib-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:4ccba93c1e5a40af45b2f08e4591969fa4697eae951c708f3f83dcbf9f6c6bb1"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6aeadc170090ff1889f0d2c3057557f9cd71f975f17535c26a5d37af98f19c27"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5e56ac47260c0eed992789fa0b8efe43404a9adb608608631a948cee4fc2b052"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a911aa73588d9a8b08d662b9484bc0567949529824a55d3885b77e8dd62a127a"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05ba88adf3d7189a974b2de7a9d56731548d35dc0a822ec3dc669caa7019b29"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb04c5650de6668b853623eceadcdb1a9f2fee381f5d7b6bc842ee7c239eeec4"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df3ec9959241d07bc261f4983d25a1205ff37703faf42b474f15d54d88b4f8c9"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fbc08c02e9b147a11dfcaa1ac8a83168b699863493e183f7c0c8b12850b7d259"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c099cafc1834d3c5dac305865d04235f7c21c167c8dd31ebc3d6bbc357e2f023"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-win32.whl", hash = "sha256:b5b0f7e294700b615a3bcf6d28b26e6da94e8eba63b079f4ec92e9ba6c0d6b54"}, - {file = "ruamel.yaml.clib-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:a37f40a859b503304dd740686359fcf541d6fb3ff7fc10f539af7f7150917c68"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7e4f9da7e7549946e02a6122dcad00b7c1168513acb1f8a726b1aaf504a99d32"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:dd7546c851e59c06197a7c651335755e74aa383a835878ca86d2c650c07a2f85"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:1c1acc3a0209ea9042cc3cfc0790edd2eddd431a2ec3f8283d081e4d5018571e"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2070bf0ad1540d5c77a664de07ebcc45eebd1ddcab71a7a06f26936920692beb"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd8fe07f49c170e09d76773fb86ad9135e0beee44f36e1576a201b0676d3d1d"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ff86876889ea478b1381089e55cf9e345707b312beda4986f823e1d95e8c0f59"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1f118b707eece8cf84ecbc3e3ec94d9db879d85ed608f95870d39b2d2efa5dca"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b30110b29484adc597df6bd92a37b90e63a8c152ca8136aad100a02f8ba6d1b6"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-win32.whl", hash = "sha256:f4e97a1cf0b7a30af9e1d9dad10a5671157b9acee790d9e26996391f49b965a2"}, - {file = "ruamel.yaml.clib-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:090782b5fb9d98df96509eecdbcaffd037d47389a89492320280d52f91330d78"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7df6f6e9d0e33c7b1d435defb185095386c469109de723d514142632a7b9d07f"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27"}, - {file = "ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:18c041b28f3456ddef1f1951d4492dbebe0f8114157c1b3c981a4611c2020792"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:d8354515ab62f95a07deaf7f845886cc50e2f345ceab240a3d2d09a9f7d77853"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:275f938692013a3883edbd848edde6d9f26825d65c9a2eb1db8baa1adc96a05d"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a60d69f4057ad9a92f3444e2367c08490daed6428291aa16cefb445c29b0e9"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ac5ff9425d8acb8f59ac5b96bcb7fd3d272dc92d96a7c730025928ffcc88a7a"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e1d1735d97fd8a48473af048739379975651fab186f8a25a9f683534e6904179"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:83bbd8354f6abb3fdfb922d1ed47ad8d1db3ea72b0523dac8d07cdacfe1c0fcf"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:808c7190a0fe7ae7014c42f73897cf8e9ef14ff3aa533450e51b1e72ec5239ad"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-win32.whl", hash = "sha256:6d5472f63a31b042aadf5ed28dd3ef0523da49ac17f0463e10fda9c4a2773352"}, - {file = "ruamel.yaml.clib-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:8dd3c2cc49caa7a8d64b67146462aed6723a0495e44bf0aa0a2e94beaa8432f6"}, - {file = "ruamel.yaml.clib-0.2.14.tar.gz", hash = "sha256:803f5044b13602d58ea378576dd75aa759f52116a0232608e8fdada4da33752e"}, -] +libyaml = ["ruamel.yaml.clibz (>=0.3.7) ; platform_python_implementation == \"CPython\""] +oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\""] [[package]] name = "ruff" @@ -3782,126 +3476,81 @@ files = [ [[package]] name = "scipy" -version = "1.13.1" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\"" -files = [ - {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, - {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, - {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, - {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, - {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, - {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, - {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, - {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, - {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, - {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, - {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, -] - -[package.dependencies] -numpy = ">=1.22.4,<2.3" - -[package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[[package]] -name = "scipy" -version = "1.16.2" +version = "1.17.1" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.11" groups = ["main"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92"}, - {file = "scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e"}, - {file = "scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173"}, - {file = "scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d"}, - {file = "scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2"}, - {file = "scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9"}, - {file = "scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3"}, - {file = "scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88"}, - {file = "scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa"}, - {file = "scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c"}, - {file = "scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d"}, - {file = "scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371"}, - {file = "scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0"}, - {file = "scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232"}, - {file = "scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1"}, - {file = "scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f"}, - {file = "scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef"}, - {file = "scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1"}, - {file = "scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e"}, - {file = "scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851"}, - {file = "scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70"}, - {file = "scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9"}, - {file = "scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5"}, - {file = "scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925"}, - {file = "scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9"}, - {file = "scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7"}, - {file = "scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb"}, - {file = "scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e"}, - {file = "scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c"}, - {file = "scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104"}, - {file = "scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1"}, - {file = "scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a"}, - {file = "scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f"}, - {file = "scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4"}, - {file = "scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21"}, - {file = "scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7"}, - {file = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8"}, - {file = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472"}, - {file = "scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351"}, - {file = "scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d"}, - {file = "scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77"}, - {file = "scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70"}, - {file = "scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88"}, - {file = "scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f"}, - {file = "scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb"}, - {file = "scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7"}, - {file = "scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548"}, - {file = "scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936"}, - {file = "scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff"}, - {file = "scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d"}, - {file = "scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8"}, - {file = "scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4"}, - {file = "scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831"}, - {file = "scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3"}, - {file = "scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac"}, - {file = "scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374"}, - {file = "scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6"}, - {file = "scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c"}, - {file = "scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9"}, - {file = "scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779"}, - {file = "scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b"}, +files = [ + {file = "scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec"}, + {file = "scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696"}, + {file = "scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee"}, + {file = "scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd"}, + {file = "scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c"}, + {file = "scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4"}, + {file = "scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444"}, + {file = "scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082"}, + {file = "scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff"}, + {file = "scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d"}, + {file = "scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8"}, + {file = "scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76"}, + {file = "scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086"}, + {file = "scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b"}, + {file = "scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21"}, + {file = "scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458"}, + {file = "scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb"}, + {file = "scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea"}, + {file = "scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87"}, + {file = "scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3"}, + {file = "scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c"}, + {file = "scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f"}, + {file = "scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d"}, + {file = "scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b"}, + {file = "scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6"}, + {file = "scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464"}, + {file = "scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950"}, + {file = "scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369"}, + {file = "scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448"}, + {file = "scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87"}, + {file = "scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a"}, + {file = "scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0"}, + {file = "scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce"}, + {file = "scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6"}, + {file = "scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e"}, + {file = "scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475"}, + {file = "scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50"}, + {file = "scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca"}, + {file = "scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c"}, + {file = "scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866"}, + {file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350"}, + {file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118"}, + {file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068"}, + {file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118"}, + {file = "scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19"}, + {file = "scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca"}, + {file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad"}, + {file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a"}, + {file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4"}, + {file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2"}, + {file = "scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484"}, + {file = "scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21"}, + {file = "scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0"}, ] [package.dependencies] -numpy = ">=1.25.2,<2.6" +numpy = ">=1.26.4,<2.7" [package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] -doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +dev = ["click (<8.3.0)", "cython-lint (>=0.12.2)", "mypy (==1.10.0)", "pycodestyle", "ruff (>=0.12.0)", "spin", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)", "tabulate"] test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest (>=8.0.0)", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] @@ -3962,18 +3611,6 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "snowballstemmer" version = "3.0.1" @@ -3989,15 +3626,15 @@ files = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.8.3" description = "A modern CSS selector implementation for Beautiful Soup." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, - {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, + {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, + {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, ] [[package]] @@ -4019,7 +3656,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.14" @@ -4031,7 +3667,6 @@ sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] @@ -4249,70 +3884,25 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] -markers = {main = "python_version < \"3.11\" and extra == \"docs\"", dev = "python_version < \"3.11\""} - [[package]] name = "tornado" -version = "6.5.2" +version = "6.5.5" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"}, - {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"}, - {file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"}, - {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"}, - {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"}, - {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"}, - {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"}, - {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"}, - {file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"}, - {file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"}, - {file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"}, - {file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"}, + {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa"}, + {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521"}, + {file = "tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5"}, + {file = "tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07"}, + {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e"}, + {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca"}, + {file = "tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7"}, + {file = "tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b"}, + {file = "tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6"}, + {file = "tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9"}, ] [[package]] @@ -4333,14 +3923,14 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "types-deprecated" -version = "1.2.15.20250304" +version = "1.3.1.20260408" description = "Typing stubs for Deprecated" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "types_deprecated-1.2.15.20250304-py3-none-any.whl", hash = "sha256:86a65aa550ea8acf49f27e226b8953288cd851de887970fbbdf2239c116c3107"}, - {file = "types_deprecated-1.2.15.20250304.tar.gz", hash = "sha256:c329030553029de5cc6cb30f269c11f4e00e598c4241290179f63cda7d33f719"}, + {file = "types_deprecated-1.3.1.20260408-py3-none-any.whl", hash = "sha256:b64e1eab560d4fa9394a27a3099211344b0e0f2f3ac8026d825c86e70d65cdd5"}, + {file = "types_deprecated-1.3.1.20260408.tar.gz", hash = "sha256:62d6a86d0cc754c14bb2de31162d069b1c6a07ce11ee65e5258f8f75308eb3a3"}, ] [[package]] @@ -4357,47 +3947,47 @@ files = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2026.2" description = "Provider of IANA time zone data" optional = true python-versions = ">=2" groups = ["main"] -markers = "extra == \"docs\"" +markers = "(sys_platform == \"win32\" or sys_platform == \"emscripten\") and extra == \"docs\"" files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"}, + {file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"}, ] [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "wcwidth" -version = "0.2.14" +version = "0.6.0" description = "Measures the displayed width of unicode strings in a terminal" optional = true -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["main"] markers = "extra == \"latex\"" files = [ - {file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"}, - {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"}, + {file = "wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad"}, + {file = "wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159"}, ] [[package]] @@ -4415,115 +4005,106 @@ files = [ [[package]] name = "wrapt" -version = "1.17.3" +version = "2.1.2" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, - {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, - {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, - {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, - {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, - {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, - {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, - {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, - {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, - {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, - {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, - {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, - {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, - {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, - {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, - {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, - {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, - {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, - {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, - {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, - {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, - {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, - {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, - {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, -] - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, + {file = "wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c"}, + {file = "wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f"}, + {file = "wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb"}, + {file = "wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e"}, + {file = "wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba"}, + {file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f"}, + {file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394"}, + {file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45"}, + {file = "wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d"}, + {file = "wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71"}, + {file = "wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc"}, + {file = "wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb"}, + {file = "wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d"}, + {file = "wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894"}, + {file = "wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842"}, + {file = "wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8"}, + {file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6"}, + {file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9"}, + {file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15"}, + {file = "wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b"}, + {file = "wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1"}, + {file = "wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a"}, + {file = "wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9"}, + {file = "wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748"}, + {file = "wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e"}, + {file = "wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8"}, + {file = "wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c"}, + {file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c"}, + {file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1"}, + {file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2"}, + {file = "wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0"}, + {file = "wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63"}, + {file = "wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf"}, + {file = "wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b"}, + {file = "wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e"}, + {file = "wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb"}, + {file = "wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca"}, + {file = "wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267"}, + {file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f"}, + {file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8"}, + {file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413"}, + {file = "wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6"}, + {file = "wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1"}, + {file = "wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf"}, + {file = "wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b"}, + {file = "wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18"}, + {file = "wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d"}, + {file = "wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015"}, + {file = "wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92"}, + {file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf"}, + {file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67"}, + {file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a"}, + {file = "wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd"}, + {file = "wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f"}, + {file = "wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679"}, + {file = "wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9"}, + {file = "wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9"}, + {file = "wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e"}, + {file = "wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c"}, + {file = "wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a"}, + {file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90"}, + {file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586"}, + {file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19"}, + {file = "wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508"}, + {file = "wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04"}, + {file = "wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575"}, + {file = "wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb"}, + {file = "wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22"}, + {file = "wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596"}, + {file = "wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044"}, + {file = "wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b"}, + {file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf"}, + {file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2"}, + {file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3"}, + {file = "wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7"}, + {file = "wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5"}, + {file = "wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00"}, + {file = "wrapt-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5e0fa9cc32300daf9eb09a1f5bdc6deb9a79defd70d5356ba453bcd50aef3742"}, + {file = "wrapt-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:710f6e5dfaf6a5d5c397d2d6758a78fecd9649deb21f1b645f5b57a328d63050"}, + {file = "wrapt-2.1.2-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:305d8a1755116bfdad5dda9e771dcb2138990a1d66e9edd81658816edf51aed1"}, + {file = "wrapt-2.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0d8fc30a43b5fe191cf2b1a0c82bab2571dadd38e7c0062ee87d6df858dd06e"}, + {file = "wrapt-2.1.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5d516e22aedb7c9c1d47cba1c63160b1a6f61ec2f3948d127cd38d5cfbb556f"}, + {file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:45914e8efbe4b9d5102fcf0e8e2e3258b83a5d5fba9f8f7b6d15681e9d29ffe0"}, + {file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:478282ebd3795a089154fb16d3db360e103aa13d3b2ad30f8f6aac0d2207de0e"}, + {file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3756219045f73fb28c5d7662778e4156fbd06cf823c4d2d4b19f97305e52819c"}, + {file = "wrapt-2.1.2-cp39-cp39-win32.whl", hash = "sha256:b8aefb4dbb18d904b96827435a763fa42fc1f08ea096a391710407a60983ced8"}, + {file = "wrapt-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:e5aeab8fe15c3dff75cfee94260dcd9cded012d4ff06add036c28fae7718593b"}, + {file = "wrapt-2.1.2-cp39-cp39-win_arm64.whl", hash = "sha256:f069e113743a21a3defac6677f000068ebb931639f789b5b226598e247a4c89e"}, + {file = "wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8"}, + {file = "wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] +dev = ["pytest", "setuptools"] [extras] docs = ["Sphinx", "matplotlib", "myst-parser", "nbsphinx", "pandoc", "seaborn", "sphinx-rtd-theme"] @@ -4531,5 +4112,5 @@ latex = ["ipython"] [metadata] lock-version = "2.1" -python-versions = "^3.9,<3.13" -content-hash = "7497c2e55515737cc5b451578890b87969f1899daf957fe71ffe1b1b9eac7974" +python-versions = ">=3.11, <3.13" +content-hash = "f3138f5e5768209e4153dbf73369f8931dd288eb814d65d16125b33f0efcc3bc" diff --git a/pyproject.toml b/pyproject.toml index b1dc24166..21291dc7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ exclude = ["pyquil/conftest.py"] [tool.poetry.dependencies] # TODO(#1816): Loosen this bound once we've resolved support for Python 3.13+. -python = "^3.9,<3.13" +python = ">=3.11, <3.13" numpy = ">=1.26,<3" scipy = "^1.11" rpcq = "^3.11.0" @@ -45,6 +45,7 @@ pandoc = {version = "2.4b0", optional = true} matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} +rigetti-quax = ">=0.5.3" [tool.poetry.extras] latex = ["ipython"] diff --git a/pyquil/noise.py b/pyquil/noise.py index d794c4f78..63f62bc91 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -41,6 +41,9 @@ class KrausModel(_KrausModel): """Encapsulate a single gate's noise model. + .. deprecated:: + Use :class:`pyquil.noise_model.Channel` for quax-based noise modeling. + :ivar str gate: The name of the gate. :ivar Sequence[float] params: Optional parameters for the gate. :ivar Sequence[int] targets: The target qubit ids. @@ -114,12 +117,15 @@ def __eq__(self, other: object) -> bool: return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() -_NoiseModel = namedtuple("_NoiseModel", ["gates", "assignment_probs"]) +_LegacyNoiseModel = namedtuple("_LegacyNoiseModel", ["gates", "assignment_probs"]) -class NoiseModel(_NoiseModel): +class LegacyNoiseModel(_LegacyNoiseModel): """Encapsulate the QPU noise model containing information about the noisy gates. + .. deprecated:: + Use :class:`pyquil.noise_model.NoiseModel` for quax-based noise modeling. + :ivar Sequence[KrausModel] gates: The tomographic estimates of all gates. :ivar dict[int,np.array] assignment_probs: The single qubit readout assignment probability matrices keyed by qubit id. @@ -150,13 +156,13 @@ def to_dict(self) -> dict[str, Any]: } @staticmethod - def from_dict(d: dict[str, Any]) -> "NoiseModel": + def from_dict(d: dict[str, Any]) -> "LegacyNoiseModel": """Re-create the noise model from a dictionary representation. :param d: The dictionary representation. :return: The restored noise model. """ - return NoiseModel( + return LegacyNoiseModel( gates=[KrausModel.from_dict(t) for t in d["gates"]], assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, ) @@ -171,7 +177,7 @@ def gates_by_name(self, name: str) -> list[KrausModel]: def __eq__(self, other: object) -> bool: """Return True if NoiseModels are equal.""" - return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict() + return isinstance(other, LegacyNoiseModel) and self.to_dict() == other.to_dict() def _check_kraus_ops(n: int, kraus_ops: Sequence[np.ndarray]) -> None: @@ -394,7 +400,7 @@ def _decoherence_noise_model( gate_time_1q: float = 50e-9, gate_time_2q: float = 150e-09, ro_fidelity: Union[dict[int, float], float] = 0.95, -) -> NoiseModel: +) -> LegacyNoiseModel: """Return default noise model. - T1 = 30 us @@ -474,10 +480,10 @@ def _decoherence_noise_model( for q, f_ro in ro_fidelity.items(): aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) - return NoiseModel(kraus_maps, aprobs) + return LegacyNoiseModel(kraus_maps, aprobs) -def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p11: float = 0.911) -> NoiseModel: +def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p11: float = 0.911) -> LegacyNoiseModel: """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. For simplicity, we use the default values for T1, T2, gate times, et al. and only allow @@ -487,10 +493,10 @@ def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p noise_model = _decoherence_noise_model(gates) aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} - return NoiseModel(noise_model.gates, aprobs) + return LegacyNoiseModel(noise_model.gates, aprobs) -def _noise_model_program_header(noise_model: NoiseModel) -> "Program": +def _noise_model_program_header(noise_model: LegacyNoiseModel) -> "Program": """Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates. The program header consists of 3 sections: @@ -533,7 +539,7 @@ def _noise_model_program_header(noise_model: NoiseModel) -> "Program": return p -def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": +def apply_noise_model(prog: "Program", noise_model: LegacyNoiseModel) -> "Program": """Apply a noise model to a program and generated a 'noisy-fied' version of the program. :param prog: A Quil Program object. @@ -796,3 +802,29 @@ def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> list[list[int]]: if bitstrings is None: raise ValueError("No readout data found in result.") return cast(list[list[int]], bitstrings.tolist()) + + +# ────────────────────────────────────────────────────────── +# Re-export quax-based noise model classes (lazy to avoid circular imports) +# ────────────────────────────────────────────────────────── + +_NOISE_MODEL_EXPORTS = ( + "Channel", + "CycleChannel", + "CustomGateMap", + "MeasurementChannel", + "NoiseModel", + "ResetChannel", + "estimate_program_fidelity", + "estimate_program_observable_fidelity", + "get_custom_gates_from_program", + "get_instruction_unitary", +) + + +def __getattr__(name: str): # type: ignore[override] + if name in _NOISE_MODEL_EXPORTS: + import pyquil.noise_model as _nm + + return getattr(_nm, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/pyquil/noise/__init__.py b/pyquil/noise/__init__.py new file mode 100644 index 000000000..e1ea7e3c5 --- /dev/null +++ b/pyquil/noise/__init__.py @@ -0,0 +1,78 @@ +""" +pyquil.noise — Noise modeling for quantum simulators. + +This package provides: + +- **Noise model** (``_legacy_noise``): Kraus-map based noise construction + for the QVM, including ``KrausModel``, ``NoiseModel``, and decoherence + helpers. + +- **Channel classes** (``_channels``): quax-backed ``Channel``, + ``MeasurementChannel``, ``ResetChannel``, and ``CycleChannel`` dataclasses + for fine-grained noise modeling. These are private and not re-exported. + +- **New noise model** (``_noise_model``): The quax-based ``NoiseModel`` + container and program-level fidelity estimation utilities. These are + private and not re-exported; they will become the public API in the next + major version. +""" + +# ── Noise model (Kraus-map based) ─────────────────────────────────────── +from pyquil.noise._legacy_noise import ( + ANGLE_TOLERANCE, + INFINITY, + KrausModel, + NO_NOISE, + NoiseModel, + NoisyGateUndefined, + _bitstring_probs_by_qubit, + _check_kraus_ops, + _create_kraus_pragmas, + _decoherence_noise_model, + _get_program_gates, + _noise_model_program_header, + _run, + add_decoherence_noise, + append_kraus_to_gate, + apply_noise_model, + bitstring_probs_to_z_moments, + combine_kraus_maps, + correct_bitstring_probs, + corrupt_bitstring_probs, + damping_after_dephasing, + damping_kraus_map, + decoherence_noise_with_asymmetric_ro, + dephasing_kraus_map, + estimate_assignment_probs, + estimate_bitstring_probs, + get_noisy_gate, + pauli_kraus_map, + tensor_kraus_maps, +) + + +__all__ = [ + # Noise model + "ANGLE_TOLERANCE", + "INFINITY", + "KrausModel", + "NO_NOISE", + "NoiseModel", + "NoisyGateUndefined", + "add_decoherence_noise", + "append_kraus_to_gate", + "apply_noise_model", + "bitstring_probs_to_z_moments", + "combine_kraus_maps", + "correct_bitstring_probs", + "corrupt_bitstring_probs", + "damping_after_dephasing", + "damping_kraus_map", + "decoherence_noise_with_asymmetric_ro", + "dephasing_kraus_map", + "estimate_assignment_probs", + "estimate_bitstring_probs", + "get_noisy_gate", + "pauli_kraus_map", + "tensor_kraus_maps", +] diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py new file mode 100644 index 000000000..b7f230bb2 --- /dev/null +++ b/pyquil/noise/_channels.py @@ -0,0 +1,1503 @@ +############################################################################## +# Copyright 2016-2026 Rigetti Computing +# +# 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. +############################################################################## +""" +Noise channel classes and gate-resolution utilities. + +This module defines ``Channel``, ``MeasurementChannel``, ``ResetChannel``, and +``CycleChannel`` dataclasses for representing noise in quantum circuits, along +with helper functions for resolving gate unitaries and extracting custom gate +definitions from Quil programs. +""" + +from __future__ import annotations + +import itertools +import json +import logging +from dataclasses import dataclass, replace +from functools import cached_property, reduce +from itertools import product +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Type, Union + +import jax.numpy as jnp +import numpy as np +import quax as qx +from jax import Array +from plotly.graph_objs import Figure +from quil.program import Program as RSProgram +from scipy.linalg import logm as scipy_logm + +from pyquil.quilatom import Expression, FormalArgument, Parameter, substitute +from pyquil.quilbase import DefCircuit, DefGate, Gate, Measurement, Reset +from quil.expression import Expression as QuilExpression + +if TYPE_CHECKING: + from pyquil import Program + +logger = logging.getLogger(__name__) + +# Type alias for the custom-gate lookup map used throughout the Channel constructors. +CustomGateMap = Dict[str, Union[qx.Unitary, Callable[..., qx.Unitary]]] + + +def _parse_quil_instruction(quil_str: str) -> Gate | Measurement | Reset: + """Parse a single Quil instruction string into a pyquil instruction object. + + Uses the ``quil`` Rust parser directly, avoiding a dependency on ``pyquil.Program``. + """ + rs_inst = RSProgram.parse(quil_str).body_instructions[0] + if rs_inst.is_gate(): + return Gate._from_rs_gate(rs_inst.to_gate()) + elif rs_inst.is_measurement(): + return Measurement._from_rs_measurement(rs_inst.to_measurement()) + elif rs_inst.is_reset(): + return Reset._from_rs_reset(rs_inst.to_reset()) + raise ValueError(f"Unsupported instruction type in: {quil_str}") + + +def _resolve_params(params: list) -> List[float]: + """ + Resolve gate parameters to concrete float values. + + :param params: The gate parameters (may include symbolic Parameters or Expressions). + :return: A list of concrete float values. + :raises ValueError: If any parameter is symbolic and cannot be evaluated to a number. + """ + fixed_params = [] + for p in params: + if isinstance(p, (Parameter, Expression)): + evaluated = p._evaluate() + if isinstance(evaluated, (Parameter, Expression)): + raise ValueError( + f"Cannot resolve symbolic parameter {p}. Provide a gate with concrete numeric parameters." + ) + fixed_params.append(float(evaluated)) + elif isinstance(p, QuilExpression): + result = p.evaluate({}, {}) + fixed_params.append(float(result.to_number()) if hasattr(result, "to_number") else float(result)) # type: ignore[arg-type] + else: + fixed_params.append(float(p.real)) + return fixed_params + + +def get_custom_gates_from_program(program: Program) -> CustomGateMap: + """ + Extract custom gate definitions from a Quil program. + + Returns a dictionary mapping gate names to unitary matrices (for fixed gates) or callables + (for parametric gates). Does not include the standard gate set — use this to augment + the standard ``qx.gates.QUANTUM_GATES`` when resolving instructions with custom gates. + + :param program: A Quil program containing DefGate definitions. + :return: A dictionary of custom gate names to unitary matrices or callables. + """ + custom_gates: CustomGateMap = {} + for defgate in program.defined_gates: + if defgate.parameters: + + def parametric_gate(*args: float, defgate: DefGate = defgate) -> qx.Unitary: + parameter_map = {Parameter(p.name): arg for p, arg in zip(defgate.parameters, args)} + matrix = jnp.asarray( + [[substitute(element, parameter_map) for element in row] for row in defgate.matrix], # type: ignore[arg-type] + dtype=complex, + ) + num_qubits = int(jnp.round(jnp.log2(matrix.shape[0]))) + return qx.Unitary.from_matrix(matrix, ((2,) * num_qubits, (2,) * num_qubits)) + + custom_gates[defgate.name] = parametric_gate + else: + matrix = jnp.asarray(defgate.matrix, dtype=complex) + num_qubits = int(jnp.round(jnp.log2(matrix.shape[0]))) + custom_gates[defgate.name] = qx.Unitary.from_matrix(matrix, ((2,) * num_qubits, (2,) * num_qubits)) + return custom_gates + + +def get_instruction_unitary( + inst: Gate, + custom_gates: Optional[CustomGateMap] = None, +) -> qx.Unitary: + """ + Get the unitary matrix associated with a gate instruction. + + Looks up the gate by name — first in ``custom_gates`` (if provided), then in the + standard quax gate table ``qx.gates.QUANTUM_GATES``. Parametric gates are supported + provided all parameters are concrete numeric values. + + :param inst: The gate instruction. + :param custom_gates: Optional dictionary of additional gate definitions (e.g. from + :func:`get_custom_gates_from_program`). Takes precedence over the standard gate set. + :return: The unitary matrix. + :raises ValueError: If any gate parameter is symbolic. + :raises KeyError: If the gate name is not found in either the custom or standard gate set. + """ + name = inst.name + + # Look up gate definition: custom gates take precedence + if custom_gates is not None and name in custom_gates: + gate_def = custom_gates[name] + elif name in qx.gates.QUANTUM_GATES: + gate_def = qx.gates.QUANTUM_GATES[name] + else: + raise KeyError(f"Unknown gate '{name}'. Provide it via custom_gates (e.g. custom_gates={{'{name}': matrix}}).") + + if inst.params: + fixed_params = _resolve_params(list(inst.params)) + if not callable(gate_def): + raise ValueError(f"Gate '{name}' is not parametric but parameters were provided.") + result = gate_def(*fixed_params) + else: + if callable(gate_def): + result = gate_def() + else: + result = gate_def + + # quax parametric gates may return Operator instead of Unitary; wrap if needed + if not isinstance(result, qx.Unitary): + result = qx.Unitary.from_matrix(result.matrix, result.dims) # type: ignore[union-attr] + return result + + +@dataclass(frozen=True) +class Channel: + """ + A noise channel attaches a superoperator to a specific gate. + + The superoperator *includes* the gate unitary, so the channel replaces the gate + rather than being applied after it. + + The ``process`` field is a ``qx.SuperOp`` which can be converted to alternative + representations (Choi, Kraus, Pauli-Liouville) via ``quax``. + + Fidelity metrics are computed relative to the ideal gate unitary, which is resolved + automatically for standard gates or provided explicitly via ``target_unitary``. + """ + + inst: Gate + """Quil gate to which the channel applies.""" + + process: qx.SuperOp + """The noisy process (superoperator) for the gate, including the gate unitary.""" + + target_unitary: Optional[qx.Unitary] = None + """ + The noiseless unitary of the gate. If ``None``, will be resolved automatically from + ``inst`` for standard gates. Required for fidelity calculations with custom gates. + """ + + @cached_property + def unitary(self) -> qx.Unitary: + """The noiseless unitary of the gate, resolved from ``inst`` or provided explicitly.""" + if self.target_unitary is not None: + return self.target_unitary + resolved = get_instruction_unitary(self.inst) + return resolved + + @cached_property + def qubits(self) -> List[int]: + """The qubits which the channel applies to.""" + return self.inst.get_qubit_indices() + + @cached_property + def num_qubits(self) -> int: + """The number of qubits the channel acts on.""" + return len(self.qubits) + + # ────────────────────────────────────────────── + # Constructors + # ────────────────────────────────────────────── + + @classmethod + def from_gate_fidelity( + cls: Type["Channel"], + inst: Gate, + fidelity: float, + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a depolarizing noise channel from an average gate fidelity. + + The resulting channel is the composition of the ideal gate unitary with a + depolarizing channel calibrated to the specified fidelity: + :math:`\\mathcal{E} = \\mathcal{D}_p \\circ \\mathcal{U}` + + :param inst: The gate to which the channel applies. + :param fidelity: The average gate fidelity, :math:`F_{\\mathrm{avg}} \\in [0, 1]`. + :param custom_gates: Optional dictionary of custom gate definitions. + :return: A Channel instance. + """ + unitary = get_instruction_unitary(inst, custom_gates) + p = qx.average_fidelity_to_depolarizing_constant(fidelity, unitary.dims[0]) + return cls.from_depolarizing_constant(inst, p, custom_gates) + + @classmethod + def from_pauli_fidelity( + cls: Type["Channel"], + inst: Gate, + pauli_fidelity: float, + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a depolarizing noise channel from a process (Pauli) fidelity. + + The process fidelity :math:`F_e` is related to the average gate fidelity by + :math:`F_{\\mathrm{avg}} = (d \\cdot F_e + 1) / (d + 1)`. + + :param inst: The gate to which the channel applies. + :param pauli_fidelity: The process fidelity (entanglement fidelity), :math:`F_e \\in [0, 1]`. + :param custom_gates: Optional dictionary of custom gate definitions. + :return: A Channel instance. + """ + unitary = get_instruction_unitary(inst, custom_gates) + p = qx.process_fidelity_to_depolarizing_constant(pauli_fidelity, unitary.dims[0]) + return cls.from_depolarizing_constant(inst, p, custom_gates) + + @classmethod + def from_depolarizing_constant( + cls: Type["Channel"], + inst: Gate, + depolarizing_constant: float, + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a depolarizing noise channel from a depolarization constant. + + The depolarizing constant :math:`p` parameterizes the channel as + :math:`\\mathcal{D}_p(\\rho) = p \\, \\rho + (1-p) \\, I/d`. + + :param inst: The gate to which the channel applies. + :param depolarizing_constant: The depolarization constant, e.g. 0.98 for 2% depolarization. + :param custom_gates: Optional dictionary of custom gate definitions. + :return: A Channel instance. + """ + unitary = get_instruction_unitary(inst, custom_gates) + depolarizing_superop = qx.depolarizing_channel_superoperator(1 - depolarizing_constant, unitary.dims[0]) + combined_superop = depolarizing_superop @ unitary + return cls(inst=inst, process=qx.to_superop(combined_superop), target_unitary=unitary) + + @classmethod + def from_pauli_noise( + cls: Type["Channel"], + inst: Gate, + pauli_noise: Dict[str, float], + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a stochastic Pauli noise channel from Pauli error rates. + + The noise is specified as a dictionary mapping Pauli strings to error probabilities, + e.g. ``{"XX": 0.03, "ZI": 0.001}``. The probabilities must sum to at most 1.0; + any remainder is assigned to the identity (no-error) term. + + :param inst: The gate to which the channel applies. + :param pauli_noise: Pauli error rates, e.g. ``{"IX": 0.01, "ZZ": 0.02}``. + :param custom_gates: Optional dictionary of custom gate definitions. + :return: A Channel instance. + """ + unitary = get_instruction_unitary(inst, custom_gates) + num_qubits = len(unitary.dims[0]) + + for pauli in pauli_noise: + if len(pauli) != num_qubits: + raise ValueError(f"Pauli term '{pauli}' has length {len(pauli)}, expected {num_qubits}.") + + all_pauli_terms = list(map(lambda term: "".join(term), itertools.product(*["IXYZ" for _ in range(num_qubits)]))) + + pauli_error_rates = [] + for term in reversed(all_pauli_terms): + if term in pauli_noise: + error_rate = pauli_noise[term] + elif all(p == "I" for p in term): + error_rate = 1 - sum(pauli_error_rates) + else: + error_rate = 0 + pauli_error_rates.append(error_rate) + assert jnp.isclose(1.0, sum(pauli_error_rates)) + pauli_error_rates = list(reversed(pauli_error_rates)) + + # Build Pauli Kraus operators using quax ensembles + single_paulis = qx.ensembles.PAULIS # ensemble of (I, X, Y, Z) + if num_qubits == 1: + pauli_ops = single_paulis + else: + pauli_ops = reduce(lambda a, b: a | b, [single_paulis for _ in range(num_qubits)]) + + # Scale each Pauli by sqrt(probability) to form Kraus operators + coeffs = jnp.sqrt(jnp.array(pauli_error_rates, dtype=float)) + kraus_matrices = coeffs[:, None, None] * pauli_ops.matrix + kraus_map = qx.KrausMap.from_matrix(kraus_matrices, unitary.dims) + + process_superop = qx.to_superop(kraus_map @ unitary) + return cls(inst=inst, process=process_superop, target_unitary=unitary) + + @classmethod + def from_random_coherent_error( + cls: Type["Channel"], + inst: Gate, + process_fidelity: float, + rng: Optional[np.random.Generator] = None, + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a channel with a random coherent (unitary) error at the specified process fidelity. + + A random unitary close to identity is generated with the given process fidelity, + then composed with the ideal gate. + + :param inst: The gate to which the channel applies. + :param process_fidelity: The process fidelity of the coherent error, :math:`F_e \\in [0, 1]`. + :param rng: NumPy random number generator for reproducibility. + :param custom_gates: Optional dictionary of custom gate definitions. + :return: A Channel instance. + """ + if rng is None: + rng = np.random.default_rng() + + ideal = get_instruction_unitary(inst, custom_gates) + num_qubits = len(ideal.dims[0]) + d = 2**num_qubits + + # Generate a random unitary error with the specified process fidelity + # using Pauli generator decomposition + angle = jnp.arccos(2 * process_fidelity - 1) / (2 * jnp.pi) + id_coeff = 1 - float(angle) + coeffs = rng.random(4**num_qubits - 1) + coeffs = (1 - id_coeff) / np.sqrt(np.sum(np.square(coeffs))) * coeffs + + # Build Pauli generator sum using quax Pauli matrices + pauli_matrices = qx.ensembles.PAULIS.matrix # shape (4, 2, 2) + pauli_sum = jnp.eye(d, dtype=complex) * id_coeff + pauli_products = list(itertools.product(pauli_matrices, repeat=num_qubits))[1:] + for paulis, coefficient in zip(pauli_products, coeffs): + pauli_sum = pauli_sum + reduce(jnp.kron, paulis) * coefficient + + from jax.scipy.linalg import expm as jax_expm + + error_unitary = jax_expm(-1j * jnp.pi * pauli_sum) + # Fix global phase + phase = jnp.exp(-1j * jnp.angle(error_unitary[0, 0])) + error_unitary = error_unitary * phase + + error_u = qx.Unitary.from_matrix(error_unitary, ideal.dims) + noisy_superop = qx.to_superop(error_u @ ideal) + return cls(inst=inst, process=noisy_superop, target_unitary=ideal) + + @classmethod + def from_mixture( + cls: Type["Channel"], + inst: Gate, + constituents: List[qx.Unitary], + probabilities: List[float], + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a mixture channel from a set of unitary errors with given probabilities. + + The channel is :math:`\\mathcal{E}(\\rho) = (1-\\sum p_i) U\\rho U^\\dagger + \\sum p_i V_i U \\rho U^\\dagger V_i^\\dagger` + where :math:`U` is the ideal gate and :math:`V_i` are the error unitaries. + + :param inst: The gate to which the channel applies. + :param constituents: Unitary error operators to mix. + :param probabilities: Probability of each unitary error. Must sum to at most 1.0. + :param custom_gates: Optional dictionary of custom gate definitions. + :return: A Channel instance. + """ + ideal = get_instruction_unitary(inst, custom_gates) + + if len(constituents) != len(probabilities): + raise ValueError("The number of constituents and probabilities must match.") + error_prob = sum(probabilities) + if error_prob > 1.0: + raise ValueError(f"The sum of probabilities ({error_prob}) must be at most 1.0.") + + # Build the mixture superop: (1-p_total) S(U) + sum p_i S(V_i @ U) + p0 = 1.0 - error_prob + noisy_superop_matrix = p0 * qx.to_superop(ideal).matrix + for p, v in zip(probabilities, constituents): + composed = v @ ideal + noisy_superop_matrix = noisy_superop_matrix + p * qx.to_superop(composed).matrix + noisy_superop = qx.SuperOp.from_matrix(noisy_superop_matrix, ideal.dims) + return cls(inst=inst, process=noisy_superop, target_unitary=ideal) + + @classmethod + def from_coherence_times( + cls: Type["Channel"], + inst: Gate, + gate_duration: float, + t1s: List[float], + t2s: Optional[List[float]] = None, + custom_gates: Optional[CustomGateMap] = None, + ) -> "Channel": + """ + Create a decoherence Channel based on the coherence times. + + In this construction, decoherence is applied _after_ the ideal gate unitary. + + :param inst: The target instruction. + :param gate_duration: The duration of the gate. + :param t1s: The t1 time(s) of the qubits + :param t2s: The t2 time(s) of the qubits. Default to 2*t1. + """ + unitary = get_instruction_unitary(inst, custom_gates) + qubits = inst.get_qubit_indices() + num_sys = len(qubits) + assert num_sys == len(t1s) + if t2s is None: + t2s = [2 * t1 for t1 in t1s] + else: + assert num_sys == len(t2s) + + t1_array = jnp.asarray(t1s) + tphi_array = 1 / (1 / jnp.asarray(t2s) - 1 / t1_array) + + choi = qx.thermal_relaxation_choi(t1s=t1_array, tphis=tphi_array, duration=gate_duration) + process = qx.to_superop(choi @ unitary) + return cls( + inst=inst, + process=process, + target_unitary=unitary, + ) + + # ────────────────────────────────────────────── + # Cached representation conversions + # ────────────────────────────────────────────── + + @cached_property + def noise_process(self) -> qx.SuperOp: + """ + The noise-only channel with the ideal gate unitary factored out. + + If the full channel is :math:`\\mathcal{E} = \\Lambda \\circ \\mathcal{U}`, this + returns :math:`\\Lambda`. + """ + return qx.to_superop(self.process @ self.unitary.h) + + # ────────────────────────────────────────────── + # Fidelity properties + # ────────────────────────────────────────────── + + @cached_property + def fidelity(self) -> float: + """Average gate fidelity :math:`F_{\\mathrm{avg}}` of the channel relative to the ideal gate.""" + return float(qx.process_fidelity_to_average_fidelity(self.pauli_fidelity, dims=self.unitary.dims[0])) + + @cached_property + def infidelity(self) -> float: + """Average gate infidelity :math:`1 - F_{\\mathrm{avg}}`.""" + return 1.0 - self.fidelity + + @cached_property + def pauli_fidelity(self) -> float: + """Process fidelity (entanglement fidelity) :math:`F_e` relative to the ideal gate.""" + process, unitary = qx.promote_hilbert_space(self.process, qx.to_superop(self.unitary)) + return float(qx.process_fidelity(process, unitary)) + + @cached_property + def pauli_infidelity(self) -> float: + """Process infidelity :math:`1 - F_e`.""" + return 1.0 - self.pauli_fidelity + + @cached_property + def stochastic_infidelity(self) -> float: + """Stochastic (incoherent) component of the process infidelity.""" + return float(qx.stochastic_infidelity(self.noise_process)) + + @cached_property + def stochastic_fidelity(self) -> float: + """Stochastic fidelity :math:`1 - e_S`.""" + return 1.0 - self.stochastic_infidelity + + @cached_property + def coherent_infidelity(self) -> float: + """Coherent component of the process infidelity: :math:`e_C = e - e_S`.""" + return self.pauli_infidelity - self.stochastic_infidelity + + @cached_property + def coherent_fidelity(self) -> float: + """Coherent fidelity :math:`1 - e_C`.""" + return 1.0 - self.coherent_infidelity + + @cached_property + def unitarity(self) -> float: + """Unitarity of the channel.""" + return float(qx.unitarity(self.noise_process)) + + # ────────────────────────────────────────────── + # Channel analysis methods + # ────────────────────────────────────────────── + + def pauli_twirl(self) -> "Channel": + """ + Return a Pauli-twirled version of this channel. + + Pauli twirling projects the channel onto the Pauli diagonal, eliminating + off-diagonal coherences in the Pauli-Liouville representation. The + resulting channel is a stochastic Pauli channel with the same diagonal + error rates. + """ + ptm = qx.to_pauli_liouville(self.process) + # Keep only the diagonal of the PTM + twirled_ptm_matrix = jnp.diag(jnp.diag(ptm.matrix)) + twirled_superop = qx.to_superop(qx.PauliLiouville.from_matrix(twirled_ptm_matrix, self.process.dims)) + return replace(self, process=twirled_superop) + + @cached_property + def _unitary_error_component(self) -> Array: + """ + Extract the dominant unitary from the noise-only channel. + + Uses eigendecomposition + SVD polar decomposition to find the closest + unitary to the noise channel. + """ + choi_matrix = qx.to_choi(self.noise_process).matrix + d = 2**self.num_qubits + + # Dominant eigenvector of the Choi matrix + eigenvalues, eigenvectors = jnp.linalg.eigh(choi_matrix) + dominant_eigenvector = eigenvectors[:, jnp.argmax(jnp.abs(eigenvalues))] + + # SVD polar decomposition to extract the closest unitary + u, _, vh = jnp.linalg.svd(dominant_eigenvector.reshape(d, d).T) + return u @ vh + + def to_coherent_channel(self) -> "Channel": + """ + Isolate the coherent (unitary) component of the noise. + + Extracts the dominant unitary from the noise Choi matrix via polar + decomposition and returns a channel consisting of that unitary error + composed with the ideal gate. + """ + u_error = self._unitary_error_component + u_error_qx = qx.Unitary.from_matrix(u_error, self.process.dims) + coherent_superop = qx.to_superop(u_error_qx @ self.unitary) + return replace(self, process=coherent_superop) + + def to_stochastic_channel(self) -> "Channel": + """ + Isolate the stochastic (incoherent) component of the noise. + + The full channel decomposes as + :math:`\\mathcal{E} = \\mathcal{S} \\circ \\mathcal{U}_{\\mathrm{err}} \\circ \\mathcal{U}_{\\mathrm{gate}}`. + This method factors out the coherent unitary error and returns + :math:`\\mathcal{S} \\circ \\mathcal{U}_{\\mathrm{gate}}`. + """ + u_error = self._unitary_error_component + # Get the noise-only superoperator and compose with U_err† + noise_superop = self.noise_process.matrix + u_err_inv_superop = jnp.kron(u_error.conj(), u_error.conj().T) + stochastic_noise_superop = noise_superop @ u_err_inv_superop + # Recompose with the ideal gate + ideal_superop = jnp.kron(self.unitary.matrix, self.unitary.matrix.conj()) + stochastic_superop = stochastic_noise_superop @ ideal_superop + return replace(self, process=qx.SuperOp.from_matrix(stochastic_superop, self.process.dims)) + + def is_pauli(self) -> bool: + """ + Check if the noise channel is a Pauli (stochastic Pauli) channel. + + A Pauli channel has a diagonal Pauli transfer matrix (noise-only part). + """ + ptm = qx.to_pauli_liouville(self.noise_process).matrix + mask = ~jnp.eye(ptm.shape[0], dtype=bool) + return bool(jnp.allclose(ptm[mask], 0)) + + def to_pauli_vector(self) -> Array: + """ + Convert the noise channel to a Pauli error probability vector. + + Returns the vector of probabilities for each Pauli error in lexicographic + order (II, IX, IY, IZ, XI, XX, ...). The vector sums to 1.0. + """ + noise_superop = self.noise_process.matrix + num_qubits = self.num_qubits + dim = noise_superop.shape[0] + + # Build all Pauli operators and their superoperators + pauli_matrices = qx.ensembles.PAULIS.matrix # (4, 2, 2): I, X, Y, Z + all_pauli_products = list(product(pauli_matrices, repeat=num_qubits)) + pauli_error_rates = [] + for pauli_tuple in all_pauli_products: + pauli_op = reduce(jnp.kron, pauli_tuple) + pauli_superop = jnp.kron(pauli_op, pauli_op.conj()) + rate = float(jnp.abs(jnp.trace(noise_superop @ pauli_superop) / dim)) + pauli_error_rates.append(rate) + + return jnp.array(pauli_error_rates, dtype=float) + + @cached_property + def pauli_vector(self) -> Array: + """The Pauli error probability vector of the noise channel.""" + return self.to_pauli_vector() + + # ────────────────────────────────────────────── + # Visualization + # ────────────────────────────────────────────── + + def plot(self, only_noise: bool = True, show_identity: bool = False) -> Figure: + """ + Plot the Pauli transfer matrix of the channel. + + :param only_noise: If True, plot the noise-only channel (gate unitary factored out). + If False, plot the full channel including the gate unitary. + :param show_identity: If True, include the identity component in the noise-only plot. + If False (default), visualize the generator of the noise channel via the matrix + logarithm of the PTM. For near-identity noise this approximates PTM - I, but + correctly captures the Lie-algebraic structure of the channel. + Only applies when ``only_noise=True``. + :return: A Plotly Figure. + """ + if only_noise: + channel = self.noise_process + if not show_identity: + ptm = qx.to_pauli_liouville(channel) + log_ptm = scipy_logm(np.asarray(ptm.matrix)) + channel = qx.PauliLiouville.from_matrix(jnp.array(log_ptm), channel.dims) + title_prefix = "Noise Channel" + else: + channel = self.process + title_prefix = "Full Channel" + + fig = qx.plot(channel) + fig.update_layout( + title=( + f"{title_prefix} for {self.inst.out()}
" + f"𝜀={self.pauli_infidelity * 100:.2f}%, " + f"𝜀u={self.coherent_infidelity * 100:.2f}%, " + f"𝜀s={self.stochastic_infidelity * 100:.2f}%" + ) + ) + return fig + + # ────────────────────────────────────────────── + # Serialization + # ────────────────────────────────────────────── + + def to_json(self) -> str: + """ + Serialize Channel to a JSON string. + + :return: JSON string representation. + """ + superop_array = np.asarray(self.process.matrix) + flat_data = [[float(val.real), float(val.imag)] for val in superop_array.flat] + + data = { + "inst": self.inst.out(), + "superop": {"_complex_array": flat_data, "shape": list(superop_array.shape)}, + } + + if self.target_unitary is not None: + u_array = np.asarray(self.target_unitary.matrix) + u_flat = [[float(val.real), float(val.imag)] for val in u_array.flat] + data["target_unitary"] = {"_complex_array": u_flat, "shape": list(u_array.shape)} + + return json.dumps(data) + + @classmethod + def from_json(cls: Type["Channel"], json_str: str) -> "Channel": + """ + Deserialize a Channel from a JSON string. + + :param json_str: JSON string as produced by :meth:`to_json`. + :return: Channel instance. + """ + data = json.loads(json_str) + inst = _parse_quil_instruction(data["inst"]) + assert isinstance(inst, Gate) + + superop_data = data["superop"] + flat = superop_data["_complex_array"] + shape = tuple(superop_data["shape"]) + superop_array = jnp.array([complex(pair[0], pair[1]) for pair in flat], dtype=complex).reshape(shape) + # Infer dims from matrix shape: (d^2, d^2) -> d qubits each of dim 2 + d = int(jnp.sqrt(shape[0])) + num_qubits = int(jnp.round(jnp.log2(d))) + dims = ((2,) * num_qubits, (2,) * num_qubits) + superop = qx.SuperOp.from_matrix(superop_array, dims) + + target_unitary = None + if "target_unitary" in data: + u_data = data["target_unitary"] + u_flat = u_data["_complex_array"] + u_shape = tuple(u_data["shape"]) + u_array = jnp.array([complex(pair[0], pair[1]) for pair in u_flat], dtype=complex).reshape(u_shape) + u_num_qubits = int(jnp.round(jnp.log2(u_shape[0]))) + u_dims = ((2,) * u_num_qubits, (2,) * u_num_qubits) + target_unitary = qx.Unitary.from_matrix(u_array, u_dims) + + return cls(inst=inst, process=superop, target_unitary=target_unitary) + + # ────────────────────────────────────────────── + # Dunder methods + # ────────────────────────────────────────────── + + def __repr__(self) -> str: + """Return a simplified string representation showing the gate and process fidelity.""" + return f"<{self.inst.out()} ~ ({100 * self.pauli_fidelity:.2f}%)>" + + def __eq__(self, other: object) -> bool: + """Check equality based on instruction and process fidelity.""" + if not isinstance(other, Channel): + return False + if self.inst != other.inst: + return False + return bool(jnp.isclose(float(qx.process_fidelity(self.process, other.process)), 1.0, atol=1e-9)) + + def __hash__(self) -> int: + """Hash based on the instruction (for use in sets/dicts).""" + return hash(self.inst) + + def __matmul__(self, other: "Channel") -> "Channel": + """ + Compose two channels: ``channel_B @ channel_A``. + + Both channels share the same gate instruction. The composition factors + out one copy of the gate unitary so the result represents the sequential + application of the two noisy processes: + + :math:`\\mathcal{E}_B \\circ \\mathcal{U}^\\dagger \\circ \\mathcal{E}_A` + + This is the natural composition: if ``channel_A`` already includes the + gate, applying ``channel_B`` after it should not double-count the gate. + """ + if not isinstance(other, Channel): + return NotImplemented + if self.inst != other.inst: + raise ValueError(f"Cannot compose channels for different gates: {self.inst.out()} vs {other.inst.out()}") + # E_B @ U† @ E_A (factor out one gate unitary between the two channels) + u_dag_superop = qx.to_superop(self.unitary.h) + composed_superop = qx.to_superop(self.process @ u_dag_superop @ other.process) + return replace(self, process=composed_superop) + + def __or__(self, other: "Channel | MeasurementChannel") -> "CycleChannel": + """ + Tensor product of two channels on disjoint qubits, producing a CycleChannel. + + The result represents a cycle containing both operations acting in parallel + on disjoint qubits. The DefCircuit encodes the parallel operations as + formal instructions. + + :param other: Another Channel or MeasurementChannel on disjoint qubits. + :return: A CycleChannel representing the tensor product. + """ + if not isinstance(other, (Channel, MeasurementChannel)): + return NotImplemented + + # Validate disjoint qubits + self_qubits = set(self.qubits) + other_qubits = set(other.qubits) + if self_qubits & other_qubits: + raise ValueError(f"Cannot tensor channels with overlapping qubits: {self_qubits & other_qubits}") + + return _build_cycle_channel([self, other]) + + +@dataclass(frozen=True) +class MeasurementChannel: + """ + A measurement noise channel attaches a quantum instrument to a specific measurement operation. + + The ``process`` field is a ``qx.QuantumInstrument`` which models both classification + errors and post-measurement back-action. + """ + + inst: Measurement + """The measurement operation to which the channel applies.""" + + process: qx.QuantumInstrument + """A quantum instrument representation of the noisy measurement.""" + + @cached_property + def qubits(self) -> List[int]: + """The qubits which the measurement applies to.""" + qubit = self.inst.qubit + return [qubit.index if hasattr(qubit, "index") else int(qubit)] # type: ignore[union-attr,arg-type] + + # ────────────────────────────────────────────── + # Constructors + # ────────────────────────────────────────────── + + @classmethod + def from_readout_fidelity( + cls: Type["MeasurementChannel"], + inst: Measurement, + fidelity: float, + asymmetry: float = 0.0, + dim: int = 2, + ) -> "MeasurementChannel": + """ + Create a readout quantum instrument with optional asymmetry. + + Produces a perfectly QND measurement with the given classification fidelity. + Error is distributed only between adjacent levels: P(j+1|j) and P(j|j+1). + Non-adjacent confusion is zero. + + :param inst: The measurement instruction. + :param fidelity: The average readout fidelity. + :param asymmetry: Value between -1 and +1. Zero is symmetric. + Positive biases toward upward confusion P(j+1|j), negative toward downward P(j|j+1). + :param dim: The dimension of the measured system (2 for qubits, 3 for qutrits, etc.). + :return: A MeasurementChannel instance. + """ + # Compute per-pair error factor so that the average diagonal equals fidelity. + # Each adjacent pair (j, j+1) contributes error_factor*(1+a) + error_factor*(1-a) + # = 2*error_factor to total off-diagonal sum. With (dim-1) pairs, the average + # column error is 2*(dim-1)*error_factor/dim, which we set equal to (1-fidelity). + error_factor = dim * (1 - fidelity) / (2 * (dim - 1)) + + confusion = jnp.zeros((dim, dim)) + for j in range(dim - 1): + confusion = confusion.at[j + 1, j].set(error_factor * (1 + asymmetry)) + confusion = confusion.at[j, j + 1].set(error_factor * (1 - asymmetry)) + # Set diagonal so each column sums to 1 + col_sums = confusion.sum(axis=0) + confusion = confusion + jnp.diag(1 - col_sums) + + transition = jnp.eye(dim) + instrument = qx.instrument_from_confusion_and_transition( + confusion_matrix=confusion, + transition_matrix=transition, + dims=(dim,), + measured_qudits=(0,), + ) + return cls(inst=inst, process=instrument) + + @classmethod + def from_confusion_and_transition( + cls: Type["MeasurementChannel"], + inst: Measurement, + confusion_matrix: Array, + transition_matrix: Array, + ) -> "MeasurementChannel": + """ + Create a MeasurementChannel from a confusion matrix and a transition matrix. + + Provides independent control over measurement classification accuracy + and post-measurement quantum state evolution. + + **Matrix Conventions (column-stochastic):** + + - ``confusion_matrix[i, j]``: P(outcome i | prepared j) + - ``transition_matrix[k, j]``: P(ending in k | input j) + - Columns sum to 1.0 + + :param inst: The measurement instruction. + :param confusion_matrix: A (d, d) classification matrix. + :param transition_matrix: A (d, d) post-measurement transition matrix. + :return: A MeasurementChannel instance. + """ + confusion = jnp.asarray(confusion_matrix) + dim = confusion.shape[0] + instrument = qx.instrument_from_confusion_and_transition( + confusion_matrix=confusion, + transition_matrix=jnp.asarray(transition_matrix), + dims=(dim,), + measured_qudits=(0,), + ) + return cls(inst=inst, process=instrument) + + @classmethod + def from_axis( + cls: Type["MeasurementChannel"], + inst: Measurement, + theta: float = 0.0, + phi: float = 0.0, + sharpness: float = 1.0, + ) -> "MeasurementChannel": + """ + Create a MeasurementChannel from a Bloch sphere measurement axis. + + The angles refer to the standard Bloch sphere notation. + Theta=0, phi=0 is the Z axis (computational basis measurement). + + :param inst: The measurement instruction. + :param theta: The colatitude with respect to the z-axis. + :param phi: The longitude with respect to the x-axis. + :param sharpness: The sharpness of the measurement. 1.0 is projective, + 0.0 is no measurement. 0 < s < 1 is a weak measurement. + :return: A MeasurementChannel instance. + """ + instrument = qx.instrument_from_axis( + theta=theta, + phi=phi, + sharpness=sharpness, + ) + return cls(inst=inst, process=instrument) + + @classmethod + def from_binary_discriminator( + cls: Type["MeasurementChannel"], + inst: Measurement, + dim: int, + threshold: int, + fidelity: float = 1.0, + ) -> "MeasurementChannel": + """ + Create a MeasurementChannel for a binary discriminator. + + Models a measurement that confuses each state at or above ``threshold`` with + the state one level below it. This is useful for measurements calibrated as + binary discriminators between groups of energy levels. + + For example, ``threshold=2, dim=3`` always confuses state 2 for state 1 + (discriminates ``{0, 1}`` vs ``{2}``). ``threshold=1, dim=3`` confuses + state 1 for state 0 and state 2 for state 1 (discriminates ``{0}`` vs ``{1, 2}``). + + An optional ``fidelity`` parameter degrades the ideal discriminator with + uniform classification noise. + + :param inst: The measurement instruction. + :param dim: The dimension of the measured system. + :param threshold: States at or above this level are confused with the level below. + Must satisfy ``1 <= threshold < dim``. + :param fidelity: Additional classification fidelity applied on top of the + discrimination (1.0 = perfect discriminator). + :return: A MeasurementChannel instance. + """ + if not (1 <= threshold < dim): + raise ValueError(f"threshold must satisfy 1 <= threshold < dim, got threshold={threshold}, dim={dim}") + + # Build the ideal binary discriminator confusion matrix: + # states below threshold are classified correctly, + # states at or above threshold are classified as the state one below. + confusion = jnp.zeros((dim, dim)) + for j in range(dim): + if j < threshold: + confusion = confusion.at[j, j].set(1.0) + else: + confusion = confusion.at[j - 1, j].set(1.0) + + # Optionally degrade with uniform noise + if fidelity < 1.0: + confusion = fidelity * confusion + (1 - fidelity) * jnp.ones((dim, dim)) / dim + + transition = jnp.eye(dim) + instrument = qx.instrument_from_confusion_and_transition( + confusion_matrix=confusion, + transition_matrix=transition, + dims=(dim,), + measured_qudits=(0,), + ) + return cls(inst=inst, process=instrument) + + # ────────────────────────────────────────────── + # Properties + # ────────────────────────────────────────────── + + @cached_property + def confusion_matrix(self) -> Array: + """The confusion matrix of the measurement. + + Shape ``(num_outcomes, d_measured)``. + Entry ``[i, j]`` is P(outcome i | prepared j). + """ + return self.process.confusion_matrix + + @cached_property + def transition_matrix(self) -> Array: + """The post-measurement transition matrix. + + Shape ``(d, d)``. Entry ``[k, j]`` is P(ending in k | input j), + marginalized over all measurement outcomes. + """ + return self.process.transition_matrix + + @cached_property + def non_demolition_fidelity(self) -> float: + """Quantum non-demolition (QND) fidelity. + + Measures how well the measurement preserves computational basis states, + averaged over outcomes and input states. + """ + return float(qx.non_demolition_fidelity(self.process)) + + @cached_property + def instrument_fidelity(self) -> float: + """Overall instrument fidelity w.r.t. ideal QND measurement. + + Accounts for both classification errors and post-measurement state disturbance. + """ + return float(qx.instrument_fidelity(self.process)) + + @cached_property + def classification_fidelity(self) -> float: + """Classification fidelity: average probability of correctly identifying the measurement outcome.""" + return float(qx.classification_fidelity(self.process)) + + # ────────────────────────────────────────────── + # Visualization + # ────────────────────────────────────────────── + + def plot(self) -> Figure: + """ + Plot the quantum instrument using the quax visualization. + + Shows per-outcome superoperator matrices and the total CPTP channel. + + :return: A Plotly Figure. + """ + fig = qx.plot(self.process) + fig.update_layout( + title=( + f"Quantum Instrument MEASURE {self.qubits[0]}
" + f"Cls: {100 * self.classification_fidelity:.2f}%, " + f"QND: {100 * self.non_demolition_fidelity:.2f}%, " + f"Instrument: {100 * self.instrument_fidelity:.2f}%" + ) + ) + return fig + + # ────────────────────────────────────────────── + # Serialization + # ────────────────────────────────────────────── + + def to_json(self) -> str: + """ + Serialize MeasurementChannel to a JSON string. + + :return: JSON string representation. + """ + # Store per-outcome Choi matrices + instrument_data = [] + for i in range(self.process.num_outcomes): + choi_i, _ = self.process.outcome_choi(i) + choi_array = np.asarray(choi_i.matrix) + flat = [[float(val.real), float(val.imag)] for val in choi_array.flat] + instrument_data.append({"_complex_array": flat, "shape": list(choi_array.shape)}) + + data = { + "inst": self.inst.out(), + "instruments": instrument_data, + "measured_qudits": list(self.process.measured_qudits), + } + return json.dumps(data) + + @classmethod + def from_json(cls: Type["MeasurementChannel"], json_str: str) -> "MeasurementChannel": + """ + Deserialize a MeasurementChannel from a JSON string. + + :param json_str: JSON string as produced by :meth:`to_json`. + :return: MeasurementChannel instance. + """ + data = json.loads(json_str) + inst = _parse_quil_instruction(data["inst"]) + assert isinstance(inst, Measurement) + measured_qudits = tuple(data["measured_qudits"]) + + choi_list = [] + for inst_data in data["instruments"]: + flat = inst_data["_complex_array"] + shape = tuple(inst_data["shape"]) + arr = jnp.array([complex(pair[0], pair[1]) for pair in flat], dtype=complex).reshape(shape) + d = int(jnp.sqrt(shape[0])) + n_qubits = int(jnp.round(jnp.log2(d))) + choi_dims = ((2,) * n_qubits, (2,) * n_qubits) + choi_list.append(qx.Choi.from_matrix(arr, choi_dims)) + + instrument = qx.QuantumInstrument.from_choi(choi_list, measured_qudits) + return cls(inst=inst, process=instrument) + + # ────────────────────────────────────────────── + # Dunder methods + # ────────────────────────────────────────────── + + def __repr__(self) -> str: + """Return a simplified string representation.""" + return f"" + + def __eq__(self, other: object) -> bool: + """Check equality based on instruction and operator.""" + if not isinstance(other, MeasurementChannel): + return False + if self.inst != other.inst: + return False + return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) + + def __hash__(self) -> int: + """Hash based on the instruction.""" + return hash(self.inst) + + def __matmul__(self, other: "MeasurementChannel") -> "MeasurementChannel": + """ + Compose two measurement channels on the same qubit. + + Models sequential application: ``channel_B @ channel_A`` means + apply ``channel_A`` first, then ``channel_B``. + """ + if not isinstance(other, MeasurementChannel): + return NotImplemented + if self.inst != other.inst: + raise ValueError( + f"Cannot compose measurement channels for different qubits: {self.inst.out()} vs {other.inst.out()}" + ) + composed = self.process @ other.process + return replace(self, process=composed) + + def __or__(self, other: "Channel | MeasurementChannel") -> "CycleChannel": + """ + Tensor product of two channels on disjoint qubits, producing a CycleChannel. + + :param other: Another Channel or MeasurementChannel on disjoint qubits. + :return: A CycleChannel representing the tensor product. + """ + if not isinstance(other, (Channel, MeasurementChannel)): + return NotImplemented + + self_qubits = set(self.qubits) + other_qubits = set(other.qubits) + if self_qubits & other_qubits: + raise ValueError(f"Cannot tensor channels with overlapping qubits: {self_qubits & other_qubits}") + + return _build_cycle_channel([self, other]) + + +@dataclass(frozen=True) +class ResetChannel: + """ + A reset noise channel attaches a superoperator to a specific reset operation. + + The ``process`` field is a ``qx.SuperOp`` which *includes* the ideal reset, so the channel + replaces the reset instruction rather than being applied after it. + """ + + inst: Reset + """The reset operation to which the channel applies.""" + + process: qx.SuperOp + """A superoperator representation of the noisy reset (including ideal reset).""" + + # ────────────────────────────────────────────── + # Constructors + # ────────────────────────────────────────────── + + @classmethod + def from_reset_fidelity( + cls: Type["ResetChannel"], + inst: Reset, + fidelity: float, + dim: int = 2, + ) -> "ResetChannel": + """ + Create a ResetChannel with depolarizing noise scaled to the given process fidelity. + + The ideal reset channel maps every state to :math:`|0\\rangle\\langle 0|`. Noise is + modelled as a depolarising channel applied after the ideal reset. + + :param inst: The reset instruction. + :param fidelity: Process fidelity of the reset channel, :math:`F \\in [0, 1]`. + 1.0 yields an ideal reset; values below 1 introduce depolarising noise. + :param dim: Hilbert-space dimension (2 for qubits). + :return: A ResetChannel instance. + """ + ideal_superop = qx.gates.RESET(dim=dim) + p = 1.0 - fidelity + d2 = dim * dim + # Depolarising channel in superop form: (1-p)*S_ideal + p*(I/d) for all inputs + # The completely depolarising superop maps everything to I/d: + # its rows are all zero except the diagonal entries corresponding to + # the trace extraction (maps vec(ρ) → vec(I/d) = vec(I)/d). + depol_superop_matrix = jnp.zeros((d2, d2), dtype=complex) + # vec(I/d) has value 1/d at positions 0, d+1, 2(d+1), ... i.e. diagonal entries + vec_identity_over_d = jnp.zeros(d2, dtype=complex) + for i in range(dim): + vec_identity_over_d = vec_identity_over_d.at[i * dim + i].set(1.0 / dim) + # The trace functional extracts sum of diagonal: positions 0, d+1, ... + trace_row = jnp.zeros(d2, dtype=complex) + for i in range(dim): + trace_row = trace_row.at[i * dim + i].set(1.0) + # Depolarising superop: each row of output is vec(I/d) * Tr(ρ) + depol_superop_matrix = jnp.outer(vec_identity_over_d, trace_row) + noisy_superop_matrix = (1.0 - p) * ideal_superop.matrix + p * depol_superop_matrix + noisy_superop = qx.SuperOp.from_matrix(noisy_superop_matrix, ideal_superop.dims) + return cls(inst=inst, process=noisy_superop) + + # ────────────────────────────────────────────── + # Properties + # ────────────────────────────────────────────── + + @cached_property + def qubits(self) -> List[int]: + """The qubit(s) that the reset applies to.""" + qubit = self.inst.qubit + if qubit is None: + return [] + return [qubit.index if hasattr(qubit, "index") else int(qubit)] # type: ignore[union-attr,arg-type] + + @cached_property + def fidelity(self) -> float: + """Process fidelity of the reset channel relative to the ideal reset. + + Defined as :math:`F = \\mathrm{Tr}[\\Lambda_{\\mathrm{ideal}}^\\dagger \\Lambda] / d^2` + where :math:`\\Lambda` is the Choi matrix of the noisy channel and + :math:`\\Lambda_{\\mathrm{ideal}}` is the ideal-reset Choi. + """ + dim = self.process.dims[0][0] + ideal_choi = qx.to_choi(qx.gates.RESET(dim=dim)) + noisy_choi = qx.to_choi(self.process) + # Process fidelity = Tr[ideal_choi† @ noisy_choi] / d^2 + d2 = float(dim * dim) + return float(jnp.real(jnp.trace(ideal_choi.matrix.conj().T @ noisy_choi.matrix)) / d2) + + @cached_property + def noise_process(self) -> qx.SuperOp: + """The noise-only channel (ideal reset factored out). + + For a reset channel the noise framing is less natural than for unitary gates; + this property returns the full process superoperator. + """ + return self.process + + # ────────────────────────────────────────────── + # Visualization + # ────────────────────────────────────────────── + + def plot(self) -> Figure: + """ + Plot the Pauli transfer matrix of the reset channel. + + :return: A Plotly Figure. + """ + fig = qx.plot(self.process) + qubit_str = str(self.qubits[0]) if self.qubits else "?" + fig.update_layout(title=(f"Reset Channel RESET {qubit_str}
F_\u03c7={self.fidelity * 100:.2f}%")) + return fig + + # ────────────────────────────────────────────── + # Serialization + # ────────────────────────────────────────────── + + def to_json(self) -> str: + """ + Serialize ResetChannel to a JSON string. + + :return: JSON string representation. + """ + superop_array = np.asarray(self.process.matrix) + flat = [[float(v.real), float(v.imag)] for v in superop_array.flat] + data = { + "inst": self.inst.out(), + "superop": {"_complex_array": flat, "shape": list(superop_array.shape)}, + } + return json.dumps(data) + + @classmethod + def from_json(cls: Type["ResetChannel"], json_str: str) -> "ResetChannel": + """ + Deserialize a ResetChannel from a JSON string. + + :param json_str: JSON string as produced by :meth:`to_json`. + :return: ResetChannel instance. + """ + data = json.loads(json_str) + inst = _parse_quil_instruction(data["inst"]) + assert isinstance(inst, Reset) + superop_data = data["superop"] + flat = superop_data["_complex_array"] + shape = tuple(superop_data["shape"]) + arr = jnp.array([complex(pair[0], pair[1]) for pair in flat], dtype=complex).reshape(shape) + d = int(jnp.sqrt(shape[0])) + num_qubits = int(jnp.round(jnp.log2(d))) + dims = ((2,) * num_qubits, (2,) * num_qubits) + process = qx.SuperOp.from_matrix(arr, dims) + return cls(inst=inst, process=process) + + # ────────────────────────────────────────────── + # Dunder methods + # ────────────────────────────────────────────── + + def __repr__(self) -> str: + """Return a simplified string representation.""" + qubit_str = str(self.qubits[0]) if self.qubits else "?" + return f"" + + def __eq__(self, other: object) -> bool: + """Check equality based on instruction and process matrix.""" + if not isinstance(other, ResetChannel): + return False + if self.inst != other.inst: + return False + return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) + + def __hash__(self) -> int: + """Hash based on the instruction.""" + return hash(self.inst) + + +@dataclass(frozen=True) +class CycleChannel: + """ + A cycle noise channel attaches superoperators to a specific cycle. + + Cycles can include gates and measurements. The constituent channels are stored + directly, allowing fidelity metrics and serialization to be derived from them. + """ + + inst: Gate + """The cycle to which the channel applies.""" + + defcircuit: DefCircuit + """The DefCircuit representing the logical cycle to which instruction represents.""" + + channels: Tuple["Channel | MeasurementChannel", ...] + """Constituent channels (one per operation in the cycle) on disjoint qubits.""" + + # ────────────────────────────────────────────── + # Derived properties + # ────────────────────────────────────────────── + + @cached_property + def operator(self) -> Tuple[qx.SuperOp | qx.QuantumInstrument, ...]: + """Tuple of process superoperators, one per constituent channel.""" + return tuple(ch.process for ch in self.channels) + + @cached_property + def qubits(self) -> List[int]: + """All qubits in the cycle, derived from the instruction.""" + return self.inst.get_qubit_indices() + + @cached_property + def pauli_fidelity(self) -> float: + """Product of process (Pauli) fidelities over all gate channels in the cycle. + + Measurement channels do not contribute a gate fidelity and are skipped. + For near-ideal noise the product approximation is exact since constituent + channels act on disjoint subsystems. + """ + f = 1.0 + for ch in self.channels: + if isinstance(ch, Channel): + f *= ch.pauli_fidelity + return f + + @cached_property + def fidelity(self) -> float: + """Product of average gate fidelities over all gate channels in the cycle. + + Measurement channels do not contribute a gate fidelity and are skipped. + """ + f = 1.0 + for ch in self.channels: + if isinstance(ch, Channel): + f *= ch.fidelity + return f + + @cached_property + def infidelity(self) -> float: + """``1 - fidelity``.""" + return 1.0 - self.fidelity + + @cached_property + def pauli_infidelity(self) -> float: + """``1 - pauli_fidelity``.""" + return 1.0 - self.pauli_fidelity + + # ────────────────────────────────────────────── + # Serialization + # ────────────────────────────────────────────── + + def to_json(self) -> str: + """ + Serialize CycleChannel to a JSON string. + + :return: JSON string representation. + """ + ch_data = [] + for ch in self.channels: + ch_data.append({"type": type(ch).__name__, "data": ch.to_json()}) + data = { + "channels": ch_data, + } + return json.dumps(data) + + @classmethod + def from_json(cls: Type["CycleChannel"], json_str: str) -> "CycleChannel": + """ + Deserialize a CycleChannel from a JSON string. + + The ``inst`` and ``defcircuit`` fields are reconstructed from the constituent + channels, consistent with how :func:`_build_cycle_channel` builds them. + + :param json_str: JSON string as produced by :meth:`to_json`. + :return: CycleChannel instance. + """ + data = json.loads(json_str) + _type_map: Dict[str, Type["Channel | MeasurementChannel"]] = { + "Channel": Channel, + "MeasurementChannel": MeasurementChannel, + } + constituent_channels: List["Channel | MeasurementChannel"] = [ + _type_map[ch_data["type"]].from_json(ch_data["data"]) # type: ignore[index] + for ch_data in data["channels"] + ] + return _build_cycle_channel(constituent_channels) + + # ────────────────────────────────────────────── + # Dunder methods + # ────────────────────────────────────────────── + + def __repr__(self) -> str: + """Return a simplified string representation showing the gate and process fidelity.""" + return f"<{self.inst.out()} ~ ({100 * self.pauli_fidelity:.2f}%)>" + + def __eq__(self, other: object) -> bool: + """Check equality based on instruction and constituent channels.""" + if not isinstance(other, CycleChannel): + return False + if self.inst != other.inst: + return False + return self.channels == other.channels + + def __hash__(self) -> int: + """Hash based on the instruction.""" + return hash(self.inst) + + +def _channel_to_formal_inst(channel: Channel | MeasurementChannel) -> Gate | Measurement: + """Convert a channel's instruction to use formal arguments for DefCircuit.""" + if isinstance(channel, Channel): + inst = channel.inst + return Gate( + name=inst.name, + params=inst.params, + qubits=[FormalArgument(f"q{q}") for q in inst.get_qubit_indices()], + modifiers=inst.modifiers, # type: ignore[arg-type] + ) + elif isinstance(channel, MeasurementChannel): + qubit_idx = channel.qubits[0] + return Measurement( + qubit=FormalArgument(f"q{qubit_idx}"), + classical_reg=None, + ) + raise TypeError(f"Unsupported channel type: {type(channel)}") + + +def _build_cycle_channel( + channels: List["Channel | MeasurementChannel"], +) -> "CycleChannel": + """Build a CycleChannel from a list of Channel/MeasurementChannel on disjoint qubits.""" + all_qubits = sorted(q for ch in channels for q in ch.qubits) + cycle_name = "CYCLE" + formal_insts = [_channel_to_formal_inst(ch) for ch in channels] + + defcircuit = DefCircuit( + name=cycle_name, + parameters=[], + qubits=[FormalArgument(f"q{q}") for q in all_qubits], + instructions=list(formal_insts), # type: ignore[arg-type] + ) + inst = Gate(name=cycle_name, params=[], qubits=all_qubits) + return CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels)) diff --git a/pyquil/noise/_legacy_noise.py b/pyquil/noise/_legacy_noise.py new file mode 100644 index 000000000..8e7c4f013 --- /dev/null +++ b/pyquil/noise/_legacy_noise.py @@ -0,0 +1,809 @@ +############################################################################## +# Copyright 2018 Rigetti Computing +# +# 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. +############################################################################## +"""Module for creating and verifying noisy gate and readout definitions.""" + +import sys +from collections import namedtuple +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Any, Optional, Union, cast + +import numpy as np +from deprecated import deprecated + +from pyquil.external.rpcq import CompilerISA +from pyquil.gates import MEASURE, RX, I +from pyquil.noise_gates import _get_qvm_noise_supported_gates +from pyquil.quilatom import MemoryReference, ParameterDesignator, format_parameter +from pyquil.quilbase import Declare, Gate, Pragma + +if TYPE_CHECKING: + from pyquil.api import QuantumComputer as PyquilApiQuantumComputer + from pyquil.quil import Program + +INFINITY = float("inf") +"Used for infinite coherence times." + +_KrausModel = namedtuple("_KrausModel", ["gate", "params", "targets", "kraus_ops", "fidelity"]) + + +class KrausModel(_KrausModel): + """Encapsulate a single gate's noise model. + + .. deprecated:: + Use :class:`pyquil.noise.Channel` for quax-based noise modeling. + + :ivar str gate: The name of the gate. + :ivar Sequence[float] params: Optional parameters for the gate. + :ivar Sequence[int] targets: The target qubit ids. + :ivar Sequence[np.array] kraus_ops: The Kraus operators (must be square complex numpy arrays). + :ivar float fidelity: The average gate fidelity associated with the Kraus map relative to the + ideal operation. + """ + + @staticmethod + def unpack_kraus_matrix(m: Union[list[Any], np.ndarray]) -> np.ndarray: + """Unpack a JSON compatible representation of a complex Kraus matrix. + + :param m: The representation of a Kraus operator. Either a complex + square matrix (as numpy array or nested lists) or a JSON-able pair of real matrices + (as nested lists) representing the element-wise real and imaginary part of m. + :return: A complex square numpy array representing the Kraus operator. + """ + matrix = np.asarray(m, dtype=complex) + if matrix.ndim == 3: + matrix = matrix[0] + 1j * matrix[1] + if not matrix.ndim == 2: # pragma no coverage + raise ValueError("Need 2d array.") + if not matrix.shape[0] == matrix.shape[1]: # pragma no coverage + raise ValueError("Need square matrix.") + return matrix + + def to_dict(self) -> dict[str, Any]: + """Create a dictionary representation of a KrausModel. + + For example:: + + { + "gate": "RX", + "params": np.pi, + "targets": [0], + "kraus_ops": [ # In this example single Kraus op = ideal RX(pi) gate + [ + [ + [0, 0], # element-wise real part of matrix + [0, 0], + ], + [ + [0, -1], # element-wise imaginary part of matrix + [-1, 0], + ], + ] + ], + "fidelity": 1.0, + } + + :return: A JSON compatible dictionary representation. + :rtype: dict[str,Any] + """ + res = self._asdict() + res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops] + return res + + @staticmethod + def from_dict(d: dict[str, Any]) -> "KrausModel": + """Recreate a KrausModel from the dictionary representation. + + :param d: The dictionary representing the KrausModel. See `to_dict` for an + example. + :return: The deserialized KrausModel. + """ + kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] + return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"]) + + def __eq__(self, other: object) -> bool: + """Return True if both KrausModels are equal.""" + return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() + + +_NoiseModel = namedtuple("_NoiseModel", ["gates", "assignment_probs"]) + + +@deprecated( + version="4.17.0", + reason="Use the quax-based noise model in pyquil.noise._noise_model instead. " + "This class will be removed in the next major version of pyquil.", +) +class NoiseModel(_NoiseModel): + """Encapsulate the QPU noise model containing information about the noisy gates. + + :ivar Sequence[KrausModel] gates: The tomographic estimates of all gates. + :ivar dict[int,np.array] assignment_probs: The single qubit readout assignment + probability matrices keyed by qubit id. + """ + + def to_dict(self) -> dict[str, Any]: + """Create a JSON serializable representation of the noise model. + + For example:: + + { + "gates": [ + # list of embedded dictionary representations of KrausModels here [...] + ] + "assignment_probs": { + "0": [[.8, .1], + [.2, .9]], + "1": [[.9, .4], + [.1, .6]], + } + } + + :return: A dictionary representation of self. + """ + return { + "gates": [km.to_dict() for km in self.gates], + "assignment_probs": {str(qid): a.tolist() for qid, a in self.assignment_probs.items()}, + } + + @staticmethod + def from_dict(d: dict[str, Any]) -> "NoiseModel": + """Re-create the noise model from a dictionary representation. + + :param d: The dictionary representation. + :return: The restored noise model. + """ + return NoiseModel( + gates=[KrausModel.from_dict(t) for t in d["gates"]], + assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, + ) + + def gates_by_name(self, name: str) -> list[KrausModel]: + """Return all defined noisy gates of a particular gate name. + + :param str name: The gate name. + :return: A list of noise models representing that gate. + """ + return [g for g in self.gates if g.gate == name] + + def __eq__(self, other: object) -> bool: + """Return True if NoiseModels are equal.""" + return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict() + + +def _check_kraus_ops(n: int, kraus_ops: Sequence[np.ndarray]) -> None: + """Verify that the Kraus operators are of the correct shape and satisfy the correct normalization. + + :param n: Number of qubits + :param kraus_ops: The Kraus operators as numpy.ndarrays. + """ + for k in kraus_ops: + if not np.shape(k) == (2**n, 2**n): + raise ValueError(f"Kraus operators for {n} qubits must have shape {2**n}x{2**n}: {k}") + + kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops) + if not np.allclose(kdk_sum, np.eye(2**n), atol=1e-3): + raise ValueError(f"Kraus operator not correctly normalized: sum_j K_j^*K_j == {kdk_sum}") + + +def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[np.ndarray]) -> list[Pragma]: + """Generate the pragmas to define a Kraus map for a specific gate on some qubits. + + :param name: The name of the gate. + :param qubit_indices: The qubits + :param kraus_ops: The Kraus operators as matrices. + :return: A QUIL string with PRAGMA ADD-KRAUS ... statements. + """ + pragmas = [ + Pragma( + "ADD-KRAUS", + (name,) + tuple(qubit_indices), + "({})".format(" ".join(map(format_parameter, np.ravel(k)))), + ) + for k in kraus_ops + ] + return pragmas + + +def append_kraus_to_gate( + kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray +) -> list[Union[np.number, np.ndarray]]: + """Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. + + :param kraus_ops: The Kraus operators. + :param gate_matrix: The unitary gate. + :return: A list of transformed Kraus operators. + """ + return [kj.dot(gate_matrix) for kj in kraus_ops] + + +def pauli_kraus_map(probabilities: Sequence[float]) -> list[np.ndarray]: + r"""Generate the Kraus operators corresponding to a pauli channel. + + :params probabilities: The 4^num_qubits list of probabilities specifying the + desired pauli channel. There should be either 4 or 16 probabilities specified in the + order I, X, Y, Z for 1 qubit or II, IX, IY, IZ, XI, XX, XY, etc for 2 qubits. + + For example:: + + The d-dimensional depolarizing channel \Delta parameterized as + \Delta(\rho) = p \rho + [(1-p)/d] I + is specified by the list of probabilities + [p + (1-p)/d, (1-p)/d, (1-p)/d), ... , (1-p)/d)] + + :return: A list of the 4^num_qubits Kraus operators that parametrize the map. + """ + if len(probabilities) not in [4, 16]: + raise ValueError( + "Currently we only support one or two qubits, " + "so the provided list of probabilities must have length 4 or 16." + ) + if not np.allclose(sum(probabilities), 1.0, atol=1e-3): + raise ValueError("Probabilities must sum to one.") + + paulis = [ + np.eye(2), + np.array([[0, 1], [1, 0]]), + np.array([[0, -1j], [1j, 0]]), + np.array([[1, 0], [0, -1]]), + ] + + if len(probabilities) == 4: + operators = paulis + else: + operators = np.kron(paulis, paulis) # type: ignore + + return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] + + +def damping_kraus_map(p: float = 0.10) -> list[np.ndarray]: + """Generate the Kraus operators corresponding to an amplitude damping noise channel. + + :param p: The one-step damping probability. + :return: A list [k1, k2] of the Kraus operators that parametrize the map. + :rtype: list + """ + damping_op = np.sqrt(p) * np.array([[0, 1], [0, 0]]) + + residual_kraus = np.diag([1, np.sqrt(1 - p)]) + return [residual_kraus, damping_op] + + +def dephasing_kraus_map(p: float = 0.10) -> list[np.ndarray]: + """Generate the Kraus operators corresponding to a dephasing channel. + + :params float p: The one-step dephasing probability. + :return: A list [k1, k2] of the Kraus operators that parametrize the map. + :rtype: list + """ + return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])] + + +def tensor_kraus_maps(k1: list[np.ndarray], k2: list[np.ndarray]) -> list[np.ndarray]: + """Generate the Kraus map corresponding to the composition of two maps on different qubits. + + :param k1: The Kraus operators for the first qubit. + :param k2: The Kraus operators for the second qubit. + :return: A list of tensored Kraus operators. + """ + return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2] + + +def combine_kraus_maps(k1: list[np.ndarray], k2: list[np.ndarray]) -> list[np.ndarray]: + """Generate the Kraus map for two composed maps, with k1 applied after k2 on the same qubits. + + :param k1: The list of Kraus operators that are applied second. + :param k2: The list of Kraus operators that are applied first. + :return: A combinatorially generated list of composed Kraus operators. + """ + return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2] + + +def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> list[np.ndarray]: + """Generate the Kraus map for a dephasing channel followed by an amplitude damping channel. + + :param T1: The amplitude damping time + :param T2: The dephasing time + :param gate_time: The gate duration. + :return: A list of Kraus operators. + """ + if T1 < 0 or T2 < 0: + raise ValueError("T1 and T2 must be non-negative.") + + if T1 != INFINITY: + damping = damping_kraus_map(p=1 - np.exp(-float(gate_time) / float(T1))) + else: + damping = [np.eye(2)] + + if T2 != INFINITY: + gamma_phi = float(gate_time) / float(T2) + if T1 != INFINITY: + if T2 > 2 * T1: + raise ValueError("T2 is upper bounded by 2 * T1") + gamma_phi -= float(gate_time) / float(2 * T1) + + dephasing = dephasing_kraus_map(p=0.5 * (1 - np.exp(-gamma_phi))) + else: + dephasing = [np.eye(2)] + return combine_kraus_maps(damping, dephasing) + + +# You can only apply gate-noise to non-parametrized gates or parametrized gates at fixed parameters. +NO_NOISE = ["RZ"] +ANGLE_TOLERANCE = 1e-10 + + +class NoisyGateUndefined(Exception): + """Raise when user attempts to use noisy gate outside of currently supported set.""" + + pass + + +def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> tuple[np.ndarray, str]: + """Look up the numerical gate representation and a proposed 'noisy' name. + + :param gate_name: The Quil gate name + :param params: The gate parameters. + :return: A tuple (matrix, noisy_name) with the representation of the ideal gate matrix + and a proposed name for the noisy version. + """ + params = tuple(params) + if gate_name == "I": + if params != (): + raise ValueError(f"Identity gate does not take parameters: {params}") + return np.eye(2), "NOISY-I" + if gate_name == "RX": + (angle,) = params + if not isinstance(angle, (int, float, complex)): + raise TypeError(f"Cannot produce noisy gate for parameter of type {type(angle)}") + + if np.isclose(angle, np.pi / 2, atol=ANGLE_TOLERANCE): + return np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), "NOISY-RX-PLUS-90" + elif np.isclose(angle, -np.pi / 2, atol=ANGLE_TOLERANCE): + return np.array([[1, 1j], [1j, 1]]) / np.sqrt(2), "NOISY-RX-MINUS-90" + elif np.isclose(angle, np.pi, atol=ANGLE_TOLERANCE): + return np.array([[0, -1j], [-1j, 0]]), "NOISY-RX-PLUS-180" + elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE): + return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180" + elif gate_name == "CZ": + if params != (): + raise ValueError(f"CZ gate does not take parameters: {params}") + return np.diag([1, 1, 1, -1]), "NOISY-CZ" + + raise NoisyGateUndefined( + f"Undefined gate and params: {gate_name}{params}\n" "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ" + ) + + +def _get_program_gates(prog: "Program") -> list[Gate]: + """Get all gate applications appearing in prog. + + :param prog: The program + :return: A list of all Gates in prog (without duplicates). + """ + return sorted({i for i in prog if isinstance(i, Gate)}, key=lambda g: g.out()) + + +def _decoherence_noise_model( + gates: Sequence[Gate], + T1: Union[dict[int, float], float] = 30e-6, + T2: Union[dict[int, float], float] = 30e-6, + gate_time_1q: float = 50e-9, + gate_time_2q: float = 150e-09, + ro_fidelity: Union[dict[int, float], float] = 0.95, +) -> NoiseModel: + """Return default noise model. + + - T1 = 30 us + - T2 = 30 us + - 1q gate time = 50 ns + - 2q gate time = 150 ns + + are currently typical for near-term devices. + + This function will define new gates and add Kraus noise to these gates. It will translate + the input program to use the noisy version of the gates. + + :param gates: The gates to provide the noise model for. + :param T1: The T1 amplitude damping time either globally or in a + dictionary indexed by qubit id. By default, this is 30 us. + :param T2: The T2 dephasing time either globally or in a + dictionary indexed by qubit id. By default, this is also 30 us. + :param gate_time_1q: The duration of the one-qubit gates, namely RX(+pi/2) and RX(-pi/2). + By default, this is 50 ns. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. + By default, this is 150 ns. + :param ro_fidelity: The readout assignment fidelity + :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. + :return: A NoiseModel with the appropriate Kraus operators defined. + """ + all_qubits = set(sum((g.get_qubit_indices() for g in gates), [])) + if isinstance(T1, dict): + all_qubits.update(T1.keys()) + if isinstance(T2, dict): + all_qubits.update(T2.keys()) + if isinstance(ro_fidelity, dict): + all_qubits.update(ro_fidelity.keys()) + + if not isinstance(T1, dict): + T1 = {q: T1 for q in all_qubits} + + if not isinstance(T2, dict): + T2 = {q: T2 for q in all_qubits} + + if not isinstance(ro_fidelity, dict): + ro_fidelity = {q: ro_fidelity for q in all_qubits} + + noisy_identities_1q = { + q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_1q) for q in all_qubits + } + noisy_identities_2q = { + q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_2q) for q in all_qubits + } + kraus_maps = [] + for g in gates: + targets = tuple(g.get_qubit_indices()) + if g.name in NO_NOISE: + continue + matrix, _ = get_noisy_gate(g.name, g.params) + + if len(targets) == 1: + noisy_I = noisy_identities_1q[targets[0]] + else: + if len(targets) != 2: + raise ValueError("Noisy gates on more than 2Q not currently supported") + + # note this ordering of the tensor factors is necessary due to how the QVM orders + # the wavefunction basis + noisy_I = tensor_kraus_maps(noisy_identities_2q[targets[1]], noisy_identities_2q[targets[0]]) + kraus_maps.append( + KrausModel( + g.name, + tuple(g.params), + targets, + combine_kraus_maps(noisy_I, [matrix]), + # FIXME (Nik): compute actual avg gate fidelity for this simple + # noise model + 1.0, + ) + ) + aprobs = {} + for q, f_ro in ro_fidelity.items(): + aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) + + return NoiseModel(kraus_maps, aprobs) + + +def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p11: float = 0.911) -> NoiseModel: + """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. + + For simplicity, we use the default values for T1, T2, gate times, et al. and only allow + the specification of readout fidelities. + """ + gates = _get_qvm_noise_supported_gates(isa) + noise_model = _decoherence_noise_model(gates) + aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) + aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} + return NoiseModel(noise_model.gates, aprobs) + + +def _noise_model_program_header(noise_model: NoiseModel) -> "Program": + """Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates. + + The program header consists of 3 sections: + + - The ``DEFGATE`` statements that define the meaning of the newly introduced "noisy" gate + names. + - The ``PRAGMA ADD-KRAUS`` statements to overload these noisy gates on specific qubit + targets with their noisy implementation. + - THe ``PRAGMA READOUT-POVM`` statements that define the noisy readout per qubit. + + :param noise_model: The assumed noise model. + :return: A quil Program with the noise pragmas. + """ + from pyquil.quil import Program + + p = Program() + defgates: set[str] = set() + for k in noise_model.gates: + # obtain ideal gate matrix and new, noisy name by looking it up in the NOISY_GATES dict + try: + ideal_gate, new_name = get_noisy_gate(k.gate, tuple(k.params)) + + # if ideal version of gate has not yet been DEFGATE'd, do this + if new_name not in defgates: + p.defgate(new_name, ideal_gate) + defgates.add(new_name) + except NoisyGateUndefined: + print( + f"WARNING: Could not find ideal gate definition for gate {k.gate}", + file=sys.stderr, + ) + new_name = k.gate + + # define noisy version of gate on specific targets + p.define_noisy_gate(new_name, k.targets, k.kraus_ops) + + # define noisy readouts + for q, ap in noise_model.assignment_probs.items(): + p.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) + return p + + +def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": + """Apply a noise model to a program and generated a 'noisy-fied' version of the program. + + :param prog: A Quil Program object. + :param noise_model: A NoiseModel, either generated from an ISA or + from a simple decoherence model. + :return: A new program translated to a noisy gateset and with noisy readout as described by the + noisemodel. + """ + new_prog = _noise_model_program_header(noise_model) + for i in prog: + if isinstance(i, Gate) and noise_model.gates: + try: + _, new_name = get_noisy_gate(i.name, tuple(i.params)) + new_prog += Gate(new_name, [], i.qubits) + except NoisyGateUndefined: + new_prog += i + else: + new_prog += i + return prog.copy_everything_except_instructions() + new_prog + + +def add_decoherence_noise( + prog: "Program", + T1: Union[dict[int, float], float] = 30e-6, + T2: Union[dict[int, float], float] = 30e-6, + gate_time_1q: float = 50e-9, + gate_time_2q: float = 150e-09, + ro_fidelity: Union[dict[int, float], float] = 0.95, +) -> "Program": + """Add generic damping and dephasing noise to a program. + + This high-level function is provided as a convenience to investigate the effects of a + generic noise model on a program. For more fine-grained control, please investigate + the other methods available in the ``pyquil.noise`` module. + + In an attempt to closely model the QPU, noisy versions of RX(+-pi/2) and CZ are provided; + I and parametric RZ are noiseless, and other gates are not allowed. To use this function, + you need to compile your program to this native gate set. + + The default noise parameters + + - T1 = 30 us + - T2 = 30 us + - 1q gate time = 50 ns + - 2q gate time = 150 ns + + are currently typical for near-term devices. + + This function will define new gates and add Kraus noise to these gates. It will translate + the input program to use the noisy version of the gates. + + :param prog: A pyquil program consisting of I, RZ, CZ, and RX(+-pi/2) instructions + :param T1: The T1 amplitude damping time either globally or in a + dictionary indexed by qubit id. By default, this is 30 us. + :param T2: The T2 dephasing time either globally or in a + dictionary indexed by qubit id. By default, this is also 30 us. + :param gate_time_1q: The duration of the one-qubit gates, namely RX(+pi/2) and RX(-pi/2). + By default, this is 50 ns. + :param gate_time_2q: The duration of the two-qubit gates, namely CZ. + By default, this is 150 ns. + :param ro_fidelity: The readout assignment fidelity + :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. + :return: A new program with noisy operators. + """ + gates = _get_program_gates(prog) + noise_model = _decoherence_noise_model( + gates, + T1=T1, + T2=T2, + gate_time_1q=gate_time_1q, + gate_time_2q=gate_time_2q, + ro_fidelity=ro_fidelity, + ) + return apply_noise_model(prog, noise_model) + + +def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray: + """Ensure array p has a separate axis for each qubit so ``p[i,j,...,k]`` gives the probability of bitstring ``ij...k``. + + This should not allocate much memory if ``p`` is already in ``C``-contiguous order (row-major). + + :param p: An array that enumerates bitstring probabilities. When flattened out + ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must therefore be a + power of 2. + :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. + """ + p = np.asarray(p, order="C") + num_qubits = int(round(np.log2(p.size))) + return p.reshape((2,) * num_qubits) + + +def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray: + """Given an array of single shot results estimate the probability distribution over all bitstrings. + + :param results: A 2d array where the outer axis iterates over shots + and the inner axis over bits. + :return: An array with as many axes as there are qubit and normalized such that it sums to one. + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + """ + nshots, nq = np.shape(results) + outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) + probs = np.histogram(outcomes, bins=np.arange(-0.5, 2**nq, 1))[0] / float(nshots) + return _bitstring_probs_by_qubit(probs) + + +_CHARS = "klmnopqrstuvwxyzabcdefgh0123456789" + + +def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarray: + """Apply local 2x2 matrices to each index in a 2D array of single shot results using assignment probability matrices. + + Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits) + and a list of assignment probability matrices (one for each bit in the readout, ordered like + the inner axis of results) apply local 2x2 matrices to each bit index. + + :param p: An array that enumerates a function indexed by + bitstrings:: + + f(ijk...) = p[i,j,k,...] + + :param ts: A sequence of 2x2 transform-matrices, one for each bit. + :return: ``p_transformed`` an array with as many dimensions as there are bits with the result of + contracting p along each axis by the corresponding bit transformation:: + + p_transformed[ijk...] = f'(ijk...) = sum_lmn... ts[0][il] ts[1][jm] ts[2][kn] f(lmn...) + """ + p_corrected = _bitstring_probs_by_qubit(p) + nq = p_corrected.ndim + for idx, trafo_idx in enumerate(ts): + # this contraction pattern looks like + # 'ij,abcd...jklm...->abcd...iklm...' so it properly applies a "local" + # transformation to a single tensor-index without changing the order of + # indices + einsum_pat = ( + "ij," + _CHARS[:idx] + "j" + _CHARS[idx : nq - 1] + "->" + _CHARS[:idx] + "i" + _CHARS[idx : nq - 1] + ) + p_corrected = np.einsum(einsum_pat, trafo_idx, p_corrected) + + return p_corrected + + +def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: list[np.ndarray]) -> np.ndarray: + """Given a 2D array of bitstring probabilities and assignment matrices, compute the corrupted probabilities. + + Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis + over bits) and a list of assignment probability matrices (one for each bit in the readout, + ordered like the inner axis of results) compute the corrupted probabilities. + + :param p: An array that enumerates bitstring probabilities. When + flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must + therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + :param assignment_probabilities: A list of assignment probability matrices + per qubit. Each assignment probability matrix is expected to be of the form:: + + [[p00 p01] + [p10 p11]] + + :return: ``p_corrected`` an array with as many dimensions as there are qubits that contains + the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + """ + return _apply_local_transforms(p, assignment_probabilities) + + +def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: list[np.ndarray]) -> np.ndarray: + """Given a 2D array of corrupted bitstring probabilities and assignment matrices, compute the corrected probabilities. + + Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner + axis over bits) and a list of assignment probability matrices (one for each bit in the readout) + compute the corrected probabilities. + + :param p: An array that enumerates bitstring probabilities. When + flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must + therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + :param assignment_probabilities: A list of assignment probability matrices + per qubit. Each assignment probability matrix is expected to be of the form:: + + [[p00 p01] + [p10 p11]] + + :return: ``p_corrected`` an array with as many dimensions as there are qubits that contains + the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + """ + return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) + + +def bitstring_probs_to_z_moments(p: np.ndarray) -> np.ndarray: + """Convert between bitstring probabilities and joint Z moment expectations. + + :param p: An array that enumerates bitstring probabilities. When + flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must + therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that + ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. + :return: ``z_moments``, an np.array with one length-2 axis per qubit which contains the + expectations of all monomials in ``{I, Z_0, Z_1, ..., Z_{n-1}}``. The expectations of each + monomial can be accessed via:: + + = z_moments[j_0,j_1,...,j_m] + """ + zmat = np.array([[1, 1], [1, -1]]) + return _apply_local_transforms(p, (zmat for _ in range(p.ndim))) + + +def estimate_assignment_probs( + q: int, + trials: int, + qc: "PyquilApiQuantumComputer", + p0: Optional["Program"] = None, +) -> np.ndarray: + """Estimate the readout assignment probabilities for a given qubit ``q``. + + The returned matrix is of the form:: + + [[p00 p01] + [p10 p11]] + + :param q: The index of the qubit. + :param trials: The number of samples for each state preparation. + :param qc: The quantum computer to sample from. + :param p0: A header program to prepend to the state preparation programs. Will not be compiled by quilc, so it must + be native Quil. + :return: The assignment probability matrix + """ + from pyquil.quil import Program + + if p0 is None: # pragma no coverage + p0 = Program() + + p_i = ( + p0 + + Program( + Declare("ro", "BIT", 1), + I(q), + MEASURE(q, MemoryReference("ro", 0)), + ) + ).wrap_in_numshots_loop(trials) + results_i = np.sum(_run(qc, p_i)) + + p_x = ( + p0 + + Program( + Declare("ro", "BIT", 1), + RX(np.pi, q), + MEASURE(q, MemoryReference("ro", 0)), + ) + ).wrap_in_numshots_loop(trials) + results_x = np.sum(_run(qc, p_x)) + + p00 = 1.0 - results_i / float(trials) + p11 = results_x / float(trials) + return np.array([[p00, 1 - p11], [1 - p00, p11]]) + + +def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> list[list[int]]: + result = qc.run(qc.compiler.native_quil_to_executable(program)) + bitstrings = result.readout_data.get("ro") + if bitstrings is None: + raise ValueError("No readout data found in result.") + return cast(list[list[int]], bitstrings.tolist()) + + diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py new file mode 100644 index 000000000..a0b89778e --- /dev/null +++ b/pyquil/noise/_noise_model.py @@ -0,0 +1,476 @@ +############################################################################## +# Copyright 2016-2026 Rigetti Computing +# +# 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. +############################################################################## +""" +Noise model container and program-level fidelity estimation. + +This module defines: + +- ``NoiseModelLike``: A ``typing.Protocol`` defining the interface that all noise + models must satisfy (a single ``get_channel`` method). +- ``NoiseModel``: The primary concrete implementation — a frozen dataclass that + collects per-instruction noise channels. +- ``DepolarizingNoiseModel``: A convenience implementation that returns a + depolarizing channel for any gate. +- ``CompositeNoiseModel``: Chains multiple noise models, returning the first + non-None channel. +- ``NO_NOISE``: A sentinel noise model that always returns ``None``. +- Program-level fidelity estimation utilities. +""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass, field +from functools import cached_property, reduce +from operator import mul +from typing import ( + TYPE_CHECKING, + Dict, + FrozenSet, + Iterable, + List, + Optional, + Protocol, + Sequence, + Set, + Tuple, + Type, + Union, + overload, + runtime_checkable, +) + +import quax as qx + +from pyquil.external.rpcq import CompilerISA +from pyquil.quilbase import Gate, Measurement, ResetQubit + +from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel + +if TYPE_CHECKING: + from pyquil import Program + +logger = logging.getLogger(__name__) + +# ────────────────────────────────────────────────────────── +# Protocol +# ────────────────────────────────────────────────────────── + +# Channel union type returned by get_channel +ChannelType = Union[Channel, MeasurementChannel, ResetChannel, CycleChannel] + + +@runtime_checkable +class NoiseModelLike(Protocol): + """Protocol defining the noise model interface. + + Any object that implements ``get_channel`` with the correct signature can be + used wherever a noise model is expected. This enables alternative noise model + strategies (depolarizing, dynamic, crosstalk-aware) without modifying + consumers. + + The standard concrete implementation is :class:`NoiseModel`. + """ + + @overload + def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... + + @overload + def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... + + @overload + def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... + + def get_channel( + self, inst: Gate | Measurement | ResetQubit + ) -> ChannelType | None: + """Retrieve the noise channel for a specific instruction. + + :param inst: A gate, measurement, or reset instruction. + :return: The associated noise channel, or ``None`` if the instruction + should be treated as ideal (noiseless). + """ + ... + + +@dataclass(frozen=True) +class NoiseModel: + """ + A noise model collects all the noise channels for a given quantum program. + + This includes gate channels, measurement channels, reset channels, and cycle channels. + + The constructor accepts any iterable of channels (list, tuple, set, generator, etc.) + which is coerced to a tuple for immutable storage. + """ + + channels: Tuple[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel], ...] + """Immutable tuple of all noise channels in the model.""" + + def __init__( + self, + channels: Iterable[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = (), + ) -> None: + # Accept any iterable, coerce to tuple for immutable storage. + if isinstance(channels, tuple): + object.__setattr__(self, "channels", channels) + else: + object.__setattr__(self, "channels", tuple(channels)) + + @cached_property + def _channel_map(self) -> Dict[Union[Gate, Measurement, ResetQubit], Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]]: + """Map from instruction to channel for fast lookup.""" + return {ch.inst: ch for ch in self.channels} + + @overload + def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... + + @overload + def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... + + @overload + def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... + + def get_channel( + self, inst: Gate | Measurement | ResetQubit + ) -> Channel | MeasurementChannel | ResetChannel | CycleChannel | None: + """ + Retrieve the noise channel associated with a specific instruction. + + :param inst: The instruction (gate, measurement, or reset) for which to retrieve the noise channel. + :return: The noise channel associated with the instruction, or None if no channel is found. + """ + return self._channel_map.get(inst) + + # ────────────────────────────────────────────── + # Constructors + # ────────────────────────────────────────────── + + @classmethod + def from_isa(cls: Type["NoiseModel"], compiler_isa: "CompilerISA") -> "NoiseModel": + """ + Create a noise model from an instruction set architecture. + + Gate fidelities are converted to depolarizing channels and measurement + errors are symmetric. Only gates with concrete numeric parameters are + included. + + :param compiler_isa: The compiler ISA. + :return: A NoiseModel with channels according to the provided fidelities. + """ + from pyquil.external.rpcq import GateInfo, MeasureInfo + from pyquil.quilatom import Qubit as QuilQubit + + channels: Set[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = set() + seen_measure_qubits: Set[int] = set() + + for qubit_label, qubit in compiler_isa.qubits.items(): + for op_info in qubit.gates: + if isinstance(op_info, GateInfo): + qubits = [int(qubit_label)] + gate_name = op_info.operator + fidelity = op_info.fidelity + params = op_info.parameters + + if gate_name is None: + continue + # Skip gates with non-numeric parameters + if not all(isinstance(p, (float, int, complex)) for p in params): + continue + + numeric_params: List[float] = [float(p) for p in params if isinstance(p, (float, int, complex))] + inst = Gate(name=gate_name, params=numeric_params, qubits=qubits) + if fidelity is not None and fidelity < 1.0: + channels.add(Channel.from_gate_fidelity(inst=inst, fidelity=fidelity)) + + elif isinstance(op_info, MeasureInfo): + if op_info.qubit is None: + continue + # Use qubit_label from the enclosing section when qubit is a wildcard + qubit_str = op_info.qubit if op_info.qubit != "_" else qubit_label + try: + qubit_idx = int(qubit_str) + except (ValueError, TypeError): + continue + fidelity = op_info.fidelity + if qubit_idx in seen_measure_qubits: + continue + seen_measure_qubits.add(qubit_idx) + if fidelity is None: + continue + m_inst = Measurement(qubit=QuilQubit(qubit_idx), classical_reg=None) + channels.add(MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=fidelity)) + + for edge_label, edge in compiler_isa.edges.items(): + for op_info in edge.gates: + if isinstance(op_info, GateInfo): + qubits = [int(q) for q in edge_label.split("-")] + gate_name = op_info.operator + fidelity = op_info.fidelity + params = op_info.parameters + + if gate_name is None: + continue + if not all(isinstance(p, (float, int, complex)) for p in params): + continue + + numeric_params = [float(p) for p in params if isinstance(p, (float, int, complex))] + inst = Gate(name=gate_name, params=numeric_params, qubits=qubits) + if fidelity is not None and fidelity < 1.0: + channels.add(Channel.from_gate_fidelity(inst=inst, fidelity=fidelity)) + + return cls(channels=channels) + + # ────────────────────────────────────────────── + # Serialization + # ────────────────────────────────────────────── + + def to_json(self) -> str: + """ + Serialize NoiseModel to a JSON string. + + :return: JSON string representation. + """ + channel_data = [] + for ch in self.channels: + if isinstance(ch, (Channel, MeasurementChannel, ResetChannel, CycleChannel)): + channel_data.append({"type": type(ch).__name__, "data": ch.to_json()}) + else: + logger.warning(f"Skipping serialization of {type(ch).__name__} (not yet supported).") + return json.dumps({"channels": channel_data}) + + @classmethod + def from_json(cls: Type["NoiseModel"], json_str: str) -> "NoiseModel": + """ + Deserialize a NoiseModel from a JSON string. + + :param json_str: JSON string as produced by :meth:`to_json`. + :return: NoiseModel instance. + """ + data = json.loads(json_str) + _type_map = { + "Channel": Channel, + "MeasurementChannel": MeasurementChannel, + "ResetChannel": ResetChannel, + "CycleChannel": CycleChannel, + } + channels: List[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = [] + for ch_data in data["channels"]: + ch_cls = _type_map.get(ch_data["type"]) + if ch_cls is None: + raise ValueError(f"Unknown channel type: {ch_data['type']}") + channels.append(ch_cls.from_json(ch_data["data"])) + return cls(channels=channels) + + # ────────────────────────────────────────────── + # Dunder methods + # ────────────────────────────────────────────── + + def __eq__(self, other: object) -> bool: + """Check if two NoiseModels contain equivalent channel maps.""" + if not isinstance(other, NoiseModel): + return False + return self._channel_map == other._channel_map + + def __hash__(self) -> int: + """Hash based on id (NoiseModel is not value-hashable due to array contents).""" + return id(self) + + def __add__(self, other: "NoiseModel") -> "NoiseModel": + """ + Combine two NoiseModels. + + For channels with matching instructions, compose them (``channel_A @ channel_B``). + For non-overlapping channels, include both. + """ + if not isinstance(other, NoiseModel): + return NotImplemented + + my_channels = {ch.inst: ch for ch in self.channels} + other_channels = {ch.inst: ch for ch in other.channels} + + combined: List[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = [] + all_insts = list(dict.fromkeys(list(my_channels) + list(other_channels))) + for inst in all_insts: + mine = my_channels.get(inst) + theirs = other_channels.get(inst) + if mine is not None and theirs is not None: + # Both have a channel for this instruction — compose them + # (only same-type composition is defined) + composed = mine @ theirs # type: ignore[operator] + combined.append(composed) # type: ignore[arg-type] + elif mine is not None: + combined.append(mine) + elif theirs is not None: + combined.append(theirs) + + return NoiseModel(channels=combined) + + +# ────────────────────────────────────────────────────────── +# Convenience NoiseModelLike implementations +# ────────────────────────────────────────────────────────── + + +NOISELESS: NoiseModelLike = NoiseModel() +"""Sentinel noise model that applies no noise to any instruction.""" + + +@dataclass(frozen=True) +class DepolarizingNoiseModel: + """A noise model that applies uniform depolarizing noise to every gate. + + For any ``Gate`` instruction, returns a :class:`Channel` with the specified + depolarizing constant. Measurements and resets are treated as ideal. + + :param depolarizing_constant: The depolarization constant :math:`p` where + :math:`\\mathcal{D}_p(\\rho) = p \\, \\rho + (1-p) \\, I/d`. + A value of 1.0 means no noise; 0.0 means full depolarization. + """ + + depolarizing_constant: float + + @overload + def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... + + @overload + def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... + + @overload + def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... + + def get_channel( + self, inst: Gate | Measurement | ResetQubit + ) -> ChannelType | None: + """Return a depolarizing channel for gates; ``None`` for measurements/resets.""" + if isinstance(inst, Gate): + return Channel.from_depolarizing_constant(inst, self.depolarizing_constant) + return None + + +@dataclass(frozen=True) +class CompositeNoiseModel: + """A noise model that chains multiple models, returning the first non-None channel. + + Models are queried in order. The first model that returns a non-None channel + for a given instruction wins. + + :param models: Sequence of noise models to query in priority order. + """ + + models: Tuple[NoiseModelLike, ...] + + def __init__(self, models: Sequence[NoiseModelLike]) -> None: + object.__setattr__(self, "models", tuple(models)) + + @overload + def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... + + @overload + def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... + + @overload + def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... + + def get_channel( + self, inst: Gate | Measurement | ResetQubit + ) -> ChannelType | None: + """Query each model in order, returning the first non-None result.""" + for model in self.models: + channel = model.get_channel(inst) + if channel is not None: + return channel + return None + + +# ────────────────────────────────────────────────────────── +# Program-level fidelity estimation +# ────────────────────────────────────────────────────────── + + +def estimate_program_fidelity(program: Program, noise_model: NoiseModelLike) -> float: + """ + Estimate the program fidelity for a given noise model. + + Works by multiplying the gate process fidelities together. Readout noise + is not considered. + + :param program: The program of interest. + :param noise_model: A noise model. + :return: The estimated process fidelity. + """ + gate_fidelities = [1.0] + for inst in program.instructions: + if isinstance(inst, Gate): + channel = noise_model.get_channel(inst) + if channel is not None and isinstance(channel, Channel): + gate_fidelities.append(channel.pauli_fidelity) + elif channel is not None and isinstance(channel, CycleChannel): + gate_fidelities.append(channel.pauli_fidelity) + + return reduce(mul, gate_fidelities) + + +def _light_cone_program(program: Program, qubits: List[int]) -> Program: + """Return a sub-program containing only gates in the backward light cone of *qubits*. + + Walks backward through the program's gate instructions. Any gate that + acts on a qubit currently in the light-cone set is included, and all of + its qubits are added to the set (because earlier gates on those qubits + are now causally relevant). + """ + from pyquil import Program as _Program + + gate_instructions = [inst for inst in program.instructions if isinstance(inst, Gate)] + relevant_qubits = set(qubits) + included: List[Gate] = [] + for inst in reversed(gate_instructions): + inst_qubits = {q.index for q in inst.qubits} + if inst_qubits & relevant_qubits: + included.append(inst) + relevant_qubits |= inst_qubits + reduced = _Program() + for inst in reversed(included): + reduced += inst + return reduced + + +def estimate_program_observable_fidelity( + program: Program, + noise_model: NoiseModelLike, + observable: Union["PauliSum", "PauliTerm"], +) -> float: + """Estimate program fidelity restricted to the backward light cone of *observable*. + + Reduces the program to only the gates causally connected to the + observable qubits, then multiplies gate process fidelities together. + Readout noise is not considered. + + :param program: The program of interest. + :param noise_model: A noise model. + :param observable: A ``PauliTerm`` or ``PauliSum`` whose qubits define + the light cone. + :return: The estimated process fidelity for the light-cone-reduced program. + """ + from pyquil.paulis import PauliSum, PauliTerm + + if isinstance(observable, PauliTerm): + observable = PauliSum(terms=[observable]) + + qubits = [int(q) for term in observable.terms for q, _ in term.operations_as_set()] + reduced_program = _light_cone_program(program, qubits) + return estimate_program_fidelity(reduced_program, noise_model) diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py index 768c4c8e2..e6125f10a 100644 --- a/pyquil/quilbase.py +++ b/pyquil/quilbase.py @@ -59,10 +59,37 @@ if TYPE_CHECKING: # avoids circular import from pyquil.paulis import PauliSum +import math + import quil.expression as quil_rs_expr import quil.instructions as quil_rs +def _is_perfect_power(n: int) -> bool: + """Check if n is a prime power (p^k for prime p, k >= 1). + + This ensures the matrix dimension can be interpreted as k qudits of + dimension p. Composite non-prime-power dimensions like 6 = 2*3 are + ambiguous and rejected. + """ + if n < 2: + return False + # Find the smallest prime factor. + factor = 0 + for p in range(2, int(math.isqrt(n)) + 1): + if n % p == 0: + factor = p + break + if factor == 0: + # n is prime → n = n^1, valid single-qudit dimension. + return True + # Check that n is a power of this smallest prime factor. + val = factor + while val < n: + val *= factor + return val == n + + class _InstructionMeta(abc.ABCMeta): """A metaclass that allows us to group all instruction types from quil-rs and pyQuil as an `AbstractInstruction`. @@ -715,8 +742,8 @@ def _validate_matrix( else: raise TypeError("Matrix argument must be a list or NumPy array/matrix") - if 0 != rows & (rows - 1): - raise ValueError(f"Dimension of matrix must be a power of 2, got {rows}") + if not _is_perfect_power(rows): + raise ValueError(f"Dimension of matrix must be a perfect power of an integer (e.g. 2, 3, 4, 8, 9, ...), got {rows}") if not contains_parameters: np_matrix = np.asarray(matrix) @@ -741,9 +768,19 @@ def get_constructor(self) -> Union[Callable[..., Gate], Callable[..., Callable[. return lambda *qubits: Gate(name=self.name, params=[], qubits=list(map(unpack_qubit, qubits))) def num_args(self) -> int: - """Get the number of qubit arguments the gate takes.""" + """Get the number of qudit arguments the gate takes. + + For a matrix of dimension d^k, returns k where d is the smallest + integer base >= 2 such that rows = d^k. + """ rows = len(self.matrix) - return int(np.log2(rows)) + if rows < 2: + return 0 + for base in range(2, rows + 1): + k = int(round(math.log(rows, base))) + if base**k == rows: + return k + return 1 @property def matrix(self) -> np.ndarray: diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py new file mode 100644 index 000000000..10ffbd61c --- /dev/null +++ b/pyquil/simulation/_resolver.py @@ -0,0 +1,696 @@ +############################################################################## +# Copyright 2016-2026 Rigetti Computing +# +# 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. +############################################################################## +""" +simulation._resolver module +---------------------------- + +Shared infrastructure for the density-matrix and state-vector simulators. + +This module provides the three front-end stages of the simulation pipeline: + +1. **Linearizer** — converts a ``MemoryMap`` into a flat JAX parameter vector. +2. **Resolver** — converts a parameter vector into a list of + ``(operator, subsystem)`` pairs using native quax types. +3. **Adapters** — convert resolved operations into the form expected by each + simulator backend (``SuperOp`` for density matrices; ``Unitary``/``KrausMap``/ + ``QuantumInstrument`` for state-vector trajectories). + +It also provides shared utilities: DAG construction, dimension inference. +""" + +from __future__ import annotations + +import logging +from typing import Callable, Dict, List, Set, Tuple, Union + +import jax +import jax.numpy as jnp +import networkx as nx +import quax as qx +from jax import Array + +from pyquil.api import MemoryMap +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference +from pyquil.quilbase import Gate, Measurement, Reset, ResetQubit + +from pyquil.noise._channels import ( + Channel, + CycleChannel, + MeasurementChannel, + ResetChannel, + get_custom_gates_from_program, + get_instruction_unitary, +) +from pyquil.noise._noise_model import ( + NoiseModelLike, +) + +logger = logging.getLogger(__name__) + +# ────────────────────────────────────────────────────────── +# Type aliases +# ────────────────────────────────────────────────────────── + +# Resolved operations retain the most specific native quax type. +ResolvedOp = Tuple[Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument], Tuple[int, ...]] + +# Trajectory operations for the state-vector simulator. +TrajectoryOp = Tuple[Union[qx.Unitary, qx.KrausMap, qx.QuantumInstrument], Tuple[int, ...]] + +# Density-matrix operations. +DensityMatrixOp = Tuple[qx.SuperOp, Tuple[int, ...]] + +# Custom gate definitions. +CustomGateMap = dict + + +# ══════════════════════════════════════════════════════════ +# Linearizer +# ══════════════════════════════════════════════════════════ + + +class Linearizer: + """Converts a MemoryMap into a flat JAX parameter vector. + + Constructed via :func:`linearizer_from_program`. Call instances directly + to perform the conversion:: + + lin = linearizer_from_program(program) + params = lin(memory_map) + + :param n_params: The number of scalar parameters in the vector. + """ + + __slots__ = ("_linearize_fn", "n_params") + + def __init__(self, linearize_fn: Callable[[MemoryMap], Array], n_params: int) -> None: + self._linearize_fn = linearize_fn + self.n_params = n_params + + def __call__(self, memory_map: MemoryMap) -> Array: + return self._linearize_fn(memory_map) + + +def linearizer_from_program(program: Program) -> Linearizer: + """Build a :class:`Linearizer` that converts a memory map to a flat JAX parameter vector. + + Walks the program to identify parameter registers (skipping ``"ro"`` and + any register that is the target of a ``MEASURE`` instruction). For each + gate parameter that is a :class:`MemoryReference`, records ``(name, offset)`` + in program order. + + :param program: Expanded Quil program. + :return: A :class:`Linearizer` instance. + """ + # Find registers written to by MEASURE — these are output registers, not params + measure_registers: Set[str] = set() + for inst in program.instructions: + if isinstance(inst, Measurement): + cr = inst.classical_reg + if cr is not None: + measure_registers.add(cr.name) + + # Collect parameter references in program order + param_refs: List[Tuple[str, int]] = [] + for inst in program.instructions: + if isinstance(inst, Gate): + for param in inst.params: + if isinstance(param, MemoryReference): + if param.name not in measure_registers: + param_refs.append((param.name, param.offset)) + + def linearize(memory_map: MemoryMap) -> Array: + if not param_refs: + return jnp.array([], dtype=float) + values = [float(memory_map[name][offset]) for name, offset in param_refs] + return jnp.array(values, dtype=float) + + return Linearizer(linearize, n_params=len(param_refs)) + + +# ══════════════════════════════════════════════════════════ +# Program DAG +# ══════════════════════════════════════════════════════════ + + +def dag_from_program( + program: Program, + qubit_indices: Dict[int, int], +) -> Tuple[nx.DiGraph, List[int]]: + """Build a directed acyclic graph from program instructions. + + Each ``Gate``, ``Measurement``, ``ResetQubit``, or ``Reset`` becomes a node + keyed by its index in the instruction list. Edges encode qubit-level data + dependencies: for each qubit, there is an edge from the previous instruction + that touched it to the current one. + + :param program: Expanded Quil program. + :param qubit_indices: Mapping from physical qubit id → 0-based index. + :return: Tuple of ``(dag, node_order)`` where ``node_order`` is the list of + node keys in instruction order. + """ + dag = nx.DiGraph() + last_on_qubit: Dict[int, int] = {} # qubit_index → last node key + node_order: List[int] = [] + + for idx, inst in enumerate(program.instructions): + if isinstance(inst, Gate): + qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) + elif isinstance(inst, Measurement): + qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) + elif isinstance(inst, ResetQubit): + qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) # type: ignore[union-attr] + elif isinstance(inst, Reset): + qubits = tuple(sorted(qubit_indices.values())) + else: + continue + + dag.add_node(idx, inst=inst, qubits=qubits) + node_order.append(idx) + + for q in qubits: + if q in last_on_qubit: + dag.add_edge(last_on_qubit[q], idx) + last_on_qubit[q] = idx + + return dag, node_order + + +# ══════════════════════════════════════════════════════════ +# Resolver +# ══════════════════════════════════════════════════════════ + + +class Resolver: + """Resolves a flat parameter vector into a list of (operator, subsystem) pairs. + + Constructed via :func:`resolver_from_program`. Call instances directly:: + + resolver = resolver_from_program(program, ...) + ops = resolver(params) + + :param dims: Inferred per-qudit dimensions (e.g. ``(2, 2, 3)``). + """ + + __slots__ = ("_resolve_fn", "dims") + + def __init__(self, resolve_fn: Callable[[Array], List[ResolvedOp]], dims: Tuple[int, ...]) -> None: + self._resolve_fn = resolve_fn + self.dims = dims + + def __call__(self, params: Array) -> List[ResolvedOp]: + return self._resolve_fn(params) + + +def _is_parameterized(inst: Gate) -> bool: + """Check if a gate instruction has any MemoryReference parameters.""" + return any(isinstance(p, MemoryReference) for p in inst.params) + + +def _measure_registers(program: Program) -> Set[str]: + """Return the set of register names that are targets of MEASURE instructions.""" + regs: Set[str] = set() + for inst in program.instructions: + if isinstance(inst, Measurement): + cr = inst.classical_reg + if cr is not None: + regs.add(cr.name) + return regs + + +def resolver_from_program( + program: Program, + noise_model: NoiseModelLike | None, + qubit_indices: Dict[int, int], + custom_gates: CustomGateMap | None, + dag: nx.DiGraph, + node_order: List[int], +) -> Resolver: + """Build a :class:`Resolver` that maps parameter vectors to operators. + + The returned object accepts a flat parameter vector and produces one + ``(operator, subsystem)`` pair per DAG node, in ``node_order``. + + Operators are returned in their most specific native type: + + * Ideal gates → ``qx.Unitary`` + * Noisy gates (``Channel``) → ``qx.SuperOp`` + * Noisy gates (``CycleChannel``) → multiple ``(SuperOp | QuantumInstrument, subsystem)`` + * Measurements → ``qx.QuantumInstrument`` + * Noisy resets (``ResetChannel``) → ``qx.SuperOp`` + * Ideal resets → ``qx.SuperOp`` + + No type conversion (``to_kraus``, ``to_superop``) is performed here; + that is the adapter's responsibility. + + :param program: Expanded Quil program. + :param noise_model: Optional noise model. + :param qubit_indices: Mapping from physical qubit id → 0-based index. + :param custom_gates: Custom gate definitions. + :param dag: Program dependency DAG. + :param node_order: Node keys in instruction order. + :return: A :class:`Resolver` instance with inferred ``dims``. + """ + measure_regs = _measure_registers(program) + + # Assign parameter vector indices to each gate's MemoryReference params. + param_counter = 0 + gate_param_indices: Dict[int, List[int]] = {} + for idx in node_order: + inst = dag.nodes[idx]["inst"] + if isinstance(inst, Gate): + indices = [] + for param in inst.params: + if isinstance(param, MemoryReference) and param.name not in measure_regs: + indices.append(param_counter) + param_counter += 1 + else: + indices.append(-1) + gate_param_indices[idx] = indices + + # Build a recipe per DAG node. + # Recipe: (op_or_callable, subsystem) where op_or_callable is either a + # pre-computed qx object or a callable(params) -> qx object. + recipes: List[Tuple[object, Tuple[int, ...]]] = [] + + # Pre-scan gate instructions to infer per-qudit dimensions. + # This is needed so that MEASURE and RESET use the correct dim. + qudit_dims: Dict[int, int] = {} # qubit_index → dimension + for node_key in node_order: + inst = dag.nodes[node_key]["inst"] + if isinstance(inst, Gate): + subsystem = dag.nodes[node_key]["qubits"] + channel = noise_model.get_channel(inst) if noise_model is not None else None + if channel is not None and isinstance(channel, Channel): + op_dims = channel.process.dims[0] + elif channel is not None and isinstance(channel, CycleChannel): + continue # CycleChannel dims are per-sub-channel, handled separately + else: + try: + unitary = get_instruction_unitary(inst, custom_gates=custom_gates) + op_dims = unitary.dims[0] + except Exception: + continue + for slot, dim in zip(subsystem, op_dims): + if dim > qudit_dims.get(slot, 2): + qudit_dims[slot] = dim + + for node_key in node_order: + inst = dag.nodes[node_key]["inst"] + subsystem = dag.nodes[node_key]["qubits"] + + match inst: + case Gate(): + channel = None + if noise_model is not None: + channel = noise_model.get_channel(inst) + + if _is_parameterized(inst): + gate_name = inst.name + if custom_gates is not None and gate_name in custom_gates: + gate_def = custom_gates[gate_name] + elif gate_name in qx.gates.QUANTUM_GATES: + gate_def = qx.gates.QUANTUM_GATES[gate_name] + else: + raise KeyError(f"Unknown gate '{gate_name}'.") + pidx = gate_param_indices[node_key] + cparams = list(inst.params) + + def _make_param_recipe( + gdef: object, cp: list, pi: List[int], + ) -> Callable[[Array], qx.Unitary]: + def recipe(params: Array) -> qx.Unitary: + resolved = [] + for p, pv in zip(cp, pi): + if pv >= 0: + resolved.append(params[pv]) + else: + resolved.append(float(p.real) if hasattr(p, "real") else float(p)) + result = gdef(*resolved) if callable(gdef) else gdef # type: ignore[operator] + if not isinstance(result, qx.Unitary): + result = qx.Unitary.from_matrix(result.matrix, result.dims) + return result + return recipe + + recipes.append((_make_param_recipe(gate_def, cparams, pidx), subsystem)) + + elif channel is not None and isinstance(channel, Channel): + # Channel.process is a SuperOp that includes the gate unitary + recipes.append((channel.process, subsystem)) + elif channel is not None and isinstance(channel, CycleChannel): + # CycleChannel: decompose into constituent channel recipes + for sub_ch in channel.channels: + sub_qubits = tuple(qubit_indices[q] for q in sub_ch.qubits) + if isinstance(sub_ch, Channel): + recipes.append((sub_ch.process, sub_qubits)) + elif isinstance(sub_ch, MeasurementChannel): + recipes.append((sub_ch.process, sub_qubits)) + else: + unitary = get_instruction_unitary(inst, custom_gates=custom_gates) + recipes.append((unitary, subsystem)) + + case Measurement(): + meas_channel = None + if noise_model is not None: + meas_channel = noise_model.get_channel(inst) + if meas_channel is not None and isinstance(meas_channel, MeasurementChannel): + recipes.append((meas_channel.process, subsystem)) + else: + dim = qudit_dims.get(subsystem[0], 2) + recipes.append((qx.gates.MEASURE(dim=dim), subsystem)) + + case ResetQubit(): + reset_channel = None + if noise_model is not None: + reset_channel = noise_model.get_channel(inst) + if reset_channel is not None and isinstance(reset_channel, ResetChannel): + recipes.append((reset_channel.process, subsystem)) + else: + dim = qudit_dims.get(subsystem[0], 2) + recipes.append((qx.gates.RESET(dim=dim), subsystem)) + + case Reset(): + for _, q_idx in sorted(qubit_indices.items()): + dim = qudit_dims.get(q_idx, 2) + recipes.append((qx.gates.RESET(dim=dim), (q_idx,))) + + def resolve(params: Array) -> List[ResolvedOp]: + ops: List[ResolvedOp] = [] + for op_or_fn, subsystem in recipes: + if callable(op_or_fn) and not isinstance(op_or_fn, (qx.Unitary, qx.KrausMap, qx.SuperOp, qx.QuantumInstrument)): + ops.append((op_or_fn(params), subsystem)) + else: + ops.append((op_or_fn, subsystem)) # type: ignore[arg-type] + return ops + + # Compute per-qudit dimensions from the pre-scan. + n_qubits = len(qubit_indices) + dims = tuple(qudit_dims.get(i, 2) for i in range(n_qubits)) + + return Resolver(resolve, dims=dims) + + +# ══════════════════════════════════════════════════════════ +# Adapters +# +# These adapters live outside the resolver intentionally. The resolver produces +# operators in their most specific native type (Unitary, SuperOp, KrausMap, +# QuantumInstrument). Each simulator backend then adapts these to its required +# representation. This separation keeps the resolver backend-agnostic and the +# per-op conversion cost (type dispatch + matrix reshape) is negligible compared +# to the actual simulation. +# ══════════════════════════════════════════════════════════ + + +def adapt_for_density_matrix( + ops: List[ResolvedOp], +) -> List[DensityMatrixOp]: + """Convert resolved operations to ``(SuperOp, subsystem)`` pairs for density-matrix simulation. + + * ``Unitary`` → ``qx.to_superop(op)`` + * ``SuperOp`` → pass through + * ``KrausMap`` → ``qx.to_superop(op)`` + * ``QuantumInstrument`` → ``qx.to_superop(op.total_channel())`` + + :param ops: Resolved operations from :func:`build_resolver`. + :return: List of ``(SuperOp, subsystem)`` pairs. + """ + result: List[DensityMatrixOp] = [] + for op, subsystem in ops: + if isinstance(op, qx.SuperOp): + result.append((op, subsystem)) + elif isinstance(op, qx.QuantumInstrument): + result.append((qx.to_superop(op.total_channel()), subsystem)) + else: + # Unitary, KrausMap + result.append((qx.to_superop(op), subsystem)) + return result + + +def adapt_for_trajectory( + ops: List[ResolvedOp], + kraus_truncation_threshold: float = 1e-6, +) -> List[TrajectoryOp]: + """Convert resolved operations to trajectory-compatible types. + + * ``Unitary`` → pass through + * ``SuperOp`` → ``truncate_kraus(to_kraus(op))`` → ``KrausMap`` + * ``KrausMap`` → pass through + * ``QuantumInstrument`` → pass through + + :param ops: Resolved operations from :func:`build_resolver`. + :param kraus_truncation_threshold: Threshold for Kraus truncation. + :return: List of ``(Unitary | KrausMap | QuantumInstrument, subsystem)`` pairs. + """ + result: List[TrajectoryOp] = [] + for op, subsystem in ops: + if isinstance(op, qx.SuperOp): + km = qx.truncate_kraus(qx.to_kraus(op), atol=kraus_truncation_threshold) + result.append((km, subsystem)) + else: + # Unitary, KrausMap, QuantumInstrument — pass through + result.append((op, subsystem)) # type: ignore[arg-type] + return result + + +# ══════════════════════════════════════════════════════════ +# Compressor (greedy edge contraction) +# ══════════════════════════════════════════════════════════ + + +def _merge_ops( + ops_with_subsystems: List[ResolvedOp], + merged_subsystem: Tuple[int, ...], + dims: Tuple[int, ...], +) -> ResolvedOp: + """Merge a sequence of operators into a single operator on the union subsystem. + + Each operator is embedded into the merged Hilbert space and then composed + sequentially using the ``@`` operator, which handles type promotion + automatically (Unitary, SuperOp, KrausMap). + + For groups containing only ``Unitary`` operators, the result is a ``Unitary``. + For groups containing any noisy operator (``SuperOp``, ``KrausMap``), all + operators are promoted to ``SuperOp``, composed, and the result is returned + as a ``SuperOp``. Downstream adapters handle final conversion (e.g. to + ``KrausMap`` for trajectories). + + :param ops_with_subsystems: Ordered list of ``(operator, subsystem)`` pairs + to merge (applied in order: first element is applied first). + :param merged_subsystem: Sorted tuple of qubit indices for the merged operator. + :param dims: Global per-qudit dimensions tuple. + :return: A single ``(operator, merged_subsystem)`` pair. + """ + has_noisy = any(isinstance(op, (qx.KrausMap, qx.SuperOp)) for op, _ in ops_with_subsystems) + + target_dims = tuple(dims[q] for q in merged_subsystem) + + accumulated = None + for op, subsystem in ops_with_subsystems: + positions = tuple(merged_subsystem.index(q) for q in subsystem) + + if has_noisy: + embedded = qx.embed(qx.to_superop(op), target_dims=target_dims, positions=positions) + else: + embedded = qx.embed(op, target_dims=target_dims, positions=positions) + + accumulated = embedded if accumulated is None else embedded @ accumulated + + assert accumulated is not None + return accumulated, merged_subsystem + + +class _UnionFind: + """Simple union-find (disjoint set) data structure for node grouping.""" + + def __init__(self) -> None: + self._parent: Dict[int, int] = {} + self._rank: Dict[int, int] = {} + + def make_set(self, x: int) -> None: + self._parent[x] = x + self._rank[x] = 0 + + def find(self, x: int) -> int: + while self._parent[x] != x: + self._parent[x] = self._parent[self._parent[x]] # path compression + x = self._parent[x] + return x + + def union(self, x: int, y: int) -> int: + rx, ry = self.find(x), self.find(y) + if rx == ry: + return rx + if self._rank[rx] < self._rank[ry]: + rx, ry = ry, rx + self._parent[ry] = rx + if self._rank[rx] == self._rank[ry]: + self._rank[rx] += 1 + return rx + + +def compressor_from_dag( + dag: nx.DiGraph, + node_order: List[int], + max_subsystem_size: int, + dims: Tuple[int, ...] = (), +) -> Callable[[List[ResolvedOp]], List[ResolvedOp]]: + """Build a compressor that merges operators via greedy edge contraction. + + The algorithm: + 1. Classify each node as *mergeable* (gate, reset) or *barrier* (measurement). + 2. Greedily contract DAG edges: for each edge ``(u, v)`` in topological + order, merge the groups of ``u`` and ``v`` if both are mergeable and + the union of their qubit sets fits within ``max_subsystem_size``. + 3. Build a merge plan mapping each group to its constituent nodes and + merged qubit subsystem. + 4. Return a closure that receives the resolved operator list and produces + a compressed operator list. + + :param dag: Program dependency DAG. + :param node_order: Node keys in instruction order. + :param max_subsystem_size: Maximum number of qubits in a merged group. + 0 disables merging entirely. + :return: A closure ``compress(ops) -> List[ResolvedOp]``. + """ + n_original = len(node_order) + + if max_subsystem_size == 0 or n_original == 0: + # No merging — pass through + def compress_passthrough(ops: List[ResolvedOp]) -> List[ResolvedOp]: + return ops + + logger.info( + "Compressor: %d ops (no merging), max_subsystem_size=0", + n_original, + ) + return compress_passthrough + + def _is_mergeable(node_key: int) -> bool: + inst = dag.nodes[node_key]["inst"] + return isinstance(inst, (Gate, Measurement, ResetQubit, Reset)) and not isinstance(inst, Measurement) + + # --- Greedy edge contraction --- + uf = _UnionFind() + group_qubits: Dict[int, Set[int]] = {} # root → set of qubit indices + + for nk in node_order: + uf.make_set(nk) + group_qubits[nk] = set(dag.nodes[nk]["qubits"]) + + topo_order = list(nx.topological_sort(dag)) + + for u_node in topo_order: + for v_node in dag.successors(u_node): + ru = uf.find(u_node) + rv = uf.find(v_node) + if ru == rv: + continue + if not _is_mergeable(u_node) or not _is_mergeable(v_node): + continue + union_qubits = group_qubits[ru] | group_qubits[rv] + if len(union_qubits) > max_subsystem_size: + continue + new_root = uf.union(ru, rv) + group_qubits[new_root] = union_qubits + # Clean up the non-root entry + old_root = rv if new_root == ru else ru + if old_root in group_qubits: + del group_qubits[old_root] + + # --- Build merge plan --- + root_to_nodes: Dict[int, List[int]] = {} + for nk in topo_order: + root = uf.find(nk) + root_to_nodes.setdefault(root, []).append(nk) + + root_to_subsystem: Dict[int, Tuple[int, ...]] = {} + for root, qubits in group_qubits.items(): + root_to_subsystem[root] = tuple(sorted(qubits)) + + node_key_to_idx: Dict[int, int] = {nk: i for i, nk in enumerate(node_order)} + + emit_order: List[Tuple[int, List[int], Tuple[int, ...]]] = [] + emitted_roots: Set[int] = set() + for nk in topo_order: + root = uf.find(nk) + if root not in emitted_roots: + emitted_roots.add(root) + nodes = root_to_nodes[root] + subsystem = root_to_subsystem[root] + emit_order.append((root, nodes, subsystem)) + + # --- Log the compression statistics --- + n_groups = len(emit_order) + n_multi = sum(1 for _, nodes, _ in emit_order if len(nodes) > 1) + subsystem_sizes = [len(sub) for _, _, sub in emit_order] + avg_subsystem = sum(subsystem_sizes) / len(subsystem_sizes) if subsystem_sizes else 0.0 + max_sub = max(subsystem_sizes) if subsystem_sizes else 0 + + logger.info( + "Compressor: %d ops → %d groups (ratio=%.2f), " + "%d merged groups, avg_subsystem=%.2f, max_subsystem=%d, max_subsystem_size=%d", + n_original, n_groups, n_groups / n_original if n_original else 1.0, + n_multi, avg_subsystem, max_sub, max_subsystem_size, + ) + + # --- Build compress closure --- + def compress(ops: List[ResolvedOp]) -> List[ResolvedOp]: + result: List[ResolvedOp] = [] + for _, nodes, subsystem in emit_order: + if len(nodes) == 1: + idx = node_key_to_idx[nodes[0]] + result.append(ops[idx]) + else: + group_ops = [(ops[node_key_to_idx[nk]][0], ops[node_key_to_idx[nk]][1]) + for nk in nodes] + merged = _merge_ops(group_ops, subsystem, dims) + result.append(merged) + return result + + return compress + + +# ══════════════════════════════════════════════════════════ +# Dimension inference +# ══════════════════════════════════════════════════════════ + + +def infer_qudit_dims( + operations: List[ResolvedOp] | List[TrajectoryOp] | List[DensityMatrixOp], + n_qudits: int, +) -> Tuple[int, ...]: + """Infer per-qudit dimensions from resolved operations. + + Starts with all registers at dimension 2 (qubit). For each operation, + checks the operator's dims and upgrades any slot whose operator dimension + exceeds the current assignment. + + :param operations: Resolved list of ``(operator, subsystem)`` pairs. + :param n_qudits: Number of qudit slots. + :return: Tuple of per-qudit dimensions, e.g. ``(2, 3, 2)``. + """ + qudit_dims: List[int] = [2] * n_qudits + for op, subsystem in operations: + # All quax operators expose dims as ((out_dims), (in_dims)) + op_dims = op.dims[0] if hasattr(op, "dims") else None + if op_dims is None: + continue + for slot, dim in zip(subsystem, op_dims): + if dim > qudit_dims[slot]: + qudit_dims[slot] = dim + return tuple(qudit_dims) diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py new file mode 100644 index 000000000..2302e093a --- /dev/null +++ b/pyquil/simulation/_simulator.py @@ -0,0 +1,530 @@ +############################################################################## +# Copyright 2016-2026 Rigetti Computing +# +# 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. +############################################################################## +"""Unified program simulators backed by quax. + +Three simulators share a common preprocessing pipeline: + +* :class:`PureStateVectorSimulator` — gate-only programs (no noise, + measurements, or resets). Jit- and grad-friendly. +* :class:`DensityMatrixSimulator` — any program, optionally with noise. + Jit- and grad-friendly. +* :class:`TrajectorySimulator` — Monte Carlo trajectory simulation for + programs with measurements and resets, optionally with noise. + +Each simulator is constructed from a :class:`~pyquil.quil.Program` and +exposes ``linearize``, ``resolve``, ``compress``, and ``compute`` methods. +The ``compute`` method is the main entry point and can be passed directly +to ``jax.jit`` or ``jax.grad``. +""" + +from __future__ import annotations + +import logging +import time +from typing import List, Tuple + +import jax +import jax.numpy as jnp +import quax as qx +from jax import Array + +from pyquil.api import MemoryMap +from pyquil.quil import Program +from pyquil.quilbase import Measurement, Reset, ResetQubit + +from pyquil.noise._noise_model import NoiseModelLike +from pyquil.noise._channels import CycleChannel, get_custom_gates_from_program + +from pyquil.transform import expand_defcircuits + +from pyquil.simulation._resolver import ( + Linearizer, + Resolver, + ResolvedOp, + TrajectoryOp, + DensityMatrixOp, + adapt_for_density_matrix, + adapt_for_trajectory, + compressor_from_dag, + linearizer_from_program, + dag_from_program, + resolver_from_program, +) + +logger = logging.getLogger(__name__) + + +def _get_cycle_channel_names(noise_model: NoiseModelLike | None) -> frozenset: + """Extract DefCircuit names from CycleChannels in the noise model.""" + if noise_model is None: + return frozenset() + from pyquil.noise._noise_model import NoiseModel + if isinstance(noise_model, NoiseModel): + names = frozenset( + ch.inst.name for ch in noise_model.channels + if isinstance(ch, CycleChannel) + ) + return names + return frozenset() + + +# ══════════════════════════════════════════════════════════ +# Base class +# ══════════════════════════════════════════════════════════ + + +class ProgramSimulator: + """Base class for program simulators. + + Handles all shared preprocessing: circuit expansion, qubit ordering, + building the linearizer, resolver, and compressor closures, and + inferring per-qudit dimensions. + + Subclasses override :meth:`_validate` and :meth:`compute`. + + Instances are immutable after construction. + """ + + __slots__ = ("n_qubits", "qubits", "dims", "_linearize_fn", "_resolve_fn", "_compress_fn") + + def __init__( + self, + program: Program, + qubits: List[int] | None = None, + *, + noise_model: NoiseModelLike | None = None, + max_subsystem_size: int = 0, + ) -> None: + # Only expand DefCircuits that don't correspond to CycleChannels in the + # noise model. CycleChannels are keyed by the cycle Gate instruction, + # so expanding their DefCircuit would destroy the match. + cycle_names = _get_cycle_channel_names(noise_model) + if cycle_names: + program = expand_defcircuits(program, expand_names_except=cycle_names) + else: + program = expand_defcircuits(program) + self._validate(program) + + if qubits is None: + qubits = sorted(program.get_qubit_indices()) + self.qubits = qubits + self.n_qubits = len(qubits) + qubit_indices = {q: i for i, q in enumerate(qubits)} + + custom_gates = get_custom_gates_from_program(program) + + self._linearize_fn = linearizer_from_program(program) + + dag, node_order = dag_from_program(program, qubit_indices) + + self._resolve_fn = resolver_from_program( + program, noise_model, qubit_indices, custom_gates or None, + dag, node_order, + ) + + # Dims are inferred during resolver construction from gate/channel inspection. + self.dims = self._resolve_fn.dims + + self._compress_fn = compressor_from_dag(dag, node_order, max_subsystem_size, dims=self.dims) + + # -- hook for subclass validation --------------------- + + def _validate(self, program: Program) -> None: + """Override to reject unsupported instructions.""" + + # -- public pipeline methods -------------------------- + + def linearize(self, memory_map: MemoryMap) -> Array: + """Convert a memory map to a flat JAX parameter vector.""" + return self._linearize_fn(memory_map) + + def resolve(self, params: Array) -> List[ResolvedOp]: + """Resolve parameters into one operator per DAG node.""" + return self._resolve_fn(params) + + def compress(self, resolved: List[ResolvedOp]) -> List[ResolvedOp]: + """Merge operators via greedy edge contraction.""" + return self._compress_fn(resolved) + + def compute(self, params: Array, **kwargs): + """Compute the simulation result. Subclasses must override.""" + raise NotImplementedError + + +# ══════════════════════════════════════════════════════════ +# Pure state-vector simulator +# ══════════════════════════════════════════════════════════ + + +class PureStateVectorSimulator(ProgramSimulator): + """Simulator for gate-only programs (no noise, measurements, or resets). + + All methods are jit- and grad-friendly:: + + sim = PureStateVectorSimulator(program) + params = sim.linearize(memory_map) + psi = jax.jit(sim.compute)(params) + U = jax.jit(sim.unitary)(params) + """ + + __slots__ = ("_psi0",) + + def __init__( + self, + program: Program, + qubits: List[int] | None = None, + *, + max_subsystem_size: int = 0, + ) -> None: + super().__init__(program, qubits, noise_model=None, max_subsystem_size=max_subsystem_size) + self._psi0 = qx.zero_state_vector(dims=self.dims) + + def _validate(self, program: Program) -> None: + for inst in program.instructions: + if isinstance(inst, Measurement): + raise ValueError( + "PureStateVectorSimulator does not support measurements. " + f"Found: {inst}" + ) + if isinstance(inst, (Reset, ResetQubit)): + raise ValueError( + "PureStateVectorSimulator does not support resets. " + f"Found: {inst}" + ) + + def compute(self, params: Array) -> qx.StateVector: + """Compute the final state vector. + + :param params: Flat parameter vector from :meth:`linearize`. + :return: The final state vector. + """ + resolved = self.resolve(params) + compressed = self.compress(resolved) + psi = self._psi0 + for unitary, subsystem in compressed: + psi = qx.targeted_apply_unitary(unitary, psi, subsystem) + return psi + + def __call__(self, params: Array) -> qx.StateVector: + return self.compute(params) + + def unitary(self, params: Array) -> qx.Unitary: + """Compute the full program unitary. + + :param params: Flat parameter vector from :meth:`linearize`. + :return: The full unitary matrix. + """ + resolved = self.resolve(params) + compressed = self.compress(resolved) + + accumulated: qx.Unitary | None = None + for op, subsystem in compressed: + embedded = qx.embed(op, target_dims=self.dims, positions=subsystem) + if accumulated is None: + accumulated = embedded + else: + accumulated = embedded @ accumulated + + if accumulated is None: + d = 1 + for dim in self.dims: + d *= dim + return qx.Unitary.from_matrix(jnp.eye(d, dtype=complex), self.dims) + + return accumulated + + +# ══════════════════════════════════════════════════════════ +# Density-matrix simulator +# ══════════════════════════════════════════════════════════ + + +class DensityMatrixSimulator(ProgramSimulator): + """Density-matrix simulator for any program, optionally with noise. + + All methods are jit- and grad-friendly:: + + sim = DensityMatrixSimulator(program, noise_model=noise_model) + params = sim.linearize(memory_map) + rho = jax.jit(sim.compute)(params) + """ + + __slots__ = ("_rho0",) + + def __init__( + self, + program: Program, + qubits: List[int] | None = None, + *, + noise_model: NoiseModelLike | None = None, + max_subsystem_size: int = 0, + ) -> None: + super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) + self._rho0 = qx.zero_state_matrix(dims=self.dims) + + def compute(self, params: Array) -> qx.DensityMatrix: + """Compute the final density matrix. + + :param params: Flat parameter vector from :meth:`linearize`. + :return: The final density matrix. + """ + resolved = self.resolve(params) + compressed = self.compress(resolved) + operations = adapt_for_density_matrix(compressed) + rho = self._rho0 + for superop, subsystem in operations: + rho = qx.targeted_apply_superop(superop, rho, subsystem) + return rho + + def __call__(self, params: Array) -> qx.DensityMatrix: + return self.compute(params) + + +# ══════════════════════════════════════════════════════════ +# Trajectory simulator +# ══════════════════════════════════════════════════════════ + + +class TrajectorySimulator(ProgramSimulator): + """Monte Carlo trajectory simulator for programs with measurements and resets. + + The ``compute`` method requires a JAX PRNG key. The number of + trajectories is determined by the key shape: a scalar key runs one + trajectory; a batch of keys ``jax.random.split(key, n)`` runs *n* + trajectories in parallel:: + + sim = TrajectorySimulator(program, noise_model=noise_model) + params = sim.linearize(memory_map) + + # Single trajectory + key = jax.random.key(0) + psi, outcomes = sim.compute(params, key) + + # Batched trajectories + keys = jax.random.split(jax.random.key(0), 100) + psi_batch, outcomes_batch = sim.compute(params, keys) + + The ``sample`` method is a convenience wrapper that runs trajectories + in batches and discards state vectors, returning only measurement + outcomes. + """ + + __slots__ = ("_kraus_truncation_threshold",) + + def __init__( + self, + program: Program, + qubits: List[int] | None = None, + *, + noise_model: NoiseModelLike | None = None, + max_subsystem_size: int = 0, + kraus_truncation_threshold: float = 1e-6, + ) -> None: + super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) + self._kraus_truncation_threshold = kraus_truncation_threshold + + def adapt(self, compressed: List[ResolvedOp]) -> List[TrajectoryOp]: + """Convert compressed ops to trajectory-compatible types.""" + return adapt_for_trajectory(compressed, self._kraus_truncation_threshold) + + def compute( + self, + params: Array, + key: Array, + ) -> Tuple[qx.StateVector, Array]: + """Run trajectory simulation. + + :param params: Flat parameter vector from :meth:`linearize`. + :param key: JAX PRNG key. Scalar key → single trajectory. + Batch of keys (from ``jax.random.split``) → batched trajectories. + :return: Tuple of ``(state_vector, measurement_outcomes)``. + """ + resolved = self.resolve(params) + compressed = self.compress(resolved) + operations = self.adapt(compressed) + + if key.ndim == 0: + psi = qx.zero_state_vector(dims=self.dims) + else: + n_traj = key.shape[0] + psi = qx.zero_state_vector(dims=self.dims, ensemble_size=(n_traj,)) + + return _apply_trajectory_operations(operations, psi, key) + + def __call__(self, params: Array, key: Array) -> Tuple[qx.StateVector, Array]: + return self.compute(params, key) + + def sample( + self, + params: Array, + num_trajectories: int = 1000, + batch_size: int = 250, + random_seed: int = 0, + ) -> Array: + """Run trajectory simulation in batches, returning only measurement outcomes. + + State vectors are discarded after each batch, making this scalable + to arbitrarily many trajectories. + + :param params: Flat parameter vector from :meth:`linearize`. + :param num_trajectories: Total number of trajectories to simulate. + :param batch_size: Maximum number of trajectories per batch. + :param random_seed: Seed for the JAX PRNG. + :return: Measurement outcomes with shape ``(num_trajectories, n_measurements)``. + """ + resolved = self.resolve(params) + compressed = self.compress(resolved) + operations = self.adapt(compressed) + + _, all_outcomes = _run_batched_trajectories( + operations, self.n_qubits, num_trajectories, batch_size, random_seed, + keep_states=False, dims=self.dims, + ) + + if len(all_outcomes) == 1: + return all_outcomes[0] + return jnp.concatenate(all_outcomes, axis=0) + + +# ══════════════════════════════════════════════════════════ +# Trajectory simulation internals +# ══════════════════════════════════════════════════════════ + + +def _apply_trajectory_operations( + operations: List[TrajectoryOp], + psi: qx.StateVector, + key: Array, +) -> Tuple[qx.StateVector, Array]: + """Apply trajectory operations to a (batched) state vector. + + Dispatches each operation by type: + + - ``qx.Unitary``: deterministic gate application + - ``qx.KrausMap``: probabilistic Kraus operator sampling + - ``qx.QuantumInstrument``: measurement with outcome recording + + :param operations: Ordered list of (operator, subsystem) pairs. + :param psi: Initial state vector, optionally batched via ensemble dimension. + :param key: JAX PRNG key (scalar typed key). Will be split internally to + produce per-trajectory, per-operation sub-keys. + :return: Tuple of ``(final_state_vector, measurement_outcomes)`` where + measurement_outcomes has shape ``(*ensemble, n_measurements)`` with + dtype int32. + """ + measurement_outcomes: List[Array] = [] + + n_stochastic = sum( + 1 for op, _ in operations + if isinstance(op, (qx.KrausMap, qx.QuantumInstrument)) + ) + + ensemble_size = psi.ensemble_size + + if n_stochastic > 0: + if ensemble_size: + n_traj = ensemble_size[0] + all_keys = jax.random.split(key, n_stochastic * n_traj) + all_keys = all_keys.reshape(n_stochastic, n_traj) + else: + all_keys = jax.random.split(key, n_stochastic) + + key_idx = 0 + + for op, subsystem in operations: + match op: + case qx.Unitary(): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + case qx.KrausMap(): + op_keys = all_keys[key_idx] + psi = qx.targeted_apply_kraus_map_trajectory(op, psi, op_keys, subsystem) + key_idx += 1 + case qx.QuantumInstrument(): + op_keys = all_keys[key_idx] + psi, outcome = qx.targeted_apply_instrument_to_state_vector(op, psi, op_keys, subsystem) + measurement_outcomes.append(outcome) + key_idx += 1 + case _: + raise TypeError(f"Unsupported operator type: {type(op)}") + + if measurement_outcomes: + outcomes = jnp.stack(measurement_outcomes, axis=-1) + else: + outcomes = jnp.empty((*psi.ensemble_size, 0), dtype=jnp.int32) + + return psi, outcomes + + +def _run_batched_trajectories( + operations: List[TrajectoryOp], + n_qubits: int, + num_trajectories: int, + batch_size: int, + random_seed: int, + keep_states: bool = True, + dims: Tuple[int, ...] | None = None, +) -> Tuple[List[qx.StateVector] | None, List[Array]]: + """Run trajectory simulation in batches.""" + if dims is None: + dims = (2,) * n_qubits + + key = jax.random.key(random_seed) + all_psis: List[qx.StateVector] = [] if keep_states else [] + all_outcomes: List[Array] = [] + + remaining = num_trajectories + batch_idx = 0 + t_total = 0.0 + while remaining > 0: + this_batch = min(remaining, batch_size) + key, batch_key = jax.random.split(key) + + if this_batch == 1: + psi = qx.zero_state_vector(dims=dims) + else: + psi = qx.zero_state_vector(dims=dims, ensemble_size=(this_batch,)) + + t0 = time.perf_counter() + psi_out, outcomes = _apply_trajectory_operations(operations, psi, batch_key) + psi_out.matrix.block_until_ready() + t1 = time.perf_counter() + t_total += t1 - t0 + + if this_batch == 1: + psi_out = qx.StateVector.from_matrix( + psi_out.matrix[jnp.newaxis], psi_out.dims, + ) + outcomes = outcomes[jnp.newaxis] + + logger.debug( + "Batch %d: %d trajectories, %d qubits, %.3f s", + batch_idx, this_batch, n_qubits, t1 - t0, + ) + + if keep_states: + all_psis.append(psi_out) + all_outcomes.append(outcomes) + remaining -= this_batch + batch_idx += 1 + + logger.info( + "Trajectories complete: %d total, %d batches (size=%d), " + "n_qubits=%d, %.3f s total, %.1f traj/s", + num_trajectories, batch_idx, batch_size, n_qubits, + t_total, num_trajectories / t_total if t_total > 0 else float("inf"), + ) + + return (all_psis if keep_states else None), all_outcomes diff --git a/pyquil/transform.py b/pyquil/transform.py new file mode 100644 index 000000000..3a6460d57 --- /dev/null +++ b/pyquil/transform.py @@ -0,0 +1,213 @@ +""" +transform module +---------------- + +Utility functions for Quil program manipulation. +""" + +from __future__ import annotations + +from copy import deepcopy +from typing import AbstractSet, List, Optional + +from pyquil.api import MemoryMap +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference, substitute +from pyquil.quilbase import Declare, DefCircuit, Gate, Measurement, ResetQubit +from quil.instructions import CircuitDefinition +from quil.instructions import Instruction as QuilInstruction +from quil.program import Program as QuilProgram + + +def copy_everything_except_instructions( + program: Program, include_defcircuits: bool = True, include_kraus: bool = True +) -> Program: + """Create a new program with only the definitions of the input program. + + :param program: A pyQuil program. + :param include_defcircuits: If True, include DEFCIRCUIT definitions. + :param include_kraus: If True, include KRAUS definitions. + """ + from pyquil.quilbase import Pragma + + p = QuilProgram() + p.waveforms = program._program.waveforms + p.calibrations = program._program.calibrations + p.frames = program._program.frames + p.gate_definitions = program._program.gate_definitions + + program_definitions = Program() + program_definitions._program = p + + # Pragma externs are definitions + program_definitions += ( + [QuilInstruction.from_pragma(pragma) for pragma in program._program.pragma_extern_map.values()], + ) + + if include_defcircuits is True: + defcircuits = set() + for inst in program._program.to_instructions(): + if isinstance(inst.inner(), CircuitDefinition) and str(inst) not in defcircuits: + defcircuits.add(str(inst)) + program_definitions._program.add_instruction(inst) + + if include_kraus is True: + for inst in program.instructions: + if isinstance(inst, Pragma): + try: + if inst.command == "ADD-KRAUS": + program_definitions._program.add_instruction(inst) + except Exception: + pass + + return program_definitions + + +def unparameterize(program: Program, memory_map: MemoryMap) -> Program: + """Apply a memory map to a program, and evaluate any arithmetic. + + Memory declarations will be removed, except "ro". + + :param program: A pyquil program, possibly with parameters. + :param memory_map: A memory map, with values for the parameters. + """ + unparameterized_program = Program() + unparameterized_program += copy_everything_except_instructions(program) + instructions = program.instructions + parameter_substitution_map = {} + + if memory_map is not None: + parameter_substitution_map = { + MemoryReference(name=name, offset=offset, declared_size=len(value)) + if isinstance(name, str) + else name: value[offset] + for name, value in memory_map.items() + for offset in range(len(value)) + } + + for idx, inst in enumerate(instructions): + if isinstance(inst, Declare): + if inst.name == "ro": + unparameterized_program += deepcopy(inst) + elif isinstance(inst, Gate): + if len(inst.params) > 0: + unparameterized_program += Gate( + name=inst.name, + params=[substitute(p, parameter_substitution_map) for p in inst.params], + qubits=inst.qubits, + ) + else: + unparameterized_program += inst + + else: + unparameterized_program += inst + + unparameterized_program.wrap_in_numshots_loop(program.num_shots) + + return unparameterized_program + + +def expand_defcircuits( + program: Program, + expand_if_defcal: bool = True, + calibration_program: Optional[Program] = None, + keep_defcircuits: bool = False, + expand_names_except: AbstractSet[str] = frozenset(), +) -> Program: + """Expand DEFCIRCUITS into individual instructions. + + :param program: A Quil program, which may contain DefCircuits. + :param expand_if_defcal: Expand the defcircuit even if it has a defcalibration. + :param calibration_program: Calibrations to supplement those in ``program``. Existing + calibrations in ``program`` take precedence. + :param keep_defcircuits: If True, keep the DEFCIRCUIT definitions in the returned program. + :param expand_names_except: Set of DefCircuit names to skip during expansion. + Instructions matching these names are left unexpanded. + :return: A Quil program, with any Circuit instructions expanded to individual instructions. + """ + instructions: List = [] + circuit_definitions: dict = {} + for inst in program.instructions: + if isinstance(inst, DefCircuit): + circuit_definitions[inst.name] = inst + if keep_defcircuits is True: + instructions.append(inst) + else: + instructions.append(inst) + + holistic_calibration_program = Program() + if calibration_program is not None: + holistic_calibration_program += calibration_program + holistic_calibration_program += copy_everything_except_instructions(program, include_defcircuits=False) + + expanded_program = Program() + expanded_program += holistic_calibration_program + + if len(circuit_definitions) == 0 and len(instructions) == 0: + return expanded_program + + def _expand_instruction(inst: Gate) -> List: + instruction_name = inst.name + expanded_instructions: List = [] + if expand_if_defcal is False: + cal = holistic_calibration_program.get_calibration(inst) + if cal is not None: + return [inst] + + defcircuit = circuit_definitions[instruction_name] + qubit_variables = defcircuit.qubit_variables + qubits = inst.qubits + + qarg_to_arg_map = {qarg: q for q, qarg in zip(qubits, qubit_variables)} + parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters)} + + for circuit_inst in defcircuit.instructions: + match circuit_inst: + case Gate(): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] + if hasattr(circuit_inst, "params"): + circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] + if circuit_inst.name in circuit_definitions: + expanded_instructions += _expand_instruction(circuit_inst) + else: + expanded_instructions.append(circuit_inst) + case Measurement(): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] + expanded_instructions.append(circuit_inst) + case ResetQubit(): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] + expanded_instructions.append(circuit_inst) + case _: + expanded_instructions.append(deepcopy(circuit_inst)) + return expanded_instructions + + expanded_instructions: List = [] + for inst in instructions: + if isinstance(inst, Gate): + instruction_name = inst.name + if instruction_name in expand_names_except: + expanded_instructions.append(inst) + continue + qubits = tuple(int(q) for q in inst.get_qubit_indices()) + if ( + (instruction_name in circuit_definitions) + and len(qubits) == len(circuit_definitions[instruction_name].qubit_variables) + and len(inst.params) == len(circuit_definitions[instruction_name].parameters) + ): + if expand_if_defcal is False: + cal = program.get_calibration(inst) + if cal is not None: + expanded_instructions.append(inst) + continue + + expanded_instructions += _expand_instruction(inst) + else: + expanded_instructions.append(inst) + else: + expanded_instructions.append(inst) + + expanded_program += expanded_instructions + return expanded_program diff --git a/test/unit/conftest.py b/test/unit/conftest.py index fcb8d236a..445c6562c 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -1,6 +1,10 @@ import os from typing import Any, Dict +import jax + +jax.config.update("jax_enable_x64", True) + import numpy as np import pytest from qcs_sdk import QCSClient diff --git a/test/unit/test_legacy_noise.py b/test/unit/test_legacy_noise.py new file mode 100644 index 000000000..5a4861785 --- /dev/null +++ b/test/unit/test_legacy_noise.py @@ -0,0 +1,371 @@ +from collections import OrderedDict + +import numpy as np +import pytest +from pytest_mock import MockerFixture +from qcs_sdk import ExecutionData, RegisterData, ResultData +from qcs_sdk.qvm import QVMResultData + +from pyquil.api._qam import QAMExecutionResult +from pyquil.gates import CZ, RX, RZ, I +from pyquil.noise import ( + INFINITY, + NO_NOISE, + KrausModel, + NoiseModel, + _decoherence_noise_model, + _get_program_gates, + _noise_model_program_header, + add_decoherence_noise, + apply_noise_model, + bitstring_probs_to_z_moments, + combine_kraus_maps, + correct_bitstring_probs, + corrupt_bitstring_probs, + damping_after_dephasing, + damping_kraus_map, + dephasing_kraus_map, + estimate_assignment_probs, + estimate_bitstring_probs, + pauli_kraus_map, + tensor_kraus_maps, +) +from pyquil.quil import Pragma, Program +from pyquil.quilbase import DefGate, Gate + + +def test_pauli_kraus_map(): + probabilities = [0.1, 0.2, 0.3, 0.4] + k1, k2, k3, k4 = pauli_kraus_map(probabilities) + assert np.allclose(k1, np.sqrt(0.1) * np.eye(2), atol=1 * 10**-8) + assert np.allclose(k2, np.sqrt(0.2) * np.array([[0, 1.0], [1.0, 0]]), atol=1 * 10**-8) + assert np.allclose(k3, np.sqrt(0.3) * np.array([[0, -1.0j], [1.0j, 0]]), atol=1 * 10**-8) + assert np.allclose(k4, np.sqrt(0.4) * np.array([[1, 0], [0, -1]]), atol=1 * 10**-8) + + two_q_pauli_kmaps = pauli_kraus_map(np.kron(probabilities, list(reversed(probabilities)))) + q1_pauli_kmaps = [k1, k2, k3, k4] + q2_pauli_kmaps = pauli_kraus_map(list(reversed(probabilities))) + tensor_kmaps = tensor_kraus_maps(q1_pauli_kmaps, q2_pauli_kmaps) + assert np.allclose(two_q_pauli_kmaps, tensor_kmaps) + + +def test_damping_kraus_map(): + p = 0.05 + k1, k2 = damping_kraus_map(p=p) + assert k1[1, 1] == np.sqrt(1 - p) + assert k2[0, 1] == np.sqrt(p) + + +def test_dephasing_kraus_map(): + p = 0.05 + k1, k2 = dephasing_kraus_map(p=p) + np.testing.assert_allclose(np.diag(k1), [np.sqrt(1 - p)] * 2) + np.testing.assert_allclose(np.abs(np.diag(k2)), [np.sqrt(p)] * 2) + + +def test_tensor_kraus_maps(): + damping = damping_kraus_map() + k1, k2, k3, k4 = tensor_kraus_maps(damping, damping) + assert k1.shape == (4, 4) + assert k2.shape == (4, 4) + assert k3.shape == (4, 4) + assert k4.shape == (4, 4) + np.testing.assert_allclose(k1[-1, -1], 1 - 0.1) + + +def test_combine_kraus_maps(): + damping = damping_kraus_map() + dephasing = dephasing_kraus_map() + k1, k2, k3, k4 = combine_kraus_maps(damping, dephasing) + assert k1.shape == (2, 2) + assert k2.shape == (2, 2) + assert k3.shape == (2, 2) + assert k4.shape == (2, 2) + + +def test_damping_after_dephasing(): + gate_time = 1 + T1 = 10 + T2 = 3 + kraus_map = damping_after_dephasing(T1, T2, gate_time) + + # Density matrix for the |+> state + rho = 0.5 * np.ones((2, 2)) + + # See Eq. 7.144 of Nielsen and Chuang + target_rho = [ + [1 - 0.5 * np.exp(-gate_time / T1), 0.5 * np.exp(-gate_time / T2)], + [0.5 * np.exp(-gate_time / T2), 0.5 * np.exp(-gate_time / T1)], + ] + + noisy_rho = np.zeros((2, 2)) + for op in kraus_map: + noisy_rho += op @ rho @ (op.T.conj()) + + np.testing.assert_allclose(noisy_rho, target_rho) + + +def test_noise_helpers(): + gates = RX(np.pi / 2, 0), RX(-np.pi / 2, 1), I(1), CZ(0, 1) + prog = Program(*gates) + inferred_gates = [g.out() for g in _get_program_gates(prog)] + assert set(inferred_gates) == set([g.out() for g in gates]) + + +def test_decoherence_noise(): + prog = Program(RX(np.pi / 2, 0), CZ(0, 1), RZ(np.pi, 0)) + gates = _get_program_gates(prog) + m1 = _decoherence_noise_model(gates, T1=INFINITY, T2=INFINITY, ro_fidelity=1.0) + + # with no readout error, assignment_probs = identity matrix + assert np.allclose(m1.assignment_probs[0], np.eye(2)) + assert np.allclose(m1.assignment_probs[1], np.eye(2)) + for g in m1.gates: + # with infinite coherence time all kraus maps should only have a single, unitary kraus op + assert len(g.kraus_ops) == 1 + (k0,) = g.kraus_ops + # check unitarity + k0dk0 = k0.dot(k0.conjugate().transpose()) + assert np.allclose(k0dk0, np.eye(k0dk0.shape[0])) + + # verify that selective (by qubit) dephasing and readout infidelity is working + m2 = _decoherence_noise_model(gates, T1=INFINITY, T2={0: 30e-6}, ro_fidelity={0: 0.95, 1: 1.0}) + assert np.allclose(m2.assignment_probs[0], [[0.95, 0.05], [0.05, 0.95]]) + assert np.allclose(m2.assignment_probs[1], np.eye(2)) + for g in m2.gates: + if 0 in g.targets: + # single dephasing (no damping) channel on qc 0, no noise on qc1 -> 2 Kraus ops + assert len(g.kraus_ops) == 2 + else: + assert len(g.kraus_ops) == 1 + + # verify that combined T1 and T2 will lead to 4 outcome Kraus map. + m3 = _decoherence_noise_model(gates, T1={0: 30e-6}, T2={0: 30e-6}) + for g in m3.gates: + if 0 in g.targets: + # damping (implies dephasing) channel on qc 0, no noise on qc1 -> 4 Kraus ops + assert len(g.kraus_ops) == 4 + else: + assert len(g.kraus_ops) == 1 + + # verify that gate names are translated + new_prog = apply_noise_model(prog, m3) + + # check that headers have been embedded + headers = _noise_model_program_header(m3) + assert all( + (isinstance(i, Pragma) and i.command in ["ADD-KRAUS", "READOUT-POVM"]) or isinstance(i, DefGate) + for i in headers + ) + assert headers.out() in new_prog.out() + + # verify that high-level add_decoherence_noise reproduces new_prog + new_prog2 = add_decoherence_noise(prog, T1={0: 30e-6}, T2={0: 30e-6}) + assert new_prog == new_prog2 + + +def test_kraus_model_1(): + km = KrausModel("I", (5.0,), (0, 1), [np.array([[1 + 1j]])], 1.0) + d = km.to_dict() + assert d == OrderedDict( + [ + ("gate", km.gate), + ("params", km.params), + ("targets", (0, 1)), + ("kraus_ops", [[[[1.0]], [[1.0]]]]), + ("fidelity", 1.0), + ] + ) + assert KrausModel.from_dict(d) == km + + +@pytest.fixture +def kraus_model_I_dict(): + return { + "gate": "I", + "fidelity": 1.0, + "kraus_ops": [[[[1.0]], [[1.0]]]], + "targets": (0, 1), + "params": (5.0,), + } + + +def test_kraus_model_2(kraus_model_I_dict): + km = KrausModel.from_dict(kraus_model_I_dict) + assert km == KrausModel( + gate=kraus_model_I_dict["gate"], + params=kraus_model_I_dict["params"], + targets=kraus_model_I_dict["targets"], + kraus_ops=[KrausModel.unpack_kraus_matrix(kraus_op) for kraus_op in kraus_model_I_dict["kraus_ops"]], + fidelity=kraus_model_I_dict["fidelity"], + ) + d = km.to_dict() + assert d == OrderedDict( + [ + ("gate", km.gate), + ("params", km.params), + ("targets", (0, 1)), + ("kraus_ops", [[[[1.0]], [[1.0]]]]), + ("fidelity", 1.0), + ] + ) + + +def test_noise_model_1(): + km1 = KrausModel("I", (5.0,), (0, 1), [np.array([[1 + 1j]])], 1.0) + km2 = KrausModel("RX", (np.pi / 2,), (0,), [np.array([[1 + 1j]])], 1.0) + nm = NoiseModel([km1, km2], {0: np.eye(2), 1: np.eye(2)}) + + assert nm == NoiseModel.from_dict(nm.to_dict()) + assert nm.gates_by_name("I") == [km1] + assert nm.gates_by_name("RX") == [km2] + + +@pytest.fixture +def kraus_model_RX90_dict(): + return { + "gate": "RX", + "fidelity": 1.0, + "kraus_ops": [[[[1.0]], [[1.0]]]], + "targets": (0,), + "params": (np.pi / 2.0,), + } + + +def test_noise_model_2(kraus_model_I_dict, kraus_model_RX90_dict): + noise_model_dict = { + "gates": [kraus_model_I_dict, kraus_model_RX90_dict], + "assignment_probs": {"1": [[1.0, 0.0], [0.0, 1.0]], "0": [[1.0, 0.0], [0.0, 1.0]]}, + } + + nm = NoiseModel.from_dict(noise_model_dict) + km1 = KrausModel.from_dict(kraus_model_I_dict) + km2 = KrausModel.from_dict(kraus_model_RX90_dict) + assert nm == NoiseModel(gates=[km1, km2], assignment_probs={0: np.eye(2), 1: np.eye(2)}) + assert nm.gates_by_name("I") == [km1] + assert nm.gates_by_name("RX") == [km2] + assert nm.to_dict() == noise_model_dict + + +def test_readout_compensation(): + np.random.seed(1234124) + p = np.random.rand(2, 2, 2, 2, 2, 2) + p /= p.sum() + + aps = [np.eye(2) + 0.2 * (np.random.rand(2, 2) - 1) for _ in range(p.ndim)] + for ap in aps: + ap.flat[ap.flat < 0] = 0.0 + ap /= ap.sum() + assert (ap >= 0).all() + + assert (p >= 0).all() + + p_corrupted = corrupt_bitstring_probs(p, aps) + p_restored = correct_bitstring_probs(p_corrupted, aps) + assert np.allclose(p, p_restored) + + results = [[0, 0, 0]] * 100 + [[0, 1, 1]] * 200 + p1 = estimate_bitstring_probs(results) + assert np.isclose(p1[0, 0, 0], 1.0 / 3.0) + assert np.isclose(p1[0, 1, 1], 2.0 / 3.0) + assert np.isclose(p1.sum(), 1.0) + + zm = bitstring_probs_to_z_moments(p1) + assert np.isclose(zm[0, 0, 0], 1) + assert np.isclose(zm[1, 0, 0], 1) + assert np.isclose(zm[0, 1, 0], -1.0 / 3) + assert np.isclose(zm[0, 0, 1], -1.0 / 3) + assert np.isclose(zm[0, 1, 1], 1.0) + assert np.isclose(zm[1, 1, 0], -1.0 / 3) + assert np.isclose(zm[1, 0, 1], -1.0 / 3) + assert np.isclose(zm[1, 1, 1], 1.0) + + +def test_estimate_assignment_probs(mocker: MockerFixture): + mock_qc = mocker.patch("pyquil.api.QuantumComputer").return_value + mock_compiler = mocker.patch("pyquil.api._abstract_compiler.AbstractCompiler").return_value + + trials = 100 + p00 = 0.8 + p11 = 0.75 + + mock_compiler.native_quil_to_executable.return_value = Program() + mock_qc.compiler = mock_compiler + mock_qc + mock_qc.run.side_effect = [ + QAMExecutionResult( + executable=None, + data=ExecutionData( + result_data=ResultData( + QVMResultData.from_memory_map( + { + "ro": RegisterData.from_i16( + ( + np.array([[0]]) * int(round(p00 * trials)) + + np.array([[1]]) * int(round((1 - p00) * trials)) + ).tolist() + ) + } + ) + ) + ), + ), # I gate results + QAMExecutionResult( + executable=None, + data=ExecutionData( + result_data=ResultData( + QVMResultData.from_memory_map( + { + "ro": RegisterData.from_i16( + ( + np.array([[1]]) * int(round(p11 * trials)) + + np.array([[0]]) * int(round((1 - p11) * trials)) + ).tolist() + ) + } + ) + ) + ), + ), # X gate results + ] + ap_target = np.array([[p00, 1 - p11], [1 - p00, p11]]) + + povm_pragma = Pragma("READOUT-POVM", [0], "({} {} {} {})".format(*ap_target.flatten())) + ap = estimate_assignment_probs(0, trials, mock_qc, Program(povm_pragma)) + + assert mock_compiler.native_quil_to_executable.call_count == 2 + assert mock_qc.run.call_count == 2 + + for call in mock_compiler.native_quil_to_executable.call_args_list: + args, kwargs = call + prog = args[0] + assert povm_pragma in prog.instructions + + assert np.allclose(ap, ap_target) + + +def test_apply_noise_model(): + p = Program(RX(np.pi / 2, 0), RX(np.pi / 2, 1), CZ(0, 1), RX(np.pi / 2, 1)) + noise_model = _decoherence_noise_model(_get_program_gates(p)) + pnoisy = apply_noise_model(p, noise_model) + for i in pnoisy: + if isinstance(i, DefGate): + pass + elif isinstance(i, Pragma): + assert i.command in ["ADD-KRAUS", "READOUT-POVM"] + elif isinstance(i, Gate): + assert i.name in NO_NOISE or not i.params + + +def test_apply_noise_model_perturbed_angles(): + eps = 1e-15 + p = Program(RX(np.pi / 2 + eps, 0), RX(np.pi / 2 - eps, 1), CZ(0, 1), RX(np.pi / 2 + eps, 1)) + noise_model = _decoherence_noise_model(_get_program_gates(p)) + pnoisy = apply_noise_model(p, noise_model) + for i in pnoisy: + if isinstance(i, DefGate): + pass + elif isinstance(i, Pragma): + assert i.command in ["ADD-KRAUS", "READOUT-POVM"] + elif isinstance(i, Gate): + assert i.name in NO_NOISE or not i.params diff --git a/test/unit/test_noise_model.py b/test/unit/test_noise_model.py new file mode 100644 index 000000000..09e77bc4b --- /dev/null +++ b/test/unit/test_noise_model.py @@ -0,0 +1,310 @@ +# Copyright 2024-2026 Rigetti Computing +# +# 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. + +"""Unit tests for the quax-based noise model (Channel, MeasurementChannel, ResetChannel, NoiseModel).""" + +import jax.numpy as jnp +import numpy as np +import pytest +import quax as qx + +from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, X +from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel, get_instruction_unitary +from pyquil.noise._noise_model import NoiseModel +from pyquil.quil import Program +from pyquil.quilbase import Gate, Measurement, ResetQubit +from pyquil.simulation._simulator import DensityMatrixSimulator + +_EMPTY_PARAMS = jnp.array([], dtype=float) + + +def _dm(program, noise_model=None, qubits=None): + """Compute density matrix.""" + sim = DensityMatrixSimulator(program, qubits=qubits, noise_model=noise_model) + return sim.compute(_EMPTY_PARAMS) + + +# ────────────────────────────────────────────────────────── +# Channel tests +# ────────────────────────────────────────────────────────── + + +class TestChannel: + def test_from_depolarizing_constant(self): + """Channel.from_depolarizing_constant produces valid superoperator.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + assert isinstance(ch.process, qx.SuperOp) + # Process fidelity should be close to the depolarizing constant + assert ch.fidelity < 1.0 + assert ch.fidelity > 0.95 + + def test_from_gate_fidelity(self): + """Channel.from_gate_fidelity produces correct fidelity.""" + inst = RX(np.pi / 2, 0) + ch = Channel.from_gate_fidelity(inst=inst, fidelity=0.99) + assert abs(ch.fidelity - 0.99) < 0.001 + + def test_from_pauli_fidelity(self): + """Channel.from_pauli_fidelity produces a valid channel.""" + inst = X(0) + ch = Channel.from_pauli_fidelity(inst=inst, pauli_fidelity=0.97) + assert isinstance(ch.process, qx.SuperOp) + assert ch.pauli_fidelity == pytest.approx(0.97, abs=0.001) + + def test_from_pauli_noise(self): + """Channel.from_pauli_noise produces a valid Pauli noise channel.""" + inst = RX(0.5, 0) + ch = Channel.from_pauli_noise(inst=inst, pauli_noise={"X": 0.01, "Z": 0.02}) + assert isinstance(ch.process, qx.SuperOp) + assert ch.fidelity < 1.0 + + def test_from_coherence_times(self): + """Channel.from_coherence_times produces a valid decoherence channel.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_coherence_times(inst=inst, gate_duration=40e-9, t1s=[30e-6], t2s=[20e-6]) + assert isinstance(ch.process, qx.SuperOp) + assert ch.fidelity < 1.0 + assert ch.fidelity > 0.99 # short gate relative to T1/T2 + + def test_qubits(self): + """Channel.qubits reflects the instruction's qubits.""" + ch = Channel.from_depolarizing_constant(inst=RX(0.1, 3), depolarizing_constant=0.99) + assert ch.qubits == [3] + + def test_num_qubits(self): + """Channel.num_qubits is correct for 2Q gates.""" + ch = Channel.from_depolarizing_constant(inst=CNOT(0, 1), depolarizing_constant=0.95) + assert ch.num_qubits == 2 + + def test_fidelity_properties(self): + """Fidelity, infidelity, pauli_fidelity, pauli_infidelity are consistent.""" + ch = Channel.from_gate_fidelity(inst=RX(0.3, 0), fidelity=0.98) + assert ch.fidelity + ch.infidelity == pytest.approx(1.0) + assert ch.pauli_fidelity + ch.pauli_infidelity == pytest.approx(1.0) + assert ch.stochastic_fidelity + ch.stochastic_infidelity == pytest.approx(1.0) + assert ch.coherent_fidelity + ch.coherent_infidelity == pytest.approx(1.0) + + def test_noise_process(self): + """noise_process factors out the ideal gate unitary.""" + ch = Channel.from_depolarizing_constant(inst=RX(np.pi / 4, 0), depolarizing_constant=0.99) + noise = ch.noise_process + assert isinstance(noise, qx.SuperOp) + + def test_is_pauli(self): + """A depolarizing channel on a Clifford gate should be a Pauli channel.""" + ch = Channel.from_depolarizing_constant(inst=X(0), depolarizing_constant=0.98) + assert ch.is_pauli() + + def test_pauli_twirl(self): + """Pauli twirl of a channel on a Clifford gate should be a Pauli channel.""" + ch = Channel.from_random_coherent_error( + inst=X(0), process_fidelity=0.97, rng=np.random.default_rng(42) + ) + twirled = ch.pauli_twirl() + assert twirled.is_pauli() + + def test_unitarity(self): + """A depolarizing channel should have unitarity < 1.""" + ch = Channel.from_depolarizing_constant(inst=RX(0.5, 0), depolarizing_constant=0.95) + assert 0 < ch.unitarity < 1.0 + + def test_pauli_vector_sums_to_one(self): + """Pauli error probability vector should sum to 1.""" + ch = Channel.from_depolarizing_constant(inst=X(0), depolarizing_constant=0.97) + pv = ch.pauli_vector + assert float(jnp.sum(pv)) == pytest.approx(1.0, abs=1e-8) + + def test_perfect_channel(self): + """A depolarizing constant of 1.0 should give fidelity 1.0.""" + ch = Channel.from_depolarizing_constant(inst=RX(np.pi, 0), depolarizing_constant=1.0) + assert ch.fidelity == pytest.approx(1.0, abs=1e-10) + + +# ────────────────────────────────────────────────────────── +# MeasurementChannel tests +# ────────────────────────────────────────────────────────── + + +class TestMeasurementChannel: + def test_from_readout_fidelity(self): + """MeasurementChannel.from_readout_fidelity produces a valid quantum instrument.""" + inst = Measurement(Gate("MEASURE", [], [0]).qubits[0], None) + # Use the pyquil MEASURE gate to get qubit + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + ch = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.95) + assert isinstance(ch.process, qx.QuantumInstrument) + + def test_from_readout_fidelity_with_asymmetry(self): + """MeasurementChannel with asymmetry produces asymmetric confusion.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + ch = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.95, asymmetry=0.5) + assert isinstance(ch.process, qx.QuantumInstrument) + + def test_qubits(self): + """MeasurementChannel.qubits returns the correct qubit.""" + prog = Program(MEASURE(5, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + ch = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.99) + assert ch.qubits == [5] + + +# ────────────────────────────────────────────────────────── +# NoiseModel tests +# ────────────────────────────────────────────────────────── + + +class TestNoiseModel: + def test_empty_model(self): + """An empty NoiseModel has no channels.""" + nm = NoiseModel(channels=frozenset()) + assert nm.get_channel(RX(0.5, 0)) is None + + def test_get_channel_gate(self): + """NoiseModel.get_channel returns the correct Channel for a gate.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + nm = NoiseModel(channels=frozenset([ch])) + retrieved = nm.get_channel(inst) + assert retrieved is ch + + def test_get_channel_returns_none_for_missing(self): + """get_channel returns None for instructions not in the model.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + nm = NoiseModel(channels=frozenset([ch])) + other_inst = RY(np.pi / 2, 1) + assert nm.get_channel(other_inst) is None + + def test_multiple_channels(self): + """NoiseModel with multiple channels retrieves each correctly.""" + inst1 = RX(0.5, 0) + inst2 = RY(0.3, 1) + inst3 = CNOT(0, 1) + ch1 = Channel.from_depolarizing_constant(inst=inst1, depolarizing_constant=0.99) + ch2 = Channel.from_depolarizing_constant(inst=inst2, depolarizing_constant=0.97) + ch3 = Channel.from_depolarizing_constant(inst=inst3, depolarizing_constant=0.95) + nm = NoiseModel(channels=frozenset([ch1, ch2, ch3])) + assert nm.get_channel(inst1) is ch1 + assert nm.get_channel(inst2) is ch2 + assert nm.get_channel(inst3) is ch3 + + +# ────────────────────────────────────────────────────────── +# get_instruction_unitary tests +# ────────────────────────────────────────────────────────── + + +class TestGetInstructionUnitary: + def test_standard_gate(self): + """get_instruction_unitary resolves standard gates.""" + u = get_instruction_unitary(RX(np.pi / 2, 0)) + assert isinstance(u, qx.Unitary) + assert u.matrix.shape == (2, 2) + + def test_two_qubit_gate(self): + """get_instruction_unitary resolves 2Q gates.""" + u = get_instruction_unitary(CNOT(0, 1)) + assert isinstance(u, qx.Unitary) + assert u.matrix.shape == (4, 4) + + def test_custom_gate(self): + """get_instruction_unitary resolves custom gates from custom_gates dict.""" + custom_matrix = np.array([[0, 1], [1, 0]], dtype=complex) + inst = Gate("MY_GATE", [], [0]) + u = get_instruction_unitary(inst, custom_gates={"MY_GATE": qx.Unitary.from_matrix(custom_matrix, ((2,), (2,)))}) + assert isinstance(u, qx.Unitary) + assert np.allclose(np.asarray(u.matrix), custom_matrix) + + +# ────────────────────────────────────────────────────────── +# ResetChannel tests +# ────────────────────────────────────────────────────────── + + +class TestResetChannel: + def test_from_reset_fidelity_perfect(self): + """A perfect reset (fidelity=1.0) should produce a valid superoperator.""" + inst = RESET(0) + ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) + assert isinstance(ch.process, qx.SuperOp) + + def test_from_reset_fidelity_noisy(self): + """A noisy reset should have lower fidelity than a perfect one.""" + inst = RESET(0) + ch_perfect = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) + ch_noisy = ResetChannel.from_reset_fidelity(inst=inst, fidelity=0.95) + assert isinstance(ch_noisy.process, qx.SuperOp) + assert ch_noisy.fidelity < ch_perfect.fidelity + + def test_qubits(self): + """ResetChannel.qubits returns the correct qubit.""" + inst = RESET(3) + ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=0.99) + assert ch.qubits == [3] + + def test_ideal_reset_maps_excited_to_ground(self): + """An ideal reset on an excited qubit should produce |0><0|.""" + inst = RESET(0) + ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) + noise_model = NoiseModel(channels=frozenset([ch])) + # Prepare |1> then reset + program = Program(X(0), RESET(0)) + rho = _dm(program, noise_model=noise_model) + target_rho = qx.zero_state_matrix(1) + assert qx.fidelity(rho, target_rho) > 0.9999 + + def test_ideal_reset_maps_superposition_to_ground(self): + """An ideal reset on a superposition state should produce |0><0|.""" + inst = RESET(0) + ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) + noise_model = NoiseModel(channels=frozenset([ch])) + # Prepare |+> then reset + program = Program(RX(np.pi / 2, 0), RESET(0)) + rho = _dm(program, noise_model=noise_model) + target_rho = qx.zero_state_matrix(1) + assert qx.fidelity(rho, target_rho) > 0.9999 + + def test_noisy_reset_reduces_fidelity(self): + """A noisy reset should produce a state with fidelity < 1 relative to |0><0|.""" + inst = RESET(0) + ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=0.90) + noise_model = NoiseModel(channels=frozenset([ch])) + program = Program(X(0), RESET(0)) + rho = _dm(program, noise_model=noise_model) + target_rho = qx.zero_state_matrix(1) + fid = float(qx.fidelity(rho, target_rho)) + # Should be less than perfect but still high + assert 0.85 < fid < 1.0 + + def test_reset_in_multi_qubit_circuit(self): + """Reset on one qubit should not affect the other qubit.""" + inst = RESET(0) + ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) + noise_model = NoiseModel(channels=frozenset([ch])) + # Prepare |11> then reset qubit 0 + program = Program(X(0), X(1), RESET(0)) + rho = _dm(program, noise_model=noise_model) + # Expected state: |0> on qubit 0, |1> on qubit 1 → |01> + target_rho = (qx.gates.I | qx.gates.X) @ qx.zero_state_matrix(2) + assert qx.fidelity(rho, target_rho) > 0.9999 + + def test_global_reset(self): + """A global RESET (no qubit specified) resets all qubits to |0>.""" + program = Program(X(0), X(1), RESET()) + rho = _dm(program) + target_rho = qx.zero_state_matrix(2) + assert qx.fidelity(rho, target_rho) > 0.9999 diff --git a/test/unit/test_qutrit_simulation.py b/test/unit/test_qutrit_simulation.py new file mode 100644 index 000000000..b92454ab0 --- /dev/null +++ b/test/unit/test_qutrit_simulation.py @@ -0,0 +1,564 @@ +"""Unit tests for qutrit (d=3) and mixed qubit/qutrit simulation.""" + +import jax +import jax.numpy as jnp +import numpy as np +import pytest +import quax as qx + +from pyquil.gates import H, MEASURE, X +from pyquil.quil import Program +from pyquil.quilatom import Qubit +from pyquil.quilbase import DefGate, Gate, Measurement + +from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel +from pyquil.noise._noise_model import NoiseModel +from pyquil.simulation._simulator import ( + PureStateVectorSimulator, + DensityMatrixSimulator, + TrajectorySimulator, +) + +_EMPTY_PARAMS = jnp.array([], dtype=float) + + +def _sv(program, qubits=None): + """Compute pure state vector for a gate-only program.""" + sim = PureStateVectorSimulator(program, qubits=qubits) + return sim.compute(_EMPTY_PARAMS) + + +def _dm(program, qubits=None, noise_model=None): + """Compute density matrix.""" + sim = DensityMatrixSimulator(program, qubits=qubits, noise_model=noise_model) + return sim.compute(_EMPTY_PARAMS) + + +def _sample(program, qubits=None, noise_model=None, num_trajectories=1000, + batch_size=250, random_seed=0): + """Run trajectory sampling, returning outcomes.""" + sim = TrajectorySimulator(program, qubits=qubits, noise_model=noise_model) + return sim.sample(_EMPTY_PARAMS, num_trajectories=num_trajectories, + batch_size=batch_size, random_seed=random_seed) + + +# ══════════════════════════════════════════════════════════ +# Test: Apply qutrit channels to programs +# ══════════════════════════════════════════════════════════ + + +class TestQutritProgramSimulation: + """Test that qutrit gates in programs produce correct state vectors.""" + + def test_tx_gate_cycles(self): + """TX (cyclic shift) maps |0> -> |2> -> |1> -> |0>.""" + # Apply TX once: |0> -> |2> + p = Program() + p += Gate("TX", [], [0]) + psi = _sv(p, qubits=[0]) + expected = qx.StateVector.from_matrix( + jnp.array([0, 0, 1], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_tx_gate_double(self): + """TX^2 maps |0> -> |1>.""" + p = Program() + p += Gate("TX", [], [0]) + p += Gate("TX", [], [0]) + psi = _sv(p, qubits=[0]) + expected = qx.StateVector.from_matrix( + jnp.array([0, 1, 0], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_tx_gate_triple_identity(self): + """TX^3 = I for qutrits.""" + p = Program() + p += Gate("TX", [], [0]) + p += Gate("TX", [], [0]) + p += Gate("TX", [], [0]) + psi = _sv(p, qubits=[0]) + expected = qx.StateVector.from_matrix( + jnp.array([1, 0, 0], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_th_creates_superposition(self): + """TH (qutrit Hadamard/QFT) creates uniform superposition from |0>.""" + p = Program() + p += Gate("TH", [], [0]) + psi = _sv(p, qubits=[0]) + # QFT on |0> = (|0> + |1> + |2>) / sqrt(3) + expected = qx.StateVector.from_matrix( + jnp.array([1, 1, 1], dtype=complex) / jnp.sqrt(3), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_tz_clock_matrix(self): + """TZ (clock matrix) adds phases: |k> -> omega^k |k>.""" + # TX|0> = |2>, then TZ on |2> should give omega^2 * |2> where omega = exp(2*pi*i/3) + p = Program() + p += Gate("TX", [], [0]) + p += Gate("TZ", [], [0]) + psi = _sv(p, qubits=[0]) + omega = jnp.exp(2j * jnp.pi / 3) + expected = qx.StateVector.from_matrix( + jnp.array([0, 0, omega**2], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_parametric_trx01(self): + """TRX01(pi) acts as a pi rotation in the |0>-|1> subspace.""" + p = Program() + p += Gate("TRX01", [np.pi], [0]) + psi = _sv(p, qubits=[0]) + # RX(pi)|0> = -i|1> in the 0-1 subspace, |2> untouched + expected = qx.StateVector.from_matrix( + jnp.array([0, -1j, 0], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_parametric_trx02(self): + """TRX02(pi) rotates between |0> and |2> subspace.""" + p = Program() + p += Gate("TRX02", [np.pi], [0]) + psi = _sv(p, qubits=[0]) + # RX(pi) in 0-2 subspace: |0> -> -i|2> + expected = qx.StateVector.from_matrix( + jnp.array([0, 0, -1j], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_two_qutrit_tswap(self): + """TSWAP swaps two qutrit registers.""" + # Prepare |2>|0> (TX maps |0>->|2>) then swap -> |0>|2> + p = Program() + p += Gate("TX", [], [0]) + p += Gate("TSWAP", [], [0, 1]) + psi = _sv(p, qubits=[0, 1]) + # After swap: qubit 0 in |0>, qubit 1 in |2> + # State: |0>|2> in (3,3) space -> index 0*3 + 2 = 2 in 9-dim + expected_vec = jnp.zeros(9, dtype=complex).at[2].set(1.0) + expected = qx.StateVector.from_matrix(expected_vec, dims=(3, 3)) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_multi_qutrit_independence(self): + """Two independent qutrit operations on separate registers.""" + p = Program() + p += Gate("TX", [], [0]) # |0> -> |2> + p += Gate("TH", [], [1]) # |0> -> (|0>+|1>+|2>)/sqrt(3) + psi = _sv(p, qubits=[0, 1]) + assert psi.dims == (3, 3) + # Product state: |2> ⊗ (|0>+|1>+|2>)/sqrt(3) + q0 = jnp.array([0, 0, 1], dtype=complex) + q1 = jnp.array([1, 1, 1], dtype=complex) / jnp.sqrt(3) + expected_vec = jnp.kron(q0, q1) + expected = qx.StateVector.from_matrix(expected_vec, dims=(3, 3)) + assert qx.fidelity(psi, expected) > 0.9999 + + +# ══════════════════════════════════════════════════════════ +# Test: Mixed qubit/qutrit systems +# ══════════════════════════════════════════════════════════ + + +class TestMixedQubitQutrit: + """Test that mixed qubit/qutrit registers are handled correctly.""" + + def test_dimension_inference_mixed(self): + """Dimension inference correctly identifies qubit vs qutrit registers.""" + # Qubit gate on register 0, qutrit gate on register 1 + p = Program() + p += X(0) # qubit gate on q0 + p += Gate("TX", [], [1]) # qutrit gate on q1 + sim = PureStateVectorSimulator(p, qubits=[0, 1]) + assert sim.dims == (2, 3) + + def test_mixed_state_vector_dims(self): + """State vector from mixed system has correct dims.""" + p = Program() + p += X(0) # qubit: |0> -> |1> + p += Gate("TX", [], [1]) # qutrit: |0> -> |2> + psi = _sv(p, qubits=[0, 1]) + assert psi.dims == (2, 3) + # State should be |1>⊗|2> in (2,3) space = index 1*3 + 2 = 5 in 6-dim + expected_vec = jnp.zeros(6, dtype=complex).at[5].set(1.0) + expected = qx.StateVector.from_matrix(expected_vec, dims=(2, 3)) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_mixed_density_matrix_dims(self): + """Density matrix simulator also handles mixed qubit/qutrit.""" + p = Program() + p += X(0) # qubit on q0 + p += Gate("TX", [], [1]) # qutrit on q1 + rho = _dm(p, qubits=[0, 1]) + assert rho.dims == (2, 3) + + def test_mixed_three_registers(self): + """Three registers: qubit, qutrit, qubit.""" + p = Program() + p += X(0) # qubit + p += Gate("TX", [], [1]) # qutrit + p += H(2) # qubit + psi = _sv(p, qubits=[0, 1, 2]) + assert psi.dims == (2, 3, 2) + + def test_mixed_qubit_qutrit_entanglement_via_defgate(self): + """Test entanglement with qutrit gates. + + Note: DefGate requires matrix dimensions that are a perfect power + of an integer (2, 3, 4, 8, 9, 16, 25, 27, ...). Mixed qubit/qutrit + custom gates (e.g. 6x6) cannot use DefGate since 6 is not a + perfect power. We test entanglement using built-in qutrit gates. + """ + # DefGate rejects non-perfect-power matrices (6 = 2*3) + mat = np.eye(6, dtype=complex) + with pytest.raises(ValueError, match="perfect power"): + DefGate("BAD_GATE", mat) + + # Test entanglement using built-in gates instead: + # Use TSWAP to entangle two qutrits + p = Program() + p += Gate("TH", [], [0]) # superposition on q0 + p += Gate("TSWAP", [], [0, 1]) # entangle q0 and q1 + p += Gate("TH", [], [0]) # further evolve q0 + psi = _sv(p, qubits=[0, 1]) + assert psi.dims == (3, 3) + # The state should NOT be a product state (entangled) + # Check by verifying reduced purity < 1 + rho = _dm( + Program([ + Gate("TH", [], [0]), + Gate("TSWAP", [], [0, 1]), + Gate("TH", [], [0]), + ]), + qubits=[0, 1], + ) + full_purity = float(jnp.real(jnp.trace(rho.matrix @ rho.matrix))) + assert full_purity > 0.9999 # pure state + + def test_dimension_inference_density_matrix(self): + """Density matrix preprocess_program infers mixed dims correctly.""" + p = Program([ + H(0), # qubit + Gate("TH", [], [1]), # qutrit + ]) + sim = DensityMatrixSimulator(p, qubits=[0, 1]) + assert sim.dims == (2, 3) + + +# ══════════════════════════════════════════════════════════ +# Test: Qutrit measurements +# ══════════════════════════════════════════════════════════ + + +class TestQutritMeasurements: + """Test that qutrit measurements produce correct outcome distributions.""" + + def test_qutrit_measure_ground_state(self): + """Measuring a qutrit in |0> always yields outcome 0.""" + # Use the identity-like approach: TRX01(0) is identity but establishes dim=3 + p_ground = Program() + p_ground += Gate("TRX01", [0.0], [0]) # identity rotation + p_ground += Measurement(qubit=Qubit(0), classical_reg=None) + + outcomes = _sample( + p_ground, qubits=[0], num_trajectories=100, random_seed=42 + ) + # All outcomes should be 0 (ground state) + assert jnp.all(outcomes == 0) + + def test_qutrit_measure_excited_state(self): + """Measuring a qutrit in |2> (via TX) always yields outcome 2.""" + p = Program() + p += Gate("TX", [], [0]) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + outcomes = _sample( + p, qubits=[0], num_trajectories=100, random_seed=42 + ) + assert jnp.all(outcomes == 2) + + def test_qutrit_measure_second_excited(self): + """Measuring a qutrit in |1> (via TX^2) always yields outcome 1.""" + p = Program() + p += Gate("TX", [], [0]) + p += Gate("TX", [], [0]) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + outcomes = _sample( + p, qubits=[0], num_trajectories=100, random_seed=42 + ) + assert jnp.all(outcomes == 1) + + def test_qutrit_measure_superposition_statistics(self): + """TH|0> = (|0>+|1>+|2>)/sqrt(3) gives uniform measurement distribution.""" + p = Program() + p += Gate("TH", [], [0]) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + n_traj = 3000 + outcomes = _sample( + p, qubits=[0], num_trajectories=n_traj, random_seed=123 + ) + # Each outcome should appear ~1/3 of the time + counts = jnp.bincount(outcomes.flatten(), length=3) + freqs = counts / n_traj + np.testing.assert_allclose(freqs, [1 / 3, 1 / 3, 1 / 3], atol=0.05) + + def test_qutrit_noisy_measurement_channel(self): + """Noisy qutrit measurement with confusion matrix.""" + meas_inst = Measurement(qubit=Qubit(0), classical_reg=None) + meas_ch = MeasurementChannel.from_readout_fidelity( + inst=meas_inst, fidelity=0.9, dim=3 + ) + noise_model = NoiseModel(channels=frozenset([meas_ch])) + + # Prepare |2> (TX|0>=|2>) and measure with noise + p = Program() + p += Gate("TX", [], [0]) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + n_traj = 2000 + outcomes = _sample( + p, noise_model=noise_model, qubits=[0], + num_trajectories=n_traj, random_seed=42, + ) + counts = jnp.bincount(outcomes.flatten(), length=3) + # Should be mostly outcome 2 (the prepared state), with some errors + assert counts[2] / n_traj > 0.7 # majority correct + + def test_mixed_qubit_qutrit_measurement(self): + """Measure both a qubit and qutrit in the same program.""" + p = Program() + p += X(0) # qubit |0> -> |1> + p += Gate("TX", [], [1]) # qutrit |0> -> |2> + p += Measurement(qubit=Qubit(0), classical_reg=None) + p += Measurement(qubit=Qubit(1), classical_reg=None) + + outcomes = _sample( + p, qubits=[0, 1], num_trajectories=50, random_seed=0 + ) + # qubit measurement should give 1, qutrit measurement should give 2 + assert outcomes.shape == (50, 2) + assert jnp.all(outcomes[:, 0] == 1) + assert jnp.all(outcomes[:, 1] == 2) + + +# ══════════════════════════════════════════════════════════ +# Test: Dimension inference +# ══════════════════════════════════════════════════════════ + + +class TestDimensionInference: + """Test the mechanism for deciding initial register dimension.""" + + def test_all_qubit_program(self): + """A program with only qubit gates infers dims=(2,2).""" + p = Program(H(0), X(1)) + sim = PureStateVectorSimulator(p, qubits=[0, 1]) + assert sim.dims == (2, 2) + + def test_single_qutrit_program(self): + """A program with a single qutrit gate infers dims=(3,).""" + p = Program(Gate("TX", [], [0])) + sim = PureStateVectorSimulator(p, qubits=[0]) + assert sim.dims == (3,) + + def test_mixed_dims_from_operations(self): + """Operations on different registers infer heterogeneous dims.""" + p = Program( + H(0), # dim=2 on slot 0 + Gate("TX", [], [1]), # dim=3 on slot 1 + X(2), # dim=2 on slot 2 + ) + sim = PureStateVectorSimulator(p, qubits=[0, 1, 2]) + assert sim.dims == (2, 3, 2) + + def test_dimension_upgrade_takes_max(self): + """If a slot sees both dim=2 and dim=3 ops, dim=3 wins.""" + p = Program( + Gate("TX", [], [0]), # dim=3 + ) + sim = PureStateVectorSimulator(p, qubits=[0]) + assert sim.dims == (3,) + + def test_density_matrix_dimension_inference_consistency(self): + """State vector and density matrix simulators infer same dims.""" + p = Program( + X(0), + Gate("TH", [], [1]), + Gate("TX", [], [2]), + H(3), + ) + # Density matrix path + dm_sim = DensityMatrixSimulator(p, qubits=[0, 1, 2, 3]) + dm_dims = dm_sim.dims + + # State vector path + sv_sim = PureStateVectorSimulator(p, qubits=[0, 1, 2, 3]) + sv_dims = sv_sim.dims + + assert dm_dims == sv_dims == (2, 3, 3, 2) + + def test_two_qutrit_gate_infers_both_slots(self): + """A two-qutrit gate (TSWAP) upgrades both slots to dim=3.""" + p = Program(Gate("TSWAP", [], [0, 1])) + sim = PureStateVectorSimulator(p, qubits=[0, 1]) + assert sim.dims == (3, 3) + + def test_custom_defgate_qutrit_dimensions(self): + """DefGate accepts 3x3 unitary matrices for single-qutrit gates.""" + # 3x3 identity is a valid qutrit gate + mat = np.eye(3, dtype=complex) + dg = DefGate("MY_QUTRIT_GATE", mat) + assert dg.num_args() == 1 + + # Built-in qutrit gates also work and infer dim=3 + p = Program(Gate("TX", [], [0])) + psi = _sv(p, qubits=[0]) + assert psi.dims == (3,) + + def test_custom_defgate_two_qutrit(self): + """DefGate accepts 9x9 unitary matrices for two-qutrit gates.""" + mat = np.eye(9, dtype=complex) + dg = DefGate("TWO_QUTRIT_ID", mat) + assert dg.num_args() == 2 + + # Built-in TSWAP also works for two-qutrit systems + p = Program(Gate("TSWAP", [], [0, 1])) + psi = _sv(p, qubits=[0, 1]) + assert psi.matrix.shape[-1] == 9 + assert psi.dims == (3, 3) + + def test_custom_defgate_rejects_non_perfect_power(self): + """DefGate rejects matrices whose dimension is not a perfect power.""" + # 6 = 2*3 is not a perfect power of any integer + mat = np.eye(6, dtype=complex) + with pytest.raises(ValueError, match="perfect power"): + DefGate("BAD_GATE", mat) + + +# ══════════════════════════════════════════════════════════ +# Test: Qutrit noise channels +# ══════════════════════════════════════════════════════════ + + +class TestQutritNoiseChannels: + """Test noisy qutrit simulation via NoiseModel.""" + + def test_qutrit_depolarizing_channel(self): + """A depolarizing channel on a qutrit gate mixes the state.""" + inst = Gate("TX", [], [0]) + channel = Channel.from_gate_fidelity(inst=inst, fidelity=0.8) + noise_model = NoiseModel(channels=frozenset([channel])) + + # Density matrix should show mixed state + p = Program(Gate("TX", [], [0])) + rho = _dm(p, noise_model=noise_model, qubits=[0]) + assert rho.dims == (3,) + # Purity < 1 indicates noise + purity = float(jnp.real(jnp.trace(rho.matrix @ rho.matrix))) + assert purity < 0.99 + + def test_qutrit_depolarizing_trajectory(self): + """Trajectory simulation with qutrit depolarizing noise.""" + inst = Gate("TX", [], [0]) + channel = Channel.from_gate_fidelity(inst=inst, fidelity=0.9) + noise_model = NoiseModel(channels=frozenset([channel])) + + p = Program() + p += Gate("TX", [], [0]) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + n_traj = 2000 + outcomes = _sample( + p, noise_model=noise_model, qubits=[0], + num_trajectories=n_traj, random_seed=7, + ) + counts = jnp.bincount(outcomes.flatten(), length=3) + # Most should be outcome 2 (ideal TX|0>=|2>), with some noise + assert counts[2] / n_traj > 0.7 + + def test_qutrit_reset_channel(self): + """Noisy qutrit reset via ResetChannel.""" + from pyquil.quilbase import ResetQubit + + reset_inst = ResetQubit(Qubit(0)) + reset_ch = ResetChannel.from_reset_fidelity(inst=reset_inst, fidelity=0.9, dim=3) + noise_model = NoiseModel(channels=frozenset([reset_ch])) + + # Prepare |1> (TX^2|0>=|1>), then reset — should mostly go to |0> + p = Program() + p += Gate("TX", [], [0]) + p += Gate("TX", [], [0]) # |0> -> |1> + p += ResetQubit(Qubit(0)) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + n_traj = 1000 + outcomes = _sample( + p, noise_model=noise_model, qubits=[0], + num_trajectories=n_traj, random_seed=55, + ) + counts = jnp.bincount(outcomes.flatten(), length=3) + # Majority should be reset to |0> + assert counts[0] / n_traj > 0.7 + + def test_mixed_noise_qubit_and_qutrit(self): + """Noise model with channels for both qubit and qutrit gates.""" + # Noisy qubit X gate + ch_qubit = Channel.from_gate_fidelity( + inst=Gate("X", [], [0]), fidelity=0.95 + ) + # Noisy qutrit TX gate + ch_qutrit = Channel.from_gate_fidelity( + inst=Gate("TX", [], [1]), fidelity=0.95 + ) + noise_model = NoiseModel(channels=frozenset([ch_qubit, ch_qutrit])) + + p = Program() + p += X(0) + p += Gate("TX", [], [1]) + rho = _dm(p, noise_model=noise_model, qubits=[0, 1]) + assert rho.dims == (2, 3) + # Both registers should have purity < 1 due to noise + purity = float(jnp.real(jnp.trace(rho.matrix @ rho.matrix))) + assert purity < 0.99 + + +# ══════════════════════════════════════════════════════════ +# Test: Qutrit state vector trajectories (batched) +# ══════════════════════════════════════════════════════════ + + +class TestQutritTrajectoryBatching: + """Test that batched trajectory simulation works for qutrits.""" + + def test_noiseless_qutrit_batch(self): + """Batched noiseless qutrit simulation produces identical trajectories.""" + p = Program(Gate("TX", [], [0])) + sim = TrajectorySimulator(p, qubits=[0]) + keys = jax.random.split(jax.random.key(0), 10) + psi, _ = sim.compute(_EMPTY_PARAMS, keys) + # All 10 trajectories should be identical (noiseless) + assert psi.ensemble_size == (10,) + assert psi.dims == (3,) + expected = jnp.array([0, 0, 1], dtype=complex) # TX|0> = |2> + for i in range(10): + fid = float(jnp.abs(jnp.vdot(psi.matrix[i], expected)) ** 2) + assert fid > 0.9999 + + def test_qutrit_trajectory_outcomes_shape(self): + """Trajectory outcomes have correct shape for qutrit programs.""" + p = Program() + p += Gate("TH", [], [0]) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + outcomes = _sample( + p, qubits=[0], num_trajectories=500, batch_size=100, random_seed=0 + ) + assert outcomes.shape == (500, 1) + # Outcomes should be in {0, 1, 2} + assert jnp.all(outcomes >= 0) + assert jnp.all(outcomes <= 2) diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py new file mode 100644 index 000000000..a658100ee --- /dev/null +++ b/test/unit/test_state_vector.py @@ -0,0 +1,1094 @@ +"""Unit tests for the quax-based state vector simulator.""" + +import jax +import jax.numpy as jnp +import numpy as np +import pytest +import quax as qx + +from pyquil.gates import CNOT, CZ, H, MEASURE, RESET, RX, RY, RZ, X +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference, Qubit +from pyquil.quilbase import Declare, DefGate, Gate as QuilGate, Measurement as QuilMeasurement, ResetQubit +from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel +from pyquil.noise._noise_model import NoiseModel +from pyquil.simulation._simulator import ( + PureStateVectorSimulator, + DensityMatrixSimulator, + TrajectorySimulator, + _apply_trajectory_operations as apply_trajectory_operations, + _run_batched_trajectories, +) + +_EMPTY_PARAMS = jnp.array([], dtype=float) + + +def _sv(program, qubits=None, memory_map=None): + """Compute pure state vector for a gate-only program.""" + sim = PureStateVectorSimulator(program, qubits=qubits) + if memory_map: + params = sim.linearize(memory_map) + else: + params = _EMPTY_PARAMS + return sim.compute(params) + + +def _simulate_trajectories(program, noise_model=None, qubits=None, num_trajectories=1, + batch_size=256, random_seed=0): + """Helper: build + compress + run trajectories, returning (psi, outcomes).""" + sim = TrajectorySimulator(program, noise_model=noise_model, qubits=qubits) + resolved = sim.resolve(_EMPTY_PARAMS) + compressed = sim.compress(resolved) + operations = sim.adapt(compressed) + all_psis, all_outcomes = _run_batched_trajectories( + operations, sim.n_qubits, num_trajectories, batch_size, random_seed, + keep_states=True, dims=sim.dims, + ) + assert all_psis is not None + if len(all_psis) == 1: + return all_psis[0], all_outcomes[0] + combined_data = jnp.concatenate([p.matrix for p in all_psis], axis=0) + combined_psi = qx.StateVector.from_matrix(combined_data, all_psis[0].dims) + combined_outcomes = jnp.concatenate(all_outcomes, axis=0) + return combined_psi, combined_outcomes + + +class TestSingleQubitGates: + def test_identity(self): + p = Program() + psi = _sv(p, qubits=[0]) + target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0], dtype=complex), dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + def test_x_gate(self): + p = Program(X(0)) + psi = _sv(p, qubits=[0]) + target = qx.StateVector.from_matrix(jnp.array([0.0, 1.0], dtype=complex), dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + def test_hadamard(self): + p = Program(H(0)) + psi = _sv(p, qubits=[0]) + target = qx.StateVector.from_matrix(jnp.array([1.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + @pytest.mark.parametrize("angle", [0.0, np.pi / 4, np.pi / 2, np.pi, 3 * np.pi / 2]) + def test_rx_gate(self, angle): + p = Program(RX(angle, 0)) + psi = _sv(p, qubits=[0]) + expected = jnp.asarray(qx.gates.RX(angle).matrix) @ jnp.array([1.0, 0.0], dtype=complex) + target = qx.StateVector.from_matrix(expected, dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + @pytest.mark.parametrize("angle", [0.0, np.pi / 4, np.pi / 2, np.pi]) + def test_ry_gate(self, angle): + p = Program(RY(angle, 0)) + psi = _sv(p, qubits=[0]) + expected = jnp.asarray(qx.gates.RY(angle).matrix) @ jnp.array([1.0, 0.0], dtype=complex) + target = qx.StateVector.from_matrix(expected, dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + @pytest.mark.parametrize("angle", [0.0, np.pi / 4, np.pi / 2, np.pi]) + def test_rz_gate(self, angle): + p = Program(RZ(angle, 0)) + psi = _sv(p, qubits=[0]) + expected = jnp.asarray(qx.gates.RZ(angle).matrix) @ jnp.array([1.0, 0.0], dtype=complex) + target = qx.StateVector.from_matrix(expected, dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + +class TestMultiQubitGates: + def test_bell_state(self): + p = Program(H(0), CNOT(0, 1)) + psi = _sv(p, qubits=[0, 1]) + target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0, 0.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2)) + assert qx.fidelity(psi, target) > 0.9999 + + def test_ghz_state_3q(self): + p = Program(H(0), CNOT(0, 1), CNOT(1, 2)) + psi = _sv(p, qubits=[0, 1, 2]) + target = qx.StateVector.from_matrix(jnp.array([1.0, 0, 0, 0, 0, 0, 0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2, 2)) + assert qx.fidelity(psi, target) > 0.9999 + + def test_qubit_ordering(self): + """State vector should respect the provided qubit ordering.""" + p = Program(X(5)) + psi = _sv(p, qubits=[5, 6]) + # qubit 5 is index 0, qubit 6 is index 1 + # X on qubit 5 → |10> → state [0, 0, 1, 0] + target = qx.StateVector.from_matrix(jnp.array([0.0, 0.0, 1.0, 0.0], dtype=complex), dims=(2, 2)) + assert qx.fidelity(psi, target) > 0.9999 + + +class TestParameterizedPrograms: + def test_parameterized_rx(self): + p = Program( + Declare("theta", "REAL"), + RX(MemoryReference("theta"), 0), + ) + angle = np.pi / 3 + psi = _sv(p, qubits=[0], memory_map={"theta": [angle]}) + expected = jnp.asarray(qx.gates.RX(angle).matrix) @ jnp.array([1.0, 0.0], dtype=complex) + target = qx.StateVector.from_matrix(expected, dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + +class TestCustomGates: + def test_defgate(self): + """Test that DefGate-defined gates work correctly.""" + cnot_matrix = np.asarray(qx.gates.CNOT.matrix) + p = Program() + p += DefGate("MY_CNOT", cnot_matrix) + p += QuilGate("MY_CNOT", [], [Qubit(0), Qubit(1)]) + # Prepare |1,0> first + p2 = Program(X(0)) + p + psi = _sv(p2, qubits=[0, 1]) + # X(0) gives |10>, then CNOT gives |11> + target = qx.StateVector.from_matrix(jnp.array([0.0, 0.0, 0.0, 1.0], dtype=complex), dims=(2, 2)) + assert qx.fidelity(psi, target) > 0.9999 + + +class TestAutoQubitDetection: + def test_auto_qubits(self): + """When qubits=None, should auto-detect from program.""" + p = Program(H(2), CNOT(2, 5)) + psi = _sv(p) + # Should use qubits [2, 5] in sorted order + target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0, 0.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2)) + assert qx.fidelity(psi, target) > 0.9999 + + +# ────────────────────────────────────────────────────────────────────────────── +# Trajectory simulator tests +# ────────────────────────────────────────────────────────────────────────────── + + +class TestTrajectoryNoiseless: + """Test that the trajectory simulator preserves noiseless behavior.""" + + def test_single_gate_noiseless(self): + """Without noise, trajectory simulation matches unitary simulation.""" + p = Program(H(0)) + psi_noiseless = _sv(p, qubits=[0]) + psi_traj, outcomes = _simulate_trajectories( + p, noise_model=None, qubits=[0], num_trajectories=1 + ) + assert qx.fidelity(psi_noiseless, psi_traj) > 0.9999 + + def test_bell_state_noiseless(self): + """Multi-qubit noiseless trajectory.""" + p = Program(H(0), CNOT(0, 1)) + psi_noiseless = _sv(p, qubits=[0, 1]) + psi_traj, outcomes = _simulate_trajectories( + p, noise_model=None, qubits=[0, 1], num_trajectories=1 + ) + assert qx.fidelity(psi_noiseless, psi_traj) > 0.9999 + + def test_multiple_trajectories_noiseless_deterministic(self): + """Multiple noiseless trajectories should all give same result.""" + p = Program(X(0)) + psi_batch, outcomes = _simulate_trajectories( + p, noise_model=None, qubits=[0], num_trajectories=8 + ) + # Each trajectory should be |1⟩ + target = qx.StateVector.from_matrix(jnp.array([0.0, 1.0], dtype=complex), dims=(2,)) + probs = qx.probabilities(psi_batch) + # All trajectories: prob of |1⟩ = 1 + assert jnp.allclose(probs[:, 1], 1.0, atol=1e-6) + + +class TestTrajectoryNoisy: + """Test noisy trajectory simulation with known analytical results.""" + + def _make_bitflip_noise_model(self, p_error: float, qubit: int = 0) -> NoiseModel: + """Create a noise model with a bit-flip channel on X gate.""" + # Bit-flip channel applied AFTER the gate: E(rho) = (1-p) U rho U† + p X U rho U† X + # As a combined superop that includes the gate: + inst = X(qubit) + unitary = qx.gates.X + # Build noisy superop: (1-p)|U> NoiseModel: + """Create a noise model with depolarizing noise on X gate.""" + inst = X(qubit) + channel = Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) + return NoiseModel(channels=frozenset([channel])) + + def test_noiseless_gate_with_noise_model(self): + """A noise model that doesn't cover the applied gate should leave it noiseless.""" + # Noise model only covers X gate, but we apply H + noise_model = self._make_bitflip_noise_model(0.1, qubit=0) + p = Program(H(0)) + psi, outcomes = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=1 + ) + target = qx.StateVector.from_matrix( + jnp.array([1.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2,) + ) + assert qx.fidelity(psi, target) > 0.9999 + + def test_bitflip_statistics(self): + """Bit-flip noise should produce correct outcome statistics.""" + p_error = 0.3 + noise_model = self._make_bitflip_noise_model(p_error, qubit=0) + # X gate with bit-flip noise: X|0⟩=|1⟩, then bit-flip with p=0.3 + # So final state: (1-p)|1⟩ + p|0⟩ in trajectory picture + p = Program(X(0)) + num_traj = 2048 + psi_batch, outcomes = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=num_traj, + batch_size=256, random_seed=42, + ) + # Get probabilities for each trajectory + probs = qx.probabilities(psi_batch) # shape (num_traj, 2) + # Each trajectory should collapse to either |0⟩ or |1⟩ + # Count how many ended in |0⟩ (bit-flipped from |1⟩) + in_zero = jnp.sum(probs[:, 0] > 0.5) + observed_flip_rate = float(in_zero) / num_traj + # Expected: p_error fraction should flip to |0⟩ + assert abs(observed_flip_rate - p_error) < 0.05, ( + f"Expected flip rate ~{p_error}, got {observed_flip_rate}" + ) + + def test_depolarizing_statistics(self): + """Depolarizing noise on identity-like circuit should produce mixed results.""" + fidelity_val = 0.9 + noise_model = self._make_depolarizing_noise_model(fidelity_val, qubit=0) + p = Program(X(0)) + num_traj = 2048 + psi_batch, outcomes = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=num_traj, + batch_size=256, random_seed=123, + ) + probs = qx.probabilities(psi_batch) + # Average probability of |1⟩ across trajectories should be close to + # the expected value from depolarizing channel on |1⟩: + # p(|1⟩) = F + (1-F)/d where d=2 for single qubit depol + # Actually for depol with constant p: output prob depends on p + avg_prob_1 = float(jnp.mean(probs[:, 1])) + # For depolarizing channel with fidelity F on a single qubit: + # After X|0⟩=|1⟩, depol: prob(|1⟩) = (2F-1) * 1/2 + 1/2 = F + # (since F_avg = (d*p + 1)/(d+1) and rho_out = p*rho + (1-p)*I/d) + assert abs(avg_prob_1 - fidelity_val) < 0.05 + + def test_two_qubit_noise(self): + """Test that noise applies independently to separate qubits.""" + p_error = 0.2 + inst_q0 = X(0) + inst_q1 = X(1) + ch0 = Channel.from_pauli_noise(inst=inst_q0, pauli_noise={"X": p_error}) + ch1 = Channel.from_pauli_noise(inst=inst_q1, pauli_noise={"X": p_error}) + noise_model = NoiseModel(channels=frozenset([ch0, ch1])) + + prog = Program(X(0), X(1)) + num_traj = 2048 + psi_batch, _ = _simulate_trajectories( + prog, noise_model=noise_model, qubits=[0, 1], num_trajectories=num_traj, + batch_size=256, random_seed=7, + ) + probs = qx.probabilities(psi_batch) # shape (num_traj, 4) + # State |11⟩ = index 3. Both flipped: p_error^2 gives |00⟩ + # Expected: P(|11⟩) ≈ (1-p)^2, P(|00⟩) ≈ p^2 + avg_prob_11 = float(jnp.mean(probs[:, 3])) + expected_prob_11 = (1 - p_error) ** 2 + assert abs(avg_prob_11 - expected_prob_11) < 0.05 + + +class TestTrajectoryMeasurement: + """Test mid-circuit measurement in trajectory simulation.""" + + def test_measurement_records_outcome(self): + """Measurement should record classical outcome.""" + p = Program(H(0), MEASURE(0, None)) + psi, outcomes = _simulate_trajectories( + p, noise_model=None, qubits=[0], num_trajectories=100, + batch_size=100, random_seed=42, + ) + # outcomes shape should be (100, 1) — one measurement + assert outcomes.shape == (100, 1) + # Outcomes should be 0 or 1 + assert jnp.all((outcomes == 0) | (outcomes == 1)) + # Roughly 50/50 from H|0⟩ + frac_0 = float(jnp.mean(outcomes == 0)) + assert 0.3 < frac_0 < 0.7 + + def test_measurement_collapses_state(self): + """After measurement, state should be consistent with outcome.""" + p = Program(H(0), MEASURE(0, None)) + psi, outcomes = _simulate_trajectories( + p, noise_model=None, qubits=[0], num_trajectories=64, + batch_size=64, random_seed=99, + ) + probs = qx.probabilities(psi) # (64, 2) + # For each trajectory, the state should be collapsed + for i in range(64): + outcome = int(outcomes[i, 0]) + assert probs[i, outcome] > 0.999 + + def test_noisy_measurement(self): + """Noisy measurement with confusion should produce errors.""" + # Prepare |0⟩, measure with 80% fidelity + qubit = Qubit(0) + m_inst = QuilMeasurement(qubit=qubit, classical_reg=None) + meas_ch = MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=0.8) + noise_model = NoiseModel(channels=frozenset([meas_ch])) + + p = Program(MEASURE(0, None)) + psi, outcomes = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=1024, + batch_size=256, random_seed=55, + ) + # Prepared in |0⟩, ideal measurement gives 0, but with 20% error → ~20% ones + frac_1 = float(jnp.mean(outcomes == 1)) + assert 0.1 < frac_1 < 0.3 + + +class TestTrajectoryReset: + """Test reset operations in trajectory simulation.""" + + def test_reset_to_ground(self): + """Reset should bring qubit to |0⟩.""" + p = Program(X(0), ResetQubit(Qubit(0))) + psi, _ = _simulate_trajectories( + p, noise_model=None, qubits=[0], num_trajectories=1, + ) + target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0], dtype=complex), dims=(2,)) + assert qx.fidelity(psi, target) > 0.9999 + + def test_global_reset(self): + """Global RESET should reset all qubits.""" + p = Program(X(0), X(1), RESET()) + psi, _ = _simulate_trajectories( + p, noise_model=None, qubits=[0, 1], num_trajectories=1, + ) + target = qx.StateVector.from_matrix( + jnp.array([1.0, 0.0, 0.0, 0.0], dtype=complex), dims=(2, 2) + ) + assert qx.fidelity(psi, target) > 0.9999 + + def test_noisy_reset(self): + """Noisy reset should have imperfect fidelity.""" + qubit = Qubit(0) + reset_inst = ResetQubit(qubit) + reset_ch = ResetChannel.from_reset_fidelity(inst=reset_inst, fidelity=0.9) + noise_model = NoiseModel(channels=frozenset([reset_ch])) + + # Start in |1⟩, apply noisy reset + p = Program(X(0), ResetQubit(Qubit(0))) + num_traj = 2048 + psi, _ = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=num_traj, + batch_size=256, random_seed=13, + ) + probs = qx.probabilities(psi) # (num_traj, 2) + # With 90% reset fidelity, ~90% should end in |0⟩ + avg_prob_0 = float(jnp.mean(probs[:, 0])) + assert avg_prob_0 > 0.85 + + +class TestTrajectoryBatching: + """Test that batch processing works correctly.""" + + def test_batch_size_smaller_than_trajectories(self): + """Multiple batches should produce same statistics as single batch.""" + p = Program(H(0)) + noise_model = None + + # Single batch + psi_1, outcomes_1 = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=64, + batch_size=64, random_seed=42, + ) + # Multiple batches (same seed) + psi_2, outcomes_2 = _simulate_trajectories( + p, noise_model=noise_model, qubits=[0], num_trajectories=64, + batch_size=16, random_seed=42, + ) + # Note: different batching may produce different results due to key splitting, + # but shapes should match + assert psi_1.matrix.shape == psi_2.matrix.shape + assert outcomes_1.shape == outcomes_2.shape + + +class TestComputeProgramStateVectorWithNoise: + """Test the TrajectorySimulator with noise_model parameter.""" + + def test_noise_model_none_unchanged(self): + """With noise_model=None, behavior is identical to original.""" + p = Program(H(0), CNOT(0, 1)) + psi = _sv(p, qubits=[0, 1]) + target = qx.StateVector.from_matrix( + jnp.array([1.0, 0.0, 0.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2) + ) + assert qx.fidelity(psi, target) > 0.9999 + + def test_noise_model_single_trajectory(self): + """With noise_model provided, runs a single trajectory.""" + inst = X(0) + channel = Channel.from_gate_fidelity(inst=inst, fidelity=1.0) + noise_model = NoiseModel(channels=frozenset([channel])) + p = Program(X(0)) + sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0]) + psi, _ = sim.compute(_EMPTY_PARAMS, jax.random.key(0)) + # Perfect fidelity channel → same as noiseless + target = qx.StateVector.from_matrix(jnp.array([0.0, 1.0], dtype=complex), dims=(2,)) + assert qx.fidelity(psi, target) > 0.999 + + +class TestSampleProgramTrajectories: + """Test the scalable TrajectorySimulator.sample function.""" + + def test_returns_outcomes_only(self): + """Should return measurement outcomes without state vectors.""" + p = Program(H(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, noise_model=None, qubits=[0]) + outcomes = sim.sample( + _EMPTY_PARAMS, num_trajectories=100, + batch_size=32, random_seed=42, + ) + assert outcomes.shape == (100, 1) + assert jnp.all((outcomes == 0) | (outcomes == 1)) + + def test_no_measurements_empty_outcomes(self): + """Without measurements, outcomes array should have zero columns.""" + p = Program(H(0)) + sim = TrajectorySimulator(p, noise_model=None, qubits=[0]) + outcomes = sim.sample( + _EMPTY_PARAMS, num_trajectories=10, + ) + assert outcomes.shape == (10, 0) + + def test_bitflip_statistics(self): + """Outcome statistics should match noise model.""" + p_error = 0.3 + inst = X(0) + ch = Channel.from_pauli_noise(inst=inst, pauli_noise={"X": p_error}) + noise_model = NoiseModel(channels=frozenset([ch])) + + p = Program(X(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0]) + outcomes = sim.sample( + _EMPTY_PARAMS, num_trajectories=2048, + batch_size=256, random_seed=42, + ) + # X|0⟩ = |1⟩, then bit-flip with p=0.3 → ~30% get |0⟩ + # Measurement outcome reflects the final state + frac_0 = float(jnp.mean(outcomes == 0)) + assert abs(frac_0 - p_error) < 0.05 + + def test_batch_size_does_not_affect_shape(self): + """Different batch sizes should produce same output shape.""" + p = Program(H(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, qubits=[0]) + outcomes_small = sim.sample( + _EMPTY_PARAMS, num_trajectories=100, batch_size=10, + ) + outcomes_large = sim.sample( + _EMPTY_PARAMS, num_trajectories=100, batch_size=100, + ) + assert outcomes_small.shape == outcomes_large.shape == (100, 1) + + +# ────────────────────────────────────────────────────────────────────────────── +# Linearizer / Compressor architecture tests +# ────────────────────────────────────────────────────────────────────────────── + + +class TestBuildSimulationLinearizer: + """Tests for the simulator linearizer closure.""" + + def test_no_params_returns_empty(self): + p = Program(H(0), CNOT(0, 1), MEASURE(0, MemoryReference("ro", 0))) + p += Declare("ro", "BIT", 1) + sim = TrajectorySimulator(p) + params = sim.linearize({}) + assert params.shape == (0,) + assert sim.n_qubits == 2 + + def test_single_param(self): + p = Program() + p += Declare("theta", "REAL", 1) + p += Declare("ro", "BIT", 1) + p += RZ(MemoryReference("theta", 0), 0) + p += MEASURE(0, MemoryReference("ro", 0)) + sim = TrajectorySimulator(p) + params = sim.linearize({"theta": [1.23]}) + assert params.shape == (1,) + np.testing.assert_allclose(float(params[0]), 1.23) + + def test_multiple_params_ordering(self): + p = Program() + p += Declare("alpha", "REAL", 1) + p += Declare("beta", "REAL", 2) + p += Declare("ro", "BIT", 2) + p += RZ(MemoryReference("alpha", 0), 0) + p += RX(MemoryReference("beta", 0), 0) + p += RY(MemoryReference("beta", 1), 1) + p += MEASURE(0, MemoryReference("ro", 0)) + p += MEASURE(1, MemoryReference("ro", 1)) + sim = TrajectorySimulator(p) + params = sim.linearize({"alpha": [0.1], "beta": [0.2, 0.3]}) + assert params.shape == (3,) + np.testing.assert_allclose(float(params[0]), 0.1) + np.testing.assert_allclose(float(params[1]), 0.2) + np.testing.assert_allclose(float(params[2]), 0.3) + + def test_ro_register_excluded(self): + """Ensure 'ro' register is not treated as a parameter register.""" + p = Program() + p += Declare("theta", "REAL", 1) + p += Declare("ro", "BIT", 1) + p += RZ(MemoryReference("theta", 0), 0) + p += MEASURE(0, MemoryReference("ro", 0)) + sim = TrajectorySimulator(p) + params = sim.linearize({"theta": [np.pi]}) + assert params.shape == (1,) + + +class TestCompressor: + """Tests for the compressor at various max_subsystem_size settings.""" + + # ── max_subsystem_size=0 (no merging) ── + + def test_no_merge_noiseless_matches_direct(self): + """max_subsystem_size=0 compressor output should match direct computation.""" + p = Program(H(0), CNOT(0, 1), RZ(0.5, 0)) + psi_direct = _sv(p) + + sim = TrajectorySimulator(p, max_subsystem_size=0) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + psi = qx.zero_state_vector(sim.n_qubits) + for op, subsystem in ops: + if isinstance(op, qx.Unitary): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + + assert qx.fidelity(psi, psi_direct) > 0.9999 + + def test_no_merge_parameterized_gate(self): + """max_subsystem_size=0 should handle parameterized gates via the param vector.""" + p = Program() + p += Declare("theta", "REAL", 1) + p += Declare("ro", "BIT", 1) + p += RZ(MemoryReference("theta", 0), 0) + p += MEASURE(0, MemoryReference("ro", 0)) + + sim = TrajectorySimulator(p, max_subsystem_size=0) + params = sim.linearize({"theta": [np.pi]}) + ops = sim.adapt(sim.compress(sim.resolve(params))) + + psi = qx.zero_state_vector(sim.n_qubits) + for op, subsystem in ops: + if isinstance(op, qx.Unitary): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + elif isinstance(op, qx.QuantumInstrument): + key = jax.random.key(0) + psi, _ = qx.targeted_apply_instrument_to_state_vector(op, psi, key, subsystem) + + psi_direct = _sv(Program(RZ(np.pi, 0))) + assert qx.fidelity(psi, psi_direct) > 0.9999 + + def test_no_merge_noisy_ops_count(self): + """max_subsystem_size=0 noisy: should have exactly one op per instruction.""" + p = Program(RX(np.pi / 2, 0), CNOT(0, 1), MEASURE(0, MemoryReference("ro", 0))) + p += Declare("ro", "BIT", 1) + + channels = [ + Channel.from_coherence_times(RX(np.pi / 2, 0), gate_duration=0.04, t1s=[30.0], t2s=[20.0]), + ] + noise_model = NoiseModel(channels=frozenset(channels)) + + sim = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + # RX(noisy) + CNOT(noiseless) + MEASURE = 3 ops + assert len(ops) == 3 + + # ── max_subsystem_size=1 (1Q gate merging) ── + + def test_merges_consecutive_1q_gates(self): + """Three consecutive 1Q gates on qubit 0 should merge into one op.""" + p = Program(RZ(0.1, 0), RX(0.2, 0), RZ(0.3, 0)) + + sim = TrajectorySimulator(p, max_subsystem_size=1) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + assert len(ops) == 1 + + psi_direct = _sv(p) + psi = qx.zero_state_vector(sim.n_qubits) + for op, subsystem in ops: + if isinstance(op, qx.Unitary): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + assert qx.fidelity(psi, psi_direct) > 0.9999 + + def test_2q_gate_breaks_run(self): + """A 2Q gate should break the 1Q run.""" + p = Program(RZ(0.1, 0), RX(0.2, 0), CNOT(0, 1), RZ(0.3, 0)) + + sim = TrajectorySimulator(p, max_subsystem_size=1) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + assert len(ops) == 3 + + psi_direct = _sv(p) + psi = qx.zero_state_vector(sim.n_qubits) + for op, subsystem in ops: + if isinstance(op, qx.Unitary): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + assert qx.fidelity(psi, psi_direct) > 0.9999 + + def test_independent_qubit_runs(self): + """1Q gates on different qubits should form separate runs.""" + p = Program( + RZ(0.1, 0), RX(0.2, 0), + RZ(0.3, 1), RX(0.4, 1), + ) + + sim = TrajectorySimulator(p, max_subsystem_size=1) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + assert len(ops) == 2 + + psi_direct = _sv(p) + psi = qx.zero_state_vector(sim.n_qubits) + for op, subsystem in ops: + if isinstance(op, qx.Unitary): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + assert qx.fidelity(psi, psi_direct) > 0.9999 + + def test_parameterized_merge(self): + """Parameterized gates in a 1Q run should merge correctly.""" + p = Program() + p += Declare("theta", "REAL", 2) + p += Declare("ro", "BIT", 1) + p += RZ(MemoryReference("theta", 0), 0) + p += RX(MemoryReference("theta", 1), 0) + p += MEASURE(0, MemoryReference("ro", 0)) + + sim = TrajectorySimulator(p, max_subsystem_size=1) + + theta_vals = [np.pi / 4, np.pi / 2] + params = sim.linearize({"theta": theta_vals}) + ops = sim.adapt(sim.compress(sim.resolve(params))) + + assert len(ops) == 2 + + psi_direct = _sv( + Program(RZ(theta_vals[0], 0), RX(theta_vals[1], 0)) + ) + psi = qx.zero_state_vector(sim.n_qubits) + for op, subsystem in ops: + if isinstance(op, qx.Unitary): + psi = qx.targeted_apply_unitary(op, psi, subsystem) + assert qx.fidelity(psi, psi_direct) > 0.9999 + + def test_noisy_1q_merge(self): + """Noisy 1Q gates should merge via SuperOp composition.""" + p = Program(RX(np.pi / 2, 0), RZ(0.5, 0)) + channels = [ + Channel.from_coherence_times(RX(np.pi / 2, 0), gate_duration=0.04, t1s=[30.0], t2s=[20.0]), + ] + noise_model = NoiseModel(channels=frozenset(channels)) + + sim0 = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) + sim1 = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=1) + + ops0 = sim0.adapt(sim0.compress(sim0.resolve(_EMPTY_PARAMS))) + ops1 = sim1.adapt(sim1.compress(sim1.resolve(_EMPTY_PARAMS))) + + assert len(ops0) == 2 + assert len(ops1) == 1 + assert isinstance(ops1[0][0], qx.KrausMap) + + def test_measurement_breaks_run(self): + """A MEASURE should break 1Q runs.""" + p = Program() + p += Declare("ro", "BIT", 1) + p += RZ(0.1, 0) + p += MEASURE(0, MemoryReference("ro", 0)) + p += RZ(0.2, 0) + + sim = TrajectorySimulator(p, max_subsystem_size=1) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + assert len(ops) == 3 + + def test_typical_circuit_compression_ratio(self): + """A typical layered circuit should have < 1.0 compression ratio.""" + n_q = 4 + p = Program() + for _ in range(3): + for q in range(n_q): + p += RZ(np.random.uniform(-np.pi, np.pi), q) + p += RX(np.pi / 2, q) + p += RZ(np.random.uniform(-np.pi, np.pi), q) + for i in range(0, n_q - 1, 2): + p += CNOT(i, i + 1) + + sim0 = TrajectorySimulator(p, max_subsystem_size=0) + sim1 = TrajectorySimulator(p, max_subsystem_size=1) + n0 = len(sim0.adapt(sim0.compress(sim0.resolve(_EMPTY_PARAMS)))) + n1 = len(sim1.adapt(sim1.compress(sim1.resolve(_EMPTY_PARAMS)))) + assert n1 < n0 + + +class TestBuildSimulationIntegration: + """Integration tests: TrajectorySimulator pipeline flows through to trajectory simulation.""" + + def test_noisy_trajectory_via_simulator(self): + """Full pipeline: TrajectorySimulator resolve + compress + adapt + apply_trajectory_operations.""" + p = Program(H(0), CNOT(0, 1), MEASURE(0, MemoryReference("ro", 0)), MEASURE(1, MemoryReference("ro", 1))) + p += Declare("ro", "BIT", 2) + + channels = [ + Channel.from_coherence_times(CNOT(0, 1), gate_duration=0.1, t1s=[30.0, 30.0], t2s=[20.0, 20.0]), + ] + noise_model = NoiseModel(channels=frozenset(channels)) + + sim = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + + n_traj = 16 + psi = qx.zero_state_vector(sim.n_qubits, ensemble_size=(n_traj,)) + key = jax.random.key(42) + psi_out, outcomes = apply_trajectory_operations(ops, psi, key) + assert outcomes.shape == (n_traj, 2) + assert set(int(v) for v in jnp.unique(outcomes)) <= {0, 1} + + def test_parameterized_trajectory(self): + """Parameterized circuit through TrajectorySimulator → trajectory sim.""" + p = Program() + p += Declare("theta", "REAL", 1) + p += Declare("ro", "BIT", 1) + p += RX(MemoryReference("theta", 0), 0) + p += MEASURE(0, MemoryReference("ro", 0)) + + sim = TrajectorySimulator(p, max_subsystem_size=0) + params = sim.linearize({"theta": [np.pi]}) + ops = sim.adapt(sim.compress(sim.resolve(params))) + + n_traj = 32 + psi = qx.zero_state_vector(sim.n_qubits, ensemble_size=(n_traj,)) + key = jax.random.key(0) + _, outcomes = apply_trajectory_operations(ops, psi, key) + assert jnp.all(outcomes == 1) + +# ────────────────────────────────────────────────────────────────────────────── +# Compressor op-count benchmarks +# ────────────────────────────────────────────────────────────────────────────── + + + +def _op_count(program, max_subsystem_size, noise_model=None): + """Return the number of compressed ops for a program.""" + sim = TrajectorySimulator( + program, noise_model=noise_model, max_subsystem_size=max_subsystem_size, + ) + return len(sim.adapt(sim.compress(sim.resolve(sim.linearize({}))))) + + +class TestCompressorOpCounts: + """Tests that verify the compressor produces the expected number of ops.""" + + def test_single_qubit_sequence_merges_to_one(self): + """RZ-RX-RZ-RX-RZ on one qubit → 1 op at max_size ≥ 1.""" + p = Program(RZ(0.1, 0), RX(0.2, 0), RZ(0.3, 0), RX(0.4, 0), RZ(0.5, 0)) + assert _op_count(p, max_subsystem_size=0) == 5 + assert _op_count(p, max_subsystem_size=1) == 1 + + # Verify correctness + sim = TrajectorySimulator(p, max_subsystem_size=1) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + psi = qx.zero_state_vector(sim.n_qubits) + for op, sub in ops: + psi = qx.targeted_apply_unitary(op, psi, sub) + assert qx.fidelity(psi, _sv(p)) > 0.9999 + + def test_two_qubit_layer_max_size_1(self): + """ZXZXZ on q0, ZXZXZ on q1, CZ 0 1, repeated 2×. + + With max_size=1: 1Q runs merge within each qubit between CZs, but CZ + can't merge into a size-1 group. Structure per repetition: + merged(5×q0) + merged(5×q1) + CZ = 3 ops; ×2 reps = 6 ops. + """ + p = Program() + for _ in range(2): + for q in (0, 1): + p += RZ(0.1, q) + p += RX(0.2, q) + p += RZ(0.3, q) + p += RX(0.4, q) + p += RZ(0.5, q) + p += CZ(0, 1) + + assert _op_count(p, max_subsystem_size=1) == 6 + + def test_two_qubit_layer_max_size_2(self): + """Same circuit as above, but with max_size=2 → everything merges to 1.""" + p = Program() + for _ in range(2): + for q in (0, 1): + p += RZ(0.1, q) + p += RX(0.2, q) + p += RZ(0.3, q) + p += RX(0.4, q) + p += RZ(0.5, q) + p += CZ(0, 1) + + assert _op_count(p, max_subsystem_size=2) == 1 + + # Verify correctness + sim = TrajectorySimulator(p, max_subsystem_size=2) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + psi = qx.zero_state_vector(sim.n_qubits) + for op, sub in ops: + psi = qx.targeted_apply_unitary(op, psi, sub) + assert qx.fidelity(psi, _sv(p)) > 0.9999 + + def test_cnot_pair_merge(self): + """CNOT 0 1, CNOT 1 0 should merge into 1 op at max_size ≥ 2.""" + p = Program(CNOT(0, 1), CNOT(1, 0)) + + assert _op_count(p, max_subsystem_size=0) == 2 + assert _op_count(p, max_subsystem_size=1) == 2 # both are 2Q, can't fit in size 1 + assert _op_count(p, max_subsystem_size=2) == 1 + + # Verify correctness + sim = TrajectorySimulator(p, max_subsystem_size=2) + ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) + assert len(ops) == 1 + assert ops[0][1] == (0, 1) + psi = qx.zero_state_vector(sim.n_qubits) + for op, sub in ops: + psi = qx.targeted_apply_unitary(op, psi, sub) + assert qx.fidelity(psi, _sv(p)) > 0.9999 + + @pytest.mark.parametrize("num_qubits", [4, 8, 12]) + @pytest.mark.parametrize("max_subsystem_size", [0, 1, 2, 3]) + def test_random_circuit_compression(self, num_qubits, max_subsystem_size): + """Random layered circuits should compress monotonically with max_size.""" + rng = np.random.default_rng(42) + n_layers = 5 + + p = Program() + for _ in range(n_layers): + # 1Q layer + for q in range(num_qubits): + gate = rng.choice([RZ, RX, RY]) + p += gate(rng.uniform(-np.pi, np.pi), q) + # 2Q layer (linear chain, even edges) + for i in range(0, num_qubits - 1, 2): + p += CNOT(i, i + 1) + # 1Q layer + for q in range(num_qubits): + gate = rng.choice([RZ, RX, RY]) + p += gate(rng.uniform(-np.pi, np.pi), q) + # 2Q layer (odd edges) + for i in range(1, num_qubits - 1, 2): + p += CNOT(i, i + 1) + + n_ops = _op_count(p, max_subsystem_size) + n_uncompressed = _op_count(p, 0) + + # Compression should never increase op count + assert n_ops <= n_uncompressed, ( + f"max_size={max_subsystem_size}: {n_ops} ops > {n_uncompressed} uncompressed" + ) + + # With max_size > 0, we expect at least some compression for this circuit + if max_subsystem_size > 0: + assert n_ops < n_uncompressed + + def test_random_circuit_compression_summary(self, capsys): + """Print a summary table of compression ratios for various configs.""" + rng = np.random.default_rng(42) + + configs = [ + (4, 5), (8, 5), (12, 5), (16, 3), + ] + max_sizes = [0, 1, 2, 3, 4] + + rows = [] + for num_qubits, n_layers in configs: + p = Program() + for _ in range(n_layers): + for q in range(num_qubits): + p += RZ(rng.uniform(-np.pi, np.pi), q) + p += RX(np.pi / 2, q) + for i in range(0, num_qubits - 1, 2): + p += CNOT(i, i + 1) + for q in range(num_qubits): + p += RZ(rng.uniform(-np.pi, np.pi), q) + for i in range(1, num_qubits - 1, 2): + p += CNOT(i, i + 1) + + counts = {s: _op_count(p, s) for s in max_sizes} + rows.append((num_qubits, n_layers, counts)) + + # Print table + header = f"{'qubits':>6} {'layers':>6}" + "".join(f" {'s=' + str(s):>8}" for s in max_sizes) + print(f"\n{'Compression op counts':=^{len(header)}}") + print(header) + print("-" * len(header)) + for nq, nl, counts in rows: + line = f"{nq:>6} {nl:>6}" + for s in max_sizes: + ratio = counts[s] / counts[0] if counts[0] > 0 else 0 + line += f" {counts[s]:>4} ({ratio:.2f})" + # line += f" {counts[s]:>8}" + print(line) + +# ────────────────────────────────────────────────────────────────────────────── +# State Vector simulation benchmarks +# ────────────────────────────────────────────────────────────────────────────── + +_DEFAULT_NUM_QUBITS = 15 +_DEFAULT_NUM_LAYERS = 10 +_DEFAULT_NUM_TRAJECTORIES = 128 +_DEFAULT_BATCH_SIZE = 32 +_DEFAULT_MAX_SUBSYSTEM_SIZE = 1 + + +def _build_noisy_program_and_model(num_qubits, num_layers, seed=4867): + """Build a layered noisy circuit and matching noise model. + + Circuit structure per layer (×2 for even/odd edge sets): + RZ-RX-RZ-RX-RZ on every qubit, then CNOTs on edges. + Total: 5*num_layers*num_qubits 1Q gates + (num_qubits-1)*num_layers 2Q gates. + """ + edges_0 = [(i, i + 1) for i in range(0, num_qubits - 1, 2)] + edges_1 = [(i, i + 1) for i in range(1, num_qubits - 1, 2)] + rng = np.random.default_rng(seed) + + t1s, t2s = {}, {} + for q in range(num_qubits): + t1 = np.clip(rng.normal(30, 10), 10, 50) + t2 = np.clip(rng.normal(30, 20), 5, 2 * t1) + t1s[q], t2s[q] = t1, t2 + + channels = [ + Channel.from_coherence_times( + CNOT(*edge), gate_duration=0.1, t1s=[t1s[q] for q in edge], t2s=[t2s[q] for q in edge] + ) + for edge in edges_0 + edges_1 + ] + [ + Channel.from_coherence_times(RX(np.pi / 2, q), gate_duration=0.04, t1s=[t1s[q]], t2s=[t2s[q]]) + for q in range(num_qubits) + ] + noise_model = NoiseModel(channels=frozenset(channels)) + + program = Program() + for _ in range(num_layers): + for edges in [edges_0, edges_1]: + program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] + program += [RX(np.pi / 2, idx) for idx in range(num_qubits)] + program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] + program += [RX(np.pi / 2, idx) for idx in range(num_qubits)] + program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] + program += [CNOT(*edge) for edge in edges] + + return program, noise_model + + +def _run_perf_benchmark( + benchmark, + num_qubits=_DEFAULT_NUM_QUBITS, + num_layers=_DEFAULT_NUM_LAYERS, + num_trajectories=_DEFAULT_NUM_TRAJECTORIES, + batch_size=_DEFAULT_BATCH_SIZE, + max_subsystem_size=_DEFAULT_MAX_SUBSYSTEM_SIZE, +): + """Shared benchmark harness: build, warmup, then benchmark the JAX kernel.""" + program, noise_model = _build_noisy_program_and_model(num_qubits, num_layers) + + sim = TrajectorySimulator( + program, noise_model=noise_model, max_subsystem_size=max_subsystem_size, + ) + params = sim.linearize({}) + operations = sim.adapt(sim.compress(sim.resolve(params))) + + # Warmup: trigger JIT compilation + warmup_psi = qx.zero_state_vector(sim.n_qubits, ensemble_size=(batch_size,)) + key = jax.random.key(0) + apply_trajectory_operations(operations, warmup_psi, key)[0].matrix.block_until_ready() + + def thunk(): + key = jax.random.key(0) + remaining = num_trajectories + while remaining > 0: + this_batch = min(remaining, batch_size) + key, batch_key = jax.random.split(key) + psi = qx.zero_state_vector(sim.n_qubits, ensemble_size=(this_batch,)) + result = apply_trajectory_operations(operations, psi, batch_key) + result[0].matrix.block_until_ready() + remaining -= this_batch + + benchmark.pedantic(thunk, iterations=1, rounds=3) + + +class TestPerformance: + """Trajectory simulator performance benchmarks. + + Defaults: 15 qubits, depth 10, 128 trajectories, batch_size 32, + max_subsystem_size 1. Each test varies one axis while holding the + others constant. + """ + + # ── Vary num_qubits ────────────────────────────────── + @pytest.mark.parametrize("num_qubits", [ + pytest.param(3, id="3q"), + pytest.param(6, id="6q"), + pytest.param(9, id="9q"), + pytest.param(12, id="12q"), + pytest.param(15, id="15q"), + ]) + def test_scaling_qubits(self, benchmark, num_qubits): + _run_perf_benchmark(benchmark, num_qubits=num_qubits) + + # ── Vary depth (num_layers) ────────────────────────── + @pytest.mark.parametrize("num_layers", [ + pytest.param(1, id="1L"), + pytest.param(3, id="3L"), + pytest.param(10, id="10L"), + pytest.param(20, id="20L"), + ]) + def test_scaling_depth(self, benchmark, num_layers): + _run_perf_benchmark(benchmark, num_layers=num_layers) + + # ── Vary batch_size ────────────────────────────────── + @pytest.mark.parametrize("batch_size", [ + pytest.param(8, id="b8"), + pytest.param(16, id="b16"), + pytest.param(32, id="b32"), + pytest.param(64, id="b64"), + pytest.param(128, id="b128"), + ]) + def test_scaling_batch_size(self, benchmark, batch_size): + _run_perf_benchmark(benchmark, batch_size=batch_size) + + # ── Vary max_subsystem_size ────────────────────────── + @pytest.mark.parametrize("max_subsystem_size", [ + pytest.param(0, id="s0"), + pytest.param(1, id="s1"), + ]) + def test_scaling_subsystem_size(self, benchmark, max_subsystem_size): + _run_perf_benchmark(benchmark, max_subsystem_size=max_subsystem_size) + + # ── 17-qubit batch_size sweep ──────────────────────── + @pytest.mark.parametrize("batch_size", [ + pytest.param(8, id="b8"), + pytest.param(16, id="b16"), + pytest.param(32, id="b32"), + pytest.param(64, id="b64"), + pytest.param(128, id="b128"), + ]) + def test_17q_batch_size(self, benchmark, batch_size): + _run_perf_benchmark(benchmark, num_qubits=17, batch_size=batch_size) + + From 0e0daded1abb4ff88b4551853231e25f74e52542 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Fri, 22 May 2026 17:07:04 +0000 Subject: [PATCH 02/37] Target unitary required --- pyquil/noise/_channels.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index b7f230bb2..000eec6a3 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -181,8 +181,9 @@ class Channel: The ``process`` field is a ``qx.SuperOp`` which can be converted to alternative representations (Choi, Kraus, Pauli-Liouville) via ``quax``. - Fidelity metrics are computed relative to the ideal gate unitary, which is resolved - automatically for standard gates or provided explicitly via ``target_unitary``. + Fidelity metrics are computed relative to the ideal gate unitary stored in + ``target_unitary``. For standard gates use the class methods (e.g. + :meth:`from_gate_fidelity`) which resolve the unitary automatically. """ inst: Gate @@ -191,19 +192,13 @@ class Channel: process: qx.SuperOp """The noisy process (superoperator) for the gate, including the gate unitary.""" - target_unitary: Optional[qx.Unitary] = None - """ - The noiseless unitary of the gate. If ``None``, will be resolved automatically from - ``inst`` for standard gates. Required for fidelity calculations with custom gates. - """ + target_unitary: qx.Unitary + """The noiseless unitary of the gate.""" @cached_property def unitary(self) -> qx.Unitary: - """The noiseless unitary of the gate, resolved from ``inst`` or provided explicitly.""" - if self.target_unitary is not None: - return self.target_unitary - resolved = get_instruction_unitary(self.inst) - return resolved + """The noiseless unitary of the gate.""" + return self.target_unitary @cached_property def qubits(self) -> List[int]: @@ -699,10 +694,9 @@ def to_json(self) -> str: "superop": {"_complex_array": flat_data, "shape": list(superop_array.shape)}, } - if self.target_unitary is not None: - u_array = np.asarray(self.target_unitary.matrix) - u_flat = [[float(val.real), float(val.imag)] for val in u_array.flat] - data["target_unitary"] = {"_complex_array": u_flat, "shape": list(u_array.shape)} + u_array = np.asarray(self.target_unitary.matrix) + u_flat = [[float(val.real), float(val.imag)] for val in u_array.flat] + data["target_unitary"] = {"_complex_array": u_flat, "shape": list(u_array.shape)} return json.dumps(data) @@ -728,7 +722,6 @@ def from_json(cls: Type["Channel"], json_str: str) -> "Channel": dims = ((2,) * num_qubits, (2,) * num_qubits) superop = qx.SuperOp.from_matrix(superop_array, dims) - target_unitary = None if "target_unitary" in data: u_data = data["target_unitary"] u_flat = u_data["_complex_array"] @@ -737,6 +730,8 @@ def from_json(cls: Type["Channel"], json_str: str) -> "Channel": u_num_qubits = int(jnp.round(jnp.log2(u_shape[0]))) u_dims = ((2,) * u_num_qubits, (2,) * u_num_qubits) target_unitary = qx.Unitary.from_matrix(u_array, u_dims) + else: + target_unitary = get_instruction_unitary(inst) return cls(inst=inst, process=superop, target_unitary=target_unitary) From e11791ea763970f9da4ea31fd432de481b813ed7 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sat, 23 May 2026 06:46:39 +0000 Subject: [PATCH 03/37] Comment --- pyquil/simulation/_resolver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 10ffbd61c..5855d17b9 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -289,6 +289,9 @@ def resolver_from_program( # Pre-scan gate instructions to infer per-qudit dimensions. # This is needed so that MEASURE and RESET use the correct dim. + # Quax doesn't distinguish between ideal and noisy MEASUREs and RESETs by type. + # While an ideal MEASURE should by promoted, a noisy one should be embedded + # We don't know which promotion behaviour to use until we check the noise model. qudit_dims: Dict[int, int] = {} # qubit_index → dimension for node_key in node_order: inst = dag.nodes[node_key]["inst"] From cc7e267b20623c0dbaeb1623f4ccaa26cae378ac Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 12:37:33 +0000 Subject: [PATCH 04/37] Add surface 17 test --- .../data/surface_17_depth_5_no_reset.quil | 289 ++++++++++++++++++ test/unit/test_state_vector.py | 172 ++++++++++- 2 files changed, 451 insertions(+), 10 deletions(-) create mode 100644 test/unit/data/surface_17_depth_5_no_reset.quil diff --git a/test/unit/data/surface_17_depth_5_no_reset.quil b/test/unit/data/surface_17_depth_5_no_reset.quil new file mode 100644 index 000000000..064cff716 --- /dev/null +++ b/test/unit/data/surface_17_depth_5_no_reset.quil @@ -0,0 +1,289 @@ +DEFCIRCUIT SZ_INIT q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + RZ(pi/2) q103 + RZ(pi/2) q93 + RZ(pi/2) q85 + RZ(pi/2) q77 + RZ(pi/2) q91 + RZ(pi/2) q83 + RZ(pi/2) q75 + RZ(pi/2) q65 + RZ(pi/2) q82 + RZ(pi/2) q102 + RZ(pi/2) q66 + RZ(pi/2) q86 + RZ(pi/2) q84 + I q92 + I q74 + I q94 + I q76 + +DEFCIRCUIT SX_INIT q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + RX(pi/2) q103 + RX(pi/2) q93 + RX(pi/2) q85 + RX(pi/2) q77 + RX(pi/2) q91 + RX(pi/2) q83 + RX(pi/2) q75 + RX(pi/2) q65 + RX(pi/2) q82 + RX(pi/2) q102 + RX(pi/2) q66 + RX(pi/2) q86 + RX(pi/2) q84 + I q92 + I q74 + I q94 + I q76 + +DEFCIRCUIT CZ_0 q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + CZ q83 q84 + CZ q85 q86 + CZ q65 q66 + CZ q94 q93 + CZ q76 q75 + CZ q92 q91 + I q77 + I q74 + I q82 + I q102 + I q103 + +DEFCIRCUIT SZ_DATA q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + RZ(pi/2) q102 + RZ(pi/2) q94 + RZ(pi/2) q86 + RZ(pi/2) q92 + RZ(pi/2) q84 + RZ(pi/2) q76 + RZ(pi/2) q82 + RZ(pi/2) q74 + RZ(pi/2) q66 + I q77 + I q93 + I q75 + I q103 + I q65 + I q83 + I q85 + I q91 + +DEFCIRCUIT SX_DATA q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + RX(pi/2) q102 + RX(pi/2) q94 + RX(pi/2) q86 + RX(pi/2) q92 + RX(pi/2) q84 + RX(pi/2) q76 + RX(pi/2) q82 + RX(pi/2) q74 + RX(pi/2) q66 + I q77 + I q93 + I q75 + I q103 + I q65 + I q83 + I q85 + I q91 + +DEFCIRCUIT CZ_1 q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + CZ q83 q92 + CZ q85 q94 + CZ q65 q74 + CZ q84 q93 + CZ q66 q75 + CZ q82 q91 + I q77 + I q102 + I q86 + I q103 + I q76 + +DEFCIRCUIT CZ_2 q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + CZ q83 q74 + CZ q85 q76 + CZ q103 q94 + CZ q102 q93 + CZ q84 q75 + CZ q86 q77 + I q82 + I q66 + I q65 + I q92 + I q91 + +DEFCIRCUIT CZ_3 q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + CZ q83 q82 + CZ q85 q84 + CZ q103 q102 + CZ q92 q93 + CZ q74 q75 + CZ q76 q77 + I q66 + I q94 + I q86 + I q65 + I q91 + +DEFCIRCUIT SZ_ANCILLA q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + RZ(pi/2) q103 + RZ(pi/2) q93 + RZ(pi/2) q85 + RZ(pi/2) q77 + RZ(pi/2) q91 + RZ(pi/2) q83 + RZ(pi/2) q75 + RZ(pi/2) q65 + I q74 + I q82 + I q102 + I q66 + I q94 + I q86 + I q84 + I q92 + I q76 + +DEFCIRCUIT SX_ANCILLA_ECHO q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + RX(pi/2) q103 + RX(pi/2) q93 + RX(pi/2) q85 + RX(pi/2) q77 + RX(pi/2) q91 + RX(pi/2) q83 + RX(pi/2) q75 + RX(pi/2) q65 + RX(pi) q102 + RX(pi) q94 + RX(pi) q86 + RX(pi) q92 + RX(pi) q84 + RX(pi) q76 + RX(pi) q82 + RX(pi) q74 + RX(pi) q66 + +DEFCIRCUIT MEASURE_ANCILLA q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + MEASURE q103 + MEASURE q93 + MEASURE q85 + MEASURE q77 + MEASURE q91 + MEASURE q83 + MEASURE q75 + MEASURE q65 + I q74 + I q82 + I q102 + I q66 + I q94 + I q86 + I q84 + I q92 + I q76 + +DEFCIRCUIT MEASURE_ALL q65 q66 q74 q75 q76 q77 q82 q83 q84 q85 q86 q91 q92 q93 q94 q102 q103: + MEASURE q103 + MEASURE q102 + MEASURE q94 + MEASURE q86 + MEASURE q93 + MEASURE q85 + MEASURE q77 + MEASURE q92 + MEASURE q84 + MEASURE q76 + MEASURE q91 + MEASURE q83 + MEASURE q75 + MEASURE q82 + MEASURE q74 + MEASURE q66 + MEASURE q65 + +SZ_INIT 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_INIT 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_INIT 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_0 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_1 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_2 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_3 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +MEASURE_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_0 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_1 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_2 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_3 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +MEASURE_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_0 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_1 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_2 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_3 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +MEASURE_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_0 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_1 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_2 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_3 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +MEASURE_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_ANCILLA_ECHO 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_ANCILLA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_0 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_1 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_2 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_DATA 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +CZ_3 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_INIT 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SX_INIT 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +SZ_INIT 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 +MEASURE_ALL 65 66 74 75 76 77 82 83 84 85 86 91 92 93 94 102 103 diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index a658100ee..1e03e0795 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -1,26 +1,61 @@ """Unit tests for the quax-based state vector simulator.""" +from pathlib import Path + import jax import jax.numpy as jnp import numpy as np import pytest import quax as qx -from pyquil.gates import CNOT, CZ, H, MEASURE, RESET, RX, RY, RZ, X +from pyquil.gates import CNOT, CZ, MEASURE, RESET, RX, RY, RZ, H, X +from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel +from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program from pyquil.quilatom import MemoryReference, Qubit -from pyquil.quilbase import Declare, DefGate, Gate as QuilGate, Measurement as QuilMeasurement, ResetQubit -from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel -from pyquil.noise._noise_model import NoiseModel +from pyquil.quilbase import ( + Declare, + DefCircuit, + DefGate, + ResetQubit, +) +from pyquil.quilbase import ( + Gate as QuilGate, +) +from pyquil.quilbase import ( + Measurement as QuilMeasurement, +) +from pyquil.quilbase import ( + Reset as QuilReset, +) from pyquil.simulation._simulator import ( - PureStateVectorSimulator, DensityMatrixSimulator, + PureStateVectorSimulator, TrajectorySimulator, - _apply_trajectory_operations as apply_trajectory_operations, _run_batched_trajectories, ) +from pyquil.simulation._simulator import ( + _apply_trajectory_operations as apply_trajectory_operations, +) _EMPTY_PARAMS = jnp.array([], dtype=float) +_DATA_DIR = Path(__file__).parent / "data" +_SURFACE17_FIXTURE = _DATA_DIR / "surface_17_depth_5_no_reset.quil" +_SURFACE17_QUBITS = (65, 66, 74, 75, 76, 77, 82, 83, 84, 85, 86, 91, 92, 93, 94, 102, 103) +_SURFACE17_CYCLES = { + "SZ_INIT", + "SX_INIT", + "CZ_0", + "SZ_DATA", + "SX_DATA", + "CZ_1", + "CZ_2", + "CZ_3", + "SZ_ANCILLA", + "SX_ANCILLA_ECHO", + "MEASURE_ANCILLA", + "MEASURE_ALL", +} def _sv(program, qubits=None, memory_map=None): @@ -53,6 +88,94 @@ def _simulate_trajectories(program, noise_model=None, qubits=None, num_trajector return combined_psi, combined_outcomes +def _load_surface17_depth5_program(): + """Load the checked-in surface-17 depth-5 Quil fixture.""" + return Program(_SURFACE17_FIXTURE.read_text()) + + +def _surface17_defcircuits(program): + return {inst.name: inst for inst in program.instructions if isinstance(inst, DefCircuit)} + + +def _concretize_cycle_gate(inst, qubit_map): + return QuilGate( + inst.name, + list(inst.params), + [qubit_map[qubit] for qubit in inst.qubits], + ) + + +def _concretize_cycle_measurement(inst, qubit_map): + return QuilMeasurement(qubit=qubit_map[inst.qubit], classical_reg=None) + + +def _build_surface17_cycle_noise_model( + program, + depolarizing_constant=0.99, + readout_fidelity=1.0, +): + """Build a cycle noise model that matches the surface-17 DEFCIRCUIT invocations.""" + defcircuits = _surface17_defcircuits(program) + cycle_channels = [] + + for inst in program.instructions: + if not isinstance(inst, QuilGate) or inst.name not in defcircuits: + continue + + defcircuit = defcircuits[inst.name] + qubit_map = dict(zip(defcircuit.qubit_variables, inst.qubits)) + channels = [] + + for cycle_inst in defcircuit.instructions: + if isinstance(cycle_inst, QuilGate): + concrete_gate = _concretize_cycle_gate(cycle_inst, qubit_map) + channels.append(Channel.from_depolarizing_constant(concrete_gate, depolarizing_constant)) + elif isinstance(cycle_inst, QuilMeasurement): + concrete_measurement = _concretize_cycle_measurement(cycle_inst, qubit_map) + channels.append( + MeasurementChannel.from_readout_fidelity(concrete_measurement, fidelity=readout_fidelity) + ) + + cycle_channels.append(CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels))) + + return NoiseModel(channels=cycle_channels) + + +def _run_surface17_cycle_benchmark( + benchmark, + num_trajectories=128, + batch_size=16, + depolarizing_constant=0.99, + readout_fidelity=1.0, +): + program = _load_surface17_depth5_program() + noise_model = _build_surface17_cycle_noise_model( + program, + depolarizing_constant=depolarizing_constant, + readout_fidelity=readout_fidelity, + ) + sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=0) + params = sim.linearize({}) + operations = sim.adapt(sim.compress(sim.resolve(params))) + + warmup_psi = qx.zero_state_vector(dims=sim.dims, ensemble_size=(batch_size,)) + key = jax.random.key(0) + apply_trajectory_operations(operations, warmup_psi, key)[0].matrix.block_until_ready() + + def thunk(): + key = jax.random.key(0) + remaining = num_trajectories + while remaining > 0: + this_batch = min(remaining, batch_size) + key, batch_key = jax.random.split(key) + psi = qx.zero_state_vector(dims=sim.dims, ensemble_size=(this_batch,)) + result = apply_trajectory_operations(operations, psi, batch_key) + result[0].matrix.block_until_ready() + remaining -= this_batch + + benchmark.pedantic(thunk, iterations=1, rounds=1) + + class TestSingleQubitGates: def test_identity(self): p = Program() @@ -944,6 +1067,32 @@ def test_random_circuit_compression_summary(self, capsys): # line += f" {counts[s]:>8}" print(line) + +class TestSurface17Fixture: + """Tests for the checked-in surface-17 trajectory benchmark fixture.""" + + def test_surface17_fixture_structure(self): + program = _load_surface17_depth5_program() + defcircuit_names = set(_surface17_defcircuits(program)) + invocations = [inst for inst in program.instructions if isinstance(inst, QuilGate)] + invocation_names = [inst.name for inst in invocations] + + assert _SURFACE17_FIXTURE.exists() + assert defcircuit_names == _SURFACE17_CYCLES + assert set(program.get_qubit_indices()) == set(_SURFACE17_QUBITS) + assert not any(isinstance(inst, (QuilReset, ResetQubit)) for inst in program.instructions) + assert invocation_names.count("MEASURE_ANCILLA") == 4 + assert invocation_names[-1] == "MEASURE_ALL" + + def test_surface17_cycle_noise_model_preserves_measurements(self): + program = _load_surface17_depth5_program() + noise_model = _build_surface17_cycle_noise_model(program, depolarizing_constant=1.0) + sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=0) + resolved = sim.resolve(_EMPTY_PARAMS) + + n_measurements = sum(1 for op, _ in resolved if isinstance(op, qx.QuantumInstrument)) + assert n_measurements == 49 + # ────────────────────────────────────────────────────────────────────────────── # State Vector simulation benchmarks # ────────────────────────────────────────────────────────────────────────────── @@ -1065,9 +1214,9 @@ def test_scaling_depth(self, benchmark, num_layers): @pytest.mark.parametrize("batch_size", [ pytest.param(8, id="b8"), pytest.param(16, id="b16"), - pytest.param(32, id="b32"), + # pytest.param(32, id="b32"), pytest.param(64, id="b64"), - pytest.param(128, id="b128"), + # pytest.param(128, id="b128"), ]) def test_scaling_batch_size(self, benchmark, batch_size): _run_perf_benchmark(benchmark, batch_size=batch_size) @@ -1084,11 +1233,14 @@ def test_scaling_subsystem_size(self, benchmark, max_subsystem_size): @pytest.mark.parametrize("batch_size", [ pytest.param(8, id="b8"), pytest.param(16, id="b16"), - pytest.param(32, id="b32"), + # pytest.param(32, id="b32"), pytest.param(64, id="b64"), - pytest.param(128, id="b128"), + # pytest.param(128, id="b128"), ]) def test_17q_batch_size(self, benchmark, batch_size): _run_perf_benchmark(benchmark, num_qubits=17, batch_size=batch_size) + def test_surface17_depth5_cycle_noise(self, benchmark): + _run_surface17_cycle_benchmark(benchmark) + From 7df59eb2f1f3692d3142e3ea901a6b1362e08f59 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 14:47:24 +0000 Subject: [PATCH 05/37] Improve simulator performance --- pyquil/noise.py | 830 ------------------ pyquil/noise/_channels.py | 136 +-- pyquil/noise/_noise_model.py | 62 +- pyquil/simulation/_resolver.py | 233 ++--- pyquil/simulation/_simulator.py | 99 +-- pyquil/transform.py | 120 ++- .../surface_17_depth_5_no_reset.quil | 0 test/benchmarks/test_state_vector.py | 420 +++++++++ test/unit/test_noise_model.py | 16 +- test/unit/test_qutrit_simulation.py | 10 +- test/unit/test_state_vector.py | 534 ++++------- 11 files changed, 929 insertions(+), 1531 deletions(-) delete mode 100644 pyquil/noise.py rename test/{unit/data => benchmarks/fixtures}/surface_17_depth_5_no_reset.quil (100%) create mode 100644 test/benchmarks/test_state_vector.py diff --git a/pyquil/noise.py b/pyquil/noise.py deleted file mode 100644 index 63f62bc91..000000000 --- a/pyquil/noise.py +++ /dev/null @@ -1,830 +0,0 @@ -############################################################################## -# Copyright 2018 Rigetti Computing -# -# 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. -############################################################################## -"""Module for creating and verifying noisy gate and readout definitions.""" - -import sys -from collections import namedtuple -from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING, Any, Optional, Union, cast - -import numpy as np - -from pyquil.external.rpcq import CompilerISA -from pyquil.gates import MEASURE, RX, I -from pyquil.noise_gates import _get_qvm_noise_supported_gates -from pyquil.quilatom import MemoryReference, ParameterDesignator, format_parameter -from pyquil.quilbase import Declare, Gate, Pragma - -if TYPE_CHECKING: - from pyquil.api import QuantumComputer as PyquilApiQuantumComputer - from pyquil.quil import Program - -INFINITY = float("inf") -"Used for infinite coherence times." - -_KrausModel = namedtuple("_KrausModel", ["gate", "params", "targets", "kraus_ops", "fidelity"]) - - -class KrausModel(_KrausModel): - """Encapsulate a single gate's noise model. - - .. deprecated:: - Use :class:`pyquil.noise_model.Channel` for quax-based noise modeling. - - :ivar str gate: The name of the gate. - :ivar Sequence[float] params: Optional parameters for the gate. - :ivar Sequence[int] targets: The target qubit ids. - :ivar Sequence[np.array] kraus_ops: The Kraus operators (must be square complex numpy arrays). - :ivar float fidelity: The average gate fidelity associated with the Kraus map relative to the - ideal operation. - """ - - @staticmethod - def unpack_kraus_matrix(m: Union[list[Any], np.ndarray]) -> np.ndarray: - """Unpack a JSON compatible representation of a complex Kraus matrix. - - :param m: The representation of a Kraus operator. Either a complex - square matrix (as numpy array or nested lists) or a JSON-able pair of real matrices - (as nested lists) representing the element-wise real and imaginary part of m. - :return: A complex square numpy array representing the Kraus operator. - """ - matrix = np.asarray(m, dtype=complex) - if matrix.ndim == 3: - matrix = matrix[0] + 1j * matrix[1] - if not matrix.ndim == 2: # pragma no coverage - raise ValueError("Need 2d array.") - if not matrix.shape[0] == matrix.shape[1]: # pragma no coverage - raise ValueError("Need square matrix.") - return matrix - - def to_dict(self) -> dict[str, Any]: - """Create a dictionary representation of a KrausModel. - - For example:: - - { - "gate": "RX", - "params": np.pi, - "targets": [0], - "kraus_ops": [ # In this example single Kraus op = ideal RX(pi) gate - [ - [ - [0, 0], # element-wise real part of matrix - [0, 0], - ], - [ - [0, -1], # element-wise imaginary part of matrix - [-1, 0], - ], - ] - ], - "fidelity": 1.0, - } - - :return: A JSON compatible dictionary representation. - :rtype: dict[str,Any] - """ - res = self._asdict() - res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops] - return res - - @staticmethod - def from_dict(d: dict[str, Any]) -> "KrausModel": - """Recreate a KrausModel from the dictionary representation. - - :param d: The dictionary representing the KrausModel. See `to_dict` for an - example. - :return: The deserialized KrausModel. - """ - kraus_ops = [KrausModel.unpack_kraus_matrix(k) for k in d["kraus_ops"]] - return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"]) - - def __eq__(self, other: object) -> bool: - """Return True if both KrausModels are equal.""" - return isinstance(other, KrausModel) and self.to_dict() == other.to_dict() - - -_LegacyNoiseModel = namedtuple("_LegacyNoiseModel", ["gates", "assignment_probs"]) - - -class LegacyNoiseModel(_LegacyNoiseModel): - """Encapsulate the QPU noise model containing information about the noisy gates. - - .. deprecated:: - Use :class:`pyquil.noise_model.NoiseModel` for quax-based noise modeling. - - :ivar Sequence[KrausModel] gates: The tomographic estimates of all gates. - :ivar dict[int,np.array] assignment_probs: The single qubit readout assignment - probability matrices keyed by qubit id. - """ - - def to_dict(self) -> dict[str, Any]: - """Create a JSON serializable representation of the noise model. - - For example:: - - { - "gates": [ - # list of embedded dictionary representations of KrausModels here [...] - ] - "assignment_probs": { - "0": [[.8, .1], - [.2, .9]], - "1": [[.9, .4], - [.1, .6]], - } - } - - :return: A dictionary representation of self. - """ - return { - "gates": [km.to_dict() for km in self.gates], - "assignment_probs": {str(qid): a.tolist() for qid, a in self.assignment_probs.items()}, - } - - @staticmethod - def from_dict(d: dict[str, Any]) -> "LegacyNoiseModel": - """Re-create the noise model from a dictionary representation. - - :param d: The dictionary representation. - :return: The restored noise model. - """ - return LegacyNoiseModel( - gates=[KrausModel.from_dict(t) for t in d["gates"]], - assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()}, - ) - - def gates_by_name(self, name: str) -> list[KrausModel]: - """Return all defined noisy gates of a particular gate name. - - :param str name: The gate name. - :return: A list of noise models representing that gate. - """ - return [g for g in self.gates if g.gate == name] - - def __eq__(self, other: object) -> bool: - """Return True if NoiseModels are equal.""" - return isinstance(other, LegacyNoiseModel) and self.to_dict() == other.to_dict() - - -def _check_kraus_ops(n: int, kraus_ops: Sequence[np.ndarray]) -> None: - """Verify that the Kraus operators are of the correct shape and satisfy the correct normalization. - - :param n: Number of qubits - :param kraus_ops: The Kraus operators as numpy.ndarrays. - """ - for k in kraus_ops: - if not np.shape(k) == (2**n, 2**n): - raise ValueError(f"Kraus operators for {n} qubits must have shape {2**n}x{2**n}: {k}") - - kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops) - if not np.allclose(kdk_sum, np.eye(2**n), atol=1e-3): - raise ValueError(f"Kraus operator not correctly normalized: sum_j K_j^*K_j == {kdk_sum}") - - -def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[np.ndarray]) -> list[Pragma]: - """Generate the pragmas to define a Kraus map for a specific gate on some qubits. - - :param name: The name of the gate. - :param qubit_indices: The qubits - :param kraus_ops: The Kraus operators as matrices. - :return: A QUIL string with PRAGMA ADD-KRAUS ... statements. - """ - pragmas = [ - Pragma( - "ADD-KRAUS", - (name,) + tuple(qubit_indices), - "({})".format(" ".join(map(format_parameter, np.ravel(k)))), - ) - for k in kraus_ops - ] - return pragmas - - -def append_kraus_to_gate( - kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray -) -> list[Union[np.number, np.ndarray]]: - """Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. - - :param kraus_ops: The Kraus operators. - :param gate_matrix: The unitary gate. - :return: A list of transformed Kraus operators. - """ - return [kj.dot(gate_matrix) for kj in kraus_ops] - - -def pauli_kraus_map(probabilities: Sequence[float]) -> list[np.ndarray]: - r"""Generate the Kraus operators corresponding to a pauli channel. - - :params probabilities: The 4^num_qubits list of probabilities specifying the - desired pauli channel. There should be either 4 or 16 probabilities specified in the - order I, X, Y, Z for 1 qubit or II, IX, IY, IZ, XI, XX, XY, etc for 2 qubits. - - For example:: - - The d-dimensional depolarizing channel \Delta parameterized as - \Delta(\rho) = p \rho + [(1-p)/d] I - is specified by the list of probabilities - [p + (1-p)/d, (1-p)/d, (1-p)/d), ... , (1-p)/d)] - - :return: A list of the 4^num_qubits Kraus operators that parametrize the map. - """ - if len(probabilities) not in [4, 16]: - raise ValueError( - "Currently we only support one or two qubits, " - "so the provided list of probabilities must have length 4 or 16." - ) - if not np.allclose(sum(probabilities), 1.0, atol=1e-3): - raise ValueError("Probabilities must sum to one.") - - paulis = [ - np.eye(2), - np.array([[0, 1], [1, 0]]), - np.array([[0, -1j], [1j, 0]]), - np.array([[1, 0], [0, -1]]), - ] - - if len(probabilities) == 4: - operators = paulis - else: - operators = np.kron(paulis, paulis) # type: ignore - - return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] - - -def damping_kraus_map(p: float = 0.10) -> list[np.ndarray]: - """Generate the Kraus operators corresponding to an amplitude damping noise channel. - - :param p: The one-step damping probability. - :return: A list [k1, k2] of the Kraus operators that parametrize the map. - :rtype: list - """ - damping_op = np.sqrt(p) * np.array([[0, 1], [0, 0]]) - - residual_kraus = np.diag([1, np.sqrt(1 - p)]) - return [residual_kraus, damping_op] - - -def dephasing_kraus_map(p: float = 0.10) -> list[np.ndarray]: - """Generate the Kraus operators corresponding to a dephasing channel. - - :params float p: The one-step dephasing probability. - :return: A list [k1, k2] of the Kraus operators that parametrize the map. - :rtype: list - """ - return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])] - - -def tensor_kraus_maps(k1: list[np.ndarray], k2: list[np.ndarray]) -> list[np.ndarray]: - """Generate the Kraus map corresponding to the composition of two maps on different qubits. - - :param k1: The Kraus operators for the first qubit. - :param k2: The Kraus operators for the second qubit. - :return: A list of tensored Kraus operators. - """ - return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2] - - -def combine_kraus_maps(k1: list[np.ndarray], k2: list[np.ndarray]) -> list[np.ndarray]: - """Generate the Kraus map for two composed maps, with k1 applied after k2 on the same qubits. - - :param k1: The list of Kraus operators that are applied second. - :param k2: The list of Kraus operators that are applied first. - :return: A combinatorially generated list of composed Kraus operators. - """ - return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2] - - -def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> list[np.ndarray]: - """Generate the Kraus map for a dephasing channel followed by an amplitude damping channel. - - :param T1: The amplitude damping time - :param T2: The dephasing time - :param gate_time: The gate duration. - :return: A list of Kraus operators. - """ - if T1 < 0 or T2 < 0: - raise ValueError("T1 and T2 must be non-negative.") - - if T1 != INFINITY: - damping = damping_kraus_map(p=1 - np.exp(-float(gate_time) / float(T1))) - else: - damping = [np.eye(2)] - - if T2 != INFINITY: - gamma_phi = float(gate_time) / float(T2) - if T1 != INFINITY: - if T2 > 2 * T1: - raise ValueError("T2 is upper bounded by 2 * T1") - gamma_phi -= float(gate_time) / float(2 * T1) - - dephasing = dephasing_kraus_map(p=0.5 * (1 - np.exp(-gamma_phi))) - else: - dephasing = [np.eye(2)] - return combine_kraus_maps(damping, dephasing) - - -# You can only apply gate-noise to non-parametrized gates or parametrized gates at fixed parameters. -NO_NOISE = ["RZ"] -ANGLE_TOLERANCE = 1e-10 - - -class NoisyGateUndefined(Exception): - """Raise when user attempts to use noisy gate outside of currently supported set.""" - - pass - - -def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> tuple[np.ndarray, str]: - """Look up the numerical gate representation and a proposed 'noisy' name. - - :param gate_name: The Quil gate name - :param params: The gate parameters. - :return: A tuple (matrix, noisy_name) with the representation of the ideal gate matrix - and a proposed name for the noisy version. - """ - params = tuple(params) - if gate_name == "I": - if params != (): - raise ValueError(f"Identity gate does not take parameters: {params}") - return np.eye(2), "NOISY-I" - if gate_name == "RX": - (angle,) = params - if not isinstance(angle, (int, float, complex)): - raise TypeError(f"Cannot produce noisy gate for parameter of type {type(angle)}") - - if np.isclose(angle, np.pi / 2, atol=ANGLE_TOLERANCE): - return np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), "NOISY-RX-PLUS-90" - elif np.isclose(angle, -np.pi / 2, atol=ANGLE_TOLERANCE): - return np.array([[1, 1j], [1j, 1]]) / np.sqrt(2), "NOISY-RX-MINUS-90" - elif np.isclose(angle, np.pi, atol=ANGLE_TOLERANCE): - return np.array([[0, -1j], [-1j, 0]]), "NOISY-RX-PLUS-180" - elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE): - return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180" - elif gate_name == "CZ": - if params != (): - raise ValueError(f"CZ gate does not take parameters: {params}") - return np.diag([1, 1, 1, -1]), "NOISY-CZ" - - raise NoisyGateUndefined( - f"Undefined gate and params: {gate_name}{params}\n" "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ" - ) - - -def _get_program_gates(prog: "Program") -> list[Gate]: - """Get all gate applications appearing in prog. - - :param prog: The program - :return: A list of all Gates in prog (without duplicates). - """ - return sorted({i for i in prog if isinstance(i, Gate)}, key=lambda g: g.out()) - - -def _decoherence_noise_model( - gates: Sequence[Gate], - T1: Union[dict[int, float], float] = 30e-6, - T2: Union[dict[int, float], float] = 30e-6, - gate_time_1q: float = 50e-9, - gate_time_2q: float = 150e-09, - ro_fidelity: Union[dict[int, float], float] = 0.95, -) -> LegacyNoiseModel: - """Return default noise model. - - - T1 = 30 us - - T2 = 30 us - - 1q gate time = 50 ns - - 2q gate time = 150 ns - - are currently typical for near-term devices. - - This function will define new gates and add Kraus noise to these gates. It will translate - the input program to use the noisy version of the gates. - - :param gates: The gates to provide the noise model for. - :param T1: The T1 amplitude damping time either globally or in a - dictionary indexed by qubit id. By default, this is 30 us. - :param T2: The T2 dephasing time either globally or in a - dictionary indexed by qubit id. By default, this is also 30 us. - :param gate_time_1q: The duration of the one-qubit gates, namely RX(+pi/2) and RX(-pi/2). - By default, this is 50 ns. - :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - By default, this is 150 ns. - :param ro_fidelity: The readout assignment fidelity - :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. - :return: A NoiseModel with the appropriate Kraus operators defined. - """ - all_qubits = set(sum((g.get_qubit_indices() for g in gates), [])) - if isinstance(T1, dict): - all_qubits.update(T1.keys()) - if isinstance(T2, dict): - all_qubits.update(T2.keys()) - if isinstance(ro_fidelity, dict): - all_qubits.update(ro_fidelity.keys()) - - if not isinstance(T1, dict): - T1 = {q: T1 for q in all_qubits} - - if not isinstance(T2, dict): - T2 = {q: T2 for q in all_qubits} - - if not isinstance(ro_fidelity, dict): - ro_fidelity = {q: ro_fidelity for q in all_qubits} - - noisy_identities_1q = { - q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_1q) for q in all_qubits - } - noisy_identities_2q = { - q: damping_after_dephasing(T1.get(q, INFINITY), T2.get(q, INFINITY), gate_time_2q) for q in all_qubits - } - kraus_maps = [] - for g in gates: - targets = tuple(g.get_qubit_indices()) - if g.name in NO_NOISE: - continue - matrix, _ = get_noisy_gate(g.name, g.params) - - if len(targets) == 1: - noisy_I = noisy_identities_1q[targets[0]] - else: - if len(targets) != 2: - raise ValueError("Noisy gates on more than 2Q not currently supported") - - # note this ordering of the tensor factors is necessary due to how the QVM orders - # the wavefunction basis - noisy_I = tensor_kraus_maps(noisy_identities_2q[targets[1]], noisy_identities_2q[targets[0]]) - kraus_maps.append( - KrausModel( - g.name, - tuple(g.params), - targets, - combine_kraus_maps(noisy_I, [matrix]), - # FIXME (Nik): compute actual avg gate fidelity for this simple - # noise model - 1.0, - ) - ) - aprobs = {} - for q, f_ro in ro_fidelity.items(): - aprobs[q] = np.array([[f_ro, 1.0 - f_ro], [1.0 - f_ro, f_ro]]) - - return LegacyNoiseModel(kraus_maps, aprobs) - - -def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p11: float = 0.911) -> LegacyNoiseModel: - """Similar to :py:func:`_decoherence_noise_model`, but with asymmetric readout. - - For simplicity, we use the default values for T1, T2, gate times, et al. and only allow - the specification of readout fidelities. - """ - gates = _get_qvm_noise_supported_gates(isa) - noise_model = _decoherence_noise_model(gates) - aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) - aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()} - return LegacyNoiseModel(noise_model.gates, aprobs) - - -def _noise_model_program_header(noise_model: LegacyNoiseModel) -> "Program": - """Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates. - - The program header consists of 3 sections: - - - The ``DEFGATE`` statements that define the meaning of the newly introduced "noisy" gate - names. - - The ``PRAGMA ADD-KRAUS`` statements to overload these noisy gates on specific qubit - targets with their noisy implementation. - - THe ``PRAGMA READOUT-POVM`` statements that define the noisy readout per qubit. - - :param noise_model: The assumed noise model. - :return: A quil Program with the noise pragmas. - """ - from pyquil.quil import Program - - p = Program() - defgates: set[str] = set() - for k in noise_model.gates: - # obtain ideal gate matrix and new, noisy name by looking it up in the NOISY_GATES dict - try: - ideal_gate, new_name = get_noisy_gate(k.gate, tuple(k.params)) - - # if ideal version of gate has not yet been DEFGATE'd, do this - if new_name not in defgates: - p.defgate(new_name, ideal_gate) - defgates.add(new_name) - except NoisyGateUndefined: - print( - f"WARNING: Could not find ideal gate definition for gate {k.gate}", - file=sys.stderr, - ) - new_name = k.gate - - # define noisy version of gate on specific targets - p.define_noisy_gate(new_name, k.targets, k.kraus_ops) - - # define noisy readouts - for q, ap in noise_model.assignment_probs.items(): - p.define_noisy_readout(q, p00=ap[0, 0], p11=ap[1, 1]) - return p - - -def apply_noise_model(prog: "Program", noise_model: LegacyNoiseModel) -> "Program": - """Apply a noise model to a program and generated a 'noisy-fied' version of the program. - - :param prog: A Quil Program object. - :param noise_model: A NoiseModel, either generated from an ISA or - from a simple decoherence model. - :return: A new program translated to a noisy gateset and with noisy readout as described by the - noisemodel. - """ - new_prog = _noise_model_program_header(noise_model) - for i in prog: - if isinstance(i, Gate) and noise_model.gates: - try: - _, new_name = get_noisy_gate(i.name, tuple(i.params)) - new_prog += Gate(new_name, [], i.qubits) - except NoisyGateUndefined: - new_prog += i - else: - new_prog += i - return prog.copy_everything_except_instructions() + new_prog - - -def add_decoherence_noise( - prog: "Program", - T1: Union[dict[int, float], float] = 30e-6, - T2: Union[dict[int, float], float] = 30e-6, - gate_time_1q: float = 50e-9, - gate_time_2q: float = 150e-09, - ro_fidelity: Union[dict[int, float], float] = 0.95, -) -> "Program": - """Add generic damping and dephasing noise to a program. - - This high-level function is provided as a convenience to investigate the effects of a - generic noise model on a program. For more fine-grained control, please investigate - the other methods available in the ``pyquil.noise`` module. - - In an attempt to closely model the QPU, noisy versions of RX(+-pi/2) and CZ are provided; - I and parametric RZ are noiseless, and other gates are not allowed. To use this function, - you need to compile your program to this native gate set. - - The default noise parameters - - - T1 = 30 us - - T2 = 30 us - - 1q gate time = 50 ns - - 2q gate time = 150 ns - - are currently typical for near-term devices. - - This function will define new gates and add Kraus noise to these gates. It will translate - the input program to use the noisy version of the gates. - - :param prog: A pyquil program consisting of I, RZ, CZ, and RX(+-pi/2) instructions - :param T1: The T1 amplitude damping time either globally or in a - dictionary indexed by qubit id. By default, this is 30 us. - :param T2: The T2 dephasing time either globally or in a - dictionary indexed by qubit id. By default, this is also 30 us. - :param gate_time_1q: The duration of the one-qubit gates, namely RX(+pi/2) and RX(-pi/2). - By default, this is 50 ns. - :param gate_time_2q: The duration of the two-qubit gates, namely CZ. - By default, this is 150 ns. - :param ro_fidelity: The readout assignment fidelity - :math:`F = (p(0|0) + p(1|1))/2` either globally or in a dictionary indexed by qubit id. - :return: A new program with noisy operators. - """ - gates = _get_program_gates(prog) - noise_model = _decoherence_noise_model( - gates, - T1=T1, - T2=T2, - gate_time_1q=gate_time_1q, - gate_time_2q=gate_time_2q, - ro_fidelity=ro_fidelity, - ) - return apply_noise_model(prog, noise_model) - - -def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray: - """Ensure array p has a separate axis for each qubit so ``p[i,j,...,k]`` gives the probability of bitstring ``ij...k``. - - This should not allocate much memory if ``p`` is already in ``C``-contiguous order (row-major). - - :param p: An array that enumerates bitstring probabilities. When flattened out - ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must therefore be a - power of 2. - :return: A reshaped view of ``p`` with a separate length-2 axis for each bit. - """ - p = np.asarray(p, order="C") - num_qubits = int(round(np.log2(p.size))) - return p.reshape((2,) * num_qubits) - - -def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray: - """Given an array of single shot results estimate the probability distribution over all bitstrings. - - :param results: A 2d array where the outer axis iterates over shots - and the inner axis over bits. - :return: An array with as many axes as there are qubit and normalized such that it sums to one. - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - """ - nshots, nq = np.shape(results) - outcomes = np.array([int("".join(map(str, r)), 2) for r in results]) - probs = np.histogram(outcomes, bins=np.arange(-0.5, 2**nq, 1))[0] / float(nshots) - return _bitstring_probs_by_qubit(probs) - - -_CHARS = "klmnopqrstuvwxyzabcdefgh0123456789" - - -def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarray: - """Apply local 2x2 matrices to each index in a 2D array of single shot results using assignment probability matrices. - - Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits) - and a list of assignment probability matrices (one for each bit in the readout, ordered like - the inner axis of results) apply local 2x2 matrices to each bit index. - - :param p: An array that enumerates a function indexed by - bitstrings:: - - f(ijk...) = p[i,j,k,...] - - :param ts: A sequence of 2x2 transform-matrices, one for each bit. - :return: ``p_transformed`` an array with as many dimensions as there are bits with the result of - contracting p along each axis by the corresponding bit transformation:: - - p_transformed[ijk...] = f'(ijk...) = sum_lmn... ts[0][il] ts[1][jm] ts[2][kn] f(lmn...) - """ - p_corrected = _bitstring_probs_by_qubit(p) - nq = p_corrected.ndim - for idx, trafo_idx in enumerate(ts): - # this contraction pattern looks like - # 'ij,abcd...jklm...->abcd...iklm...' so it properly applies a "local" - # transformation to a single tensor-index without changing the order of - # indices - einsum_pat = ( - "ij," + _CHARS[:idx] + "j" + _CHARS[idx : nq - 1] + "->" + _CHARS[:idx] + "i" + _CHARS[idx : nq - 1] - ) - p_corrected = np.einsum(einsum_pat, trafo_idx, p_corrected) - - return p_corrected - - -def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: list[np.ndarray]) -> np.ndarray: - """Given a 2D array of bitstring probabilities and assignment matrices, compute the corrupted probabilities. - - Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis - over bits) and a list of assignment probability matrices (one for each bit in the readout, - ordered like the inner axis of results) compute the corrupted probabilities. - - :param p: An array that enumerates bitstring probabilities. When - flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must - therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: - - [[p00 p01] - [p10 p11]] - - :return: ``p_corrected`` an array with as many dimensions as there are qubits that contains - the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - """ - return _apply_local_transforms(p, assignment_probabilities) - - -def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: list[np.ndarray]) -> np.ndarray: - """Given a 2D array of corrupted bitstring probabilities and assignment matrices, compute the corrected probabilities. - - Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner - axis over bits) and a list of assignment probability matrices (one for each bit in the readout) - compute the corrected probabilities. - - :param p: An array that enumerates bitstring probabilities. When - flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must - therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - :param assignment_probabilities: A list of assignment probability matrices - per qubit. Each assignment probability matrix is expected to be of the form:: - - [[p00 p01] - [p10 p11]] - - :return: ``p_corrected`` an array with as many dimensions as there are qubits that contains - the noisy-readout-corrected estimated probabilities for each measured bitstring, i.e., - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - """ - return _apply_local_transforms(p, (np.linalg.inv(ap) for ap in assignment_probabilities)) - - -def bitstring_probs_to_z_moments(p: np.ndarray) -> np.ndarray: - """Convert between bitstring probabilities and joint Z moment expectations. - - :param p: An array that enumerates bitstring probabilities. When - flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must - therefore be a power of 2. The canonical shape has a separate axis for each qubit, such that - ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``. - :return: ``z_moments``, an np.array with one length-2 axis per qubit which contains the - expectations of all monomials in ``{I, Z_0, Z_1, ..., Z_{n-1}}``. The expectations of each - monomial can be accessed via:: - - = z_moments[j_0,j_1,...,j_m] - """ - zmat = np.array([[1, 1], [1, -1]]) - return _apply_local_transforms(p, (zmat for _ in range(p.ndim))) - - -def estimate_assignment_probs( - q: int, - trials: int, - qc: "PyquilApiQuantumComputer", - p0: Optional["Program"] = None, -) -> np.ndarray: - """Estimate the readout assignment probabilities for a given qubit ``q``. - - The returned matrix is of the form:: - - [[p00 p01] - [p10 p11]] - - :param q: The index of the qubit. - :param trials: The number of samples for each state preparation. - :param qc: The quantum computer to sample from. - :param p0: A header program to prepend to the state preparation programs. Will not be compiled by quilc, so it must - be native Quil. - :return: The assignment probability matrix - """ - from pyquil.quil import Program - - if p0 is None: # pragma no coverage - p0 = Program() - - p_i = ( - p0 - + Program( - Declare("ro", "BIT", 1), - I(q), - MEASURE(q, MemoryReference("ro", 0)), - ) - ).wrap_in_numshots_loop(trials) - results_i = np.sum(_run(qc, p_i)) - - p_x = ( - p0 - + Program( - Declare("ro", "BIT", 1), - RX(np.pi, q), - MEASURE(q, MemoryReference("ro", 0)), - ) - ).wrap_in_numshots_loop(trials) - results_x = np.sum(_run(qc, p_x)) - - p00 = 1.0 - results_i / float(trials) - p11 = results_x / float(trials) - return np.array([[p00, 1 - p11], [1 - p00, p11]]) - - -def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> list[list[int]]: - result = qc.run(qc.compiler.native_quil_to_executable(program)) - bitstrings = result.readout_data.get("ro") - if bitstrings is None: - raise ValueError("No readout data found in result.") - return cast(list[list[int]], bitstrings.tolist()) - - -# ────────────────────────────────────────────────────────── -# Re-export quax-based noise model classes (lazy to avoid circular imports) -# ────────────────────────────────────────────────────────── - -_NOISE_MODEL_EXPORTS = ( - "Channel", - "CycleChannel", - "CustomGateMap", - "MeasurementChannel", - "NoiseModel", - "ResetChannel", - "estimate_program_fidelity", - "estimate_program_observable_fidelity", - "get_custom_gates_from_program", - "get_instruction_unitary", -) - - -def __getattr__(name: str): # type: ignore[override] - if name in _NOISE_MODEL_EXPORTS: - import pyquil.noise_model as _nm - - return getattr(_nm, name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index 000eec6a3..b9fc0d066 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -30,7 +30,7 @@ from dataclasses import dataclass, replace from functools import cached_property, reduce from itertools import product -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Callable import jax.numpy as jnp import numpy as np @@ -50,7 +50,7 @@ logger = logging.getLogger(__name__) # Type alias for the custom-gate lookup map used throughout the Channel constructors. -CustomGateMap = Dict[str, Union[qx.Unitary, Callable[..., qx.Unitary]]] +CustomGateMap = dict[str, qx.Unitary | Callable[..., qx.Unitary]] def _parse_quil_instruction(quil_str: str) -> Gate | Measurement | Reset: @@ -68,7 +68,7 @@ def _parse_quil_instruction(quil_str: str) -> Gate | Measurement | Reset: raise ValueError(f"Unsupported instruction type in: {quil_str}") -def _resolve_params(params: list) -> List[float]: +def _resolve_params(params: list) -> list[float]: """ Resolve gate parameters to concrete float values. @@ -127,7 +127,7 @@ def parametric_gate(*args: float, defgate: DefGate = defgate) -> qx.Unitary: def get_instruction_unitary( inst: Gate, - custom_gates: Optional[CustomGateMap] = None, + custom_gates: CustomGateMap | None = None, ) -> qx.Unitary: """ Get the unitary matrix associated with a gate instruction. @@ -201,7 +201,7 @@ def unitary(self) -> qx.Unitary: return self.target_unitary @cached_property - def qubits(self) -> List[int]: + def qubits(self) -> list[int]: """The qubits which the channel applies to.""" return self.inst.get_qubit_indices() @@ -216,10 +216,10 @@ def num_qubits(self) -> int: @classmethod def from_gate_fidelity( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, fidelity: float, - custom_gates: Optional[CustomGateMap] = None, + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a depolarizing noise channel from an average gate fidelity. @@ -239,10 +239,10 @@ def from_gate_fidelity( @classmethod def from_pauli_fidelity( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, pauli_fidelity: float, - custom_gates: Optional[CustomGateMap] = None, + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a depolarizing noise channel from a process (Pauli) fidelity. @@ -261,10 +261,10 @@ def from_pauli_fidelity( @classmethod def from_depolarizing_constant( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, depolarizing_constant: float, - custom_gates: Optional[CustomGateMap] = None, + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a depolarizing noise channel from a depolarization constant. @@ -284,10 +284,10 @@ def from_depolarizing_constant( @classmethod def from_pauli_noise( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, - pauli_noise: Dict[str, float], - custom_gates: Optional[CustomGateMap] = None, + pauli_noise: dict[str, float], + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a stochastic Pauli noise channel from Pauli error rates. @@ -308,7 +308,7 @@ def from_pauli_noise( if len(pauli) != num_qubits: raise ValueError(f"Pauli term '{pauli}' has length {len(pauli)}, expected {num_qubits}.") - all_pauli_terms = list(map(lambda term: "".join(term), itertools.product(*["IXYZ" for _ in range(num_qubits)]))) + all_pauli_terms = tuple("".join(term) for term in product("IXYZ", repeat=num_qubits)) pauli_error_rates = [] for term in reversed(all_pauli_terms): @@ -339,11 +339,11 @@ def from_pauli_noise( @classmethod def from_random_coherent_error( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, process_fidelity: float, - rng: Optional[np.random.Generator] = None, - custom_gates: Optional[CustomGateMap] = None, + rng: np.random.Generator | None = None, + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a channel with a random coherent (unitary) error at the specified process fidelity. @@ -391,11 +391,11 @@ def from_random_coherent_error( @classmethod def from_mixture( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, - constituents: List[qx.Unitary], - probabilities: List[float], - custom_gates: Optional[CustomGateMap] = None, + constituents: list[qx.Unitary], + probabilities: list[float], + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a mixture channel from a set of unitary errors with given probabilities. @@ -428,12 +428,12 @@ def from_mixture( @classmethod def from_coherence_times( - cls: Type["Channel"], + cls: type[Channel], inst: Gate, gate_duration: float, - t1s: List[float], - t2s: Optional[List[float]] = None, - custom_gates: Optional[CustomGateMap] = None, + t1s: list[float], + t2s: list[float] | None = None, + custom_gates: CustomGateMap | None = None, ) -> "Channel": """ Create a decoherence Channel based on the coherence times. @@ -465,6 +465,32 @@ def from_coherence_times( target_unitary=unitary, ) + @classmethod + def from_superoperator( + cls: type[Channel], + inst: Gate, + process: qx.SuperOp, + target_unitary: qx.Unitary | None = None, + custom_gates: CustomGateMap | None = None, + ) -> Channel: + """ + Create a Channel from a pre-built superoperator. + + If ``target_unitary`` is not provided it is inferred from the gate + instruction using the standard gate set (and ``custom_gates`` if given). + + :param inst: The gate to which the channel applies. + :param process: The noisy process superoperator (includes the gate unitary). + :param target_unitary: The ideal gate unitary. Resolved automatically + when omitted. + :param custom_gates: Optional dictionary of custom gate definitions, + used only when ``target_unitary`` is ``None``. + :return: A Channel instance. + """ + if target_unitary is None: + target_unitary = get_instruction_unitary(inst, custom_gates) + return cls(inst=inst, process=process, target_unitary=target_unitary) + # ────────────────────────────────────────────── # Cached representation conversions # ────────────────────────────────────────────── @@ -701,7 +727,7 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: Type["Channel"], json_str: str) -> "Channel": + def from_json(cls: type[Channel], json_str: str) -> "Channel": """ Deserialize a Channel from a JSON string. @@ -739,7 +765,7 @@ def from_json(cls: Type["Channel"], json_str: str) -> "Channel": # Dunder methods # ────────────────────────────────────────────── - def __repr__(self) -> str: + def __str__(self) -> str: """Return a simplified string representation showing the gate and process fidelity.""" return f"<{self.inst.out()} ~ ({100 * self.pauli_fidelity:.2f}%)>" @@ -751,9 +777,7 @@ def __eq__(self, other: object) -> bool: return False return bool(jnp.isclose(float(qx.process_fidelity(self.process, other.process)), 1.0, atol=1e-9)) - def __hash__(self) -> int: - """Hash based on the instruction (for use in sets/dicts).""" - return hash(self.inst) + __hash__ = None def __matmul__(self, other: "Channel") -> "Channel": """ @@ -816,7 +840,7 @@ class MeasurementChannel: """A quantum instrument representation of the noisy measurement.""" @cached_property - def qubits(self) -> List[int]: + def qubits(self) -> list[int]: """The qubits which the measurement applies to.""" qubit = self.inst.qubit return [qubit.index if hasattr(qubit, "index") else int(qubit)] # type: ignore[union-attr,arg-type] @@ -827,7 +851,7 @@ def qubits(self) -> List[int]: @classmethod def from_readout_fidelity( - cls: Type["MeasurementChannel"], + cls: type[MeasurementChannel], inst: Measurement, fidelity: float, asymmetry: float = 0.0, @@ -872,7 +896,7 @@ def from_readout_fidelity( @classmethod def from_confusion_and_transition( - cls: Type["MeasurementChannel"], + cls: type[MeasurementChannel], inst: Measurement, confusion_matrix: Array, transition_matrix: Array, @@ -906,7 +930,7 @@ def from_confusion_and_transition( @classmethod def from_axis( - cls: Type["MeasurementChannel"], + cls: type[MeasurementChannel], inst: Measurement, theta: float = 0.0, phi: float = 0.0, @@ -934,7 +958,7 @@ def from_axis( @classmethod def from_binary_discriminator( - cls: Type["MeasurementChannel"], + cls: type[MeasurementChannel], inst: Measurement, dim: int, threshold: int, @@ -1081,7 +1105,7 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: Type["MeasurementChannel"], json_str: str) -> "MeasurementChannel": + def from_json(cls: type[MeasurementChannel], json_str: str) -> "MeasurementChannel": """ Deserialize a MeasurementChannel from a JSON string. @@ -1110,7 +1134,7 @@ def from_json(cls: Type["MeasurementChannel"], json_str: str) -> "MeasurementCha # Dunder methods # ────────────────────────────────────────────── - def __repr__(self) -> str: + def __str__(self) -> str: """Return a simplified string representation.""" return f"" @@ -1122,9 +1146,7 @@ def __eq__(self, other: object) -> bool: return False return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) - def __hash__(self) -> int: - """Hash based on the instruction.""" - return hash(self.inst) + __hash__ = None def __matmul__(self, other: "MeasurementChannel") -> "MeasurementChannel": """ @@ -1181,7 +1203,7 @@ class ResetChannel: @classmethod def from_reset_fidelity( - cls: Type["ResetChannel"], + cls: type[ResetChannel], inst: Reset, fidelity: float, dim: int = 2, @@ -1225,7 +1247,7 @@ def from_reset_fidelity( # ────────────────────────────────────────────── @cached_property - def qubits(self) -> List[int]: + def qubits(self) -> list[int]: """The qubit(s) that the reset applies to.""" qubit = self.inst.qubit if qubit is None: @@ -1290,7 +1312,7 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: Type["ResetChannel"], json_str: str) -> "ResetChannel": + def from_json(cls: type[ResetChannel], json_str: str) -> "ResetChannel": """ Deserialize a ResetChannel from a JSON string. @@ -1314,7 +1336,7 @@ def from_json(cls: Type["ResetChannel"], json_str: str) -> "ResetChannel": # Dunder methods # ────────────────────────────────────────────── - def __repr__(self) -> str: + def __str__(self) -> str: """Return a simplified string representation.""" qubit_str = str(self.qubits[0]) if self.qubits else "?" return f"" @@ -1327,9 +1349,7 @@ def __eq__(self, other: object) -> bool: return False return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) - def __hash__(self) -> int: - """Hash based on the instruction.""" - return hash(self.inst) + __hash__ = None @dataclass(frozen=True) @@ -1347,7 +1367,7 @@ class CycleChannel: defcircuit: DefCircuit """The DefCircuit representing the logical cycle to which instruction represents.""" - channels: Tuple["Channel | MeasurementChannel", ...] + channels: tuple["Channel | MeasurementChannel", ...] """Constituent channels (one per operation in the cycle) on disjoint qubits.""" # ────────────────────────────────────────────── @@ -1355,12 +1375,12 @@ class CycleChannel: # ────────────────────────────────────────────── @cached_property - def operator(self) -> Tuple[qx.SuperOp | qx.QuantumInstrument, ...]: + def operator(self) -> tuple[qx.SuperOp | qx.QuantumInstrument, ...]: """Tuple of process superoperators, one per constituent channel.""" return tuple(ch.process for ch in self.channels) @cached_property - def qubits(self) -> List[int]: + def qubits(self) -> list[int]: """All qubits in the cycle, derived from the instruction.""" return self.inst.get_qubit_indices() @@ -1419,7 +1439,7 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: Type["CycleChannel"], json_str: str) -> "CycleChannel": + def from_json(cls: type[CycleChannel], json_str: str) -> "CycleChannel": """ Deserialize a CycleChannel from a JSON string. @@ -1430,11 +1450,11 @@ def from_json(cls: Type["CycleChannel"], json_str: str) -> "CycleChannel": :return: CycleChannel instance. """ data = json.loads(json_str) - _type_map: Dict[str, Type["Channel | MeasurementChannel"]] = { + _type_map: dict[str, type[Channel | MeasurementChannel]] = { "Channel": Channel, "MeasurementChannel": MeasurementChannel, } - constituent_channels: List["Channel | MeasurementChannel"] = [ + constituent_channels: list["Channel | MeasurementChannel"] = [ _type_map[ch_data["type"]].from_json(ch_data["data"]) # type: ignore[index] for ch_data in data["channels"] ] @@ -1444,7 +1464,7 @@ def from_json(cls: Type["CycleChannel"], json_str: str) -> "CycleChannel": # Dunder methods # ────────────────────────────────────────────── - def __repr__(self) -> str: + def __str__(self) -> str: """Return a simplified string representation showing the gate and process fidelity.""" return f"<{self.inst.out()} ~ ({100 * self.pauli_fidelity:.2f}%)>" @@ -1456,9 +1476,7 @@ def __eq__(self, other: object) -> bool: return False return self.channels == other.channels - def __hash__(self) -> int: - """Hash based on the instruction.""" - return hash(self.inst) + __hash__ = None def _channel_to_formal_inst(channel: Channel | MeasurementChannel) -> Gate | Measurement: @@ -1481,7 +1499,7 @@ def _channel_to_formal_inst(channel: Channel | MeasurementChannel) -> Gate | Mea def _build_cycle_channel( - channels: List["Channel | MeasurementChannel"], + channels: list["Channel | MeasurementChannel"], ) -> "CycleChannel": """Build a CycleChannel from a list of Channel/MeasurementChannel on disjoint qubits.""" all_qubits = sorted(q for ch in channels for q in ch.qubits) diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index a0b89778e..cb6b9eb05 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -34,22 +34,14 @@ import json import logging -from dataclasses import dataclass, field +from dataclasses import dataclass from functools import cached_property, reduce from operator import mul from typing import ( TYPE_CHECKING, - Dict, - FrozenSet, Iterable, - List, - Optional, Protocol, Sequence, - Set, - Tuple, - Type, - Union, overload, runtime_checkable, ) @@ -71,7 +63,7 @@ # ────────────────────────────────────────────────────────── # Channel union type returned by get_channel -ChannelType = Union[Channel, MeasurementChannel, ResetChannel, CycleChannel] +ChannelType = Channel | MeasurementChannel | ResetChannel | CycleChannel @runtime_checkable @@ -95,9 +87,7 @@ def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... @overload def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... - def get_channel( - self, inst: Gate | Measurement | ResetQubit - ) -> ChannelType | None: + def get_channel(self, inst: Gate | Measurement | ResetQubit) -> ChannelType | None: """Retrieve the noise channel for a specific instruction. :param inst: A gate, measurement, or reset instruction. @@ -118,12 +108,12 @@ class NoiseModel: which is coerced to a tuple for immutable storage. """ - channels: Tuple[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel], ...] + channels: tuple[Channel | MeasurementChannel | ResetChannel | CycleChannel, ...] """Immutable tuple of all noise channels in the model.""" def __init__( self, - channels: Iterable[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = (), + channels: Iterable[Channel | MeasurementChannel | ResetChannel | CycleChannel] = (), ) -> None: # Accept any iterable, coerce to tuple for immutable storage. if isinstance(channels, tuple): @@ -132,7 +122,9 @@ def __init__( object.__setattr__(self, "channels", tuple(channels)) @cached_property - def _channel_map(self) -> Dict[Union[Gate, Measurement, ResetQubit], Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]]: + def _channel_map( + self, + ) -> dict[Gate | Measurement | ResetQubit, Channel | MeasurementChannel | ResetChannel | CycleChannel]: """Map from instruction to channel for fast lookup.""" return {ch.inst: ch for ch in self.channels} @@ -161,7 +153,7 @@ def get_channel( # ────────────────────────────────────────────── @classmethod - def from_isa(cls: Type["NoiseModel"], compiler_isa: "CompilerISA") -> "NoiseModel": + def from_isa(cls: type[NoiseModel], compiler_isa: "CompilerISA") -> "NoiseModel": """ Create a noise model from an instruction set architecture. @@ -175,8 +167,8 @@ def from_isa(cls: Type["NoiseModel"], compiler_isa: "CompilerISA") -> "NoiseMode from pyquil.external.rpcq import GateInfo, MeasureInfo from pyquil.quilatom import Qubit as QuilQubit - channels: Set[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = set() - seen_measure_qubits: Set[int] = set() + channels: dict[Gate | Measurement, Channel | MeasurementChannel | ResetChannel | CycleChannel] = {} + seen_measure_qubits: set[int] = set() for qubit_label, qubit in compiler_isa.qubits.items(): for op_info in qubit.gates: @@ -192,10 +184,10 @@ def from_isa(cls: Type["NoiseModel"], compiler_isa: "CompilerISA") -> "NoiseMode if not all(isinstance(p, (float, int, complex)) for p in params): continue - numeric_params: List[float] = [float(p) for p in params if isinstance(p, (float, int, complex))] + numeric_params: list[float] = [float(p) for p in params if isinstance(p, (float, int, complex))] inst = Gate(name=gate_name, params=numeric_params, qubits=qubits) if fidelity is not None and fidelity < 1.0: - channels.add(Channel.from_gate_fidelity(inst=inst, fidelity=fidelity)) + channels[inst] = Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) elif isinstance(op_info, MeasureInfo): if op_info.qubit is None: @@ -213,7 +205,7 @@ def from_isa(cls: Type["NoiseModel"], compiler_isa: "CompilerISA") -> "NoiseMode if fidelity is None: continue m_inst = Measurement(qubit=QuilQubit(qubit_idx), classical_reg=None) - channels.add(MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=fidelity)) + channels[m_inst] = MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=fidelity) for edge_label, edge in compiler_isa.edges.items(): for op_info in edge.gates: @@ -231,9 +223,9 @@ def from_isa(cls: Type["NoiseModel"], compiler_isa: "CompilerISA") -> "NoiseMode numeric_params = [float(p) for p in params if isinstance(p, (float, int, complex))] inst = Gate(name=gate_name, params=numeric_params, qubits=qubits) if fidelity is not None and fidelity < 1.0: - channels.add(Channel.from_gate_fidelity(inst=inst, fidelity=fidelity)) + channels[inst] = Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) - return cls(channels=channels) + return cls(channels=channels.values()) # ────────────────────────────────────────────── # Serialization @@ -254,7 +246,7 @@ def to_json(self) -> str: return json.dumps({"channels": channel_data}) @classmethod - def from_json(cls: Type["NoiseModel"], json_str: str) -> "NoiseModel": + def from_json(cls: type[NoiseModel], json_str: str) -> "NoiseModel": """ Deserialize a NoiseModel from a JSON string. @@ -268,7 +260,7 @@ def from_json(cls: Type["NoiseModel"], json_str: str) -> "NoiseModel": "ResetChannel": ResetChannel, "CycleChannel": CycleChannel, } - channels: List[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = [] + channels: list[Channel | MeasurementChannel | ResetChannel | CycleChannel] = [] for ch_data in data["channels"]: ch_cls = _type_map.get(ch_data["type"]) if ch_cls is None: @@ -303,7 +295,7 @@ def __add__(self, other: "NoiseModel") -> "NoiseModel": my_channels = {ch.inst: ch for ch in self.channels} other_channels = {ch.inst: ch for ch in other.channels} - combined: List[Union[Channel, MeasurementChannel, ResetChannel, CycleChannel]] = [] + combined: list[Channel | MeasurementChannel | ResetChannel | CycleChannel] = [] all_insts = list(dict.fromkeys(list(my_channels) + list(other_channels))) for inst in all_insts: mine = my_channels.get(inst) @@ -353,9 +345,7 @@ def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... @overload def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... - def get_channel( - self, inst: Gate | Measurement | ResetQubit - ) -> ChannelType | None: + def get_channel(self, inst: Gate | Measurement | ResetQubit) -> ChannelType | None: """Return a depolarizing channel for gates; ``None`` for measurements/resets.""" if isinstance(inst, Gate): return Channel.from_depolarizing_constant(inst, self.depolarizing_constant) @@ -372,7 +362,7 @@ class CompositeNoiseModel: :param models: Sequence of noise models to query in priority order. """ - models: Tuple[NoiseModelLike, ...] + models: tuple[NoiseModelLike, ...] def __init__(self, models: Sequence[NoiseModelLike]) -> None: object.__setattr__(self, "models", tuple(models)) @@ -386,9 +376,7 @@ def get_channel(self, inst: Measurement) -> MeasurementChannel | None: ... @overload def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... - def get_channel( - self, inst: Gate | Measurement | ResetQubit - ) -> ChannelType | None: + def get_channel(self, inst: Gate | Measurement | ResetQubit) -> ChannelType | None: """Query each model in order, returning the first non-None result.""" for model in self.models: channel = model.get_channel(inst) @@ -425,7 +413,7 @@ def estimate_program_fidelity(program: Program, noise_model: NoiseModelLike) -> return reduce(mul, gate_fidelities) -def _light_cone_program(program: Program, qubits: List[int]) -> Program: +def _light_cone_program(program: Program, qubits: list[int]) -> Program: """Return a sub-program containing only gates in the backward light cone of *qubits*. Walks backward through the program's gate instructions. Any gate that @@ -437,7 +425,7 @@ def _light_cone_program(program: Program, qubits: List[int]) -> Program: gate_instructions = [inst for inst in program.instructions if isinstance(inst, Gate)] relevant_qubits = set(qubits) - included: List[Gate] = [] + included: list[Gate] = [] for inst in reversed(gate_instructions): inst_qubits = {q.index for q in inst.qubits} if inst_qubits & relevant_qubits: @@ -452,7 +440,7 @@ def _light_cone_program(program: Program, qubits: List[int]) -> Program: def estimate_program_observable_fidelity( program: Program, noise_model: NoiseModelLike, - observable: Union["PauliSum", "PauliTerm"], + observable: "PauliSum" | "PauliTerm", ) -> float: """Estimate program fidelity restricted to the backward light cone of *observable*. diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 5855d17b9..e70853463 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -34,9 +34,8 @@ from __future__ import annotations import logging -from typing import Callable, Dict, List, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Set, Tuple, Union, cast -import jax import jax.numpy as jnp import networkx as nx import quax as qx @@ -45,19 +44,19 @@ from pyquil.api import MemoryMap from pyquil.quil import Program from pyquil.quilatom import MemoryReference -from pyquil.quilbase import Gate, Measurement, Reset, ResetQubit +from pyquil.quilbase import DefCircuit, Gate, Measurement, Reset, ResetQubit from pyquil.noise._channels import ( Channel, CycleChannel, MeasurementChannel, ResetChannel, - get_custom_gates_from_program, get_instruction_unitary, ) from pyquil.noise._noise_model import ( NoiseModelLike, ) +from pyquil.transform import expand_defcircuit_body logger = logging.getLogger(__name__) @@ -67,6 +66,9 @@ # Resolved operations retain the most specific native quax type. ResolvedOp = Tuple[Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument], Tuple[int, ...]] +RecipeOp = Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument] +RecipeCallable = Callable[[Array], RecipeOp] +Recipe = Tuple[Union[RecipeOp, RecipeCallable], Tuple[int, ...]] # Trajectory operations for the state-vector simulator. TrajectoryOp = Tuple[Union[qx.Unitary, qx.KrausMap, qx.QuantumInstrument], Tuple[int, ...]] @@ -142,54 +144,6 @@ def linearize(memory_map: MemoryMap) -> Array: return Linearizer(linearize, n_params=len(param_refs)) -# ══════════════════════════════════════════════════════════ -# Program DAG -# ══════════════════════════════════════════════════════════ - - -def dag_from_program( - program: Program, - qubit_indices: Dict[int, int], -) -> Tuple[nx.DiGraph, List[int]]: - """Build a directed acyclic graph from program instructions. - - Each ``Gate``, ``Measurement``, ``ResetQubit``, or ``Reset`` becomes a node - keyed by its index in the instruction list. Edges encode qubit-level data - dependencies: for each qubit, there is an edge from the previous instruction - that touched it to the current one. - - :param program: Expanded Quil program. - :param qubit_indices: Mapping from physical qubit id → 0-based index. - :return: Tuple of ``(dag, node_order)`` where ``node_order`` is the list of - node keys in instruction order. - """ - dag = nx.DiGraph() - last_on_qubit: Dict[int, int] = {} # qubit_index → last node key - node_order: List[int] = [] - - for idx, inst in enumerate(program.instructions): - if isinstance(inst, Gate): - qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) - elif isinstance(inst, Measurement): - qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) - elif isinstance(inst, ResetQubit): - qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) # type: ignore[union-attr] - elif isinstance(inst, Reset): - qubits = tuple(sorted(qubit_indices.values())) - else: - continue - - dag.add_node(idx, inst=inst, qubits=qubits) - node_order.append(idx) - - for q in qubits: - if q in last_on_qubit: - dag.add_edge(last_on_qubit[q], idx) - last_on_qubit[q] = idx - - return dag, node_order - - # ══════════════════════════════════════════════════════════ # Resolver # ══════════════════════════════════════════════════════════ @@ -237,19 +191,26 @@ def resolver_from_program( noise_model: NoiseModelLike | None, qubit_indices: Dict[int, int], custom_gates: CustomGateMap | None, - dag: nx.DiGraph, - node_order: List[int], -) -> Resolver: - """Build a :class:`Resolver` that maps parameter vectors to operators. +) -> Tuple[Resolver, nx.DiGraph, List[int]]: + """Build a :class:`Resolver`, DAG, and node order from a program. - The returned object accepts a flat parameter vector and produces one + The resolver accepts a flat parameter vector and produces one ``(operator, subsystem)`` pair per DAG node, in ``node_order``. + DEFCIRCUIT expansion is handled internally: + + * If a cycle invocation matches a :class:`CycleChannel` in the noise + model, the cycle is expanded using the channel's constituent operators. + * Otherwise the DEFCIRCUIT body is expanded via qubit/param substitution + and each resulting instruction is resolved individually. + + The DAG is built simultaneously during instruction iteration. + Operators are returned in their most specific native type: * Ideal gates → ``qx.Unitary`` * Noisy gates (``Channel``) → ``qx.SuperOp`` - * Noisy gates (``CycleChannel``) → multiple ``(SuperOp | QuantumInstrument, subsystem)`` + * Expanded cycle gates with ``CycleChannel`` noise → constituent ``qx.SuperOp`` * Measurements → ``qx.QuantumInstrument`` * Noisy resets (``ResetChannel``) → ``qx.SuperOp`` * Ideal resets → ``qx.SuperOp`` @@ -257,21 +218,91 @@ def resolver_from_program( No type conversion (``to_kraus``, ``to_superop``) is performed here; that is the adapter's responsibility. - :param program: Expanded Quil program. + :param program: Quil program (may contain DEFCIRCUITs). :param noise_model: Optional noise model. :param qubit_indices: Mapping from physical qubit id → 0-based index. :param custom_gates: Custom gate definitions. - :param dag: Program dependency DAG. - :param node_order: Node keys in instruction order. - :return: A :class:`Resolver` instance with inferred ``dims``. + :return: Tuple of ``(Resolver, dag, node_order)``. """ measure_regs = _measure_registers(program) + # Extract DEFCIRCUIT definitions. + circuit_definitions: Dict[str, DefCircuit] = {} + for inst in program.instructions: + if isinstance(inst, DefCircuit): + circuit_definitions[inst.name] = inst + + # ── Expand instructions, building DAG and recipes in one pass ── + + dag = nx.DiGraph() + node_order: List[int] = [] + last_on_qubit: Dict[int, int] = {} # qubit_index → last node key + + # Flat lists populated during instruction iteration. + expanded_insts: List[Gate | Measurement | ResetQubit | Reset] = [] + expanded_channels: List[Channel | MeasurementChannel | None] = [] + + def _emit(inst: Gate | Measurement | ResetQubit | Reset, channel: Channel | MeasurementChannel | None = None) -> None: + """Emit an instruction: add a DAG node and record the channel.""" + if isinstance(inst, Gate): + qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) + elif isinstance(inst, Measurement): + qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) + elif isinstance(inst, ResetQubit): + qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) # type: ignore[union-attr] + else: # Reset + qubits = tuple(sorted(qubit_indices.values())) + node_key = len(expanded_insts) + dag.add_node(node_key, inst=inst, qubits=qubits) + node_order.append(node_key) + for q in qubits: + if q in last_on_qubit: + dag.add_edge(last_on_qubit[q], node_key) + last_on_qubit[q] = node_key + expanded_insts.append(inst) + expanded_channels.append(channel) + + def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: + """Look up noise channel for an instruction and emit it.""" + if isinstance(inst, Gate): + ch = noise_model.get_channel(inst) if noise_model is not None else None + if isinstance(ch, CycleChannel): + ch = None + _emit(inst, ch) + elif isinstance(inst, Measurement): + ch = noise_model.get_channel(inst) if noise_model is not None else None + _emit(inst, ch if isinstance(ch, MeasurementChannel) else None) + else: + _emit(inst) + + # ── Main instruction loop ── + + for inst in program.instructions: + if isinstance(inst, DefCircuit): + continue + + if isinstance(inst, Gate) and inst.name in circuit_definitions: + # DEFCIRCUIT invocation — check for CycleChannel. + channel = noise_model.get_channel(inst) if noise_model is not None else None + + if isinstance(channel, CycleChannel): + # Expand using constituent channels. + for sub_ch in channel.channels: + _emit(sub_ch.inst, sub_ch) + else: + # No CycleChannel — expand the DEFCIRCUIT body and resolve individually. + for expanded_inst in expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions): + _lookup_and_emit(expanded_inst) + elif isinstance(inst, (Gate, Measurement, ResetQubit, Reset)): + _lookup_and_emit(inst) + + # ── Build recipes from expanded instructions ── + # Assign parameter vector indices to each gate's MemoryReference params. param_counter = 0 gate_param_indices: Dict[int, List[int]] = {} for idx in node_order: - inst = dag.nodes[idx]["inst"] + inst = expanded_insts[idx] if isinstance(inst, Gate): indices = [] for param in inst.params: @@ -282,26 +313,19 @@ def resolver_from_program( indices.append(-1) gate_param_indices[idx] = indices - # Build a recipe per DAG node. - # Recipe: (op_or_callable, subsystem) where op_or_callable is either a - # pre-computed qx object or a callable(params) -> qx object. - recipes: List[Tuple[object, Tuple[int, ...]]] = [] - # Pre-scan gate instructions to infer per-qudit dimensions. - # This is needed so that MEASURE and RESET use the correct dim. - # Quax doesn't distinguish between ideal and noisy MEASUREs and RESETs by type. - # While an ideal MEASURE should by promoted, a noisy one should be embedded - # We don't know which promotion behaviour to use until we check the noise model. qudit_dims: Dict[int, int] = {} # qubit_index → dimension for node_key in node_order: - inst = dag.nodes[node_key]["inst"] + inst = expanded_insts[node_key] if isinstance(inst, Gate): subsystem = dag.nodes[node_key]["qubits"] - channel = noise_model.get_channel(inst) if noise_model is not None else None + channel = expanded_channels[node_key] + if channel is None and noise_model is not None: + channel = noise_model.get_channel(inst) if channel is not None and isinstance(channel, Channel): op_dims = channel.process.dims[0] elif channel is not None and isinstance(channel, CycleChannel): - continue # CycleChannel dims are per-sub-channel, handled separately + continue else: try: unitary = get_instruction_unitary(inst, custom_gates=custom_gates) @@ -312,17 +336,25 @@ def resolver_from_program( if dim > qudit_dims.get(slot, 2): qudit_dims[slot] = dim + recipes: List[Recipe] = [] + for node_key in node_order: - inst = dag.nodes[node_key]["inst"] + inst = expanded_insts[node_key] subsystem = dag.nodes[node_key]["qubits"] match inst: case Gate(): - channel = None - if noise_model is not None: + channel = expanded_channels[node_key] + if channel is None and noise_model is not None: channel = noise_model.get_channel(inst) - if _is_parameterized(inst): + if channel is not None and isinstance(channel, Channel): + recipes.append((channel.process, subsystem)) + elif channel is not None and isinstance(channel, MeasurementChannel): + raise ValueError(f"MeasurementChannel cannot be applied to expanded gate {inst}.") + elif channel is not None and isinstance(channel, CycleChannel): + raise ValueError(f"CycleChannel for {inst.name} was not expanded before resolver construction.") + elif _is_parameterized(inst): gate_name = inst.name if custom_gates is not None and gate_name in custom_gates: gate_def = custom_gates[gate_name] @@ -334,7 +366,9 @@ def resolver_from_program( cparams = list(inst.params) def _make_param_recipe( - gdef: object, cp: list, pi: List[int], + gdef: object, + cp: list, + pi: List[int], ) -> Callable[[Array], qx.Unitary]: def recipe(params: Array) -> qx.Unitary: resolved = [] @@ -345,33 +379,26 @@ def recipe(params: Array) -> qx.Unitary: resolved.append(float(p.real) if hasattr(p, "real") else float(p)) result = gdef(*resolved) if callable(gdef) else gdef # type: ignore[operator] if not isinstance(result, qx.Unitary): + result = cast(Any, result) result = qx.Unitary.from_matrix(result.matrix, result.dims) return result + return recipe recipes.append((_make_param_recipe(gate_def, cparams, pidx), subsystem)) - elif channel is not None and isinstance(channel, Channel): - # Channel.process is a SuperOp that includes the gate unitary - recipes.append((channel.process, subsystem)) - elif channel is not None and isinstance(channel, CycleChannel): - # CycleChannel: decompose into constituent channel recipes - for sub_ch in channel.channels: - sub_qubits = tuple(qubit_indices[q] for q in sub_ch.qubits) - if isinstance(sub_ch, Channel): - recipes.append((sub_ch.process, sub_qubits)) - elif isinstance(sub_ch, MeasurementChannel): - recipes.append((sub_ch.process, sub_qubits)) else: unitary = get_instruction_unitary(inst, custom_gates=custom_gates) recipes.append((unitary, subsystem)) case Measurement(): - meas_channel = None - if noise_model is not None: + meas_channel = expanded_channels[node_key] + if meas_channel is None and noise_model is not None: meas_channel = noise_model.get_channel(inst) if meas_channel is not None and isinstance(meas_channel, MeasurementChannel): recipes.append((meas_channel.process, subsystem)) + elif meas_channel is not None and isinstance(meas_channel, Channel): + raise ValueError(f"Channel cannot be applied to expanded measurement {inst}.") else: dim = qudit_dims.get(subsystem[0], 2) recipes.append((qx.gates.MEASURE(dim=dim), subsystem)) @@ -394,17 +421,17 @@ def recipe(params: Array) -> qx.Unitary: def resolve(params: Array) -> List[ResolvedOp]: ops: List[ResolvedOp] = [] for op_or_fn, subsystem in recipes: - if callable(op_or_fn) and not isinstance(op_or_fn, (qx.Unitary, qx.KrausMap, qx.SuperOp, qx.QuantumInstrument)): - ops.append((op_or_fn(params), subsystem)) + if isinstance(op_or_fn, (qx.Unitary, qx.KrausMap, qx.SuperOp, qx.QuantumInstrument)): + ops.append((op_or_fn, subsystem)) else: - ops.append((op_or_fn, subsystem)) # type: ignore[arg-type] + ops.append((op_or_fn(params), subsystem)) return ops # Compute per-qudit dimensions from the pre-scan. n_qubits = len(qubit_indices) dims = tuple(qudit_dims.get(i, 2) for i in range(n_qubits)) - return Resolver(resolve, dims=dims) + return Resolver(resolve, dims=dims), dag, node_order # ══════════════════════════════════════════════════════════ @@ -647,8 +674,13 @@ def _is_mergeable(node_key: int) -> bool: logger.info( "Compressor: %d ops → %d groups (ratio=%.2f), " "%d merged groups, avg_subsystem=%.2f, max_subsystem=%d, max_subsystem_size=%d", - n_original, n_groups, n_groups / n_original if n_original else 1.0, - n_multi, avg_subsystem, max_sub, max_subsystem_size, + n_original, + n_groups, + n_groups / n_original if n_original else 1.0, + n_multi, + avg_subsystem, + max_sub, + max_subsystem_size, ) # --- Build compress closure --- @@ -659,8 +691,7 @@ def compress(ops: List[ResolvedOp]) -> List[ResolvedOp]: idx = node_key_to_idx[nodes[0]] result.append(ops[idx]) else: - group_ops = [(ops[node_key_to_idx[nk]][0], ops[node_key_to_idx[nk]][1]) - for nk in nodes] + group_ops = [(ops[node_key_to_idx[nk]][0], ops[node_key_to_idx[nk]][1]) for nk in nodes] merged = _merge_ops(group_ops, subsystem, dims) result.append(merged) return result diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 2302e093a..405de2f0e 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -42,45 +42,24 @@ from jax import Array from pyquil.api import MemoryMap +from pyquil.noise._channels import get_custom_gates_from_program +from pyquil.noise._noise_model import NoiseModelLike from pyquil.quil import Program from pyquil.quilbase import Measurement, Reset, ResetQubit - -from pyquil.noise._noise_model import NoiseModelLike -from pyquil.noise._channels import CycleChannel, get_custom_gates_from_program - -from pyquil.transform import expand_defcircuits - from pyquil.simulation._resolver import ( - Linearizer, - Resolver, ResolvedOp, TrajectoryOp, - DensityMatrixOp, adapt_for_density_matrix, adapt_for_trajectory, compressor_from_dag, linearizer_from_program, - dag_from_program, resolver_from_program, ) +from pyquil.transform import expand_defcircuits logger = logging.getLogger(__name__) -def _get_cycle_channel_names(noise_model: NoiseModelLike | None) -> frozenset: - """Extract DefCircuit names from CycleChannels in the noise model.""" - if noise_model is None: - return frozenset() - from pyquil.noise._noise_model import NoiseModel - if isinstance(noise_model, NoiseModel): - names = frozenset( - ch.inst.name for ch in noise_model.channels - if isinstance(ch, CycleChannel) - ) - return names - return frozenset() - - # ══════════════════════════════════════════════════════════ # Base class # ══════════════════════════════════════════════════════════ @@ -108,31 +87,24 @@ def __init__( noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 0, ) -> None: - # Only expand DefCircuits that don't correspond to CycleChannels in the - # noise model. CycleChannels are keyed by the cycle Gate instruction, - # so expanding their DefCircuit would destroy the match. - cycle_names = _get_cycle_channel_names(noise_model) - if cycle_names: - program = expand_defcircuits(program, expand_names_except=cycle_names) - else: - program = expand_defcircuits(program) - self._validate(program) + expanded_program = expand_defcircuits(program) + self._validate(expanded_program) if qubits is None: - qubits = sorted(program.get_qubit_indices()) + qubits = sorted(expanded_program.get_qubit_indices()) self.qubits = qubits self.n_qubits = len(qubits) qubit_indices = {q: i for i, q in enumerate(qubits)} custom_gates = get_custom_gates_from_program(program) - self._linearize_fn = linearizer_from_program(program) - - dag, node_order = dag_from_program(program, qubit_indices) + self._linearize_fn = linearizer_from_program(expanded_program) - self._resolve_fn = resolver_from_program( - program, noise_model, qubit_indices, custom_gates or None, - dag, node_order, + self._resolve_fn, dag, node_order = resolver_from_program( + program, + noise_model, + qubit_indices, + custom_gates or None, ) # Dims are inferred during resolver construction from gate/channel inspection. @@ -177,7 +149,7 @@ class PureStateVectorSimulator(ProgramSimulator): sim = PureStateVectorSimulator(program) params = sim.linearize(memory_map) psi = jax.jit(sim.compute)(params) - U = jax.jit(sim.unitary)(params) + U = jax.jit(sim.unitary)(params) """ __slots__ = ("_psi0",) @@ -195,15 +167,9 @@ def __init__( def _validate(self, program: Program) -> None: for inst in program.instructions: if isinstance(inst, Measurement): - raise ValueError( - "PureStateVectorSimulator does not support measurements. " - f"Found: {inst}" - ) + raise ValueError(f"PureStateVectorSimulator does not support measurements. Found: {inst}") if isinstance(inst, (Reset, ResetQubit)): - raise ValueError( - "PureStateVectorSimulator does not support resets. " - f"Found: {inst}" - ) + raise ValueError(f"PureStateVectorSimulator does not support resets. Found: {inst}") def compute(self, params: Array) -> qx.StateVector: """Compute the final state vector. @@ -242,7 +208,7 @@ def unitary(self, params: Array) -> qx.Unitary: d = 1 for dim in self.dims: d *= dim - return qx.Unitary.from_matrix(jnp.eye(d, dtype=complex), self.dims) + return qx.Unitary.from_matrix(jnp.eye(d, dtype=complex), (self.dims, self.dims)) return accumulated @@ -390,8 +356,13 @@ def sample( operations = self.adapt(compressed) _, all_outcomes = _run_batched_trajectories( - operations, self.n_qubits, num_trajectories, batch_size, random_seed, - keep_states=False, dims=self.dims, + operations, + self.n_qubits, + num_trajectories, + batch_size, + random_seed, + keep_states=False, + dims=self.dims, ) if len(all_outcomes) == 1: @@ -427,10 +398,7 @@ def _apply_trajectory_operations( """ measurement_outcomes: List[Array] = [] - n_stochastic = sum( - 1 for op, _ in operations - if isinstance(op, (qx.KrausMap, qx.QuantumInstrument)) - ) + n_stochastic = sum(1 for op, _ in operations if isinstance(op, (qx.KrausMap, qx.QuantumInstrument))) ensemble_size = psi.ensemble_size @@ -505,13 +473,17 @@ def _run_batched_trajectories( if this_batch == 1: psi_out = qx.StateVector.from_matrix( - psi_out.matrix[jnp.newaxis], psi_out.dims, + psi_out.matrix[jnp.newaxis], + psi_out.dims, ) outcomes = outcomes[jnp.newaxis] logger.debug( "Batch %d: %d trajectories, %d qubits, %.3f s", - batch_idx, this_batch, n_qubits, t1 - t0, + batch_idx, + this_batch, + n_qubits, + t1 - t0, ) if keep_states: @@ -521,10 +493,13 @@ def _run_batched_trajectories( batch_idx += 1 logger.info( - "Trajectories complete: %d total, %d batches (size=%d), " - "n_qubits=%d, %.3f s total, %.1f traj/s", - num_trajectories, batch_idx, batch_size, n_qubits, - t_total, num_trajectories / t_total if t_total > 0 else float("inf"), + "Trajectories complete: %d total, %d batches (size=%d), n_qubits=%d, %.3f s total, %.1f traj/s", + num_trajectories, + batch_idx, + batch_size, + n_qubits, + t_total, + num_trajectories / t_total if t_total > 0 else float("inf"), ) return (all_psis if keep_states else None), all_outcomes diff --git a/pyquil/transform.py b/pyquil/transform.py index 3a6460d57..5eb427c8d 100644 --- a/pyquil/transform.py +++ b/pyquil/transform.py @@ -8,12 +8,12 @@ from __future__ import annotations from copy import deepcopy -from typing import AbstractSet, List, Optional +from typing import Dict, Iterator, List, Optional, Union from pyquil.api import MemoryMap from pyquil.quil import Program from pyquil.quilatom import MemoryReference, substitute -from pyquil.quilbase import Declare, DefCircuit, Gate, Measurement, ResetQubit +from pyquil.quilbase import Declare, DefCircuit, Gate, Measurement, Reset, ResetQubit from quil.instructions import CircuitDefinition from quil.instructions import Instruction as QuilInstruction from quil.program import Program as QuilProgram @@ -107,12 +107,53 @@ def unparameterize(program: Program, memory_map: MemoryMap) -> Program: return unparameterized_program +def expand_defcircuit_body( + inst: Gate, + defcircuit: DefCircuit, + circuit_definitions: Dict[str, DefCircuit], +) -> Iterator[Union[Gate, Measurement, ResetQubit, Reset]]: + """Yield concrete instructions from a DEFCIRCUIT invocation. + + Substitutes formal qubit/parameter arguments with the concrete values + from ``inst``. Handles nested DEFCIRCUITs via recursion. + + :param inst: The Gate that invokes the DEFCIRCUIT. + :param defcircuit: The DefCircuit definition to expand. + :param circuit_definitions: All known DEFCIRCUIT definitions (for nested expansion). + :yields: Concrete instructions with physical qubits and resolved parameters. + """ + qarg_to_arg_map = {qarg: q for q, qarg in zip(inst.qubits, defcircuit.qubit_variables)} + parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters)} + + for circuit_inst in defcircuit.instructions: + if isinstance(circuit_inst, Gate): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] + if hasattr(circuit_inst, "params"): + circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] + if circuit_inst.name in circuit_definitions: + yield from expand_defcircuit_body( + circuit_inst, circuit_definitions[circuit_inst.name], circuit_definitions + ) + else: + yield circuit_inst + elif isinstance(circuit_inst, Measurement): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] + yield circuit_inst + elif isinstance(circuit_inst, ResetQubit): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] + yield circuit_inst + else: + yield deepcopy(circuit_inst) + + def expand_defcircuits( program: Program, expand_if_defcal: bool = True, calibration_program: Optional[Program] = None, keep_defcircuits: bool = False, - expand_names_except: AbstractSet[str] = frozenset(), ) -> Program: """Expand DEFCIRCUITS into individual instructions. @@ -121,8 +162,6 @@ def expand_defcircuits( :param calibration_program: Calibrations to supplement those in ``program``. Existing calibrations in ``program`` take precedence. :param keep_defcircuits: If True, keep the DEFCIRCUIT definitions in the returned program. - :param expand_names_except: Set of DefCircuit names to skip during expansion. - Instructions matching these names are left unexpanded. :return: A Quil program, with any Circuit instructions expanded to individual instructions. """ instructions: List = [] @@ -146,66 +185,25 @@ def expand_defcircuits( if len(circuit_definitions) == 0 and len(instructions) == 0: return expanded_program - def _expand_instruction(inst: Gate) -> List: - instruction_name = inst.name - expanded_instructions: List = [] + def _should_expand(inst: Gate) -> bool: + name = inst.name + if name not in circuit_definitions: + return False + defcircuit = circuit_definitions[name] + qubits = tuple(int(q) for q in inst.get_qubit_indices()) + if len(qubits) != len(defcircuit.qubit_variables) or len(inst.params) != len(defcircuit.parameters): + return False if expand_if_defcal is False: - cal = holistic_calibration_program.get_calibration(inst) - if cal is not None: - return [inst] - - defcircuit = circuit_definitions[instruction_name] - qubit_variables = defcircuit.qubit_variables - qubits = inst.qubits - - qarg_to_arg_map = {qarg: q for q, qarg in zip(qubits, qubit_variables)} - parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters)} - - for circuit_inst in defcircuit.instructions: - match circuit_inst: - case Gate(): - circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] - if hasattr(circuit_inst, "params"): - circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] - if circuit_inst.name in circuit_definitions: - expanded_instructions += _expand_instruction(circuit_inst) - else: - expanded_instructions.append(circuit_inst) - case Measurement(): - circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] - expanded_instructions.append(circuit_inst) - case ResetQubit(): - circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] - expanded_instructions.append(circuit_inst) - case _: - expanded_instructions.append(deepcopy(circuit_inst)) - return expanded_instructions + if holistic_calibration_program.get_calibration(inst) is not None: + return False + if program.get_calibration(inst) is not None: + return False + return True expanded_instructions: List = [] for inst in instructions: - if isinstance(inst, Gate): - instruction_name = inst.name - if instruction_name in expand_names_except: - expanded_instructions.append(inst) - continue - qubits = tuple(int(q) for q in inst.get_qubit_indices()) - if ( - (instruction_name in circuit_definitions) - and len(qubits) == len(circuit_definitions[instruction_name].qubit_variables) - and len(inst.params) == len(circuit_definitions[instruction_name].parameters) - ): - if expand_if_defcal is False: - cal = program.get_calibration(inst) - if cal is not None: - expanded_instructions.append(inst) - continue - - expanded_instructions += _expand_instruction(inst) - else: - expanded_instructions.append(inst) + if isinstance(inst, Gate) and _should_expand(inst): + expanded_instructions.extend(expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions)) else: expanded_instructions.append(inst) diff --git a/test/unit/data/surface_17_depth_5_no_reset.quil b/test/benchmarks/fixtures/surface_17_depth_5_no_reset.quil similarity index 100% rename from test/unit/data/surface_17_depth_5_no_reset.quil rename to test/benchmarks/fixtures/surface_17_depth_5_no_reset.quil diff --git a/test/benchmarks/test_state_vector.py b/test/benchmarks/test_state_vector.py new file mode 100644 index 000000000..82a058896 --- /dev/null +++ b/test/benchmarks/test_state_vector.py @@ -0,0 +1,420 @@ +"""Benchmarks for the quax-backed state vector and trajectory simulators.""" + +from pathlib import Path + +import jax + +jax.config.update("jax_enable_x64", True) + +import numpy as np +import pytest +import quax as qx + +from pyquil.gates import CNOT, RX, RZ +from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel +from pyquil.noise._noise_model import NoiseModel +from pyquil.quil import Program +from pyquil.quilbase import DefCircuit, ResetQubit +from pyquil.quilbase import Gate as QuilGate +from pyquil.quilbase import Measurement as QuilMeasurement +from pyquil.quilbase import Reset as QuilReset +from pyquil.simulation._simulator import ( + TrajectorySimulator, +) +from pyquil.simulation._simulator import ( + _apply_trajectory_operations as apply_trajectory_operations, +) + +_EMPTY_PARAMS = np.array([], dtype=float) +_FIXTURES_DIR = Path(__file__).parent / "fixtures" +_SURFACE17_FIXTURE = _FIXTURES_DIR / "surface_17_depth_5_no_reset.quil" +_SURFACE17_QUBITS = (65, 66, 74, 75, 76, 77, 82, 83, 84, 85, 86, 91, 92, 93, 94, 102, 103) +_SURFACE17_CYCLES = { + "SZ_INIT", + "SX_INIT", + "CZ_0", + "SZ_DATA", + "SX_DATA", + "CZ_1", + "CZ_2", + "CZ_3", + "SZ_ANCILLA", + "SX_ANCILLA_ECHO", + "MEASURE_ANCILLA", + "MEASURE_ALL", +} +_DEFAULT_NUM_QUBITS = 15 +_DEFAULT_NUM_LAYERS = 10 +_DEFAULT_NUM_TRAJECTORIES = 128 +_DEFAULT_BATCH_SIZE = 32 +_DEFAULT_MAX_SUBSYSTEM_SIZE = 1 + + +def _build_noisy_program_and_model(num_qubits, num_layers, seed=4867): + """Build a layered noisy circuit and matching noise model for shared scaling benchmarks.""" + edges_0 = [(i, i + 1) for i in range(0, num_qubits - 1, 2)] + edges_1 = [(i, i + 1) for i in range(1, num_qubits - 1, 2)] + rng = np.random.default_rng(seed) + + t1s, t2s = {}, {} + for q in range(num_qubits): + t1 = np.clip(rng.normal(30, 10), 10, 50) + t2 = np.clip(rng.normal(30, 20), 5, 2 * t1) + t1s[q], t2s[q] = t1, t2 + + channels = [ + Channel.from_coherence_times( + CNOT(*edge), gate_duration=0.1, t1s=[t1s[q] for q in edge], t2s=[t2s[q] for q in edge] + ) + for edge in edges_0 + edges_1 + ] + [ + Channel.from_coherence_times(RX(np.pi / 2, q), gate_duration=0.04, t1s=[t1s[q]], t2s=[t2s[q]]) + for q in range(num_qubits) + ] + noise_model = NoiseModel(channels=channels) + + program = Program() + for _ in range(num_layers): + for edges in [edges_0, edges_1]: + program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] + program += [RX(np.pi / 2, idx) for idx in range(num_qubits)] + program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] + program += [RX(np.pi / 2, idx) for idx in range(num_qubits)] + program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] + program += [CNOT(*edge) for edge in edges] + + return program, noise_model + + +def _surface17_defcircuits(program): + return {inst.name: inst for inst in program.instructions if isinstance(inst, DefCircuit)} + + +def _surface17_program_variant(variant="full"): + program = Program(_SURFACE17_FIXTURE.read_text()) + if variant == "full": + return program + + if variant == "first_measurement": + prefix_program = Program() + for inst in program.instructions: + prefix_program += inst + if isinstance(inst, QuilGate) and inst.name == "MEASURE_ANCILLA": + break + return prefix_program + + if variant == "no_measurements": + keep_gates, keep_measurements = True, False + elif variant == "measurements_only": + keep_gates, keep_measurements = False, True + else: + raise ValueError(f"Unknown surface-17 variant: {variant}") + + original_defcircuits = _surface17_defcircuits(program) + filtered_defcircuits = {} + filtered_program = Program() + + for inst in program.instructions: + if isinstance(inst, DefCircuit): + instructions = [ + cycle_inst + for cycle_inst in inst.instructions + if (keep_gates and isinstance(cycle_inst, QuilGate)) + or (keep_measurements and isinstance(cycle_inst, QuilMeasurement)) + ] + if instructions: + defcircuit = DefCircuit(inst.name, inst.parameters, inst.qubit_variables, instructions) + filtered_defcircuits[inst.name] = defcircuit + filtered_program += defcircuit + elif isinstance(inst, QuilGate) and inst.name in original_defcircuits: + if inst.name in filtered_defcircuits: + filtered_program += inst + else: + filtered_program += inst + + return filtered_program + + +def _build_surface17_cycle_noise_model(program, depolarizing_constant=0.99, readout_fidelity=1.0): + defcircuits = _surface17_defcircuits(program) + cycle_channels = [] + + for inst in program.instructions: + if not isinstance(inst, QuilGate) or inst.name not in defcircuits: + continue + + defcircuit = defcircuits[inst.name] + qubit_map = dict(zip(defcircuit.qubit_variables, inst.qubits)) + channels = [] + + for cycle_inst in defcircuit.instructions: + if isinstance(cycle_inst, QuilGate): + concrete_gate = QuilGate( + cycle_inst.name, + list(cycle_inst.params), + [qubit_map[qubit] for qubit in cycle_inst.qubits], + ) + channels.append(Channel.from_depolarizing_constant(concrete_gate, depolarizing_constant)) + elif isinstance(cycle_inst, QuilMeasurement): + concrete_measurement = QuilMeasurement(qubit=qubit_map[cycle_inst.qubit], classical_reg=None) + channels.append( + MeasurementChannel.from_readout_fidelity(concrete_measurement, fidelity=readout_fidelity) + ) + + cycle_channels.append(CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels))) + + return NoiseModel(channels=cycle_channels) + + +def _prepare_trajectory_operations(program, noise_model, max_subsystem_size=0): + sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=max_subsystem_size) + params = sim.linearize({}) + resolved = sim.resolve(params) + compressed = sim.compress(resolved) + operations = sim.adapt(compressed) + return sim, resolved, compressed, operations + + +def _operation_counts(operations): + return { + "unitary_ops": sum(1 for op, _ in operations if isinstance(op, qx.Unitary)), + "kraus_ops": sum(1 for op, _ in operations if isinstance(op, qx.KrausMap)), + "instrument_ops": sum(1 for op, _ in operations if isinstance(op, qx.QuantumInstrument)), + } + + +def _record_counts(benchmark, resolved, compressed, operations): + if hasattr(benchmark, "extra_info"): + benchmark.extra_info.update( + { + "resolved_ops": len(resolved), + "compressed_ops": len(compressed), + "trajectory_ops": len(operations), + **_operation_counts(operations), + } + ) + + +def _block_until_ready(matrix, outcomes): + matrix.block_until_ready() + outcomes.block_until_ready() + + +def _benchmark_trajectory_operations( + benchmark, + sim, + operations, + *, + num_trajectories, + batch_size, + random_seed=0, + use_jit=False, +): + if use_jit: + + def apply_matrix(matrix, key): + psi = qx.StateVector.from_matrix(matrix, sim.dims) + psi_out, outcomes = apply_trajectory_operations(operations, psi, key) + return psi_out.matrix, outcomes + + apply_batch = jax.jit(apply_matrix) + else: + + def apply_batch(matrix, key): + psi = qx.StateVector.from_matrix(matrix, sim.dims) + psi_out, outcomes = apply_trajectory_operations(operations, psi, key) + return psi_out.matrix, outcomes + + warmup_psi = qx.zero_state_vector(dims=sim.dims, ensemble_size=(batch_size,)) + _block_until_ready(*apply_batch(warmup_psi.matrix, jax.random.key(random_seed))) + + def thunk(): + key = jax.random.key(random_seed) + remaining = num_trajectories + while remaining > 0: + this_batch = min(remaining, batch_size) + key, batch_key = jax.random.split(key) + psi = qx.zero_state_vector(dims=sim.dims, ensemble_size=(this_batch,)) + _block_until_ready(*apply_batch(psi.matrix, batch_key)) + remaining -= this_batch + + benchmark.pedantic(thunk, iterations=1, rounds=1) + + +def _run_perf_benchmark( + benchmark, + num_qubits=_DEFAULT_NUM_QUBITS, + num_layers=_DEFAULT_NUM_LAYERS, + num_trajectories=_DEFAULT_NUM_TRAJECTORIES, + batch_size=_DEFAULT_BATCH_SIZE, + max_subsystem_size=_DEFAULT_MAX_SUBSYSTEM_SIZE, +): + program, noise_model = _build_noisy_program_and_model(num_qubits, num_layers) + sim, resolved, compressed, operations = _prepare_trajectory_operations(program, noise_model, max_subsystem_size) + _record_counts(benchmark, resolved, compressed, operations) + _benchmark_trajectory_operations( + benchmark, + sim, + operations, + num_trajectories=num_trajectories, + batch_size=batch_size, + ) + + +def _run_surface17_benchmark( + benchmark, + *, + variant="full", + num_trajectories=128, + batch_size=16, + max_subsystem_size=2, + depolarizing_constant=0.99, + readout_fidelity=1.0, + use_jit=False, +): + program = _surface17_program_variant(variant) + noise_model = _build_surface17_cycle_noise_model( + program, + depolarizing_constant=depolarizing_constant, + readout_fidelity=readout_fidelity, + ) + sim, resolved, compressed, operations = _prepare_trajectory_operations(program, noise_model, max_subsystem_size) + _record_counts(benchmark, resolved, compressed, operations) + if hasattr(benchmark, "extra_info"): + benchmark.extra_info.update( + { + "variant": variant, + "num_trajectories": num_trajectories, + "batch_size": batch_size, + "max_subsystem_size": max_subsystem_size, + "use_jit": use_jit, + } + ) + _benchmark_trajectory_operations( + benchmark, + sim, + operations, + num_trajectories=num_trajectories, + batch_size=batch_size, + use_jit=use_jit, + ) + + +def test_surface17_fixture_structure(): + program = Program(_SURFACE17_FIXTURE.read_text()) + defcircuits = {inst.name: inst for inst in program.instructions if isinstance(inst, DefCircuit)} + invocations = [inst for inst in program.instructions if isinstance(inst, QuilGate)] + invocation_names = [inst.name for inst in invocations] + + assert _SURFACE17_FIXTURE.exists() + assert set(defcircuits) == _SURFACE17_CYCLES + assert set(program.get_qubit_indices()) == set(_SURFACE17_QUBITS) + assert not any(isinstance(inst, (QuilReset, ResetQubit)) for inst in program.instructions) + assert invocation_names.count("MEASURE_ANCILLA") == 4 + assert invocation_names[-1] == "MEASURE_ALL" + + +def test_surface17_cycle_noise_model_preserves_measurements(): + program = Program(_SURFACE17_FIXTURE.read_text()) + noise_model = _build_surface17_cycle_noise_model(program, depolarizing_constant=1.0) + sim, _, _, _ = _prepare_trajectory_operations(program, noise_model, max_subsystem_size=0) + resolved = sim.resolve(_EMPTY_PARAMS) + + assert sum(1 for op, _ in resolved if isinstance(op, qx.QuantumInstrument)) == 49 + + +@pytest.mark.parametrize("variant", ["full", "no_measurements", "measurements_only"]) +def test_surface17_cycle_noise_compression_preserves_instruments(variant): + program = _surface17_program_variant(variant) + noise_model = _build_surface17_cycle_noise_model(program) + counts = [] + + for max_subsystem_size in (0, 1, 2): + _, resolved, compressed, operations = _prepare_trajectory_operations(program, noise_model, max_subsystem_size) + counts.append((len(resolved), len(compressed), len(operations), _operation_counts(operations))) + + assert counts[0][0] == counts[1][0] == counts[2][0] + if variant == "measurements_only": + assert counts[0] == counts[1] == counts[2] + else: + assert counts[2][1] < counts[1][1] < counts[0][1] + assert counts[0][3]["instrument_ops"] == counts[1][3]["instrument_ops"] == counts[2][3]["instrument_ops"] + + +class TestPerformance: + """Trajectory simulator performance benchmarks.""" + + @pytest.mark.parametrize( + "num_qubits", + [ + pytest.param(3, id="3q"), + pytest.param(6, id="6q"), + pytest.param(9, id="9q"), + pytest.param(12, id="12q"), + pytest.param(15, id="15q"), + ], + ) + def test_scaling_qubits(self, benchmark, num_qubits): + _run_perf_benchmark(benchmark, num_qubits=num_qubits) + + @pytest.mark.parametrize( + "num_layers", + [ + pytest.param(1, id="1L"), + pytest.param(3, id="3L"), + pytest.param(10, id="10L"), + pytest.param(20, id="20L"), + ], + ) + def test_scaling_depth(self, benchmark, num_layers): + _run_perf_benchmark(benchmark, num_layers=num_layers) + + @pytest.mark.parametrize( + "batch_size", + [ + pytest.param(8, id="b8"), + pytest.param(16, id="b16"), + pytest.param(64, id="b64"), + ], + ) + def test_scaling_batch_size(self, benchmark, batch_size): + _run_perf_benchmark(benchmark, batch_size=batch_size) + + @pytest.mark.parametrize( + "max_subsystem_size", + [ + pytest.param(0, id="s0"), + pytest.param(1, id="s1"), + ], + ) + def test_scaling_subsystem_size(self, benchmark, max_subsystem_size): + _run_perf_benchmark(benchmark, max_subsystem_size=max_subsystem_size) + + @pytest.mark.parametrize( + "batch_size", + [ + pytest.param(8, id="b8"), + pytest.param(16, id="b16"), + pytest.param(64, id="b64"), + ], + ) + def test_17q_batch_size(self, benchmark, batch_size): + _run_perf_benchmark(benchmark, num_qubits=17, batch_size=batch_size) + + def test_surface17_depth5_cycle_noise(self, benchmark): + _run_surface17_benchmark(benchmark) + + def test_surface17_depth5_cycle_noise_low_trajectory(self, benchmark): + _run_surface17_benchmark(benchmark, num_trajectories=4, batch_size=4) + + def test_surface17_depth5_cycle_noise_micro(self, benchmark): + _run_surface17_benchmark(benchmark, variant="first_measurement", num_trajectories=1, batch_size=1) + + def test_surface17_depth5_cycle_noise_micro_jit(self, benchmark): + _run_surface17_benchmark(benchmark, variant="first_measurement", num_trajectories=1, batch_size=1, use_jit=True) + + def test_surface17_depth5_cycle_noise_no_measurements_micro(self, benchmark): + _run_surface17_benchmark(benchmark, variant="no_measurements", num_trajectories=4, batch_size=4) + + def test_surface17_depth5_cycle_noise_measurements_only_micro(self, benchmark): + _run_surface17_benchmark(benchmark, variant="measurements_only", num_trajectories=4, batch_size=4) diff --git a/test/unit/test_noise_model.py b/test/unit/test_noise_model.py index 09e77bc4b..b6767b833 100644 --- a/test/unit/test_noise_model.py +++ b/test/unit/test_noise_model.py @@ -170,14 +170,14 @@ def test_qubits(self): class TestNoiseModel: def test_empty_model(self): """An empty NoiseModel has no channels.""" - nm = NoiseModel(channels=frozenset()) + nm = NoiseModel(channels=()) assert nm.get_channel(RX(0.5, 0)) is None def test_get_channel_gate(self): """NoiseModel.get_channel returns the correct Channel for a gate.""" inst = RX(np.pi / 4, 0) ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) - nm = NoiseModel(channels=frozenset([ch])) + nm = NoiseModel(channels=[ch]) retrieved = nm.get_channel(inst) assert retrieved is ch @@ -185,7 +185,7 @@ def test_get_channel_returns_none_for_missing(self): """get_channel returns None for instructions not in the model.""" inst = RX(np.pi / 4, 0) ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) - nm = NoiseModel(channels=frozenset([ch])) + nm = NoiseModel(channels=[ch]) other_inst = RY(np.pi / 2, 1) assert nm.get_channel(other_inst) is None @@ -197,7 +197,7 @@ def test_multiple_channels(self): ch1 = Channel.from_depolarizing_constant(inst=inst1, depolarizing_constant=0.99) ch2 = Channel.from_depolarizing_constant(inst=inst2, depolarizing_constant=0.97) ch3 = Channel.from_depolarizing_constant(inst=inst3, depolarizing_constant=0.95) - nm = NoiseModel(channels=frozenset([ch1, ch2, ch3])) + nm = NoiseModel(channels=[ch1, ch2, ch3]) assert nm.get_channel(inst1) is ch1 assert nm.get_channel(inst2) is ch2 assert nm.get_channel(inst3) is ch3 @@ -260,7 +260,7 @@ def test_ideal_reset_maps_excited_to_ground(self): """An ideal reset on an excited qubit should produce |0><0|.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=frozenset([ch])) + noise_model = NoiseModel(channels=[ch]) # Prepare |1> then reset program = Program(X(0), RESET(0)) rho = _dm(program, noise_model=noise_model) @@ -271,7 +271,7 @@ def test_ideal_reset_maps_superposition_to_ground(self): """An ideal reset on a superposition state should produce |0><0|.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=frozenset([ch])) + noise_model = NoiseModel(channels=[ch]) # Prepare |+> then reset program = Program(RX(np.pi / 2, 0), RESET(0)) rho = _dm(program, noise_model=noise_model) @@ -282,7 +282,7 @@ def test_noisy_reset_reduces_fidelity(self): """A noisy reset should produce a state with fidelity < 1 relative to |0><0|.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=0.90) - noise_model = NoiseModel(channels=frozenset([ch])) + noise_model = NoiseModel(channels=[ch]) program = Program(X(0), RESET(0)) rho = _dm(program, noise_model=noise_model) target_rho = qx.zero_state_matrix(1) @@ -294,7 +294,7 @@ def test_reset_in_multi_qubit_circuit(self): """Reset on one qubit should not affect the other qubit.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=frozenset([ch])) + noise_model = NoiseModel(channels=[ch]) # Prepare |11> then reset qubit 0 program = Program(X(0), X(1), RESET(0)) rho = _dm(program, noise_model=noise_model) diff --git a/test/unit/test_qutrit_simulation.py b/test/unit/test_qutrit_simulation.py index b92454ab0..2468902d1 100644 --- a/test/unit/test_qutrit_simulation.py +++ b/test/unit/test_qutrit_simulation.py @@ -313,7 +313,7 @@ def test_qutrit_noisy_measurement_channel(self): meas_ch = MeasurementChannel.from_readout_fidelity( inst=meas_inst, fidelity=0.9, dim=3 ) - noise_model = NoiseModel(channels=frozenset([meas_ch])) + noise_model = NoiseModel(channels=[meas_ch]) # Prepare |2> (TX|0>=|2>) and measure with noise p = Program() @@ -452,7 +452,7 @@ def test_qutrit_depolarizing_channel(self): """A depolarizing channel on a qutrit gate mixes the state.""" inst = Gate("TX", [], [0]) channel = Channel.from_gate_fidelity(inst=inst, fidelity=0.8) - noise_model = NoiseModel(channels=frozenset([channel])) + noise_model = NoiseModel(channels=[channel]) # Density matrix should show mixed state p = Program(Gate("TX", [], [0])) @@ -466,7 +466,7 @@ def test_qutrit_depolarizing_trajectory(self): """Trajectory simulation with qutrit depolarizing noise.""" inst = Gate("TX", [], [0]) channel = Channel.from_gate_fidelity(inst=inst, fidelity=0.9) - noise_model = NoiseModel(channels=frozenset([channel])) + noise_model = NoiseModel(channels=[channel]) p = Program() p += Gate("TX", [], [0]) @@ -487,7 +487,7 @@ def test_qutrit_reset_channel(self): reset_inst = ResetQubit(Qubit(0)) reset_ch = ResetChannel.from_reset_fidelity(inst=reset_inst, fidelity=0.9, dim=3) - noise_model = NoiseModel(channels=frozenset([reset_ch])) + noise_model = NoiseModel(channels=[reset_ch]) # Prepare |1> (TX^2|0>=|1>), then reset — should mostly go to |0> p = Program() @@ -515,7 +515,7 @@ def test_mixed_noise_qubit_and_qutrit(self): ch_qutrit = Channel.from_gate_fidelity( inst=Gate("TX", [], [1]), fidelity=0.95 ) - noise_model = NoiseModel(channels=frozenset([ch_qubit, ch_qutrit])) + noise_model = NoiseModel(channels=[ch_qubit, ch_qutrit]) p = Program() p += X(0) diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index 1e03e0795..624991298 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -1,7 +1,5 @@ """Unit tests for the quax-based state vector simulator.""" -from pathlib import Path - import jax import jax.numpy as jnp import numpy as np @@ -12,7 +10,7 @@ from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program -from pyquil.quilatom import MemoryReference, Qubit +from pyquil.quilatom import FormalArgument, MemoryReference, Qubit from pyquil.quilbase import ( Declare, DefCircuit, @@ -25,11 +23,7 @@ from pyquil.quilbase import ( Measurement as QuilMeasurement, ) -from pyquil.quilbase import ( - Reset as QuilReset, -) from pyquil.simulation._simulator import ( - DensityMatrixSimulator, PureStateVectorSimulator, TrajectorySimulator, _run_batched_trajectories, @@ -39,23 +33,6 @@ ) _EMPTY_PARAMS = jnp.array([], dtype=float) -_DATA_DIR = Path(__file__).parent / "data" -_SURFACE17_FIXTURE = _DATA_DIR / "surface_17_depth_5_no_reset.quil" -_SURFACE17_QUBITS = (65, 66, 74, 75, 76, 77, 82, 83, 84, 85, 86, 91, 92, 93, 94, 102, 103) -_SURFACE17_CYCLES = { - "SZ_INIT", - "SX_INIT", - "CZ_0", - "SZ_DATA", - "SX_DATA", - "CZ_1", - "CZ_2", - "CZ_3", - "SZ_ANCILLA", - "SX_ANCILLA_ECHO", - "MEASURE_ANCILLA", - "MEASURE_ALL", -} def _sv(program, qubits=None, memory_map=None): @@ -68,16 +45,20 @@ def _sv(program, qubits=None, memory_map=None): return sim.compute(params) -def _simulate_trajectories(program, noise_model=None, qubits=None, num_trajectories=1, - batch_size=256, random_seed=0): +def _simulate_trajectories(program, noise_model=None, qubits=None, num_trajectories=1, batch_size=256, random_seed=0): """Helper: build + compress + run trajectories, returning (psi, outcomes).""" sim = TrajectorySimulator(program, noise_model=noise_model, qubits=qubits) resolved = sim.resolve(_EMPTY_PARAMS) compressed = sim.compress(resolved) operations = sim.adapt(compressed) all_psis, all_outcomes = _run_batched_trajectories( - operations, sim.n_qubits, num_trajectories, batch_size, random_seed, - keep_states=True, dims=sim.dims, + operations, + sim.n_qubits, + num_trajectories, + batch_size, + random_seed, + keep_states=True, + dims=sim.dims, ) assert all_psis is not None if len(all_psis) == 1: @@ -88,94 +69,6 @@ def _simulate_trajectories(program, noise_model=None, qubits=None, num_trajector return combined_psi, combined_outcomes -def _load_surface17_depth5_program(): - """Load the checked-in surface-17 depth-5 Quil fixture.""" - return Program(_SURFACE17_FIXTURE.read_text()) - - -def _surface17_defcircuits(program): - return {inst.name: inst for inst in program.instructions if isinstance(inst, DefCircuit)} - - -def _concretize_cycle_gate(inst, qubit_map): - return QuilGate( - inst.name, - list(inst.params), - [qubit_map[qubit] for qubit in inst.qubits], - ) - - -def _concretize_cycle_measurement(inst, qubit_map): - return QuilMeasurement(qubit=qubit_map[inst.qubit], classical_reg=None) - - -def _build_surface17_cycle_noise_model( - program, - depolarizing_constant=0.99, - readout_fidelity=1.0, -): - """Build a cycle noise model that matches the surface-17 DEFCIRCUIT invocations.""" - defcircuits = _surface17_defcircuits(program) - cycle_channels = [] - - for inst in program.instructions: - if not isinstance(inst, QuilGate) or inst.name not in defcircuits: - continue - - defcircuit = defcircuits[inst.name] - qubit_map = dict(zip(defcircuit.qubit_variables, inst.qubits)) - channels = [] - - for cycle_inst in defcircuit.instructions: - if isinstance(cycle_inst, QuilGate): - concrete_gate = _concretize_cycle_gate(cycle_inst, qubit_map) - channels.append(Channel.from_depolarizing_constant(concrete_gate, depolarizing_constant)) - elif isinstance(cycle_inst, QuilMeasurement): - concrete_measurement = _concretize_cycle_measurement(cycle_inst, qubit_map) - channels.append( - MeasurementChannel.from_readout_fidelity(concrete_measurement, fidelity=readout_fidelity) - ) - - cycle_channels.append(CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels))) - - return NoiseModel(channels=cycle_channels) - - -def _run_surface17_cycle_benchmark( - benchmark, - num_trajectories=128, - batch_size=16, - depolarizing_constant=0.99, - readout_fidelity=1.0, -): - program = _load_surface17_depth5_program() - noise_model = _build_surface17_cycle_noise_model( - program, - depolarizing_constant=depolarizing_constant, - readout_fidelity=readout_fidelity, - ) - sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=0) - params = sim.linearize({}) - operations = sim.adapt(sim.compress(sim.resolve(params))) - - warmup_psi = qx.zero_state_vector(dims=sim.dims, ensemble_size=(batch_size,)) - key = jax.random.key(0) - apply_trajectory_operations(operations, warmup_psi, key)[0].matrix.block_until_ready() - - def thunk(): - key = jax.random.key(0) - remaining = num_trajectories - while remaining > 0: - this_batch = min(remaining, batch_size) - key, batch_key = jax.random.split(key) - psi = qx.zero_state_vector(dims=sim.dims, ensemble_size=(this_batch,)) - result = apply_trajectory_operations(operations, psi, batch_key) - result[0].matrix.block_until_ready() - remaining -= this_batch - - benchmark.pedantic(thunk, iterations=1, rounds=1) - - class TestSingleQubitGates: def test_identity(self): p = Program() @@ -230,7 +123,9 @@ def test_bell_state(self): def test_ghz_state_3q(self): p = Program(H(0), CNOT(0, 1), CNOT(1, 2)) psi = _sv(p, qubits=[0, 1, 2]) - target = qx.StateVector.from_matrix(jnp.array([1.0, 0, 0, 0, 0, 0, 0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2, 2)) + target = qx.StateVector.from_matrix( + jnp.array([1.0, 0, 0, 0, 0, 0, 0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2, 2) + ) assert qx.fidelity(psi, target) > 0.9999 def test_qubit_ordering(self): @@ -293,26 +188,20 @@ def test_single_gate_noiseless(self): """Without noise, trajectory simulation matches unitary simulation.""" p = Program(H(0)) psi_noiseless = _sv(p, qubits=[0]) - psi_traj, outcomes = _simulate_trajectories( - p, noise_model=None, qubits=[0], num_trajectories=1 - ) + psi_traj, outcomes = _simulate_trajectories(p, noise_model=None, qubits=[0], num_trajectories=1) assert qx.fidelity(psi_noiseless, psi_traj) > 0.9999 def test_bell_state_noiseless(self): """Multi-qubit noiseless trajectory.""" p = Program(H(0), CNOT(0, 1)) psi_noiseless = _sv(p, qubits=[0, 1]) - psi_traj, outcomes = _simulate_trajectories( - p, noise_model=None, qubits=[0, 1], num_trajectories=1 - ) + psi_traj, outcomes = _simulate_trajectories(p, noise_model=None, qubits=[0, 1], num_trajectories=1) assert qx.fidelity(psi_noiseless, psi_traj) > 0.9999 def test_multiple_trajectories_noiseless_deterministic(self): """Multiple noiseless trajectories should all give same result.""" p = Program(X(0)) - psi_batch, outcomes = _simulate_trajectories( - p, noise_model=None, qubits=[0], num_trajectories=8 - ) + psi_batch, outcomes = _simulate_trajectories(p, noise_model=None, qubits=[0], num_trajectories=8) # Each trajectory should be |1⟩ target = qx.StateVector.from_matrix(jnp.array([0.0, 1.0], dtype=complex), dims=(2,)) probs = qx.probabilities(psi_batch) @@ -334,25 +223,21 @@ def _make_bitflip_noise_model(self, p_error: float, qubit: int = 0) -> NoiseMode # Use a Pauli channel: p_I = 1-p, p_X = p, p_Y = 0, p_Z = 0 pauli_probs = {"X": p_error} channel = Channel.from_pauli_noise(inst=inst, pauli_noise=pauli_probs) - return NoiseModel(channels=frozenset([channel])) + return NoiseModel(channels=[channel]) def _make_depolarizing_noise_model(self, fidelity: float, qubit: int = 0) -> NoiseModel: """Create a noise model with depolarizing noise on X gate.""" inst = X(qubit) channel = Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) - return NoiseModel(channels=frozenset([channel])) + return NoiseModel(channels=[channel]) def test_noiseless_gate_with_noise_model(self): """A noise model that doesn't cover the applied gate should leave it noiseless.""" # Noise model only covers X gate, but we apply H noise_model = self._make_bitflip_noise_model(0.1, qubit=0) p = Program(H(0)) - psi, outcomes = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=1 - ) - target = qx.StateVector.from_matrix( - jnp.array([1.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2,) - ) + psi, outcomes = _simulate_trajectories(p, noise_model=noise_model, qubits=[0], num_trajectories=1) + target = qx.StateVector.from_matrix(jnp.array([1.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2,)) assert qx.fidelity(psi, target) > 0.9999 def test_bitflip_statistics(self): @@ -364,8 +249,12 @@ def test_bitflip_statistics(self): p = Program(X(0)) num_traj = 2048 psi_batch, outcomes = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=num_traj, - batch_size=256, random_seed=42, + p, + noise_model=noise_model, + qubits=[0], + num_trajectories=num_traj, + batch_size=256, + random_seed=42, ) # Get probabilities for each trajectory probs = qx.probabilities(psi_batch) # shape (num_traj, 2) @@ -374,9 +263,7 @@ def test_bitflip_statistics(self): in_zero = jnp.sum(probs[:, 0] > 0.5) observed_flip_rate = float(in_zero) / num_traj # Expected: p_error fraction should flip to |0⟩ - assert abs(observed_flip_rate - p_error) < 0.05, ( - f"Expected flip rate ~{p_error}, got {observed_flip_rate}" - ) + assert abs(observed_flip_rate - p_error) < 0.05, f"Expected flip rate ~{p_error}, got {observed_flip_rate}" def test_depolarizing_statistics(self): """Depolarizing noise on identity-like circuit should produce mixed results.""" @@ -385,8 +272,12 @@ def test_depolarizing_statistics(self): p = Program(X(0)) num_traj = 2048 psi_batch, outcomes = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=num_traj, - batch_size=256, random_seed=123, + p, + noise_model=noise_model, + qubits=[0], + num_trajectories=num_traj, + batch_size=256, + random_seed=123, ) probs = qx.probabilities(psi_batch) # Average probability of |1⟩ across trajectories should be close to @@ -406,13 +297,17 @@ def test_two_qubit_noise(self): inst_q1 = X(1) ch0 = Channel.from_pauli_noise(inst=inst_q0, pauli_noise={"X": p_error}) ch1 = Channel.from_pauli_noise(inst=inst_q1, pauli_noise={"X": p_error}) - noise_model = NoiseModel(channels=frozenset([ch0, ch1])) + noise_model = NoiseModel(channels=[ch0, ch1]) prog = Program(X(0), X(1)) num_traj = 2048 psi_batch, _ = _simulate_trajectories( - prog, noise_model=noise_model, qubits=[0, 1], num_trajectories=num_traj, - batch_size=256, random_seed=7, + prog, + noise_model=noise_model, + qubits=[0, 1], + num_trajectories=num_traj, + batch_size=256, + random_seed=7, ) probs = qx.probabilities(psi_batch) # shape (num_traj, 4) # State |11⟩ = index 3. Both flipped: p_error^2 gives |00⟩ @@ -429,8 +324,12 @@ def test_measurement_records_outcome(self): """Measurement should record classical outcome.""" p = Program(H(0), MEASURE(0, None)) psi, outcomes = _simulate_trajectories( - p, noise_model=None, qubits=[0], num_trajectories=100, - batch_size=100, random_seed=42, + p, + noise_model=None, + qubits=[0], + num_trajectories=100, + batch_size=100, + random_seed=42, ) # outcomes shape should be (100, 1) — one measurement assert outcomes.shape == (100, 1) @@ -444,8 +343,12 @@ def test_measurement_collapses_state(self): """After measurement, state should be consistent with outcome.""" p = Program(H(0), MEASURE(0, None)) psi, outcomes = _simulate_trajectories( - p, noise_model=None, qubits=[0], num_trajectories=64, - batch_size=64, random_seed=99, + p, + noise_model=None, + qubits=[0], + num_trajectories=64, + batch_size=64, + random_seed=99, ) probs = qx.probabilities(psi) # (64, 2) # For each trajectory, the state should be collapsed @@ -459,12 +362,16 @@ def test_noisy_measurement(self): qubit = Qubit(0) m_inst = QuilMeasurement(qubit=qubit, classical_reg=None) meas_ch = MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=0.8) - noise_model = NoiseModel(channels=frozenset([meas_ch])) + noise_model = NoiseModel(channels=[meas_ch]) p = Program(MEASURE(0, None)) psi, outcomes = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=1024, - batch_size=256, random_seed=55, + p, + noise_model=noise_model, + qubits=[0], + num_trajectories=1024, + batch_size=256, + random_seed=55, ) # Prepared in |0⟩, ideal measurement gives 0, but with 20% error → ~20% ones frac_1 = float(jnp.mean(outcomes == 1)) @@ -478,7 +385,10 @@ def test_reset_to_ground(self): """Reset should bring qubit to |0⟩.""" p = Program(X(0), ResetQubit(Qubit(0))) psi, _ = _simulate_trajectories( - p, noise_model=None, qubits=[0], num_trajectories=1, + p, + noise_model=None, + qubits=[0], + num_trajectories=1, ) target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0], dtype=complex), dims=(2,)) assert qx.fidelity(psi, target) > 0.9999 @@ -487,11 +397,12 @@ def test_global_reset(self): """Global RESET should reset all qubits.""" p = Program(X(0), X(1), RESET()) psi, _ = _simulate_trajectories( - p, noise_model=None, qubits=[0, 1], num_trajectories=1, - ) - target = qx.StateVector.from_matrix( - jnp.array([1.0, 0.0, 0.0, 0.0], dtype=complex), dims=(2, 2) + p, + noise_model=None, + qubits=[0, 1], + num_trajectories=1, ) + target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0, 0.0, 0.0], dtype=complex), dims=(2, 2)) assert qx.fidelity(psi, target) > 0.9999 def test_noisy_reset(self): @@ -499,14 +410,18 @@ def test_noisy_reset(self): qubit = Qubit(0) reset_inst = ResetQubit(qubit) reset_ch = ResetChannel.from_reset_fidelity(inst=reset_inst, fidelity=0.9) - noise_model = NoiseModel(channels=frozenset([reset_ch])) + noise_model = NoiseModel(channels=[reset_ch]) # Start in |1⟩, apply noisy reset p = Program(X(0), ResetQubit(Qubit(0))) num_traj = 2048 psi, _ = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=num_traj, - batch_size=256, random_seed=13, + p, + noise_model=noise_model, + qubits=[0], + num_trajectories=num_traj, + batch_size=256, + random_seed=13, ) probs = qx.probabilities(psi) # (num_traj, 2) # With 90% reset fidelity, ~90% should end in |0⟩ @@ -524,13 +439,21 @@ def test_batch_size_smaller_than_trajectories(self): # Single batch psi_1, outcomes_1 = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=64, - batch_size=64, random_seed=42, + p, + noise_model=noise_model, + qubits=[0], + num_trajectories=64, + batch_size=64, + random_seed=42, ) # Multiple batches (same seed) psi_2, outcomes_2 = _simulate_trajectories( - p, noise_model=noise_model, qubits=[0], num_trajectories=64, - batch_size=16, random_seed=42, + p, + noise_model=noise_model, + qubits=[0], + num_trajectories=64, + batch_size=16, + random_seed=42, ) # Note: different batching may produce different results due to key splitting, # but shapes should match @@ -545,16 +468,14 @@ def test_noise_model_none_unchanged(self): """With noise_model=None, behavior is identical to original.""" p = Program(H(0), CNOT(0, 1)) psi = _sv(p, qubits=[0, 1]) - target = qx.StateVector.from_matrix( - jnp.array([1.0, 0.0, 0.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2) - ) + target = qx.StateVector.from_matrix(jnp.array([1.0, 0.0, 0.0, 1.0], dtype=complex) / jnp.sqrt(2), dims=(2, 2)) assert qx.fidelity(psi, target) > 0.9999 def test_noise_model_single_trajectory(self): """With noise_model provided, runs a single trajectory.""" inst = X(0) channel = Channel.from_gate_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=frozenset([channel])) + noise_model = NoiseModel(channels=[channel]) p = Program(X(0)) sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0]) psi, _ = sim.compute(_EMPTY_PARAMS, jax.random.key(0)) @@ -571,8 +492,10 @@ def test_returns_outcomes_only(self): p = Program(H(0), MEASURE(0, None)) sim = TrajectorySimulator(p, noise_model=None, qubits=[0]) outcomes = sim.sample( - _EMPTY_PARAMS, num_trajectories=100, - batch_size=32, random_seed=42, + _EMPTY_PARAMS, + num_trajectories=100, + batch_size=32, + random_seed=42, ) assert outcomes.shape == (100, 1) assert jnp.all((outcomes == 0) | (outcomes == 1)) @@ -582,7 +505,8 @@ def test_no_measurements_empty_outcomes(self): p = Program(H(0)) sim = TrajectorySimulator(p, noise_model=None, qubits=[0]) outcomes = sim.sample( - _EMPTY_PARAMS, num_trajectories=10, + _EMPTY_PARAMS, + num_trajectories=10, ) assert outcomes.shape == (10, 0) @@ -591,13 +515,15 @@ def test_bitflip_statistics(self): p_error = 0.3 inst = X(0) ch = Channel.from_pauli_noise(inst=inst, pauli_noise={"X": p_error}) - noise_model = NoiseModel(channels=frozenset([ch])) + noise_model = NoiseModel(channels=[ch]) p = Program(X(0), MEASURE(0, None)) sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0]) outcomes = sim.sample( - _EMPTY_PARAMS, num_trajectories=2048, - batch_size=256, random_seed=42, + _EMPTY_PARAMS, + num_trajectories=2048, + batch_size=256, + random_seed=42, ) # X|0⟩ = |1⟩, then bit-flip with p=0.3 → ~30% get |0⟩ # Measurement outcome reflects the final state @@ -609,10 +535,14 @@ def test_batch_size_does_not_affect_shape(self): p = Program(H(0), MEASURE(0, None)) sim = TrajectorySimulator(p, qubits=[0]) outcomes_small = sim.sample( - _EMPTY_PARAMS, num_trajectories=100, batch_size=10, + _EMPTY_PARAMS, + num_trajectories=100, + batch_size=10, ) outcomes_large = sim.sample( - _EMPTY_PARAMS, num_trajectories=100, batch_size=100, + _EMPTY_PARAMS, + num_trajectories=100, + batch_size=100, ) assert outcomes_small.shape == outcomes_large.shape == (100, 1) @@ -724,7 +654,7 @@ def test_no_merge_noisy_ops_count(self): channels = [ Channel.from_coherence_times(RX(np.pi / 2, 0), gate_duration=0.04, t1s=[30.0], t2s=[20.0]), ] - noise_model = NoiseModel(channels=frozenset(channels)) + noise_model = NoiseModel(channels=channels) sim = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) @@ -769,8 +699,10 @@ def test_2q_gate_breaks_run(self): def test_independent_qubit_runs(self): """1Q gates on different qubits should form separate runs.""" p = Program( - RZ(0.1, 0), RX(0.2, 0), - RZ(0.3, 1), RX(0.4, 1), + RZ(0.1, 0), + RX(0.2, 0), + RZ(0.3, 1), + RX(0.4, 1), ) sim = TrajectorySimulator(p, max_subsystem_size=1) @@ -802,9 +734,7 @@ def test_parameterized_merge(self): assert len(ops) == 2 - psi_direct = _sv( - Program(RZ(theta_vals[0], 0), RX(theta_vals[1], 0)) - ) + psi_direct = _sv(Program(RZ(theta_vals[0], 0), RX(theta_vals[1], 0))) psi = qx.zero_state_vector(sim.n_qubits) for op, subsystem in ops: if isinstance(op, qx.Unitary): @@ -817,7 +747,7 @@ def test_noisy_1q_merge(self): channels = [ Channel.from_coherence_times(RX(np.pi / 2, 0), gate_duration=0.04, t1s=[30.0], t2s=[20.0]), ] - noise_model = NoiseModel(channels=frozenset(channels)) + noise_model = NoiseModel(channels=channels) sim0 = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) sim1 = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=1) @@ -872,7 +802,7 @@ def test_noisy_trajectory_via_simulator(self): channels = [ Channel.from_coherence_times(CNOT(0, 1), gate_duration=0.1, t1s=[30.0, 30.0], t2s=[20.0, 20.0]), ] - noise_model = NoiseModel(channels=frozenset(channels)) + noise_model = NoiseModel(channels=channels) sim = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) @@ -902,16 +832,18 @@ def test_parameterized_trajectory(self): _, outcomes = apply_trajectory_operations(ops, psi, key) assert jnp.all(outcomes == 1) + # ────────────────────────────────────────────────────────────────────────────── -# Compressor op-count benchmarks +# Compressor op-count tests # ────────────────────────────────────────────────────────────────────────────── - def _op_count(program, max_subsystem_size, noise_model=None): """Return the number of compressed ops for a program.""" sim = TrajectorySimulator( - program, noise_model=noise_model, max_subsystem_size=max_subsystem_size, + program, + noise_model=noise_model, + max_subsystem_size=max_subsystem_size, ) return len(sim.adapt(sim.compress(sim.resolve(sim.linearize({}))))) @@ -919,6 +851,49 @@ def _op_count(program, max_subsystem_size, noise_model=None): class TestCompressorOpCounts: """Tests that verify the compressor produces the expected number of ops.""" + def test_cycle_channel_expands_and_compresses(self): + formal_qubit = FormalArgument("q") + defcircuit = DefCircuit( + "SINGLE_QUBIT_CYCLE", + [], + [formal_qubit], + [RX(0.1, formal_qubit), RZ(0.2, formal_qubit), RX(0.3, formal_qubit)], + ) + cycle_inst = QuilGate("SINGLE_QUBIT_CYCLE", [], [0]) + channels = tuple( + Channel.from_depolarizing_constant(inst, depolarizing_constant=0.99) + for inst in (RX(0.1, 0), RZ(0.2, 0), RX(0.3, 0)) + ) + noise_model = NoiseModel(channels=[CycleChannel(inst=cycle_inst, defcircuit=defcircuit, channels=channels)]) + program = Program(defcircuit, cycle_inst) + + sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=1) + resolved = sim.resolve(_EMPTY_PARAMS) + compressed = sim.compress(resolved) + + assert len(resolved) == 3 + assert all(isinstance(op, qx.SuperOp) for op, _ in resolved) + assert len(compressed) == 1 + + def test_expanded_cycle_without_cycle_channel_uses_gate_channels(self): + formal_qubit = FormalArgument("q") + defcircuit = DefCircuit( + "INDIVIDUAL_NOISE_CYCLE", + [], + [formal_qubit], + [RX(0.1, formal_qubit), RZ(0.2, formal_qubit)], + ) + cycle_inst = QuilGate("INDIVIDUAL_NOISE_CYCLE", [], [0]) + noise_model = NoiseModel(channels=[Channel.from_depolarizing_constant(RX(0.1, 0), 0.99)]) + program = Program(defcircuit, cycle_inst) + + sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=0) + resolved = sim.resolve(_EMPTY_PARAMS) + + assert len(resolved) == 2 + assert isinstance(resolved[0][0], qx.SuperOp) + assert isinstance(resolved[1][0], qx.Unitary) + def test_single_qubit_sequence_merges_to_one(self): """RZ-RX-RZ-RX-RZ on one qubit → 1 op at max_size ≥ 1.""" p = Program(RZ(0.1, 0), RX(0.2, 0), RZ(0.3, 0), RX(0.4, 0), RZ(0.5, 0)) @@ -1020,9 +995,7 @@ def test_random_circuit_compression(self, num_qubits, max_subsystem_size): n_uncompressed = _op_count(p, 0) # Compression should never increase op count - assert n_ops <= n_uncompressed, ( - f"max_size={max_subsystem_size}: {n_ops} ops > {n_uncompressed} uncompressed" - ) + assert n_ops <= n_uncompressed, f"max_size={max_subsystem_size}: {n_ops} ops > {n_uncompressed} uncompressed" # With max_size > 0, we expect at least some compression for this circuit if max_subsystem_size > 0: @@ -1033,7 +1006,10 @@ def test_random_circuit_compression_summary(self, capsys): rng = np.random.default_rng(42) configs = [ - (4, 5), (8, 5), (12, 5), (16, 3), + (4, 5), + (8, 5), + (12, 5), + (16, 3), ] max_sizes = [0, 1, 2, 3, 4] @@ -1066,181 +1042,3 @@ def test_random_circuit_compression_summary(self, capsys): line += f" {counts[s]:>4} ({ratio:.2f})" # line += f" {counts[s]:>8}" print(line) - - -class TestSurface17Fixture: - """Tests for the checked-in surface-17 trajectory benchmark fixture.""" - - def test_surface17_fixture_structure(self): - program = _load_surface17_depth5_program() - defcircuit_names = set(_surface17_defcircuits(program)) - invocations = [inst for inst in program.instructions if isinstance(inst, QuilGate)] - invocation_names = [inst.name for inst in invocations] - - assert _SURFACE17_FIXTURE.exists() - assert defcircuit_names == _SURFACE17_CYCLES - assert set(program.get_qubit_indices()) == set(_SURFACE17_QUBITS) - assert not any(isinstance(inst, (QuilReset, ResetQubit)) for inst in program.instructions) - assert invocation_names.count("MEASURE_ANCILLA") == 4 - assert invocation_names[-1] == "MEASURE_ALL" - - def test_surface17_cycle_noise_model_preserves_measurements(self): - program = _load_surface17_depth5_program() - noise_model = _build_surface17_cycle_noise_model(program, depolarizing_constant=1.0) - sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=0) - resolved = sim.resolve(_EMPTY_PARAMS) - - n_measurements = sum(1 for op, _ in resolved if isinstance(op, qx.QuantumInstrument)) - assert n_measurements == 49 - -# ────────────────────────────────────────────────────────────────────────────── -# State Vector simulation benchmarks -# ────────────────────────────────────────────────────────────────────────────── - -_DEFAULT_NUM_QUBITS = 15 -_DEFAULT_NUM_LAYERS = 10 -_DEFAULT_NUM_TRAJECTORIES = 128 -_DEFAULT_BATCH_SIZE = 32 -_DEFAULT_MAX_SUBSYSTEM_SIZE = 1 - - -def _build_noisy_program_and_model(num_qubits, num_layers, seed=4867): - """Build a layered noisy circuit and matching noise model. - - Circuit structure per layer (×2 for even/odd edge sets): - RZ-RX-RZ-RX-RZ on every qubit, then CNOTs on edges. - Total: 5*num_layers*num_qubits 1Q gates + (num_qubits-1)*num_layers 2Q gates. - """ - edges_0 = [(i, i + 1) for i in range(0, num_qubits - 1, 2)] - edges_1 = [(i, i + 1) for i in range(1, num_qubits - 1, 2)] - rng = np.random.default_rng(seed) - - t1s, t2s = {}, {} - for q in range(num_qubits): - t1 = np.clip(rng.normal(30, 10), 10, 50) - t2 = np.clip(rng.normal(30, 20), 5, 2 * t1) - t1s[q], t2s[q] = t1, t2 - - channels = [ - Channel.from_coherence_times( - CNOT(*edge), gate_duration=0.1, t1s=[t1s[q] for q in edge], t2s=[t2s[q] for q in edge] - ) - for edge in edges_0 + edges_1 - ] + [ - Channel.from_coherence_times(RX(np.pi / 2, q), gate_duration=0.04, t1s=[t1s[q]], t2s=[t2s[q]]) - for q in range(num_qubits) - ] - noise_model = NoiseModel(channels=frozenset(channels)) - - program = Program() - for _ in range(num_layers): - for edges in [edges_0, edges_1]: - program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] - program += [RX(np.pi / 2, idx) for idx in range(num_qubits)] - program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] - program += [RX(np.pi / 2, idx) for idx in range(num_qubits)] - program += [RZ(rng.uniform(-np.pi, np.pi), idx) for idx in range(num_qubits)] - program += [CNOT(*edge) for edge in edges] - - return program, noise_model - - -def _run_perf_benchmark( - benchmark, - num_qubits=_DEFAULT_NUM_QUBITS, - num_layers=_DEFAULT_NUM_LAYERS, - num_trajectories=_DEFAULT_NUM_TRAJECTORIES, - batch_size=_DEFAULT_BATCH_SIZE, - max_subsystem_size=_DEFAULT_MAX_SUBSYSTEM_SIZE, -): - """Shared benchmark harness: build, warmup, then benchmark the JAX kernel.""" - program, noise_model = _build_noisy_program_and_model(num_qubits, num_layers) - - sim = TrajectorySimulator( - program, noise_model=noise_model, max_subsystem_size=max_subsystem_size, - ) - params = sim.linearize({}) - operations = sim.adapt(sim.compress(sim.resolve(params))) - - # Warmup: trigger JIT compilation - warmup_psi = qx.zero_state_vector(sim.n_qubits, ensemble_size=(batch_size,)) - key = jax.random.key(0) - apply_trajectory_operations(operations, warmup_psi, key)[0].matrix.block_until_ready() - - def thunk(): - key = jax.random.key(0) - remaining = num_trajectories - while remaining > 0: - this_batch = min(remaining, batch_size) - key, batch_key = jax.random.split(key) - psi = qx.zero_state_vector(sim.n_qubits, ensemble_size=(this_batch,)) - result = apply_trajectory_operations(operations, psi, batch_key) - result[0].matrix.block_until_ready() - remaining -= this_batch - - benchmark.pedantic(thunk, iterations=1, rounds=3) - - -class TestPerformance: - """Trajectory simulator performance benchmarks. - - Defaults: 15 qubits, depth 10, 128 trajectories, batch_size 32, - max_subsystem_size 1. Each test varies one axis while holding the - others constant. - """ - - # ── Vary num_qubits ────────────────────────────────── - @pytest.mark.parametrize("num_qubits", [ - pytest.param(3, id="3q"), - pytest.param(6, id="6q"), - pytest.param(9, id="9q"), - pytest.param(12, id="12q"), - pytest.param(15, id="15q"), - ]) - def test_scaling_qubits(self, benchmark, num_qubits): - _run_perf_benchmark(benchmark, num_qubits=num_qubits) - - # ── Vary depth (num_layers) ────────────────────────── - @pytest.mark.parametrize("num_layers", [ - pytest.param(1, id="1L"), - pytest.param(3, id="3L"), - pytest.param(10, id="10L"), - pytest.param(20, id="20L"), - ]) - def test_scaling_depth(self, benchmark, num_layers): - _run_perf_benchmark(benchmark, num_layers=num_layers) - - # ── Vary batch_size ────────────────────────────────── - @pytest.mark.parametrize("batch_size", [ - pytest.param(8, id="b8"), - pytest.param(16, id="b16"), - # pytest.param(32, id="b32"), - pytest.param(64, id="b64"), - # pytest.param(128, id="b128"), - ]) - def test_scaling_batch_size(self, benchmark, batch_size): - _run_perf_benchmark(benchmark, batch_size=batch_size) - - # ── Vary max_subsystem_size ────────────────────────── - @pytest.mark.parametrize("max_subsystem_size", [ - pytest.param(0, id="s0"), - pytest.param(1, id="s1"), - ]) - def test_scaling_subsystem_size(self, benchmark, max_subsystem_size): - _run_perf_benchmark(benchmark, max_subsystem_size=max_subsystem_size) - - # ── 17-qubit batch_size sweep ──────────────────────── - @pytest.mark.parametrize("batch_size", [ - pytest.param(8, id="b8"), - pytest.param(16, id="b16"), - # pytest.param(32, id="b32"), - pytest.param(64, id="b64"), - # pytest.param(128, id="b128"), - ]) - def test_17q_batch_size(self, benchmark, batch_size): - _run_perf_benchmark(benchmark, num_qubits=17, batch_size=batch_size) - - def test_surface17_depth5_cycle_noise(self, benchmark): - _run_surface17_cycle_benchmark(benchmark) - - From 2874d0b2900073b683cec617c193f4a7f3c010a9 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 15:44:07 +0000 Subject: [PATCH 06/37] Format and style --- .github/workflows/test.yml | 26 +++++++-------- Dockerfile | 2 +- Dockerfile.test | 2 +- pyproject.toml | 2 -- pyquil/api/_abstract_compiler.py | 2 +- pyquil/api/_qpu.py | 3 +- pyquil/api/_quantum_computer.py | 12 +++---- pyquil/api/_qvm.py | 2 +- pyquil/experiment/_group.py | 6 ++-- pyquil/experiment/_main.py | 2 +- pyquil/latex/_diagram.py | 6 ++-- pyquil/noise/_channels.py | 25 +++++++-------- pyquil/noise/_legacy_noise.py | 4 +-- pyquil/noise/_noise_model.py | 13 ++++---- pyquil/operator_estimation.py | 4 +-- pyquil/paulis.py | 4 +-- .../transformers/qcs_isa_to_compiler_isa.py | 7 ++-- pyquil/quilbase.py | 8 +++-- pyquil/simulation/_reference.py | 2 +- pyquil/simulation/_resolver.py | 32 ++++++++++--------- pyquil/simulation/_simulator.py | 10 +++--- pyquil/simulation/tools.py | 2 +- pyquil/transform.py | 24 +++++++------- pyquil/wavefunction.py | 2 +- 24 files changed, 97 insertions(+), 105 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c94022271..7059c8276 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - uses: actions/cache@v4 with: path: .venv @@ -33,10 +33,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - uses: actions/cache@v4 with: path: .venv @@ -52,10 +52,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - uses: actions/cache@v4 with: path: .venv @@ -71,10 +71,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' - uses: actions/cache@v4 with: path: .venv @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -133,7 +133,7 @@ jobs: pull-requests: write # allows coverage bot to comment strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -166,10 +166,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v2 with: - python-version: '3.10' + python-version: '3.11' - uses: actions/cache@v4 with: path: .venv @@ -195,7 +195,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/Dockerfile b/Dockerfile index 6bc5187eb..d2028527f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # use multi-stage builds to independently pull dependency versions ARG quilc_version=1.20.0 ARG qvm_version=1.17.1 -ARG python_version=3.10 +ARG python_version=3.11 # use multi-stage builds to independently pull dependency versions FROM rigetti/quilc:$quilc_version as quilc diff --git a/Dockerfile.test b/Dockerfile.test index cac3b69e1..cd87f5e6e 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -1,7 +1,7 @@ # use multi-stage builds to independently pull dependency versions ARG quilc_version=1.20.0 ARG qvm_version=1.17.1 -ARG python_version=3.10 +ARG python_version=3.11 # use multi-stage builds to independently pull dependency versions FROM rigetti/quilc:$quilc_version as quilc diff --git a/pyproject.toml b/pyproject.toml index 21291dc7f..984b61c54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,6 @@ license = "Apache-2.0" classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 880845054..461eab8c5 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -186,7 +186,7 @@ def _check_quilc_version(version: str) -> None: major, minor, _ = map(int, version.split(".")) if major == 1 and minor < 8: raise QuilcVersionMismatch( - "Must use quilc >= 1.8.0 with pyquil >= 2.8.0, but you " f"have quilc {version} and pyquil {pyquil_version}" + f"Must use quilc >= 1.8.0 with pyquil >= 2.8.0, but you have quilc {version} and pyquil {pyquil_version}" ) diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 0fc6fae6d..6e97111fd 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -97,8 +97,7 @@ def alloc(spec: ParameterSpec) -> np.ndarray: region_width = memory_descriptors[mref.name].length if end > region_width: raise ValueError( - f"Attempted to fill {mref.name}[{mref.offset}, {end})" - f"but the declared region has width {region_width}." + f"Attempted to fill {mref.name}[{mref.offset}, {end})but the declared region has width {region_width}." ) regions[mref.name][:, mref.offset : end] = buf diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 63926f60e..9cbe6786f 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -102,7 +102,7 @@ def qubits(self) -> list[int]: """ return self.compiler.quantum_processor.qubits() - def qubit_topology(self) -> nx.graph: + def qubit_topology(self) -> nx.Graph: """Return a NetworkX graph representation of this QuantumComputer's quantum_processor's qubit connectivity. See :py:func:`AbstractQuantumProcessor.qubit_topology` for more. @@ -329,9 +329,7 @@ def run_symmetrized_readout( :return: A numpy array of shape (trials, len(ro-register)) that contains 0s and 1s. """ if not isinstance(symm_type, int): - raise ValueError( - "Symmetrization options are indicated by an int. See " "the docstrings for more information." - ) + raise ValueError("Symmetrization options are indicated by an int. See the docstrings for more information.") if meas_qubits is None: meas_qubits = list(program.get_qubit_indices()) @@ -438,12 +436,12 @@ def _parse_name(name: str, as_qvm: Optional[bool], noisy: Optional[bool]) -> tup if len(parts) >= 2 and parts[-2] == "noisy" and parts[-1] in ["qvm", "pyqvm"]: if as_qvm is not None and (not as_qvm): raise ValueError( - "The provided qc name indicates you are getting a noisy QVM, " "but you have specified `as_qvm=False`" + "The provided qc name indicates you are getting a noisy QVM, but you have specified `as_qvm=False`" ) if noisy is not None and (not noisy): raise ValueError( - "The provided qc name indicates you are getting a noisy QVM, " "but you have specified `noisy=False`" + "The provided qc name indicates you are getting a noisy QVM, but you have specified `noisy=False`" ) qvm_type = parts[-1] @@ -454,7 +452,7 @@ def _parse_name(name: str, as_qvm: Optional[bool], noisy: Optional[bool]) -> tup if len(parts) >= 1 and parts[-1] in ["qvm", "pyqvm"]: if as_qvm is not None and (not as_qvm): raise ValueError( - "The provided qc name indicates you are getting a QVM, " "but you have specified `as_qvm=False`" + "The provided qc name indicates you are getting a QVM, but you have specified `as_qvm=False`" ) qvm_type = parts[-1] if noisy is None: diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 24438c3fc..348b0057b 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -43,7 +43,7 @@ def check_qvm_version(version: str) -> None: major, minor = map(int, version.split(".")[:2]) if major == 1 and minor < 8: raise QVMVersionMismatch( - "Must use QVM >= 1.8.0 with pyquil >= 2.8.0, but you " f"have QVM {version} and pyquil {pyquil_version}" + f"Must use QVM >= 1.8.0 with pyquil >= 2.8.0, but you have QVM {version} and pyquil {pyquil_version}" ) diff --git a/pyquil/experiment/_group.py b/pyquil/experiment/_group.py index 0ece4766e..126ae0e43 100644 --- a/pyquil/experiment/_group.py +++ b/pyquil/experiment/_group.py @@ -91,9 +91,7 @@ def merge_disjoint_experiments(experiments: list[Experiment], group_merged_setti used_qubits: set[int] = set() for expt in experiments: if expt.program.get_qubits().intersection(used_qubits): - raise ValueError( - "Experiment programs act on some shared set of qubits and cannot be " "merged unambiguously." - ) + raise ValueError("Experiment programs act on some shared set of qubits and cannot be merged unambiguously.") used_qubits = used_qubits.union(cast(set[int], expt.program.get_qubits())) # get a flat list of all settings, to be regrouped later @@ -115,7 +113,7 @@ def merge_disjoint_experiments(experiments: list[Experiment], group_merged_setti def construct_tpb_graph(experiments: Experiment) -> nx.Graph: """Construct a graph where an edge signifies two experiments are diagonal in a TPB.""" - g = nx.Graph() + g: nx.Graph = nx.Graph() for expt in experiments: if len(expt) != 1: raise ValueError("There must be a single set of ExperimentSettings for each Experiment.") diff --git a/pyquil/experiment/_main.py b/pyquil/experiment/_main.py index 21ff055df..78597cf87 100644 --- a/pyquil/experiment/_main.py +++ b/pyquil/experiment/_main.py @@ -414,7 +414,7 @@ def build_symmetrization_memory_maps( zeros = np.zeros(num_meas_registers) for idx, r in enumerate(symm_registers): zeros[r] = a[idx] - memory_maps.append({f"{label}": list(zeros)}) + memory_maps.append({f"{label}": zeros.tolist()}) return memory_maps def generate_calibration_experiment(self) -> "Experiment": diff --git a/pyquil/latex/_diagram.py b/pyquil/latex/_diagram.py index ca0867739..0750febf7 100644 --- a/pyquil/latex/_diagram.py +++ b/pyquil/latex/_diagram.py @@ -286,7 +286,7 @@ def split_on_terminal_measures( elif isinstance(instr, Pragma): if instr.command == PRAGMA_END_GROUP: warn( - "Alignment of terminal MEASURE operations may" "conflict with gate group declaration.", + "Alignment of terminal MEASURE operations mayconflict with gate group declaration.", stacklevel=2, ) in_group = True @@ -347,7 +347,7 @@ def build(self) -> DiagramState: self._build_measure() elif isinstance(instr, Gate): if "FORKED" in instr.modifiers: - raise ValueError("LaTeX output does not currently support" f"FORKED modifiers: {instr}.") + raise ValueError(f"LaTeX output does not currently supportFORKED modifiers: {instr}.") # the easy case is 1q operations if len(instr.qubits) == 1: self._build_1q_unitary() @@ -357,7 +357,7 @@ def build(self) -> DiagramState: else: self._build_generic_unitary() elif isinstance(instr, UNSUPPORTED_INSTRUCTION_CLASSES): - raise ValueError("LaTeX output does not currently support" f"the following instruction: {instr.out()}") + raise ValueError(f"LaTeX output does not currently supportthe following instruction: {instr.out()}") else: self.index += 1 diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index b9fc0d066..890f13869 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -166,7 +166,7 @@ def get_instruction_unitary( # quax parametric gates may return Operator instead of Unitary; wrap if needed if not isinstance(result, qx.Unitary): - result = qx.Unitary.from_matrix(result.matrix, result.dims) # type: ignore[union-attr] + result = qx.Unitary.from_matrix(result.matrix, result.dims) return result @@ -310,7 +310,7 @@ def from_pauli_noise( all_pauli_terms = tuple("".join(term) for term in product("IXYZ", repeat=num_qubits)) - pauli_error_rates = [] + pauli_error_rates: list[float] = [] for term in reversed(all_pauli_terms): if term in pauli_noise: error_rate = pauli_noise[term] @@ -777,7 +777,7 @@ def __eq__(self, other: object) -> bool: return False return bool(jnp.isclose(float(qx.process_fidelity(self.process, other.process)), 1.0, atol=1e-9)) - __hash__ = None + __hash__ = None # type: ignore[assignment] def __matmul__(self, other: "Channel") -> "Channel": """ @@ -843,7 +843,7 @@ class MeasurementChannel: def qubits(self) -> list[int]: """The qubits which the measurement applies to.""" qubit = self.inst.qubit - return [qubit.index if hasattr(qubit, "index") else int(qubit)] # type: ignore[union-attr,arg-type] + return [qubit.index if hasattr(qubit, "index") else int(qubit)] # ────────────────────────────────────────────── # Constructors @@ -1023,7 +1023,7 @@ def confusion_matrix(self) -> Array: Shape ``(num_outcomes, d_measured)``. Entry ``[i, j]`` is P(outcome i | prepared j). """ - return self.process.confusion_matrix + return self.process.confusion_matrix # type: ignore[no-any-return] @cached_property def transition_matrix(self) -> Array: @@ -1032,7 +1032,7 @@ def transition_matrix(self) -> Array: Shape ``(d, d)``. Entry ``[k, j]`` is P(ending in k | input j), marginalized over all measurement outcomes. """ - return self.process.transition_matrix + return self.process.transition_matrix # type: ignore[no-any-return] @cached_property def non_demolition_fidelity(self) -> float: @@ -1146,7 +1146,7 @@ def __eq__(self, other: object) -> bool: return False return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) - __hash__ = None + __hash__ = None # type: ignore[assignment] def __matmul__(self, other: "MeasurementChannel") -> "MeasurementChannel": """ @@ -1252,7 +1252,7 @@ def qubits(self) -> list[int]: qubit = self.inst.qubit if qubit is None: return [] - return [qubit.index if hasattr(qubit, "index") else int(qubit)] # type: ignore[union-attr,arg-type] + return [qubit.index if hasattr(qubit, "index") else int(qubit)] @cached_property def fidelity(self) -> float: @@ -1349,7 +1349,7 @@ def __eq__(self, other: object) -> bool: return False return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) - __hash__ = None + __hash__ = None # type: ignore[assignment] @dataclass(frozen=True) @@ -1455,8 +1455,7 @@ def from_json(cls: type[CycleChannel], json_str: str) -> "CycleChannel": "MeasurementChannel": MeasurementChannel, } constituent_channels: list["Channel | MeasurementChannel"] = [ - _type_map[ch_data["type"]].from_json(ch_data["data"]) # type: ignore[index] - for ch_data in data["channels"] + _type_map[ch_data["type"]].from_json(ch_data["data"]) for ch_data in data["channels"] ] return _build_cycle_channel(constituent_channels) @@ -1476,7 +1475,7 @@ def __eq__(self, other: object) -> bool: return False return self.channels == other.channels - __hash__ = None + __hash__ = None # type: ignore[assignment] def _channel_to_formal_inst(channel: Channel | MeasurementChannel) -> Gate | Measurement: @@ -1510,7 +1509,7 @@ def _build_cycle_channel( name=cycle_name, parameters=[], qubits=[FormalArgument(f"q{q}") for q in all_qubits], - instructions=list(formal_insts), # type: ignore[arg-type] + instructions=list(formal_insts), ) inst = Gate(name=cycle_name, params=[], qubits=all_qubits) return CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels)) diff --git a/pyquil/noise/_legacy_noise.py b/pyquil/noise/_legacy_noise.py index 8e7c4f013..3d0cd823f 100644 --- a/pyquil/noise/_legacy_noise.py +++ b/pyquil/noise/_legacy_noise.py @@ -383,7 +383,7 @@ def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> tup return np.diag([1, 1, 1, -1]), "NOISY-CZ" raise NoisyGateUndefined( - f"Undefined gate and params: {gate_name}{params}\n" "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ" + f"Undefined gate and params: {gate_name}{params}\nPlease restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ" ) @@ -805,5 +805,3 @@ def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> list[list[int]]: if bitstrings is None: raise ValueError("No readout data found in result.") return cast(list[list[int]], bitstrings.tolist()) - - diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index cb6b9eb05..75531c2b6 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -55,6 +55,7 @@ if TYPE_CHECKING: from pyquil import Program + from pyquil.paulis import PauliSum, PauliTerm logger = logging.getLogger(__name__) @@ -126,7 +127,7 @@ def _channel_map( self, ) -> dict[Gate | Measurement | ResetQubit, Channel | MeasurementChannel | ResetChannel | CycleChannel]: """Map from instruction to channel for fast lookup.""" - return {ch.inst: ch for ch in self.channels} + return {ch.inst: ch for ch in self.channels} # type: ignore[misc] @overload def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... @@ -265,7 +266,7 @@ def from_json(cls: type[NoiseModel], json_str: str) -> "NoiseModel": ch_cls = _type_map.get(ch_data["type"]) if ch_cls is None: raise ValueError(f"Unknown channel type: {ch_data['type']}") - channels.append(ch_cls.from_json(ch_data["data"])) + channels.append(ch_cls.from_json(ch_data["data"])) # type: ignore[attr-defined] return cls(channels=channels) # ────────────────────────────────────────────── @@ -304,7 +305,7 @@ def __add__(self, other: "NoiseModel") -> "NoiseModel": # Both have a channel for this instruction — compose them # (only same-type composition is defined) composed = mine @ theirs # type: ignore[operator] - combined.append(composed) # type: ignore[arg-type] + combined.append(composed) elif mine is not None: combined.append(mine) elif theirs is not None: @@ -427,7 +428,7 @@ def _light_cone_program(program: Program, qubits: list[int]) -> Program: relevant_qubits = set(qubits) included: list[Gate] = [] for inst in reversed(gate_instructions): - inst_qubits = {q.index for q in inst.qubits} + inst_qubits = {q.index for q in inst.qubits} # type: ignore[union-attr] if inst_qubits & relevant_qubits: included.append(inst) relevant_qubits |= inst_qubits @@ -440,7 +441,7 @@ def _light_cone_program(program: Program, qubits: list[int]) -> Program: def estimate_program_observable_fidelity( program: Program, noise_model: NoiseModelLike, - observable: "PauliSum" | "PauliTerm", + observable: PauliSum | PauliTerm, ) -> float: """Estimate program fidelity restricted to the backward light cone of *observable*. @@ -459,6 +460,6 @@ def estimate_program_observable_fidelity( if isinstance(observable, PauliTerm): observable = PauliSum(terms=[observable]) - qubits = [int(q) for term in observable.terms for q, _ in term.operations_as_set()] + qubits = [int(q) for term in observable.terms for q, _ in term.operations_as_set()] # type: ignore[arg-type] reduced_program = _light_cone_program(program, qubits) return estimate_program_fidelity(reduced_program, noise_model) diff --git a/pyquil/operator_estimation.py b/pyquil/operator_estimation.py index 50a2e97da..6158abf20 100644 --- a/pyquil/operator_estimation.py +++ b/pyquil/operator_estimation.py @@ -209,9 +209,7 @@ def measure_observables( # calibration readout only works with symmetrization turned on if calibrate_readout is not None and symmetrization != SymmetrizationLevel.EXHAUSTIVE: - raise ValueError( - "Readout calibration only currently works with exhaustive readout " "symmetrization turned on." - ) + raise ValueError("Readout calibration only currently works with exhaustive readout symmetrization turned on.") # generate programs for each group of simultaneous settings. programs, meas_qubits = _generate_experiment_programs(tomo_experiment, reset) diff --git a/pyquil/paulis.py b/pyquil/paulis.py index 78720a2ae..a7baf72c7 100644 --- a/pyquil/paulis.py +++ b/pyquil/paulis.py @@ -405,9 +405,7 @@ def from_list( :return: PauliTerm """ if not all([isinstance(op, tuple) for op in terms_list]): - raise TypeError( - "The type of terms_list should be a list of (name, index) " "tuples suitable for PauliTerm()." - ) + raise TypeError("The type of terms_list should be a list of (name, index) tuples suitable for PauliTerm().") pterm = PauliTerm("I", 0) if not all([op[0] in PAULI_OPS for op in terms_list]): diff --git a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py index c26b0c484..f3109de24 100644 --- a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py +++ b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py @@ -46,13 +46,12 @@ def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA: if operation.node_count == 1: if len(site.node_ids) != 1: raise QCSISAParseError( - f"operation {operation.name} has node count 1, but " f"site has {len(site.node_ids)} node_ids" + f"operation {operation.name} has node count 1, but site has {len(site.node_ids)} node_ids" ) operation_qubit = get_qubit(device, site.node_ids[0]) if operation_qubit is None: raise QCSISAParseError( - f"operation {operation.name} has node {site.node_ids[0]} " - "but node not declared in architecture" + f"operation {operation.name} has node {site.node_ids[0]} but node not declared in architecture" ) if operation.name in qubit_operations_seen[operation_qubit.id]: @@ -71,7 +70,7 @@ def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA: elif operation.node_count == 2: if len(site.node_ids) != 2: QCSISAParseError( - f"operation {operation.name} has node count 2, but site " f"has {len(site.node_ids)} node_ids" + f"operation {operation.name} has node count 2, but site has {len(site.node_ids)} node_ids" ) operation_edge = get_edge(device, site.node_ids[0], site.node_ids[1]) diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py index e6125f10a..11cba4a23 100644 --- a/pyquil/quilbase.py +++ b/pyquil/quilbase.py @@ -743,7 +743,9 @@ def _validate_matrix( raise TypeError("Matrix argument must be a list or NumPy array/matrix") if not _is_perfect_power(rows): - raise ValueError(f"Dimension of matrix must be a perfect power of an integer (e.g. 2, 3, 4, 8, 9, ...), got {rows}") + raise ValueError( + f"Dimension of matrix must be a perfect power of an integer (e.g. 2, 3, 4, 8, 9, ...), got {rows}" + ) if not contains_parameters: np_matrix = np.asarray(matrix) @@ -761,8 +763,8 @@ def get_constructor(self) -> Union[Callable[..., Gate], Callable[..., Callable[. For example, `mygate.get_constructor()(1) applies the gate to qubit 1.` """ if self.parameters: - return lambda *params: lambda *qubits: Gate( - name=self.name, params=list(params), qubits=list(map(unpack_qubit, qubits)) + return lambda *params: ( + lambda *qubits: Gate(name=self.name, params=list(params), qubits=list(map(unpack_qubit, qubits))) ) else: return lambda *qubits: Gate(name=self.name, params=[], qubits=list(map(unpack_qubit, qubits))) diff --git a/pyquil/simulation/_reference.py b/pyquil/simulation/_reference.py index fe5a72936..bb68073cd 100644 --- a/pyquil/simulation/_reference.py +++ b/pyquil/simulation/_reference.py @@ -241,7 +241,7 @@ def set_initial_state(self, state_matrix: np.ndarray) -> "ReferenceDensitySimula self.initial_density = state_matrix else: raise ValueError( - "The state matrix is not valid. It must be Hermitian, trace one, " "and have non-negative eigenvalues." + "The state matrix is not valid. It must be Hermitian, trace one, and have non-negative eigenvalues." ) return self diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index e70853463..804b2aed8 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -234,7 +234,7 @@ def resolver_from_program( # ── Expand instructions, building DAG and recipes in one pass ── - dag = nx.DiGraph() + dag: nx.DiGraph = nx.DiGraph() node_order: List[int] = [] last_on_qubit: Dict[int, int] = {} # qubit_index → last node key @@ -242,7 +242,9 @@ def resolver_from_program( expanded_insts: List[Gate | Measurement | ResetQubit | Reset] = [] expanded_channels: List[Channel | MeasurementChannel | None] = [] - def _emit(inst: Gate | Measurement | ResetQubit | Reset, channel: Channel | MeasurementChannel | None = None) -> None: + def _emit( + inst: Gate | Measurement | ResetQubit | Reset, channel: Channel | MeasurementChannel | None = None + ) -> None: """Emit an instruction: add a DAG node and record the channel.""" if isinstance(inst, Gate): qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) @@ -270,8 +272,8 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: ch = None _emit(inst, ch) elif isinstance(inst, Measurement): - ch = noise_model.get_channel(inst) if noise_model is not None else None - _emit(inst, ch if isinstance(ch, MeasurementChannel) else None) + ch_m: MeasurementChannel | None = noise_model.get_channel(inst) if noise_model is not None else None + _emit(inst, ch_m if isinstance(ch_m, MeasurementChannel) else None) else: _emit(inst) @@ -319,7 +321,7 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: inst = expanded_insts[node_key] if isinstance(inst, Gate): subsystem = dag.nodes[node_key]["qubits"] - channel = expanded_channels[node_key] + channel = expanded_channels[node_key] # type: ignore[assignment] if channel is None and noise_model is not None: channel = noise_model.get_channel(inst) if channel is not None and isinstance(channel, Channel): @@ -344,15 +346,15 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: match inst: case Gate(): - channel = expanded_channels[node_key] - if channel is None and noise_model is not None: - channel = noise_model.get_channel(inst) + channel2: Channel | MeasurementChannel | CycleChannel | None = expanded_channels[node_key] + if channel2 is None and noise_model is not None: + channel2 = noise_model.get_channel(inst) - if channel is not None and isinstance(channel, Channel): - recipes.append((channel.process, subsystem)) - elif channel is not None and isinstance(channel, MeasurementChannel): + if channel2 is not None and isinstance(channel2, Channel): + recipes.append((channel2.process, subsystem)) + elif channel2 is not None and isinstance(channel2, MeasurementChannel): raise ValueError(f"MeasurementChannel cannot be applied to expanded gate {inst}.") - elif channel is not None and isinstance(channel, CycleChannel): + elif channel2 is not None and isinstance(channel2, CycleChannel): raise ValueError(f"CycleChannel for {inst.name} was not expanded before resolver construction.") elif _is_parameterized(inst): gate_name = inst.name @@ -371,13 +373,13 @@ def _make_param_recipe( pi: List[int], ) -> Callable[[Array], qx.Unitary]: def recipe(params: Array) -> qx.Unitary: - resolved = [] + resolved: list[Any] = [] for p, pv in zip(cp, pi): if pv >= 0: resolved.append(params[pv]) else: resolved.append(float(p.real) if hasattr(p, "real") else float(p)) - result = gdef(*resolved) if callable(gdef) else gdef # type: ignore[operator] + result = gdef(*resolved) if callable(gdef) else gdef if not isinstance(result, qx.Unitary): result = cast(Any, result) result = qx.Unitary.from_matrix(result.matrix, result.dims) @@ -493,7 +495,7 @@ def adapt_for_trajectory( result.append((km, subsystem)) else: # Unitary, KrausMap, QuantumInstrument — pass through - result.append((op, subsystem)) # type: ignore[arg-type] + result.append((op, subsystem)) return result diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 405de2f0e..6b6b81d1d 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -34,7 +34,7 @@ import logging import time -from typing import List, Tuple +from typing import Any, List, Tuple import jax import jax.numpy as jnp @@ -131,7 +131,7 @@ def compress(self, resolved: List[ResolvedOp]) -> List[ResolvedOp]: """Merge operators via greedy edge contraction.""" return self._compress_fn(resolved) - def compute(self, params: Array, **kwargs): + def compute(self, params: Array, **kwargs: Any) -> Any: """Compute the simulation result. Subclasses must override.""" raise NotImplementedError @@ -171,7 +171,7 @@ def _validate(self, program: Program) -> None: if isinstance(inst, (Reset, ResetQubit)): raise ValueError(f"PureStateVectorSimulator does not support resets. Found: {inst}") - def compute(self, params: Array) -> qx.StateVector: + def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] """Compute the final state vector. :param params: Flat parameter vector from :meth:`linearize`. @@ -241,7 +241,7 @@ def __init__( super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) self._rho0 = qx.zero_state_matrix(dims=self.dims) - def compute(self, params: Array) -> qx.DensityMatrix: + def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] """Compute the final density matrix. :param params: Flat parameter vector from :meth:`linearize`. @@ -306,7 +306,7 @@ def adapt(self, compressed: List[ResolvedOp]) -> List[TrajectoryOp]: """Convert compressed ops to trajectory-compatible types.""" return adapt_for_trajectory(compressed, self._kraus_truncation_threshold) - def compute( + def compute( # type: ignore[override] self, params: Array, key: Array, diff --git a/pyquil/simulation/tools.py b/pyquil/simulation/tools.py index 433bf84ca..935be18ab 100644 --- a/pyquil/simulation/tools.py +++ b/pyquil/simulation/tools.py @@ -306,7 +306,7 @@ def _gate_matrix(gate: Gate) -> np.ndarray: elif mod == "CONTROLLED": child = _strip_modifiers(gate, limit=1) matrix = _gate_matrix(child) - return np.kron(zero, np.eye(*matrix.shape)) + np.kron(one, matrix) # type: ignore + return np.kron(zero, np.eye(*matrix.shape)) + np.kron(one, matrix) elif mod == "FORKED": if len(gate.params) % 2 != 0: raise ValueError("FORKED gates must have an even number of parameters.") diff --git a/pyquil/transform.py b/pyquil/transform.py index 5eb427c8d..6b8689bbb 100644 --- a/pyquil/transform.py +++ b/pyquil/transform.py @@ -52,11 +52,11 @@ def copy_everything_except_instructions( program_definitions._program.add_instruction(inst) if include_kraus is True: - for inst in program.instructions: - if isinstance(inst, Pragma): + for kraus_inst in program.instructions: + if isinstance(kraus_inst, Pragma): try: - if inst.command == "ADD-KRAUS": - program_definitions._program.add_instruction(inst) + if kraus_inst.command == "ADD-KRAUS": + program_definitions._program.add_instruction(kraus_inst) # type: ignore[arg-type] except Exception: pass @@ -93,7 +93,7 @@ def unparameterize(program: Program, memory_map: MemoryMap) -> Program: if len(inst.params) > 0: unparameterized_program += Gate( name=inst.name, - params=[substitute(p, parameter_substitution_map) for p in inst.params], + params=[substitute(p, parameter_substitution_map) for p in inst.params], # type: ignore[arg-type] qubits=inst.qubits, ) else: @@ -128,9 +128,9 @@ def expand_defcircuit_body( for circuit_inst in defcircuit.instructions: if isinstance(circuit_inst, Gate): circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] + circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] # type: ignore[index,misc] if hasattr(circuit_inst, "params"): - circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] + circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] # type: ignore[arg-type] if circuit_inst.name in circuit_definitions: yield from expand_defcircuit_body( circuit_inst, circuit_definitions[circuit_inst.name], circuit_definitions @@ -139,14 +139,14 @@ def expand_defcircuit_body( yield circuit_inst elif isinstance(circuit_inst, Measurement): circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] # type: ignore[index] yield circuit_inst elif isinstance(circuit_inst, ResetQubit): circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] # type: ignore[index] yield circuit_inst else: - yield deepcopy(circuit_inst) + yield deepcopy(circuit_inst) # type: ignore[misc] def expand_defcircuits( @@ -203,7 +203,9 @@ def _should_expand(inst: Gate) -> bool: expanded_instructions: List = [] for inst in instructions: if isinstance(inst, Gate) and _should_expand(inst): - expanded_instructions.extend(expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions)) + expanded_instructions.extend( + expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions) + ) else: expanded_instructions.append(inst) diff --git a/pyquil/wavefunction.py b/pyquil/wavefunction.py index c6161a68d..944409858 100644 --- a/pyquil/wavefunction.py +++ b/pyquil/wavefunction.py @@ -48,7 +48,7 @@ def __init__(self, amplitude_vector: np.ndarray): self.amplitudes: np.ndarray = np.asarray(amplitude_vector) sumprob = np.sum(self.probabilities()) if not np.isclose(sumprob, 1.0): - raise ValueError("The wavefunction is not normalized. " f"The probabilities sum to {sumprob} instead of 1") + raise ValueError(f"The wavefunction is not normalized. The probabilities sum to {sumprob} instead of 1") @staticmethod def zeros(qubit_num: int) -> "Wavefunction": From 630475d70dab9620047fca02fe7845bd8a8d0f15 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 15:52:56 +0000 Subject: [PATCH 07/37] Style and formatting --- Makefile | 8 ++ pyquil/noise/__init__.py | 20 ++-- pyquil/noise/_channels.py | 203 +++++++++++++------------------- pyquil/noise/_noise_model.py | 40 +++---- pyquil/simulation/_resolver.py | 117 +++++++++--------- pyquil/simulation/_simulator.py | 36 +++--- pyquil/transform.py | 30 ++--- 7 files changed, 198 insertions(+), 256 deletions(-) diff --git a/Makefile b/Makefile index e1db7e614..ff065ee1c 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,14 @@ DEFAULT_QUILC_URL=tcp://localhost:5555 DEFAULT_QVM_URL=http://localhost:5000 DOCKER_TAG=rigetti/forest:$(COMMIT_HASH) +.DEFAULT := help + +.PHONY: help +help: + @awk 'BEGIN { FS=":.*##"; print "Supported Makefile commands:\n" } \ + /^[a-zA-Z0-9_-]+:.*##/ { cmd=$$1; desc=$$2; printf " \033[36m%-20s\033[0m %s\n", cmd, desc } \ + END { print "" }' $(MAKEFILE_LIST) + .PHONY: all all: dist diff --git a/pyquil/noise/__init__.py b/pyquil/noise/__init__.py index e1ea7e3c5..bace195bf 100644 --- a/pyquil/noise/__init__.py +++ b/pyquil/noise/__init__.py @@ -1,5 +1,4 @@ -""" -pyquil.noise — Noise modeling for quantum simulators. +"""pyquil.noise — Noise modeling for quantum simulators. This package provides: @@ -21,17 +20,17 @@ from pyquil.noise._legacy_noise import ( ANGLE_TOLERANCE, INFINITY, - KrausModel, NO_NOISE, + KrausModel, NoiseModel, NoisyGateUndefined, - _bitstring_probs_by_qubit, - _check_kraus_ops, - _create_kraus_pragmas, - _decoherence_noise_model, - _get_program_gates, - _noise_model_program_header, - _run, + _bitstring_probs_by_qubit, # noqa: F401 + _check_kraus_ops, # noqa: F401 + _create_kraus_pragmas, # noqa: F401 + _decoherence_noise_model, # noqa: F401 + _get_program_gates, # noqa: F401 + _noise_model_program_header, # noqa: F401 + _run, # noqa: F401 add_decoherence_noise, append_kraus_to_gate, apply_noise_model, @@ -50,7 +49,6 @@ tensor_kraus_maps, ) - __all__ = [ # Noise model "ANGLE_TOLERANCE", diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index 890f13869..f885d3497 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -""" -Noise channel classes and gate-resolution utilities. +"""Noise channel classes and gate-resolution utilities. This module defines ``Channel``, ``MeasurementChannel``, ``ResetChannel``, and ``CycleChannel`` dataclasses for representing noise in quantum circuits, along @@ -37,12 +36,12 @@ import quax as qx from jax import Array from plotly.graph_objs import Figure +from quil.expression import Expression as QuilExpression from quil.program import Program as RSProgram from scipy.linalg import logm as scipy_logm from pyquil.quilatom import Expression, FormalArgument, Parameter, substitute from pyquil.quilbase import DefCircuit, DefGate, Gate, Measurement, Reset -from quil.expression import Expression as QuilExpression if TYPE_CHECKING: from pyquil import Program @@ -69,8 +68,7 @@ def _parse_quil_instruction(quil_str: str) -> Gate | Measurement | Reset: def _resolve_params(params: list) -> list[float]: - """ - Resolve gate parameters to concrete float values. + """Resolve gate parameters to concrete float values. :param params: The gate parameters (may include symbolic Parameters or Expressions). :return: A list of concrete float values. @@ -94,8 +92,7 @@ def _resolve_params(params: list) -> list[float]: def get_custom_gates_from_program(program: Program) -> CustomGateMap: - """ - Extract custom gate definitions from a Quil program. + """Extract custom gate definitions from a Quil program. Returns a dictionary mapping gate names to unitary matrices (for fixed gates) or callables (for parametric gates). Does not include the standard gate set — use this to augment @@ -129,8 +126,7 @@ def get_instruction_unitary( inst: Gate, custom_gates: CustomGateMap | None = None, ) -> qx.Unitary: - """ - Get the unitary matrix associated with a gate instruction. + """Get the unitary matrix associated with a gate instruction. Looks up the gate by name — first in ``custom_gates`` (if provided), then in the standard quax gate table ``qx.gates.QUANTUM_GATES``. Parametric gates are supported @@ -172,8 +168,7 @@ def get_instruction_unitary( @dataclass(frozen=True) class Channel: - """ - A noise channel attaches a superoperator to a specific gate. + """A noise channel attaches a superoperator to a specific gate. The superoperator *includes* the gate unitary, so the channel replaces the gate rather than being applied after it. @@ -220,9 +215,8 @@ def from_gate_fidelity( inst: Gate, fidelity: float, custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a depolarizing noise channel from an average gate fidelity. + ) -> Channel: + r"""Create a depolarizing noise channel from an average gate fidelity. The resulting channel is the composition of the ideal gate unitary with a depolarizing channel calibrated to the specified fidelity: @@ -243,9 +237,8 @@ def from_pauli_fidelity( inst: Gate, pauli_fidelity: float, custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a depolarizing noise channel from a process (Pauli) fidelity. + ) -> Channel: + r"""Create a depolarizing noise channel from a process (Pauli) fidelity. The process fidelity :math:`F_e` is related to the average gate fidelity by :math:`F_{\\mathrm{avg}} = (d \\cdot F_e + 1) / (d + 1)`. @@ -265,9 +258,8 @@ def from_depolarizing_constant( inst: Gate, depolarizing_constant: float, custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a depolarizing noise channel from a depolarization constant. + ) -> Channel: + r"""Create a depolarizing noise channel from a depolarization constant. The depolarizing constant :math:`p` parameterizes the channel as :math:`\\mathcal{D}_p(\\rho) = p \\, \\rho + (1-p) \\, I/d`. @@ -288,9 +280,8 @@ def from_pauli_noise( inst: Gate, pauli_noise: dict[str, float], custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a stochastic Pauli noise channel from Pauli error rates. + ) -> Channel: + """Create a stochastic Pauli noise channel from Pauli error rates. The noise is specified as a dictionary mapping Pauli strings to error probabilities, e.g. ``{"XX": 0.03, "ZI": 0.001}``. The probabilities must sum to at most 1.0; @@ -319,7 +310,7 @@ def from_pauli_noise( else: error_rate = 0 pauli_error_rates.append(error_rate) - assert jnp.isclose(1.0, sum(pauli_error_rates)) + assert jnp.isclose(1.0, sum(pauli_error_rates)) # noqa: S101 pauli_error_rates = list(reversed(pauli_error_rates)) # Build Pauli Kraus operators using quax ensembles @@ -344,9 +335,8 @@ def from_random_coherent_error( process_fidelity: float, rng: np.random.Generator | None = None, custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a channel with a random coherent (unitary) error at the specified process fidelity. + ) -> Channel: + r"""Create a channel with a random coherent (unitary) error at the specified process fidelity. A random unitary close to identity is generated with the given process fidelity, then composed with the ideal gate. @@ -396,9 +386,8 @@ def from_mixture( constituents: list[qx.Unitary], probabilities: list[float], custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a mixture channel from a set of unitary errors with given probabilities. + ) -> Channel: + r"""Create a mixture channel from a set of unitary errors with given probabilities. The channel is :math:`\\mathcal{E}(\\rho) = (1-\\sum p_i) U\\rho U^\\dagger + \\sum p_i V_i U \\rho U^\\dagger V_i^\\dagger` where :math:`U` is the ideal gate and :math:`V_i` are the error unitaries. @@ -434,9 +423,8 @@ def from_coherence_times( t1s: list[float], t2s: list[float] | None = None, custom_gates: CustomGateMap | None = None, - ) -> "Channel": - """ - Create a decoherence Channel based on the coherence times. + ) -> Channel: + """Create a decoherence Channel based on the coherence times. In this construction, decoherence is applied _after_ the ideal gate unitary. @@ -448,11 +436,11 @@ def from_coherence_times( unitary = get_instruction_unitary(inst, custom_gates) qubits = inst.get_qubit_indices() num_sys = len(qubits) - assert num_sys == len(t1s) + assert num_sys == len(t1s) # noqa: S101 if t2s is None: t2s = [2 * t1 for t1 in t1s] else: - assert num_sys == len(t2s) + assert num_sys == len(t2s) # noqa: S101 t1_array = jnp.asarray(t1s) tphi_array = 1 / (1 / jnp.asarray(t2s) - 1 / t1_array) @@ -473,8 +461,7 @@ def from_superoperator( target_unitary: qx.Unitary | None = None, custom_gates: CustomGateMap | None = None, ) -> Channel: - """ - Create a Channel from a pre-built superoperator. + """Create a Channel from a pre-built superoperator. If ``target_unitary`` is not provided it is inferred from the gate instruction using the standard gate set (and ``custom_gates`` if given). @@ -497,8 +484,7 @@ def from_superoperator( @cached_property def noise_process(self) -> qx.SuperOp: - """ - The noise-only channel with the ideal gate unitary factored out. + r"""The noise-only channel with the ideal gate unitary factored out. If the full channel is :math:`\\mathcal{E} = \\Lambda \\circ \\mathcal{U}`, this returns :math:`\\Lambda`. @@ -511,12 +497,12 @@ def noise_process(self) -> qx.SuperOp: @cached_property def fidelity(self) -> float: - """Average gate fidelity :math:`F_{\\mathrm{avg}}` of the channel relative to the ideal gate.""" + r"""Average gate fidelity :math:`F_{\\mathrm{avg}}` of the channel relative to the ideal gate.""" return float(qx.process_fidelity_to_average_fidelity(self.pauli_fidelity, dims=self.unitary.dims[0])) @cached_property def infidelity(self) -> float: - """Average gate infidelity :math:`1 - F_{\\mathrm{avg}}`.""" + r"""Average gate infidelity :math:`1 - F_{\\mathrm{avg}}`.""" return 1.0 - self.fidelity @cached_property @@ -559,9 +545,8 @@ def unitarity(self) -> float: # Channel analysis methods # ────────────────────────────────────────────── - def pauli_twirl(self) -> "Channel": - """ - Return a Pauli-twirled version of this channel. + def pauli_twirl(self) -> Channel: + """Return a Pauli-twirled version of this channel. Pauli twirling projects the channel onto the Pauli diagonal, eliminating off-diagonal coherences in the Pauli-Liouville representation. The @@ -576,8 +561,7 @@ def pauli_twirl(self) -> "Channel": @cached_property def _unitary_error_component(self) -> Array: - """ - Extract the dominant unitary from the noise-only channel. + """Extract the dominant unitary from the noise-only channel. Uses eigendecomposition + SVD polar decomposition to find the closest unitary to the noise channel. @@ -593,9 +577,8 @@ def _unitary_error_component(self) -> Array: u, _, vh = jnp.linalg.svd(dominant_eigenvector.reshape(d, d).T) return u @ vh - def to_coherent_channel(self) -> "Channel": - """ - Isolate the coherent (unitary) component of the noise. + def to_coherent_channel(self) -> Channel: + """Isolate the coherent (unitary) component of the noise. Extracts the dominant unitary from the noise Choi matrix via polar decomposition and returns a channel consisting of that unitary error @@ -606,9 +589,8 @@ def to_coherent_channel(self) -> "Channel": coherent_superop = qx.to_superop(u_error_qx @ self.unitary) return replace(self, process=coherent_superop) - def to_stochastic_channel(self) -> "Channel": - """ - Isolate the stochastic (incoherent) component of the noise. + def to_stochastic_channel(self) -> Channel: + r"""Isolate the stochastic (incoherent) component of the noise. The full channel decomposes as :math:`\\mathcal{E} = \\mathcal{S} \\circ \\mathcal{U}_{\\mathrm{err}} \\circ \\mathcal{U}_{\\mathrm{gate}}`. @@ -626,8 +608,7 @@ def to_stochastic_channel(self) -> "Channel": return replace(self, process=qx.SuperOp.from_matrix(stochastic_superop, self.process.dims)) def is_pauli(self) -> bool: - """ - Check if the noise channel is a Pauli (stochastic Pauli) channel. + """Check if the noise channel is a Pauli (stochastic Pauli) channel. A Pauli channel has a diagonal Pauli transfer matrix (noise-only part). """ @@ -636,8 +617,7 @@ def is_pauli(self) -> bool: return bool(jnp.allclose(ptm[mask], 0)) def to_pauli_vector(self) -> Array: - """ - Convert the noise channel to a Pauli error probability vector. + """Convert the noise channel to a Pauli error probability vector. Returns the vector of probabilities for each Pauli error in lexicographic order (II, IX, IY, IZ, XI, XX, ...). The vector sums to 1.0. @@ -668,8 +648,7 @@ def pauli_vector(self) -> Array: # ────────────────────────────────────────────── def plot(self, only_noise: bool = True, show_identity: bool = False) -> Figure: - """ - Plot the Pauli transfer matrix of the channel. + """Plot the Pauli transfer matrix of the channel. :param only_noise: If True, plot the noise-only channel (gate unitary factored out). If False, plot the full channel including the gate unitary. @@ -707,8 +686,7 @@ def plot(self, only_noise: bool = True, show_identity: bool = False) -> Figure: # ────────────────────────────────────────────── def to_json(self) -> str: - """ - Serialize Channel to a JSON string. + """Serialize Channel to a JSON string. :return: JSON string representation. """ @@ -727,16 +705,15 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: type[Channel], json_str: str) -> "Channel": - """ - Deserialize a Channel from a JSON string. + def from_json(cls: type[Channel], json_str: str) -> Channel: + """Deserialize a Channel from a JSON string. :param json_str: JSON string as produced by :meth:`to_json`. :return: Channel instance. """ data = json.loads(json_str) inst = _parse_quil_instruction(data["inst"]) - assert isinstance(inst, Gate) + assert isinstance(inst, Gate) # noqa: S101 superop_data = data["superop"] flat = superop_data["_complex_array"] @@ -779,9 +756,8 @@ def __eq__(self, other: object) -> bool: __hash__ = None # type: ignore[assignment] - def __matmul__(self, other: "Channel") -> "Channel": - """ - Compose two channels: ``channel_B @ channel_A``. + def __matmul__(self, other: Channel) -> Channel: + r"""Compose two channels: ``channel_B @ channel_A``. Both channels share the same gate instruction. The composition factors out one copy of the gate unitary so the result represents the sequential @@ -801,9 +777,8 @@ def __matmul__(self, other: "Channel") -> "Channel": composed_superop = qx.to_superop(self.process @ u_dag_superop @ other.process) return replace(self, process=composed_superop) - def __or__(self, other: "Channel | MeasurementChannel") -> "CycleChannel": - """ - Tensor product of two channels on disjoint qubits, producing a CycleChannel. + def __or__(self, other: Channel | MeasurementChannel) -> CycleChannel: + """Tensor product of two channels on disjoint qubits, producing a CycleChannel. The result represents a cycle containing both operations acting in parallel on disjoint qubits. The DefCircuit encodes the parallel operations as @@ -826,8 +801,7 @@ def __or__(self, other: "Channel | MeasurementChannel") -> "CycleChannel": @dataclass(frozen=True) class MeasurementChannel: - """ - A measurement noise channel attaches a quantum instrument to a specific measurement operation. + """A measurement noise channel attaches a quantum instrument to a specific measurement operation. The ``process`` field is a ``qx.QuantumInstrument`` which models both classification errors and post-measurement back-action. @@ -856,9 +830,8 @@ def from_readout_fidelity( fidelity: float, asymmetry: float = 0.0, dim: int = 2, - ) -> "MeasurementChannel": - """ - Create a readout quantum instrument with optional asymmetry. + ) -> MeasurementChannel: + """Create a readout quantum instrument with optional asymmetry. Produces a perfectly QND measurement with the given classification fidelity. Error is distributed only between adjacent levels: P(j+1|j) and P(j|j+1). @@ -900,9 +873,8 @@ def from_confusion_and_transition( inst: Measurement, confusion_matrix: Array, transition_matrix: Array, - ) -> "MeasurementChannel": - """ - Create a MeasurementChannel from a confusion matrix and a transition matrix. + ) -> MeasurementChannel: + """Create a MeasurementChannel from a confusion matrix and a transition matrix. Provides independent control over measurement classification accuracy and post-measurement quantum state evolution. @@ -935,9 +907,8 @@ def from_axis( theta: float = 0.0, phi: float = 0.0, sharpness: float = 1.0, - ) -> "MeasurementChannel": - """ - Create a MeasurementChannel from a Bloch sphere measurement axis. + ) -> MeasurementChannel: + """Create a MeasurementChannel from a Bloch sphere measurement axis. The angles refer to the standard Bloch sphere notation. Theta=0, phi=0 is the Z axis (computational basis measurement). @@ -963,9 +934,8 @@ def from_binary_discriminator( dim: int, threshold: int, fidelity: float = 1.0, - ) -> "MeasurementChannel": - """ - Create a MeasurementChannel for a binary discriminator. + ) -> MeasurementChannel: + """Create a MeasurementChannel for a binary discriminator. Models a measurement that confuses each state at or above ``threshold`` with the state one level below it. This is useful for measurements calibrated as @@ -1061,8 +1031,7 @@ def classification_fidelity(self) -> float: # ────────────────────────────────────────────── def plot(self) -> Figure: - """ - Plot the quantum instrument using the quax visualization. + """Plot the quantum instrument using the quax visualization. Shows per-outcome superoperator matrices and the total CPTP channel. @@ -1084,8 +1053,7 @@ def plot(self) -> Figure: # ────────────────────────────────────────────── def to_json(self) -> str: - """ - Serialize MeasurementChannel to a JSON string. + """Serialize MeasurementChannel to a JSON string. :return: JSON string representation. """ @@ -1105,16 +1073,15 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: type[MeasurementChannel], json_str: str) -> "MeasurementChannel": - """ - Deserialize a MeasurementChannel from a JSON string. + def from_json(cls: type[MeasurementChannel], json_str: str) -> MeasurementChannel: + """Deserialize a MeasurementChannel from a JSON string. :param json_str: JSON string as produced by :meth:`to_json`. :return: MeasurementChannel instance. """ data = json.loads(json_str) inst = _parse_quil_instruction(data["inst"]) - assert isinstance(inst, Measurement) + assert isinstance(inst, Measurement) # noqa: S101 measured_qudits = tuple(data["measured_qudits"]) choi_list = [] @@ -1148,9 +1115,8 @@ def __eq__(self, other: object) -> bool: __hash__ = None # type: ignore[assignment] - def __matmul__(self, other: "MeasurementChannel") -> "MeasurementChannel": - """ - Compose two measurement channels on the same qubit. + def __matmul__(self, other: MeasurementChannel) -> MeasurementChannel: + """Compose two measurement channels on the same qubit. Models sequential application: ``channel_B @ channel_A`` means apply ``channel_A`` first, then ``channel_B``. @@ -1164,9 +1130,8 @@ def __matmul__(self, other: "MeasurementChannel") -> "MeasurementChannel": composed = self.process @ other.process return replace(self, process=composed) - def __or__(self, other: "Channel | MeasurementChannel") -> "CycleChannel": - """ - Tensor product of two channels on disjoint qubits, producing a CycleChannel. + def __or__(self, other: Channel | MeasurementChannel) -> CycleChannel: + """Tensor product of two channels on disjoint qubits, producing a CycleChannel. :param other: Another Channel or MeasurementChannel on disjoint qubits. :return: A CycleChannel representing the tensor product. @@ -1184,8 +1149,7 @@ def __or__(self, other: "Channel | MeasurementChannel") -> "CycleChannel": @dataclass(frozen=True) class ResetChannel: - """ - A reset noise channel attaches a superoperator to a specific reset operation. + """A reset noise channel attaches a superoperator to a specific reset operation. The ``process`` field is a ``qx.SuperOp`` which *includes* the ideal reset, so the channel replaces the reset instruction rather than being applied after it. @@ -1207,9 +1171,8 @@ def from_reset_fidelity( inst: Reset, fidelity: float, dim: int = 2, - ) -> "ResetChannel": - """ - Create a ResetChannel with depolarizing noise scaled to the given process fidelity. + ) -> ResetChannel: + r"""Create a ResetChannel with depolarizing noise scaled to the given process fidelity. The ideal reset channel maps every state to :math:`|0\\rangle\\langle 0|`. Noise is modelled as a depolarising channel applied after the ideal reset. @@ -1256,7 +1219,7 @@ def qubits(self) -> list[int]: @cached_property def fidelity(self) -> float: - """Process fidelity of the reset channel relative to the ideal reset. + r"""Process fidelity of the reset channel relative to the ideal reset. Defined as :math:`F = \\mathrm{Tr}[\\Lambda_{\\mathrm{ideal}}^\\dagger \\Lambda] / d^2` where :math:`\\Lambda` is the Choi matrix of the noisy channel and @@ -1283,8 +1246,7 @@ def noise_process(self) -> qx.SuperOp: # ────────────────────────────────────────────── def plot(self) -> Figure: - """ - Plot the Pauli transfer matrix of the reset channel. + """Plot the Pauli transfer matrix of the reset channel. :return: A Plotly Figure. """ @@ -1298,8 +1260,7 @@ def plot(self) -> Figure: # ────────────────────────────────────────────── def to_json(self) -> str: - """ - Serialize ResetChannel to a JSON string. + """Serialize ResetChannel to a JSON string. :return: JSON string representation. """ @@ -1312,16 +1273,15 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: type[ResetChannel], json_str: str) -> "ResetChannel": - """ - Deserialize a ResetChannel from a JSON string. + def from_json(cls: type[ResetChannel], json_str: str) -> ResetChannel: + """Deserialize a ResetChannel from a JSON string. :param json_str: JSON string as produced by :meth:`to_json`. :return: ResetChannel instance. """ data = json.loads(json_str) inst = _parse_quil_instruction(data["inst"]) - assert isinstance(inst, Reset) + assert isinstance(inst, Reset) # noqa: S101 superop_data = data["superop"] flat = superop_data["_complex_array"] shape = tuple(superop_data["shape"]) @@ -1354,8 +1314,7 @@ def __eq__(self, other: object) -> bool: @dataclass(frozen=True) class CycleChannel: - """ - A cycle noise channel attaches superoperators to a specific cycle. + """A cycle noise channel attaches superoperators to a specific cycle. Cycles can include gates and measurements. The constituent channels are stored directly, allowing fidelity metrics and serialization to be derived from them. @@ -1367,7 +1326,7 @@ class CycleChannel: defcircuit: DefCircuit """The DefCircuit representing the logical cycle to which instruction represents.""" - channels: tuple["Channel | MeasurementChannel", ...] + channels: tuple[Channel | MeasurementChannel, ...] """Constituent channels (one per operation in the cycle) on disjoint qubits.""" # ────────────────────────────────────────────── @@ -1425,8 +1384,7 @@ def pauli_infidelity(self) -> float: # ────────────────────────────────────────────── def to_json(self) -> str: - """ - Serialize CycleChannel to a JSON string. + """Serialize CycleChannel to a JSON string. :return: JSON string representation. """ @@ -1439,9 +1397,8 @@ def to_json(self) -> str: return json.dumps(data) @classmethod - def from_json(cls: type[CycleChannel], json_str: str) -> "CycleChannel": - """ - Deserialize a CycleChannel from a JSON string. + def from_json(cls: type[CycleChannel], json_str: str) -> CycleChannel: + """Deserialize a CycleChannel from a JSON string. The ``inst`` and ``defcircuit`` fields are reconstructed from the constituent channels, consistent with how :func:`_build_cycle_channel` builds them. @@ -1454,7 +1411,7 @@ def from_json(cls: type[CycleChannel], json_str: str) -> "CycleChannel": "Channel": Channel, "MeasurementChannel": MeasurementChannel, } - constituent_channels: list["Channel | MeasurementChannel"] = [ + constituent_channels: list[Channel | MeasurementChannel] = [ _type_map[ch_data["type"]].from_json(ch_data["data"]) for ch_data in data["channels"] ] return _build_cycle_channel(constituent_channels) @@ -1498,8 +1455,8 @@ def _channel_to_formal_inst(channel: Channel | MeasurementChannel) -> Gate | Mea def _build_cycle_channel( - channels: list["Channel | MeasurementChannel"], -) -> "CycleChannel": + channels: list[Channel | MeasurementChannel], +) -> CycleChannel: """Build a CycleChannel from a list of Channel/MeasurementChannel on disjoint qubits.""" all_qubits = sorted(q for ch in channels for q in ch.qubits) cycle_name = "CYCLE" diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index 75531c2b6..547e96a6c 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -""" -Noise model container and program-level fidelity estimation. +"""Noise model container and program-level fidelity estimation. This module defines: @@ -34,24 +33,20 @@ import json import logging +from collections.abc import Iterable, Sequence from dataclasses import dataclass from functools import cached_property, reduce from operator import mul from typing import ( TYPE_CHECKING, - Iterable, Protocol, - Sequence, overload, runtime_checkable, ) -import quax as qx - from pyquil.external.rpcq import CompilerISA -from pyquil.quilbase import Gate, Measurement, ResetQubit - from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel +from pyquil.quilbase import Gate, Measurement, ResetQubit if TYPE_CHECKING: from pyquil import Program @@ -100,8 +95,7 @@ def get_channel(self, inst: Gate | Measurement | ResetQubit) -> ChannelType | No @dataclass(frozen=True) class NoiseModel: - """ - A noise model collects all the noise channels for a given quantum program. + """A noise model collects all the noise channels for a given quantum program. This includes gate channels, measurement channels, reset channels, and cycle channels. @@ -141,8 +135,7 @@ def get_channel(self, inst: ResetQubit) -> ResetChannel | None: ... def get_channel( self, inst: Gate | Measurement | ResetQubit ) -> Channel | MeasurementChannel | ResetChannel | CycleChannel | None: - """ - Retrieve the noise channel associated with a specific instruction. + """Retrieve the noise channel associated with a specific instruction. :param inst: The instruction (gate, measurement, or reset) for which to retrieve the noise channel. :return: The noise channel associated with the instruction, or None if no channel is found. @@ -154,9 +147,8 @@ def get_channel( # ────────────────────────────────────────────── @classmethod - def from_isa(cls: type[NoiseModel], compiler_isa: "CompilerISA") -> "NoiseModel": - """ - Create a noise model from an instruction set architecture. + def from_isa(cls: type[NoiseModel], compiler_isa: CompilerISA) -> NoiseModel: + """Create a noise model from an instruction set architecture. Gate fidelities are converted to depolarizing channels and measurement errors are symmetric. Only gates with concrete numeric parameters are @@ -233,8 +225,7 @@ def from_isa(cls: type[NoiseModel], compiler_isa: "CompilerISA") -> "NoiseModel" # ────────────────────────────────────────────── def to_json(self) -> str: - """ - Serialize NoiseModel to a JSON string. + """Serialize NoiseModel to a JSON string. :return: JSON string representation. """ @@ -247,9 +238,8 @@ def to_json(self) -> str: return json.dumps({"channels": channel_data}) @classmethod - def from_json(cls: type[NoiseModel], json_str: str) -> "NoiseModel": - """ - Deserialize a NoiseModel from a JSON string. + def from_json(cls: type[NoiseModel], json_str: str) -> NoiseModel: + """Deserialize a NoiseModel from a JSON string. :param json_str: JSON string as produced by :meth:`to_json`. :return: NoiseModel instance. @@ -283,9 +273,8 @@ def __hash__(self) -> int: """Hash based on id (NoiseModel is not value-hashable due to array contents).""" return id(self) - def __add__(self, other: "NoiseModel") -> "NoiseModel": - """ - Combine two NoiseModels. + def __add__(self, other: NoiseModel) -> NoiseModel: + """Combine two NoiseModels. For channels with matching instructions, compose them (``channel_A @ channel_B``). For non-overlapping channels, include both. @@ -325,7 +314,7 @@ def __add__(self, other: "NoiseModel") -> "NoiseModel": @dataclass(frozen=True) class DepolarizingNoiseModel: - """A noise model that applies uniform depolarizing noise to every gate. + r"""A noise model that applies uniform depolarizing noise to every gate. For any ``Gate`` instruction, returns a :class:`Channel` with the specified depolarizing constant. Measurements and resets are treated as ideal. @@ -392,8 +381,7 @@ def get_channel(self, inst: Gate | Measurement | ResetQubit) -> ChannelType | No def estimate_program_fidelity(program: Program, noise_model: NoiseModelLike) -> float: - """ - Estimate the program fidelity for a given noise model. + """Estimate the program fidelity for a given noise model. Works by multiplying the gate process fidelities together. Readout noise is not considered. diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 804b2aed8..d96902df6 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -13,11 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -""" -simulation._resolver module ----------------------------- - -Shared infrastructure for the density-matrix and state-vector simulators. +"""Shared infrastructure for the density-matrix and state-vector simulators. This module provides the three front-end stages of the simulation pipeline: @@ -34,7 +30,7 @@ from __future__ import annotations import logging -from typing import Any, Callable, Dict, List, Set, Tuple, Union, cast +from typing import Any, Callable, Union, cast import jax.numpy as jnp import networkx as nx @@ -42,10 +38,6 @@ from jax import Array from pyquil.api import MemoryMap -from pyquil.quil import Program -from pyquil.quilatom import MemoryReference -from pyquil.quilbase import DefCircuit, Gate, Measurement, Reset, ResetQubit - from pyquil.noise._channels import ( Channel, CycleChannel, @@ -56,6 +48,9 @@ from pyquil.noise._noise_model import ( NoiseModelLike, ) +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference +from pyquil.quilbase import DefCircuit, Gate, Measurement, Reset, ResetQubit from pyquil.transform import expand_defcircuit_body logger = logging.getLogger(__name__) @@ -65,16 +60,16 @@ # ────────────────────────────────────────────────────────── # Resolved operations retain the most specific native quax type. -ResolvedOp = Tuple[Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument], Tuple[int, ...]] +ResolvedOp = tuple[Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument], tuple[int, ...]] RecipeOp = Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument] RecipeCallable = Callable[[Array], RecipeOp] -Recipe = Tuple[Union[RecipeOp, RecipeCallable], Tuple[int, ...]] +Recipe = tuple[Union[RecipeOp, RecipeCallable], tuple[int, ...]] # Trajectory operations for the state-vector simulator. -TrajectoryOp = Tuple[Union[qx.Unitary, qx.KrausMap, qx.QuantumInstrument], Tuple[int, ...]] +TrajectoryOp = tuple[Union[qx.Unitary, qx.KrausMap, qx.QuantumInstrument], tuple[int, ...]] # Density-matrix operations. -DensityMatrixOp = Tuple[qx.SuperOp, Tuple[int, ...]] +DensityMatrixOp = tuple[qx.SuperOp, tuple[int, ...]] # Custom gate definitions. CustomGateMap = dict @@ -119,7 +114,7 @@ def linearizer_from_program(program: Program) -> Linearizer: :return: A :class:`Linearizer` instance. """ # Find registers written to by MEASURE — these are output registers, not params - measure_registers: Set[str] = set() + measure_registers: set[str] = set() for inst in program.instructions: if isinstance(inst, Measurement): cr = inst.classical_reg @@ -127,7 +122,7 @@ def linearizer_from_program(program: Program) -> Linearizer: measure_registers.add(cr.name) # Collect parameter references in program order - param_refs: List[Tuple[str, int]] = [] + param_refs: list[tuple[str, int]] = [] for inst in program.instructions: if isinstance(inst, Gate): for param in inst.params: @@ -162,11 +157,11 @@ class Resolver: __slots__ = ("_resolve_fn", "dims") - def __init__(self, resolve_fn: Callable[[Array], List[ResolvedOp]], dims: Tuple[int, ...]) -> None: + def __init__(self, resolve_fn: Callable[[Array], list[ResolvedOp]], dims: tuple[int, ...]) -> None: self._resolve_fn = resolve_fn self.dims = dims - def __call__(self, params: Array) -> List[ResolvedOp]: + def __call__(self, params: Array) -> list[ResolvedOp]: return self._resolve_fn(params) @@ -175,9 +170,9 @@ def _is_parameterized(inst: Gate) -> bool: return any(isinstance(p, MemoryReference) for p in inst.params) -def _measure_registers(program: Program) -> Set[str]: +def _measure_registers(program: Program) -> set[str]: """Return the set of register names that are targets of MEASURE instructions.""" - regs: Set[str] = set() + regs: set[str] = set() for inst in program.instructions: if isinstance(inst, Measurement): cr = inst.classical_reg @@ -189,9 +184,9 @@ def _measure_registers(program: Program) -> Set[str]: def resolver_from_program( program: Program, noise_model: NoiseModelLike | None, - qubit_indices: Dict[int, int], + qubit_indices: dict[int, int], custom_gates: CustomGateMap | None, -) -> Tuple[Resolver, nx.DiGraph, List[int]]: +) -> tuple[Resolver, nx.DiGraph, list[int]]: """Build a :class:`Resolver`, DAG, and node order from a program. The resolver accepts a flat parameter vector and produces one @@ -227,7 +222,7 @@ def resolver_from_program( measure_regs = _measure_registers(program) # Extract DEFCIRCUIT definitions. - circuit_definitions: Dict[str, DefCircuit] = {} + circuit_definitions: dict[str, DefCircuit] = {} for inst in program.instructions: if isinstance(inst, DefCircuit): circuit_definitions[inst.name] = inst @@ -235,12 +230,12 @@ def resolver_from_program( # ── Expand instructions, building DAG and recipes in one pass ── dag: nx.DiGraph = nx.DiGraph() - node_order: List[int] = [] - last_on_qubit: Dict[int, int] = {} # qubit_index → last node key + node_order: list[int] = [] + last_on_qubit: dict[int, int] = {} # qubit_index → last node key # Flat lists populated during instruction iteration. - expanded_insts: List[Gate | Measurement | ResetQubit | Reset] = [] - expanded_channels: List[Channel | MeasurementChannel | None] = [] + expanded_insts: list[Gate | Measurement | ResetQubit | Reset] = [] + expanded_channels: list[Channel | MeasurementChannel | None] = [] def _emit( inst: Gate | Measurement | ResetQubit | Reset, channel: Channel | MeasurementChannel | None = None @@ -302,7 +297,7 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: # Assign parameter vector indices to each gate's MemoryReference params. param_counter = 0 - gate_param_indices: Dict[int, List[int]] = {} + gate_param_indices: dict[int, list[int]] = {} for idx in node_order: inst = expanded_insts[idx] if isinstance(inst, Gate): @@ -316,7 +311,7 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: gate_param_indices[idx] = indices # Pre-scan gate instructions to infer per-qudit dimensions. - qudit_dims: Dict[int, int] = {} # qubit_index → dimension + qudit_dims: dict[int, int] = {} # qubit_index → dimension for node_key in node_order: inst = expanded_insts[node_key] if isinstance(inst, Gate): @@ -332,13 +327,13 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: try: unitary = get_instruction_unitary(inst, custom_gates=custom_gates) op_dims = unitary.dims[0] - except Exception: + except Exception: # noqa: S112 continue for slot, dim in zip(subsystem, op_dims): if dim > qudit_dims.get(slot, 2): qudit_dims[slot] = dim - recipes: List[Recipe] = [] + recipes: list[Recipe] = [] for node_key in node_order: inst = expanded_insts[node_key] @@ -370,7 +365,7 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: def _make_param_recipe( gdef: object, cp: list, - pi: List[int], + pi: list[int], ) -> Callable[[Array], qx.Unitary]: def recipe(params: Array) -> qx.Unitary: resolved: list[Any] = [] @@ -420,8 +415,8 @@ def recipe(params: Array) -> qx.Unitary: dim = qudit_dims.get(q_idx, 2) recipes.append((qx.gates.RESET(dim=dim), (q_idx,))) - def resolve(params: Array) -> List[ResolvedOp]: - ops: List[ResolvedOp] = [] + def resolve(params: Array) -> list[ResolvedOp]: + ops: list[ResolvedOp] = [] for op_or_fn, subsystem in recipes: if isinstance(op_or_fn, (qx.Unitary, qx.KrausMap, qx.SuperOp, qx.QuantumInstrument)): ops.append((op_or_fn, subsystem)) @@ -449,8 +444,8 @@ def resolve(params: Array) -> List[ResolvedOp]: def adapt_for_density_matrix( - ops: List[ResolvedOp], -) -> List[DensityMatrixOp]: + ops: list[ResolvedOp], +) -> list[DensityMatrixOp]: """Convert resolved operations to ``(SuperOp, subsystem)`` pairs for density-matrix simulation. * ``Unitary`` → ``qx.to_superop(op)`` @@ -461,7 +456,7 @@ def adapt_for_density_matrix( :param ops: Resolved operations from :func:`build_resolver`. :return: List of ``(SuperOp, subsystem)`` pairs. """ - result: List[DensityMatrixOp] = [] + result: list[DensityMatrixOp] = [] for op, subsystem in ops: if isinstance(op, qx.SuperOp): result.append((op, subsystem)) @@ -474,9 +469,9 @@ def adapt_for_density_matrix( def adapt_for_trajectory( - ops: List[ResolvedOp], + ops: list[ResolvedOp], kraus_truncation_threshold: float = 1e-6, -) -> List[TrajectoryOp]: +) -> list[TrajectoryOp]: """Convert resolved operations to trajectory-compatible types. * ``Unitary`` → pass through @@ -488,7 +483,7 @@ def adapt_for_trajectory( :param kraus_truncation_threshold: Threshold for Kraus truncation. :return: List of ``(Unitary | KrausMap | QuantumInstrument, subsystem)`` pairs. """ - result: List[TrajectoryOp] = [] + result: list[TrajectoryOp] = [] for op, subsystem in ops: if isinstance(op, qx.SuperOp): km = qx.truncate_kraus(qx.to_kraus(op), atol=kraus_truncation_threshold) @@ -505,9 +500,9 @@ def adapt_for_trajectory( def _merge_ops( - ops_with_subsystems: List[ResolvedOp], - merged_subsystem: Tuple[int, ...], - dims: Tuple[int, ...], + ops_with_subsystems: list[ResolvedOp], + merged_subsystem: tuple[int, ...], + dims: tuple[int, ...], ) -> ResolvedOp: """Merge a sequence of operators into a single operator on the union subsystem. @@ -542,7 +537,7 @@ def _merge_ops( accumulated = embedded if accumulated is None else embedded @ accumulated - assert accumulated is not None + assert accumulated is not None # noqa: S101 return accumulated, merged_subsystem @@ -550,8 +545,8 @@ class _UnionFind: """Simple union-find (disjoint set) data structure for node grouping.""" def __init__(self) -> None: - self._parent: Dict[int, int] = {} - self._rank: Dict[int, int] = {} + self._parent: dict[int, int] = {} + self._rank: dict[int, int] = {} def make_set(self, x: int) -> None: self._parent[x] = x @@ -577,10 +572,10 @@ def union(self, x: int, y: int) -> int: def compressor_from_dag( dag: nx.DiGraph, - node_order: List[int], + node_order: list[int], max_subsystem_size: int, - dims: Tuple[int, ...] = (), -) -> Callable[[List[ResolvedOp]], List[ResolvedOp]]: + dims: tuple[int, ...] = (), +) -> Callable[[list[ResolvedOp]], list[ResolvedOp]]: """Build a compressor that merges operators via greedy edge contraction. The algorithm: @@ -603,7 +598,7 @@ def compressor_from_dag( if max_subsystem_size == 0 or n_original == 0: # No merging — pass through - def compress_passthrough(ops: List[ResolvedOp]) -> List[ResolvedOp]: + def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: return ops logger.info( @@ -618,7 +613,7 @@ def _is_mergeable(node_key: int) -> bool: # --- Greedy edge contraction --- uf = _UnionFind() - group_qubits: Dict[int, Set[int]] = {} # root → set of qubit indices + group_qubits: dict[int, set[int]] = {} # root → set of qubit indices for nk in node_order: uf.make_set(nk) @@ -645,19 +640,19 @@ def _is_mergeable(node_key: int) -> bool: del group_qubits[old_root] # --- Build merge plan --- - root_to_nodes: Dict[int, List[int]] = {} + root_to_nodes: dict[int, list[int]] = {} for nk in topo_order: root = uf.find(nk) root_to_nodes.setdefault(root, []).append(nk) - root_to_subsystem: Dict[int, Tuple[int, ...]] = {} + root_to_subsystem: dict[int, tuple[int, ...]] = {} for root, qubits in group_qubits.items(): root_to_subsystem[root] = tuple(sorted(qubits)) - node_key_to_idx: Dict[int, int] = {nk: i for i, nk in enumerate(node_order)} + node_key_to_idx: dict[int, int] = {nk: i for i, nk in enumerate(node_order)} - emit_order: List[Tuple[int, List[int], Tuple[int, ...]]] = [] - emitted_roots: Set[int] = set() + emit_order: list[tuple[int, list[int], tuple[int, ...]]] = [] + emitted_roots: set[int] = set() for nk in topo_order: root = uf.find(nk) if root not in emitted_roots: @@ -686,8 +681,8 @@ def _is_mergeable(node_key: int) -> bool: ) # --- Build compress closure --- - def compress(ops: List[ResolvedOp]) -> List[ResolvedOp]: - result: List[ResolvedOp] = [] + def compress(ops: list[ResolvedOp]) -> list[ResolvedOp]: + result: list[ResolvedOp] = [] for _, nodes, subsystem in emit_order: if len(nodes) == 1: idx = node_key_to_idx[nodes[0]] @@ -707,9 +702,9 @@ def compress(ops: List[ResolvedOp]) -> List[ResolvedOp]: def infer_qudit_dims( - operations: List[ResolvedOp] | List[TrajectoryOp] | List[DensityMatrixOp], + operations: list[ResolvedOp] | list[TrajectoryOp] | list[DensityMatrixOp], n_qudits: int, -) -> Tuple[int, ...]: +) -> tuple[int, ...]: """Infer per-qudit dimensions from resolved operations. Starts with all registers at dimension 2 (qubit). For each operation, @@ -720,7 +715,7 @@ def infer_qudit_dims( :param n_qudits: Number of qudit slots. :return: Tuple of per-qudit dimensions, e.g. ``(2, 3, 2)``. """ - qudit_dims: List[int] = [2] * n_qudits + qudit_dims: list[int] = [2] * n_qudits for op, subsystem in operations: # All quax operators expose dims as ((out_dims), (in_dims)) op_dims = op.dims[0] if hasattr(op, "dims") else None diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 6b6b81d1d..4cb170d38 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -34,7 +34,7 @@ import logging import time -from typing import Any, List, Tuple +from typing import Any import jax import jax.numpy as jnp @@ -82,7 +82,7 @@ class ProgramSimulator: def __init__( self, program: Program, - qubits: List[int] | None = None, + qubits: list[int] | None = None, *, noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 0, @@ -123,11 +123,11 @@ def linearize(self, memory_map: MemoryMap) -> Array: """Convert a memory map to a flat JAX parameter vector.""" return self._linearize_fn(memory_map) - def resolve(self, params: Array) -> List[ResolvedOp]: + def resolve(self, params: Array) -> list[ResolvedOp]: """Resolve parameters into one operator per DAG node.""" return self._resolve_fn(params) - def compress(self, resolved: List[ResolvedOp]) -> List[ResolvedOp]: + def compress(self, resolved: list[ResolvedOp]) -> list[ResolvedOp]: """Merge operators via greedy edge contraction.""" return self._compress_fn(resolved) @@ -157,7 +157,7 @@ class PureStateVectorSimulator(ProgramSimulator): def __init__( self, program: Program, - qubits: List[int] | None = None, + qubits: list[int] | None = None, *, max_subsystem_size: int = 0, ) -> None: @@ -233,7 +233,7 @@ class DensityMatrixSimulator(ProgramSimulator): def __init__( self, program: Program, - qubits: List[int] | None = None, + qubits: list[int] | None = None, *, noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 0, @@ -293,7 +293,7 @@ class TrajectorySimulator(ProgramSimulator): def __init__( self, program: Program, - qubits: List[int] | None = None, + qubits: list[int] | None = None, *, noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 0, @@ -302,7 +302,7 @@ def __init__( super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) self._kraus_truncation_threshold = kraus_truncation_threshold - def adapt(self, compressed: List[ResolvedOp]) -> List[TrajectoryOp]: + def adapt(self, compressed: list[ResolvedOp]) -> list[TrajectoryOp]: """Convert compressed ops to trajectory-compatible types.""" return adapt_for_trajectory(compressed, self._kraus_truncation_threshold) @@ -310,7 +310,7 @@ def compute( # type: ignore[override] self, params: Array, key: Array, - ) -> Tuple[qx.StateVector, Array]: + ) -> tuple[qx.StateVector, Array]: """Run trajectory simulation. :param params: Flat parameter vector from :meth:`linearize`. @@ -330,7 +330,7 @@ def compute( # type: ignore[override] return _apply_trajectory_operations(operations, psi, key) - def __call__(self, params: Array, key: Array) -> Tuple[qx.StateVector, Array]: + def __call__(self, params: Array, key: Array) -> tuple[qx.StateVector, Array]: return self.compute(params, key) def sample( @@ -376,10 +376,10 @@ def sample( def _apply_trajectory_operations( - operations: List[TrajectoryOp], + operations: list[TrajectoryOp], psi: qx.StateVector, key: Array, -) -> Tuple[qx.StateVector, Array]: +) -> tuple[qx.StateVector, Array]: """Apply trajectory operations to a (batched) state vector. Dispatches each operation by type: @@ -396,7 +396,7 @@ def _apply_trajectory_operations( measurement_outcomes has shape ``(*ensemble, n_measurements)`` with dtype int32. """ - measurement_outcomes: List[Array] = [] + measurement_outcomes: list[Array] = [] n_stochastic = sum(1 for op, _ in operations if isinstance(op, (qx.KrausMap, qx.QuantumInstrument))) @@ -437,21 +437,21 @@ def _apply_trajectory_operations( def _run_batched_trajectories( - operations: List[TrajectoryOp], + operations: list[TrajectoryOp], n_qubits: int, num_trajectories: int, batch_size: int, random_seed: int, keep_states: bool = True, - dims: Tuple[int, ...] | None = None, -) -> Tuple[List[qx.StateVector] | None, List[Array]]: + dims: tuple[int, ...] | None = None, +) -> tuple[list[qx.StateVector] | None, list[Array]]: """Run trajectory simulation in batches.""" if dims is None: dims = (2,) * n_qubits key = jax.random.key(random_seed) - all_psis: List[qx.StateVector] = [] if keep_states else [] - all_outcomes: List[Array] = [] + all_psis: list[qx.StateVector] = [] if keep_states else [] + all_outcomes: list[Array] = [] remaining = num_trajectories batch_idx = 0 diff --git a/pyquil/transform.py b/pyquil/transform.py index 6b8689bbb..8965a6a9f 100644 --- a/pyquil/transform.py +++ b/pyquil/transform.py @@ -1,22 +1,18 @@ -""" -transform module ----------------- - -Utility functions for Quil program manipulation. -""" +"""Utility functions for Quil program manipulation.""" from __future__ import annotations +from collections.abc import Iterator from copy import deepcopy -from typing import Dict, Iterator, List, Optional, Union + +from quil.instructions import CircuitDefinition +from quil.instructions import Instruction as QuilInstruction +from quil.program import Program as QuilProgram from pyquil.api import MemoryMap from pyquil.quil import Program from pyquil.quilatom import MemoryReference, substitute from pyquil.quilbase import Declare, DefCircuit, Gate, Measurement, Reset, ResetQubit -from quil.instructions import CircuitDefinition -from quil.instructions import Instruction as QuilInstruction -from quil.program import Program as QuilProgram def copy_everything_except_instructions( @@ -57,7 +53,7 @@ def copy_everything_except_instructions( try: if kraus_inst.command == "ADD-KRAUS": program_definitions._program.add_instruction(kraus_inst) # type: ignore[arg-type] - except Exception: + except Exception: # noqa: S110 pass return program_definitions @@ -85,7 +81,7 @@ def unparameterize(program: Program, memory_map: MemoryMap) -> Program: for offset in range(len(value)) } - for idx, inst in enumerate(instructions): + for _idx, inst in enumerate(instructions): if isinstance(inst, Declare): if inst.name == "ro": unparameterized_program += deepcopy(inst) @@ -110,8 +106,8 @@ def unparameterize(program: Program, memory_map: MemoryMap) -> Program: def expand_defcircuit_body( inst: Gate, defcircuit: DefCircuit, - circuit_definitions: Dict[str, DefCircuit], -) -> Iterator[Union[Gate, Measurement, ResetQubit, Reset]]: + circuit_definitions: dict[str, DefCircuit], +) -> Iterator[Gate | Measurement | ResetQubit | Reset]: """Yield concrete instructions from a DEFCIRCUIT invocation. Substitutes formal qubit/parameter arguments with the concrete values @@ -152,7 +148,7 @@ def expand_defcircuit_body( def expand_defcircuits( program: Program, expand_if_defcal: bool = True, - calibration_program: Optional[Program] = None, + calibration_program: Program | None = None, keep_defcircuits: bool = False, ) -> Program: """Expand DEFCIRCUITS into individual instructions. @@ -164,7 +160,7 @@ def expand_defcircuits( :param keep_defcircuits: If True, keep the DEFCIRCUIT definitions in the returned program. :return: A Quil program, with any Circuit instructions expanded to individual instructions. """ - instructions: List = [] + instructions: list = [] circuit_definitions: dict = {} for inst in program.instructions: if isinstance(inst, DefCircuit): @@ -200,7 +196,7 @@ def _should_expand(inst: Gate) -> bool: return False return True - expanded_instructions: List = [] + expanded_instructions: list = [] for inst in instructions: if isinstance(inst, Gate) and _should_expand(inst): expanded_instructions.extend( From 3d5206fe5ca0c8291c5b6ce4928ebd61a5b98417 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 18:05:48 +0000 Subject: [PATCH 08/37] Formatting and style --- poetry.lock | 18 +- pyproject.toml | 6 +- pyquil/api/_abstract_compiler.py | 14 +- pyquil/api/_benchmark.py | 12 +- pyquil/api/_compiler.py | 21 +- pyquil/api/_compiler_client.py | 21 +- pyquil/api/_qam.py | 16 +- pyquil/api/_qpu.py | 22 +- pyquil/api/_quantum_computer.py | 76 ++-- pyquil/api/_qvm.py | 16 +- pyquil/api/_wavefunction_simulator.py | 22 +- pyquil/control_flow_graph.py | 6 +- pyquil/experiment/_group.py | 6 +- pyquil/experiment/_main.py | 17 +- pyquil/experiment/_result.py | 51 ++- pyquil/experiment/_setting.py | 8 +- pyquil/external/rpcq.py | 28 +- pyquil/gates.py | 58 ++- pyquil/latex/_diagram.py | 16 +- pyquil/latex/_ipython.py | 4 +- pyquil/latex/_main.py | 4 +- pyquil/latex/latex_generation.py | 4 +- pyquil/noise/_channels.py | 179 ++++++--- pyquil/noise/_legacy_noise.py | 22 +- pyquil/noise/_noise_model.py | 2 +- pyquil/noise_gates.py | 3 +- pyquil/operator_estimation.py | 10 +- pyquil/paulis.py | 34 +- pyquil/pyqvm.py | 30 +- pyquil/quantum_processor/graph.py | 6 +- pyquil/quantum_processor/qcs.py | 8 +- .../transformers/graph_to_compiler_isa.py | 16 +- .../transformers/qcs_isa_to_compiler_isa.py | 14 +- pyquil/quil.py | 53 ++- pyquil/quilatom.py | 62 ++- pyquil/quilbase.py | 232 ++++++----- pyquil/quiltcalibrations.py | 30 +- pyquil/quiltwaveforms.py | 39 +- pyquil/simulation/_numpy.py | 10 +- pyquil/simulation/_reference.py | 10 +- pyquil/simulation/_resolver.py | 287 ++++++++++---- pyquil/simulation/tools.py | 6 +- pyquil/transform.py | 4 +- pyquil/wavefunction.py | 4 +- test/unit/test_legacy_noise.py | 371 ------------------ test/unit/test_noise.py | 4 + test/unit/test_noise_model.py | 63 ++- test/unit/test_reference_density.py | 4 + test/unit/test_reference_wavefunction.py | 4 + 49 files changed, 900 insertions(+), 1053 deletions(-) delete mode 100644 test/unit/test_legacy_noise.py diff --git a/poetry.lock b/poetry.lock index 5b459f407..ac635f2f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand. [[package]] name = "alabaster" @@ -383,7 +383,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "sys_platform == \"win32\" and (extra == \"latex\" or extra == \"docs\")", dev = "sys_platform == \"win32\""} +markers = {main = "(extra == \"latex\" or extra == \"docs\") and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} [[package]] name = "contourpy" @@ -1780,8 +1780,8 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.3", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [package.extras] @@ -2363,7 +2363,7 @@ description = "Pexpect allows easy control of interactive console applications." optional = true python-versions = "*" groups = ["main"] -markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" and extra == \"latex\"" +markers = "extra == \"latex\" and sys_platform != \"win32\" and sys_platform != \"emscripten\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -2566,7 +2566,7 @@ description = "Run a subprocess in a pseudo terminal" optional = true python-versions = "*" groups = ["main"] -markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" and extra == \"latex\"" +markers = "extra == \"latex\" and sys_platform != \"win32\" and sys_platform != \"emscripten\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -3270,14 +3270,14 @@ httpx = ">=0.21.0" [[package]] name = "rigetti-quax" -version = "0.5.3" +version = "0.6.3" description = "A high-performance library for quantum information science built on top of JAX" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ - {file = "rigetti_quax-0.5.3-py3-none-any.whl", hash = "sha256:f53ae6f5d3182deb94f7eeefc93c19597d1954b8befb7731b4ee90ef9a048e8f"}, - {file = "rigetti_quax-0.5.3.tar.gz", hash = "sha256:df758507afd5e72fecea2209f1846bbbd4b89eec066d27084a667ecc16cc7cec"}, + {file = "rigetti_quax-0.6.3-py3-none-any.whl", hash = "sha256:caa46c36c805a45ee4572a3d6fd167a2d2353ad1ec95d13931ab941c1e249fa5"}, + {file = "rigetti_quax-0.6.3.tar.gz", hash = "sha256:d1cbb64f9095a78e5f596c48c7b1a2ca38c841cfd81ccc960c369a85391a82d0"}, ] [package.dependencies] @@ -4113,4 +4113,4 @@ latex = ["ipython"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.13" -content-hash = "f3138f5e5768209e4153dbf73369f8931dd288eb814d65d16125b33f0efcc3bc" +content-hash = "7e3588a643858e0d8627af671c92bddb651ce31f8f29f97ffc4e2a4fbc37dada" diff --git a/pyproject.toml b/pyproject.toml index 984b61c54..7e1ed4c24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ pandoc = {version = "2.4b0", optional = true} matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} -rigetti-quax = ">=0.5.3" +rigetti-quax = ">=0.6.3" [tool.poetry.extras] latex = ["ipython"] @@ -99,7 +99,7 @@ exclude = [ ] line-length = 120 indent-width = 4 -target-version = "py39" +target-version = "py311" [tool.ruff.lint] select = ["D", "E4", "E7", "E9", "F", "I", "B", "S", "UP", "W", "NPY201"] @@ -107,6 +107,8 @@ ignore = [ "D105", # Allow missing documentation in dunder method. "D203", # This conflicts with D211. "D213", # This conflicts with D212. + "UP007", # Optional[X] → X | None is not applied in files with from __future__ import annotations. + "UP038", # Deprecated: isinstance(x, (A, B)) → isinstance(x, A | B) has a runtime cost. ] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 461eab8c5..83d1e2692 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -19,7 +19,7 @@ from abc import ABC, abstractmethod from collections.abc import Sequence from dataclasses import dataclass, field, fields -from typing import Any, Optional, Union +from typing import Any, Union from deprecated.sphinx import deprecated from qcs_sdk import QCSClient @@ -100,8 +100,8 @@ def __init__( *, quantum_processor: AbstractQuantumProcessor, timeout: float, - client_configuration: Optional[QCSClient] = None, - quilc_client: Optional[QuilcClient] = None, + client_configuration: QCSClient | None = None, + quilc_client: QuilcClient | None = None, ) -> None: self.quantum_processor = quantum_processor self._timeout = timeout @@ -121,7 +121,7 @@ def get_version_info(self) -> dict[str, Any]: """ return {"quilc": self._compiler_client.get_version()} - def quil_to_native_quil(self, program: Program, *, protoquil: Optional[bool] = None) -> Program: + def quil_to_native_quil(self, program: Program, *, protoquil: bool | None = None) -> Program: """Convert a Quil program into native Quil, which is supported for execution on a QPU.""" result = self._compile_with_quilc( program.out(calibrations=False), @@ -134,7 +134,7 @@ def quil_to_native_quil(self, program: Program, *, protoquil: Optional[bool] = N return native_program - def _compile_with_quilc(self, input: str, options: Optional[CompilerOpts] = None) -> CompilationResult: + def _compile_with_quilc(self, input: str, options: CompilerOpts | None = None) -> CompilationResult: self._connect() # convert the pyquil ``TargetDevice`` to the qcs_sdk ``TargetDevice`` @@ -208,8 +208,8 @@ def generate_rb_sequence( self, depth: int, gateset: Sequence[Gate], - seed: Optional[int] = None, - interleaver: Optional[Program] = None, + seed: int | None = None, + interleaver: Program | None = None, ) -> list[Program]: r"""Construct a randomized benchmarking experiment on the given qubits, decomposing into gateset. diff --git a/pyquil/api/_benchmark.py b/pyquil/api/_benchmark.py index 253bf7a51..6e1eaf512 100644 --- a/pyquil/api/_benchmark.py +++ b/pyquil/api/_benchmark.py @@ -14,7 +14,7 @@ # limitations under the License. ############################################################################## from collections.abc import Sequence -from typing import Optional, cast +from typing import cast from qcs_sdk import QCSClient from qcs_sdk.compiler.quilc import ( @@ -35,7 +35,7 @@ class BenchmarkConnection(AbstractBenchmarker): """Represents a connection to a server that generates benchmarking data.""" - def __init__(self, *, timeout: float = 10.0, client_configuration: Optional[QCSClient] = None): + def __init__(self, *, timeout: float = 10.0, client_configuration: QCSClient | None = None): """Client to communicate with the benchmarking data endpoint. :param timeout: Time limit for requests, in seconds. @@ -60,7 +60,7 @@ def apply_clifford_to_pauli(self, clifford: Program, pauli_in: PauliTerm) -> Pau if is_identity(pauli_in): return pauli_in - indices_and_terms = list(zip(*list(pauli_in.operations_as_set()))) + indices_and_terms = list(zip(*list(pauli_in.operations_as_set()), strict=False)) request = ConjugateByCliffordRequest( pauli=QuilcPauliTerm( @@ -87,8 +87,8 @@ def generate_rb_sequence( self, depth: int, gateset: Sequence[Gate], - seed: Optional[int] = None, - interleaver: Optional[Program] = None, + seed: int | None = None, + interleaver: Program | None = None, ) -> list[Program]: """Construct a randomized benchmarking experiment on the given qubits, decomposing into gateset. @@ -124,7 +124,7 @@ def generate_rb_sequence( gateset_as_program = address_qubits(sum(gateset, Program())) # type: ignore qubits = len(gateset_as_program.get_qubits()) gateset_for_api = gateset_as_program.out().splitlines() - interleaver_out: Optional[str] = None + interleaver_out: str | None = None if interleaver: if not isinstance(interleaver, Program): raise ValueError("interleaver must be a Program") diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py index 0ce1e9228..41465aaef 100644 --- a/pyquil/api/_compiler.py +++ b/pyquil/api/_compiler.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -from typing import Any, Optional +from typing import Any, TypeAlias from warnings import warn from qcs_sdk import QCSClient @@ -27,7 +27,6 @@ TranslationOptions as _TranslationOptions, ) from rpcq.messages import ParameterSpec -from typing_extensions import TypeAlias from pyquil.api._abstract_compiler import AbstractCompiler, EncryptedProgram, QuantumExecutable from pyquil.quantum_processor import AbstractQuantumProcessor @@ -75,7 +74,7 @@ def _collect_memory_descriptors(program: Program) -> dict[str, ParameterSpec]: class QPUCompiler(AbstractCompiler): """Client to communicate with the compiler and translation service.""" - api_options: Optional[QPUCompilerAPIOptions] + api_options: QPUCompilerAPIOptions | None def __init__( self, @@ -83,9 +82,9 @@ def __init__( quantum_processor_id: str, quantum_processor: AbstractQuantumProcessor, timeout: float = 10.0, - client_configuration: Optional[QCSClient] = None, - api_options: Optional[QPUCompilerAPIOptions] = None, - quilc_client: Optional[QuilcClient] = None, + client_configuration: QCSClient | None = None, + api_options: QPUCompilerAPIOptions | None = None, + quilc_client: QuilcClient | None = None, ) -> None: """Instantiate a new QPU compiler client. @@ -104,10 +103,10 @@ def __init__( self.api_options = api_options self.quantum_processor_id = quantum_processor_id - self._calibration_program: Optional[Program] = None + self._calibration_program: Program | None = None def native_quil_to_executable( - self, nq_program: Program, *, api_options: Optional[QPUCompilerAPIOptions] = None, **kwargs: Any + self, nq_program: Program, *, api_options: QPUCompilerAPIOptions | None = None, **kwargs: Any ) -> QuantumExecutable: """Convert a native Quil program into an executable binary which can be executed by a QPU. @@ -182,8 +181,8 @@ def __init__( *, quantum_processor: AbstractQuantumProcessor, timeout: float = 10.0, - client_configuration: Optional[QCSClient] = None, - quilc_client: Optional[QuilcClient] = None, + client_configuration: QCSClient | None = None, + quilc_client: QuilcClient | None = None, ) -> None: """Client to communicate with compiler. @@ -210,7 +209,7 @@ class IncompatibleBackendForQuantumProcessorIDWarning(Warning): def select_backend_for_quantum_processor_id( - quantum_processor_id: str, backend: Optional[TranslationBackend] + quantum_processor_id: str, backend: TranslationBackend | None ) -> TranslationBackend: """Check that the translation backend is supported for the quantum processor. diff --git a/pyquil/api/_compiler_client.py b/pyquil/api/_compiler_client.py index b8fa87644..d12b29048 100644 --- a/pyquil/api/_compiler_client.py +++ b/pyquil/api/_compiler_client.py @@ -15,7 +15,6 @@ ############################################################################## import json from dataclasses import dataclass -from typing import Optional from qcs_sdk import QCSClient from qcs_sdk.compiler.quilc import ( @@ -45,7 +44,7 @@ class CompileToNativeQuilRequest: target_quantum_processor: TargetQuantumProcessor """Quantum processor to target.""" - protoquil: Optional[bool] + protoquil: bool | None """Whether or not to restrict to protoquil. Overrides server default when provided.""" @@ -56,25 +55,25 @@ class NativeQuilMetadataResponse: final_rewiring: list[int] """Output qubit index relabeling due to SWAP insertion.""" - gate_depth: Optional[int] + gate_depth: int | None """Maximum number of successive gates in the native Quil program.""" - gate_volume: Optional[int] + gate_volume: int | None """Total number of gates in the native Quil program.""" - multiqubit_gate_depth: Optional[int] + multiqubit_gate_depth: int | None """Maximum number of successive two-qubit gates in the native Quil program.""" - program_duration: Optional[float] + program_duration: float | None """Rough estimate of native Quil program length in nanoseconds.""" - program_fidelity: Optional[float] + program_fidelity: float | None """Rough estimate of the fidelity of the full native Quil program.""" - topological_swaps: Optional[int] + topological_swaps: int | None """Total number of SWAPs in the native Quil program.""" - qpu_runtime_estimation: Optional[float] + qpu_runtime_estimation: float | None """ The estimated runtime (milliseconds) on a Rigetti QPU (protoquil program). Available only for protoquil-compliant programs. @@ -88,7 +87,7 @@ class CompileToNativeQuilResponse: native_program: str """Native Quil program.""" - metadata: Optional[NativeQuilMetadata] + metadata: NativeQuilMetadata | None """Metadata for the returned Native Quil.""" @@ -100,7 +99,7 @@ def __init__( *, client_configuration: QCSClient, request_timeout: float = 10.0, - quilc_client: Optional[QuilcClient] = None, + quilc_client: QuilcClient | None = None, ) -> None: """Instantiate a new compiler client. diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index e49c83589..95d0808ad 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -17,7 +17,7 @@ from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Generic, Optional, TypeVar, Union +from typing import Any, Generic, TypeVar import numpy as np from deprecated import deprecated @@ -35,7 +35,7 @@ class QAMError(RuntimeError): T = TypeVar("T") """A generic parameter describing the opaque job handle returned from QAM#execute and subclasses.""" -MemoryMap = Mapping[str, Union[Sequence[int], Sequence[float]]] +MemoryMap = Mapping[str, Sequence[int] | Sequence[float]] """A mapping of memory regions to a list containing the values to be written into that memory region.""" @@ -51,7 +51,7 @@ class QAMExecutionResult: to get at the data in a more convenient format. """ - def get_register_map(self) -> dict[str, Optional[np.ndarray]]: + def get_register_map(self) -> dict[str, np.ndarray | None]: """Map a register name (ie. "ro") to a ``np.ndarray`` containing the values for the register. Raises a ``RegisterMatrixConversionError`` if the inner execution data for any of the @@ -74,7 +74,7 @@ def get_register_map(self) -> dict[str, Optional[np.ndarray]]: register_map = self.data.result_data.to_register_map() return {key: matrix.to_ndarray() for key, matrix in register_map.items()} - def get_raw_readout_data(self) -> Union[RawQVMReadoutData, RawQPUReadoutData]: + def get_raw_readout_data(self) -> RawQVMReadoutData | RawQPUReadoutData: """Get the raw result data. This will be a flattened structure derived @@ -108,12 +108,12 @@ def get_memory_values(self) -> Mapping[str, MemoryValues]: "and will be removed in future versions. Use the `get_register_map()` method instead." ), ) - def readout_data(self) -> Mapping[str, Optional[np.ndarray]]: + def readout_data(self) -> Mapping[str, np.ndarray | None]: """Readout data returned from the QAM, keyed on the name of the readout register or post-processing node.""" return self.get_register_map() @property - def execution_duration_microseconds(self) -> Optional[float]: + def execution_duration_microseconds(self) -> float | None: """Duration job held exclusive hardware access. Defaults to ``None`` when information is not available.""" if isinstance(self.data.duration, timedelta): return self.data.duration.total_seconds() * 1e6 @@ -127,7 +127,7 @@ class QAM(ABC, Generic[T]): def execute( self, executable: QuantumExecutable, - memory_map: Optional[MemoryMap] = None, + memory_map: MemoryMap | None = None, **kwargs: Any, ) -> T: """Run an executable on a QAM, returning a handle to be used to retrieve results. @@ -160,7 +160,7 @@ def get_result(self, execute_response: T) -> QAMExecutionResult: """ def run( - self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **kwargs: Any + self, executable: QuantumExecutable, memory_map: MemoryMap | None = None, **kwargs: Any ) -> QAMExecutionResult: """Run an executable to completion on the QAM.""" return self.get_result(self.execute(executable, memory_map, **kwargs)) diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 6e97111fd..1537d5e9e 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -17,7 +17,7 @@ from collections.abc import Iterable from dataclasses import dataclass from datetime import timedelta -from typing import Any, Optional, Union +from typing import Any import numpy as np from numpy.typing import NDArray @@ -41,7 +41,7 @@ ) -def decode_buffer(buffer: ExecutionResult) -> Union[NDArray[np.complex64], NDArray[np.int32]]: +def decode_buffer(buffer: ExecutionResult) -> NDArray[np.complex64] | NDArray[np.int32]: """Translate a DataBuffer into a numpy array. :param buffer: Dictionary with 'data' byte array, 'dtype', and 'shape' fields @@ -109,7 +109,7 @@ def alloc(spec: ParameterSpec) -> np.ndarray: class QPUExecuteResponse: job_id: str _executable: EncryptedProgram - execution_options: Optional[ExecutionOptions] + execution_options: ExecutionOptions | None class QPU(QAM[QPUExecuteResponse]): @@ -118,10 +118,10 @@ def __init__( *, quantum_processor_id: str, priority: int = 1, - timeout: Optional[float] = 30.0, - client_configuration: Optional[QCSClient] = None, - endpoint_id: Optional[str] = None, - execution_options: Optional[ExecutionOptions] = None, + timeout: float | None = 30.0, + client_configuration: QCSClient | None = None, + endpoint_id: str | None = None, + execution_options: ExecutionOptions | None = None, ) -> None: """Connect to the QPU. @@ -140,7 +140,7 @@ def __init__( self._client_configuration = client_configuration or QCSClient.load() self._last_results: dict[str, np.ndarray] = {} - self._memory_results: dict[str, Optional[np.ndarray]] = defaultdict(lambda: None) + self._memory_results: dict[str, np.ndarray | None] = defaultdict(lambda: None) self._quantum_processor_id = quantum_processor_id if execution_options is None: execution_options_builder = ExecutionOptionsBuilder.default() @@ -159,8 +159,8 @@ def quantum_processor_id(self) -> str: def execute( self, executable: QuantumExecutable, - memory_map: Optional[MemoryMap] = None, - execution_options: Optional[ExecutionOptions] = None, + memory_map: MemoryMap | None = None, + execution_options: ExecutionOptions | None = None, **__: Any, ) -> QPUExecuteResponse: """Enqueue a job for execution on the QPU. @@ -185,7 +185,7 @@ def execute_with_memory_map_batch( self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], - execution_options: Optional[ExecutionOptions] = None, + execution_options: ExecutionOptions | None = None, **__: Any, ) -> list[QPUExecuteResponse]: """Execute a compiled program on a QPU with multiple sets of `memory_maps`. diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 9cbe6786f..83e900189 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -23,8 +23,6 @@ from math import log, pi from typing import ( Any, - Optional, - Union, cast, ) @@ -117,7 +115,7 @@ def to_compiler_isa(self) -> CompilerISA: return self.compiler.quantum_processor.to_compiler_isa() def run( - self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **kwargs: Any + self, executable: QuantumExecutable, memory_map: MemoryMap | None = None, **kwargs: Any ) -> QAMExecutionResult: """Run a quil executable. @@ -157,7 +155,7 @@ def calibrate(self, experiment: Experiment) -> list[ExperimentResult]: def run_experiment( self, experiment: Experiment, - memory_map: Optional[MemoryMap] = None, + memory_map: MemoryMap | None = None, ) -> list[ExperimentResult]: """Run an ``Experiment`` on a QVM or QPU backend. @@ -249,7 +247,7 @@ def run_experiment( std_errs = np.std(expectations, axis=0, ddof=1) / np.sqrt(len(expectations)) joint_results = [] - for qubit_subset, mean, std_err in zip(joint_expectations, means, std_errs): + for qubit_subset, mean, std_err in zip(joint_expectations, means, std_errs, strict=False): out_operator = PauliTerm.from_list([(setting.out_operator[i], i) for i in qubit_subset]) s = ExperimentSetting( in_state=setting.in_state, @@ -274,7 +272,7 @@ def run_symmetrized_readout( program: Program, trials: int, symm_type: int = 3, - meas_qubits: Optional[Sequence[QubitDesignator]] = None, + meas_qubits: Sequence[QubitDesignator] | None = None, ) -> np.ndarray: r"""Run a quil program in such a way that the readout error is made symmetric. @@ -361,7 +359,7 @@ def compile( to_native_gates: bool = True, optimize: bool = True, *, - protoquil: Optional[bool] = None, + protoquil: bool | None = None, ) -> QuantumExecutable: """Provide a high-level interface for program compilation. @@ -406,7 +404,7 @@ def list_quantum_computers( qpus: bool = True, qvms: bool = True, timeout: float = 10.0, - client_configuration: Optional[QCSClient] = None, + client_configuration: QCSClient | None = None, ) -> list[str]: """List the names of available quantum computers. @@ -426,12 +424,12 @@ def list_quantum_computers( return qc_names -def _parse_name(name: str, as_qvm: Optional[bool], noisy: Optional[bool]) -> tuple[str, Optional[str], bool]: +def _parse_name(name: str, as_qvm: bool | None, noisy: bool | None) -> tuple[str, str | None, bool]: """Try to figure out whether we're getting a (noisy) qvm, and the associated qpu name. See :py:func:`get_qc` for examples of valid names + flags. """ - qvm_type: Optional[str] + qvm_type: str | None parts = name.split("-") if len(parts) >= 2 and parts[-2] == "noisy" and parts[-1] in ["qvm", "pyqvm"]: if as_qvm is not None and (not as_qvm): @@ -471,7 +469,7 @@ def _parse_name(name: str, as_qvm: Optional[bool], noisy: Optional[bool]) -> tup return name, qvm_type, noisy -def _canonicalize_name(prefix: str, qvm_type: Optional[str], noisy: bool) -> str: +def _canonicalize_name(prefix: str, qvm_type: str | None, noisy: bool) -> str: """Take the output of _parse_name to create a canonical name.""" if noisy: noise_suffix = "-noisy" @@ -494,11 +492,11 @@ def _canonicalize_name(prefix: str, qvm_type: Optional[str], noisy: bool) -> str def _get_qvm_or_pyqvm( *, qvm_type: str, - qvm_client: Optional[QVMClient], - noise_model: Optional[NoiseModel], - quantum_processor: Optional[AbstractQuantumProcessor], + qvm_client: QVMClient | None, + noise_model: NoiseModel | None, + quantum_processor: AbstractQuantumProcessor | None, execution_timeout: float, -) -> Union[QVM, PyQVM]: +) -> QVM | PyQVM: if qvm_type == "qvm": return QVM(noise_model=noise_model, timeout=execution_timeout, client=qvm_client) elif qvm_type == "pyqvm": @@ -517,9 +515,9 @@ def _get_qvm_qc( quantum_processor: AbstractQuantumProcessor, compiler_timeout: float, execution_timeout: float, - noise_model: Optional[NoiseModel], - quilc_client: Optional[QuilcClient] = None, - qvm_client: Optional[QVMClient] = None, + noise_model: NoiseModel | None, + quilc_client: QuilcClient | None = None, + qvm_client: QVMClient | None = None, ) -> QuantumComputer: """Construct a QuantumComputer backed by a QVM. @@ -561,8 +559,8 @@ def _get_qvm_with_topology( qvm_type: str, compiler_timeout: float, execution_timeout: float, - quilc_client: Optional[QuilcClient] = None, - qvm_client: Optional[QVMClient] = None, + quilc_client: QuilcClient | None = None, + qvm_client: QVMClient | None = None, ) -> QuantumComputer: """Construct a QVM with the provided topology. @@ -581,9 +579,7 @@ def _get_qvm_with_topology( # Note to developers: consider making this function public and advertising it. quantum_processor = NxQuantumProcessor(topology=topology) if noisy: - noise_model: Optional[NoiseModel] = decoherence_noise_with_asymmetric_ro( - isa=quantum_processor.to_compiler_isa() - ) + noise_model: NoiseModel | None = decoherence_noise_with_asymmetric_ro(isa=quantum_processor.to_compiler_isa()) else: noise_model = None return _get_qvm_qc( @@ -607,8 +603,8 @@ def _get_9q_square_qvm( qvm_type: str, compiler_timeout: float, execution_timeout: float, - quilc_client: Optional[QuilcClient] = None, - qvm_client: Optional[QVMClient] = None, + quilc_client: QuilcClient | None = None, + qvm_client: QVMClient | None = None, ) -> QuantumComputer: """Nine-qubit 3x3 square lattice. @@ -646,8 +642,8 @@ def _get_unrestricted_qvm( qvm_type: str, compiler_timeout: float, execution_timeout: float, - quilc_client: Optional[QuilcClient] = None, - qvm_client: Optional[QVMClient] = None, + quilc_client: QuilcClient | None = None, + qvm_client: QVMClient | None = None, ) -> QuantumComputer: """QVM with a fully-connected topology. @@ -685,8 +681,8 @@ def _get_qvm_based_on_real_quantum_processor( qvm_type: str, compiler_timeout: float, execution_timeout: float, - quilc_client: Optional[QuilcClient] = None, - qvm_client: Optional[QVMClient] = None, + quilc_client: QuilcClient | None = None, + qvm_client: QVMClient | None = None, ) -> QuantumComputer: """QVM based on a real quantum_processor. @@ -722,14 +718,14 @@ def _get_qvm_based_on_real_quantum_processor( def get_qc( name: str, *, - as_qvm: Optional[bool] = None, - noisy: Optional[bool] = None, + as_qvm: bool | None = None, + noisy: bool | None = None, compiler_timeout: float = 30.0, execution_timeout: float = 30.0, - client_configuration: Optional[QCSClient] = None, - endpoint_id: Optional[str] = None, - quilc_client: Optional[QuilcClient] = None, - qvm_client: Optional[QVMClient] = None, + client_configuration: QCSClient | None = None, + endpoint_id: str | None = None, + quilc_client: QuilcClient | None = None, + qvm_client: QVMClient | None = None, ) -> QuantumComputer: """Get a quantum computer. @@ -912,7 +908,7 @@ def local_forest_runtime( qvm_port: int = 5000, quilc_port: int = 5555, use_protoquil: bool = False, -) -> Iterator[tuple[Optional[subprocess.Popen], Optional[subprocess.Popen]]]: +) -> Iterator[tuple[subprocess.Popen | None, subprocess.Popen | None]]: """Context manager for local QVM and QUIL compiler. You must first have installed the `qvm` and `quilc` executables from @@ -954,8 +950,8 @@ def local_forest_runtime( ports is in use, the process won't be started and the respective value in the tuple will be ``None``. """ - qvm: Optional[subprocess.Popen] = None - quilc: Optional[subprocess.Popen] = None + qvm: subprocess.Popen | None = None + quilc: subprocess.Popen | None = None # If the host we should listen to is 0.0.0.0, we replace it # with 127.0.0.1 to use a valid IP when checking if the port is in use. @@ -1004,7 +1000,7 @@ def _flip_array_to_prog(flip_array: tuple[bool], qubits: Sequence[QubitDesignato raise ValueError("Mismatch of qubits and operations") prog = Program() - for qubit, flip_output in zip(qubits, flip_array): + for qubit, flip_output in zip(qubits, flip_array, strict=False): if flip_output == 0: continue elif flip_output == 1: @@ -1086,7 +1082,7 @@ def _consolidate_symmetrization_outputs(outputs: list[np.ndarray], flip_arrays: raise ValueError("The length of outputs must equal the length of flip_arrays") output = [] - for bitarray, flip_array in zip(outputs, flip_arrays): + for bitarray, flip_array in zip(outputs, flip_arrays, strict=False): if len(flip_array) == 0: output.append(bitarray) else: diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 348b0057b..89c289b1e 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -15,7 +15,7 @@ ############################################################################## from collections.abc import Iterable, Sequence from dataclasses import dataclass -from typing import Any, Optional +from typing import Any import numpy as np from qcs_sdk import ExecutionData, QCSClient, ResultData, qvm @@ -60,12 +60,12 @@ def memory(self) -> dict[str, np.ndarray]: class QVM(QAM[QVMExecuteResponse]): def __init__( self, - noise_model: Optional[NoiseModel] = None, - gate_noise: Optional[tuple[float, float, float]] = None, - measurement_noise: Optional[tuple[float, float, float]] = None, - random_seed: Optional[int] = None, + noise_model: NoiseModel | None = None, + gate_noise: tuple[float, float, float] | None = None, + measurement_noise: tuple[float, float, float] | None = None, + random_seed: int | None = None, timeout: float = 10.0, - client: Optional[QVMClient] = None, + client: QVMClient | None = None, ) -> None: """Return a virtual machine that classically emulates the execution of Quil programs. @@ -138,7 +138,7 @@ def execute_with_memory_map_batch( def execute( self, executable: QuantumExecutable, - memory_map: Optional[MemoryMap] = None, + memory_map: MemoryMap | None = None, **__: Any, ) -> QVMExecuteResponse: """Execute the input program to completion.""" @@ -180,7 +180,7 @@ def get_version_info(self) -> str: return qvm.api.get_version_info(self._client, options=QVMOptions(timeout_seconds=self.timeout)) -def validate_noise_probabilities(noise_parameter: Optional[tuple[float, float, float]]) -> None: +def validate_noise_probabilities(noise_parameter: tuple[float, float, float] | None) -> None: """Validate the noise probabilities. This function checks that the provided noise parameters are in the correct format and within the expected ranges. diff --git a/pyquil/api/_wavefunction_simulator.py b/pyquil/api/_wavefunction_simulator.py index 6b44937e6..682372185 100644 --- a/pyquil/api/_wavefunction_simulator.py +++ b/pyquil/api/_wavefunction_simulator.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -from typing import Optional, Union, cast +from typing import cast import numpy as np from qcs_sdk import QCSClient, qvm @@ -34,11 +34,11 @@ class WavefunctionSimulator: def __init__( self, *, - gate_noise: Optional[tuple[float, float, float]] = None, - measurement_noise: Optional[tuple[float, float, float]] = None, - random_seed: Optional[int] = None, + gate_noise: tuple[float, float, float] | None = None, + measurement_noise: tuple[float, float, float] | None = None, + random_seed: int | None = None, timeout: float = 10.0, - client_configuration: Optional[QCSClient] = None, + client_configuration: QCSClient | None = None, ) -> None: """Return a simulator that propagates a wavefunction representation of a quantum state. @@ -67,7 +67,7 @@ def __init__( self._client = client_configuration or QCSClient.load() self._qvm_client = qvm.QVMClient.new_http(self._client.qvm_url) - def wavefunction(self, quil_program: Program, memory_map: Optional[MemoryMap] = None) -> Wavefunction: + def wavefunction(self, quil_program: Program, memory_map: MemoryMap | None = None) -> Wavefunction: """Simulate a Quil program and return the wavefunction. .. note:: If your program contains measurements or noisy gates, this method may not do what @@ -102,9 +102,9 @@ def wavefunction(self, quil_program: Program, memory_map: Optional[MemoryMap] = def expectation( self, prep_prog: Program, - pauli_terms: Union[PauliSum, list[PauliTerm]], - memory_map: Optional[dict[str, list[Union[int, float]]]] = None, - ) -> Union[float, np.ndarray]: + pauli_terms: PauliSum | list[PauliTerm], + memory_map: dict[str, list[int | float]] | None = None, + ) -> float | np.ndarray: """Calculate the expectation value of Pauli operators given a state prepared by prep_program. If ``pauli_terms`` is a ``PauliSum`` then the returned value is a single ``float``, @@ -151,9 +151,9 @@ def expectation( def run_and_measure( self, quil_program: Program, - qubits: Optional[list[int]] = None, + qubits: list[int] | None = None, trials: int = 1, - memory_map: Optional[MemoryMap] = None, + memory_map: MemoryMap | None = None, ) -> np.ndarray: """Run a Quil program once to determine the final wavefunction, and measure multiple times. diff --git a/pyquil/control_flow_graph.py b/pyquil/control_flow_graph.py index ff100bae6..25c3ab490 100644 --- a/pyquil/control_flow_graph.py +++ b/pyquil/control_flow_graph.py @@ -1,9 +1,9 @@ """Classes that represent the control flow graph of a Quil program.""" -from typing import Optional +from typing import Self from quil import program as quil_rs -from typing_extensions import Self, override +from typing_extensions import override from pyquil.quilbase import ( AbstractInstruction, @@ -30,7 +30,7 @@ def instructions(self) -> list[AbstractInstruction]: # type: ignore[override] return _convert_to_py_instructions(super().instructions()) @override - def terminator(self) -> Optional[AbstractInstruction]: # type: ignore[override] + def terminator(self) -> AbstractInstruction | None: # type: ignore[override] inst = super().terminator() if inst is None: return None diff --git a/pyquil/experiment/_group.py b/pyquil/experiment/_group.py index 126ae0e43..b552297cb 100644 --- a/pyquil/experiment/_group.py +++ b/pyquil/experiment/_group.py @@ -17,7 +17,7 @@ import itertools from collections.abc import Iterable, Sequence from operator import mul -from typing import Union, cast +from typing import cast import networkx as nx from networkx.algorithms.approximation.clique import clique_removal @@ -167,7 +167,7 @@ def group_settings_clique_removal(experiments: Experiment) -> Experiment: ) -def _max_weight_operator(ops: Iterable[PauliTerm]) -> Union[None, PauliTerm]: +def _max_weight_operator(ops: Iterable[PauliTerm]) -> None | PauliTerm: """Construct a PauliTerm operator by taking the non-identity single-qubit operator at each qubit position. This function will return ``None`` if the input operators do not share a natural tensor product basis. @@ -189,7 +189,7 @@ def _max_weight_operator(ops: Iterable[PauliTerm]) -> Union[None, PauliTerm]: return op -def _max_weight_state(states: Iterable[TensorProductState]) -> Union[None, TensorProductState]: +def _max_weight_state(states: Iterable[TensorProductState]) -> None | TensorProductState: """Construct a TensorProductState by taking the single-qubit state at each qubit position. This function will return ``None`` if the input states are not compatible diff --git a/pyquil/experiment/_main.py b/pyquil/experiment/_main.py index 78597cf87..332933272 100644 --- a/pyquil/experiment/_main.py +++ b/pyquil/experiment/_main.py @@ -22,13 +22,10 @@ import json import logging import warnings -from collections.abc import Generator, Mapping, Sequence +from collections.abc import Callable, Generator, Mapping, Sequence from json import JSONEncoder from typing import ( Any, - Callable, - Optional, - Union, cast, ) @@ -136,7 +133,7 @@ class Experiment: def __init__( self, - settings: Union[list[ExperimentSetting], list[list[ExperimentSetting]]], + settings: list[ExperimentSetting] | list[list[ExperimentSetting]], program: Program, *, symmetrization: int = SymmetrizationLevel.EXHAUSTIVE, @@ -193,7 +190,7 @@ def __reversed__(self) -> Generator[list[ExperimentSetting], None, None]: def __contains__(self, item: list[ExperimentSetting]) -> bool: return item in self._settings - def append(self, expts: Union[ExperimentSetting, list[ExperimentSetting]]) -> None: + def append(self, expts: ExperimentSetting | list[ExperimentSetting]) -> None: if not isinstance(expts, list): expts = [expts] self._settings.append(expts) @@ -219,7 +216,7 @@ def remove(self, expt: list[ExperimentSetting]) -> None: def reverse(self) -> None: self._settings.reverse() - def sort(self, key: Optional[Callable[[list[ExperimentSetting]], Any]] = None, reverse: bool = False) -> None: + def sort(self, key: Callable[[list[ExperimentSetting]], Any] | None = None, reverse: bool = False) -> None: return self._settings.sort(key=key, reverse=reverse) def setting_strings(self) -> Generator[str, None, None]: @@ -228,7 +225,7 @@ def setting_strings(self) -> Generator[str, None, None]: for i, settings in enumerate(self._settings) ) - def settings_string(self, abbrev_after: Optional[int] = None) -> str: + def settings_string(self, abbrev_after: int | None = None) -> str: setting_strs = list(self.setting_strings()) if abbrev_after is not None and len(setting_strs) > abbrev_after: first_n = abbrev_after // 2 @@ -273,7 +270,7 @@ def get_meas_qubits(self) -> list[int]: meas_qubits.update(cast(list[int], settings[0].out_operator.get_qubits())) return sorted(meas_qubits) - def get_meas_registers(self, qubits: Optional[Sequence[int]] = None) -> list[int]: + def get_meas_registers(self, qubits: Sequence[int] | None = None) -> list[int]: """Return the sorted list of memory registers corresponding to the list of qubits provided. If no qubits are provided, just returns the list of numbers from 0 to n-1 where n is the @@ -495,7 +492,7 @@ def to_json(fn: str, obj: Any) -> str: return fn -def _operator_object_hook(obj: Mapping[str, Any]) -> Union[Mapping[str, Any], Experiment]: +def _operator_object_hook(obj: Mapping[str, Any]) -> Mapping[str, Any] | Experiment: if "type" in obj and obj["type"] in ["Experiment", "TomographyExperiment"]: # I bet this doesn't work for grouped experiment settings settings = [[ExperimentSetting.from_str(s) for s in stt] for stt in obj["settings"]] diff --git a/pyquil/experiment/_result.py b/pyquil/experiment/_result.py index 5885c99cc..950434cba 100644 --- a/pyquil/experiment/_result.py +++ b/pyquil/experiment/_result.py @@ -21,7 +21,7 @@ import logging from dataclasses import dataclass -from typing import Any, Optional, Union +from typing import Any import numpy as np @@ -40,28 +40,28 @@ class ExperimentResult: """ setting: ExperimentSetting - expectation: Union[float, complex] + expectation: float | complex total_counts: int - std_err: Optional[Union[float, complex]] = None - raw_expectation: Optional[Union[float, complex]] = None - raw_std_err: Optional[float] = None - calibration_expectation: Optional[Union[float, complex]] = None - calibration_std_err: Optional[Union[float, complex]] = None - calibration_counts: Optional[int] = None - additional_results: Optional[list["ExperimentResult"]] = None + std_err: float | complex | None = None + raw_expectation: float | complex | None = None + raw_std_err: float | None = None + calibration_expectation: float | complex | None = None + calibration_std_err: float | complex | None = None + calibration_counts: int | None = None + additional_results: list["ExperimentResult"] | None = None def __init__( self, setting: ExperimentSetting, - expectation: Union[float, complex], + expectation: float | complex, total_counts: int, - std_err: Optional[Union[float, complex]] = None, - raw_expectation: Optional[Union[float, complex]] = None, - raw_std_err: Optional[Union[float, complex]] = None, - calibration_expectation: Optional[Union[float, complex]] = None, - calibration_std_err: Optional[Union[float, complex]] = None, - calibration_counts: Optional[int] = None, - additional_results: Optional[list["ExperimentResult"]] = None, + std_err: float | complex | None = None, + raw_expectation: float | complex | None = None, + raw_std_err: float | complex | None = None, + calibration_expectation: float | complex | None = None, + calibration_std_err: float | complex | None = None, + calibration_counts: int | None = None, + additional_results: list["ExperimentResult"] | None = None, ): object.__setattr__(self, "setting", setting) object.__setattr__(self, "expectation", expectation) @@ -95,9 +95,7 @@ def serializable(self) -> dict[str, Any]: } -def bitstrings_to_expectations( - bitstrings: np.ndarray, joint_expectations: Optional[list[list[int]]] = None -) -> np.ndarray: +def bitstrings_to_expectations(bitstrings: np.ndarray, joint_expectations: list[list[int]] | None = None) -> np.ndarray: """Given an array of bitstrings, map them to expectation values and return the desired joint expectation values. If no joint expectations are desired, then just the 1 -> -1, 0 -> 1 mapping is performed. @@ -157,7 +155,8 @@ def correct_experiment_result( len_calibration = len(calibration.additional_results) raise ValueError(f"Length of results ({len_result}) should match calibration ({len_calibration}).") additional_results = [ - correct_experiment_result(r, c) for r, c in zip(result.additional_results, calibration.additional_results) + correct_experiment_result(r, c) + for r, c in zip(result.additional_results, calibration.additional_results, strict=False) ] return ExperimentResult( @@ -175,11 +174,11 @@ def correct_experiment_result( def ratio_variance( - a: Union[float, complex, np.number, np.ndarray], - var_a: Union[float, complex, np.number, np.ndarray], - b: Union[float, complex, np.number, np.ndarray], - var_b: Union[float, complex, np.number, np.ndarray], -) -> Union[float, complex, np.number, np.ndarray]: + a: float | complex | np.number | np.ndarray, + var_a: float | complex | np.number | np.ndarray, + b: float | complex | np.number | np.ndarray, + var_b: float | complex | np.number | np.ndarray, +) -> float | complex | np.number | np.ndarray: r"""Compute the variance on the ratio Y = A/B. Given random variables 'A' and 'B', compute the variance on the ratio Y = A/B. Denote the diff --git a/pyquil/experiment/_setting.py b/pyquil/experiment/_setting.py index faf443927..64f776a63 100644 --- a/pyquil/experiment/_setting.py +++ b/pyquil/experiment/_setting.py @@ -23,7 +23,7 @@ import re from collections.abc import Generator, Iterable from dataclasses import dataclass -from typing import Any, Optional, cast +from typing import Any, cast from pyquil.paulis import PauliTerm, sI @@ -60,7 +60,7 @@ class TensorProductState: states: list[_OneQState] - def __init__(self, states: Optional[Iterable[_OneQState]] = None): + def __init__(self, states: Iterable[_OneQState] | None = None): if states is None: states = [] object.__setattr__(self, "states", list(states)) @@ -172,13 +172,13 @@ class ExperimentSetting: in_state: TensorProductState out_operator: PauliTerm - additional_expectations: Optional[list[list[int]]] = None + additional_expectations: list[list[int]] | None = None def __init__( self, in_state: TensorProductState, out_operator: PauliTerm, - additional_expectations: Optional[list[list[int]]] = None, + additional_expectations: list[list[int]] | None = None, ): object.__setattr__(self, "in_state", in_state) object.__setattr__(self, "out_operator", out_operator) diff --git a/pyquil/external/rpcq.py b/pyquil/external/rpcq.py index 25890dfeb..29ca6ea0d 100644 --- a/pyquil/external/rpcq.py +++ b/pyquil/external/rpcq.py @@ -2,7 +2,7 @@ import json from dataclasses import dataclass, field -from typing import Literal, Optional, Union +from typing import Literal, Union from deprecated.sphinx import deprecated from rpcq.messages import TargetDevice as TargetQuantumProcessor @@ -14,9 +14,9 @@ class Operator: """Operator class for representing a quantum gate or measurement.""" - operator: Optional[str] = None - duration: Optional[float] = None - fidelity: Optional[float] = None + operator: str | None = None + duration: float | None = None + fidelity: float | None = None def __post_init__(self): self.duration = float(self.duration) if self.duration is not None else None @@ -57,8 +57,8 @@ def parse_obj(cls, dictionary: dict): class MeasureInfo(Operator): """MeasureInfo class for representing a measurement operation.""" - qubit: Optional[Union[int, str]] = None - target: Optional[Union[int, str]] = None + qubit: int | str | None = None + target: int | str | None = None operator_type: Literal["measure"] = "measure" def __post_init__(self): @@ -107,8 +107,8 @@ def parse_obj(cls, dictionary: dict): class GateInfo(Operator): """GateInfo class for representing a quantum gate operation.""" - parameters: list[Union[float, str]] = field(default_factory=list) - arguments: list[Union[int, str]] = field(default_factory=list) + parameters: list[float | str] = field(default_factory=list) + arguments: list[int | str] = field(default_factory=list) operator_type: Literal["gate"] = "gate" def _dict(self) -> dict[str, JsonValue]: @@ -149,7 +149,7 @@ def parse_obj(cls, dictionary: dict): return cls._parse_obj(dictionary) -def _parse_operator(dictionary: dict) -> Union[GateInfo, MeasureInfo]: +def _parse_operator(dictionary: dict) -> GateInfo | MeasureInfo: operator_type = dictionary["operator_type"] if operator_type == "measure": return MeasureInfo._parse_obj(dictionary) @@ -163,8 +163,8 @@ class Qubit: """Qubit class for representing a qubit in a quantum processor.""" id: int - dead: Optional[bool] = False - gates: list[Union[GateInfo, MeasureInfo]] = field(default_factory=list) + dead: bool | None = False + gates: list[GateInfo | MeasureInfo] = field(default_factory=list) def _dict(self) -> dict[str, JsonValue]: encoding = dict(id=self.id, gates=[g._dict() for g in self.gates]) @@ -203,7 +203,7 @@ class Edge: """Edge class for representing a connection between two qubits.""" ids: list[int] - dead: Optional[bool] = False + dead: bool | None = False gates: list[GateInfo] = field(default_factory=list) def _dict(self) -> dict[str, JsonValue]: @@ -296,7 +296,7 @@ def add_qubit(quantum_processor: CompilerISA, node_id: int) -> Qubit: return quantum_processor.qubits[str(node_id)] -def get_qubit(quantum_processor: CompilerISA, node_id: int) -> Optional[Qubit]: +def get_qubit(quantum_processor: CompilerISA, node_id: int) -> Qubit | None: """Get a qubit from the quantum processor ISA.""" return quantum_processor.qubits.get(str(node_id)) @@ -314,7 +314,7 @@ def add_edge(quantum_processor: CompilerISA, qubit1: int, qubit2: int) -> Edge: return quantum_processor.edges[edge_id] -def get_edge(quantum_processor: CompilerISA, qubit1: int, qubit2: int) -> Optional[Edge]: +def get_edge(quantum_processor: CompilerISA, qubit1: int, qubit2: int) -> Edge | None: """Get an Edge between two qubit IDs.""" edge_id = make_edge_id(qubit1, qubit2) return quantum_processor.edges.get(edge_id) diff --git a/pyquil/gates.py b/pyquil/gates.py index 9709e13b3..248ae4602 100644 --- a/pyquil/gates.py +++ b/pyquil/gates.py @@ -15,9 +15,9 @@ ############################################################################## """Standard Gate definitions to use in a ``Program``.""" -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from numbers import Real -from typing import Callable, Optional, Union, no_type_check +from typing import no_type_check import numpy as np from deprecated.sphinx import deprecated, versionadded @@ -82,8 +82,8 @@ def unpack_reg_val_pair( classical_reg1: MemoryReferenceDesignator, - classical_reg2: Union[MemoryReferenceDesignator, int, float], -) -> tuple[MemoryReference, Union[MemoryReference, int, float]]: + classical_reg2: MemoryReferenceDesignator | int | float, +) -> tuple[MemoryReference, MemoryReference | int | float]: """Type check/coerce arguments to constructors for binary classical operators. :param classical_reg1: Specifier for the classical memory address to be modified. @@ -100,8 +100,8 @@ def unpack_reg_val_pair( def prepare_ternary_operands( classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator, - classical_reg3: Union[MemoryReferenceDesignator, int, float], -) -> tuple[MemoryReference, MemoryReference, Union[MemoryReference, int, float]]: + classical_reg3: MemoryReferenceDesignator | int | float, +) -> tuple[MemoryReference, MemoryReference, MemoryReference | int | float]: """Type check/coerce arguments to constructors for ternary classical operators. :param classical_reg1: Specifier for the classical memory address to be modified. @@ -420,7 +420,7 @@ def CPHASE10(angle: ParameterDesignator, control: QubitDesignator, target: Qubit # Cannot resolve forward reference in type annotations of "pyquil.gates.CPHASE": # name 'Expression' is not defined def CPHASE( - angle: Union[Expression, MemoryReference, np.int_, int, float, complex], + angle: Expression | MemoryReference | np.int_ | int | float | complex, control: QubitDesignator, target: QubitDesignator, ) -> Gate: @@ -651,7 +651,7 @@ def RYY(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> G """ -def RESET(qubit_index: Optional[QubitDesignator] = None) -> Union[Reset, ResetQubit]: +def RESET(qubit_index: QubitDesignator | None = None) -> Reset | ResetQubit: """Reset all qubits or just one specific qubit. :param qubit_index: The qubit to reset. @@ -685,8 +685,8 @@ def DECLARE( name: str, memory_type: str = "BIT", memory_size: int = 1, - shared_region: Optional[str] = None, - offsets: Optional[Sequence[tuple[int, str]]] = None, + shared_region: str | None = None, + offsets: Sequence[tuple[int, str]] | None = None, ) -> Declare: return Declare( name=name, @@ -697,7 +697,7 @@ def DECLARE( ) -def MEASURE(qubit: QubitDesignator, classical_reg: Optional[MemoryReferenceDesignator]) -> Measurement: +def MEASURE(qubit: QubitDesignator, classical_reg: MemoryReferenceDesignator | None) -> Measurement: """Produce a MEASURE instruction. :param qubit: The qubit to measure. @@ -730,9 +730,7 @@ def NOT(classical_reg: MemoryReferenceDesignator) -> ClassicalNot: return ClassicalNot(unpack_classical_reg(classical_reg)) -def AND( - classical_reg1: MemoryReferenceDesignator, classical_reg2: Union[MemoryReferenceDesignator, int] -) -> ClassicalAnd: +def AND(classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator | int) -> ClassicalAnd: """Produce an AND instruction. NOTE: The order of operands was reversed in pyQuil <=1.9 . @@ -748,7 +746,7 @@ def AND( def IOR( - classical_reg1: MemoryReferenceDesignator, classical_reg2: Union[MemoryReferenceDesignator, int] + classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator | int ) -> ClassicalInclusiveOr: """Produce an inclusive OR instruction. @@ -763,7 +761,7 @@ def IOR( def XOR( - classical_reg1: MemoryReferenceDesignator, classical_reg2: Union[MemoryReferenceDesignator, int] + classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator | int ) -> ClassicalExclusiveOr: """Produce an exclusive OR instruction. @@ -779,7 +777,7 @@ def XOR( def MOVE( classical_reg1: MemoryReferenceDesignator, - classical_reg2: Union[MemoryReferenceDesignator, int, float], + classical_reg2: MemoryReferenceDesignator | int | float, ) -> ClassicalMove: """Produce a MOVE instruction. @@ -819,7 +817,7 @@ def LOAD( def STORE( region_name: str, offset_reg: MemoryReferenceDesignator, - source: Union[MemoryReferenceDesignator, int, float], + source: MemoryReferenceDesignator | int | float, ) -> ClassicalStore: """Produce a STORE instruction. @@ -843,7 +841,7 @@ def CONVERT(classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryRef return ClassicalConvert(unpack_classical_reg(classical_reg1), unpack_classical_reg(classical_reg2)) -def ADD(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalAdd: +def ADD(classical_reg: MemoryReferenceDesignator, right: MemoryReferenceDesignator | int | float) -> ClassicalAdd: """Produce an ADD instruction. :param classical_reg: Left operand for the arithmetic operation. Also serves as the store @@ -855,7 +853,7 @@ def ADD(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe return ClassicalAdd(left, right) -def SUB(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalSub: +def SUB(classical_reg: MemoryReferenceDesignator, right: MemoryReferenceDesignator | int | float) -> ClassicalSub: """Produce a SUB instruction. :param classical_reg: Left operand for the arithmetic operation. Also serves as the store @@ -867,7 +865,7 @@ def SUB(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe return ClassicalSub(left, right) -def MUL(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalMul: +def MUL(classical_reg: MemoryReferenceDesignator, right: MemoryReferenceDesignator | int | float) -> ClassicalMul: """Produce a MUL instruction. :param classical_reg: Left operand for the arithmetic operation. Also serves as the store @@ -879,7 +877,7 @@ def MUL(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe return ClassicalMul(left, right) -def DIV(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalDiv: +def DIV(classical_reg: MemoryReferenceDesignator, right: MemoryReferenceDesignator | int | float) -> ClassicalDiv: """Produce an DIV instruction. :param classical_reg: Left operand for the arithmetic operation. Also serves as the store @@ -894,7 +892,7 @@ def DIV(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe def EQ( classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator, - classical_reg3: Union[MemoryReferenceDesignator, int, float], + classical_reg3: MemoryReferenceDesignator | int | float, ) -> ClassicalEqual: """Produce an EQ instruction. @@ -913,7 +911,7 @@ def EQ( def LT( classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator, - classical_reg3: Union[MemoryReferenceDesignator, int, float], + classical_reg3: MemoryReferenceDesignator | int | float, ) -> ClassicalLessThan: """Produce an LT instruction. @@ -931,7 +929,7 @@ def LT( def LE( classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator, - classical_reg3: Union[MemoryReferenceDesignator, int, float], + classical_reg3: MemoryReferenceDesignator | int | float, ) -> ClassicalLessEqual: """Produce an LE instruction. @@ -949,7 +947,7 @@ def LE( def GT( classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator, - classical_reg3: Union[MemoryReferenceDesignator, int, float], + classical_reg3: MemoryReferenceDesignator | int | float, ) -> ClassicalGreaterThan: """Produce an GT instruction. @@ -967,7 +965,7 @@ def GT( def GE( classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator, - classical_reg3: Union[MemoryReferenceDesignator, int, float], + classical_reg3: MemoryReferenceDesignator | int | float, ) -> ClassicalGreaterEqual: """Produce an GE instruction. @@ -1101,7 +1099,7 @@ def RAW_CAPTURE( # with a float, and everything in between should be of a particular # type T. @no_type_check -def DELAY(*args) -> Union[DelayFrames, DelayQubits]: +def DELAY(*args) -> DelayFrames | DelayQubits: """Produce a DELAY instruction. Note: There are two variants of DELAY. One applies to specific frames on some @@ -1133,7 +1131,7 @@ def DELAY(*args) -> Union[DelayFrames, DelayQubits]: ) -def FENCE(*qubits: Union[int, Qubit, FormalArgument]) -> Union[FenceAll, Fence]: +def FENCE(*qubits: int | Qubit | FormalArgument) -> FenceAll | Fence: """Produce a FENCE instruction. Note: If no qubits are specified, then this is interpreted as a global FENCE. @@ -1200,7 +1198,7 @@ def FENCE(*qubits: Union[int, Qubit, FormalArgument]) -> Union[FenceAll, Fence]: Dictionary of Quil-T AST construction functions keyed by instruction name. """ -STANDARD_INSTRUCTIONS: Mapping[str, Union[AbstractInstruction, Callable[..., AbstractInstruction]]] = { +STANDARD_INSTRUCTIONS: Mapping[str, AbstractInstruction | Callable[..., AbstractInstruction]] = { "WAIT": WAIT, "RESET": RESET, "DECLARE": DECLARE, diff --git a/pyquil/latex/_diagram.py b/pyquil/latex/_diagram.py index 0750febf7..0f7b4ab87 100644 --- a/pyquil/latex/_diagram.py +++ b/pyquil/latex/_diagram.py @@ -17,7 +17,7 @@ from collections import defaultdict from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, replace -from typing import Optional, cast +from typing import cast from warnings import warn from pyquil.quil import Program @@ -144,23 +144,23 @@ def TIKZ_MEASURE() -> str: return r"\meter{}" -def _format_parameter(param: ParameterDesignator, settings: Optional[DiagramSettings] = None) -> str: +def _format_parameter(param: ParameterDesignator, settings: DiagramSettings | None = None) -> str: formatted: str = format_parameter(param) if settings and settings.texify_numerical_constants: formatted = formatted.replace("pi", r"\pi") return formatted -def _format_parameters(params: Iterable[ParameterDesignator], settings: Optional[DiagramSettings] = None) -> str: +def _format_parameters(params: Iterable[ParameterDesignator], settings: DiagramSettings | None = None) -> str: return "(" + ",".join(_format_parameter(param, settings) for param in params) + ")" def TIKZ_GATE( name: str, size: int = 1, - params: Optional[Sequence[ParameterDesignator]] = None, + params: Sequence[ParameterDesignator] | None = None, dagger: bool = False, - settings: Optional[DiagramSettings] = None, + settings: DiagramSettings | None = None, ) -> str: cmd = r"\gate" rotations = ["RX", "RY", "RZ"] @@ -221,7 +221,7 @@ def append(self, qubit: int, op: str) -> None: """Add an operation to the rightmost edge of the specified qubit line.""" self.lines[qubit].append(op) - def append_diagram(self, diagram: "DiagramState", group: Optional[str] = None) -> "DiagramState": + def append_diagram(self, diagram: "DiagramState", group: str | None = None) -> "DiagramState": """Add all operations represented by the given diagram to their corresponding qubit lines in this diagram. If group is not None, then a TIKZ_GATE_GROUP is created with the label indicated by group. @@ -306,13 +306,13 @@ def __init__(self, circuit: Program, settings: DiagramSettings): self.circuit = circuit self.settings = settings # instructions currently being processed - self.working_instructions: Optional[list[AbstractInstruction]] = None + self.working_instructions: list[AbstractInstruction] | None = None # index into working instructions. we maintain the invariant that # working_instructions[0:index] has been processed, with the diagram # updated accordingly self.index = 0 # partially constructed diagram - self.diagram: Optional[DiagramState] = None + self.diagram: DiagramState | None = None def build(self) -> DiagramState: """Build the diagram.""" diff --git a/pyquil/latex/_ipython.py b/pyquil/latex/_ipython.py index 9ebe0994d..7ee58d258 100644 --- a/pyquil/latex/_ipython.py +++ b/pyquil/latex/_ipython.py @@ -18,7 +18,7 @@ import shutil import subprocess import tempfile -from typing import Any, Optional +from typing import Any from IPython.display import Image @@ -27,7 +27,7 @@ from pyquil.quil import Program -def display(circuit: Program, settings: Optional[DiagramSettings] = None, **image_options: Any) -> Image: +def display(circuit: Program, settings: DiagramSettings | None = None, **image_options: Any) -> Image: """Display a PyQuil circuit as an IPython image object. .. note:: diff --git a/pyquil/latex/_main.py b/pyquil/latex/_main.py index c45d91646..b59aed4a9 100644 --- a/pyquil/latex/_main.py +++ b/pyquil/latex/_main.py @@ -15,13 +15,11 @@ ############################################################################## """The main entry point to the LaTeX generation functionality in pyQuil.""" -from typing import Optional - from pyquil.latex._diagram import DiagramBuilder, DiagramSettings from pyquil.quil import Program -def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> str: +def to_latex(circuit: Program, settings: DiagramSettings | None = None) -> str: """Translate a given pyQuil Program to a TikZ picture in a LaTeX document. Here are some high points of the generation procedure (see ``pyquil/latex/_diagram.py``): diff --git a/pyquil/latex/latex_generation.py b/pyquil/latex/latex_generation.py index 815c0ae96..c4faec872 100644 --- a/pyquil/latex/latex_generation.py +++ b/pyquil/latex/latex_generation.py @@ -18,8 +18,6 @@ Note: this is a deprecated module: Import from pyquil.latex instead. """ -from typing import Optional - from deprecated.classic import deprecated from pyquil.latex._diagram import DiagramSettings @@ -30,7 +28,7 @@ version="4.0", reason="This module has been moved -- please import it as 'from pyquil.latex import to_latex' going forward", ) -def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> str: +def to_latex(circuit: Program, settings: DiagramSettings | None = None) -> str: """Produce a circuit diagram in LaTeX for a given pyQuil Program.""" from pyquil.latex._main import to_latex diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index f885d3497..f0ba21062 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -26,10 +26,11 @@ import itertools import json import logging +from collections.abc import Callable from dataclasses import dataclass, replace from functools import cached_property, reduce from itertools import product -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any import jax.numpy as jnp import numpy as np @@ -41,7 +42,7 @@ from scipy.linalg import logm as scipy_logm from pyquil.quilatom import Expression, FormalArgument, Parameter, substitute -from pyquil.quilbase import DefCircuit, DefGate, Gate, Measurement, Reset +from pyquil.quilbase import DefCircuit, DefGate, Gate, Measurement, Reset, ResetQubit if TYPE_CHECKING: from pyquil import Program @@ -63,10 +64,67 @@ def _parse_quil_instruction(quil_str: str) -> Gate | Measurement | Reset: elif rs_inst.is_measurement(): return Measurement._from_rs_measurement(rs_inst.to_measurement()) elif rs_inst.is_reset(): - return Reset._from_rs_reset(rs_inst.to_reset()) + reset = rs_inst.to_reset() + if reset.qubit is None: + return Reset._from_rs_reset(reset) + return ResetQubit._from_rs_reset(reset) raise ValueError(f"Unsupported instruction type in: {quil_str}") +def _pack_complex_array(array: Array | np.ndarray) -> dict[str, Any]: + """Pack a complex array into JSON-compatible real/imaginary pairs.""" + np_array = np.asarray(array) + return { + "_complex_array": [[float(value.real), float(value.imag)] for value in np_array.flat], + "shape": list(np_array.shape), + } + + +def _unpack_complex_array(data: dict[str, Any]) -> Array: + """Unpack a complex array from :func:`_pack_complex_array` data.""" + shape = tuple(data["shape"]) + return jnp.array([complex(pair[0], pair[1]) for pair in data["_complex_array"]], dtype=complex).reshape(shape) + + +def _pack_dims(dims: tuple[tuple[int, ...], tuple[int, ...]]) -> list[list[int]]: + """Pack quax operator dims into JSON-compatible lists.""" + return [list(dims[0]), list(dims[1])] + + +def _unpack_dims(data: list[list[int]]) -> tuple[tuple[int, ...], tuple[int, ...]]: + """Unpack quax operator dims from JSON-compatible lists.""" + if len(data) != 2: + raise ValueError(f"Serialized operator dims must contain output and input dims, got {data}.") + return (tuple(int(dim) for dim in data[0]), tuple(int(dim) for dim in data[1])) + + +def _infer_legacy_qubit_dims(shape: tuple[int, ...], *, superoperator: bool) -> tuple[tuple[int, ...], tuple[int, ...]]: + """Infer qubit-only dims for data serialized before dims were stored.""" + hilbert_dim = int(round(np.sqrt(shape[0]))) if superoperator else int(shape[0]) + num_qubits = int(round(np.log2(hilbert_dim))) + if 2**num_qubits != hilbert_dim: + raise ValueError( + "Serialized operator data does not include dims and its shape is not compatible with qubit-only dims." + ) + return ((2,) * num_qubits, (2,) * num_qubits) + + +def _pack_operator(operator: qx.SuperOp | qx.Unitary | qx.Choi) -> dict[str, Any]: + """Pack a quax operator matrix with explicit dimension metadata.""" + data = _pack_complex_array(operator.matrix) + data["dims"] = _pack_dims(operator.dims) + return data + + +def _unpack_operator_dims( + data: dict[str, Any], shape: tuple[int, ...], *, superoperator: bool +) -> tuple[tuple[int, ...], tuple[int, ...]]: + """Read explicit dims, falling back to the legacy qubit-only inference.""" + if "dims" in data: + return _unpack_dims(data["dims"]) + return _infer_legacy_qubit_dims(shape, superoperator=superoperator) + + def _resolve_params(params: list) -> list[float]: """Resolve gate parameters to concrete float values. @@ -106,7 +164,7 @@ def get_custom_gates_from_program(program: Program) -> CustomGateMap: if defgate.parameters: def parametric_gate(*args: float, defgate: DefGate = defgate) -> qx.Unitary: - parameter_map = {Parameter(p.name): arg for p, arg in zip(defgate.parameters, args)} + parameter_map = {Parameter(p.name): arg for p, arg in zip(defgate.parameters, args, strict=False)} matrix = jnp.asarray( [[substitute(element, parameter_map) for element in row] for row in defgate.matrix], # type: ignore[arg-type] dtype=complex, @@ -295,6 +353,14 @@ def from_pauli_noise( unitary = get_instruction_unitary(inst, custom_gates) num_qubits = len(unitary.dims[0]) + total_error_rate = 0.0 + for pauli, error_rate in pauli_noise.items(): + if error_rate < 0.0: + raise ValueError(f"Pauli term '{pauli}' has negative error rate {error_rate}.") + total_error_rate += error_rate + if total_error_rate > 1.0: + raise ValueError(f"Pauli error rates must sum to at most 1.0, got {total_error_rate}.") + for pauli in pauli_noise: if len(pauli) != num_qubits: raise ValueError(f"Pauli term '{pauli}' has length {len(pauli)}, expected {num_qubits}.") @@ -310,7 +376,8 @@ def from_pauli_noise( else: error_rate = 0 pauli_error_rates.append(error_rate) - assert jnp.isclose(1.0, sum(pauli_error_rates)) # noqa: S101 + if not jnp.isclose(1.0, sum(pauli_error_rates)): + raise ValueError("Pauli error rates plus the implicit identity rate must sum to 1.0.") pauli_error_rates = list(reversed(pauli_error_rates)) # Build Pauli Kraus operators using quax ensembles @@ -365,7 +432,7 @@ def from_random_coherent_error( pauli_matrices = qx.ensembles.PAULIS.matrix # shape (4, 2, 2) pauli_sum = jnp.eye(d, dtype=complex) * id_coeff pauli_products = list(itertools.product(pauli_matrices, repeat=num_qubits))[1:] - for paulis, coefficient in zip(pauli_products, coeffs): + for paulis, coefficient in zip(pauli_products, coeffs, strict=False): pauli_sum = pauli_sum + reduce(jnp.kron, paulis) * coefficient from jax.scipy.linalg import expm as jax_expm @@ -409,7 +476,7 @@ def from_mixture( # Build the mixture superop: (1-p_total) S(U) + sum p_i S(V_i @ U) p0 = 1.0 - error_prob noisy_superop_matrix = p0 * qx.to_superop(ideal).matrix - for p, v in zip(probabilities, constituents): + for p, v in zip(probabilities, constituents, strict=False): composed = v @ ideal noisy_superop_matrix = noisy_superop_matrix + p * qx.to_superop(composed).matrix noisy_superop = qx.SuperOp.from_matrix(noisy_superop_matrix, ideal.dims) @@ -436,11 +503,13 @@ def from_coherence_times( unitary = get_instruction_unitary(inst, custom_gates) qubits = inst.get_qubit_indices() num_sys = len(qubits) - assert num_sys == len(t1s) # noqa: S101 + if num_sys != len(t1s): + raise ValueError(f"Expected {num_sys} T1 values for {inst.out()}, got {len(t1s)}.") if t2s is None: t2s = [2 * t1 for t1 in t1s] else: - assert num_sys == len(t2s) # noqa: S101 + if num_sys != len(t2s): + raise ValueError(f"Expected {num_sys} T2 values for {inst.out()}, got {len(t2s)}.") t1_array = jnp.asarray(t1s) tphi_array = 1 / (1 / jnp.asarray(t2s) - 1 / t1_array) @@ -690,17 +759,12 @@ def to_json(self) -> str: :return: JSON string representation. """ - superop_array = np.asarray(self.process.matrix) - flat_data = [[float(val.real), float(val.imag)] for val in superop_array.flat] - data = { + "schema_version": 1, "inst": self.inst.out(), - "superop": {"_complex_array": flat_data, "shape": list(superop_array.shape)}, + "superop": _pack_operator(self.process), } - - u_array = np.asarray(self.target_unitary.matrix) - u_flat = [[float(val.real), float(val.imag)] for val in u_array.flat] - data["target_unitary"] = {"_complex_array": u_flat, "shape": list(u_array.shape)} + data["target_unitary"] = _pack_operator(self.target_unitary) return json.dumps(data) @@ -713,25 +777,20 @@ def from_json(cls: type[Channel], json_str: str) -> Channel: """ data = json.loads(json_str) inst = _parse_quil_instruction(data["inst"]) - assert isinstance(inst, Gate) # noqa: S101 + if not isinstance(inst, Gate): + raise TypeError(f"Channel JSON must contain a gate instruction, got {type(inst).__name__}.") superop_data = data["superop"] - flat = superop_data["_complex_array"] shape = tuple(superop_data["shape"]) - superop_array = jnp.array([complex(pair[0], pair[1]) for pair in flat], dtype=complex).reshape(shape) - # Infer dims from matrix shape: (d^2, d^2) -> d qubits each of dim 2 - d = int(jnp.sqrt(shape[0])) - num_qubits = int(jnp.round(jnp.log2(d))) - dims = ((2,) * num_qubits, (2,) * num_qubits) + superop_array = _unpack_complex_array(superop_data) + dims = _unpack_operator_dims(superop_data, shape, superoperator=True) superop = qx.SuperOp.from_matrix(superop_array, dims) if "target_unitary" in data: u_data = data["target_unitary"] - u_flat = u_data["_complex_array"] u_shape = tuple(u_data["shape"]) - u_array = jnp.array([complex(pair[0], pair[1]) for pair in u_flat], dtype=complex).reshape(u_shape) - u_num_qubits = int(jnp.round(jnp.log2(u_shape[0]))) - u_dims = ((2,) * u_num_qubits, (2,) * u_num_qubits) + u_array = _unpack_complex_array(u_data) + u_dims = _unpack_operator_dims(u_data, u_shape, superoperator=False) target_unitary = qx.Unitary.from_matrix(u_array, u_dims) else: target_unitary = get_instruction_unitary(inst) @@ -1057,15 +1116,14 @@ def to_json(self) -> str: :return: JSON string representation. """ - # Store per-outcome Choi matrices + # Store per-outcome superoperator matrices. instrument_data = [] for i in range(self.process.num_outcomes): - choi_i, _ = self.process.outcome_choi(i) - choi_array = np.asarray(choi_i.matrix) - flat = [[float(val.real), float(val.imag)] for val in choi_array.flat] - instrument_data.append({"_complex_array": flat, "shape": list(choi_array.shape)}) + superop_i, _ = self.process.outcome_superop(i) + instrument_data.append(_pack_operator(superop_i)) data = { + "schema_version": 1, "inst": self.inst.out(), "instruments": instrument_data, "measured_qudits": list(self.process.measured_qudits), @@ -1081,20 +1139,28 @@ def from_json(cls: type[MeasurementChannel], json_str: str) -> MeasurementChanne """ data = json.loads(json_str) inst = _parse_quil_instruction(data["inst"]) - assert isinstance(inst, Measurement) # noqa: S101 + if not isinstance(inst, Measurement): + raise TypeError( + f"MeasurementChannel JSON must contain a measurement instruction, got {type(inst).__name__}." + ) measured_qudits = tuple(data["measured_qudits"]) - choi_list = [] + superop_matrices = [] + instrument_dims = None for inst_data in data["instruments"]: - flat = inst_data["_complex_array"] shape = tuple(inst_data["shape"]) - arr = jnp.array([complex(pair[0], pair[1]) for pair in flat], dtype=complex).reshape(shape) - d = int(jnp.sqrt(shape[0])) - n_qubits = int(jnp.round(jnp.log2(d))) - choi_dims = ((2,) * n_qubits, (2,) * n_qubits) - choi_list.append(qx.Choi.from_matrix(arr, choi_dims)) - - instrument = qx.QuantumInstrument.from_choi(choi_list, measured_qudits) + arr = _unpack_complex_array(inst_data) + op_dims = _unpack_operator_dims(inst_data, shape, superoperator=True) + if instrument_dims is None: + instrument_dims = op_dims + elif instrument_dims != op_dims: + raise ValueError("All serialized measurement outcomes must have the same dims.") + superop_matrices.append(arr) + + if instrument_dims is None: + raise ValueError("MeasurementChannel JSON must contain at least one outcome superoperator.") + + instrument = qx.QuantumInstrument.from_matrix(jnp.stack(superop_matrices), instrument_dims, measured_qudits) return cls(inst=inst, process=instrument) # ────────────────────────────────────────────── @@ -1155,12 +1221,17 @@ class ResetChannel: replaces the reset instruction rather than being applied after it. """ - inst: Reset + inst: ResetQubit """The reset operation to which the channel applies.""" process: qx.SuperOp """A superoperator representation of the noisy reset (including ideal reset).""" + def __post_init__(self) -> None: + """Validate that ResetChannel is attached to a targeted reset.""" + if not isinstance(self.inst, ResetQubit): + raise TypeError("ResetChannel only supports targeted ResetQubit instructions.") + # ────────────────────────────────────────────── # Constructors # ────────────────────────────────────────────── @@ -1168,7 +1239,7 @@ class ResetChannel: @classmethod def from_reset_fidelity( cls: type[ResetChannel], - inst: Reset, + inst: ResetQubit, fidelity: float, dim: int = 2, ) -> ResetChannel: @@ -1183,6 +1254,9 @@ def from_reset_fidelity( :param dim: Hilbert-space dimension (2 for qubits). :return: A ResetChannel instance. """ + if not isinstance(inst, ResetQubit): + raise TypeError("ResetChannel only supports targeted ResetQubit instructions.") + ideal_superop = qx.gates.RESET(dim=dim) p = 1.0 - fidelity d2 = dim * dim @@ -1264,11 +1338,10 @@ def to_json(self) -> str: :return: JSON string representation. """ - superop_array = np.asarray(self.process.matrix) - flat = [[float(v.real), float(v.imag)] for v in superop_array.flat] data = { + "schema_version": 1, "inst": self.inst.out(), - "superop": {"_complex_array": flat, "shape": list(superop_array.shape)}, + "superop": _pack_operator(self.process), } return json.dumps(data) @@ -1281,14 +1354,12 @@ def from_json(cls: type[ResetChannel], json_str: str) -> ResetChannel: """ data = json.loads(json_str) inst = _parse_quil_instruction(data["inst"]) - assert isinstance(inst, Reset) # noqa: S101 + if not isinstance(inst, ResetQubit): + raise TypeError(f"ResetChannel JSON must contain a targeted reset instruction, got {type(inst).__name__}.") superop_data = data["superop"] - flat = superop_data["_complex_array"] shape = tuple(superop_data["shape"]) - arr = jnp.array([complex(pair[0], pair[1]) for pair in flat], dtype=complex).reshape(shape) - d = int(jnp.sqrt(shape[0])) - num_qubits = int(jnp.round(jnp.log2(d))) - dims = ((2,) * num_qubits, (2,) * num_qubits) + arr = _unpack_complex_array(superop_data) + dims = _unpack_operator_dims(superop_data, shape, superoperator=True) process = qx.SuperOp.from_matrix(arr, dims) return cls(inst=inst, process=process) diff --git a/pyquil/noise/_legacy_noise.py b/pyquil/noise/_legacy_noise.py index 3d0cd823f..730a0b8a0 100644 --- a/pyquil/noise/_legacy_noise.py +++ b/pyquil/noise/_legacy_noise.py @@ -18,7 +18,7 @@ import sys from collections import namedtuple from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING, Any, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Optional, cast import numpy as np from deprecated import deprecated @@ -54,7 +54,7 @@ class KrausModel(_KrausModel): """ @staticmethod - def unpack_kraus_matrix(m: Union[list[Any], np.ndarray]) -> np.ndarray: + def unpack_kraus_matrix(m: list[Any] | np.ndarray) -> np.ndarray: """Unpack a JSON compatible representation of a complex Kraus matrix. :param m: The representation of a Kraus operator. Either a complex @@ -217,9 +217,7 @@ def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Se return pragmas -def append_kraus_to_gate( - kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray -) -> list[Union[np.number, np.ndarray]]: +def append_kraus_to_gate(kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray) -> list[np.number | np.ndarray]: """Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``. :param kraus_ops: The Kraus operators. @@ -265,7 +263,7 @@ def pauli_kraus_map(probabilities: Sequence[float]) -> list[np.ndarray]: else: operators = np.kron(paulis, paulis) # type: ignore - return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)] + return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators, strict=False)] def damping_kraus_map(p: float = 0.10) -> list[np.ndarray]: @@ -398,11 +396,11 @@ def _get_program_gates(prog: "Program") -> list[Gate]: def _decoherence_noise_model( gates: Sequence[Gate], - T1: Union[dict[int, float], float] = 30e-6, - T2: Union[dict[int, float], float] = 30e-6, + T1: dict[int, float] | float = 30e-6, + T2: dict[int, float] | float = 30e-6, gate_time_1q: float = 50e-9, gate_time_2q: float = 150e-09, - ro_fidelity: Union[dict[int, float], float] = 0.95, + ro_fidelity: dict[int, float] | float = 0.95, ) -> NoiseModel: """Return default noise model. @@ -566,11 +564,11 @@ def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": def add_decoherence_noise( prog: "Program", - T1: Union[dict[int, float], float] = 30e-6, - T2: Union[dict[int, float], float] = 30e-6, + T1: dict[int, float] | float = 30e-6, + T2: dict[int, float] | float = 30e-6, gate_time_1q: float = 50e-9, gate_time_2q: float = 150e-09, - ro_fidelity: Union[dict[int, float], float] = 0.95, + ro_fidelity: dict[int, float] | float = 0.95, ) -> "Program": """Add generic damping and dephasing noise to a program. diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index 547e96a6c..d47f69bcc 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -121,7 +121,7 @@ def _channel_map( self, ) -> dict[Gate | Measurement | ResetQubit, Channel | MeasurementChannel | ResetChannel | CycleChannel]: """Map from instruction to channel for fast lookup.""" - return {ch.inst: ch for ch in self.channels} # type: ignore[misc] + return {ch.inst: ch for ch in self.channels} @overload def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... diff --git a/pyquil/noise_gates.py b/pyquil/noise_gates.py index 7ba5db6e2..db699c740 100644 --- a/pyquil/noise_gates.py +++ b/pyquil/noise_gates.py @@ -1,7 +1,6 @@ """Utility functions for generating noise gates compatible with a QVM's instruction set architecture.""" import logging -from typing import Optional from pyquil.external.rpcq import CompilerISA, Edge, GateInfo, Supported1QGate, Supported2QGate from pyquil.quilatom import Parameter, unpack_qubit @@ -45,7 +44,7 @@ def _get_qvm_noise_supported_gates(isa: CompilerISA) -> list[Gate]: return gates -def _transform_rpcq_qubit_gate_info_to_qvm_noise_supported_gate(qubit_id: int, gate: GateInfo) -> Optional[Gate]: +def _transform_rpcq_qubit_gate_info_to_qvm_noise_supported_gate(qubit_id: int, gate: GateInfo) -> Gate | None: if gate.operator == Supported1QGate.RX: if len(gate.parameters) == 1 and gate.parameters[0] == 0.0: return None diff --git a/pyquil/operator_estimation.py b/pyquil/operator_estimation.py index 6158abf20..ed2182374 100644 --- a/pyquil/operator_estimation.py +++ b/pyquil/operator_estimation.py @@ -1,10 +1,10 @@ """Tools for estimating the expectation value of operators on a quantum computer.""" import logging -from collections.abc import Generator, Mapping +from collections.abc import Callable, Generator, Mapping from math import pi from numbers import Complex -from typing import Callable, Optional, cast +from typing import cast import numpy as np @@ -187,8 +187,8 @@ def _generate_experiment_programs( def measure_observables( qc: QuantumComputer, tomo_experiment: Experiment, - progress_callback: Optional[Callable[[int, int], None]] = None, - calibrate_readout: Optional[str] = "plus-eig", + progress_callback: Callable[[int, int], None] | None = None, + calibrate_readout: str | None = "plus-eig", ) -> Generator[ExperimentResult, None, None]: """Measure all the observables in a TomographyExperiment. @@ -214,7 +214,7 @@ def measure_observables( # generate programs for each group of simultaneous settings. programs, meas_qubits = _generate_experiment_programs(tomo_experiment, reset) - for i, (prog, qubits, settings) in enumerate(zip(programs, meas_qubits, tomo_experiment)): + for i, (prog, qubits, settings) in enumerate(zip(programs, meas_qubits, tomo_experiment, strict=False)): log.info(f"Collecting bitstrings for the {len(settings)} settings: {settings}") # we don't need to do any actual measurement if the combined operator is simply the diff --git a/pyquil/paulis.py b/pyquil/paulis.py index a7baf72c7..79f863a28 100644 --- a/pyquil/paulis.py +++ b/pyquil/paulis.py @@ -19,13 +19,11 @@ import re import warnings from collections import OrderedDict -from collections.abc import Hashable, Iterable, Iterator, Sequence +from collections.abc import Callable, Hashable, Iterable, Iterator, Sequence from functools import reduce from itertools import product from numbers import Complex, Number from typing import ( - Callable, - Optional, Union, cast, ) @@ -117,7 +115,7 @@ def __init__(self, *args: object, **kwargs: object): """ -def _valid_qubit(index: Optional[Union[PauliTargetDesignator, QubitPlaceholder]]) -> bool: +def _valid_qubit(index: PauliTargetDesignator | QubitPlaceholder | None) -> bool: return ( (isinstance(index, integer_types) and index >= 0) or isinstance(index, QubitPlaceholder) @@ -131,7 +129,7 @@ class PauliTerm: def __init__( self, op: str, - index: Optional[PauliTargetDesignator], + index: PauliTargetDesignator | None, coefficient: ExpressionDesignator = 1.0, ): """Create a new Pauli Term with a Pauli operator at a particular index and a leading coefficient. @@ -152,7 +150,7 @@ def __init__( self._ops[index] = op if isinstance(coefficient, Number): - self.coefficient: Union[complex, Expression] = complex(coefficient) + self.coefficient: complex | Expression = complex(coefficient) else: self.coefficient = coefficient @@ -286,7 +284,7 @@ def _multiply_factor(self, factor: str, index: PauliTargetDesignator) -> "PauliT return new_term - def __mul__(self, term: Union[PauliDesignator, ExpressionDesignator]) -> PauliDesignator: + def __mul__(self, term: PauliDesignator | ExpressionDesignator) -> PauliDesignator: """Multiply this Pauli Term with another PauliTerm, PauliSum, or number according to the Pauli algebra rules. :param term: (PauliTerm or PauliSum or Number) A term to multiply by. @@ -333,7 +331,7 @@ def __pow__(self, power: int) -> "PauliTerm": result = cast(PauliTerm, result * self) return result - def __add__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum": + def __add__(self, other: PauliDesignator | ExpressionDesignator) -> "PauliSum": """Add this PauliTerm with another one. :param other: A PauliTerm object, a PauliSum object, or a Number @@ -448,7 +446,7 @@ def from_compact_str(cls, str_pauli_term: str) -> "PauliTerm": # parse the coefficient into either a float or complex str_coef = str_coef.replace(" ", "") try: - coef: Union[float, complex] = float(str_coef) + coef: float | complex = float(str_coef) except ValueError: try: coef = complex(str_coef) @@ -473,7 +471,7 @@ def from_compact_str(cls, str_pauli_term: str) -> "PauliTerm": raise ValueError(f"Expected operation to be PauliTerm, got {type(op)}.") return op - def pauli_string(self, qubits: Optional[Iterable[int]] = None) -> str: + def pauli_string(self, qubits: Iterable[int] | None = None) -> str: """Return a string representation of this PauliTerm without its coefficient and with implicit qubit indices. If an iterable of qubits is provided, each character in the resulting string represents @@ -506,7 +504,7 @@ def ZERO() -> PauliTerm: return PauliTerm("I", 0, 0) -def sI(q: Optional[int] = None) -> PauliTerm: +def sI(q: int | None = None) -> PauliTerm: """Return the identity operator, optionally on a particular qubit. This can be specified without a qubit. @@ -579,7 +577,7 @@ def __init__(self, terms: Sequence[PauliTerm]): def _from_rs_pauli_sum(cls, pauli_sum: quil_rs.PauliSum) -> "PauliSum": return cls([PauliTerm._from_rs_pauli_term(term) for term in pauli_sum.terms]) - def _to_rs_pauli_sum(self, arguments: Optional[list[PauliTargetDesignator]] = None) -> quil_rs.PauliSum: + def _to_rs_pauli_sum(self, arguments: list[PauliTargetDesignator] | None = None) -> quil_rs.PauliSum: rs_arguments: list[str] if arguments is None: argument_set: dict[str, None] = {} @@ -630,7 +628,7 @@ def __iter__(self) -> Iterator[PauliTerm]: """Iterate over the PauliTerms in the sum.""" return self.terms.__iter__() - def __mul__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum": + def __mul__(self, other: PauliDesignator | ExpressionDesignator) -> "PauliSum": """Multiply and simplify this PauliSum with another PauliSum, PauliTerm or Number object. :param other: a PauliSum, PauliTerm or Number object @@ -639,7 +637,7 @@ def __mul__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "Pauli if not isinstance(other, (Expression, Number, PauliTerm, PauliSum)): raise ValueError("Cannot multiply PauliSum by term that is not a Number, PauliTerm, or PauliSum") - other_terms: list[Union[PauliTerm, ExpressionDesignator]] = [] + other_terms: list[PauliTerm | ExpressionDesignator] = [] if isinstance(other, PauliSum): other_terms += other.terms else: @@ -686,7 +684,7 @@ def __pow__(self, power: int) -> "PauliSum": result *= self return result - def __add__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum": + def __add__(self, other: PauliDesignator | ExpressionDesignator) -> "PauliSum": """Add and simplify this PauliSum with another PauliSum, PauliTerm or Number objects. :param other: a PauliSum, PauliTerm or Number object @@ -715,7 +713,7 @@ def __radd__(self, other: ExpressionDesignator) -> "PauliSum": raise TypeError(f"Expected a Number object, got {type(other)}") return self + other - def __sub__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum": + def __sub__(self, other: PauliDesignator | ExpressionDesignator) -> "PauliSum": """Subtract and simplify this PauliSum with another PauliSum, PauliTerm or Number object. :param other: a PauliSum, PauliTerm or Number object @@ -723,7 +721,7 @@ def __sub__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "Pauli """ return self + -1.0 * other - def __rsub__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum": + def __rsub__(self, other: PauliDesignator | ExpressionDesignator) -> "PauliSum": """Subtract and simplify this PauliSum with another PauliSum, PauliTerm or Number object. :param other: a PauliSum, PauliTerm or Number object @@ -933,7 +931,7 @@ def combined_exp_wrap(param: float) -> Program: def exponentiate_pauli_sum( - pauli_sum: Union[PauliSum, PauliTerm], + pauli_sum: PauliSum | PauliTerm, ) -> NDArray[np.complex128]: r"""Exponentiate a sequence of PauliTerms, which may or may not commute. diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index d0e59a5b9..0f9e5c236 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -18,7 +18,7 @@ import logging from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence -from typing import Any, Optional, Union +from typing import Any import numpy as np from numpy.random.mtrand import RandomState @@ -71,7 +71,7 @@ class AbstractQuantumSimulator(ABC): """An abstract interface for a quantum simulator.""" @abstractmethod - def __init__(self, n_qubits: int, rs: Optional[RandomState]): + def __init__(self, n_qubits: int, rs: RandomState | None): """Initialize. :param n_qubits: Number of qubits to simulate. @@ -115,7 +115,7 @@ def do_measurement(self, qubit: int) -> int: """ @abstractmethod - def expectation(self, operator: Union[PauliTerm, PauliSum]) -> complex: + def expectation(self, operator: PauliTerm | PauliSum) -> complex: """Compute the expectation of an operator. :param operator: The operator @@ -156,9 +156,9 @@ class PyQVM(QAM["PyQVM"]): def __init__( self, n_qubits: int, - quantum_simulator_type: Optional[type[AbstractQuantumSimulator]] = None, - seed: Optional[int] = None, - post_gate_noise_probabilities: Optional[dict[str, float]] = None, + quantum_simulator_type: type[AbstractQuantumSimulator] | None = None, + seed: int | None = None, + post_gate_noise_probabilities: dict[str, float] | None = None, ): """PyQuil's built-in Quil virtual machine. @@ -189,19 +189,19 @@ def __init__( quantum_simulator_type = ReferenceDensitySimulator self.n_qubits = n_qubits - self.ram: dict[str, list[Union[float, int]]] = {} + self.ram: dict[str, list[float | int]] = {} if post_gate_noise_probabilities is None: post_gate_noise_probabilities = {} self.post_gate_noise_probabilities = post_gate_noise_probabilities - self.program: Optional[Program] = None + self.program: Program | None = None self.program_counter: int = 0 self.defined_gates: dict[str, np.ndarray] = dict() # private implementation details - self._qubit_to_ram: Optional[dict[int, int]] = None - self._ro_size: Optional[int] = None + self._qubit_to_ram: dict[int, int] | None = None + self._ro_size: int | None = None self._memory_results = {} # type: ignore self.rs = np.random.RandomState(seed=seed) @@ -227,7 +227,7 @@ def execute_with_memory_map_batch( "PyQVM does not support batch execution as the state of the instance is reset at the start of each execute." ) - def execute(self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **__: Any) -> "PyQVM": + def execute(self, executable: QuantumExecutable, memory_map: MemoryMap | None = None, **__: Any) -> "PyQVM": """Execute a program on the PyQVM. Note that the state of the instance is reset on each call to ``execute``. @@ -287,7 +287,7 @@ def read_memory(self, *, region_name: str) -> np.ndarray: raise ValueError("No memory results available.") return np.asarray(self._memory_results[region_name]) - def find_label(self, label: Union[Label, LabelPlaceholder]) -> int: + def find_label(self, label: Label | LabelPlaceholder) -> int: """Iterate over the program and find a JumpTarget that has a Label matching the input label. :param label: Label object to search for in program @@ -332,7 +332,7 @@ def transition(self) -> bool: elif isinstance(instruction, Measurement): measured_val = self.wf_simulator.do_measurement(qubit=instruction.get_qubit_indices().pop()) - meas_reg: Optional[MemoryReference] = instruction.classical_reg + meas_reg: MemoryReference | None = instruction.classical_reg if meas_reg is None: raise ValueError("Measurement instruction must have a classical register.") self.ram[meas_reg.name][meas_reg.offset] = measured_val @@ -361,7 +361,7 @@ def transition(self) -> bool: elif isinstance(instruction, (JumpWhen, JumpUnless)): # JumpWhen/Unless; check classical reg - jump_reg: Optional[MemoryReference] = instruction.condition + jump_reg: MemoryReference | None = instruction.condition if jump_reg is None: raise ValueError("JumpWhen/Unless instruction must have a classical register.") cond = self.ram[jump_reg.name][jump_reg.offset] @@ -411,7 +411,7 @@ def transition(self) -> bool: if isinstance(instruction, ClassicalAnd): if not isinstance(left_val, int) or not isinstance(right_val, int): raise ValueError("AND requires a data type of INTEGER; not {type(left_val)} and {type(right_val)}") - new_val: Union[int, float] = left_val & right_val + new_val: int | float = left_val & right_val elif isinstance(instruction, ClassicalInclusiveOr): if not isinstance(left_val, int) or not isinstance(right_val, int): raise ValueError("OR requires a data type of INTEGER; not {type(left_val)} and {type(right_val)}") diff --git a/pyquil/quantum_processor/graph.py b/pyquil/quantum_processor/graph.py index d9eb49287..eff8546e2 100644 --- a/pyquil/quantum_processor/graph.py +++ b/pyquil/quantum_processor/graph.py @@ -1,6 +1,6 @@ """An implementation of an AbstractQuantumProcessor based on a NetworkX graph topology.""" -from typing import Any, Optional +from typing import Any import networkx as nx @@ -19,8 +19,8 @@ class NxQuantumProcessor(AbstractQuantumProcessor): def __init__( self, topology: nx.Graph, - gates_1q: Optional[list[str]] = None, - gates_2q: Optional[list[str]] = None, + gates_1q: list[str] | None = None, + gates_2q: list[str] | None = None, ) -> None: """Initialize a new NxQuantumProcessor. diff --git a/pyquil/quantum_processor/qcs.py b/pyquil/quantum_processor/qcs.py index f00101020..b23fe11b9 100644 --- a/pyquil/quantum_processor/qcs.py +++ b/pyquil/quantum_processor/qcs.py @@ -1,7 +1,5 @@ """An implementation of AbstractQuantumProcessor based on an InstructionSetArchitecture returned from the QCS API.""" -from typing import Optional - import networkx as nx from qcs_sdk import QCSClient from qcs_sdk.qpu.isa import InstructionSetArchitecture, get_instruction_set_architecture @@ -20,13 +18,13 @@ class QCSQuantumProcessor(AbstractQuantumProcessor): quantum_processor_id: str _isa: InstructionSetArchitecture - noise_model: Optional[NoiseModel] + noise_model: NoiseModel | None def __init__( self, quantum_processor_id: str, isa: InstructionSetArchitecture, - noise_model: Optional[NoiseModel] = None, + noise_model: NoiseModel | None = None, ): """Initialize a new QCSQuantumProcessor. @@ -61,7 +59,7 @@ def __repr__(self) -> str: def get_qcs_quantum_processor( quantum_processor_id: str, - client_configuration: Optional[QCSClient] = None, + client_configuration: QCSClient | None = None, timeout: float = 10.0, ) -> QCSQuantumProcessor: """Retrieve an instruction set architecture for the specified QPU ID and initialize a ``QCSQuantumProcessor`` with it. diff --git a/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py b/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py index ae0b1a31c..75ceabb04 100644 --- a/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py +++ b/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py @@ -1,6 +1,6 @@ """Transformers for converting between NetworkX graphs and CompilerISAs.""" -from typing import Optional, Union, cast +from typing import cast import networkx as nx import numpy as np @@ -28,7 +28,7 @@ def graph_to_compiler_isa( - graph: nx.Graph, gates_1q: Optional[list[str]] = None, gates_2q: Optional[list[str]] = None + graph: nx.Graph, gates_1q: list[str] | None = None, gates_2q: list[str] | None = None ) -> CompilerISA: """Generate an ``CompilerISA`` object from a NetworkX graph and list of 1Q and 2Q gates. @@ -108,17 +108,17 @@ def _make_wildcard_1q_gates() -> list[GateInfo]: def _transform_qubit_operation_to_gates( operation_name: str, -) -> list[Union[GateInfo, MeasureInfo]]: +) -> list[GateInfo | MeasureInfo]: if operation_name == Supported1QGate.I: - return cast(list[Union[GateInfo, MeasureInfo]], _make_i_gates()) + return cast(list[GateInfo | MeasureInfo], _make_i_gates()) elif operation_name == Supported1QGate.RX: - return cast(list[Union[GateInfo, MeasureInfo]], _make_rx_gates()) + return cast(list[GateInfo | MeasureInfo], _make_rx_gates()) elif operation_name == Supported1QGate.RZ: - return cast(list[Union[GateInfo, MeasureInfo]], _make_rz_gates()) + return cast(list[GateInfo | MeasureInfo], _make_rz_gates()) elif operation_name == Supported1QGate.MEASURE: - return cast(list[Union[GateInfo, MeasureInfo]], _make_measure_gates()) + return cast(list[GateInfo | MeasureInfo], _make_measure_gates()) elif operation_name == Supported1QGate.WILDCARD: - return cast(list[Union[GateInfo, MeasureInfo]], _make_wildcard_1q_gates()) + return cast(list[GateInfo | MeasureInfo], _make_wildcard_1q_gates()) else: raise GraphGateError(f"Unsupported graph qubit operation: {operation_name}") diff --git a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py index f3109de24..aca806322 100644 --- a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py +++ b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py @@ -2,7 +2,7 @@ from collections import defaultdict from collections.abc import Sequence -from typing import Optional, Union, cast +from typing import cast import numpy as np from qcs_sdk.qpu.isa import Characteristic, InstructionSetArchitecture, Operation @@ -190,7 +190,7 @@ def _make_rz_gates(node_id: int) -> list[GateInfo]: ] -def _get_frb_sim_1q(node_id: int, benchmarks: Sequence[Operation]) -> Optional[float]: +def _get_frb_sim_1q(node_id: int, benchmarks: Sequence[Operation]) -> float | None: frb_sim_1q = next( (benchmark for benchmark in benchmarks if benchmark.name == "randomized_benchmark_simultaneous_1q"), None ) @@ -230,15 +230,15 @@ def _transform_qubit_operation_to_gates( node_id: int, characteristics: Sequence[Characteristic], benchmarks: Sequence[Operation], -) -> list[Union[GateInfo, MeasureInfo]]: +) -> list[GateInfo | MeasureInfo]: if operation_name == Supported1QGate.RX: - return cast(list[Union[GateInfo, MeasureInfo]], _make_rx_gates(node_id, benchmarks)) + return cast(list[GateInfo | MeasureInfo], _make_rx_gates(node_id, benchmarks)) elif operation_name == Supported1QGate.RZ: - return cast(list[Union[GateInfo, MeasureInfo]], _make_rz_gates(node_id)) + return cast(list[GateInfo | MeasureInfo], _make_rz_gates(node_id)) elif operation_name == Supported1QGate.MEASURE: - return cast(list[Union[GateInfo, MeasureInfo]], _make_measure_gates(node_id, characteristics)) + return cast(list[GateInfo | MeasureInfo], _make_measure_gates(node_id, characteristics)) elif operation_name == Supported1QGate.WILDCARD: - return cast(list[Union[GateInfo, MeasureInfo]], _make_wildcard_1q_gates(node_id)) + return cast(list[GateInfo | MeasureInfo], _make_wildcard_1q_gates(node_id)) elif operation_name in {"I", "RESET"}: return [] else: diff --git a/pyquil/quil.py b/pyquil/quil.py index 43a5126f6..e74121939 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -19,11 +19,10 @@ import types import warnings from collections import defaultdict -from collections.abc import Generator, Iterable, Iterator, Sequence +from collections.abc import Callable, Generator, Iterable, Iterator, Sequence from copy import deepcopy from typing import ( Any, - Callable, Optional, TypeVar, Union, @@ -139,7 +138,7 @@ def __init__(self, *instructions: InstructionDesignator): # default number of shots to loop through self.num_shots = 1 - self.native_quil_metadata: Optional[NativeQuilMetadata] = None + self.native_quil_metadata: NativeQuilMetadata | None = None # The following properties are cached on the first call and won't be re-built unless cleared. # Any method that mutates the state program should use the `@_invalidates_cached_properties` @@ -224,7 +223,7 @@ def instructions(self, instructions: list[AbstractInstruction]) -> None: self._program = new_program._program @_invalidates_cached_properties - def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Program": + def inst(self, *instructions: InstructionDesignator | RSProgram) -> "Program": """Mutates the Program object by appending new instructions. This function accepts a number of different valid forms, e.g. @@ -314,8 +313,8 @@ def with_loop( self, num_iterations: int, iteration_count_reference: MemoryReference, - start_label: Union[Label, LabelPlaceholder], - end_label: Union[Label, LabelPlaceholder], + start_label: Label | LabelPlaceholder, + end_label: Label | LabelPlaceholder, ) -> "Program": r"""Return a copy of the ``Program`` wrapped in a Quil loop that will execute ``num_iterations`` times. @@ -364,8 +363,8 @@ def resolve_placeholders(self) -> None: def resolve_placeholders_with_custom_resolvers( self, *, - label_resolver: Optional[Callable[[LabelPlaceholder], Optional[str]]] = None, - qubit_resolver: Optional[Callable[[QubitPlaceholder], Optional[int]]] = None, + label_resolver: Callable[[LabelPlaceholder], str | None] | None = None, + qubit_resolver: Callable[[QubitPlaceholder], int | None] | None = None, ) -> None: r"""Resolve ``LabelPlaceholder``\\s and ``QubitPlaceholder``\\s within the program using a function. @@ -380,13 +379,13 @@ def resolve_placeholders_with_custom_resolvers( rs_qubit_resolver = None if qubit_resolver is not None: - def rs_qubit_resolver(placeholder: quil_rs.QubitPlaceholder) -> Optional[int]: + def rs_qubit_resolver(placeholder: quil_rs.QubitPlaceholder) -> int | None: return qubit_resolver(QubitPlaceholder(placeholder=placeholder)) rs_label_resolver = None if label_resolver is not None: - def rs_label_resolver(placeholder: quil_rs.TargetPlaceholder) -> Optional[str]: + def rs_label_resolver(placeholder: quil_rs.TargetPlaceholder) -> str | None: return label_resolver(LabelPlaceholder(placeholder=placeholder)) self._program.resolve_placeholders_with_custom_resolvers( @@ -402,7 +401,7 @@ def resolve_qubit_placeholders(self) -> None: def resolve_qubit_placeholders_with_mapping(self, qubit_mapping: dict[QubitPlaceholder, int]) -> None: r"""Resolve all qubit placeholders using a mapping of ``QubitPlaceholder``\\s to the index they resolve to.""" - def qubit_resolver(placeholder: quil_rs.QubitPlaceholder) -> Optional[int]: + def qubit_resolver(placeholder: quil_rs.QubitPlaceholder) -> int | None: return qubit_mapping.get(QubitPlaceholder(placeholder), None) def label_resolver(_: quil_rs.TargetPlaceholder) -> None: @@ -502,7 +501,7 @@ def gate( self, name: str, params: Sequence[ParameterDesignator], - qubits: Sequence[Union[Qubit, QubitPlaceholder]], + qubits: Sequence[Qubit | QubitPlaceholder], ) -> "Program": """Add a gate to the program. @@ -523,8 +522,8 @@ def gate( def defgate( self, name: str, - matrix: Union[list[list[Any]], np.ndarray, np.matrix], - parameters: Optional[list[Parameter]] = None, + matrix: list[list[Any]] | np.ndarray | np.matrix, + parameters: list[Parameter] | None = None, ) -> "Program": """Define a new static gate. @@ -563,7 +562,7 @@ def define_noisy_gate(self, name: str, qubit_indices: Sequence[int], kraus_ops: _check_kraus_ops(len(qubit_indices), kraus_ops) return self.inst(_create_kraus_pragmas(name, tuple(qubit_indices), kraus_ops)) - def define_noisy_readout(self, qubit: Union[int], p00: float, p11: float) -> "Program": + def define_noisy_readout(self, qubit: int, p00: float, p11: float) -> "Program": """For this program define a classical bit flip readout error channel parametrized by ``p00`` and ``p11``. This models the effect of thermal noise that corrupts the readout signal **after** it has interrogated the @@ -598,7 +597,7 @@ def no_noise(self) -> "Program": """ return self.inst(Pragma("NO-NOISE")) - def measure(self, qubit: QubitDesignator, classical_reg: Optional[MemoryReferenceDesignator]) -> "Program": + def measure(self, qubit: QubitDesignator, classical_reg: MemoryReferenceDesignator | None) -> "Program": """Measures a qubit at qubit_index and puts the result in classical_reg. :param qubit: The qubit to measure. @@ -608,7 +607,7 @@ def measure(self, qubit: QubitDesignator, classical_reg: Optional[MemoryReferenc """ return self.inst(MEASURE(qubit, classical_reg)) - def reset(self, qubit_index: Optional[int] = None) -> "Program": + def reset(self, qubit_index: int | None = None) -> "Program": """Reset all qubits or just a specific qubit at qubit_index. :param qubit_index: The address of the qubit to reset. @@ -618,7 +617,7 @@ def reset(self, qubit_index: Optional[int] = None) -> "Program": """ return self.inst(RESET(qubit_index)) - def measure_all(self, *qubit_reg_pairs: tuple[QubitDesignator, Optional[MemoryReferenceDesignator]]) -> "Program": + def measure_all(self, *qubit_reg_pairs: tuple[QubitDesignator, MemoryReferenceDesignator | None]) -> "Program": """Measures many qubits into their specified classical bits, in the order they were entered. If no qubit/register pairs are provided, measure all qubits present in the program into classical addresses of @@ -738,8 +737,8 @@ def declare( name: str, memory_type: str = "BIT", memory_size: int = 1, - shared_region: Optional[str] = None, - offsets: Optional[Sequence[tuple[int, str]]] = None, + shared_region: str | None = None, + offsets: Sequence[tuple[int, str]] | None = None, ) -> MemoryReference: """DECLARE a quil variable. @@ -789,7 +788,7 @@ def wrap_in_numshots_loop(self, shots: int) -> "Program": self.num_shots = shots return self - def out(self, *, calibrations: Optional[bool] = True) -> str: + def out(self, *, calibrations: bool | None = True) -> str: """Serialize the Quil program to a string suitable for submitting to the QVM or QPU.""" if calibrations: return self._program.to_quil() @@ -802,7 +801,7 @@ def out(self, *, calibrations: Optional[bool] = True) -> str: version="4.0", reason="The indices flag will be removed. Use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Return all of the qubit indices used in this program, including gate applications and allocated qubits. For example: @@ -834,7 +833,7 @@ def get_qubit_indices(self) -> set[int]: """ return {q.to_fixed() for q in self._program.get_used_qubits()} - def match_calibrations(self, instr: AbstractInstruction) -> Optional[CalibrationMatch]: + def match_calibrations(self, instr: AbstractInstruction) -> CalibrationMatch | None: """Attempt to match a calibration to the provided instruction. Note: preference is given to later calibrations, i.e. in a program with @@ -866,7 +865,7 @@ def match_calibrations(self, instr: AbstractInstruction) -> Optional[Calibration return None - def get_calibration(self, instr: AbstractInstruction) -> Optional[Union[DefCalibration, DefMeasureCalibration]]: + def get_calibration(self, instr: AbstractInstruction) -> DefCalibration | DefMeasureCalibration | None: """Get the calibration corresponding to the provided instruction. :param instr: An instruction. @@ -881,7 +880,7 @@ def get_calibration(self, instr: AbstractInstruction) -> Optional[Union[DefCalib def calibrate( self, instruction: AbstractInstruction, - previously_calibrated_instructions: Optional[set[AbstractInstruction]] = None, + previously_calibrated_instructions: set[AbstractInstruction] | None = None, ) -> list[AbstractInstruction]: """Expand an instruction into its calibrated definition. @@ -948,7 +947,7 @@ def __iadd__(self, other: InstructionDesignator) -> "Program": self.inst(other) return self - def __getitem__(self, index: Union[slice, int]) -> Union[AbstractInstruction, "Program"]: + def __getitem__(self, index: slice | int) -> Union[AbstractInstruction, "Program"]: """Get the instruction at the given index, or a Program from a slice.""" return Program(self.instructions[index]) if isinstance(index, slice) else self.instructions[index] @@ -1046,7 +1045,7 @@ def get_classical_addresses_from_program(program: Program) -> dict[str, list[int return flattened_addresses -def address_qubits(program: Program, qubit_mapping: Optional[dict[QubitPlaceholder, int]] = None) -> Program: +def address_qubits(program: Program, qubit_mapping: dict[QubitPlaceholder, int] | None = None) -> Program: """Take a program which contains placeholders and assigns the all defined values. Either all qubits must be defined or all undefined. If qubits are diff --git a/pyquil/quilatom.py b/pyquil/quilatom.py index fd7b7f026..d7d782771 100644 --- a/pyquil/quilatom.py +++ b/pyquil/quilatom.py @@ -16,15 +16,14 @@ """Classes that represent the atomic building blocks of Quil expressions.""" import inspect -from collections.abc import Iterable, Mapping, Sequence +from collections.abc import Callable, Iterable, Mapping, Sequence from fractions import Fraction from numbers import Number from typing import ( Any, - Callable, ClassVar, NoReturn, - Optional, + Self, Union, cast, ) @@ -33,7 +32,6 @@ import quil.expression as quil_rs_expr import quil.instructions as quil_rs from deprecated.sphinx import deprecated -from typing_extensions import Self class QuilAtom: @@ -127,7 +125,7 @@ class QubitPlaceholder(QuilAtom): Qubit placeholders must be resolved to actual qubits before they can be used in a program. """ - def __init__(self, placeholder: Optional[quil_rs.QubitPlaceholder] = None): + def __init__(self, placeholder: quil_rs.QubitPlaceholder | None = None): """Initialize a qubit placeholder, or get a new handle for an existing placeholder.""" if placeholder is not None: self._placeholder = placeholder @@ -191,7 +189,7 @@ def __lt__(self, other: object) -> bool: QubitDesignator = Union[Qubit, QubitPlaceholder, FormalArgument, int] -def _convert_to_rs_qubit(qubit: Union[QubitDesignator, quil_rs.Qubit, QubitPlaceholder]) -> quil_rs.Qubit: +def _convert_to_rs_qubit(qubit: QubitDesignator | quil_rs.Qubit | QubitPlaceholder) -> quil_rs.Qubit: if isinstance(qubit, quil_rs.Qubit): return qubit if isinstance(qubit, Qubit): @@ -209,7 +207,7 @@ def _convert_to_rs_qubits(qubits: Iterable[QubitDesignator]) -> list[quil_rs.Qub return [_convert_to_rs_qubit(qubit) for qubit in qubits] -def _convert_to_py_qubit(qubit: Union[QubitDesignator, quil_rs.Qubit, quil_rs.QubitPlaceholder]) -> QubitDesignator: +def _convert_to_py_qubit(qubit: QubitDesignator | quil_rs.Qubit | quil_rs.QubitPlaceholder) -> QubitDesignator: if isinstance(qubit, quil_rs.Qubit): if qubit.is_fixed(): return Qubit(qubit.to_fixed()) @@ -222,11 +220,11 @@ def _convert_to_py_qubit(qubit: Union[QubitDesignator, quil_rs.Qubit, quil_rs.Qu raise ValueError(f"{type(qubit)} is not a valid QubitDesignator") -def _convert_to_py_qubits(qubits: Iterable[Union[QubitDesignator, quil_rs.Qubit]]) -> list[QubitDesignator]: +def _convert_to_py_qubits(qubits: Iterable[QubitDesignator | quil_rs.Qubit]) -> list[QubitDesignator]: return [_convert_to_py_qubit(qubit) for qubit in qubits] -def unpack_qubit(qubit: Union[QubitDesignator, FormalArgument]) -> Union[Qubit, QubitPlaceholder, FormalArgument]: +def unpack_qubit(qubit: QubitDesignator | FormalArgument) -> Qubit | QubitPlaceholder | FormalArgument: """Get a qubit from an object. :param qubit: the qubit designator to unpack. @@ -339,7 +337,7 @@ class LabelPlaceholder(QuilAtom): All placeholders must be resolved to actual labels before they can be used in a program. """ - def __init__(self, prefix: str = "L", *, placeholder: Optional[quil_rs.TargetPlaceholder] = None): + def __init__(self, prefix: str = "L", *, placeholder: quil_rs.TargetPlaceholder | None = None): """Initialize a new label placeholder.""" if placeholder: self.target = quil_rs.Target.from_placeholder(placeholder) @@ -378,7 +376,7 @@ def __hash__(self) -> int: def _convert_to_rs_expression( - parameter: Union[ParameterDesignator, quil_rs_expr.Expression], + parameter: ParameterDesignator | quil_rs_expr.Expression, ) -> quil_rs_expr.Expression: if isinstance(parameter, quil_rs_expr.Expression): return parameter @@ -390,7 +388,7 @@ def _convert_to_rs_expression( def _convert_to_rs_expressions( - parameters: Sequence[Union[ParameterDesignator, quil_rs_expr.Expression]], + parameters: Sequence[ParameterDesignator | quil_rs_expr.Expression], ) -> list[quil_rs_expr.Expression]: return [_convert_to_rs_expression(parameter) for parameter in parameters] @@ -448,13 +446,11 @@ def format_parameter(element: ParameterDesignator) -> str: def _convert_to_py_expression( - expression: Union[ - ParameterDesignator, - ExpressionDesignator, - ExpressionValueDesignator, - quil_rs_expr.Expression, - quil_rs.MemoryReference, - ], + expression: ParameterDesignator + | ExpressionDesignator + | ExpressionValueDesignator + | quil_rs_expr.Expression + | quil_rs.MemoryReference, ) -> ExpressionDesignator: if isinstance(expression, (Expression, Number)): return expression @@ -494,7 +490,7 @@ def _convert_to_py_expression( def _convert_to_py_expressions( expressions: Sequence[ - Union[ParameterDesignator, ExpressionDesignator, quil_rs_expr.Expression, quil_rs.MemoryReference] + ParameterDesignator | ExpressionDesignator | quil_rs_expr.Expression | quil_rs.MemoryReference ], ) -> Sequence[ExpressionDesignator]: return [_convert_to_py_expression(expression) for expression in expressions] @@ -582,7 +578,7 @@ def __float__(self) -> float: raise ValueError(f"Cannot convert complex value with non-zero imaginary value to float: {value}") return float(value.real) - def __array__(self, dtype: Optional[np.dtype] = None) -> np.ndarray: + def __array__(self, dtype: np.dtype | None = None) -> np.ndarray: """Implement the numpy array protocol for this expression. If the dtype is not object, then there will be an attempt to simplify the expression to a @@ -621,7 +617,7 @@ def substitute(expr: ExpressionDesignator, d: ParameterSubstitutionsMapDesignato return expr -def substitute_array(a: Union[Sequence[Expression], np.ndarray], d: ParameterSubstitutionsMapDesignator) -> np.ndarray: +def substitute_array(a: Sequence[Expression] | np.ndarray, d: ParameterSubstitutionsMapDesignator) -> np.ndarray: """Apply ``substitute`` to all elements of an array ``a`` and return the resulting array. :param a: The array of expressions whose parameters or memory references are to be substituted. @@ -955,7 +951,7 @@ class MemoryReference(QuilAtom, Expression): the declared variable is of length >1 or 1, resp. """ - def __init__(self, name: str, offset: int = 0, declared_size: Optional[int] = None): + def __init__(self, name: str, offset: int = 0, declared_size: int | None = None): """Initialize a new memory reference.""" if not isinstance(offset, int) or offset < 0: raise TypeError("MemoryReference offset must be a non-negative int") @@ -1069,7 +1065,7 @@ def __str__(self) -> str: class WaveformInvocation(quil_rs.WaveformInvocation, QuilAtom): """A waveform invocation.""" - def __new__(cls, name: str, parameters: Optional[dict[str, ParameterDesignator]] = None) -> Self: + def __new__(cls, name: str, parameters: dict[str, ParameterDesignator] | None = None) -> Self: """Initialize a new waveform invocation.""" if parameters is None: parameters = {} @@ -1108,7 +1104,7 @@ def __new__(cls, name: str) -> Self: def _template_waveform_property( - name: str, *, dtype: Optional[Union[type[int], type[float]]] = None, doc: Optional[str] = None + name: str, *, dtype: type[int] | type[float] | None = None, doc: str | None = None ) -> property: """Initialize a getters, setters, and deleter for a parameter on a ``TemplateWaveform``. @@ -1121,7 +1117,7 @@ def _template_waveform_property( :param doc: Docstring for the property. """ - def fget(self: "TemplateWaveform") -> Optional[ParameterDesignator]: + def fget(self: "TemplateWaveform") -> ParameterDesignator | None: parameter = self.get_parameter(name) if parameter is None or dtype is None: return parameter @@ -1161,7 +1157,7 @@ def __new__( name: str, *, duration: float, - **kwargs: Union[Optional[ParameterDesignator], Optional[ExpressionDesignator]], + **kwargs: ParameterDesignator | None | ExpressionDesignator | None, ) -> Self: """Initialize a new TemplateWaveform.""" rs_parameters = {key: _convert_to_rs_expression(value) for key, value in kwargs.items() if value is not None} @@ -1172,14 +1168,14 @@ def out(self) -> str: """Return the waveform as a valid Quil string.""" return str(self) - def get_parameter(self, name: str) -> Optional[ParameterDesignator]: + def get_parameter(self, name: str) -> ParameterDesignator | None: """Get a parameter in the waveform by name.""" parameter = super().parameters.get(name, None) if parameter is None: return None return _convert_to_py_expression(parameter) - def set_parameter(self, name: str, value: Optional[ParameterDesignator]) -> None: + def set_parameter(self, name: str, value: ParameterDesignator | None) -> None: """Set a parameter with a value.""" parameters = super().parameters if value is None: @@ -1265,9 +1261,9 @@ def __str__(self) -> str: def _update_envelope( iqs: np.ndarray, rate: float, - scale: Optional[float], - phase: Optional[float], - detuning: Optional[float], + scale: float | None, + phase: float | None, + detuning: float | None, ) -> np.ndarray: """Update a pulse envelope by optional shape parameters. @@ -1278,7 +1274,7 @@ def _update_envelope( :return: The updated pulse envelope. """ - def default(obj: Optional[float], val: float) -> float: + def default(obj: float | None, val: float) -> float: return obj if obj is not None else val scale = default(scale, 1.0) diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py index 11cba4a23..5f15345b6 100644 --- a/pyquil/quilbase.py +++ b/pyquil/quilbase.py @@ -16,20 +16,17 @@ """Contains the core pyQuil objects that correspond to Quil instructions.""" import abc -from collections.abc import Container, Iterable, Sequence +from collections.abc import Callable, Container, Iterable, Sequence from typing import ( TYPE_CHECKING, Any, - Callable, ClassVar, - Optional, + Self, TypeVar, - Union, ) import numpy as np from deprecated.sphinx import deprecated -from typing_extensions import Self from pyquil.quilatom import ( Expression, @@ -148,7 +145,7 @@ def __reduce__(self: Any) -> tuple[Callable[[Any], AbstractInstruction], tuple[A return cls -def _convert_to_rs_instruction(instr: Union[AbstractInstruction, quil_rs.Instruction]) -> quil_rs.Instruction: +def _convert_to_rs_instruction(instr: AbstractInstruction | quil_rs.Instruction) -> quil_rs.Instruction: if isinstance(instr, quil_rs.Instruction): return instr if isinstance(instr, quil_rs.Arithmetic): @@ -375,7 +372,7 @@ def __new__( cls, name: str, params: Sequence[ParameterDesignator], - qubits: Sequence[Union[Qubit, QubitPlaceholder, FormalArgument, int]], + qubits: Sequence[Qubit | QubitPlaceholder | FormalArgument | int], modifiers: Sequence[quil_rs.GateModifier] = [], ) -> Self: """Initialize a new gate instruction.""" @@ -404,7 +401,7 @@ def qubits(self) -> list[QubitDesignator]: return self.get_qubits(indices=False) # type: ignore @qubits.setter # type: ignore[override] - def qubits(self, qubits: Sequence[Union[Qubit, QubitPlaceholder, FormalArgument]]) -> None: + def qubits(self, qubits: Sequence[Qubit | QubitPlaceholder | FormalArgument]) -> None: quil_rs.Gate.qubits.__set__(self, _convert_to_rs_qubits(qubits)) # type: ignore @property @@ -422,7 +419,7 @@ def modifiers(self) -> list[str]: return [str(modifier).upper() for modifier in super().modifiers] @modifiers.setter # type: ignore[override] - def modifiers(self, modifiers: Union[list[str], list[quil_rs.GateModifier]]) -> None: + def modifiers(self, modifiers: list[str] | list[quil_rs.GateModifier]) -> None: modifiers = [ self._to_rs_gate_modifier(modifier) if isinstance(modifier, str) else modifier for modifier in modifiers ] @@ -444,11 +441,7 @@ def get_qubit_indices(self) -> list[int]: def controlled( self, - control_qubit: Union[ - quil_rs.Qubit, - QubitDesignator, - Sequence[Union[QubitDesignator, quil_rs.Qubit]], - ], + control_qubit: quil_rs.Qubit | QubitDesignator | Sequence[QubitDesignator | quil_rs.Qubit], ) -> "Gate": """Add the CONTROLLED modifier to the gate with the given control qubit or Sequence of control qubits.""" if isinstance(control_qubit, Sequence): @@ -461,8 +454,8 @@ def controlled( def forked( self, - fork_qubit: Union[quil_rs.Qubit, QubitDesignator], - alt_params: Union[Sequence[ParameterDesignator], Sequence[quil_rs_expr.Expression]], + fork_qubit: quil_rs.Qubit | QubitDesignator, + alt_params: Sequence[ParameterDesignator] | Sequence[quil_rs_expr.Expression], ) -> "Gate": """Add the FORKED modifier to the gate with the given fork qubit and given additional parameters.""" forked = super().forked(_convert_to_rs_qubit(fork_qubit), _convert_to_rs_expressions(alt_params)) @@ -499,7 +492,7 @@ def __deepcopy__(self, memo: dict) -> "Gate": return Gate._from_rs_gate(super().__deepcopy__(memo)) -def _strip_modifiers(gate: Gate, limit: Optional[int] = None) -> Gate: +def _strip_modifiers(gate: Gate, limit: int | None = None) -> Gate: """Remove modifiers from :py:class:`Gate`. This function removes up to ``limit`` gate modifiers from the given gate, @@ -544,14 +537,14 @@ class Measurement(quil_rs.Measurement, AbstractInstruction): def __new__( cls, qubit: QubitDesignator, - classical_reg: Optional[MemoryReference], + classical_reg: MemoryReference | None, ) -> Self: """Initialize a new measurement instruction.""" target = cls._reg_to_target(classical_reg) return super().__new__(cls, _convert_to_rs_qubit(qubit), target) @classmethod - def _reg_to_target(cls, classical_reg: Optional[MemoryReference]) -> Optional[quil_rs.MemoryReference]: + def _reg_to_target(cls, classical_reg: MemoryReference | None) -> quil_rs.MemoryReference | None: if isinstance(classical_reg, quil_rs.MemoryReference): return classical_reg @@ -574,7 +567,7 @@ def qubit(self, qubit: QubitDesignator) -> None: quil_rs.Measurement.qubit.__set__(self, _convert_to_rs_qubit(qubit)) # type: ignore[attr-defined] @property - def classical_reg(self) -> Optional[MemoryReference]: + def classical_reg(self) -> MemoryReference | None: """Get the MemoryReference that this instruction writes to, if any.""" target = super().target if target is None: @@ -582,7 +575,7 @@ def classical_reg(self) -> Optional[MemoryReference]: return MemoryReference._from_rs_memory_reference(target) @classical_reg.setter - def classical_reg(self, classical_reg: Optional[MemoryReference]) -> None: + def classical_reg(self, classical_reg: MemoryReference | None) -> None: target = self._reg_to_target(classical_reg) quil_rs.Measurement.target.__set__(self, target) # type: ignore[attr-defined] @@ -590,7 +583,7 @@ def classical_reg(self, classical_reg: Optional[MemoryReference]) -> None: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubit this instruction measures.""" if indices: return self.get_qubit_indices() @@ -619,9 +612,9 @@ def __deepcopy__(self, memo: dict) -> "Measurement": class Reset(quil_rs.Reset, AbstractInstruction): """The RESET instruction.""" - def __new__(cls, qubit: Optional[Union[Qubit, QubitPlaceholder, FormalArgument, int]] = None) -> Self: + def __new__(cls, qubit: Qubit | QubitPlaceholder | FormalArgument | int | None = None) -> Self: """Initialize a new reset instruction.""" - rs_qubit: Optional[quil_rs.Qubit] = None + rs_qubit: quil_rs.Qubit | None = None if qubit is not None: rs_qubit = _convert_to_rs_qubit(qubit) return super().__new__(cls, rs_qubit) @@ -638,7 +631,7 @@ def out(self) -> str: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Optional[set[QubitDesignator]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | None: """Get the qubit this instruction resets.""" if super().qubit is None: return None @@ -646,22 +639,22 @@ def get_qubits(self, indices: bool = True) -> Optional[set[QubitDesignator]]: return self.get_qubit_indices() # type: ignore return {_convert_to_py_qubit(super().qubit)} # type: ignore - def get_qubit_indices(self) -> Optional[set[int]]: + def get_qubit_indices(self) -> set[int] | None: """Get the qubit this instruction resets, as an integer index.""" if super().qubit is None: return None return {super().qubit.to_fixed()} # type: ignore @property # type: ignore[override] - def qubit(self) -> Optional[QubitDesignator]: + def qubit(self) -> QubitDesignator | None: """Get the qubit this instruction resets, if any.""" if super().qubit: return _convert_to_py_qubit(super().qubit) # type: ignore return None @qubit.setter - def qubit(self, qubit: Optional[QubitDesignator]) -> None: - rs_qubit: Optional[quil_rs.Qubit] = None + def qubit(self, qubit: QubitDesignator | None) -> None: + rs_qubit: quil_rs.Qubit | None = None if qubit is not None: rs_qubit = _convert_to_rs_qubit(qubit) quil_rs.Reset.qubit.__set__(self, rs_qubit) # type: ignore[attr-defined] @@ -681,7 +674,7 @@ def __deepcopy__(self, memo: dict) -> "Reset": class ResetQubit(Reset): """A targeted RESET instruction.""" - def __new__(cls, qubit: Union[Qubit, QubitPlaceholder, FormalArgument, int]) -> Self: + def __new__(cls, qubit: Qubit | QubitPlaceholder | FormalArgument | int) -> Self: """Initialize a new reset instruction, with a target qubit.""" if qubit is None: raise TypeError("qubit should not be None") @@ -702,8 +695,8 @@ class DefGate(quil_rs.GateDefinition, AbstractInstruction): def __new__( cls, name: str, - matrix: Union[list[list[Expression]], np.ndarray, np.matrix], - parameters: Optional[list[Parameter]] = None, + matrix: list[list[Expression]] | np.ndarray | np.matrix, + parameters: list[Parameter] | None = None, ) -> Self: """Initialize a new gate definition. @@ -722,15 +715,13 @@ def _from_rs_gate_definition(cls, gate_definition: quil_rs.GateDefinition) -> Se @staticmethod def _convert_to_matrix_specification( - matrix: Union[list[list[Expression]], np.ndarray, np.matrix], + matrix: list[list[Expression]] | np.ndarray | np.matrix, ) -> quil_rs.GateSpecification: to_rs_matrix = np.vectorize(_convert_to_rs_expression, otypes=["O"]) return quil_rs.GateSpecification.from_matrix(to_rs_matrix(np.asarray(matrix))) @staticmethod - def _validate_matrix( - matrix: Union[list[list[Expression]], np.ndarray, np.matrix], contains_parameters: bool - ) -> None: + def _validate_matrix(matrix: list[list[Expression]] | np.ndarray | np.matrix, contains_parameters: bool) -> None: if isinstance(matrix, list): rows = len(matrix) if not all([len(row) == rows for row in matrix]): @@ -757,7 +748,7 @@ def out(self) -> str: """Return the Gate as a valid Quil string.""" return super().to_quil() - def get_constructor(self) -> Union[Callable[..., Gate], Callable[..., Callable[..., Gate]]]: + def get_constructor(self) -> Callable[..., Gate] | Callable[..., Callable[..., Gate]]: """Return a function that constructs this gate on variable qubit indices. For example, `mygate.get_constructor()(1) applies the gate to qubit 1.` @@ -800,7 +791,7 @@ def parameters(self) -> list[Parameter]: return [Parameter(name) for name in super().parameters] @parameters.setter # type: ignore[override] - def parameters(self, parameters: Optional[list[Parameter]]) -> None: + def parameters(self, parameters: list[Parameter] | None) -> None: quil_rs.GateDefinition.parameters.__set__(self, [param.name for param in parameters or []]) # type: ignore[attr-defined] # noqa def __hash__(self) -> int: @@ -819,14 +810,14 @@ def __deepcopy__(self, memo: dict) -> "DefGate": class DefPermutationGate(DefGate): """A gate defined by a permutation of numbers.""" - def __new__(cls, name: str, permutation: Union[list[int], np.ndarray]) -> Self: + def __new__(cls, name: str, permutation: list[int] | np.ndarray) -> Self: """Initialize a new gate definition with a permutation.""" specification = DefPermutationGate._convert_to_permutation_specification(permutation) gate_definition = quil_rs.GateDefinition(name, [], specification) return super()._from_rs_gate_definition(gate_definition) @staticmethod - def _convert_to_permutation_specification(permutation: Union[list[int], np.ndarray]) -> quil_rs.GateSpecification: + def _convert_to_permutation_specification(permutation: list[int] | np.ndarray) -> quil_rs.GateSpecification: return quil_rs.GateSpecification.from_permutation([int(x) for x in permutation]) @property @@ -908,7 +899,7 @@ def __str__(self) -> str: class JumpTarget(quil_rs.Label, AbstractInstruction): """Representation of a target that can be jumped to.""" - def __new__(cls, label: Union[Label, LabelPlaceholder]) -> Self: + def __new__(cls, label: Label | LabelPlaceholder) -> Self: """Initialize a new target.""" return super().__new__(cls, label.target) @@ -917,7 +908,7 @@ def _from_rs_label(cls, label: quil_rs.Label) -> "JumpTarget": return super().__new__(cls, label.target) @property - def label(self) -> Union[Label, LabelPlaceholder]: + def label(self) -> Label | LabelPlaceholder: """Get the target as a label.""" if super().target.is_placeholder(): return LabelPlaceholder._from_rs_target(super().target) @@ -941,7 +932,7 @@ def __deepcopy__(self, memo: dict) -> "JumpTarget": class JumpWhen(quil_rs.JumpWhen, AbstractInstruction): """The JUMP-WHEN instruction.""" - def __new__(cls, target: Union[Label, LabelPlaceholder], condition: MemoryReference) -> Self: + def __new__(cls, target: Label | LabelPlaceholder, condition: MemoryReference) -> Self: """Initialize a new JumpWhen instruction. :param target: The target to jump to if the condition is true. @@ -969,14 +960,14 @@ def condition(self, condition: MemoryReference) -> None: quil_rs.JumpWhen.condition.__set__(self, condition._to_rs_memory_reference()) # type: ignore[attr-defined] @property # type: ignore[override] - def target(self) -> Union[Label, LabelPlaceholder]: + def target(self) -> Label | LabelPlaceholder: """Get the target the instruction will jump to if the condition bit is not 1.""" if super().target.is_placeholder(): return LabelPlaceholder._from_rs_target(super().target) return Label._from_rs_target(super().target) @target.setter - def target(self, target: Union[Label, LabelPlaceholder]) -> None: + def target(self, target: Label | LabelPlaceholder) -> None: quil_rs.JumpWhen.target.__set__(self, target) # type: ignore[attr-defined] def __str__(self) -> str: @@ -993,7 +984,7 @@ def __deepcopy__(self, memo: dict) -> "JumpWhen": class JumpUnless(quil_rs.JumpUnless, AbstractInstruction): """The JUMP-UNLESS instruction.""" - def __new__(cls, target: Union[Label, LabelPlaceholder], condition: MemoryReference) -> Self: + def __new__(cls, target: Label | LabelPlaceholder, condition: MemoryReference) -> Self: """Initialize a new JumpUnless instruction. :param target: The target to jump to if the condition is true. @@ -1021,14 +1012,14 @@ def condition(self, condition: MemoryReference) -> None: quil_rs.JumpUnless.condition.__set__(self, condition._to_rs_memory_reference()) # type: ignore[attr-defined] @property # type: ignore[override] - def target(self) -> Union[Label, LabelPlaceholder]: + def target(self) -> Label | LabelPlaceholder: """Get the target the instruction will jump to if the condition bit is not 1.""" if super().target.is_placeholder(): return LabelPlaceholder._from_rs_target(super().target) return Label._from_rs_target(super().target) @target.setter - def target(self, target: Union[Label, LabelPlaceholder]) -> None: + def target(self, target: Label | LabelPlaceholder) -> None: quil_rs.JumpUnless.target.__set__(self, target) # type: ignore[attr-defined] def __str__(self) -> str: @@ -1135,7 +1126,7 @@ class LogicalBinaryOp(quil_rs.BinaryLogic, AbstractInstruction): op: ClassVar[quil_rs.BinaryOperator] - def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int]) -> Self: + def __new__(cls, left: MemoryReference, right: MemoryReference | int) -> Self: """Initialize the operands of the binary logical instruction.""" destination = left._to_rs_memory_reference() source = cls._to_rs_binary_operand(right) @@ -1146,13 +1137,13 @@ def _from_rs_binary_logic(cls, binary_logic: quil_rs.BinaryLogic) -> "LogicalBin return super().__new__(cls, binary_logic.operator, binary_logic.destination, binary_logic.source) @staticmethod - def _to_rs_binary_operand(operand: Union[MemoryReference, int]) -> quil_rs.BinaryOperand: + def _to_rs_binary_operand(operand: MemoryReference | int) -> quil_rs.BinaryOperand: if isinstance(operand, MemoryReference): return quil_rs.BinaryOperand.from_memory_reference(operand._to_rs_memory_reference()) return quil_rs.BinaryOperand.from_literal_integer(operand) @staticmethod - def _to_py_binary_operand(operand: quil_rs.BinaryOperand) -> Union[MemoryReference, int]: + def _to_py_binary_operand(operand: quil_rs.BinaryOperand) -> MemoryReference | int: if operand.is_literal_integer(): return operand.to_literal_integer() return MemoryReference._from_rs_memory_reference(operand.to_memory_reference()) @@ -1168,12 +1159,12 @@ def left(self, left: MemoryReference) -> None: quil_rs.BinaryLogic.destination.__set__(self, destination) # type: ignore[attr-defined] @property - def right(self) -> Union[MemoryReference, int]: + def right(self) -> MemoryReference | int: """The right hand side of the binary expression.""" return self._to_py_binary_operand(super().source) @right.setter - def right(self, right: Union[MemoryReference, int]) -> None: + def right(self, right: MemoryReference | int) -> None: source = self._to_rs_binary_operand(right) quil_rs.BinaryLogic.source.__set__(self, source) # type: ignore[attr-defined] @@ -1217,7 +1208,7 @@ class ArithmeticBinaryOp(quil_rs.Arithmetic, AbstractInstruction): op: ClassVar[quil_rs.ArithmeticOperator] - def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int, float]) -> Self: + def __new__(cls, left: MemoryReference, right: MemoryReference | int | float) -> Self: """Initialize the operands of the binary arithmetic instruction.""" right_operand = _to_rs_arithmetic_operand(right) return super().__new__(cls, cls.op, left._to_rs_memory_reference(), right_operand) @@ -1238,12 +1229,12 @@ def left(self, left: MemoryReference) -> None: ) @property - def right(self) -> Union[MemoryReference, int, float]: + def right(self) -> MemoryReference | int | float: """The left hand side of the binary expression.""" return _to_py_arithmetic_operand(super().source) @right.setter - def right(self, right: Union[MemoryReference, int, float]) -> None: + def right(self, right: MemoryReference | int | float) -> None: quil_rs.Arithmetic.source.__set__(self, _to_rs_arithmetic_operand(right)) # type: ignore[attr-defined] def out(self) -> str: @@ -1290,7 +1281,7 @@ class ClassicalDiv(ArithmeticBinaryOp): class ClassicalMove(quil_rs.Move, AbstractInstruction): """The MOVE instruction.""" - def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int, float]) -> "ClassicalMove": + def __new__(cls, left: MemoryReference, right: MemoryReference | int | float) -> "ClassicalMove": """Initialize a new MOVE instruction.""" return super().__new__(cls, left._to_rs_memory_reference(), _to_rs_arithmetic_operand(right)) @@ -1308,12 +1299,12 @@ def left(self, left: MemoryReference) -> None: quil_rs.Move.destination.__set__(self, left._to_rs_memory_reference()) # type: ignore @property - def right(self) -> Union[MemoryReference, int, float]: + def right(self) -> MemoryReference | int | float: """The right hand side (or "source") of the move instruction.""" return _to_py_arithmetic_operand(super().source) @right.setter - def right(self, right: Union[MemoryReference, int, float]) -> None: + def right(self, right: MemoryReference | int | float) -> None: quil_rs.Move.source.__set__(self, _to_rs_arithmetic_operand(right)) # type: ignore def out(self) -> str: @@ -1475,7 +1466,7 @@ def __deepcopy__(self, memo: dict) -> "ClassicalLoad": return ClassicalLoad._from_rs_load(super().__deepcopy__(memo)) -def _to_rs_arithmetic_operand(operand: Union[MemoryReference, int, float]) -> quil_rs.ArithmeticOperand: +def _to_rs_arithmetic_operand(operand: MemoryReference | int | float) -> quil_rs.ArithmeticOperand: if isinstance(operand, MemoryReference): return quil_rs.ArithmeticOperand.from_memory_reference(operand._to_rs_memory_reference()) if isinstance(operand, int): @@ -1485,7 +1476,7 @@ def _to_rs_arithmetic_operand(operand: Union[MemoryReference, int, float]) -> qu raise TypeError(f"{type(operand)} is not a valid ArithmeticOperand") -def _to_py_arithmetic_operand(operand: quil_rs.ArithmeticOperand) -> Union[MemoryReference, int, float]: +def _to_py_arithmetic_operand(operand: quil_rs.ArithmeticOperand) -> MemoryReference | int | float: if not isinstance(operand, quil_rs.ArithmeticOperand): raise TypeError(f"{type(operand)} is not an ArithmeticOperand") inner = operand.inner() @@ -1498,7 +1489,7 @@ def _to_py_arithmetic_operand(operand: quil_rs.ArithmeticOperand) -> Union[Memor class ClassicalStore(quil_rs.Store, AbstractInstruction): """The STORE instruction.""" - def __new__(cls, target: str, left: MemoryReference, right: Union[MemoryReference, int, float]) -> "ClassicalStore": + def __new__(cls, target: str, left: MemoryReference, right: MemoryReference | int | float) -> "ClassicalStore": """Initialize a new STORE instruction.""" rs_right = _to_rs_arithmetic_operand(right) return super().__new__(cls, target, left._to_rs_memory_reference(), rs_right) @@ -1526,12 +1517,12 @@ def left(self, left: MemoryReference) -> None: quil_rs.Store.offset.__set__(self, left._to_rs_memory_reference()) # type: ignore @property - def right(self) -> Union[MemoryReference, int, float]: + def right(self) -> MemoryReference | int | float: """The left hand side of the STORE instruction.""" return _to_py_arithmetic_operand(super().source) @right.setter - def right(self, right: Union[MemoryReference, int, float]) -> None: + def right(self, right: MemoryReference | int | float) -> None: quil_rs.Store.source.__set__(self, _to_rs_arithmetic_operand(right)) # type: ignore def out(self) -> str: @@ -1558,7 +1549,7 @@ def __new__( cls, target: MemoryReference, left: MemoryReference, - right: Union[MemoryReference, int, float], + right: MemoryReference | int | float, ) -> "ClassicalComparison": """Initialize a new comparison instruction.""" rs_target, rs_left, rs_right = ( @@ -1573,7 +1564,7 @@ def _from_rs_comparison(cls, comparison: quil_rs.Comparison) -> Self: return super().__new__(cls, comparison.operator, comparison.destination, comparison.lhs, comparison.rhs) @staticmethod - def _to_comparison_operand(operand: Union[MemoryReference, int, float]) -> quil_rs.ComparisonOperand: + def _to_comparison_operand(operand: MemoryReference | int | float) -> quil_rs.ComparisonOperand: if isinstance(operand, MemoryReference): return quil_rs.ComparisonOperand.from_memory_reference(operand._to_rs_memory_reference()) elif isinstance(operand, int): @@ -1583,7 +1574,7 @@ def _to_comparison_operand(operand: Union[MemoryReference, int, float]) -> quil_ raise TypeError(f"{type(operand)} is not a valid ComparisonOperand") @staticmethod - def _to_py_operand(operand: quil_rs.ComparisonOperand) -> Union[MemoryReference, int, float]: + def _to_py_operand(operand: quil_rs.ComparisonOperand) -> MemoryReference | int | float: if not isinstance(operand, quil_rs.ComparisonOperand): raise TypeError(f"{type(operand)} is not an ComparisonOperand") inner = operand.inner() @@ -1610,7 +1601,7 @@ def left(self, left: MemoryReference) -> None: quil_rs.Comparison.lhs.__set__(self, left._to_rs_memory_reference()) # type: ignore @property - def right(self) -> Union[MemoryReference, int, float]: + def right(self) -> MemoryReference | int | float: """The right hand side of the comparison.""" return self._to_py_operand(super().rhs) @@ -1668,7 +1659,7 @@ class ClassicalGreaterEqual(ClassicalComparison): class Jump(quil_rs.Jump, AbstractInstruction): """Representation of an unconditional jump instruction (JUMP).""" - def __new__(cls, target: Union[Label, LabelPlaceholder]) -> Self: + def __new__(cls, target: Label | LabelPlaceholder) -> Self: """Initialize a new jump instruction.""" return super().__new__(cls, target.target) @@ -1677,14 +1668,14 @@ def _from_rs_jump(cls, jump: quil_rs.Jump) -> Self: return super().__new__(cls, jump.target) @property # type: ignore[override] - def target(self) -> Union[Label, LabelPlaceholder]: + def target(self) -> Label | LabelPlaceholder: """Get the target of the jump.""" if super().target.is_placeholder(): return LabelPlaceholder._from_rs_target(super().target) return Label._from_rs_target(super().target) @target.setter - def target(self, target: Union[Label, LabelPlaceholder]) -> None: + def target(self, target: Label | LabelPlaceholder) -> None: quil_rs.Jump.target.__set__(self, target.target) # type: ignore[attr-defined] def out(self) -> str: @@ -1714,7 +1705,7 @@ class Pragma(quil_rs.Pragma, AbstractInstruction): def __new__( cls, command: str, - args: Sequence[Union[Qubit, FormalArgument, int, str]] = (), + args: Sequence[Qubit | FormalArgument | int | str] = (), freeform_string: str = "", ) -> Self: """Initialize a new PRAGMA instruction.""" @@ -1726,7 +1717,7 @@ def _from_rs_pragma(cls, pragma: quil_rs.Pragma) -> "Pragma": return super().__new__(cls, pragma.name, pragma.arguments, pragma.data) @staticmethod - def _to_pragma_arguments(args: Sequence[Union[QubitDesignator, str]]) -> list[quil_rs.PragmaArgument]: + def _to_pragma_arguments(args: Sequence[QubitDesignator | str]) -> list[quil_rs.PragmaArgument]: pragma_arguments = [] for arg in args: if isinstance(arg, Qubit): @@ -1771,7 +1762,7 @@ def args(self) -> tuple[QubitDesignator]: return tuple(Pragma._to_py_arguments(super().arguments)) # type: ignore[return-value] @args.setter - def args(self, args: Sequence[Union[QubitDesignator, str]]) -> None: + def args(self, args: Sequence[QubitDesignator | str]) -> None: quil_rs.Pragma.arguments.__set__(self, Pragma._to_pragma_arguments(args)) # type: ignore[attr-defined] @property @@ -1804,8 +1795,8 @@ def __new__( name: str, memory_type: str, memory_size: int = 1, - shared_region: Optional[str] = None, - offsets: Optional[Sequence[tuple[int, str]]] = None, + shared_region: str | None = None, + offsets: Sequence[tuple[int, str]] | None = None, ) -> Self: """Initialize a new DECLARE directive.""" vector = quil_rs.Vector(Declare._memory_type_to_scalar_type(memory_type), memory_size) @@ -1832,7 +1823,7 @@ def _memory_type_to_scalar_type(memory_type: str) -> quil_rs.ScalarType: raise ValueError(f"{memory_type} is not a valid scalar type.") @staticmethod - def _to_rs_offsets(offsets: Optional[Sequence[tuple[int, str]]]) -> list[quil_rs.Offset]: + def _to_rs_offsets(offsets: Sequence[tuple[int, str]] | None) -> list[quil_rs.Offset]: if offsets is None: return [] return [ @@ -1862,7 +1853,7 @@ def memory_size(self, memory_size: int) -> None: quil_rs.Declaration.size.__set__(self, vector) # type: ignore[attr-defined] @property - def shared_region(self) -> Optional[str]: + def shared_region(self) -> str | None: """Get the memory region this declaration is sharing with, if any.""" sharing = super().sharing if sharing is None: @@ -1870,7 +1861,7 @@ def shared_region(self) -> Optional[str]: return sharing.name @shared_region.setter - def shared_region(self, shared_region: Optional[str]) -> None: + def shared_region(self, shared_region: str | None) -> None: sharing = None if not shared_region else quil_rs.Sharing(shared_region, []) current_sharing = super().sharing if sharing and isinstance(current_sharing, quil_rs.Sharing): @@ -1886,14 +1877,14 @@ def offsets(self) -> list[tuple[int, str]]: return [(offset.offset, str(offset.data_type).upper()) for offset in sharing.offsets] @offsets.setter - def offsets(self, offsets: Optional[list[tuple[int, str]]]) -> None: + def offsets(self, offsets: list[tuple[int, str]] | None) -> None: sharing = super().sharing if sharing is None: raise ValueError("DECLARE without a shared region cannot use offsets") sharing.offsets = Declare._to_rs_offsets(offsets) quil_rs.Declaration.sharing.__set__(self, sharing) # type: ignore[attr-defined] - def asdict(self) -> dict[str, Union[Sequence[tuple[int, str]], Optional[str], int]]: + def asdict(self) -> dict[str, Sequence[tuple[int, str]] | str | None | int]: """Get the DECLARE directive as a dictionary.""" return { "name": self.name, @@ -1962,7 +1953,7 @@ def __str__(self) -> str: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the pulse operates on.""" if indices: return self.get_qubit_indices() @@ -2048,7 +2039,7 @@ def freq(self, freq: ParameterDesignator) -> None: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the frequency is set on.""" if indices: return self.get_qubit_indices() @@ -2106,7 +2097,7 @@ def freq(self, freq: ParameterDesignator) -> None: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the frequency is shifted on.""" if indices: return self.get_qubit_indices() @@ -2164,7 +2155,7 @@ def phase(self, phase: ParameterDesignator) -> None: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the quibts the phase is set on.""" if indices: return self.get_qubit_indices() @@ -2222,7 +2213,7 @@ def phase(self, phase: ParameterDesignator) -> None: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the phase is shifted on.""" if indices: return self.get_qubit_indices() @@ -2280,7 +2271,7 @@ def __str__(self) -> str: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the swap-phases instruction operates on.""" if indices: return self.get_qubit_indices() @@ -2338,7 +2329,7 @@ def scale(self, scale: ParameterDesignator) -> None: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the scale is set on.""" if indices: return self.get_qubit_indices() @@ -2452,7 +2443,7 @@ def __str__(self) -> str: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the capture instruction operates on.""" if indices: return self.get_qubit_indices() @@ -2541,7 +2532,7 @@ def __str__(self) -> str: version="4.0", reason="The indices flag will be removed, use get_qubit_indices() instead.", ) - def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]: + def get_qubits(self, indices: bool = True) -> set[QubitDesignator] | set[int]: """Get the qubits the raw-capture instruction operates on.""" if indices: return self.get_qubit_indices() @@ -2563,7 +2554,7 @@ def __deepcopy__(self, memo: dict) -> "RawCapture": class Delay(quil_rs.Delay, AbstractInstruction): """The DELAY instruction.""" - def __new__(cls, frames: list[Frame], qubits: Sequence[Union[int, Qubit, FormalArgument]], duration: float) -> Self: + def __new__(cls, frames: list[Frame], qubits: Sequence[int | Qubit | FormalArgument], duration: float) -> Self: """Initialize a new DELAY instruction.""" frame_names = [frame.name for frame in frames] rs_qubits = _convert_to_rs_qubits(Delay._join_frame_qubits(frames, list(qubits))) @@ -2576,8 +2567,8 @@ def _from_rs_delay(cls, delay: quil_rs.Delay) -> "Delay": @staticmethod def _join_frame_qubits( - frames: Sequence[Frame], qubits: Sequence[Union[int, Qubit, FormalArgument]] - ) -> list[Union[int, Qubit, FormalArgument]]: + frames: Sequence[Frame], qubits: Sequence[int | Qubit | FormalArgument] + ) -> list[int | Qubit | FormalArgument]: merged_qubits = set(qubits) for frame in frames: merged_qubits.update(frame.qubits) # type: ignore @@ -2596,7 +2587,7 @@ def qubits(self) -> list[QubitDesignator]: return _convert_to_py_qubits(super().qubits) @qubits.setter # type: ignore[override] - def qubits(self, qubits: Sequence[Union[int, Qubit, FormalArgument]]) -> None: + def qubits(self, qubits: Sequence[int | Qubit | FormalArgument]) -> None: quil_rs.Delay.qubits.__set__(self, _convert_to_rs_qubits(qubits)) # type: ignore @property @@ -2645,7 +2636,7 @@ def _from_rs_delay(cls, delay: quil_rs.Delay) -> "DelayFrames": class DelayQubits(Delay): """Initialize a new DELAY instruction that operates on qubits.""" - def __new__(cls, qubits: Sequence[Union[Qubit, FormalArgument]], duration: float) -> Self: + def __new__(cls, qubits: Sequence[Qubit | FormalArgument], duration: float) -> Self: """Initialize a new DELAY instruction that operates on qubits.""" return super().__new__(cls, [], qubits, duration) @@ -2658,7 +2649,7 @@ def _from_rs_delay(cls, delay: quil_rs.Delay) -> "DelayQubits": class Fence(quil_rs.Fence, AbstractInstruction): """The FENCE instruction.""" - def __new__(cls, qubits: list[Union[Qubit, FormalArgument]]) -> Self: + def __new__(cls, qubits: list[Qubit | FormalArgument]) -> Self: """Initialize a new FENCE instruction.""" return super().__new__(cls, _convert_to_rs_qubits(qubits)) @@ -2682,7 +2673,7 @@ def qubits(self) -> list[QubitDesignator]: return _convert_to_py_qubits(super().qubits) @qubits.setter # type: ignore[override] - def qubits(self, qubits: list[Union[Qubit, FormalArgument]]) -> None: + def qubits(self, qubits: list[Qubit | FormalArgument]) -> None: quil_rs.Fence.qubits.__set__(self, _convert_to_rs_qubits(qubits)) # type: ignore[attr-defined] def __copy__(self) -> Self: @@ -2708,7 +2699,7 @@ def __new__( cls, name: str, parameters: list[Parameter], - entries: list[Union[complex, Expression]], + entries: list[complex | Expression], ) -> Self: """Initialize a new waveform definition.""" rs_waveform = DefWaveform._build_rs_waveform(parameters, entries) @@ -2719,7 +2710,7 @@ def _from_rs_waveform_definition(cls, waveform_definition: quil_rs.WaveformDefin return super().__new__(cls, waveform_definition.name, waveform_definition.definition) @staticmethod - def _build_rs_waveform(parameters: list[Parameter], entries: list[Union[complex, Expression]]) -> quil_rs.Waveform: + def _build_rs_waveform(parameters: list[Parameter], entries: list[complex | Expression]) -> quil_rs.Waveform: rs_parameters = [parameter.name for parameter in parameters] return quil_rs.Waveform(_convert_to_rs_expressions(entries), rs_parameters) @@ -2747,7 +2738,7 @@ def entries(self) -> Sequence[ParameterDesignator]: return _convert_to_py_expressions(super().definition.matrix) @entries.setter - def entries(self, entries: list[Union[complex, Expression]]) -> None: + def entries(self, entries: list[complex | Expression]) -> None: waveform = super().definition waveform.matrix = _convert_to_rs_expressions(entries) quil_rs.WaveformDefinition.definition.__set__(self, waveform) # type: ignore[attr-defined] @@ -2838,9 +2829,9 @@ def __new__( cls, name: str, parameters: Sequence[ParameterDesignator], - qubits: Sequence[Union[Qubit, FormalArgument]], + qubits: Sequence[Qubit | FormalArgument], instrs: Sequence[AbstractInstruction], - modifiers: Optional[list[quil_rs.GateModifier]] = None, + modifiers: list[quil_rs.GateModifier] | None = None, ) -> Self: """Initialize a new calibration definition.""" return super().__new__( @@ -2929,7 +2920,7 @@ class DefMeasureCalibration(quil_rs.MeasureCalibrationDefinition, AbstractInstru def __new__( cls, - qubit: Optional[Union[Qubit, FormalArgument]], + qubit: Qubit | FormalArgument | None, memory_reference: MemoryReference, instrs: list[AbstractInstruction], ) -> Self: @@ -2948,7 +2939,7 @@ def _from_rs_measure_calibration_definition( return super().__new__(cls, calibration.identifier, calibration.instructions) @property # type: ignore[override] - def qubit(self) -> Optional[QubitDesignator]: + def qubit(self) -> QubitDesignator | None: """Get the qubit this calibration matches.""" qubit = super().identifier.qubit if not qubit: @@ -2962,7 +2953,7 @@ def qubit(self, qubit: QubitDesignator) -> None: quil_rs.MeasureCalibrationDefinition.identifier.__set__(self, identifier) # type: ignore[attr-defined] # noqa @property - def memory_reference(self) -> Optional[MemoryReference]: + def memory_reference(self) -> MemoryReference | None: """Get the memory reference this calibration writes to.""" return MemoryReference._from_parameter_str(super().identifier.parameter) @@ -3011,13 +3002,13 @@ class DefFrame(quil_rs.FrameDefinition, AbstractInstruction): def __new__( cls, frame: Frame, - direction: Optional[str] = None, - initial_frequency: Optional[float] = None, - hardware_object: Optional[str] = None, - sample_rate: Optional[float] = None, - center_frequency: Optional[float] = None, - enable_raw_capture: Optional[str] = None, - channel_delay: Optional[float] = None, + direction: str | None = None, + initial_frequency: float | None = None, + hardware_object: str | None = None, + sample_rate: float | None = None, + center_frequency: float | None = None, + enable_raw_capture: str | None = None, + channel_delay: float | None = None, ) -> Self: """Get the frame definition.""" attributes = { @@ -3041,6 +3032,7 @@ def __new__( enable_raw_capture, channel_delay, ], + strict=False, ) if value is not None } @@ -3057,7 +3049,7 @@ def _from_rs_attribute_values( return super().__new__(cls, frame, attributes) @staticmethod - def _to_attribute_value(value: Union[str, float]) -> quil_rs.AttributeValue: + def _to_attribute_value(value: str | float) -> quil_rs.AttributeValue: if isinstance(value, str): return quil_rs.AttributeValue.from_string(value) if isinstance(value, (int, float, complex)): @@ -3080,13 +3072,13 @@ def frame(self) -> Frame: def frame(self, frame: Frame) -> None: quil_rs.FrameDefinition.identifier.__set__(self, frame) # type: ignore[attr-defined] - def set_attribute(self, name: str, value: Union[str, float]) -> None: + def set_attribute(self, name: str, value: str | float) -> None: """Set an attribute on the frame definition.""" updated = super().attributes updated.update({name: DefFrame._to_attribute_value(value)}) quil_rs.FrameDefinition.attributes.__set__(self, updated) # type: ignore[attr-defined] - def get_attribute(self, name: str) -> Optional[Union[str, float]]: + def get_attribute(self, name: str) -> str | float | None: """Get an attribute's value on the frame definition.""" value = super().attributes.get(name, None) if value is None: @@ -3095,7 +3087,7 @@ def get_attribute(self, name: str) -> Optional[Union[str, float]]: return value.to_string() return value.to_expression().to_number().real - def __getitem__(self, name: str) -> Union[str, float]: + def __getitem__(self, name: str) -> str | float: if not isinstance(name, str): raise TypeError("Frame attribute keys must be strings") value = self.get_attribute(name) @@ -3103,7 +3095,7 @@ def __getitem__(self, name: str) -> Union[str, float]: raise AttributeError(f"Attribute {name} not found") return value - def __setitem__(self, name: str, value: Union[str, float]) -> None: + def __setitem__(self, name: str, value: str | float) -> None: if not isinstance(name, str): raise TypeError("Frame attribute keys must be strings") self.set_attribute(name, value) @@ -3113,7 +3105,7 @@ def __setitem__(self, name: str, value: Union[str, float]) -> None: version="4.0", reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('DIRECTION') instead.", ) - def direction(self) -> Optional[str]: + def direction(self) -> str | None: """Get the DIRECTION attribute of the frame.""" return self.get_attribute("DIRECTION") # type: ignore @@ -3130,7 +3122,7 @@ def direction(self, direction: str) -> None: version="4.0", reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use set_attribute('INITIAL-FREQUENCY') instead.", # noqa: E501 ) - def initial_frequency(self) -> Optional[float]: + def initial_frequency(self) -> float | None: """Get the INITIAL-FREQUENCY attribute of the frame.""" return self.get_attribute("INITIAL-FREQUENCY") # type: ignore @@ -3147,7 +3139,7 @@ def initial_frequency(self, initial_frequency: float) -> None: version="4.0", reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('HARDWARE-OBJECT') instead.", ) - def hardware_object(self) -> Optional[str]: + def hardware_object(self) -> str | None: """Get the HARDWARE-OBJECT attribute of the frame.""" return self.get_attribute("HARDWARE-OBJECT") # type: ignore diff --git a/pyquil/quiltcalibrations.py b/pyquil/quiltcalibrations.py index 2614bb673..e35bd7eb2 100644 --- a/pyquil/quiltcalibrations.py +++ b/pyquil/quiltcalibrations.py @@ -2,7 +2,7 @@ from collections.abc import Sequence from dataclasses import dataclass -from typing import Any, Optional, Union +from typing import Any import quil.expression as quil_expr import quil.instructions as quil_rs @@ -39,20 +39,20 @@ class CalibrationDoesntMatch(CalibrationError): class CalibrationMatch: """A match between a calibration definition and an instruction.""" - cal: Union[DefCalibration, DefMeasureCalibration] - settings: dict[Union[QubitDesignator, ExpressionDesignator], Any] + cal: DefCalibration | DefMeasureCalibration + settings: dict[QubitDesignator | ExpressionDesignator, Any] def _convert_to_calibration_match( - instruction: Union[quil_rs.Gate, quil_rs.Measurement], - calibration: Optional[Union[quil_rs.Calibration, quil_rs.MeasureCalibrationDefinition]], -) -> Optional[CalibrationMatch]: + instruction: quil_rs.Gate | quil_rs.Measurement, + calibration: quil_rs.Calibration | quil_rs.MeasureCalibrationDefinition | None, +) -> CalibrationMatch | None: if isinstance(instruction, quil_rs.Gate) and isinstance(calibration, quil_rs.Calibration): target_qubits = instruction.qubits - target_values: Sequence[Union[quil_expr.Expression, MemoryReference]] = instruction.parameters + target_values: Sequence[quil_expr.Expression | MemoryReference] = instruction.parameters parameter_qubits = calibration.identifier.qubits - parameter_values: Sequence[Union[quil_expr.Expression, MemoryReference]] = calibration.identifier.parameters - py_calibration: Union[DefCalibration, DefMeasureCalibration] = DefCalibration._from_rs_calibration(calibration) + parameter_values: Sequence[quil_expr.Expression | MemoryReference] = calibration.identifier.parameters + py_calibration: DefCalibration | DefMeasureCalibration = DefCalibration._from_rs_calibration(calibration) elif isinstance(instruction, quil_rs.Measurement) and isinstance(calibration, quil_rs.MeasureCalibrationDefinition): target_qubits = [instruction.qubit] target_values = ( @@ -65,15 +65,15 @@ def _convert_to_calibration_match( else: return None - settings: dict[Union[QubitDesignator, ExpressionDesignator], Union[QubitDesignator, ExpressionDesignator]] = { + settings: dict[QubitDesignator | ExpressionDesignator, QubitDesignator | ExpressionDesignator] = { _convert_to_py_qubit(param): _convert_to_py_qubit(qubit) - for param, qubit in zip(parameter_qubits, target_qubits) + for param, qubit in zip(parameter_qubits, target_qubits, strict=False) if isinstance(param, MemoryReference) or param.is_variable() } settings.update( { _convert_to_py_expression(param): _convert_to_py_expression(value) - for param, value in zip(parameter_values, target_values) + for param, value in zip(parameter_values, target_values, strict=False) if isinstance(param, MemoryReference) or param.is_variable() } ) @@ -82,8 +82,8 @@ def _convert_to_calibration_match( def match_calibration( - instr: AbstractInstruction, cal: Union[DefCalibration, DefMeasureCalibration] -) -> Optional[CalibrationMatch]: + instr: AbstractInstruction, cal: DefCalibration | DefMeasureCalibration +) -> CalibrationMatch | None: """Match a calibration definition to an instruction. On a successful match, return a (possibly empty) dictionary mapping calibration @@ -97,7 +97,7 @@ def match_calibration( instruction = _convert_to_rs_instruction(instr) gate = instruction.to_gate() calibration_set = CalibrationSet([calibration.to_calibration_definition()], []) - matched_calibration: Optional[Union[quil_rs.Calibration, quil_rs.MeasureCalibrationDefinition]] = ( + matched_calibration: quil_rs.Calibration | quil_rs.MeasureCalibrationDefinition | None = ( calibration_set.get_match_for_gate(gate) ) return _convert_to_calibration_match(gate, matched_calibration) diff --git a/pyquil/quiltwaveforms.py b/pyquil/quiltwaveforms.py index 834265032..06a522e8b 100644 --- a/pyquil/quiltwaveforms.py +++ b/pyquil/quiltwaveforms.py @@ -1,10 +1,9 @@ """Waveform templates that are commonly useful when working with pulse programs.""" -from typing import Optional +from typing import Self import numpy as np from scipy.special import erf -from typing_extensions import Self from pyquil.quilatom import ( TemplateWaveform, @@ -22,9 +21,9 @@ def __new__( cls, duration: float, iq: complex, - scale: Optional[float] = None, - phase: Optional[float] = None, - detuning: Optional[float] = None, + scale: float | None = None, + phase: float | None = None, + detuning: float | None = None, ) -> Self: """Initialize a new FlatWaveform.""" return super().__new__(cls, cls.NAME, duration=duration, iq=iq, scale=scale, phase=phase, detuning=detuning) @@ -50,9 +49,9 @@ def __new__( duration: float, fwhm: float, t0: float, - scale: Optional[float] = None, - phase: Optional[float] = None, - detuning: Optional[float] = None, + scale: float | None = None, + phase: float | None = None, + detuning: float | None = None, ) -> Self: """Initialize a new GaussianWaveform.""" return super().__new__( @@ -89,9 +88,9 @@ def __new__( t0: float, anh: float, alpha: float, - scale: Optional[float] = None, - phase: Optional[float] = None, - detuning: Optional[float] = None, + scale: float | None = None, + phase: float | None = None, + detuning: float | None = None, ) -> Self: """Initialize a new DragGaussianWaveform.""" return super().__new__( @@ -149,9 +148,9 @@ def __new__( anh: float, alpha: float, second_order_hrm_coeff: float, - scale: Optional[float] = None, - phase: Optional[float] = None, - detuning: Optional[float] = None, + scale: float | None = None, + phase: float | None = None, + detuning: float | None = None, ) -> Self: """Initialize a new HrmGaussianWaveform.""" return super().__new__( @@ -216,9 +215,9 @@ def __new__( risetime: float, pad_left: float, pad_right: float, - scale: Optional[float] = None, - phase: Optional[float] = None, - detuning: Optional[float] = None, + scale: float | None = None, + phase: float | None = None, + detuning: float | None = None, ) -> Self: """Initialize a new ErfSquareWaveform.""" return super().__new__( @@ -273,9 +272,9 @@ class BoxcarAveragerKernel(TemplateWaveform): def __new__( cls, duration: float, - scale: Optional[float] = None, - phase: Optional[float] = None, - detuning: Optional[float] = None, + scale: float | None = None, + phase: float | None = None, + detuning: float | None = None, ) -> Self: """Initialize a new BoxcarAveragerKernel.""" return super().__new__(cls, cls.NAME, duration=duration, scale=scale, phase=phase, detuning=detuning) diff --git a/pyquil/simulation/_numpy.py b/pyquil/simulation/_numpy.py index 0bba502eb..aa602899c 100644 --- a/pyquil/simulation/_numpy.py +++ b/pyquil/simulation/_numpy.py @@ -14,7 +14,7 @@ # limitations under the License. ############################################################################## from collections.abc import Sequence -from typing import Any, Optional, Union, cast +from typing import Any, cast import numpy as np from numpy.random.mtrand import RandomState @@ -75,7 +75,7 @@ def targeted_einsum(gate: np.ndarray, wf: np.ndarray, wf_target_inds: list[int]) used_data_indices = tuple(data_indices[q] for q in wf_target_inds) input_indices = work_indices + used_data_indices output_indices = list(data_indices) - for w, t in zip(work_indices, wf_target_inds): + for w, t in zip(work_indices, wf_target_inds, strict=False): output_indices[t] = w # TODO: `out` does not work if input matrices share memory with outputs, as is usually @@ -122,7 +122,7 @@ def targeted_tensordot(gate: np.ndarray, wf: np.ndarray, wf_target_inds: Sequenc where_td_put_them = where_td_put_them[sorty] sorted_targets = np.asarray(wf_target_inds)[sorty] # now that everything is sorted, we can do the insertion. - for target_ind, from_ind in zip(sorted_targets, where_td_put_them): + for target_ind, from_ind in zip(sorted_targets, where_td_put_them, strict=False): axes_ordering.insert(target_ind, from_ind) # A quick call to transpose gives us the right thing. @@ -177,7 +177,7 @@ def _term_expectation(wf: np.ndarray, term: PauliTerm) -> Any: class NumpyWavefunctionSimulator(AbstractQuantumSimulator): - def __init__(self, n_qubits: int, rs: Optional[RandomState] = None): + def __init__(self, n_qubits: int, rs: RandomState | None = None): """Initialize a wavefunction simulator that uses numpy's tensordot or einsum to update a state vector. Please consider using @@ -276,7 +276,7 @@ def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "NumpyWav self.wf = targeted_tensordot(gate=tensor, wf=self.wf, wf_target_inds=qubits) return self - def expectation(self, operator: Union[PauliTerm, PauliSum]) -> float: + def expectation(self, operator: PauliTerm | PauliSum) -> float: """Compute the expectation of an operator. :param operator: The operator diff --git a/pyquil/simulation/_reference.py b/pyquil/simulation/_reference.py index bb68073cd..573d03d22 100644 --- a/pyquil/simulation/_reference.py +++ b/pyquil/simulation/_reference.py @@ -15,7 +15,7 @@ ############################################################################## import warnings from collections.abc import Sequence -from typing import Any, Optional, Union +from typing import Any import numpy as np from numpy.random.mtrand import RandomState @@ -70,7 +70,7 @@ def _is_valid_quantum_state(state_matrix: np.ndarray, rtol: float = 1e-05, atol: class ReferenceWavefunctionSimulator(AbstractQuantumSimulator): - def __init__(self, n_qubits: int, rs: Optional[RandomState] = None): + def __init__(self, n_qubits: int, rs: RandomState | None = None): """Initialize a wavefunction simulator that prioritizes readability over performance. Please consider using @@ -160,7 +160,7 @@ def do_measurement(self, qubit: int) -> int: self.wf = unitary.dot(self.wf) return 1 - def expectation(self, operator: Union[PauliTerm, PauliSum]) -> float: + def expectation(self, operator: PauliTerm | PauliSum) -> float: """Compute the expectation of an operator. :param operator: The operator @@ -210,7 +210,7 @@ class ReferenceDensitySimulator(AbstractQuantumSimulator): doing anything stochastic. A value of ``None`` disallows doing anything stochastic. """ - def __init__(self, n_qubits: int, rs: Optional[RandomState] = None): + def __init__(self, n_qubits: int, rs: RandomState | None = None): self.n_qubits = n_qubits self.rs = rs self.density: np.ndarray @@ -320,7 +320,7 @@ def do_measurement(self, qubit: int) -> int: self.density = unitary.dot(self.density).dot(np.conj(unitary.T)) return 1 - def expectation(self, operator: Union[PauliTerm, PauliSum]) -> complex: + def expectation(self, operator: PauliTerm | PauliSum) -> complex: raise NotImplementedError("To implement") def reset(self) -> "AbstractQuantumSimulator": diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index d96902df6..0103837dc 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -30,7 +30,8 @@ from __future__ import annotations import logging -from typing import Any, Callable, Union, cast +from collections.abc import Callable +from typing import Any, Union, cast import jax.numpy as jnp import networkx as nx @@ -60,13 +61,13 @@ # ────────────────────────────────────────────────────────── # Resolved operations retain the most specific native quax type. -ResolvedOp = tuple[Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument], tuple[int, ...]] +ResolvedOp = tuple[qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument, tuple[int, ...]] RecipeOp = Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument] RecipeCallable = Callable[[Array], RecipeOp] -Recipe = tuple[Union[RecipeOp, RecipeCallable], tuple[int, ...]] +Recipe = tuple[RecipeOp | RecipeCallable, tuple[int, ...]] # Trajectory operations for the state-vector simulator. -TrajectoryOp = tuple[Union[qx.Unitary, qx.KrausMap, qx.QuantumInstrument], tuple[int, ...]] +TrajectoryOp = tuple[qx.Unitary | qx.KrausMap | qx.QuantumInstrument, tuple[int, ...]] # Density-matrix operations. DensityMatrixOp = tuple[qx.SuperOp, tuple[int, ...]] @@ -181,66 +182,66 @@ def _measure_registers(program: Program) -> set[str]: return regs -def resolver_from_program( +def _collect_circuit_definitions(program: Program) -> dict[str, DefCircuit]: + """Extract DEFCIRCUIT definitions from a program. + + :param program: Quil program. + :return: Mapping from circuit name to definition. + """ + circuit_definitions: dict[str, DefCircuit] = {} + for inst in program.instructions: + if isinstance(inst, DefCircuit): + circuit_definitions[inst.name] = inst + return circuit_definitions + + +# ── Instruction expansion result ── + + +class _ExpandedProgram: + """Result of :func:`_expand_instructions`. + + Bundles the expanded instruction/channel lists with the dependency DAG + and topological node order so they can be passed to downstream phases + without a long argument list. + """ + + __slots__ = ("insts", "channels", "dag", "node_order") + + def __init__(self) -> None: + self.insts: list[Gate | Measurement | ResetQubit | Reset] = [] + self.channels: list[Channel | MeasurementChannel | None] = [] + self.dag: nx.DiGraph = nx.DiGraph() + self.node_order: list[int] = [] + + +def _expand_instructions( program: Program, noise_model: NoiseModelLike | None, qubit_indices: dict[int, int], - custom_gates: CustomGateMap | None, -) -> tuple[Resolver, nx.DiGraph, list[int]]: - """Build a :class:`Resolver`, DAG, and node order from a program. + circuit_definitions: dict[str, DefCircuit], +) -> _ExpandedProgram: + """Expand a program's instructions, building a dependency DAG. - The resolver accepts a flat parameter vector and produces one - ``(operator, subsystem)`` pair per DAG node, in ``node_order``. - - DEFCIRCUIT expansion is handled internally: + DEFCIRCUIT invocations are expanded: * If a cycle invocation matches a :class:`CycleChannel` in the noise model, the cycle is expanded using the channel's constituent operators. * Otherwise the DEFCIRCUIT body is expanded via qubit/param substitution and each resulting instruction is resolved individually. - The DAG is built simultaneously during instruction iteration. - - Operators are returned in their most specific native type: - - * Ideal gates → ``qx.Unitary`` - * Noisy gates (``Channel``) → ``qx.SuperOp`` - * Expanded cycle gates with ``CycleChannel`` noise → constituent ``qx.SuperOp`` - * Measurements → ``qx.QuantumInstrument`` - * Noisy resets (``ResetChannel``) → ``qx.SuperOp`` - * Ideal resets → ``qx.SuperOp`` - - No type conversion (``to_kraus``, ``to_superop``) is performed here; - that is the adapter's responsibility. - - :param program: Quil program (may contain DEFCIRCUITs). + :param program: Quil program. :param noise_model: Optional noise model. :param qubit_indices: Mapping from physical qubit id → 0-based index. - :param custom_gates: Custom gate definitions. - :return: Tuple of ``(Resolver, dag, node_order)``. + :param circuit_definitions: DEFCIRCUIT definitions from the program. + :return: An :class:`_ExpandedProgram` containing instructions, channels, DAG, and node order. """ - measure_regs = _measure_registers(program) - - # Extract DEFCIRCUIT definitions. - circuit_definitions: dict[str, DefCircuit] = {} - for inst in program.instructions: - if isinstance(inst, DefCircuit): - circuit_definitions[inst.name] = inst - - # ── Expand instructions, building DAG and recipes in one pass ── - - dag: nx.DiGraph = nx.DiGraph() - node_order: list[int] = [] - last_on_qubit: dict[int, int] = {} # qubit_index → last node key - - # Flat lists populated during instruction iteration. - expanded_insts: list[Gate | Measurement | ResetQubit | Reset] = [] - expanded_channels: list[Channel | MeasurementChannel | None] = [] + ep = _ExpandedProgram() + last_on_qubit: dict[int, int] = {} def _emit( inst: Gate | Measurement | ResetQubit | Reset, channel: Channel | MeasurementChannel | None = None ) -> None: - """Emit an instruction: add a DAG node and record the channel.""" if isinstance(inst, Gate): qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) elif isinstance(inst, Measurement): @@ -249,18 +250,17 @@ def _emit( qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) # type: ignore[union-attr] else: # Reset qubits = tuple(sorted(qubit_indices.values())) - node_key = len(expanded_insts) - dag.add_node(node_key, inst=inst, qubits=qubits) - node_order.append(node_key) + node_key = len(ep.insts) + ep.dag.add_node(node_key, inst=inst, qubits=qubits) + ep.node_order.append(node_key) for q in qubits: if q in last_on_qubit: - dag.add_edge(last_on_qubit[q], node_key) + ep.dag.add_edge(last_on_qubit[q], node_key) last_on_qubit[q] = node_key - expanded_insts.append(inst) - expanded_channels.append(channel) + ep.insts.append(inst) + ep.channels.append(channel) def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: - """Look up noise channel for an instruction and emit it.""" if isinstance(inst, Gate): ch = noise_model.get_channel(inst) if noise_model is not None else None if isinstance(ch, CycleChannel): @@ -272,34 +272,41 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: else: _emit(inst) - # ── Main instruction loop ── - for inst in program.instructions: if isinstance(inst, DefCircuit): continue if isinstance(inst, Gate) and inst.name in circuit_definitions: - # DEFCIRCUIT invocation — check for CycleChannel. channel = noise_model.get_channel(inst) if noise_model is not None else None if isinstance(channel, CycleChannel): - # Expand using constituent channels. for sub_ch in channel.channels: _emit(sub_ch.inst, sub_ch) else: - # No CycleChannel — expand the DEFCIRCUIT body and resolve individually. for expanded_inst in expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions): _lookup_and_emit(expanded_inst) elif isinstance(inst, (Gate, Measurement, ResetQubit, Reset)): _lookup_and_emit(inst) - # ── Build recipes from expanded instructions ── + return ep + - # Assign parameter vector indices to each gate's MemoryReference params. +def _assign_param_indices( + ep: _ExpandedProgram, + measure_regs: set[str], +) -> tuple[dict[int, list[int]], int]: + """Assign parameter-vector indices to each gate's ``MemoryReference`` parameters. + + :param ep: Expanded program. + :param measure_regs: Register names that are targets of MEASURE (excluded from params). + :return: Tuple of ``(gate_param_indices, total_param_count)``. + ``gate_param_indices`` maps node key → list of param-vector indices + (``-1`` for literal/non-parameter slots). + """ param_counter = 0 gate_param_indices: dict[int, list[int]] = {} - for idx in node_order: - inst = expanded_insts[idx] + for idx in ep.node_order: + inst = ep.insts[idx] if isinstance(inst, Gate): indices = [] for param in inst.params: @@ -309,39 +316,99 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: else: indices.append(-1) gate_param_indices[idx] = indices + return gate_param_indices, param_counter + + +def _infer_dims_from_instructions( + ep: _ExpandedProgram, + noise_model: NoiseModelLike | None, + custom_gates: CustomGateMap | None, +) -> dict[int, int]: + """Infer per-qudit dimensions from the expanded instruction list. + + Scans all expanded instructions — gates, measurements, and resets — and + their associated channels/unitaries to determine the Hilbert-space + dimension of each qubit slot (defaulting to 2 for standard qubits). + + :param ep: Expanded program. + :param noise_model: Optional noise model. + :param custom_gates: Custom gate definitions. + :return: Mapping from qubit index → dimension (only entries > 2 are guaranteed present). + """ + qudit_dims: dict[int, int] = {} + + def _update(subsystem: tuple[int, ...], op_dims: tuple[int, ...]) -> None: + for slot, dim in zip(subsystem, op_dims, strict=False): + if dim > qudit_dims.get(slot, 2): + qudit_dims[slot] = dim + + for node_key in ep.node_order: + inst = ep.insts[node_key] + subsystem = ep.dag.nodes[node_key]["qubits"] + channel: Channel | MeasurementChannel | ResetChannel | CycleChannel | None = ep.channels[node_key] - # Pre-scan gate instructions to infer per-qudit dimensions. - qudit_dims: dict[int, int] = {} # qubit_index → dimension - for node_key in node_order: - inst = expanded_insts[node_key] if isinstance(inst, Gate): - subsystem = dag.nodes[node_key]["qubits"] - channel = expanded_channels[node_key] # type: ignore[assignment] if channel is None and noise_model is not None: channel = noise_model.get_channel(inst) - if channel is not None and isinstance(channel, Channel): - op_dims = channel.process.dims[0] - elif channel is not None and isinstance(channel, CycleChannel): + if isinstance(channel, Channel): + _update(subsystem, channel.process.dims[0]) + elif isinstance(channel, CycleChannel): continue else: try: unitary = get_instruction_unitary(inst, custom_gates=custom_gates) - op_dims = unitary.dims[0] + _update(subsystem, unitary.dims[0]) except Exception: # noqa: S112 continue - for slot, dim in zip(subsystem, op_dims): - if dim > qudit_dims.get(slot, 2): - qudit_dims[slot] = dim + elif isinstance(inst, Measurement): + if channel is None and noise_model is not None: + channel = noise_model.get_channel(inst) + if isinstance(channel, MeasurementChannel): + _update(subsystem, channel.process.dims[0]) + + elif isinstance(inst, ResetQubit): + reset_ch = None + if noise_model is not None: + reset_ch = noise_model.get_channel(inst) + if isinstance(reset_ch, ResetChannel): + _update(subsystem, reset_ch.process.dims[0]) + + return qudit_dims + + +def _build_recipes( + ep: _ExpandedProgram, + noise_model: NoiseModelLike | None, + qubit_indices: dict[int, int], + custom_gates: CustomGateMap | None, + gate_param_indices: dict[int, list[int]], + measure_regs: set[str], + qudit_dims: dict[int, int], +) -> list[Recipe]: + """Convert expanded instructions into operator recipes. + + Each recipe is either a concrete quax operator or a callable that + accepts a flat parameter vector and returns an operator. + + :param ep: Expanded program. + :param noise_model: Optional noise model. + :param qubit_indices: Mapping from physical qubit id → 0-based index. + :param custom_gates: Custom gate definitions. + :param gate_param_indices: Parameter-vector indices per gate node. + :param measure_regs: Register names that are targets of MEASURE. + :param qudit_dims: Per-qubit dimensions from :func:`_infer_dims_from_instructions`. + :return: Ordered list of ``(operator_or_callable, subsystem)`` recipes. + """ recipes: list[Recipe] = [] - for node_key in node_order: - inst = expanded_insts[node_key] - subsystem = dag.nodes[node_key]["qubits"] + for node_key in ep.node_order: + inst = ep.insts[node_key] + subsystem = ep.dag.nodes[node_key]["qubits"] match inst: case Gate(): - channel2: Channel | MeasurementChannel | CycleChannel | None = expanded_channels[node_key] + channel2: Channel | MeasurementChannel | CycleChannel | None = ep.channels[node_key] if channel2 is None and noise_model is not None: channel2 = noise_model.get_channel(inst) @@ -369,7 +436,7 @@ def _make_param_recipe( ) -> Callable[[Array], qx.Unitary]: def recipe(params: Array) -> qx.Unitary: resolved: list[Any] = [] - for p, pv in zip(cp, pi): + for p, pv in zip(cp, pi, strict=False): if pv >= 0: resolved.append(params[pv]) else: @@ -389,7 +456,7 @@ def recipe(params: Array) -> qx.Unitary: recipes.append((unitary, subsystem)) case Measurement(): - meas_channel = expanded_channels[node_key] + meas_channel = ep.channels[node_key] if meas_channel is None and noise_model is not None: meas_channel = noise_model.get_channel(inst) if meas_channel is not None and isinstance(meas_channel, MeasurementChannel): @@ -415,6 +482,54 @@ def recipe(params: Array) -> qx.Unitary: dim = qudit_dims.get(q_idx, 2) recipes.append((qx.gates.RESET(dim=dim), (q_idx,))) + return recipes + + +def resolver_from_program( + program: Program, + noise_model: NoiseModelLike | None, + qubit_indices: dict[int, int], + custom_gates: CustomGateMap | None, +) -> tuple[Resolver, nx.DiGraph, list[int]]: + """Build a :class:`Resolver`, DAG, and node order from a program. + + The resolver accepts a flat parameter vector and produces one + ``(operator, subsystem)`` pair per DAG node, in ``node_order``. + + Operators are returned in their most specific native type: + + * Ideal gates → ``qx.Unitary`` + * Noisy gates (``Channel``) → ``qx.SuperOp`` + * Expanded cycle gates with ``CycleChannel`` noise → constituent ``qx.SuperOp`` + * Measurements → ``qx.QuantumInstrument`` + * Noisy resets (``ResetChannel``) → ``qx.SuperOp`` + * Ideal resets → ``qx.SuperOp`` + + No type conversion (``to_kraus``, ``to_superop``) is performed here; + that is the adapter's responsibility. + + :param program: Quil program (may contain DEFCIRCUITs). + :param noise_model: Optional noise model. + :param qubit_indices: Mapping from physical qubit id → 0-based index. + :param custom_gates: Custom gate definitions. + :return: Tuple of ``(Resolver, dag, node_order)``. + """ + measure_regs = _measure_registers(program) + circuit_defs = _collect_circuit_definitions(program) + + # Phase 1: Expand instructions and build the dependency DAG. + ep = _expand_instructions(program, noise_model, qubit_indices, circuit_defs) + + # Phase 2: Assign parameter-vector indices. + gate_param_indices, _ = _assign_param_indices(ep, measure_regs) + + # Phase 3: Infer per-qudit dimensions. + qudit_dims = _infer_dims_from_instructions(ep, noise_model, custom_gates) + + # Phase 4: Build operator recipes. + recipes = _build_recipes(ep, noise_model, qubit_indices, custom_gates, gate_param_indices, measure_regs, qudit_dims) + + # Phase 5: Build the resolve closure. def resolve(params: Array) -> list[ResolvedOp]: ops: list[ResolvedOp] = [] for op_or_fn, subsystem in recipes: @@ -424,11 +539,10 @@ def resolve(params: Array) -> list[ResolvedOp]: ops.append((op_or_fn(params), subsystem)) return ops - # Compute per-qudit dimensions from the pre-scan. n_qubits = len(qubit_indices) dims = tuple(qudit_dims.get(i, 2) for i in range(n_qubits)) - return Resolver(resolve, dims=dims), dag, node_order + return Resolver(resolve, dims=dims), ep.dag, ep.node_order # ══════════════════════════════════════════════════════════ @@ -537,7 +651,8 @@ def _merge_ops( accumulated = embedded if accumulated is None else embedded @ accumulated - assert accumulated is not None # noqa: S101 + if accumulated is None: + raise ValueError("Cannot merge an empty operation group.") return accumulated, merged_subsystem @@ -721,7 +836,7 @@ def infer_qudit_dims( op_dims = op.dims[0] if hasattr(op, "dims") else None if op_dims is None: continue - for slot, dim in zip(subsystem, op_dims): + for slot, dim in zip(subsystem, op_dims, strict=False): if dim > qudit_dims[slot]: qudit_dims[slot] = dim return tuple(qudit_dims) diff --git a/pyquil/simulation/tools.py b/pyquil/simulation/tools.py index 935be18ab..8418cb2a0 100644 --- a/pyquil/simulation/tools.py +++ b/pyquil/simulation/tools.py @@ -16,7 +16,7 @@ """Miscellaneous tools that are helpful for simulation.""" from collections.abc import Sequence -from typing import Union, cast +from typing import cast import numpy as np @@ -348,7 +348,7 @@ def program_unitary(program: Program, n_qubits: int) -> np.ndarray: return umat -def lifted_pauli(pauli_sum: Union[PauliSum, PauliTerm], qubits: list[int]) -> np.ndarray: +def lifted_pauli(pauli_sum: PauliSum | PauliTerm, qubits: list[int]) -> np.ndarray: """Return a matrix corresponding to the tensor representation of the given PauliSum and qubits. Useful for generating the full Hamiltonian after a particular fermion to @@ -389,7 +389,7 @@ def lifted_pauli(pauli_sum: Union[PauliSum, PauliTerm], qubits: list[int]) -> np return result_hilbert -def tensor_up(pauli_sum: Union[PauliSum, PauliTerm], qubits: list[int]) -> np.ndarray: +def tensor_up(pauli_sum: PauliSum | PauliTerm, qubits: list[int]) -> np.ndarray: """Return a matrix corresponding to the tensor representation of the given PauliSum and qubits. This is the same as :py:func:`lifted_pauli`. Nick R originally wrote this functionality diff --git a/pyquil/transform.py b/pyquil/transform.py index 8965a6a9f..8f03df5f8 100644 --- a/pyquil/transform.py +++ b/pyquil/transform.py @@ -118,8 +118,8 @@ def expand_defcircuit_body( :param circuit_definitions: All known DEFCIRCUIT definitions (for nested expansion). :yields: Concrete instructions with physical qubits and resolved parameters. """ - qarg_to_arg_map = {qarg: q for q, qarg in zip(inst.qubits, defcircuit.qubit_variables)} - parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters)} + qarg_to_arg_map = {qarg: q for q, qarg in zip(inst.qubits, defcircuit.qubit_variables, strict=False)} + parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters, strict=False)} for circuit_inst in defcircuit.instructions: if isinstance(circuit_inst, Gate): diff --git a/pyquil/wavefunction.py b/pyquil/wavefunction.py index 944409858..c53e645b2 100644 --- a/pyquil/wavefunction.py +++ b/pyquil/wavefunction.py @@ -17,7 +17,7 @@ import itertools from collections.abc import Iterator, Sequence -from typing import Optional, cast +from typing import cast import numpy as np @@ -143,7 +143,7 @@ def pretty_print(self, decimal_digits: int = 2) -> str: pp_string = pp_string[:-3] # remove the dangling + if it is there return pp_string - def plot(self, qubit_subset: Optional[Sequence[int]] = None) -> None: + def plot(self, qubit_subset: Sequence[int] | None = None) -> None: """Plot a bar chart with bitstring on the x-axis and probability on the y-axis. :param qubit_subset: Optional parameter used for plotting a subset of the Hilbert space. diff --git a/test/unit/test_legacy_noise.py b/test/unit/test_legacy_noise.py deleted file mode 100644 index 5a4861785..000000000 --- a/test/unit/test_legacy_noise.py +++ /dev/null @@ -1,371 +0,0 @@ -from collections import OrderedDict - -import numpy as np -import pytest -from pytest_mock import MockerFixture -from qcs_sdk import ExecutionData, RegisterData, ResultData -from qcs_sdk.qvm import QVMResultData - -from pyquil.api._qam import QAMExecutionResult -from pyquil.gates import CZ, RX, RZ, I -from pyquil.noise import ( - INFINITY, - NO_NOISE, - KrausModel, - NoiseModel, - _decoherence_noise_model, - _get_program_gates, - _noise_model_program_header, - add_decoherence_noise, - apply_noise_model, - bitstring_probs_to_z_moments, - combine_kraus_maps, - correct_bitstring_probs, - corrupt_bitstring_probs, - damping_after_dephasing, - damping_kraus_map, - dephasing_kraus_map, - estimate_assignment_probs, - estimate_bitstring_probs, - pauli_kraus_map, - tensor_kraus_maps, -) -from pyquil.quil import Pragma, Program -from pyquil.quilbase import DefGate, Gate - - -def test_pauli_kraus_map(): - probabilities = [0.1, 0.2, 0.3, 0.4] - k1, k2, k3, k4 = pauli_kraus_map(probabilities) - assert np.allclose(k1, np.sqrt(0.1) * np.eye(2), atol=1 * 10**-8) - assert np.allclose(k2, np.sqrt(0.2) * np.array([[0, 1.0], [1.0, 0]]), atol=1 * 10**-8) - assert np.allclose(k3, np.sqrt(0.3) * np.array([[0, -1.0j], [1.0j, 0]]), atol=1 * 10**-8) - assert np.allclose(k4, np.sqrt(0.4) * np.array([[1, 0], [0, -1]]), atol=1 * 10**-8) - - two_q_pauli_kmaps = pauli_kraus_map(np.kron(probabilities, list(reversed(probabilities)))) - q1_pauli_kmaps = [k1, k2, k3, k4] - q2_pauli_kmaps = pauli_kraus_map(list(reversed(probabilities))) - tensor_kmaps = tensor_kraus_maps(q1_pauli_kmaps, q2_pauli_kmaps) - assert np.allclose(two_q_pauli_kmaps, tensor_kmaps) - - -def test_damping_kraus_map(): - p = 0.05 - k1, k2 = damping_kraus_map(p=p) - assert k1[1, 1] == np.sqrt(1 - p) - assert k2[0, 1] == np.sqrt(p) - - -def test_dephasing_kraus_map(): - p = 0.05 - k1, k2 = dephasing_kraus_map(p=p) - np.testing.assert_allclose(np.diag(k1), [np.sqrt(1 - p)] * 2) - np.testing.assert_allclose(np.abs(np.diag(k2)), [np.sqrt(p)] * 2) - - -def test_tensor_kraus_maps(): - damping = damping_kraus_map() - k1, k2, k3, k4 = tensor_kraus_maps(damping, damping) - assert k1.shape == (4, 4) - assert k2.shape == (4, 4) - assert k3.shape == (4, 4) - assert k4.shape == (4, 4) - np.testing.assert_allclose(k1[-1, -1], 1 - 0.1) - - -def test_combine_kraus_maps(): - damping = damping_kraus_map() - dephasing = dephasing_kraus_map() - k1, k2, k3, k4 = combine_kraus_maps(damping, dephasing) - assert k1.shape == (2, 2) - assert k2.shape == (2, 2) - assert k3.shape == (2, 2) - assert k4.shape == (2, 2) - - -def test_damping_after_dephasing(): - gate_time = 1 - T1 = 10 - T2 = 3 - kraus_map = damping_after_dephasing(T1, T2, gate_time) - - # Density matrix for the |+> state - rho = 0.5 * np.ones((2, 2)) - - # See Eq. 7.144 of Nielsen and Chuang - target_rho = [ - [1 - 0.5 * np.exp(-gate_time / T1), 0.5 * np.exp(-gate_time / T2)], - [0.5 * np.exp(-gate_time / T2), 0.5 * np.exp(-gate_time / T1)], - ] - - noisy_rho = np.zeros((2, 2)) - for op in kraus_map: - noisy_rho += op @ rho @ (op.T.conj()) - - np.testing.assert_allclose(noisy_rho, target_rho) - - -def test_noise_helpers(): - gates = RX(np.pi / 2, 0), RX(-np.pi / 2, 1), I(1), CZ(0, 1) - prog = Program(*gates) - inferred_gates = [g.out() for g in _get_program_gates(prog)] - assert set(inferred_gates) == set([g.out() for g in gates]) - - -def test_decoherence_noise(): - prog = Program(RX(np.pi / 2, 0), CZ(0, 1), RZ(np.pi, 0)) - gates = _get_program_gates(prog) - m1 = _decoherence_noise_model(gates, T1=INFINITY, T2=INFINITY, ro_fidelity=1.0) - - # with no readout error, assignment_probs = identity matrix - assert np.allclose(m1.assignment_probs[0], np.eye(2)) - assert np.allclose(m1.assignment_probs[1], np.eye(2)) - for g in m1.gates: - # with infinite coherence time all kraus maps should only have a single, unitary kraus op - assert len(g.kraus_ops) == 1 - (k0,) = g.kraus_ops - # check unitarity - k0dk0 = k0.dot(k0.conjugate().transpose()) - assert np.allclose(k0dk0, np.eye(k0dk0.shape[0])) - - # verify that selective (by qubit) dephasing and readout infidelity is working - m2 = _decoherence_noise_model(gates, T1=INFINITY, T2={0: 30e-6}, ro_fidelity={0: 0.95, 1: 1.0}) - assert np.allclose(m2.assignment_probs[0], [[0.95, 0.05], [0.05, 0.95]]) - assert np.allclose(m2.assignment_probs[1], np.eye(2)) - for g in m2.gates: - if 0 in g.targets: - # single dephasing (no damping) channel on qc 0, no noise on qc1 -> 2 Kraus ops - assert len(g.kraus_ops) == 2 - else: - assert len(g.kraus_ops) == 1 - - # verify that combined T1 and T2 will lead to 4 outcome Kraus map. - m3 = _decoherence_noise_model(gates, T1={0: 30e-6}, T2={0: 30e-6}) - for g in m3.gates: - if 0 in g.targets: - # damping (implies dephasing) channel on qc 0, no noise on qc1 -> 4 Kraus ops - assert len(g.kraus_ops) == 4 - else: - assert len(g.kraus_ops) == 1 - - # verify that gate names are translated - new_prog = apply_noise_model(prog, m3) - - # check that headers have been embedded - headers = _noise_model_program_header(m3) - assert all( - (isinstance(i, Pragma) and i.command in ["ADD-KRAUS", "READOUT-POVM"]) or isinstance(i, DefGate) - for i in headers - ) - assert headers.out() in new_prog.out() - - # verify that high-level add_decoherence_noise reproduces new_prog - new_prog2 = add_decoherence_noise(prog, T1={0: 30e-6}, T2={0: 30e-6}) - assert new_prog == new_prog2 - - -def test_kraus_model_1(): - km = KrausModel("I", (5.0,), (0, 1), [np.array([[1 + 1j]])], 1.0) - d = km.to_dict() - assert d == OrderedDict( - [ - ("gate", km.gate), - ("params", km.params), - ("targets", (0, 1)), - ("kraus_ops", [[[[1.0]], [[1.0]]]]), - ("fidelity", 1.0), - ] - ) - assert KrausModel.from_dict(d) == km - - -@pytest.fixture -def kraus_model_I_dict(): - return { - "gate": "I", - "fidelity": 1.0, - "kraus_ops": [[[[1.0]], [[1.0]]]], - "targets": (0, 1), - "params": (5.0,), - } - - -def test_kraus_model_2(kraus_model_I_dict): - km = KrausModel.from_dict(kraus_model_I_dict) - assert km == KrausModel( - gate=kraus_model_I_dict["gate"], - params=kraus_model_I_dict["params"], - targets=kraus_model_I_dict["targets"], - kraus_ops=[KrausModel.unpack_kraus_matrix(kraus_op) for kraus_op in kraus_model_I_dict["kraus_ops"]], - fidelity=kraus_model_I_dict["fidelity"], - ) - d = km.to_dict() - assert d == OrderedDict( - [ - ("gate", km.gate), - ("params", km.params), - ("targets", (0, 1)), - ("kraus_ops", [[[[1.0]], [[1.0]]]]), - ("fidelity", 1.0), - ] - ) - - -def test_noise_model_1(): - km1 = KrausModel("I", (5.0,), (0, 1), [np.array([[1 + 1j]])], 1.0) - km2 = KrausModel("RX", (np.pi / 2,), (0,), [np.array([[1 + 1j]])], 1.0) - nm = NoiseModel([km1, km2], {0: np.eye(2), 1: np.eye(2)}) - - assert nm == NoiseModel.from_dict(nm.to_dict()) - assert nm.gates_by_name("I") == [km1] - assert nm.gates_by_name("RX") == [km2] - - -@pytest.fixture -def kraus_model_RX90_dict(): - return { - "gate": "RX", - "fidelity": 1.0, - "kraus_ops": [[[[1.0]], [[1.0]]]], - "targets": (0,), - "params": (np.pi / 2.0,), - } - - -def test_noise_model_2(kraus_model_I_dict, kraus_model_RX90_dict): - noise_model_dict = { - "gates": [kraus_model_I_dict, kraus_model_RX90_dict], - "assignment_probs": {"1": [[1.0, 0.0], [0.0, 1.0]], "0": [[1.0, 0.0], [0.0, 1.0]]}, - } - - nm = NoiseModel.from_dict(noise_model_dict) - km1 = KrausModel.from_dict(kraus_model_I_dict) - km2 = KrausModel.from_dict(kraus_model_RX90_dict) - assert nm == NoiseModel(gates=[km1, km2], assignment_probs={0: np.eye(2), 1: np.eye(2)}) - assert nm.gates_by_name("I") == [km1] - assert nm.gates_by_name("RX") == [km2] - assert nm.to_dict() == noise_model_dict - - -def test_readout_compensation(): - np.random.seed(1234124) - p = np.random.rand(2, 2, 2, 2, 2, 2) - p /= p.sum() - - aps = [np.eye(2) + 0.2 * (np.random.rand(2, 2) - 1) for _ in range(p.ndim)] - for ap in aps: - ap.flat[ap.flat < 0] = 0.0 - ap /= ap.sum() - assert (ap >= 0).all() - - assert (p >= 0).all() - - p_corrupted = corrupt_bitstring_probs(p, aps) - p_restored = correct_bitstring_probs(p_corrupted, aps) - assert np.allclose(p, p_restored) - - results = [[0, 0, 0]] * 100 + [[0, 1, 1]] * 200 - p1 = estimate_bitstring_probs(results) - assert np.isclose(p1[0, 0, 0], 1.0 / 3.0) - assert np.isclose(p1[0, 1, 1], 2.0 / 3.0) - assert np.isclose(p1.sum(), 1.0) - - zm = bitstring_probs_to_z_moments(p1) - assert np.isclose(zm[0, 0, 0], 1) - assert np.isclose(zm[1, 0, 0], 1) - assert np.isclose(zm[0, 1, 0], -1.0 / 3) - assert np.isclose(zm[0, 0, 1], -1.0 / 3) - assert np.isclose(zm[0, 1, 1], 1.0) - assert np.isclose(zm[1, 1, 0], -1.0 / 3) - assert np.isclose(zm[1, 0, 1], -1.0 / 3) - assert np.isclose(zm[1, 1, 1], 1.0) - - -def test_estimate_assignment_probs(mocker: MockerFixture): - mock_qc = mocker.patch("pyquil.api.QuantumComputer").return_value - mock_compiler = mocker.patch("pyquil.api._abstract_compiler.AbstractCompiler").return_value - - trials = 100 - p00 = 0.8 - p11 = 0.75 - - mock_compiler.native_quil_to_executable.return_value = Program() - mock_qc.compiler = mock_compiler - mock_qc - mock_qc.run.side_effect = [ - QAMExecutionResult( - executable=None, - data=ExecutionData( - result_data=ResultData( - QVMResultData.from_memory_map( - { - "ro": RegisterData.from_i16( - ( - np.array([[0]]) * int(round(p00 * trials)) - + np.array([[1]]) * int(round((1 - p00) * trials)) - ).tolist() - ) - } - ) - ) - ), - ), # I gate results - QAMExecutionResult( - executable=None, - data=ExecutionData( - result_data=ResultData( - QVMResultData.from_memory_map( - { - "ro": RegisterData.from_i16( - ( - np.array([[1]]) * int(round(p11 * trials)) - + np.array([[0]]) * int(round((1 - p11) * trials)) - ).tolist() - ) - } - ) - ) - ), - ), # X gate results - ] - ap_target = np.array([[p00, 1 - p11], [1 - p00, p11]]) - - povm_pragma = Pragma("READOUT-POVM", [0], "({} {} {} {})".format(*ap_target.flatten())) - ap = estimate_assignment_probs(0, trials, mock_qc, Program(povm_pragma)) - - assert mock_compiler.native_quil_to_executable.call_count == 2 - assert mock_qc.run.call_count == 2 - - for call in mock_compiler.native_quil_to_executable.call_args_list: - args, kwargs = call - prog = args[0] - assert povm_pragma in prog.instructions - - assert np.allclose(ap, ap_target) - - -def test_apply_noise_model(): - p = Program(RX(np.pi / 2, 0), RX(np.pi / 2, 1), CZ(0, 1), RX(np.pi / 2, 1)) - noise_model = _decoherence_noise_model(_get_program_gates(p)) - pnoisy = apply_noise_model(p, noise_model) - for i in pnoisy: - if isinstance(i, DefGate): - pass - elif isinstance(i, Pragma): - assert i.command in ["ADD-KRAUS", "READOUT-POVM"] - elif isinstance(i, Gate): - assert i.name in NO_NOISE or not i.params - - -def test_apply_noise_model_perturbed_angles(): - eps = 1e-15 - p = Program(RX(np.pi / 2 + eps, 0), RX(np.pi / 2 - eps, 1), CZ(0, 1), RX(np.pi / 2 + eps, 1)) - noise_model = _decoherence_noise_model(_get_program_gates(p)) - pnoisy = apply_noise_model(p, noise_model) - for i in pnoisy: - if isinstance(i, DefGate): - pass - elif isinstance(i, Pragma): - assert i.command in ["ADD-KRAUS", "READOUT-POVM"] - elif isinstance(i, Gate): - assert i.name in NO_NOISE or not i.params diff --git a/test/unit/test_noise.py b/test/unit/test_noise.py index 5a4861785..d3ab8255a 100644 --- a/test/unit/test_noise.py +++ b/test/unit/test_noise.py @@ -1,3 +1,7 @@ +# TODO(pyquil-5.0): Remove this file. These tests exercise the legacy KrausModel/NoiseModel +# API in pyquil.noise, which is superseded by pyquil.noise._channels / pyquil.noise._noise_model. +# Equivalent coverage lives in test_noise_model.py. + from collections import OrderedDict import numpy as np diff --git a/test/unit/test_noise_model.py b/test/unit/test_noise_model.py index b6767b833..212abd691 100644 --- a/test/unit/test_noise_model.py +++ b/test/unit/test_noise_model.py @@ -109,9 +109,7 @@ def test_is_pauli(self): def test_pauli_twirl(self): """Pauli twirl of a channel on a Clifford gate should be a Pauli channel.""" - ch = Channel.from_random_coherent_error( - inst=X(0), process_fidelity=0.97, rng=np.random.default_rng(42) - ) + ch = Channel.from_random_coherent_error(inst=X(0), process_fidelity=0.97, rng=np.random.default_rng(42)) twirled = ch.pauli_twirl() assert twirled.is_pauli() @@ -131,6 +129,36 @@ def test_perfect_channel(self): ch = Channel.from_depolarizing_constant(inst=RX(np.pi, 0), depolarizing_constant=1.0) assert ch.fidelity == pytest.approx(1.0, abs=1e-10) + def test_from_pauli_noise_rejects_invalid_probabilities(self): + """Pauli error rates must be probabilities with total error no greater than 1.""" + with pytest.raises(ValueError, match="negative"): + Channel.from_pauli_noise(inst=RX(0.5, 0), pauli_noise={"X": -0.1}) + + with pytest.raises(ValueError, match="at most 1.0"): + Channel.from_pauli_noise(inst=RX(0.5, 0), pauli_noise={"X": 0.6, "Z": 0.5}) + + def test_json_roundtrip_preserves_qutrit_dims(self): + """Channel JSON includes explicit dims for non-qubit operators.""" + qutrit_x = jnp.array( + [ + [0.0, 0.0, 1.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + ], + dtype=complex, + ) + target_unitary = qx.Unitary.from_matrix(qutrit_x, ((3,), (3,))) + channel = Channel( + inst=Gate("TX", [], [0]), process=qx.to_superop(target_unitary), target_unitary=target_unitary + ) + + restored = Channel.from_json(channel.to_json()) + + assert restored.inst == channel.inst + assert restored.process.dims == ((3,), (3,)) + assert restored.target_unitary.dims == ((3,), (3,)) + assert jnp.allclose(restored.process.matrix, channel.process.matrix) + # ────────────────────────────────────────────────────────── # MeasurementChannel tests @@ -140,7 +168,6 @@ def test_perfect_channel(self): class TestMeasurementChannel: def test_from_readout_fidelity(self): """MeasurementChannel.from_readout_fidelity produces a valid quantum instrument.""" - inst = Measurement(Gate("MEASURE", [], [0]).qubits[0], None) # Use the pyquil MEASURE gate to get qubit prog = Program(MEASURE(0, None)) meas_inst = [i for i in prog if isinstance(i, Measurement)][0] @@ -161,6 +188,19 @@ def test_qubits(self): ch = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.99) assert ch.qubits == [5] + def test_json_roundtrip_preserves_qutrit_dims(self): + """MeasurementChannel JSON includes explicit dims for non-qubit instruments.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + channel = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.95, dim=3) + + restored = MeasurementChannel.from_json(channel.to_json()) + + assert restored.inst == channel.inst + assert restored.process.dims == channel.process.dims + assert restored.process.measured_qudits == channel.process.measured_qudits + assert jnp.allclose(restored.process.matrix, channel.process.matrix) + # ────────────────────────────────────────────────────────── # NoiseModel tests @@ -308,3 +348,18 @@ def test_global_reset(self): rho = _dm(program) target_rho = qx.zero_state_matrix(2) assert qx.fidelity(rho, target_rho) > 0.9999 + + def test_global_reset_channel_rejected(self): + """ResetChannel is intentionally scoped to targeted resets.""" + with pytest.raises(TypeError, match="targeted"): + ResetChannel.from_reset_fidelity(inst=RESET(), fidelity=1.0) + + def test_json_roundtrip_preserves_qutrit_dims(self): + """ResetChannel JSON includes explicit dims for non-qubit resets.""" + channel = ResetChannel.from_reset_fidelity(inst=ResetQubit(0), fidelity=0.9, dim=3) + + restored = ResetChannel.from_json(channel.to_json()) + + assert restored.inst == channel.inst + assert restored.process.dims == ((3,), (3,)) + assert jnp.allclose(restored.process.matrix, channel.process.matrix) diff --git a/test/unit/test_reference_density.py b/test/unit/test_reference_density.py index 265f3d335..a85ca36cf 100644 --- a/test/unit/test_reference_density.py +++ b/test/unit/test_reference_density.py @@ -1,3 +1,7 @@ +# TODO(pyquil-5.0): Remove this file. These tests exercise ReferenceDensitySimulator/PyQVM, +# which are superseded by DensityMatrixSimulator. Equivalent coverage lives in +# test_state_vector.py and test_noise_model.py. + import networkx as nx import numpy as np import pytest diff --git a/test/unit/test_reference_wavefunction.py b/test/unit/test_reference_wavefunction.py index 938d95bc1..a77e45e6d 100644 --- a/test/unit/test_reference_wavefunction.py +++ b/test/unit/test_reference_wavefunction.py @@ -1,3 +1,7 @@ +# TODO(pyquil-5.0): Remove this file. These tests exercise ReferenceWavefunctionSimulator/PyQVM, +# which are superseded by PureStateVectorSimulator and TrajectorySimulator. +# Equivalent coverage lives in test_state_vector.py. + import functools import inspect import random From 6334f3f33e7fb9fa069d67fb9ea30be03493ff15 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 20:42:11 +0000 Subject: [PATCH 09/37] Update quax bound --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7e1ed4c24..66e1f05f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ pandoc = {version = "2.4b0", optional = true} matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} -rigetti-quax = ">=0.6.3" +rigetti-quax = ">=0.6.2" [tool.poetry.extras] latex = ["ipython"] From 490515b3007788d6f78bc8bc6180a43acffb9fcf Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Mon, 25 May 2026 21:09:36 +0000 Subject: [PATCH 10/37] Add sharding support --- poetry.lock | 2 +- pyquil/simulation/_simulator.py | 120 ++++++++++++++++++++++++++------ test/unit/test_state_vector.py | 98 ++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 24 deletions(-) diff --git a/poetry.lock b/poetry.lock index ac635f2f3..2517fa8b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4113,4 +4113,4 @@ latex = ["ipython"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.13" -content-hash = "7e3588a643858e0d8627af671c92bddb651ce31f8f29f97ffc4e2a4fbc37dada" +content-hash = "3ede6a989f05bee1b80907269ff0cd6a2a3e14707124c3b0ff561e3f95f8610f" diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 4cb170d38..4d29d878c 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -38,8 +38,10 @@ import jax import jax.numpy as jnp +import numpy as np import quax as qx from jax import Array +from jax.sharding import Mesh, NamedSharding, PartitionSpec from pyquil.api import MemoryMap from pyquil.noise._channels import get_custom_gates_from_program @@ -288,7 +290,7 @@ class TrajectorySimulator(ProgramSimulator): outcomes. """ - __slots__ = ("_kraus_truncation_threshold",) + __slots__ = ("_kraus_truncation_threshold", "_devices") def __init__( self, @@ -298,9 +300,11 @@ def __init__( noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 0, kraus_truncation_threshold: float = 1e-6, + devices: list[jax.Device] | None = None, ) -> None: super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) self._kraus_truncation_threshold = kraus_truncation_threshold + self._devices = devices if devices is not None else jax.devices() def adapt(self, compressed: list[ResolvedOp]) -> list[TrajectoryOp]: """Convert compressed ops to trajectory-compatible types.""" @@ -343,11 +347,14 @@ def sample( """Run trajectory simulation in batches, returning only measurement outcomes. State vectors are discarded after each batch, making this scalable - to arbitrarily many trajectories. + to arbitrarily many trajectories. When multiple devices are + available, each batch is sharded across them so that every device + processes ``batch_size // n_devices`` trajectories concurrently. :param params: Flat parameter vector from :meth:`linearize`. :param num_trajectories: Total number of trajectories to simulate. - :param batch_size: Maximum number of trajectories per batch. + :param batch_size: Maximum number of trajectories per batch + (total across all devices). :param random_seed: Seed for the JAX PRNG. :return: Measurement outcomes with shape ``(num_trajectories, n_measurements)``. """ @@ -363,6 +370,7 @@ def sample( random_seed, keep_states=False, dims=self.dims, + devices=self._devices, ) if len(all_outcomes) == 1: @@ -388,6 +396,10 @@ def _apply_trajectory_operations( - ``qx.KrausMap``: probabilistic Kraus operator sampling - ``qx.QuantumInstrument``: measurement with outcome recording + Key generation is sharding-friendly: per-operation keys are derived + lazily via ``jax.random.fold_in`` so that the key array is never + materialised in full on a single device. + :param operations: Ordered list of (operator, subsystem) pairs. :param psi: Initial state vector, optionally batched via ensemble dimension. :param key: JAX PRNG key (scalar typed key). Will be split internally to @@ -398,33 +410,42 @@ def _apply_trajectory_operations( """ measurement_outcomes: list[Array] = [] - n_stochastic = sum(1 for op, _ in operations if isinstance(op, (qx.KrausMap, qx.QuantumInstrument))) - ensemble_size = psi.ensemble_size - if n_stochastic > 0: - if ensemble_size: - n_traj = ensemble_size[0] - all_keys = jax.random.split(key, n_stochastic * n_traj) - all_keys = all_keys.reshape(n_stochastic, n_traj) + # Derive per-trajectory base keys once. When the state is sharded + # across devices the resulting key array inherits the same sharding, + # so each device only materialises its own slice. + if ensemble_size: + if key.ndim > 0: + # Already per-trajectory keys (e.g. from multi-device sharding + # or batched ``compute()``). + per_traj_keys = key else: - all_keys = jax.random.split(key, n_stochastic) + per_traj_keys = jax.random.split(key, ensemble_size[0]) + else: + per_traj_keys = None - key_idx = 0 + stochastic_idx = 0 for op, subsystem in operations: match op: case qx.Unitary(): psi = qx.targeted_apply_unitary(op, psi, subsystem) case qx.KrausMap(): - op_keys = all_keys[key_idx] + if per_traj_keys is not None: + op_keys = jax.vmap(lambda k: jax.random.fold_in(k, stochastic_idx))(per_traj_keys) + else: + op_keys = jax.random.fold_in(key, stochastic_idx) psi = qx.targeted_apply_kraus_map_trajectory(op, psi, op_keys, subsystem) - key_idx += 1 + stochastic_idx += 1 case qx.QuantumInstrument(): - op_keys = all_keys[key_idx] + if per_traj_keys is not None: + op_keys = jax.vmap(lambda k: jax.random.fold_in(k, stochastic_idx))(per_traj_keys) + else: + op_keys = jax.random.fold_in(key, stochastic_idx) psi, outcome = qx.targeted_apply_instrument_to_state_vector(op, psi, op_keys, subsystem) measurement_outcomes.append(outcome) - key_idx += 1 + stochastic_idx += 1 case _: raise TypeError(f"Unsupported operator type: {type(op)}") @@ -436,6 +457,20 @@ def _apply_trajectory_operations( return psi, outcomes +def _make_mesh(devices: list[jax.Device] | None) -> Mesh | None: + """Build a 1-D ``Mesh`` over *devices*, or ``None`` for single-device.""" + if devices is None: + devices = jax.devices() + if len(devices) <= 1: + return None + return Mesh(np.array(devices), axis_names=("traj",)) + + +def _round_up_to(n: int, divisor: int) -> int: + """Round *n* up to the nearest multiple of *divisor*.""" + return ((n + divisor - 1) // divisor) * divisor + + def _run_batched_trajectories( operations: list[TrajectoryOp], n_qubits: int, @@ -444,11 +479,21 @@ def _run_batched_trajectories( random_seed: int, keep_states: bool = True, dims: tuple[int, ...] | None = None, + devices: list[jax.Device] | None = None, ) -> tuple[list[qx.StateVector] | None, list[Array]]: - """Run trajectory simulation in batches.""" + """Run trajectory simulation in batches, optionally sharded across devices. + + When *devices* contains more than one device a :class:`jax.sharding.Mesh` + is constructed and both the initial state vector and PRNG keys are sharded + along the trajectory (ensemble) axis. XLA's SPMD partitioner then + distributes the work so that each device processes its own slice. + """ if dims is None: dims = (2,) * n_qubits + mesh = _make_mesh(devices) + n_devices = len(mesh.devices.flat) if mesh is not None else 1 + key = jax.random.key(random_seed) all_psis: list[qx.StateVector] = [] if keep_states else [] all_outcomes: list[Array] = [] @@ -458,20 +503,46 @@ def _run_batched_trajectories( t_total = 0.0 while remaining > 0: this_batch = min(remaining, batch_size) + + # Pad to a multiple of n_devices so the shard split is even. + padded_batch = _round_up_to(this_batch, n_devices) if n_devices > 1 else this_batch + n_pad = padded_batch - this_batch + key, batch_key = jax.random.split(key) - if this_batch == 1: + if padded_batch == 1: psi = qx.zero_state_vector(dims=dims) else: - psi = qx.zero_state_vector(dims=dims, ensemble_size=(this_batch,)) + psi = qx.zero_state_vector(dims=dims, ensemble_size=(padded_batch,)) + + # Shard state and key across devices when a mesh is available. + if mesh is not None: + sharding = NamedSharding(mesh, PartitionSpec("traj")) # type: ignore[no-untyped-call] + psi = qx.StateVector.from_matrix( + jax.device_put(psi.matrix, sharding), + psi.dims, + ) + # Split a per-trajectory key vector and shard it. + batch_keys = jax.random.split(batch_key, padded_batch) + batch_keys = jax.device_put(batch_keys, sharding) + else: + batch_keys = batch_key t0 = time.perf_counter() - psi_out, outcomes = _apply_trajectory_operations(operations, psi, batch_key) + psi_out, outcomes = _apply_trajectory_operations(operations, psi, batch_keys) psi_out.matrix.block_until_ready() t1 = time.perf_counter() t_total += t1 - t0 - if this_batch == 1: + # Strip padding rows. + if n_pad > 0: + psi_out = qx.StateVector.from_matrix( + psi_out.matrix[:this_batch], + psi_out.dims, + ) + outcomes = outcomes[:this_batch] + + if this_batch == 1 and padded_batch == 1: psi_out = qx.StateVector.from_matrix( psi_out.matrix[jnp.newaxis], psi_out.dims, @@ -479,10 +550,12 @@ def _run_batched_trajectories( outcomes = outcomes[jnp.newaxis] logger.debug( - "Batch %d: %d trajectories, %d qubits, %.3f s", + "Batch %d: %d trajectories (%d padded), %d qubits, %d device(s), %.3f s", batch_idx, this_batch, + padded_batch, n_qubits, + n_devices, t1 - t0, ) @@ -493,11 +566,12 @@ def _run_batched_trajectories( batch_idx += 1 logger.info( - "Trajectories complete: %d total, %d batches (size=%d), n_qubits=%d, %.3f s total, %.1f traj/s", + "Trajectories complete: %d total, %d batches (size=%d), n_qubits=%d, %d device(s), %.3f s total, %.1f traj/s", num_trajectories, batch_idx, batch_size, n_qubits, + n_devices, t_total, num_trajectories / t_total if t_total > 0 else float("inf"), ) diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index 624991298..5f85e0b78 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -27,6 +27,8 @@ PureStateVectorSimulator, TrajectorySimulator, _run_batched_trajectories, + _make_mesh, + _round_up_to, ) from pyquil.simulation._simulator import ( _apply_trajectory_operations as apply_trajectory_operations, @@ -1042,3 +1044,99 @@ def test_random_circuit_compression_summary(self, capsys): line += f" {counts[s]:>4} ({ratio:.2f})" # line += f" {counts[s]:>8}" print(line) + + +# ────────────────────────────────────────────────────────────────────────────── +# Multi-device / sharding tests +# ────────────────────────────────────────────────────────────────────────────── + + +class TestMultiDeviceHelpers: + def test_round_up_to(self): + assert _round_up_to(7, 4) == 8 + assert _round_up_to(8, 4) == 8 + assert _round_up_to(1, 3) == 3 + assert _round_up_to(0, 5) == 0 + + def test_make_mesh_single_device_returns_none(self): + """A single device should return None (no mesh needed).""" + devices = jax.devices()[:1] + assert _make_mesh(devices) is None + + def test_make_mesh_none_uses_default(self): + """Passing None should query jax.devices().""" + mesh = _make_mesh(None) + if len(jax.devices()) <= 1: + assert mesh is None + else: + assert mesh is not None + + +class TestMultiDeviceTrajectory: + """Tests that exercise the multi-device code paths. + + On a single-CPU host these still validate the padding/unpadding logic + and the ``devices`` parameter plumbing. On a multi-GPU host they + exercise real cross-device sharding. + """ + + def test_devices_parameter_accepted(self): + """TrajectorySimulator should accept a ``devices`` keyword.""" + p = Program(H(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, qubits=[0], devices=jax.devices()) + outcomes = sim.sample(_EMPTY_PARAMS, num_trajectories=10) + assert outcomes.shape == (10, 1) + + def test_sample_results_match_single_device(self): + """Outcomes shape and value range must be the same regardless of device list.""" + p = Program(H(0), MEASURE(0, None)) + sim_default = TrajectorySimulator(p, qubits=[0]) + sim_explicit = TrajectorySimulator(p, qubits=[0], devices=jax.devices()) + + out_default = sim_default.sample(_EMPTY_PARAMS, num_trajectories=64, batch_size=16, random_seed=99) + out_explicit = sim_explicit.sample(_EMPTY_PARAMS, num_trajectories=64, batch_size=16, random_seed=99) + + assert out_default.shape == out_explicit.shape + assert jnp.all((out_default == 0) | (out_default == 1)) + assert jnp.all((out_explicit == 0) | (out_explicit == 1)) + + def test_padding_stripped_correctly(self): + """When num_trajectories is not a multiple of n_devices, padding must be removed.""" + p = Program(H(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, qubits=[0], devices=jax.devices()) + # 7 is unlikely to be a multiple of any device count > 1 + outcomes = sim.sample(_EMPTY_PARAMS, num_trajectories=7, batch_size=7) + assert outcomes.shape == (7, 1) + + def test_batched_trajectories_with_devices(self): + """_run_batched_trajectories should accept and use devices parameter.""" + p = Program(H(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, qubits=[0]) + resolved = sim.resolve(_EMPTY_PARAMS) + compressed = sim.compress(resolved) + operations = sim.adapt(compressed) + + _, outcomes = _run_batched_trajectories( + operations, + sim.n_qubits, + num_trajectories=20, + batch_size=8, + random_seed=42, + keep_states=False, + dims=sim.dims, + devices=jax.devices(), + ) + total = sum(o.shape[0] for o in outcomes) + assert total == 20 + + def test_noisy_sample_with_devices(self): + """Multi-device path should work with noise models.""" + p_error = 0.3 + ch = Channel.from_pauli_noise(inst=X(0), pauli_noise={"X": p_error}) + noise_model = NoiseModel(channels=[ch]) + p = Program(X(0), MEASURE(0, None)) + sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0], devices=jax.devices()) + outcomes = sim.sample(_EMPTY_PARAMS, num_trajectories=1024, batch_size=256, random_seed=7) + assert outcomes.shape == (1024, 1) + frac_0 = float(jnp.mean(outcomes == 0)) + assert abs(frac_0 - p_error) < 0.05 From 3b9bd4fa5a83e34335de25d220a2b5fb34ddf811 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Tue, 26 May 2026 14:58:49 +0000 Subject: [PATCH 11/37] Resolver simplification --- .github/workflows/benchmark_base.yml | 6 +- .github/workflows/benchmark_pr.yml | 6 +- .github/workflows/publish.yml | 8 +- pyquil/simulation/_resolver.py | 714 +++++++++++---------------- pyquil/simulation/_simulator.py | 39 +- test/unit/test_resolver.py | 183 +++++++ 6 files changed, 519 insertions(+), 437 deletions(-) create mode 100644 test/unit/test_resolver.py diff --git a/.github/workflows/benchmark_base.yml b/.github/workflows/benchmark_base.yml index f56c957a5..4c43695ea 100644 --- a/.github/workflows/benchmark_base.yml +++ b/.github/workflows/benchmark_base.yml @@ -12,10 +12,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - name: Set up Python 3.12 + uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.12' - uses: actions/cache@v4 with: path: .venv diff --git a/.github/workflows/benchmark_pr.yml b/.github/workflows/benchmark_pr.yml index 77df0e807..69d152eca 100644 --- a/.github/workflows/benchmark_pr.yml +++ b/.github/workflows/benchmark_pr.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - name: Set up Python 3.12 + uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.12' - uses: actions/cache@v4 with: path: .venv diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6049701ce..29894491f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.12' - uses: snok/install-poetry@v1 with: virtualenvs-in-project: true @@ -43,7 +45,9 @@ jobs: contents: read steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.12' - uses: snok/install-poetry@v1 with: virtualenvs-in-project: true diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 0103837dc..3359680bc 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -15,23 +15,27 @@ ############################################################################## """Shared infrastructure for the density-matrix and state-vector simulators. -This module provides the three front-end stages of the simulation pipeline: +This module provides the simulation preprocessing pipeline: 1. **Linearizer** — converts a ``MemoryMap`` into a flat JAX parameter vector. -2. **Resolver** — converts a parameter vector into a list of +2. **Expander** — expands a program into a flat list of operators and physical + qubit tuples, resolving noise channels, custom gates, and DEFCIRCUIT + bodies. Fixed (non-parameterized) operations are returned as concrete + quax types; parameterized gates are returned as callables. +3. **Resolver** — converts a parameter vector into a list of ``(operator, subsystem)`` pairs using native quax types. -3. **Adapters** — convert resolved operations into the form expected by each +4. **Adapters** — convert resolved operations into the form expected by each simulator backend (``SuperOp`` for density matrices; ``Unitary``/``KrausMap``/ ``QuantumInstrument`` for state-vector trajectories). - -It also provides shared utilities: DAG construction, dimension inference. +5. **Compressor** — merges adjacent operators via greedy edge contraction. """ from __future__ import annotations +import heapq import logging from collections.abc import Callable -from typing import Any, Union, cast +from typing import Any, cast import jax.numpy as jnp import networkx as nx @@ -44,6 +48,7 @@ CycleChannel, MeasurementChannel, ResetChannel, + get_custom_gates_from_program, get_instruction_unitary, ) from pyquil.noise._noise_model import ( @@ -60,11 +65,15 @@ # Type aliases # ────────────────────────────────────────────────────────── +# A fixed (non-parameterized) operator — the most specific native quax type. +FixedOp = qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument + +# An expanded item is either a fixed operator or a callable that resolves +# parameters into a Unitary (only parameterized gates produce callables). +ExpandedOp = FixedOp | Callable[[Array], qx.Unitary] + # Resolved operations retain the most specific native quax type. -ResolvedOp = tuple[qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument, tuple[int, ...]] -RecipeOp = Union[qx.Unitary, qx.SuperOp, qx.KrausMap, qx.QuantumInstrument] -RecipeCallable = Callable[[Array], RecipeOp] -Recipe = tuple[RecipeOp | RecipeCallable, tuple[int, ...]] +ResolvedOp = tuple[FixedOp, tuple[int, ...]] # Trajectory operations for the state-vector simulator. TrajectoryOp = tuple[qx.Unitary | qx.KrausMap | qx.QuantumInstrument, tuple[int, ...]] @@ -72,9 +81,6 @@ # Density-matrix operations. DensityMatrixOp = tuple[qx.SuperOp, tuple[int, ...]] -# Custom gate definitions. -CustomGateMap = dict - # ══════════════════════════════════════════════════════════ # Linearizer @@ -141,36 +147,10 @@ def linearize(memory_map: MemoryMap) -> Array: # ══════════════════════════════════════════════════════════ -# Resolver +# Expander # ══════════════════════════════════════════════════════════ -class Resolver: - """Resolves a flat parameter vector into a list of (operator, subsystem) pairs. - - Constructed via :func:`resolver_from_program`. Call instances directly:: - - resolver = resolver_from_program(program, ...) - ops = resolver(params) - - :param dims: Inferred per-qudit dimensions (e.g. ``(2, 2, 3)``). - """ - - __slots__ = ("_resolve_fn", "dims") - - def __init__(self, resolve_fn: Callable[[Array], list[ResolvedOp]], dims: tuple[int, ...]) -> None: - self._resolve_fn = resolve_fn - self.dims = dims - - def __call__(self, params: Array) -> list[ResolvedOp]: - return self._resolve_fn(params) - - -def _is_parameterized(inst: Gate) -> bool: - """Check if a gate instruction has any MemoryReference parameters.""" - return any(isinstance(p, MemoryReference) for p in inst.params) - - def _measure_registers(program: Program) -> set[str]: """Return the set of register names that are targets of MEASURE instructions.""" regs: set[str] = set() @@ -182,46 +162,15 @@ def _measure_registers(program: Program) -> set[str]: return regs -def _collect_circuit_definitions(program: Program) -> dict[str, DefCircuit]: - """Extract DEFCIRCUIT definitions from a program. - - :param program: Quil program. - :return: Mapping from circuit name to definition. - """ - circuit_definitions: dict[str, DefCircuit] = {} - for inst in program.instructions: - if isinstance(inst, DefCircuit): - circuit_definitions[inst.name] = inst - return circuit_definitions - - -# ── Instruction expansion result ── - - -class _ExpandedProgram: - """Result of :func:`_expand_instructions`. - - Bundles the expanded instruction/channel lists with the dependency DAG - and topological node order so they can be passed to downstream phases - without a long argument list. - """ - - __slots__ = ("insts", "channels", "dag", "node_order") - - def __init__(self) -> None: - self.insts: list[Gate | Measurement | ResetQubit | Reset] = [] - self.channels: list[Channel | MeasurementChannel | None] = [] - self.dag: nx.DiGraph = nx.DiGraph() - self.node_order: list[int] = [] - - -def _expand_instructions( +def expand_program( program: Program, - noise_model: NoiseModelLike | None, - qubit_indices: dict[int, int], - circuit_definitions: dict[str, DefCircuit], -) -> _ExpandedProgram: - """Expand a program's instructions, building a dependency DAG. + noise_model: NoiseModelLike | None = None, +) -> tuple[list[ExpandedOp], list[tuple[int, ...]]]: + """Expand a program into operators and physical qubit tuples. + + Fixed (non-parameterized) operations are returned as concrete quax types + (``Unitary``, ``SuperOp``, ``QuantumInstrument``). Only parameterized + gates are returned as ``Callable[[Array], Unitary]``. DEFCIRCUIT invocations are expanded: @@ -230,47 +179,123 @@ def _expand_instructions( * Otherwise the DEFCIRCUIT body is expanded via qubit/param substitution and each resulting instruction is resolved individually. - :param program: Quil program. + The noise model is fully resolved during expansion: noisy gates become + ``SuperOp``, noisy measurements become ``QuantumInstrument``, and noisy + resets become ``SuperOp``. + + :param program: Quil program (may contain DEFCIRCUITs). :param noise_model: Optional noise model. - :param qubit_indices: Mapping from physical qubit id → 0-based index. - :param circuit_definitions: DEFCIRCUIT definitions from the program. - :return: An :class:`_ExpandedProgram` containing instructions, channels, DAG, and node order. + :return: Tuple of ``(ops, qubit_tuples)`` where each op is either a + concrete quax operator or a ``Callable[[Array], Unitary]`` for + parameterized gates, and each qubit tuple contains physical qubit IDs. """ - ep = _ExpandedProgram() - last_on_qubit: dict[int, int] = {} + # Derive circuit definitions and custom gates from the program. + circuit_definitions: dict[str, DefCircuit] = {} + for inst in program.instructions: + if isinstance(inst, DefCircuit): + circuit_definitions[inst.name] = inst - def _emit( - inst: Gate | Measurement | ResetQubit | Reset, channel: Channel | MeasurementChannel | None = None - ) -> None: - if isinstance(inst, Gate): - qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) - elif isinstance(inst, Measurement): - qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) - elif isinstance(inst, ResetQubit): - qubits = tuple(qubit_indices[q] for q in inst.get_qubit_indices()) # type: ignore[union-attr] - else: # Reset - qubits = tuple(sorted(qubit_indices.values())) - node_key = len(ep.insts) - ep.dag.add_node(node_key, inst=inst, qubits=qubits) - ep.node_order.append(node_key) - for q in qubits: - if q in last_on_qubit: - ep.dag.add_edge(last_on_qubit[q], node_key) - last_on_qubit[q] = node_key - ep.insts.append(inst) - ep.channels.append(channel) + custom_gates = get_custom_gates_from_program(program) or None - def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: - if isinstance(inst, Gate): - ch = noise_model.get_channel(inst) if noise_model is not None else None - if isinstance(ch, CycleChannel): - ch = None - _emit(inst, ch) - elif isinstance(inst, Measurement): - ch_m: MeasurementChannel | None = noise_model.get_channel(inst) if noise_model is not None else None - _emit(inst, ch_m if isinstance(ch_m, MeasurementChannel) else None) - else: - _emit(inst) + measure_regs = _measure_registers(program) + all_qubits = sorted(program.get_qubit_indices()) + + ops: list[ExpandedOp] = [] + qubit_tuples: list[tuple[int, ...]] = [] + param_counter = 0 + + def _emit_op(op: ExpandedOp, qubits: tuple[int, ...]) -> None: + ops.append(op) + qubit_tuples.append(qubits) + + def _resolve_gate(inst: Gate) -> tuple[ExpandedOp, tuple[int, ...]]: + """Resolve a single gate instruction to an operator or callable.""" + nonlocal param_counter + qubits = tuple(inst.get_qubit_indices()) + + # Check noise model first. + channel = noise_model.get_channel(inst) if noise_model is not None else None + if isinstance(channel, Channel): + return channel.process, qubits + if isinstance(channel, CycleChannel): + raise ValueError(f"CycleChannel for {inst.name} was not expanded before gate resolution.") + + # Parameterized gate → callable that resolves params at call time. + if any(isinstance(p, MemoryReference) for p in inst.params): + gate_name = inst.name + if custom_gates is not None and gate_name in custom_gates: + gate_def = custom_gates[gate_name] + elif gate_name in qx.gates.QUANTUM_GATES: + gate_def = qx.gates.QUANTUM_GATES[gate_name] + else: + raise KeyError(f"Unknown gate '{gate_name}'.") + + param_indices: list[int] = [] + concrete_params = list(inst.params) + for p in inst.params: + if isinstance(p, MemoryReference) and p.name not in measure_regs: + param_indices.append(param_counter) + param_counter += 1 + else: + param_indices.append(-1) + + def _make_param_callable( + gdef: object, + cp: list, + pi: list[int], + ) -> Callable[[Array], qx.Unitary]: + def resolve_params(params: Array) -> qx.Unitary: + resolved: list[Any] = [] + for p, pv in zip(cp, pi, strict=False): + if pv >= 0: + resolved.append(params[pv]) + else: + resolved.append(float(p.real) if hasattr(p, "real") else float(p)) + result = gdef(*resolved) if callable(gdef) else gdef + if not isinstance(result, qx.Unitary): + result = cast(Any, result) + result = qx.Unitary.from_matrix(result.matrix, result.dims) + return result + + return resolve_params + + return _make_param_callable(gate_def, concrete_params, param_indices), qubits + + # Fixed gate → resolve to Unitary now. + unitary = get_instruction_unitary(inst, custom_gates=custom_gates) + return unitary, qubits + + def _resolve_measurement(inst: Measurement) -> tuple[FixedOp, tuple[int, ...]]: + """Resolve a measurement instruction.""" + qubits = tuple(inst.get_qubit_indices()) + channel = noise_model.get_channel(inst) if noise_model is not None else None + if isinstance(channel, MeasurementChannel): + return channel.process, qubits + return qx.gates.MEASURE(), qubits + + def _resolve_reset_qubit(inst: ResetQubit) -> tuple[FixedOp, tuple[int, ...]]: + """Resolve a targeted reset instruction.""" + qubits = tuple(inst.get_qubit_indices()) # type: ignore[union-attr] + channel = noise_model.get_channel(inst) if noise_model is not None else None + if isinstance(channel, ResetChannel): + return channel.process, qubits + return qx.gates.RESET(), qubits + + def _emit_instruction(inst: Gate | Measurement | ResetQubit | Reset) -> None: + """Resolve and emit a single instruction.""" + match inst: + case Gate(): + op, qubits = _resolve_gate(inst) + _emit_op(op, qubits) + case Measurement(): + op, qubits = _resolve_measurement(inst) + _emit_op(op, qubits) + case ResetQubit(): + op, qubits = _resolve_reset_qubit(inst) + _emit_op(op, qubits) + case Reset(): + for q in all_qubits: + _emit_op(qx.gates.RESET(), (q,)) for inst in program.instructions: if isinstance(inst, DefCircuit): @@ -280,221 +305,98 @@ def _lookup_and_emit(inst: Gate | Measurement | ResetQubit | Reset) -> None: channel = noise_model.get_channel(inst) if noise_model is not None else None if isinstance(channel, CycleChannel): + # Expand using the channel's constituent operators. for sub_ch in channel.channels: - _emit(sub_ch.inst, sub_ch) + sub_qubits = tuple(sub_ch.inst.get_qubit_indices()) + _emit_op(sub_ch.process, sub_qubits) else: - for expanded_inst in expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions): - _lookup_and_emit(expanded_inst) + # Expand DEFCIRCUIT body and resolve each instruction. + for expanded_inst in expand_defcircuit_body( + inst, circuit_definitions[inst.name], circuit_definitions + ): + _emit_instruction(expanded_inst) elif isinstance(inst, (Gate, Measurement, ResetQubit, Reset)): - _lookup_and_emit(inst) + _emit_instruction(inst) - return ep + return ops, qubit_tuples -def _assign_param_indices( - ep: _ExpandedProgram, - measure_regs: set[str], -) -> tuple[dict[int, list[int]], int]: - """Assign parameter-vector indices to each gate's ``MemoryReference`` parameters. +# ══════════════════════════════════════════════════════════ +# DAG construction & qubit remapping +# ══════════════════════════════════════════════════════════ + - :param ep: Expanded program. - :param measure_regs: Register names that are targets of MEASURE (excluded from params). - :return: Tuple of ``(gate_param_indices, total_param_count)``. - ``gate_param_indices`` maps node key → list of param-vector indices - (``-1`` for literal/non-parameter slots). +def remap_qubits( + qubit_tuples: list[tuple[int, ...]], + qubit_indices: dict[int, int], +) -> list[tuple[int, ...]]: + """Remap physical qubit IDs to 0-based indices. + + :param qubit_tuples: List of physical qubit tuples from :func:`expand_program`. + :param qubit_indices: Mapping from physical qubit id → 0-based index. + :return: Remapped qubit tuples. """ - param_counter = 0 - gate_param_indices: dict[int, list[int]] = {} - for idx in ep.node_order: - inst = ep.insts[idx] - if isinstance(inst, Gate): - indices = [] - for param in inst.params: - if isinstance(param, MemoryReference) and param.name not in measure_regs: - indices.append(param_counter) - param_counter += 1 - else: - indices.append(-1) - gate_param_indices[idx] = indices - return gate_param_indices, param_counter + return [tuple(qubit_indices[q] for q in qubits) for qubits in qubit_tuples] -def _infer_dims_from_instructions( - ep: _ExpandedProgram, - noise_model: NoiseModelLike | None, - custom_gates: CustomGateMap | None, -) -> dict[int, int]: - """Infer per-qudit dimensions from the expanded instruction list. +def build_dag(qubit_tuples: list[tuple[int, ...]]) -> nx.DiGraph: + """Build a dependency DAG from qubit tuples. - Scans all expanded instructions — gates, measurements, and resets — and - their associated channels/unitaries to determine the Hilbert-space - dimension of each qubit slot (defaulting to 2 for standard qubits). + Each node corresponds to one operation (indexed 0..N-1). An edge + ``(u, v)`` exists when ``u`` and ``v`` act on a shared qubit and + ``u`` precedes ``v`` in program order. - :param ep: Expanded program. - :param noise_model: Optional noise model. - :param custom_gates: Custom gate definitions. - :return: Mapping from qubit index → dimension (only entries > 2 are guaranteed present). + :param qubit_tuples: Remapped qubit tuples (0-based indices). + :return: DAG with node attribute ``"qubits"`` storing each node's qubit tuple. """ - qudit_dims: dict[int, int] = {} + dag = nx.DiGraph() + last_on_qubit: dict[int, int] = {} - def _update(subsystem: tuple[int, ...], op_dims: tuple[int, ...]) -> None: - for slot, dim in zip(subsystem, op_dims, strict=False): - if dim > qudit_dims.get(slot, 2): - qudit_dims[slot] = dim + for idx, qubits in enumerate(qubit_tuples): + dag.add_node(idx, qubits=qubits) + for q in qubits: + if q in last_on_qubit: + dag.add_edge(last_on_qubit[q], idx) + last_on_qubit[q] = idx - for node_key in ep.node_order: - inst = ep.insts[node_key] - subsystem = ep.dag.nodes[node_key]["qubits"] - channel: Channel | MeasurementChannel | ResetChannel | CycleChannel | None = ep.channels[node_key] + return dag - if isinstance(inst, Gate): - if channel is None and noise_model is not None: - channel = noise_model.get_channel(inst) - if isinstance(channel, Channel): - _update(subsystem, channel.process.dims[0]) - elif isinstance(channel, CycleChannel): - continue - else: - try: - unitary = get_instruction_unitary(inst, custom_gates=custom_gates) - _update(subsystem, unitary.dims[0]) - except Exception: # noqa: S112 - continue - - elif isinstance(inst, Measurement): - if channel is None and noise_model is not None: - channel = noise_model.get_channel(inst) - if isinstance(channel, MeasurementChannel): - _update(subsystem, channel.process.dims[0]) - - elif isinstance(inst, ResetQubit): - reset_ch = None - if noise_model is not None: - reset_ch = noise_model.get_channel(inst) - if isinstance(reset_ch, ResetChannel): - _update(subsystem, reset_ch.process.dims[0]) - - return qudit_dims - - -def _build_recipes( - ep: _ExpandedProgram, - noise_model: NoiseModelLike | None, - qubit_indices: dict[int, int], - custom_gates: CustomGateMap | None, - gate_param_indices: dict[int, list[int]], - measure_regs: set[str], - qudit_dims: dict[int, int], -) -> list[Recipe]: - """Convert expanded instructions into operator recipes. - Each recipe is either a concrete quax operator or a callable that - accepts a flat parameter vector and returns an operator. +# ══════════════════════════════════════════════════════════ +# Resolver +# ══════════════════════════════════════════════════════════ - :param ep: Expanded program. - :param noise_model: Optional noise model. - :param qubit_indices: Mapping from physical qubit id → 0-based index. - :param custom_gates: Custom gate definitions. - :param gate_param_indices: Parameter-vector indices per gate node. - :param measure_regs: Register names that are targets of MEASURE. - :param qudit_dims: Per-qubit dimensions from :func:`_infer_dims_from_instructions`. - :return: Ordered list of ``(operator_or_callable, subsystem)`` recipes. - """ - recipes: list[Recipe] = [] - for node_key in ep.node_order: - inst = ep.insts[node_key] - subsystem = ep.dag.nodes[node_key]["qubits"] +class Resolver: + """Resolves a flat parameter vector into a list of (operator, subsystem) pairs. - match inst: - case Gate(): - channel2: Channel | MeasurementChannel | CycleChannel | None = ep.channels[node_key] - if channel2 is None and noise_model is not None: - channel2 = noise_model.get_channel(inst) - - if channel2 is not None and isinstance(channel2, Channel): - recipes.append((channel2.process, subsystem)) - elif channel2 is not None and isinstance(channel2, MeasurementChannel): - raise ValueError(f"MeasurementChannel cannot be applied to expanded gate {inst}.") - elif channel2 is not None and isinstance(channel2, CycleChannel): - raise ValueError(f"CycleChannel for {inst.name} was not expanded before resolver construction.") - elif _is_parameterized(inst): - gate_name = inst.name - if custom_gates is not None and gate_name in custom_gates: - gate_def = custom_gates[gate_name] - elif gate_name in qx.gates.QUANTUM_GATES: - gate_def = qx.gates.QUANTUM_GATES[gate_name] - else: - raise KeyError(f"Unknown gate '{gate_name}'.") - pidx = gate_param_indices[node_key] - cparams = list(inst.params) - - def _make_param_recipe( - gdef: object, - cp: list, - pi: list[int], - ) -> Callable[[Array], qx.Unitary]: - def recipe(params: Array) -> qx.Unitary: - resolved: list[Any] = [] - for p, pv in zip(cp, pi, strict=False): - if pv >= 0: - resolved.append(params[pv]) - else: - resolved.append(float(p.real) if hasattr(p, "real") else float(p)) - result = gdef(*resolved) if callable(gdef) else gdef - if not isinstance(result, qx.Unitary): - result = cast(Any, result) - result = qx.Unitary.from_matrix(result.matrix, result.dims) - return result - - return recipe - - recipes.append((_make_param_recipe(gate_def, cparams, pidx), subsystem)) + Constructed via :func:`resolver_from_program`. Call instances directly:: - else: - unitary = get_instruction_unitary(inst, custom_gates=custom_gates) - recipes.append((unitary, subsystem)) + resolver = resolver_from_program(program, ...) + ops = resolver(params) - case Measurement(): - meas_channel = ep.channels[node_key] - if meas_channel is None and noise_model is not None: - meas_channel = noise_model.get_channel(inst) - if meas_channel is not None and isinstance(meas_channel, MeasurementChannel): - recipes.append((meas_channel.process, subsystem)) - elif meas_channel is not None and isinstance(meas_channel, Channel): - raise ValueError(f"Channel cannot be applied to expanded measurement {inst}.") - else: - dim = qudit_dims.get(subsystem[0], 2) - recipes.append((qx.gates.MEASURE(dim=dim), subsystem)) + :param dims: Inferred per-qudit dimensions (e.g. ``(2, 2, 3)``). + """ - case ResetQubit(): - reset_channel = None - if noise_model is not None: - reset_channel = noise_model.get_channel(inst) - if reset_channel is not None and isinstance(reset_channel, ResetChannel): - recipes.append((reset_channel.process, subsystem)) - else: - dim = qudit_dims.get(subsystem[0], 2) - recipes.append((qx.gates.RESET(dim=dim), subsystem)) + __slots__ = ("_resolve_fn", "dims") - case Reset(): - for _, q_idx in sorted(qubit_indices.items()): - dim = qudit_dims.get(q_idx, 2) - recipes.append((qx.gates.RESET(dim=dim), (q_idx,))) + def __init__(self, resolve_fn: Callable[[Array], list[ResolvedOp]], dims: tuple[int, ...]) -> None: + self._resolve_fn = resolve_fn + self.dims = dims - return recipes + def __call__(self, params: Array) -> list[ResolvedOp]: + return self._resolve_fn(params) def resolver_from_program( program: Program, - noise_model: NoiseModelLike | None, - qubit_indices: dict[int, int], - custom_gates: CustomGateMap | None, -) -> tuple[Resolver, nx.DiGraph, list[int]]: - """Build a :class:`Resolver`, DAG, and node order from a program. + noise_model: NoiseModelLike | None = None, + qubits: list[int] | None = None, +) -> tuple[Resolver, nx.DiGraph]: + """Build a :class:`Resolver` and dependency DAG from a program. The resolver accepts a flat parameter vector and produces one - ``(operator, subsystem)`` pair per DAG node, in ``node_order``. + ``(operator, subsystem)`` pair per operation, in program order. Operators are returned in their most specific native type: @@ -505,44 +407,38 @@ def resolver_from_program( * Noisy resets (``ResetChannel``) → ``qx.SuperOp`` * Ideal resets → ``qx.SuperOp`` - No type conversion (``to_kraus``, ``to_superop``) is performed here; - that is the adapter's responsibility. + Custom gate definitions (DEFGATE) and circuit definitions (DEFCIRCUIT) + are derived from the program automatically. - :param program: Quil program (may contain DEFCIRCUITs). + :param program: Quil program (may contain DEFCIRCUITs and DEFGATEs). :param noise_model: Optional noise model. - :param qubit_indices: Mapping from physical qubit id → 0-based index. - :param custom_gates: Custom gate definitions. - :return: Tuple of ``(Resolver, dag, node_order)``. + :param qubits: Optional explicit qubit list. If ``None``, inferred from + the program. Use this when the simulator knows about qubits that + don't appear in the program. + :return: Tuple of ``(Resolver, dag)``. """ - measure_regs = _measure_registers(program) - circuit_defs = _collect_circuit_definitions(program) + # Phase 1: Expand into flat operators + physical qubit tuples. + expanded_ops, phys_qubits = expand_program(program, noise_model) - # Phase 1: Expand instructions and build the dependency DAG. - ep = _expand_instructions(program, noise_model, qubit_indices, circuit_defs) + # Phase 2: Remap physical qubits to 0-based indices. + if qubits is None: + qubits = sorted(program.get_qubit_indices()) + qubit_indices = {q: i for i, q in enumerate(qubits)} + mapped_qubits = remap_qubits(phys_qubits, qubit_indices) - # Phase 2: Assign parameter-vector indices. - gate_param_indices, _ = _assign_param_indices(ep, measure_regs) + # Phase 3: Build dependency DAG. + dag = build_dag(mapped_qubits) - # Phase 3: Infer per-qudit dimensions. - qudit_dims = _infer_dims_from_instructions(ep, noise_model, custom_gates) + # Phase 4: Build the resolve closure. + frozen_ops = list(zip(expanded_ops, mapped_qubits)) - # Phase 4: Build operator recipes. - recipes = _build_recipes(ep, noise_model, qubit_indices, custom_gates, gate_param_indices, measure_regs, qudit_dims) - - # Phase 5: Build the resolve closure. def resolve(params: Array) -> list[ResolvedOp]: - ops: list[ResolvedOp] = [] - for op_or_fn, subsystem in recipes: - if isinstance(op_or_fn, (qx.Unitary, qx.KrausMap, qx.SuperOp, qx.QuantumInstrument)): - ops.append((op_or_fn, subsystem)) - else: - ops.append((op_or_fn(params), subsystem)) - return ops + return [(item(params) if callable(item) else item, subsystem) for item, subsystem in frozen_ops] - n_qubits = len(qubit_indices) - dims = tuple(qudit_dims.get(i, 2) for i in range(n_qubits)) + n_qubits = len(qubits) + dims = (2,) * n_qubits - return Resolver(resolve, dims=dims), ep.dag, ep.node_order + return Resolver(resolve, dims=dims), dag # ══════════════════════════════════════════════════════════ @@ -572,13 +468,13 @@ def adapt_for_density_matrix( """ result: list[DensityMatrixOp] = [] for op, subsystem in ops: - if isinstance(op, qx.SuperOp): - result.append((op, subsystem)) - elif isinstance(op, qx.QuantumInstrument): - result.append((qx.to_superop(op.total_channel()), subsystem)) - else: - # Unitary, KrausMap - result.append((qx.to_superop(op), subsystem)) + match op: + case qx.SuperOp(): + result.append((op, subsystem)) + case qx.QuantumInstrument(): + result.append((qx.to_superop(op.total_channel()), subsystem)) + case qx.Unitary() | qx.KrausMap(): + result.append((qx.to_superop(op), subsystem)) return result @@ -599,12 +495,12 @@ def adapt_for_trajectory( """ result: list[TrajectoryOp] = [] for op, subsystem in ops: - if isinstance(op, qx.SuperOp): - km = qx.truncate_kraus(qx.to_kraus(op), atol=kraus_truncation_threshold) - result.append((km, subsystem)) - else: - # Unitary, KrausMap, QuantumInstrument — pass through - result.append((op, subsystem)) + match op: + case qx.SuperOp(): + km = qx.truncate_kraus(qx.to_kraus(op), atol=kraus_truncation_threshold) + result.append((km, subsystem)) + case qx.Unitary() | qx.KrausMap() | qx.QuantumInstrument(): + result.append((op, subsystem)) return result @@ -687,32 +583,39 @@ def union(self, x: int, y: int) -> int: def compressor_from_dag( dag: nx.DiGraph, - node_order: list[int], max_subsystem_size: int, dims: tuple[int, ...] = (), + *, + barrier_nodes: set[int] | None = None, ) -> Callable[[list[ResolvedOp]], list[ResolvedOp]]: """Build a compressor that merges operators via greedy edge contraction. - The algorithm: - 1. Classify each node as *mergeable* (gate, reset) or *barrier* (measurement). - 2. Greedily contract DAG edges: for each edge ``(u, v)`` in topological - order, merge the groups of ``u`` and ``v`` if both are mergeable and - the union of their qubit sets fits within ``max_subsystem_size``. - 3. Build a merge plan mapping each group to its constituent nodes and - merged qubit subsystem. + The algorithm prioritises merging small gates into larger groups, which + reduces the number of distinct subsystem shapes and therefore JIT + compilation time. + + 1. Classify each node as *mergeable* or *barrier* (e.g. measurement). + Barrier nodes are never merged. + 2. Build a priority queue of candidate edge merges sorted by resulting + subsystem size (ascending), so 1-qubit gates are absorbed into + neighbouring multi-qubit groups first. + 3. Greedily contract edges while the merged subsystem fits within + ``max_subsystem_size``. 4. Return a closure that receives the resolved operator list and produces a compressed operator list. - :param dag: Program dependency DAG. - :param node_order: Node keys in instruction order. + :param dag: Program dependency DAG (nodes indexed 0..N-1, each with + a ``"qubits"`` attribute). :param max_subsystem_size: Maximum number of qubits in a merged group. 0 disables merging entirely. - :return: A closure ``compress(ops) -> List[ResolvedOp]``. + :param dims: Per-qudit dimensions tuple for embedding during merge. + :param barrier_nodes: Optional set of node indices that should not be + merged (e.g. measurement nodes). If ``None``, all nodes are mergeable. + :return: A closure ``compress(ops) -> list[ResolvedOp]``. """ - n_original = len(node_order) + n_original = dag.number_of_nodes() if max_subsystem_size == 0 or n_original == 0: - # No merging — pass through def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: return ops @@ -722,39 +625,56 @@ def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: ) return compress_passthrough - def _is_mergeable(node_key: int) -> bool: - inst = dag.nodes[node_key]["inst"] - return isinstance(inst, (Gate, Measurement, ResetQubit, Reset)) and not isinstance(inst, Measurement) + if barrier_nodes is None: + barrier_nodes = set() - # --- Greedy edge contraction --- + # --- Priority-queue based greedy edge contraction --- uf = _UnionFind() - group_qubits: dict[int, set[int]] = {} # root → set of qubit indices + group_qubits: dict[int, set[int]] = {} - for nk in node_order: + for nk in dag.nodes: uf.make_set(nk) group_qubits[nk] = set(dag.nodes[nk]["qubits"]) - topo_order = list(nx.topological_sort(dag)) - - for u_node in topo_order: - for v_node in dag.successors(u_node): - ru = uf.find(u_node) - rv = uf.find(v_node) - if ru == rv: - continue - if not _is_mergeable(u_node) or not _is_mergeable(v_node): - continue - union_qubits = group_qubits[ru] | group_qubits[rv] - if len(union_qubits) > max_subsystem_size: + # Build initial candidate heap: (union_size, u, v) + # Smaller union sizes are processed first. + heap: list[tuple[int, int, int]] = [] + for u_node, v_node in dag.edges: + if u_node in barrier_nodes or v_node in barrier_nodes: + continue + union_size = len(group_qubits[u_node] | group_qubits[v_node]) + if union_size <= max_subsystem_size: + heapq.heappush(heap, (union_size, u_node, v_node)) + + while heap: + _, u_node, v_node = heapq.heappop(heap) + ru = uf.find(u_node) + rv = uf.find(v_node) + if ru == rv: + continue + union_qubits = group_qubits[ru] | group_qubits[rv] + if len(union_qubits) > max_subsystem_size: + continue + new_root = uf.union(ru, rv) + group_qubits[new_root] = union_qubits + old_root = rv if new_root == ru else ru + if old_root in group_qubits: + del group_qubits[old_root] + + # Re-enqueue edges from the newly merged group to its neighbours. + for neighbour in set(dag.successors(u_node)) | set(dag.predecessors(u_node)) | set( + dag.successors(v_node) + ) | set(dag.predecessors(v_node)): + rn = uf.find(neighbour) + if rn == new_root or neighbour in barrier_nodes: continue - new_root = uf.union(ru, rv) - group_qubits[new_root] = union_qubits - # Clean up the non-root entry - old_root = rv if new_root == ru else ru - if old_root in group_qubits: - del group_qubits[old_root] + new_union_size = len(group_qubits[new_root] | group_qubits[rn]) + if new_union_size <= max_subsystem_size: + heapq.heappush(heap, (new_union_size, u_node, neighbour)) # --- Build merge plan --- + topo_order = list(nx.topological_sort(dag)) + root_to_nodes: dict[int, list[int]] = {} for nk in topo_order: root = uf.find(nk) @@ -764,8 +684,6 @@ def _is_mergeable(node_key: int) -> bool: for root, qubits in group_qubits.items(): root_to_subsystem[root] = tuple(sorted(qubits)) - node_key_to_idx: dict[int, int] = {nk: i for i, nk in enumerate(node_order)} - emit_order: list[tuple[int, list[int], tuple[int, ...]]] = [] emitted_roots: set[int] = set() for nk in topo_order: @@ -800,43 +718,11 @@ def compress(ops: list[ResolvedOp]) -> list[ResolvedOp]: result: list[ResolvedOp] = [] for _, nodes, subsystem in emit_order: if len(nodes) == 1: - idx = node_key_to_idx[nodes[0]] - result.append(ops[idx]) + result.append(ops[nodes[0]]) else: - group_ops = [(ops[node_key_to_idx[nk]][0], ops[node_key_to_idx[nk]][1]) for nk in nodes] + group_ops = [(ops[nk][0], ops[nk][1]) for nk in nodes] merged = _merge_ops(group_ops, subsystem, dims) result.append(merged) return result return compress - - -# ══════════════════════════════════════════════════════════ -# Dimension inference -# ══════════════════════════════════════════════════════════ - - -def infer_qudit_dims( - operations: list[ResolvedOp] | list[TrajectoryOp] | list[DensityMatrixOp], - n_qudits: int, -) -> tuple[int, ...]: - """Infer per-qudit dimensions from resolved operations. - - Starts with all registers at dimension 2 (qubit). For each operation, - checks the operator's dims and upgrades any slot whose operator dimension - exceeds the current assignment. - - :param operations: Resolved list of ``(operator, subsystem)`` pairs. - :param n_qudits: Number of qudit slots. - :return: Tuple of per-qudit dimensions, e.g. ``(2, 3, 2)``. - """ - qudit_dims: list[int] = [2] * n_qudits - for op, subsystem in operations: - # All quax operators expose dims as ((out_dims), (in_dims)) - op_dims = op.dims[0] if hasattr(op, "dims") else None - if op_dims is None: - continue - for slot, dim in zip(subsystem, op_dims, strict=False): - if dim > qudit_dims[slot]: - qudit_dims[slot] = dim - return tuple(qudit_dims) diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 4d29d878c..7ca409936 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -44,7 +44,6 @@ from jax.sharding import Mesh, NamedSharding, PartitionSpec from pyquil.api import MemoryMap -from pyquil.noise._channels import get_custom_gates_from_program from pyquil.noise._noise_model import NoiseModelLike from pyquil.quil import Program from pyquil.quilbase import Measurement, Reset, ResetQubit @@ -53,9 +52,11 @@ TrajectoryOp, adapt_for_density_matrix, adapt_for_trajectory, + build_dag, compressor_from_dag, + expand_program, linearizer_from_program, - resolver_from_program, + remap_qubits, ) from pyquil.transform import expand_defcircuits @@ -71,8 +72,7 @@ class ProgramSimulator: """Base class for program simulators. Handles all shared preprocessing: circuit expansion, qubit ordering, - building the linearizer, resolver, and compressor closures, and - inferring per-qudit dimensions. + building the linearizer, resolver, and compressor closures. Subclasses override :meth:`_validate` and :meth:`compute`. @@ -96,23 +96,32 @@ def __init__( qubits = sorted(expanded_program.get_qubit_indices()) self.qubits = qubits self.n_qubits = len(qubits) + + self._linearize_fn = linearizer_from_program(expanded_program) + + # Build resolver from the expanded program. + expanded_ops, phys_qubits = expand_program(program, noise_model) qubit_indices = {q: i for i, q in enumerate(qubits)} + mapped_qubits = remap_qubits(phys_qubits, qubit_indices) + dag = build_dag(mapped_qubits) - custom_gates = get_custom_gates_from_program(program) + frozen_ops = list(zip(expanded_ops, mapped_qubits)) - self._linearize_fn = linearizer_from_program(expanded_program) + def resolve(params: Array) -> list[ResolvedOp]: + return [(item(params) if callable(item) else item, subsystem) for item, subsystem in frozen_ops] - self._resolve_fn, dag, node_order = resolver_from_program( - program, - noise_model, - qubit_indices, - custom_gates or None, - ) + self.dims = (2,) * self.n_qubits + self._resolve_fn = resolve - # Dims are inferred during resolver construction from gate/channel inspection. - self.dims = self._resolve_fn.dims + # Derive barrier nodes: measurements (QuantumInstrument) should not + # be merged by the compressor. + barrier_nodes = { + i for i, op in enumerate(expanded_ops) if isinstance(op, qx.QuantumInstrument) + } - self._compress_fn = compressor_from_dag(dag, node_order, max_subsystem_size, dims=self.dims) + self._compress_fn = compressor_from_dag( + dag, max_subsystem_size, dims=self.dims, barrier_nodes=barrier_nodes, + ) # -- hook for subclass validation --------------------- diff --git a/test/unit/test_resolver.py b/test/unit/test_resolver.py new file mode 100644 index 000000000..38becc9fb --- /dev/null +++ b/test/unit/test_resolver.py @@ -0,0 +1,183 @@ +"""Unit tests for the resolver pipeline.""" + +import jax.numpy as jnp +import numpy as np +import pytest +import quax as qx + +from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, RZ, H, X +from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel +from pyquil.noise._noise_model import NoiseModel +from pyquil.quil import Program +from pyquil.quilatom import FormalArgument, MemoryReference, Qubit +from pyquil.quilbase import ( + Declare, + DefCircuit, + Gate, + Measurement, + Reset, + ResetQubit, +) +from pyquil.simulation._resolver import ( + _measure_registers, + build_dag, + expand_program, + remap_qubits, + resolver_from_program, +) + +_EMPTY_PARAMS = jnp.array([], dtype=float) + + +# ────────────────────────────────────────────────────────── +# expand_program +# ────────────────────────────────────────────────────────── + + +class TestExpandProgram: + def test_simple_gates(self): + p = Program(H(0), X(1), CNOT(0, 1)) + ops, qubit_tuples = expand_program(p) + assert len(ops) == 3 + assert len(qubit_tuples) == 3 + # Physical qubit IDs + assert qubit_tuples[0] == (0,) + assert qubit_tuples[1] == (1,) + assert qubit_tuples[2] == (0, 1) + + def test_fixed_gates_are_concrete(self): + p = Program(H(0), X(1)) + ops, _ = expand_program(p) + for op in ops: + assert isinstance(op, qx.Unitary) + + def test_measurement_emitted(self): + p = Program(Declare("ro", "BIT", 1), H(0), MEASURE(0, MemoryReference("ro", 0))) + ops, qubit_tuples = expand_program(p) + assert len(ops) == 2 + # Measurement should be a concrete QuantumInstrument + assert isinstance(ops[1], qx.QuantumInstrument) + + def test_reset_emitted(self): + p = Program(RESET(), H(0)) + ops, qubit_tuples = expand_program(p) + # Bare RESET expands to one reset per qubit (only qubit 0 in this program) + assert len(ops) == 2 + assert isinstance(ops[0], qx.SuperOp) + + def test_noise_channel_resolved(self): + p = Program(X(0)) + ch = Channel.from_gate_fidelity(inst=X(0), fidelity=0.99) + nm = NoiseModel(channels=[ch]) + ops, _ = expand_program(p, nm) + assert isinstance(ops[0], qx.SuperOp) + + def test_defcircuit_expansion_no_cycle_channel(self): + q0, q1 = FormalArgument("q0"), FormalArgument("q1") + dc = DefCircuit("MY_CYCLE", [], [q0, q1], [H(q0), CNOT(q0, q1)]) + p = Program(dc, Gate("MY_CYCLE", [], [Qubit(0), Qubit(1)])) + ops, qubit_tuples = expand_program(p) + assert len(ops) == 2 + + def test_cycle_channel_expansion(self): + """CycleChannel constituents are emitted instead of DEFCIRCUIT body.""" + q0 = FormalArgument("q") + dc = DefCircuit("SQC", [], [q0], [RX(0.1, q0), RZ(0.2, q0)]) + cycle_inst = Gate("SQC", [], [Qubit(0)]) + channels = tuple( + Channel.from_depolarizing_constant(inst, depolarizing_constant=0.99) for inst in (RX(0.1, 0), RZ(0.2, 0)) + ) + nm = NoiseModel(channels=[CycleChannel(inst=cycle_inst, defcircuit=dc, channels=channels)]) + p = Program(dc, cycle_inst) + ops, qubit_tuples = expand_program(p, nm) + assert len(ops) == 2 + # All should be concrete noisy SuperOps + for op in ops: + assert isinstance(op, qx.SuperOp) + + def test_parameterized_gate_produces_callable(self): + p = Program(Declare("theta", "REAL", 1), RZ(MemoryReference("theta", 0), 0)) + ops, _ = expand_program(p) + assert len(ops) == 1 + assert callable(ops[0]) + result = ops[0](jnp.array([1.23])) + assert isinstance(result, qx.Unitary) + + +# ────────────────────────────────────────────────────────── +# remap_qubits & build_dag +# ────────────────────────────────────────────────────────── + + +class TestRemapAndDag: + def test_remap_qubits(self): + qubit_tuples = [(3,), (5,), (3, 5)] + qubit_indices = {3: 0, 5: 1} + result = remap_qubits(qubit_tuples, qubit_indices) + assert result == [(0,), (1,), (0, 1)] + + def test_build_dag_single_qubit_chain(self): + qubit_tuples = [(0,), (0,), (0,)] + dag = build_dag(qubit_tuples) + assert dag.has_edge(0, 1) + assert dag.has_edge(1, 2) + assert not dag.has_edge(0, 2) + + def test_build_dag_independent_qubits(self): + qubit_tuples = [(0,), (1,)] + dag = build_dag(qubit_tuples) + assert dag.number_of_edges() == 0 + + def test_build_dag_multi_qubit(self): + qubit_tuples = [(0,), (1,), (0, 1)] + dag = build_dag(qubit_tuples) + assert dag.has_edge(0, 2) + assert dag.has_edge(1, 2) + assert not dag.has_edge(0, 1) + + +# ────────────────────────────────────────────────────────── +# resolver_from_program (integration) +# ────────────────────────────────────────────────────────── + + +class TestResolverFromProgram: + def test_basic_roundtrip(self): + p = Program(H(0), CNOT(0, 1), X(1)) + resolver, dag = resolver_from_program(p) + ops = resolver(_EMPTY_PARAMS) + assert len(ops) == 3 + assert all(isinstance(op, qx.Unitary) for op, _ in ops) + assert resolver.dims == (2, 2) + + def test_with_noise(self): + p = Program(X(0), H(1)) + ch = Channel.from_gate_fidelity(inst=X(0), fidelity=0.99) + nm = NoiseModel(channels=[ch]) + resolver, dag = resolver_from_program(p, nm) + ops = resolver(_EMPTY_PARAMS) + assert len(ops) == 2 + assert isinstance(ops[0][0], qx.SuperOp) + assert isinstance(ops[1][0], qx.Unitary) + + def test_parameterized(self): + p = Program(Declare("theta", "REAL", 1), RZ(MemoryReference("theta", 0), 0)) + resolver, _ = resolver_from_program(p) + params = jnp.array([np.pi / 4]) + ops = resolver(params) + assert len(ops) == 1 + assert isinstance(ops[0][0], qx.Unitary) + + def test_dag_structure(self): + p = Program(H(0), X(0), CNOT(0, 1)) + _, dag = resolver_from_program(p) + assert dag.has_edge(0, 1) + assert dag.has_edge(1, 2) + + def test_measurement_and_reset(self): + p = Program(Declare("ro", "BIT", 1), H(0), MEASURE(0, MemoryReference("ro", 0))) + resolver, _ = resolver_from_program(p) + ops = resolver(_EMPTY_PARAMS) + assert len(ops) == 2 + assert isinstance(ops[0][0], qx.Unitary) + assert isinstance(ops[1][0], qx.QuantumInstrument) From 597476b284e9e6d03fb10178e9f5c0377c58f656 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Tue, 26 May 2026 15:06:27 +0000 Subject: [PATCH 12/37] Update workflows --- .github/workflows/benchmark_base.yml | 4 ++-- .github/workflows/benchmark_pr.yml | 4 ++-- .github/workflows/publish.yml | 20 ++++++++-------- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 36 ++++++++++++++-------------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/benchmark_base.yml b/.github/workflows/benchmark_base.yml index 4c43695ea..8953f9190 100644 --- a/.github/workflows/benchmark_base.yml +++ b/.github/workflows/benchmark_base.yml @@ -9,11 +9,11 @@ on: jobs: benchmark_base_branch: name: Continuous Benchmarking with Bencher - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' - uses: actions/cache@v4 diff --git a/.github/workflows/benchmark_pr.yml b/.github/workflows/benchmark_pr.yml index 69d152eca..02dc229a2 100644 --- a/.github/workflows/benchmark_pr.yml +++ b/.github/workflows/benchmark_pr.yml @@ -8,11 +8,11 @@ jobs: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository permissions: pull-requests: write - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' - uses: actions/cache@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 29894491f..ed5f11b07 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,8 +11,8 @@ jobs: name: Build and Publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: snok/install-poetry@v1 @@ -44,8 +44,8 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: snok/install-poetry@v1 @@ -59,7 +59,7 @@ jobs: run: | poetry build --no-interaction - name: Upload wheels as artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: wheels path: dist @@ -82,7 +82,7 @@ jobs: # Determine the tags to publish based on the release tag - name: Docker Metadata id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | ${{ vars.DOCKER_IMAGE_NAME }} @@ -92,9 +92,9 @@ jobs: type=raw,value=${{ env.PYQUIL_TAG_RC }},enable=${{ env.PYQUIL_TAG_RC != '' }} # Checkout is needed to use the path context: . - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build and Test - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . load: true @@ -106,12 +106,12 @@ jobs: docker run --rm "${{ vars.DOCKER_IMAGE_NAME }}:test" python -c "from pyquil import get_qc" # Build and publish the image - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and Push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . push: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8b11958d..0b0ba567d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.PAT }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.PAT }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7059c8276..23717b885 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,9 +12,9 @@ jobs: name: Build and test documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.11 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/cache@v4 @@ -32,9 +32,9 @@ jobs: name: Check formatting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.11 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/cache@v4 @@ -51,9 +51,9 @@ jobs: name: Check style runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.11 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/cache@v4 @@ -70,9 +70,9 @@ jobs: name: Check types runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.11 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/cache@v4 @@ -90,7 +90,7 @@ jobs: name: Check dependencies for vulnerabilities runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 - name: Install OSV scanner @@ -107,9 +107,9 @@ jobs: matrix: python-version: ["3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v4 @@ -135,9 +135,9 @@ jobs: matrix: python-version: ["3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v4 @@ -165,9 +165,9 @@ jobs: name: Check docker image runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.11 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/cache@v4 @@ -180,7 +180,7 @@ jobs: . scripts/ci_install_deps poetry build -o wheels - name: Build and Test - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: file: Dockerfile.test context: . @@ -197,9 +197,9 @@ jobs: matrix: python-version: ["3.11", "3.12"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v4 From 9f376ac4574e964bf4b3eb2bdffb7bbcbf0b1f99 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Tue, 26 May 2026 15:16:43 +0000 Subject: [PATCH 13/37] Format and style --- pyquil/simulation/_resolver.py | 161 ++++++++++++------------ pyquil/simulation/_simulator.py | 82 +++++-------- pyquil/transform.py | 209 -------------------------------- test/unit/test_resolver.py | 16 +-- 4 files changed, 112 insertions(+), 356 deletions(-) delete mode 100644 pyquil/transform.py diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 3359680bc..125880ba3 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -17,32 +17,30 @@ This module provides the simulation preprocessing pipeline: -1. **Linearizer** — converts a ``MemoryMap`` into a flat JAX parameter vector. -2. **Expander** — expands a program into a flat list of operators and physical +1. **Expander** — expands a program into a flat list of operators and physical qubit tuples, resolving noise channels, custom gates, and DEFCIRCUIT bodies. Fixed (non-parameterized) operations are returned as concrete quax types; parameterized gates are returned as callables. -3. **Resolver** — converts a parameter vector into a list of +2. **Resolver** — converts a parameter vector into a list of ``(operator, subsystem)`` pairs using native quax types. -4. **Adapters** — convert resolved operations into the form expected by each +3. **Adapters** — convert resolved operations into the form expected by each simulator backend (``SuperOp`` for density matrices; ``Unitary``/``KrausMap``/ ``QuantumInstrument`` for state-vector trajectories). -5. **Compressor** — merges adjacent operators via greedy edge contraction. +4. **Compressor** — merges adjacent operators via greedy edge contraction. """ from __future__ import annotations import heapq import logging -from collections.abc import Callable -from typing import Any, cast +from collections.abc import Callable, Iterator +from copy import deepcopy +from typing import Any, TypeAlias, cast -import jax.numpy as jnp import networkx as nx import quax as qx from jax import Array -from pyquil.api import MemoryMap from pyquil.noise._channels import ( Channel, CycleChannel, @@ -55,9 +53,8 @@ NoiseModelLike, ) from pyquil.quil import Program -from pyquil.quilatom import MemoryReference +from pyquil.quilatom import MemoryReference, substitute from pyquil.quilbase import DefCircuit, Gate, Measurement, Reset, ResetQubit -from pyquil.transform import expand_defcircuit_body logger = logging.getLogger(__name__) @@ -66,11 +63,11 @@ # ────────────────────────────────────────────────────────── # A fixed (non-parameterized) operator — the most specific native quax type. -FixedOp = qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument +FixedOp: TypeAlias = qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument # An expanded item is either a fixed operator or a callable that resolves # parameters into a Unitary (only parameterized gates produce callables). -ExpandedOp = FixedOp | Callable[[Array], qx.Unitary] +ExpandedOp: TypeAlias = FixedOp | Callable[[Array], qx.Unitary] # Resolved operations retain the most specific native quax type. ResolvedOp = tuple[FixedOp, tuple[int, ...]] @@ -82,68 +79,51 @@ DensityMatrixOp = tuple[qx.SuperOp, tuple[int, ...]] -# ══════════════════════════════════════════════════════════ -# Linearizer -# ══════════════════════════════════════════════════════════ - - -class Linearizer: - """Converts a MemoryMap into a flat JAX parameter vector. - - Constructed via :func:`linearizer_from_program`. Call instances directly - to perform the conversion:: - - lin = linearizer_from_program(program) - params = lin(memory_map) - - :param n_params: The number of scalar parameters in the vector. - """ - - __slots__ = ("_linearize_fn", "n_params") - - def __init__(self, linearize_fn: Callable[[MemoryMap], Array], n_params: int) -> None: - self._linearize_fn = linearize_fn - self.n_params = n_params - - def __call__(self, memory_map: MemoryMap) -> Array: - return self._linearize_fn(memory_map) +# ────────────────────────────────────────────────────────── +# DEFCIRCUIT expansion +# ────────────────────────────────────────────────────────── -def linearizer_from_program(program: Program) -> Linearizer: - """Build a :class:`Linearizer` that converts a memory map to a flat JAX parameter vector. +def expand_defcircuit_body( + inst: Gate, + defcircuit: DefCircuit, + circuit_definitions: dict[str, DefCircuit], +) -> Iterator[Gate | Measurement | ResetQubit | Reset]: + """Yield concrete instructions from a DEFCIRCUIT invocation. - Walks the program to identify parameter registers (skipping ``"ro"`` and - any register that is the target of a ``MEASURE`` instruction). For each - gate parameter that is a :class:`MemoryReference`, records ``(name, offset)`` - in program order. + Substitutes formal qubit/parameter arguments with the concrete values + from ``inst``. Handles nested DEFCIRCUITs via recursion. - :param program: Expanded Quil program. - :return: A :class:`Linearizer` instance. + :param inst: The Gate that invokes the DEFCIRCUIT. + :param defcircuit: The DefCircuit definition to expand. + :param circuit_definitions: All known DEFCIRCUIT definitions (for nested expansion). + :yields: Concrete instructions with physical qubits and resolved parameters. """ - # Find registers written to by MEASURE — these are output registers, not params - measure_registers: set[str] = set() - for inst in program.instructions: - if isinstance(inst, Measurement): - cr = inst.classical_reg - if cr is not None: - measure_registers.add(cr.name) - - # Collect parameter references in program order - param_refs: list[tuple[str, int]] = [] - for inst in program.instructions: - if isinstance(inst, Gate): - for param in inst.params: - if isinstance(param, MemoryReference): - if param.name not in measure_registers: - param_refs.append((param.name, param.offset)) - - def linearize(memory_map: MemoryMap) -> Array: - if not param_refs: - return jnp.array([], dtype=float) - values = [float(memory_map[name][offset]) for name, offset in param_refs] - return jnp.array(values, dtype=float) - - return Linearizer(linearize, n_params=len(param_refs)) + qarg_to_arg_map = {qarg: q for q, qarg in zip(inst.qubits, defcircuit.qubit_variables, strict=False)} + parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters, strict=False)} + + for circuit_inst in defcircuit.instructions: + if isinstance(circuit_inst, Gate): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] # type: ignore[index,misc] + if hasattr(circuit_inst, "params"): + circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] # type: ignore[arg-type] + if circuit_inst.name in circuit_definitions: + yield from expand_defcircuit_body( + circuit_inst, circuit_definitions[circuit_inst.name], circuit_definitions + ) + else: + yield circuit_inst + elif isinstance(circuit_inst, Measurement): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] # type: ignore[index] + yield circuit_inst + elif isinstance(circuit_inst, ResetQubit): + circuit_inst = deepcopy(circuit_inst) + circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] # type: ignore[index] + yield circuit_inst + else: + yield deepcopy(circuit_inst) # type: ignore[misc] # ══════════════════════════════════════════════════════════ @@ -165,7 +145,7 @@ def _measure_registers(program: Program) -> set[str]: def expand_program( program: Program, noise_model: NoiseModelLike | None = None, -) -> tuple[list[ExpandedOp], list[tuple[int, ...]]]: +) -> tuple[list[ExpandedOp], list[tuple[int, ...]], list[tuple[str, int]]]: """Expand a program into operators and physical qubit tuples. Fixed (non-parameterized) operations are returned as concrete quax types @@ -185,9 +165,11 @@ def expand_program( :param program: Quil program (may contain DEFCIRCUITs). :param noise_model: Optional noise model. - :return: Tuple of ``(ops, qubit_tuples)`` where each op is either a - concrete quax operator or a ``Callable[[Array], Unitary]`` for - parameterized gates, and each qubit tuple contains physical qubit IDs. + :return: Tuple of ``(ops, qubit_tuples, param_refs)`` where each op is + either a concrete quax operator or a ``Callable[[Array], Unitary]`` + for parameterized gates, each qubit tuple contains physical qubit + IDs, and ``param_refs`` is a list of ``(register_name, offset)`` + pairs for each scalar parameter in program order. """ # Derive circuit definitions and custom gates from the program. circuit_definitions: dict[str, DefCircuit] = {} @@ -202,6 +184,7 @@ def expand_program( ops: list[ExpandedOp] = [] qubit_tuples: list[tuple[int, ...]] = [] + param_refs: list[tuple[str, int]] = [] param_counter = 0 def _emit_op(op: ExpandedOp, qubits: tuple[int, ...]) -> None: @@ -235,6 +218,7 @@ def _resolve_gate(inst: Gate) -> tuple[ExpandedOp, tuple[int, ...]]: for p in inst.params: if isinstance(p, MemoryReference) and p.name not in measure_regs: param_indices.append(param_counter) + param_refs.append((p.name, p.offset)) param_counter += 1 else: param_indices.append(-1) @@ -275,7 +259,7 @@ def _resolve_measurement(inst: Measurement) -> tuple[FixedOp, tuple[int, ...]]: def _resolve_reset_qubit(inst: ResetQubit) -> tuple[FixedOp, tuple[int, ...]]: """Resolve a targeted reset instruction.""" - qubits = tuple(inst.get_qubit_indices()) # type: ignore[union-attr] + qubits = tuple(inst.get_qubit_indices()) # type: ignore[arg-type] channel = noise_model.get_channel(inst) if noise_model is not None else None if isinstance(channel, ResetChannel): return channel.process, qubits @@ -311,14 +295,12 @@ def _emit_instruction(inst: Gate | Measurement | ResetQubit | Reset) -> None: _emit_op(sub_ch.process, sub_qubits) else: # Expand DEFCIRCUIT body and resolve each instruction. - for expanded_inst in expand_defcircuit_body( - inst, circuit_definitions[inst.name], circuit_definitions - ): + for expanded_inst in expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions): _emit_instruction(expanded_inst) elif isinstance(inst, (Gate, Measurement, ResetQubit, Reset)): _emit_instruction(inst) - return ops, qubit_tuples + return ops, qubit_tuples, param_refs # ══════════════════════════════════════════════════════════ @@ -349,7 +331,7 @@ def build_dag(qubit_tuples: list[tuple[int, ...]]) -> nx.DiGraph: :param qubit_tuples: Remapped qubit tuples (0-based indices). :return: DAG with node attribute ``"qubits"`` storing each node's qubit tuple. """ - dag = nx.DiGraph() + dag: nx.DiGraph = nx.DiGraph() last_on_qubit: dict[int, int] = {} for idx, qubits in enumerate(qubit_tuples): @@ -418,7 +400,7 @@ def resolver_from_program( :return: Tuple of ``(Resolver, dag)``. """ # Phase 1: Expand into flat operators + physical qubit tuples. - expanded_ops, phys_qubits = expand_program(program, noise_model) + expanded_ops, phys_qubits, _param_refs = expand_program(program, noise_model) # Phase 2: Remap physical qubits to 0-based indices. if qubits is None: @@ -430,10 +412,13 @@ def resolver_from_program( dag = build_dag(mapped_qubits) # Phase 4: Build the resolve closure. - frozen_ops = list(zip(expanded_ops, mapped_qubits)) + frozen_ops = list(zip(expanded_ops, mapped_qubits, strict=False)) def resolve(params: Array) -> list[ResolvedOp]: - return [(item(params) if callable(item) else item, subsystem) for item, subsystem in frozen_ops] + return [ + (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) + for item, subsystem in frozen_ops + ] n_qubits = len(qubits) dims = (2,) * n_qubits @@ -616,6 +601,7 @@ def compressor_from_dag( n_original = dag.number_of_nodes() if max_subsystem_size == 0 or n_original == 0: + def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: return ops @@ -662,9 +648,12 @@ def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: del group_qubits[old_root] # Re-enqueue edges from the newly merged group to its neighbours. - for neighbour in set(dag.successors(u_node)) | set(dag.predecessors(u_node)) | set( - dag.successors(v_node) - ) | set(dag.predecessors(v_node)): + for neighbour in ( + set(dag.successors(u_node)) + | set(dag.predecessors(u_node)) + | set(dag.successors(v_node)) + | set(dag.predecessors(v_node)) + ): rn = uf.find(neighbour) if rn == new_root or neighbour in barrier_nodes: continue diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 7ca409936..de8a43c96 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -32,9 +32,8 @@ from __future__ import annotations -import logging -import time -from typing import Any +from collections.abc import Callable +from typing import Any, cast import jax import jax.numpy as jnp @@ -55,13 +54,8 @@ build_dag, compressor_from_dag, expand_program, - linearizer_from_program, remap_qubits, ) -from pyquil.transform import expand_defcircuits - -logger = logging.getLogger(__name__) - # ══════════════════════════════════════════════════════════ # Base class @@ -87,40 +81,49 @@ def __init__( qubits: list[int] | None = None, *, noise_model: NoiseModelLike | None = None, - max_subsystem_size: int = 0, + max_subsystem_size: int = 2, ) -> None: - expanded_program = expand_defcircuits(program) - self._validate(expanded_program) + self._validate(program) if qubits is None: - qubits = sorted(expanded_program.get_qubit_indices()) + qubits = sorted(program.get_qubit_indices()) self.qubits = qubits self.n_qubits = len(qubits) - self._linearize_fn = linearizer_from_program(expanded_program) - # Build resolver from the expanded program. - expanded_ops, phys_qubits = expand_program(program, noise_model) + expanded_ops, phys_qubits, param_refs = expand_program(program, noise_model) qubit_indices = {q: i for i, q in enumerate(qubits)} mapped_qubits = remap_qubits(phys_qubits, qubit_indices) dag = build_dag(mapped_qubits) - frozen_ops = list(zip(expanded_ops, mapped_qubits)) + frozen_ops = list(zip(expanded_ops, mapped_qubits, strict=False)) def resolve(params: Array) -> list[ResolvedOp]: - return [(item(params) if callable(item) else item, subsystem) for item, subsystem in frozen_ops] + return [ + (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) + for item, subsystem in frozen_ops + ] + + # Build linearizer from parameter references discovered during expansion. + def linearize(memory_map: MemoryMap) -> Array: + if not param_refs: + return jnp.array([], dtype=float) + values = [float(memory_map[name][offset]) for name, offset in param_refs] + return jnp.array(values, dtype=float) self.dims = (2,) * self.n_qubits + self._linearize_fn = linearize self._resolve_fn = resolve # Derive barrier nodes: measurements (QuantumInstrument) should not # be merged by the compressor. - barrier_nodes = { - i for i, op in enumerate(expanded_ops) if isinstance(op, qx.QuantumInstrument) - } + barrier_nodes = {i for i, op in enumerate(expanded_ops) if isinstance(op, qx.QuantumInstrument)} self._compress_fn = compressor_from_dag( - dag, max_subsystem_size, dims=self.dims, barrier_nodes=barrier_nodes, + dag, + max_subsystem_size, + dims=self.dims, + barrier_nodes=barrier_nodes, ) # -- hook for subclass validation --------------------- @@ -170,7 +173,7 @@ def __init__( program: Program, qubits: list[int] | None = None, *, - max_subsystem_size: int = 0, + max_subsystem_size: int = 2, ) -> None: super().__init__(program, qubits, noise_model=None, max_subsystem_size=max_subsystem_size) self._psi0 = qx.zero_state_vector(dims=self.dims) @@ -247,7 +250,7 @@ def __init__( qubits: list[int] | None = None, *, noise_model: NoiseModelLike | None = None, - max_subsystem_size: int = 0, + max_subsystem_size: int = 2, ) -> None: super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) self._rho0 = qx.zero_state_matrix(dims=self.dims) @@ -307,7 +310,7 @@ def __init__( qubits: list[int] | None = None, *, noise_model: NoiseModelLike | None = None, - max_subsystem_size: int = 0, + max_subsystem_size: int = 2, kraus_truncation_threshold: float = 1e-6, devices: list[jax.Device] | None = None, ) -> None: @@ -442,14 +445,14 @@ def _apply_trajectory_operations( psi = qx.targeted_apply_unitary(op, psi, subsystem) case qx.KrausMap(): if per_traj_keys is not None: - op_keys = jax.vmap(lambda k: jax.random.fold_in(k, stochastic_idx))(per_traj_keys) + op_keys = jax.vmap(lambda k, s=stochastic_idx: jax.random.fold_in(k, s))(per_traj_keys) else: op_keys = jax.random.fold_in(key, stochastic_idx) psi = qx.targeted_apply_kraus_map_trajectory(op, psi, op_keys, subsystem) stochastic_idx += 1 case qx.QuantumInstrument(): if per_traj_keys is not None: - op_keys = jax.vmap(lambda k: jax.random.fold_in(k, stochastic_idx))(per_traj_keys) + op_keys = jax.vmap(lambda k, s=stochastic_idx: jax.random.fold_in(k, s))(per_traj_keys) else: op_keys = jax.random.fold_in(key, stochastic_idx) psi, outcome = qx.targeted_apply_instrument_to_state_vector(op, psi, op_keys, subsystem) @@ -508,8 +511,6 @@ def _run_batched_trajectories( all_outcomes: list[Array] = [] remaining = num_trajectories - batch_idx = 0 - t_total = 0.0 while remaining > 0: this_batch = min(remaining, batch_size) @@ -537,11 +538,8 @@ def _run_batched_trajectories( else: batch_keys = batch_key - t0 = time.perf_counter() psi_out, outcomes = _apply_trajectory_operations(operations, psi, batch_keys) psi_out.matrix.block_until_ready() - t1 = time.perf_counter() - t_total += t1 - t0 # Strip padding rows. if n_pad > 0: @@ -558,31 +556,9 @@ def _run_batched_trajectories( ) outcomes = outcomes[jnp.newaxis] - logger.debug( - "Batch %d: %d trajectories (%d padded), %d qubits, %d device(s), %.3f s", - batch_idx, - this_batch, - padded_batch, - n_qubits, - n_devices, - t1 - t0, - ) - if keep_states: all_psis.append(psi_out) all_outcomes.append(outcomes) remaining -= this_batch - batch_idx += 1 - - logger.info( - "Trajectories complete: %d total, %d batches (size=%d), n_qubits=%d, %d device(s), %.3f s total, %.1f traj/s", - num_trajectories, - batch_idx, - batch_size, - n_qubits, - n_devices, - t_total, - num_trajectories / t_total if t_total > 0 else float("inf"), - ) return (all_psis if keep_states else None), all_outcomes diff --git a/pyquil/transform.py b/pyquil/transform.py deleted file mode 100644 index 8f03df5f8..000000000 --- a/pyquil/transform.py +++ /dev/null @@ -1,209 +0,0 @@ -"""Utility functions for Quil program manipulation.""" - -from __future__ import annotations - -from collections.abc import Iterator -from copy import deepcopy - -from quil.instructions import CircuitDefinition -from quil.instructions import Instruction as QuilInstruction -from quil.program import Program as QuilProgram - -from pyquil.api import MemoryMap -from pyquil.quil import Program -from pyquil.quilatom import MemoryReference, substitute -from pyquil.quilbase import Declare, DefCircuit, Gate, Measurement, Reset, ResetQubit - - -def copy_everything_except_instructions( - program: Program, include_defcircuits: bool = True, include_kraus: bool = True -) -> Program: - """Create a new program with only the definitions of the input program. - - :param program: A pyQuil program. - :param include_defcircuits: If True, include DEFCIRCUIT definitions. - :param include_kraus: If True, include KRAUS definitions. - """ - from pyquil.quilbase import Pragma - - p = QuilProgram() - p.waveforms = program._program.waveforms - p.calibrations = program._program.calibrations - p.frames = program._program.frames - p.gate_definitions = program._program.gate_definitions - - program_definitions = Program() - program_definitions._program = p - - # Pragma externs are definitions - program_definitions += ( - [QuilInstruction.from_pragma(pragma) for pragma in program._program.pragma_extern_map.values()], - ) - - if include_defcircuits is True: - defcircuits = set() - for inst in program._program.to_instructions(): - if isinstance(inst.inner(), CircuitDefinition) and str(inst) not in defcircuits: - defcircuits.add(str(inst)) - program_definitions._program.add_instruction(inst) - - if include_kraus is True: - for kraus_inst in program.instructions: - if isinstance(kraus_inst, Pragma): - try: - if kraus_inst.command == "ADD-KRAUS": - program_definitions._program.add_instruction(kraus_inst) # type: ignore[arg-type] - except Exception: # noqa: S110 - pass - - return program_definitions - - -def unparameterize(program: Program, memory_map: MemoryMap) -> Program: - """Apply a memory map to a program, and evaluate any arithmetic. - - Memory declarations will be removed, except "ro". - - :param program: A pyquil program, possibly with parameters. - :param memory_map: A memory map, with values for the parameters. - """ - unparameterized_program = Program() - unparameterized_program += copy_everything_except_instructions(program) - instructions = program.instructions - parameter_substitution_map = {} - - if memory_map is not None: - parameter_substitution_map = { - MemoryReference(name=name, offset=offset, declared_size=len(value)) - if isinstance(name, str) - else name: value[offset] - for name, value in memory_map.items() - for offset in range(len(value)) - } - - for _idx, inst in enumerate(instructions): - if isinstance(inst, Declare): - if inst.name == "ro": - unparameterized_program += deepcopy(inst) - elif isinstance(inst, Gate): - if len(inst.params) > 0: - unparameterized_program += Gate( - name=inst.name, - params=[substitute(p, parameter_substitution_map) for p in inst.params], # type: ignore[arg-type] - qubits=inst.qubits, - ) - else: - unparameterized_program += inst - - else: - unparameterized_program += inst - - unparameterized_program.wrap_in_numshots_loop(program.num_shots) - - return unparameterized_program - - -def expand_defcircuit_body( - inst: Gate, - defcircuit: DefCircuit, - circuit_definitions: dict[str, DefCircuit], -) -> Iterator[Gate | Measurement | ResetQubit | Reset]: - """Yield concrete instructions from a DEFCIRCUIT invocation. - - Substitutes formal qubit/parameter arguments with the concrete values - from ``inst``. Handles nested DEFCIRCUITs via recursion. - - :param inst: The Gate that invokes the DEFCIRCUIT. - :param defcircuit: The DefCircuit definition to expand. - :param circuit_definitions: All known DEFCIRCUIT definitions (for nested expansion). - :yields: Concrete instructions with physical qubits and resolved parameters. - """ - qarg_to_arg_map = {qarg: q for q, qarg in zip(inst.qubits, defcircuit.qubit_variables, strict=False)} - parg_to_arg_map = {parg: param for param, parg in zip(inst.params, defcircuit.parameters, strict=False)} - - for circuit_inst in defcircuit.instructions: - if isinstance(circuit_inst, Gate): - circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubits = [qarg_to_arg_map[qarg] for qarg in circuit_inst.qubits] # type: ignore[index,misc] - if hasattr(circuit_inst, "params"): - circuit_inst.params = [substitute(param, parg_to_arg_map) for param in circuit_inst.params] # type: ignore[arg-type] - if circuit_inst.name in circuit_definitions: - yield from expand_defcircuit_body( - circuit_inst, circuit_definitions[circuit_inst.name], circuit_definitions - ) - else: - yield circuit_inst - elif isinstance(circuit_inst, Measurement): - circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] # type: ignore[index] - yield circuit_inst - elif isinstance(circuit_inst, ResetQubit): - circuit_inst = deepcopy(circuit_inst) - circuit_inst.qubit = qarg_to_arg_map[circuit_inst.qubit] # type: ignore[index] - yield circuit_inst - else: - yield deepcopy(circuit_inst) # type: ignore[misc] - - -def expand_defcircuits( - program: Program, - expand_if_defcal: bool = True, - calibration_program: Program | None = None, - keep_defcircuits: bool = False, -) -> Program: - """Expand DEFCIRCUITS into individual instructions. - - :param program: A Quil program, which may contain DefCircuits. - :param expand_if_defcal: Expand the defcircuit even if it has a defcalibration. - :param calibration_program: Calibrations to supplement those in ``program``. Existing - calibrations in ``program`` take precedence. - :param keep_defcircuits: If True, keep the DEFCIRCUIT definitions in the returned program. - :return: A Quil program, with any Circuit instructions expanded to individual instructions. - """ - instructions: list = [] - circuit_definitions: dict = {} - for inst in program.instructions: - if isinstance(inst, DefCircuit): - circuit_definitions[inst.name] = inst - if keep_defcircuits is True: - instructions.append(inst) - else: - instructions.append(inst) - - holistic_calibration_program = Program() - if calibration_program is not None: - holistic_calibration_program += calibration_program - holistic_calibration_program += copy_everything_except_instructions(program, include_defcircuits=False) - - expanded_program = Program() - expanded_program += holistic_calibration_program - - if len(circuit_definitions) == 0 and len(instructions) == 0: - return expanded_program - - def _should_expand(inst: Gate) -> bool: - name = inst.name - if name not in circuit_definitions: - return False - defcircuit = circuit_definitions[name] - qubits = tuple(int(q) for q in inst.get_qubit_indices()) - if len(qubits) != len(defcircuit.qubit_variables) or len(inst.params) != len(defcircuit.parameters): - return False - if expand_if_defcal is False: - if holistic_calibration_program.get_calibration(inst) is not None: - return False - if program.get_calibration(inst) is not None: - return False - return True - - expanded_instructions: list = [] - for inst in instructions: - if isinstance(inst, Gate) and _should_expand(inst): - expanded_instructions.extend( - expand_defcircuit_body(inst, circuit_definitions[inst.name], circuit_definitions) - ) - else: - expanded_instructions.append(inst) - - expanded_program += expanded_instructions - return expanded_program diff --git a/test/unit/test_resolver.py b/test/unit/test_resolver.py index 38becc9fb..9a04dc3a0 100644 --- a/test/unit/test_resolver.py +++ b/test/unit/test_resolver.py @@ -37,7 +37,7 @@ class TestExpandProgram: def test_simple_gates(self): p = Program(H(0), X(1), CNOT(0, 1)) - ops, qubit_tuples = expand_program(p) + ops, qubit_tuples, _ = expand_program(p) assert len(ops) == 3 assert len(qubit_tuples) == 3 # Physical qubit IDs @@ -47,20 +47,20 @@ def test_simple_gates(self): def test_fixed_gates_are_concrete(self): p = Program(H(0), X(1)) - ops, _ = expand_program(p) + ops, _, _ = expand_program(p) for op in ops: assert isinstance(op, qx.Unitary) def test_measurement_emitted(self): p = Program(Declare("ro", "BIT", 1), H(0), MEASURE(0, MemoryReference("ro", 0))) - ops, qubit_tuples = expand_program(p) + ops, qubit_tuples, _ = expand_program(p) assert len(ops) == 2 # Measurement should be a concrete QuantumInstrument assert isinstance(ops[1], qx.QuantumInstrument) def test_reset_emitted(self): p = Program(RESET(), H(0)) - ops, qubit_tuples = expand_program(p) + ops, qubit_tuples, _ = expand_program(p) # Bare RESET expands to one reset per qubit (only qubit 0 in this program) assert len(ops) == 2 assert isinstance(ops[0], qx.SuperOp) @@ -69,14 +69,14 @@ def test_noise_channel_resolved(self): p = Program(X(0)) ch = Channel.from_gate_fidelity(inst=X(0), fidelity=0.99) nm = NoiseModel(channels=[ch]) - ops, _ = expand_program(p, nm) + ops, _, _ = expand_program(p, nm) assert isinstance(ops[0], qx.SuperOp) def test_defcircuit_expansion_no_cycle_channel(self): q0, q1 = FormalArgument("q0"), FormalArgument("q1") dc = DefCircuit("MY_CYCLE", [], [q0, q1], [H(q0), CNOT(q0, q1)]) p = Program(dc, Gate("MY_CYCLE", [], [Qubit(0), Qubit(1)])) - ops, qubit_tuples = expand_program(p) + ops, qubit_tuples, _ = expand_program(p) assert len(ops) == 2 def test_cycle_channel_expansion(self): @@ -89,7 +89,7 @@ def test_cycle_channel_expansion(self): ) nm = NoiseModel(channels=[CycleChannel(inst=cycle_inst, defcircuit=dc, channels=channels)]) p = Program(dc, cycle_inst) - ops, qubit_tuples = expand_program(p, nm) + ops, qubit_tuples, _ = expand_program(p, nm) assert len(ops) == 2 # All should be concrete noisy SuperOps for op in ops: @@ -97,7 +97,7 @@ def test_cycle_channel_expansion(self): def test_parameterized_gate_produces_callable(self): p = Program(Declare("theta", "REAL", 1), RZ(MemoryReference("theta", 0), 0)) - ops, _ = expand_program(p) + ops, _, _ = expand_program(p) assert len(ops) == 1 assert callable(ops[0]) result = ops[0](jnp.array([1.23])) From 390474964ec217f7a73a2e1c248a293727d5e9bd Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sun, 31 May 2026 16:23:28 +0000 Subject: [PATCH 14/37] Use scan --- pyquil/simulation/_simulator.py | 316 +++++++++++++++++++++------ test/benchmarks/test_state_vector.py | 92 ++++++++ 2 files changed, 347 insertions(+), 61 deletions(-) diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index de8a43c96..0d65e1b89 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -32,6 +32,7 @@ from __future__ import annotations +import math from collections.abc import Callable from typing import Any, cast @@ -41,6 +42,7 @@ import quax as qx from jax import Array from jax.sharding import Mesh, NamedSharding, PartitionSpec +from quax._apply import _sample_kraus_map_trajectory from pyquil.api import MemoryMap from pyquil.noise._noise_model import NoiseModelLike @@ -57,6 +59,62 @@ remap_qubits, ) + +def _pad_matrix(mat: Array, *target: int) -> Array: + """Zero-pad the trailing dimensions of *mat* up to *target* sizes. + + Only the last ``len(target)`` axes are padded (top-left aligned); any + leading (ensemble/stack) axes are left untouched. + """ + if all(mat.shape[-len(target) + i] == t for i, t in enumerate(target)): + return mat + pad = [(0, 0)] * (mat.ndim - len(target)) + [ + (0, t - mat.shape[mat.ndim - len(target) + i]) for i, t in enumerate(target) + ] + return jnp.pad(mat, pad) + + +def _make_unitary_branch( + base: tuple[int, ...], + base_dims: tuple[int, ...], + db: int, +) -> Callable[[Array, qx.StateVector], qx.StateVector]: + """Build a ``jax.lax.switch`` branch that applies a unitary on *base*.""" + + def branch(op_mat: Array, psi: qx.StateVector) -> qx.StateVector: + unitary = qx.Unitary.from_matrix(op_mat[:db, :db], (base_dims, base_dims)) + return qx.targeted_apply_unitary(unitary, psi, base) + + return branch + + +def _make_superop_branch( + base: tuple[int, ...], + base_dims: tuple[int, ...], + db2: int, +) -> Callable[[Array, qx.DensityMatrix], qx.DensityMatrix]: + """Build a ``jax.lax.switch`` branch that applies a superoperator on *base*.""" + + def branch(op_mat: Array, rho: qx.DensityMatrix) -> qx.DensityMatrix: + superop = qx.SuperOp.from_matrix(op_mat[:db2, :db2], (base_dims, base_dims)) + return qx.targeted_apply_superop(superop, rho, base) + + return branch + + +def _make_kraus_trajectory_branch( + base: tuple[int, ...], + base_dims: tuple[int, ...], + db: int, +) -> Callable[[Array, qx.StateVector, Array], tuple[qx.StateVector, Array]]: + """Build a ``jax.lax.switch`` branch that samples a Kraus trajectory on *base*.""" + + def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: + kraus_map = qx.KrausMap.from_matrix(op_mat[:, :db, :db], (base_dims, base_dims)) + return _sample_kraus_map_trajectory(kraus_map, psi, key, base) + + return branch + # ══════════════════════════════════════════════════════════ # Base class # ══════════════════════════════════════════════════════════ @@ -73,7 +131,8 @@ class ProgramSimulator: Instances are immutable after construction. """ - __slots__ = ("n_qubits", "qubits", "dims", "_linearize_fn", "_resolve_fn", "_compress_fn") + __slots__ = ("n_qubits", "qubits", "dims", "_linearize_fn", "_resolve_fn", "_compress_fn", + "bases", "op_index", "base_dims", "base_total_dim", "d_max") def __init__( self, @@ -126,6 +185,28 @@ def linearize(memory_map: MemoryMap) -> Array: barrier_nodes=barrier_nodes, ) + # Enumerate the *base subsystems* produced by the compressor. The merge + # structure depends only on the DAG (not on parameter values), so a + # structural probe with zero parameters yields exactly the subsystem + # sequence that ``compress`` will produce for any parameters. The + # lax-loop ``compute`` methods dispatch each compressed operation through + # a ``jax.lax.switch`` keyed by its base, so the number of distinct bases + # (rather than the number of operations) determines the size of the + # traced/compiled graph. + probe = self._compress_fn(self._resolve_fn(jnp.zeros(len(param_refs)))) + self.bases = [] + sub_to_branch: dict[tuple[int, ...], int] = {} + op_index: list[int] = [] + for _, subsystem in probe: + if subsystem not in sub_to_branch: + sub_to_branch[subsystem] = len(self.bases) + self.bases.append(subsystem) + op_index.append(sub_to_branch[subsystem]) + self.op_index = tuple(op_index) + self.base_dims = [tuple(self.dims[q] for q in base) for base in self.bases] + self.base_total_dim = [math.prod(d) for d in self.base_dims] + self.d_max = max(self.base_total_dim) if self.base_total_dim else 1 + # -- hook for subclass validation --------------------- def _validate(self, program: Program) -> None: @@ -166,7 +247,7 @@ class PureStateVectorSimulator(ProgramSimulator): U = jax.jit(sim.unitary)(params) """ - __slots__ = ("_psi0",) + __slots__ = ("_psi0", "_branches", "_idx_arr") def __init__( self, @@ -177,6 +258,11 @@ def __init__( ) -> None: super().__init__(program, qubits, noise_model=None, max_subsystem_size=max_subsystem_size) self._psi0 = qx.zero_state_vector(dims=self.dims) + self._branches = [ + _make_unitary_branch(base, base_dims, db) + for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) + ] + self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) def _validate(self, program: Program) -> None: for inst in program.instructions: @@ -185,19 +271,40 @@ def _validate(self, program: Program) -> None: if isinstance(inst, (Reset, ResetQubit)): raise ValueError(f"PureStateVectorSimulator does not support resets. Found: {inst}") + def _stack_unitaries(self, resolved: list[ResolvedOp]) -> Array: + """Compress, then stack each gate's matrix into ``(N, d_max, d_max)``.""" + compressed = self.compress(resolved) + mats = [_pad_matrix(cast(qx.Unitary, op).matrix, self.d_max, self.d_max) for op, _ in compressed] + return jnp.stack(mats, axis=0) + def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] """Compute the final state vector. + Operators are stacked into a single array and applied with a + :func:`jax.lax.scan` whose body dispatches each operator to the right + base subsystem via :func:`jax.lax.switch`. This keeps the traced graph + size proportional to the number of distinct base subsystems rather than + the number of operations, dramatically reducing JIT compilation time + for large programs. + :param params: Flat parameter vector from :meth:`linearize`. :return: The final state vector. """ resolved = self.resolve(params) - compressed = self.compress(resolved) - psi = self._psi0 - for unitary, subsystem in compressed: - psi = qx.targeted_apply_unitary(unitary, psi, subsystem) + if not resolved: + return self._psi0 + op_stack = self._stack_unitaries(resolved) + branches = self._branches + + def body(psi: qx.StateVector, xs: tuple[Array, Array]) -> tuple[qx.StateVector, None]: + op_mat, sidx = xs + psi = jax.lax.switch(sidx, branches, op_mat, psi) + return psi, None + + psi, _ = jax.lax.scan(body, self._psi0, (op_stack, self._idx_arr)) return psi + def __call__(self, params: Array) -> qx.StateVector: return self.compute(params) @@ -242,7 +349,7 @@ class DensityMatrixSimulator(ProgramSimulator): rho = jax.jit(sim.compute)(params) """ - __slots__ = ("_rho0",) + __slots__ = ("_rho0", "_branches", "_idx_arr") def __init__( self, @@ -254,19 +361,43 @@ def __init__( ) -> None: super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) self._rho0 = qx.zero_state_matrix(dims=self.dims) + self._branches = [ + _make_superop_branch(base, base_dims, db * db) + for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) + ] + self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) + + def _stack_superops(self, resolved: list[ResolvedOp]) -> Array: + """Compress, promote each op to a SuperOp, and stack.""" + compressed = self.compress(resolved) + superops = adapt_for_density_matrix(compressed) + d_max2 = self.d_max * self.d_max + mats = [_pad_matrix(superop.matrix, d_max2, d_max2) for superop, _ in superops] + return jnp.stack(mats, axis=0) def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] """Compute the final density matrix. + Superoperators are stacked and applied with a :func:`jax.lax.scan` + whose body dispatches to the correct base subsystem via + :func:`jax.lax.switch`, keeping the compiled graph size proportional to + the number of distinct base subsystems. + :param params: Flat parameter vector from :meth:`linearize`. :return: The final density matrix. """ resolved = self.resolve(params) - compressed = self.compress(resolved) - operations = adapt_for_density_matrix(compressed) - rho = self._rho0 - for superop, subsystem in operations: - rho = qx.targeted_apply_superop(superop, rho, subsystem) + if not resolved: + return self._rho0 + op_stack = self._stack_superops(resolved) + branches = self._branches + + def body(rho: qx.DensityMatrix, xs: tuple[Array, Array]) -> tuple[qx.DensityMatrix, None]: + op_mat, sidx = xs + rho = jax.lax.switch(sidx, branches, op_mat, rho) + return rho, None + + rho, _ = jax.lax.scan(body, self._rho0, (op_stack, self._idx_arr)) return rho def __call__(self, params: Array) -> qx.DensityMatrix: @@ -395,76 +526,139 @@ def sample( # ══════════════════════════════════════════════════════════ +def _op_to_kraus_matrix( + op: qx.Unitary | qx.KrausMap | qx.QuantumInstrument, +) -> tuple[Array, int, bool]: + """Convert a single trajectory operator to a padded Kraus matrix. + + Every trajectory operator is expressed as a Kraus map so that a single, + uniform ``jax.lax.switch`` branch (Kraus trajectory sampling) can handle + all operation types: + + - ``qx.Unitary`` → a one-operator Kraus map. + - ``qx.KrausMap`` → itself. + - ``qx.QuantumInstrument`` → its outcome and Kraus axes are merged into a + single Kraus axis (replicating the flattening in + :func:`quax.targeted_apply_instrument_to_state_vector`). The returned + *divisor* is the number of Kraus operators per outcome, so the sampled + Kraus index ``k`` decodes to the measurement outcome ``k // divisor``. + + :param op: The operator (already acting on its base subsystem). + :return: ``(matrix, divisor, is_measurement)`` where ``matrix`` has shape + ``(n_kraus, d, d)``. + """ + match op: + case qx.Unitary(): + return op.matrix[jnp.newaxis, :, :], 1, False + case qx.KrausMap(): + return op.matrix, 1, False + case qx.QuantumInstrument(): + kraus_map = qx.superop_to_kraus(qx.SuperOp(op.data, op.num_qubits)) + data = kraus_map.data + n_ens_i = len(op.ensemble_size) + shape = data.shape + n_kraus_per_outcome = shape[n_ens_i + 1] + n_total_kraus = op.num_outcomes * n_kraus_per_outcome + data = data.reshape(shape[:n_ens_i] + (n_total_kraus,) + shape[n_ens_i + 2 :]) + merged = qx.KrausMap(data=data, num_qubits=kraus_map.num_qubits) + return merged.matrix, n_kraus_per_outcome, True + case _: + raise TypeError(f"Unsupported operator type: {type(op)}") + + def _apply_trajectory_operations( operations: list[TrajectoryOp], psi: qx.StateVector, key: Array, ) -> tuple[qx.StateVector, Array]: - """Apply trajectory operations to a (batched) state vector. + """Apply trajectory operations to a (batched) state vector via a JAX loop. - Dispatches each operation by type: + Every operator is converted to a (zero-padded) Kraus map and stacked into a + single array. A :func:`jax.lax.fori_loop` then iterates over the stack, + dispatching each operator to the correct base subsystem with a + :func:`jax.lax.switch`. Because only one loop body and one switch branch + per distinct subsystem are traced, the compiled graph size scales with the + number of distinct subsystems rather than the number of operations. - - ``qx.Unitary``: deterministic gate application - - ``qx.KrausMap``: probabilistic Kraus operator sampling - - ``qx.QuantumInstrument``: measurement with outcome recording + Measurements are handled uniformly: a quantum instrument is flattened so + that sampling a Kraus index also selects an outcome (``index // divisor``). + Zero-padded Kraus operators have zero Born probability and are therefore + never sampled. - Key generation is sharding-friendly: per-operation keys are derived - lazily via ``jax.random.fold_in`` so that the key array is never - materialised in full on a single device. + Key generation is sharding-friendly: per-operation keys are derived lazily + via ``jax.random.fold_in`` so the key array is never materialised in full. - :param operations: Ordered list of (operator, subsystem) pairs. + :param operations: Ordered list of ``(operator, subsystem)`` pairs. :param psi: Initial state vector, optionally batched via ensemble dimension. - :param key: JAX PRNG key (scalar typed key). Will be split internally to - produce per-trajectory, per-operation sub-keys. + :param key: JAX PRNG key (scalar) or per-trajectory key vector. :return: Tuple of ``(final_state_vector, measurement_outcomes)`` where measurement_outcomes has shape ``(*ensemble, n_measurements)`` with dtype int32. """ - measurement_outcomes: list[Array] = [] - ensemble_size = psi.ensemble_size - # Derive per-trajectory base keys once. When the state is sharded - # across devices the resulting key array inherits the same sharding, - # so each device only materialises its own slice. + if not operations: + return psi, jnp.empty((*ensemble_size, 0), dtype=jnp.int32) + + # 1. Enumerate distinct subsystems → one switch branch each. + distinct_subsystems: list[tuple[int, ...]] = [] + sub_to_branch: dict[tuple[int, ...], int] = {} + for _, subsystem in operations: + if subsystem not in sub_to_branch: + sub_to_branch[subsystem] = len(distinct_subsystems) + distinct_subsystems.append(subsystem) + + branches = [ + _make_kraus_trajectory_branch( + subsystem, + tuple(psi.dims[q] for q in subsystem), + math.prod(psi.dims[q] for q in subsystem), + ) + for subsystem in distinct_subsystems + ] + + # 2. Convert every operator to a padded Kraus matrix and stack. + kraus_mats: list[Array] = [] + divisors: list[int] = [] + measure_positions: list[int] = [] + branch_index: list[int] = [] + for i, (op, subsystem) in enumerate(operations): + mat, divisor, is_measure = _op_to_kraus_matrix(op) + kraus_mats.append(mat) + divisors.append(divisor) + branch_index.append(sub_to_branch[subsystem]) + if is_measure: + measure_positions.append(i) + + max_k = max(mat.shape[0] for mat in kraus_mats) + d_max = max(mat.shape[-1] for mat in kraus_mats) + op_stack = jnp.stack([_pad_matrix(mat, max_k, d_max, d_max) for mat in kraus_mats], axis=0) + branch_arr = jnp.asarray(branch_index, dtype=jnp.int32) + + # 3. Per-trajectory base keys. if ensemble_size: - if key.ndim > 0: - # Already per-trajectory keys (e.g. from multi-device sharding - # or batched ``compute()``). - per_traj_keys = key - else: - per_traj_keys = jax.random.split(key, ensemble_size[0]) + per_traj_keys = key if key.ndim > 0 else jax.random.split(key, ensemble_size[0]) else: per_traj_keys = None - stochastic_idx = 0 - - for op, subsystem in operations: - match op: - case qx.Unitary(): - psi = qx.targeted_apply_unitary(op, psi, subsystem) - case qx.KrausMap(): - if per_traj_keys is not None: - op_keys = jax.vmap(lambda k, s=stochastic_idx: jax.random.fold_in(k, s))(per_traj_keys) - else: - op_keys = jax.random.fold_in(key, stochastic_idx) - psi = qx.targeted_apply_kraus_map_trajectory(op, psi, op_keys, subsystem) - stochastic_idx += 1 - case qx.QuantumInstrument(): - if per_traj_keys is not None: - op_keys = jax.vmap(lambda k, s=stochastic_idx: jax.random.fold_in(k, s))(per_traj_keys) - else: - op_keys = jax.random.fold_in(key, stochastic_idx) - psi, outcome = qx.targeted_apply_instrument_to_state_vector(op, psi, op_keys, subsystem) - measurement_outcomes.append(outcome) - stochastic_idx += 1 - case _: - raise TypeError(f"Unsupported operator type: {type(op)}") - - if measurement_outcomes: - outcomes = jnp.stack(measurement_outcomes, axis=-1) + n_ops = len(operations) + sampled_init = jnp.zeros((n_ops, *ensemble_size), dtype=jnp.int32) + + def body(i: Array, carry: tuple[qx.StateVector, Array]) -> tuple[qx.StateVector, Array]: + psi_c, sampled = carry + if per_traj_keys is not None: + op_key = jax.vmap(lambda k: jax.random.fold_in(k, i))(per_traj_keys) + else: + op_key = jax.random.fold_in(key, i) + psi_c, sampled_idx = jax.lax.switch(branch_arr[i], branches, op_stack[i], psi_c, op_key) + return psi_c, sampled.at[i].set(sampled_idx.astype(jnp.int32)) + + psi, sampled = jax.lax.fori_loop(0, n_ops, body, (psi, sampled_init)) + + if measure_positions: + outcomes = jnp.stack([sampled[p] // divisors[p] for p in measure_positions], axis=-1) else: - outcomes = jnp.empty((*psi.ensemble_size, 0), dtype=jnp.int32) + outcomes = jnp.empty((*ensemble_size, 0), dtype=jnp.int32) return psi, outcomes diff --git a/test/benchmarks/test_state_vector.py b/test/benchmarks/test_state_vector.py index 82a058896..76fa35775 100644 --- a/test/benchmarks/test_state_vector.py +++ b/test/benchmarks/test_state_vector.py @@ -19,6 +19,8 @@ from pyquil.quilbase import Measurement as QuilMeasurement from pyquil.quilbase import Reset as QuilReset from pyquil.simulation._simulator import ( + DensityMatrixSimulator, + PureStateVectorSimulator, TrajectorySimulator, ) from pyquil.simulation._simulator import ( @@ -418,3 +420,93 @@ def test_surface17_depth5_cycle_noise_no_measurements_micro(self, benchmark): def test_surface17_depth5_cycle_noise_measurements_only_micro(self, benchmark): _run_surface17_benchmark(benchmark, variant="measurements_only", num_trajectories=4, batch_size=4) + + +def _build_gate_program(num_qubits, num_layers, seed=4867): + """Build a layered gate-only (noise-free) brickwork circuit.""" + rng = np.random.default_rng(seed) + program = Program() + for _ in range(num_layers): + for q in range(num_qubits): + program += RX(rng.uniform(-np.pi, np.pi), q) + program += RZ(rng.uniform(-np.pi, np.pi), q) + for q in range(0, num_qubits - 1, 2): + program += CNOT(q, q + 1) + for q in range(1, num_qubits - 1, 2): + program += CNOT(q, q + 1) + return program + + +def _benchmark_compile_time(benchmark, sim, params, extra=None): + """Benchmark the JIT compile time of ``sim.compute``. + + A fresh ``jax.jit`` wrapper is created on every round so the XLA compilation + cache is bypassed and the full lower+compile cost is measured each time. + """ + if hasattr(benchmark, "extra_info") and extra: + benchmark.extra_info.update(extra) + + def thunk(): + return jax.jit(lambda p: sim.compute(p)).lower(params).compile() + + benchmark.pedantic(thunk, iterations=1, rounds=1) + + +class TestJitCompileTime: + """JIT compile-time benchmarks for the lax-loop simulators. + + These measure how compilation time scales with program depth. The lax-loop + ``compute`` traces a single loop body plus one switch branch per distinct + base subsystem, so the compiled graph size is bounded by the number of + distinct subsystems rather than the number of operations. + """ + + @pytest.mark.parametrize( + "num_layers", + [ + pytest.param(10, id="10L"), + pytest.param(40, id="40L"), + pytest.param(80, id="80L"), + ], + ) + def test_state_vector_compile_depth(self, benchmark, num_layers): + num_qubits = 10 + program = _build_gate_program(num_qubits, num_layers) + sim = PureStateVectorSimulator(program, qubits=list(range(num_qubits))) + params = sim.linearize({}) + _benchmark_compile_time( + benchmark, + sim, + params, + extra={ + "num_qubits": num_qubits, + "num_layers": num_layers, + "num_ops": len(program.instructions), + "num_bases": len(sim.bases), + }, + ) + + @pytest.mark.parametrize( + "num_layers", + [ + pytest.param(5, id="5L"), + pytest.param(20, id="20L"), + pytest.param(40, id="40L"), + ], + ) + def test_density_matrix_compile_depth(self, benchmark, num_layers): + num_qubits = 6 + program = _build_gate_program(num_qubits, num_layers) + sim = DensityMatrixSimulator(program, qubits=list(range(num_qubits))) + params = sim.linearize({}) + _benchmark_compile_time( + benchmark, + sim, + params, + extra={ + "num_qubits": num_qubits, + "num_layers": num_layers, + "num_ops": len(program.instructions), + "num_bases": len(sim.bases), + }, + ) From d56c2b48ab88746ed10b539adcf0444b9c6fe0ed Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sun, 31 May 2026 16:49:11 +0000 Subject: [PATCH 15/37] Improve compile time --- pyquil/simulation/_simulator.py | 54 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 0d65e1b89..a9d87ea67 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -132,7 +132,7 @@ class ProgramSimulator: """ __slots__ = ("n_qubits", "qubits", "dims", "_linearize_fn", "_resolve_fn", "_compress_fn", - "bases", "op_index", "base_dims", "base_total_dim", "d_max") + "bases", "op_index", "base_dims", "base_total_dim", "d_max", "_has_params") def __init__( self, @@ -174,6 +174,13 @@ def linearize(memory_map: MemoryMap) -> Array: self._linearize_fn = linearize self._resolve_fn = resolve + # Whether any gate matrix depends on a runtime parameter. When it does + # not, the compressed operator stack is a compile-time constant and can + # be materialised eagerly (outside the traced graph), which avoids XLA + # constant-folding/autotuning a large ``compose_operator`` subgraph — the + # dominant JIT cost on accelerators for deep, literal-angle programs. + self._has_params = bool(param_refs) + # Derive barrier nodes: measurements (QuantumInstrument) should not # be merged by the compressor. barrier_nodes = {i for i, op in enumerate(expanded_ops) if isinstance(op, qx.QuantumInstrument)} @@ -247,7 +254,7 @@ class PureStateVectorSimulator(ProgramSimulator): U = jax.jit(sim.unitary)(params) """ - __slots__ = ("_psi0", "_branches", "_idx_arr") + __slots__ = ("_psi0", "_branches", "_idx_arr", "_const_op_stack") def __init__( self, @@ -264,6 +271,16 @@ def __init__( ] self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) + # For programs without runtime parameters the operator stack is constant. + # Build it eagerly once so the traced ``compute`` graph contains only the + # scan over a small concrete array, not the (constant-folded, autotuned) + # ``compress``/``compose_operator`` construction. + self._const_op_stack: Array | None = None + if not self._has_params and self.op_index: + self._const_op_stack = jax.block_until_ready( + self._stack_unitaries(self.resolve(jnp.zeros(0))) + ) + def _validate(self, program: Program) -> None: for inst in program.instructions: if isinstance(inst, Measurement): @@ -290,10 +307,13 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] :param params: Flat parameter vector from :meth:`linearize`. :return: The final state vector. """ - resolved = self.resolve(params) - if not resolved: - return self._psi0 - op_stack = self._stack_unitaries(resolved) + if self._const_op_stack is not None: + op_stack = self._const_op_stack + else: + resolved = self.resolve(params) + if not resolved: + return self._psi0 + op_stack = self._stack_unitaries(resolved) branches = self._branches def body(psi: qx.StateVector, xs: tuple[Array, Array]) -> tuple[qx.StateVector, None]: @@ -349,7 +369,7 @@ class DensityMatrixSimulator(ProgramSimulator): rho = jax.jit(sim.compute)(params) """ - __slots__ = ("_rho0", "_branches", "_idx_arr") + __slots__ = ("_rho0", "_branches", "_idx_arr", "_const_op_stack") def __init__( self, @@ -367,6 +387,15 @@ def __init__( ] self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) + # See :class:`PureStateVectorSimulator`: for parameter-free programs the + # superoperator stack is constant, so build it eagerly to keep the traced + # graph to just the scan over a concrete array. + self._const_op_stack: Array | None = None + if not self._has_params and self.op_index: + self._const_op_stack = jax.block_until_ready( + self._stack_superops(self.resolve(jnp.zeros(0))) + ) + def _stack_superops(self, resolved: list[ResolvedOp]) -> Array: """Compress, promote each op to a SuperOp, and stack.""" compressed = self.compress(resolved) @@ -386,10 +415,13 @@ def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] :param params: Flat parameter vector from :meth:`linearize`. :return: The final density matrix. """ - resolved = self.resolve(params) - if not resolved: - return self._rho0 - op_stack = self._stack_superops(resolved) + if self._const_op_stack is not None: + op_stack = self._const_op_stack + else: + resolved = self.resolve(params) + if not resolved: + return self._rho0 + op_stack = self._stack_superops(resolved) branches = self._branches def body(rho: qx.DensityMatrix, xs: tuple[Array, Array]) -> tuple[qx.DensityMatrix, None]: From efde04b0b881c972392fc707e0f5666a1011fbb8 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sun, 31 May 2026 18:47:02 +0000 Subject: [PATCH 16/37] Improve efficiency of _resolver --- pyquil/simulation/_resolver.py | 71 +++++++++----- pyquil/simulation/_simulator.py | 166 +++++++++++++++++++++++++++++--- 2 files changed, 197 insertions(+), 40 deletions(-) diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 125880ba3..ed84b20c3 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -65,9 +65,48 @@ # A fixed (non-parameterized) operator — the most specific native quax type. FixedOp: TypeAlias = qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument -# An expanded item is either a fixed operator or a callable that resolves -# parameters into a Unitary (only parameterized gates produce callables). -ExpandedOp: TypeAlias = FixedOp | Callable[[Array], qx.Unitary] + +class ParametricGate: + """A parametric gate whose matrix depends on runtime parameters. + + Instances are callable: ``gate(params) -> qx.Unitary``. They also expose + the gate constructor and parameter layout so that the simulator can group + gates by type and use ``jax.vmap`` for efficient batch construction. + """ + + __slots__ = ("gate_fn", "param_indices", "concrete_values") + + def __init__( + self, + gate_fn: Callable[..., qx.Unitary], + param_indices: tuple[int, ...], + concrete_values: tuple[float, ...], + ) -> None: + #: The quax gate constructor (e.g. ``qx.gates.RX``). + self.gate_fn = gate_fn + #: Per-argument index into the flat parameter vector, or ``-1`` + #: when that argument is a compile-time constant. + self.param_indices = param_indices + #: Per-argument concrete value (``nan`` for runtime-parametric slots). + self.concrete_values = concrete_values + + def __call__(self, params: Array) -> qx.Unitary: + resolved: list[Any] = [] + for pi, cv in zip(self.param_indices, self.concrete_values): + if pi >= 0: + resolved.append(params[pi]) + else: + resolved.append(cv) + result = self.gate_fn(*resolved) + if not isinstance(result, qx.Unitary): + result = cast(Any, result) + result = qx.Unitary.from_matrix(result.matrix, result.dims) + return result + + +# An expanded item is either a fixed operator or a ParametricGate that +# resolves parameters into a Unitary. +ExpandedOp: TypeAlias = FixedOp | ParametricGate # Resolved operations retain the most specific native quax type. ResolvedOp = tuple[FixedOp, tuple[int, ...]] @@ -214,36 +253,18 @@ def _resolve_gate(inst: Gate) -> tuple[ExpandedOp, tuple[int, ...]]: raise KeyError(f"Unknown gate '{gate_name}'.") param_indices: list[int] = [] - concrete_params = list(inst.params) + concrete_values: list[float] = [] for p in inst.params: if isinstance(p, MemoryReference) and p.name not in measure_regs: param_indices.append(param_counter) + concrete_values.append(float("nan")) param_refs.append((p.name, p.offset)) param_counter += 1 else: param_indices.append(-1) + concrete_values.append(float(p.real) if hasattr(p, "real") else float(p)) - def _make_param_callable( - gdef: object, - cp: list, - pi: list[int], - ) -> Callable[[Array], qx.Unitary]: - def resolve_params(params: Array) -> qx.Unitary: - resolved: list[Any] = [] - for p, pv in zip(cp, pi, strict=False): - if pv >= 0: - resolved.append(params[pv]) - else: - resolved.append(float(p.real) if hasattr(p, "real") else float(p)) - result = gdef(*resolved) if callable(gdef) else gdef - if not isinstance(result, qx.Unitary): - result = cast(Any, result) - result = qx.Unitary.from_matrix(result.matrix, result.dims) - return result - - return resolve_params - - return _make_param_callable(gate_def, concrete_params, param_indices), qubits + return ParametricGate(gate_def, tuple(param_indices), tuple(concrete_values)), qubits # Fixed gate → resolve to Unitary now. unitary = get_instruction_unitary(inst, custom_gates=custom_gates) diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index a9d87ea67..d950c8227 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -49,6 +49,7 @@ from pyquil.quil import Program from pyquil.quilbase import Measurement, Reset, ResetQubit from pyquil.simulation._resolver import ( + ParametricGate, ResolvedOp, TrajectoryOp, adapt_for_density_matrix, @@ -132,7 +133,8 @@ class ProgramSimulator: """ __slots__ = ("n_qubits", "qubits", "dims", "_linearize_fn", "_resolve_fn", "_compress_fn", - "bases", "op_index", "base_dims", "base_total_dim", "d_max", "_has_params") + "bases", "op_index", "base_dims", "base_total_dim", "d_max", "_has_params", + "_expanded_ops", "_raw_subsystems") def __init__( self, @@ -173,6 +175,8 @@ def linearize(memory_map: MemoryMap) -> Array: self.dims = (2,) * self.n_qubits self._linearize_fn = linearize self._resolve_fn = resolve + self._expanded_ops = tuple(expanded_ops) + self._raw_subsystems = tuple(mapped_qubits) # Whether any gate matrix depends on a runtime parameter. When it does # not, the compressed operator stack is a compile-time constant and can @@ -238,6 +242,106 @@ def compute(self, params: Array, **kwargs: Any) -> Any: raise NotImplementedError +# ══════════════════════════════════════════════════════════ +# Vectorized gate construction +# ══════════════════════════════════════════════════════════ + + +def _build_vectorized_unitary_constructor( + expanded_ops: tuple, + raw_subsystems: tuple[tuple[int, ...], ...], + d_max: int, +) -> Callable[[Array], Array]: + """Build a function that constructs all raw gate matrices via ``jax.vmap``. + + Parametric gates are grouped by their constructor function (e.g. all + ``RX`` gates together). Each group is batch-constructed with a single + ``vmap`` call, so the traced graph contains one subgraph per gate *type* + rather than per gate *instance* — typically reducing HLO size by 10–100×. + + Constant (non-parametric) gates are pre-built and placed as a single + concrete array. + """ + n_ops = len(expanded_ops) + + # ── Group parametric gates by (gate_fn, concrete layout) ── + GroupInfo = tuple[Callable, tuple[int, ...], tuple[float, ...], list[int], list[list[int]]] + param_groups: dict[tuple, GroupInfo] = {} + const_positions: list[int] = [] + const_matrices: list[np.ndarray] = [] + + for i, eop in enumerate(expanded_ops): + if isinstance(eop, ParametricGate): + concrete_mask = tuple(j for j, pi in enumerate(eop.param_indices) if pi < 0) + concrete_vals = tuple(eop.concrete_values[j] for j in concrete_mask) + key = (id(eop.gate_fn), concrete_mask, concrete_vals) + if key not in param_groups: + param_groups[key] = (eop.gate_fn, eop.param_indices, eop.concrete_values, [], []) + param_groups[key][3].append(i) + param_groups[key][4].append([pi for pi in eop.param_indices if pi >= 0]) + else: + const_positions.append(i) + mat = np.asarray(eop.matrix) + const_matrices.append(np.pad(mat, [(0, d_max - s) for s in mat.shape])) + + # ── Build vmapped constructors ── + vmapped_specs: list[tuple[np.ndarray, Callable[[Array], Array]]] = [] + + for gate_fn, template_pi, template_cv, positions, pidx_lists in param_groups.values(): + pos_arr = np.array(positions) + n_parametric = len(pidx_lists[0]) + + # Probe for output matrix shape → pad widths + probe_args = [0.0 if pi >= 0 else cv for pi, cv in zip(template_pi, template_cv)] + probe_mat = gate_fn(*probe_args).matrix + pad_w = tuple((0, d_max - s) for s in probe_mat.shape) + + # Build partial function that bakes in concrete values + parametric_slots = [j for j, pi in enumerate(template_pi) if pi >= 0] + concrete_slots = [(j, cv) for j, (pi, cv) in enumerate(zip(template_pi, template_cv)) if pi < 0] + n_total = len(template_pi) + + def _make_batch(gf: Callable, ps: list[int], cs: list[tuple[int, float]], + nt: int, pw: tuple, np_: int, pidx: Array) -> Callable[[Array], Array]: + def _single(parametric_values: Array) -> Array: + args: list[Any] = [None] * nt + for slot, val in cs: + args[slot] = val + for k, slot in enumerate(ps): + args[slot] = parametric_values[k] + return jnp.pad(gf(*args).matrix, pw) + + batched = jax.vmap(_single) + + def build(params: Array) -> Array: + return batched(params[pidx]) + + return build + + pidx_arr = jnp.array(pidx_lists) # (n_gates, n_parametric) + builder = _make_batch(gate_fn, parametric_slots, concrete_slots, + n_total, pad_w, n_parametric, pidx_arr) + vmapped_specs.append((pos_arr, builder)) + + # ── Pre-build constant matrix array ── + if const_matrices: + const_pos_arr = np.array(const_positions) + const_stack = jnp.array(np.stack(const_matrices)) + else: + const_pos_arr = None + const_stack = None + + def build_op_stack(params: Array) -> Array: + result = jnp.zeros((n_ops, d_max, d_max), dtype=complex) + for pos_arr, builder in vmapped_specs: + result = result.at[pos_arr].set(builder(params)) + if const_stack is not None: + result = result.at[const_pos_arr].set(const_stack) + return result + + return build_op_stack + + # ══════════════════════════════════════════════════════════ # Pure state-vector simulator # ══════════════════════════════════════════════════════════ @@ -254,7 +358,7 @@ class PureStateVectorSimulator(ProgramSimulator): U = jax.jit(sim.unitary)(params) """ - __slots__ = ("_psi0", "_branches", "_idx_arr", "_const_op_stack") + __slots__ = ("_psi0", "_branches", "_idx_arr", "_const_op_stack", "_vmapped_build_fn") def __init__( self, @@ -265,21 +369,51 @@ def __init__( ) -> None: super().__init__(program, qubits, noise_model=None, max_subsystem_size=max_subsystem_size) self._psi0 = qx.zero_state_vector(dims=self.dims) - self._branches = [ - _make_unitary_branch(base, base_dims, db) - for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) - ] - self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) - # For programs without runtime parameters the operator stack is constant. - # Build it eagerly once so the traced ``compute`` graph contains only the - # scan over a small concrete array, not the (constant-folded, autotuned) - # ``compress``/``compose_operator`` construction. - self._const_op_stack: Array | None = None - if not self._has_params and self.op_index: - self._const_op_stack = jax.block_until_ready( - self._stack_unitaries(self.resolve(jnp.zeros(0))) + if self._has_params: + # ── Parametric path ── + # Use raw (uncompressed) ops with vectorized gate construction. + # This trades ~4× more scan steps for ~100× smaller traced graph + # by batching gate matrix construction with ``jax.vmap``. + self._const_op_stack = None + + # Build raw-op bases (from original subsystems, not compressed). + raw_bases: list[tuple[int, ...]] = [] + raw_sub_to_branch: dict[tuple[int, ...], int] = {} + raw_op_index: list[int] = [] + for sub in self._raw_subsystems: + if sub not in raw_sub_to_branch: + raw_sub_to_branch[sub] = len(raw_bases) + raw_bases.append(sub) + raw_op_index.append(raw_sub_to_branch[sub]) + + raw_base_dims = [tuple(self.dims[q] for q in b) for b in raw_bases] + raw_base_total_dim = [math.prod(d) for d in raw_base_dims] + raw_d_max = max(raw_base_total_dim) if raw_base_total_dim else 1 + + self._branches = [ + _make_unitary_branch(base, bd, bt) + for base, bd, bt in zip(raw_bases, raw_base_dims, raw_base_total_dim, strict=True) + ] + self._idx_arr = jnp.asarray(raw_op_index, dtype=jnp.int32) + self._vmapped_build_fn = _build_vectorized_unitary_constructor( + self._expanded_ops, self._raw_subsystems, raw_d_max, ) + else: + # ── Non-parametric path ── + # Operator stack is a compile-time constant → build eagerly using + # the compressor and feed as a concrete array into the scan. + self._vmapped_build_fn = None + self._branches = [ + _make_unitary_branch(base, base_dims, db) + for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) + ] + self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) + self._const_op_stack = None + if self.op_index: + self._const_op_stack = jax.block_until_ready( + self._stack_unitaries(self.resolve(jnp.zeros(0))) + ) def _validate(self, program: Program) -> None: for inst in program.instructions: @@ -309,6 +443,8 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] """ if self._const_op_stack is not None: op_stack = self._const_op_stack + elif self._vmapped_build_fn is not None: + op_stack = self._vmapped_build_fn(params) else: resolved = self.resolve(params) if not resolved: From 35b86e64dc8d06367e4ece5f3d26a0d8c489ad22 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sun, 31 May 2026 19:30:25 +0000 Subject: [PATCH 17/37] fix performance regression --- pyquil/simulation/_resolver.py | 3 + pyquil/simulation/_simulator.py | 280 ++++++++++++++++++++++++-------- 2 files changed, 216 insertions(+), 67 deletions(-) diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index ed84b20c3..b92f3634e 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -735,4 +735,7 @@ def compress(ops: list[ResolvedOp]) -> list[ResolvedOp]: result.append(merged) return result + # Expose merge recipe: for each group, (nodes_in_topo_order, merged_subsystem). + compress.emit_order = emit_order # type: ignore[attr-defined] + return compress diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index d950c8227..1980ae1aa 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -247,69 +247,189 @@ def compute(self, params: Array, **kwargs: Any) -> Any: # ══════════════════════════════════════════════════════════ -def _build_vectorized_unitary_constructor( - expanded_ops: tuple, - raw_subsystems: tuple[tuple[int, ...], ...], +def _embed_matrix_np(mat: np.ndarray, op_subsystem: tuple[int, ...], + group_subsystem: tuple[int, ...], dims: tuple[int, ...], + d_max: int) -> np.ndarray: + """Embed a gate matrix into a larger subsystem (numpy, for constant ops). + + Computes the d_max×d_max padded matrix that applies ``mat`` on + ``op_subsystem`` within the Hilbert space of ``group_subsystem``. + """ + if op_subsystem == group_subsystem: + return np.pad(mat, [(0, d_max - s) for s in mat.shape]) + import quax as qx # noqa: F811 — local re-import for clarity + target_dims = tuple(dims[q] for q in group_subsystem) + positions = tuple(group_subsystem.index(q) for q in op_subsystem) + op = qx.Unitary.from_matrix(jnp.array(mat), tuple((dims[q] for q in op_subsystem),) * 2) + embedded = qx.embed(op, target_dims=target_dims, positions=positions) + result = np.asarray(embedded.matrix) + return np.pad(result, [(0, d_max - s) for s in result.shape]) + + +def _make_embed_fn( + op_subsystem: tuple[int, ...], + group_subsystem: tuple[int, ...], + dims: tuple[int, ...], d_max: int, ) -> Callable[[Array], Array]: - """Build a function that constructs all raw gate matrices via ``jax.vmap``. + """Return a JIT-friendly function that embeds a gate matrix into a group subsystem. + + Uses simple Kronecker products rather than the full qx.embed machinery + to minimize the traced graph size. + """ + if op_subsystem == group_subsystem: + D = math.prod(dims[q] for q in op_subsystem) + pad_w = ((0, d_max - D),) * 2 + def _identity_embed(mat: Array) -> Array: + return jnp.pad(mat, pad_w) + return _identity_embed + + # General case: embed via Kronecker products. + # The group_subsystem is ordered; figure out which positions in the group + # the op occupies, and insert identities for the remaining positions. + target_dims = tuple(dims[q] for q in group_subsystem) + positions = tuple(group_subsystem.index(q) for q in op_subsystem) + n_group = len(group_subsystem) + D = math.prod(target_dims) + pad_w = ((0, d_max - D),) * 2 + + # Precompute which group positions are "identity" positions. + # Strategy: reshape the op matrix into a tensor, then embed into the full + # group tensor, then reshape back. This avoids the overhead of qx objects. + op_dims = tuple(dims[q] for q in op_subsystem) + n_op = len(op_subsystem) + + # Build a permutation: the full group tensor has axes for each qubit. + # We place op axes at their positions, then the remaining axes carry identity. + # Result = I ⊗ ... ⊗ op ⊗ ... ⊗ I (with op spread across `positions`). + + # For the common case: 1-qubit gate in a 2-qubit group + if n_op == 1 and n_group == 2 and all(d == 2 for d in target_dims): + pos = positions[0] + I2 = jnp.eye(2, dtype=complex) + if pos == 0: + def _embed(mat: Array) -> Array: + return jnp.pad(jnp.kron(mat, I2), pad_w) + return _embed + else: + def _embed(mat: Array) -> Array: + return jnp.pad(jnp.kron(I2, mat), pad_w) + return _embed + + # General fallback: use einsum-based embedding + # Build the full unitary as a tensor product with identities. + non_op_positions = [i for i in range(n_group) if i not in positions] + non_op_dims = [target_dims[i] for i in non_op_positions] + identity_factors = [jnp.eye(d, dtype=complex) for d in non_op_dims] + + def _embed_general(mat: Array) -> Array: + # Reshape op into a tensor + op_tensor = mat.reshape([op_dims[i] for i in range(n_op)] * 2) + # Build full group tensor via einsum + # Start with the op tensor, then kron with identities for non-op positions + result = mat + for i, nop in enumerate(non_op_positions): + # Determine if this identity goes before or after + d = non_op_dims[i] + # Simple sequential kron (ordering may need adjustment) + result = jnp.kron(result, identity_factors[i]) if nop > max(positions) else jnp.kron(identity_factors[i], result) + return jnp.pad(result, pad_w) + + return _embed_general - Parametric gates are grouped by their constructor function (e.g. all - ``RX`` gates together). Each group is batch-constructed with a single - ``vmap`` call, so the traced graph contains one subgraph per gate *type* - rather than per gate *instance* — typically reducing HLO size by 10–100×. - Constant (non-parametric) gates are pre-built and placed as a single - concrete array. +def _build_vectorized_unitary_constructor( + expanded_ops: tuple, + raw_subsystems: tuple[tuple[int, ...], ...], + emit_order: list, + dims: tuple[int, ...], + d_max: int, +) -> tuple[Callable[[Array], Array], np.ndarray, np.ndarray]: + """Build a vectorized constructor that produces *embedded* gate matrices. + + Each raw gate matrix is embedded into its merge group's subsystem so that + compression can be performed as a simple segmented matmul scan. + + Returns ``(build_fn, sort_order, group_boundaries)`` where: + - ``build_fn(params)`` → ``(N_raw, d_max, d_max)`` array of embedded matrices + in group-sorted order (ops within each group are consecutive). + - ``sort_order[i]`` = raw op index for position *i* in the sorted array. + - ``group_boundaries[g]`` = start index of group *g* in the sorted array. + The final compressed op for group *g* is the product of sorted ops from + ``group_boundaries[g]`` to ``group_boundaries[g+1]``. """ n_ops = len(expanded_ops) - # ── Group parametric gates by (gate_fn, concrete layout) ── - GroupInfo = tuple[Callable, tuple[int, ...], tuple[float, ...], list[int], list[list[int]]] - param_groups: dict[tuple, GroupInfo] = {} - const_positions: list[int] = [] - const_matrices: list[np.ndarray] = [] + # ── Compute group membership and embedding for each raw op ── + # raw_to_group[i] = which compressed group raw op i belongs to + # raw_to_embed_key[i] = (id(gate_fn_or_None), concrete_key, embed_key) for grouping + raw_to_group = np.empty(n_ops, dtype=np.int32) + raw_to_group_sub: list[tuple[int, ...]] = [() for _ in range(n_ops)] + + sorted_raw_indices: list[int] = [] # raw op indices in group-then-topo order + group_boundaries: list[int] = [0] + for group_idx, (_, nodes, subsystem) in enumerate(emit_order): + for nk in nodes: + raw_to_group[nk] = group_idx + raw_to_group_sub[nk] = subsystem + sorted_raw_indices.append(nk) + group_boundaries.append(len(sorted_raw_indices)) + + sort_order = np.array(sorted_raw_indices, dtype=np.int32) + group_bounds = np.array(group_boundaries, dtype=np.int32) + + # ── Group ops by (gate_fn, concrete_layout, embedding_config) for vmap ── + # The embedding_config is (op_subsystem, group_subsystem) which determines + # how the raw matrix is expanded into the group's Hilbert space. + param_groups: dict[tuple, tuple[Callable, tuple, tuple, list[int], list[list[int]], Callable]] = {} + const_entries: list[tuple[int, np.ndarray]] = [] # (sorted_position, embedded_matrix) + + for sorted_pos, raw_idx in enumerate(sorted_raw_indices): + eop = expanded_ops[raw_idx] + op_sub = raw_subsystems[raw_idx] + grp_sub = raw_to_group_sub[raw_idx] - for i, eop in enumerate(expanded_ops): if isinstance(eop, ParametricGate): concrete_mask = tuple(j for j, pi in enumerate(eop.param_indices) if pi < 0) concrete_vals = tuple(eop.concrete_values[j] for j in concrete_mask) - key = (id(eop.gate_fn), concrete_mask, concrete_vals) + # Group by embedding TYPE not physical qubits: all embeddings with + # the same (op_dims, target_dims, positions_within_group) produce + # identical traced graphs, so they can share a single vmap. + op_dims_key = tuple(dims[q] for q in op_sub) + target_dims_key = tuple(dims[q] for q in grp_sub) + positions_key = tuple(grp_sub.index(q) for q in op_sub) + embed_key = (op_dims_key, target_dims_key, positions_key) + key = (id(eop.gate_fn), concrete_mask, concrete_vals, embed_key) if key not in param_groups: - param_groups[key] = (eop.gate_fn, eop.param_indices, eop.concrete_values, [], []) - param_groups[key][3].append(i) + embed_fn = _make_embed_fn(op_sub, grp_sub, dims, d_max) + param_groups[key] = (eop.gate_fn, eop.param_indices, eop.concrete_values, + [], [], embed_fn) + param_groups[key][3].append(sorted_pos) param_groups[key][4].append([pi for pi in eop.param_indices if pi >= 0]) else: - const_positions.append(i) mat = np.asarray(eop.matrix) - const_matrices.append(np.pad(mat, [(0, d_max - s) for s in mat.shape])) + embedded = _embed_matrix_np(mat, op_sub, grp_sub, dims, d_max) + const_entries.append((sorted_pos, embedded)) # ── Build vmapped constructors ── vmapped_specs: list[tuple[np.ndarray, Callable[[Array], Array]]] = [] - for gate_fn, template_pi, template_cv, positions, pidx_lists in param_groups.values(): + for gate_fn, template_pi, template_cv, positions, pidx_lists, embed_fn in param_groups.values(): pos_arr = np.array(positions) - n_parametric = len(pidx_lists[0]) - # Probe for output matrix shape → pad widths - probe_args = [0.0 if pi >= 0 else cv for pi, cv in zip(template_pi, template_cv)] - probe_mat = gate_fn(*probe_args).matrix - pad_w = tuple((0, d_max - s) for s in probe_mat.shape) - - # Build partial function that bakes in concrete values parametric_slots = [j for j, pi in enumerate(template_pi) if pi >= 0] concrete_slots = [(j, cv) for j, (pi, cv) in enumerate(zip(template_pi, template_cv)) if pi < 0] n_total = len(template_pi) def _make_batch(gf: Callable, ps: list[int], cs: list[tuple[int, float]], - nt: int, pw: tuple, np_: int, pidx: Array) -> Callable[[Array], Array]: + nt: int, ef: Callable, pidx: Array) -> Callable[[Array], Array]: def _single(parametric_values: Array) -> Array: args: list[Any] = [None] * nt for slot, val in cs: args[slot] = val for k, slot in enumerate(ps): args[slot] = parametric_values[k] - return jnp.pad(gf(*args).matrix, pw) + return ef(gf(*args).matrix) batched = jax.vmap(_single) @@ -318,28 +438,60 @@ def build(params: Array) -> Array: return build - pidx_arr = jnp.array(pidx_lists) # (n_gates, n_parametric) + pidx_arr = jnp.array(pidx_lists) builder = _make_batch(gate_fn, parametric_slots, concrete_slots, - n_total, pad_w, n_parametric, pidx_arr) + n_total, embed_fn, pidx_arr) vmapped_specs.append((pos_arr, builder)) - # ── Pre-build constant matrix array ── - if const_matrices: - const_pos_arr = np.array(const_positions) - const_stack = jnp.array(np.stack(const_matrices)) + # ── Pre-build constant embedded matrices ── + if const_entries: + const_positions = np.array([p for p, _ in const_entries]) + const_stack = jnp.array(np.stack([m for _, m in const_entries])) else: - const_pos_arr = None + const_positions = None const_stack = None - def build_op_stack(params: Array) -> Array: - result = jnp.zeros((n_ops, d_max, d_max), dtype=complex) + # ── Build gather index for padded parallel composition ── + n_groups = len(group_boundaries) - 1 + group_sizes = np.diff(group_boundaries) + max_group_size = int(group_sizes.max()) if n_groups > 0 else 1 + + # gather_idx[g, k] = sorted position of the k-th op in group g, + # or n_ops (the identity sentinel position) for padding. + gather_idx = np.full((n_groups, max_group_size), n_ops, dtype=np.int32) + for g in range(n_groups): + start, end = group_boundaries[g], group_boundaries[g + 1] + size = end - start + gather_idx[g, :size] = np.arange(start, end) + gather_idx_jax = jnp.array(gather_idx) + eye_mat = jnp.eye(d_max, dtype=complex) + + def build_compressed_stack(params: Array) -> Array: + """Build all gate matrices and compose within each merge group. + + Returns ``(n_groups, d_max, d_max)`` — one compressed matrix per group. + """ + # 1. Build all embedded gate matrices in group-sorted order. + raw_mats = jnp.zeros((n_ops, d_max, d_max), dtype=complex) for pos_arr, builder in vmapped_specs: - result = result.at[pos_arr].set(builder(params)) + raw_mats = raw_mats.at[pos_arr].set(builder(params)) if const_stack is not None: - result = result.at[const_pos_arr].set(const_stack) - return result + raw_mats = raw_mats.at[const_positions].set(const_stack) + + # 2. Append identity sentinel and gather into (n_groups, max_size, d, d). + raw_mats_plus = jnp.concatenate([raw_mats, eye_mat[None]], axis=0) + padded = raw_mats_plus[gather_idx_jax] # (n_groups, max_size, d, d) - return build_op_stack + # 3. Parallel fold: vmap a scan over all groups simultaneously. + def group_product(mats: Array) -> Array: + def body(acc: Array, mat: Array) -> tuple[Array, None]: + return mat @ acc, None + final, _ = jax.lax.scan(body, eye_mat, mats) + return final + + return jax.vmap(group_product)(padded) # (n_groups, d, d) + + return build_compressed_stack, sort_order, group_bounds # ══════════════════════════════════════════════════════════ @@ -358,7 +510,8 @@ class PureStateVectorSimulator(ProgramSimulator): U = jax.jit(sim.unitary)(params) """ - __slots__ = ("_psi0", "_branches", "_idx_arr", "_const_op_stack", "_vmapped_build_fn") + __slots__ = ("_psi0", "_branches", "_idx_arr", "_const_op_stack", + "_vmapped_build_fn") def __init__( self, @@ -372,33 +525,24 @@ def __init__( if self._has_params: # ── Parametric path ── - # Use raw (uncompressed) ops with vectorized gate construction. - # This trades ~4× more scan steps for ~100× smaller traced graph - # by batching gate matrix construction with ``jax.vmap``. + # Vectorized gate construction (vmap per gate type) followed by a + # segmented matmul scan for compression. This gives both fast + # compilation (small traced graph) AND fast runtime (compressed + # op count in the state-evolution scan). self._const_op_stack = None - # Build raw-op bases (from original subsystems, not compressed). - raw_bases: list[tuple[int, ...]] = [] - raw_sub_to_branch: dict[tuple[int, ...], int] = {} - raw_op_index: list[int] = [] - for sub in self._raw_subsystems: - if sub not in raw_sub_to_branch: - raw_sub_to_branch[sub] = len(raw_bases) - raw_bases.append(sub) - raw_op_index.append(raw_sub_to_branch[sub]) - - raw_base_dims = [tuple(self.dims[q] for q in b) for b in raw_bases] - raw_base_total_dim = [math.prod(d) for d in raw_base_dims] - raw_d_max = max(raw_base_total_dim) if raw_base_total_dim else 1 + emit_order = self._compress_fn.emit_order # type: ignore[attr-defined] + build_fn, sort_order, group_bounds = _build_vectorized_unitary_constructor( + self._expanded_ops, self._raw_subsystems, emit_order, self.dims, self.d_max, + ) + self._vmapped_build_fn = build_fn + # Compressed-op branch index (same as non-parametric path). self._branches = [ - _make_unitary_branch(base, bd, bt) - for base, bd, bt in zip(raw_bases, raw_base_dims, raw_base_total_dim, strict=True) + _make_unitary_branch(base, base_dims, db) + for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) ] - self._idx_arr = jnp.asarray(raw_op_index, dtype=jnp.int32) - self._vmapped_build_fn = _build_vectorized_unitary_constructor( - self._expanded_ops, self._raw_subsystems, raw_d_max, - ) + self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) else: # ── Non-parametric path ── # Operator stack is a compile-time constant → build eagerly using @@ -444,6 +588,8 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] if self._const_op_stack is not None: op_stack = self._const_op_stack elif self._vmapped_build_fn is not None: + # Vectorized parametric path: builds embedded matrices via vmap, + # then composes within each merge group via parallel fold. op_stack = self._vmapped_build_fn(params) else: resolved = self.resolve(params) From 5170fddafda829b91bd9e65437d6662fc0d59915 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Fri, 5 Jun 2026 00:19:35 +0000 Subject: [PATCH 18/37] clean up pure state simulator --- pyquil/simulation/_simulator.py | 117 ++++++++++++----------- test/unit/test_qutrit_simulation.py | 143 +++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 58 deletions(-) diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 1980ae1aa..0e9eadb24 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -116,6 +116,27 @@ def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVect return branch + +def _infer_dims(resolved: list[ResolvedOp], n_qubits: int) -> tuple[int, ...]: + """Infer per-qudit dimensions from a resolved operation list. + + Each operator carries its own per-qudit input dimensions via + ``op.dims[1]``. A slot's dimension is the maximum dimension seen across + every operation acting on it, defaulting to ``2`` (qubit) for slots that + no operation touches. + + :param resolved: Resolved ``(operator, subsystem)`` pairs. + :param n_qubits: Number of qudit slots. + :return: Inferred per-qudit dimensions tuple. + """ + dims = [2] * n_qubits + for op, subsystem in resolved: + op_dims = op.dims[1] + for q, d in zip(subsystem, op_dims, strict=False): + dims[q] = max(dims[q], d) + return tuple(dims) + + # ══════════════════════════════════════════════════════════ # Base class # ══════════════════════════════════════════════════════════ @@ -143,6 +164,7 @@ def __init__( *, noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 2, + dims: tuple[int, ...] | None = None, ) -> None: self._validate(program) @@ -153,6 +175,8 @@ def __init__( # Build resolver from the expanded program. expanded_ops, phys_qubits, param_refs = expand_program(program, noise_model) + + # Remap the qudits from 0 to n qubit_indices = {q: i for i, q in enumerate(qubits)} mapped_qubits = remap_qubits(phys_qubits, qubit_indices) dag = build_dag(mapped_qubits) @@ -172,12 +196,22 @@ def linearize(memory_map: MemoryMap) -> Array: values = [float(memory_map[name][offset]) for name, offset in param_refs] return jnp.array(values, dtype=float) - self.dims = (2,) * self.n_qubits self._linearize_fn = linearize self._resolve_fn = resolve self._expanded_ops = tuple(expanded_ops) self._raw_subsystems = tuple(mapped_qubits) + # Per-qudit dimensions: use the explicit override when provided, + # otherwise infer each slot's dimension from the operators acting on it + # (e.g. a qutrit gate upgrades its slots to dimension 3). Inference + # resolves the ops once with zero parameters; the matrix *shapes* are + # parameter-independent, so this yields the correct dimensions for any + # parameter values. + if dims is not None: + self.dims = dims + else: + self.dims = _infer_dims(resolve(jnp.zeros(len(param_refs))), self.n_qubits) + # Whether any gate matrix depends on a runtime parameter. When it does # not, the compressed operator stack is a compile-time constant and can # be materialised eagerly (outside the traced graph), which avoids XLA @@ -260,7 +294,8 @@ def _embed_matrix_np(mat: np.ndarray, op_subsystem: tuple[int, ...], import quax as qx # noqa: F811 — local re-import for clarity target_dims = tuple(dims[q] for q in group_subsystem) positions = tuple(group_subsystem.index(q) for q in op_subsystem) - op = qx.Unitary.from_matrix(jnp.array(mat), tuple((dims[q] for q in op_subsystem),) * 2) + op_dims = tuple(dims[q] for q in op_subsystem) + op = qx.Unitary.from_matrix(jnp.array(mat), (op_dims, op_dims)) embedded = qx.embed(op, target_dims=target_dims, positions=positions) result = np.asarray(embedded.matrix) return np.pad(result, [(0, d_max - s) for s in result.shape]) @@ -510,8 +545,7 @@ class PureStateVectorSimulator(ProgramSimulator): U = jax.jit(sim.unitary)(params) """ - __slots__ = ("_psi0", "_branches", "_idx_arr", "_const_op_stack", - "_vmapped_build_fn") + __slots__ = ("_psi0", "_branches", "_idx_arr", "_vmapped_build_fn") def __init__( self, @@ -523,41 +557,21 @@ def __init__( super().__init__(program, qubits, noise_model=None, max_subsystem_size=max_subsystem_size) self._psi0 = qx.zero_state_vector(dims=self.dims) - if self._has_params: - # ── Parametric path ── - # Vectorized gate construction (vmap per gate type) followed by a - # segmented matmul scan for compression. This gives both fast - # compilation (small traced graph) AND fast runtime (compressed - # op count in the state-evolution scan). - self._const_op_stack = None - - emit_order = self._compress_fn.emit_order # type: ignore[attr-defined] - build_fn, sort_order, group_bounds = _build_vectorized_unitary_constructor( - self._expanded_ops, self._raw_subsystems, emit_order, self.dims, self.d_max, - ) - self._vmapped_build_fn = build_fn + # Vectorized gate construction (vmap per gate type) followed by a + # segmented matmul scan for compression. This gives both fast + # compilation (small traced graph) AND fast runtime (compressed + # op count in the state-evolution scan). + emit_order = getattr(self._compress_fn, "emit_order", []) + build_fn, _sort_order, _group_bounds = _build_vectorized_unitary_constructor( + self._expanded_ops, self._raw_subsystems, emit_order, self.dims, self.d_max, + ) + self._vmapped_build_fn = build_fn - # Compressed-op branch index (same as non-parametric path). - self._branches = [ - _make_unitary_branch(base, base_dims, db) - for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) - ] - self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) - else: - # ── Non-parametric path ── - # Operator stack is a compile-time constant → build eagerly using - # the compressor and feed as a concrete array into the scan. - self._vmapped_build_fn = None - self._branches = [ - _make_unitary_branch(base, base_dims, db) - for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) - ] - self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) - self._const_op_stack = None - if self.op_index: - self._const_op_stack = jax.block_until_ready( - self._stack_unitaries(self.resolve(jnp.zeros(0))) - ) + self._branches = [ + _make_unitary_branch(base, base_dims, db) + for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) + ] + self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) def _validate(self, program: Program) -> None: for inst in program.instructions: @@ -566,12 +580,6 @@ def _validate(self, program: Program) -> None: if isinstance(inst, (Reset, ResetQubit)): raise ValueError(f"PureStateVectorSimulator does not support resets. Found: {inst}") - def _stack_unitaries(self, resolved: list[ResolvedOp]) -> Array: - """Compress, then stack each gate's matrix into ``(N, d_max, d_max)``.""" - compressed = self.compress(resolved) - mats = [_pad_matrix(cast(qx.Unitary, op).matrix, self.d_max, self.d_max) for op, _ in compressed] - return jnp.stack(mats, axis=0) - def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] """Compute the final state vector. @@ -585,17 +593,13 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] :param params: Flat parameter vector from :meth:`linearize`. :return: The final state vector. """ - if self._const_op_stack is not None: - op_stack = self._const_op_stack - elif self._vmapped_build_fn is not None: - # Vectorized parametric path: builds embedded matrices via vmap, - # then composes within each merge group via parallel fold. - op_stack = self._vmapped_build_fn(params) - else: - resolved = self.resolve(params) - if not resolved: - return self._psi0 - op_stack = self._stack_unitaries(resolved) + # No operations (e.g. empty program) → the initial state is the result. + if not self._branches: + return self._psi0 + + # Vectorized construction: build embedded matrices via vmap, then + # compose within each merge group via a parallel fold. + op_stack = self._vmapped_build_fn(params) branches = self._branches def body(psi: qx.StateVector, xs: tuple[Array, Array]) -> tuple[qx.StateVector, None]: @@ -758,8 +762,9 @@ def __init__( max_subsystem_size: int = 2, kraus_truncation_threshold: float = 1e-6, devices: list[jax.Device] | None = None, + dims: tuple[int, ...] | None = None, ) -> None: - super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) + super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size, dims=dims) self._kraus_truncation_threshold = kraus_truncation_threshold self._devices = devices if devices is not None else jax.devices() diff --git a/test/unit/test_qutrit_simulation.py b/test/unit/test_qutrit_simulation.py index 2468902d1..f5be64067 100644 --- a/test/unit/test_qutrit_simulation.py +++ b/test/unit/test_qutrit_simulation.py @@ -8,8 +8,8 @@ from pyquil.gates import H, MEASURE, X from pyquil.quil import Program -from pyquil.quilatom import Qubit -from pyquil.quilbase import DefGate, Gate, Measurement +from pyquil.quilatom import MemoryReference, Qubit +from pyquil.quilbase import Declare, DefGate, Gate, Measurement from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel from pyquil.noise._noise_model import NoiseModel @@ -158,6 +158,145 @@ def test_multi_qutrit_independence(self): assert qx.fidelity(psi, expected) > 0.9999 +# ══════════════════════════════════════════════════════════ +# Test: All quax qutrit gates through the pure-state simulator +# ══════════════════════════════════════════════════════════ + + +def _quax_qutrit_gates(): + """Yield ``(name, gate)`` for every unitary qutrit gate in quax. + + Projectors (TP0/TP1/TP2) are excluded because they are not unitary and + therefore not valid for the pure-state simulator. + """ + for name, gate in qx.gates.QUANTUM_GATES.items(): + if name.startswith("TP"): + continue # projectors are not unitary + if callable(gate): + # Parametric gates accept one or more angles; probe arity. + unitary = None + for n_args in (1, 2, 3): + try: + unitary = gate(*([0.0] * n_args)) + break + except TypeError: + continue + if unitary is None: + continue + else: + unitary = gate + if any(d == 3 for d in unitary.dims[1]): + yield name, gate + + +class TestAllQutritGates: + """Every unitary qutrit gate in quax simulates correctly on |0...0>.""" + + # Single-qutrit fixed gates (non-parametric, dims == (3,)). + SINGLE_FIXED = [ + name + for name, gate in _quax_qutrit_gates() + if not callable(gate) and gate.dims[1] == (3,) + ] + # Parametric single-qutrit rotations (callable, dims == (3,)). + SINGLE_PARAM = [ + name + for name, gate in _quax_qutrit_gates() + if callable(gate) + ] + + def test_gate_inventory_is_nonempty(self): + """Sanity check: quax exposes the expected qutrit gate families.""" + # Clock/shift, Hadamard, Pauli-like, and Weyl operators are all present. + for expected in ("TX", "TY", "TZ", "TH", "TSHIFT", "TSWAP", "W00", "W22"): + assert expected in qx.gates.QUANTUM_GATES + assert set(self.SINGLE_PARAM) >= { + "TRX01", "TRY01", "TRZ01", + "TRX02", "TRY02", "TRZ02", + "TRX12", "TRY12", "TRZ12", + } + + @pytest.mark.parametrize("name", SINGLE_FIXED) + def test_single_qutrit_fixed_gate(self, name): + """Each fixed single-qutrit gate produces the expected |0> column.""" + p = Program(Gate(name, [], [0])) + psi = _sv(p, qubits=[0]) + assert psi.dims == (3,) + # The output equals the gate's first column (its action on |0>). + expected = qx.gates.QUANTUM_GATES[name].matrix[:, 0] + np.testing.assert_allclose(psi.matrix, expected, atol=1e-6) + # Unitary gates preserve normalization. + np.testing.assert_allclose( + float(jnp.sum(jnp.abs(psi.matrix) ** 2)), 1.0, atol=1e-6 + ) + + @pytest.mark.parametrize("name", SINGLE_PARAM) + def test_single_qutrit_parametric_gate(self, name): + """Each parametric single-qutrit rotation simulates and is unitary.""" + angle = np.pi / 3 + p = Program(Gate(name, [angle], [0])) + sim = PureStateVectorSimulator(p, qubits=[0]) + psi = sim.compute(jnp.array([], dtype=float)) + assert psi.dims == (3,) + expected = qx.gates.QUANTUM_GATES[name](angle).matrix[:, 0] + np.testing.assert_allclose(psi.matrix, expected, atol=1e-6) + np.testing.assert_allclose( + float(jnp.sum(jnp.abs(psi.matrix) ** 2)), 1.0, atol=1e-6 + ) + + def test_parametric_qutrit_via_memory_map(self): + """A parametric qutrit rotation resolves a runtime memory parameter.""" + p = Program() + p += Declare("theta", "REAL", 1) + p += Gate("TRX01", [MemoryReference("theta")], [0]) + sim = PureStateVectorSimulator(p, qubits=[0]) + params = sim.linearize({"theta": [np.pi]}) + psi = sim.compute(params) + # TRX01(pi)|0> = -i|1> (pi rotation in the 0-1 subspace). + expected = qx.StateVector.from_matrix( + jnp.array([0, -1j, 0], dtype=complex), dims=(3,) + ) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_two_qutrit_tswap_at_position_one(self): + """A single-qutrit gate merged at the high slot of a TSWAP pair. + + Exercises embedding a qutrit gate at a non-zero position within a + two-qutrit merge group (TH on slot 1 alongside TSWAP on (0, 1)). + """ + p = Program() + p += Gate("TX", [], [0]) # |0> -> |2> on slot 0 + p += Gate("TH", [], [1]) # superposition on slot 1 + p += Gate("TSWAP", [], [0, 1]) # swap the two qutrits + psi = _sv(p, qubits=[0, 1]) + assert psi.dims == (3, 3) + # After swap: slot 0 holds TH|0>, slot 1 holds |2>. + q0 = jnp.array([1, 1, 1], dtype=complex) / jnp.sqrt(3) + q1 = jnp.array([0, 0, 1], dtype=complex) + expected = qx.StateVector.from_matrix(jnp.kron(q0, q1), dims=(3, 3)) + assert qx.fidelity(psi, expected) > 0.9999 + + def test_jit_and_grad_qutrit(self): + """The qutrit pure-state simulator is jit- and grad-friendly.""" + p = Program() + p += Declare("theta", "REAL", 1) + p += Gate("TRX01", [MemoryReference("theta")], [0]) + sim = PureStateVectorSimulator(p, qubits=[0]) + + def excited_population(theta): + params = jnp.array([theta], dtype=float) + psi = sim.compute(params) + return jnp.abs(psi.matrix[1]) ** 2 + + # jit produces the same result as eager execution. + val_eager = float(excited_population(np.pi / 2)) + val_jit = float(jax.jit(excited_population)(np.pi / 2)) + np.testing.assert_allclose(val_jit, val_eager, atol=1e-6) + # grad is finite and well-defined. + g = float(jax.grad(excited_population)(np.pi / 2)) + assert np.isfinite(g) + + # ══════════════════════════════════════════════════════════ # Test: Mixed qubit/qutrit systems # ══════════════════════════════════════════════════════════ From 8b371e49cb5a333b4132c3ee7da2859481e893a1 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sun, 7 Jun 2026 06:42:27 +0000 Subject: [PATCH 19/37] Fix measurement order --- pyquil/simulation/_resolver.py | 63 +- test/unit/test_trajectory_compression.py | 830 +++++++++++++++++++++++ 2 files changed, 892 insertions(+), 1 deletion(-) create mode 100644 test/unit/test_trajectory_compression.py diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index b92f3634e..be1f77db0 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -643,6 +643,50 @@ def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: uf.make_set(nk) group_qubits[nk] = set(dag.nodes[nk]["qubits"]) + # Quotient graph over current group roots, kept in lock-step with the + # union-find structure. It starts as a copy of the dependency DAG and is + # contracted whenever two groups merge. It is the authority on whether a + # candidate merge is *convex*: contracting two groups must not reorder any + # operation that lies topologically between them (see ``_contraction_cycles``). + quotient: nx.DiGraph = nx.DiGraph() + quotient.add_nodes_from(dag.nodes) + quotient.add_edges_from(dag.edges) + + def _contraction_cycles(root_a: int, root_b: int) -> bool: + """Return ``True`` if merging two groups would create a cycle. + + The quotient graph is always a DAG, so contracting ``root_a`` and + ``root_b`` introduces a cycle iff there is a directed path of length + ``>= 2`` between them in *either* direction — i.e. some other group is + sandwiched on a dependency path from one to the other. Merging across + such a node would force it to be reordered relative to the merged group, + which is exactly what must be forbidden for barriers (measurements) and + for any non-commuting gate. A direct edge ``root_a -> root_b`` alone is + fine; only an *indirect* path is a problem. + """ + for src, dst in ((root_a, root_b), (root_b, root_a)): + stack = [s for s in quotient.successors(src) if s != dst] + seen = set(stack) + while stack: + node = stack.pop() + if node == dst: + return True + for nxt in quotient.successors(node): + if nxt not in seen: + seen.add(nxt) + stack.append(nxt) + return False + + def _contract_quotient(keep: int, drop: int) -> None: + """Contract ``drop`` into ``keep`` in the quotient graph.""" + for pred in list(quotient.predecessors(drop)): + if pred != keep: + quotient.add_edge(pred, keep) + for succ in list(quotient.successors(drop)): + if succ != keep: + quotient.add_edge(keep, succ) + quotient.remove_node(drop) + # Build initial candidate heap: (union_size, u, v) # Smaller union sizes are processed first. heap: list[tuple[int, int, int]] = [] @@ -662,11 +706,18 @@ def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: union_qubits = group_qubits[ru] | group_qubits[rv] if len(union_qubits) > max_subsystem_size: continue + # Reject merges that would move a barrier (measurement) or a + # non-commuting gate across the merged group. Without this check the + # compressor can fuse two gates that straddle a mid-circuit measurement, + # silently reordering the measurement and corrupting the result. + if _contraction_cycles(ru, rv): + continue new_root = uf.union(ru, rv) group_qubits[new_root] = union_qubits old_root = rv if new_root == ru else ru if old_root in group_qubits: del group_qubits[old_root] + _contract_quotient(new_root, old_root) # Re-enqueue edges from the newly merged group to its neighbours. for neighbour in ( @@ -683,7 +734,17 @@ def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: heapq.heappush(heap, (new_union_size, u_node, neighbour)) # --- Build merge plan --- - topo_order = list(nx.topological_sort(dag)) + # Use a *lexicographical* topological sort keyed by node index (= program + # order). Any topological order is physically valid, but breaking ties by + # program index guarantees that barrier nodes — measurements in particular, + # which are never merged and therefore each form a singleton group — are + # emitted in program order. Because a measurement's predecessors all have + # smaller indices, this sort can never emit a later measurement before an + # earlier one, so the order in which ``QuantumInstrument`` ops appear in the + # compressed list (and hence the order of measurement-outcome columns in + # :func:`_apply_trajectory_operations`) matches the order of ``MEASURE`` + # instructions in the program, independent of how gates are merged. + topo_order = list(nx.lexicographical_topological_sort(dag)) root_to_nodes: dict[int, list[int]] = {} for nk in topo_order: diff --git a/test/unit/test_trajectory_compression.py b/test/unit/test_trajectory_compression.py new file mode 100644 index 000000000..0dab0a9c2 --- /dev/null +++ b/test/unit/test_trajectory_compression.py @@ -0,0 +1,830 @@ +"""Correctness tests for the compressor used by ``TrajectorySimulator``. + +The simulators in :mod:`pyquil.simulation._simulator` share a *compressor* +(see :func:`pyquil.simulation._resolver.compressor_from_dag` and +``_merge_ops``) which merges adjacent operations into multi-qudit groups when +``max_subsystem_size >= 2``. Compression must be a no-op on the *physics*: the +state produced by a compressed simulation has to match the state produced by an +uncompressed one (and an independent oracle). + +These tests pin that invariant down for the :class:`TrajectorySimulator` across +the full matrix of cases requested: + +* qubit (d=2) and qutrit (d=3) registers, +* gate-only and gate+measurement circuits, +* noiseless and noisy circuits, +* small (2-register) and larger (5-register) circuits. + +All tests run at ``max_subsystem_size=2`` and use **non-sequential** qubit +indices so that the physical-to-logical remapping is exercised. Several +circuits deliberately use multi-qudit gates whose qubit arguments are *not* +sorted (e.g. ``CNOT(2, 5)``, ``TSWAP(2, 5)``) to exercise the embedding of an +operator at non-trivial positions inside a merged subsystem — the path most +likely to be dimension-sensitive. + +Verification strategy +--------------------- +The ground-truth ("oracle") is always produced **without** compression +(``max_subsystem_size=0``), which the user has confirmed to be correct: + +* gate-only oracles use :class:`PureStateVectorSimulator` (pure state) and + :class:`DensityMatrixSimulator` (density matrix); +* noisy / measurement oracles use :class:`DensityMatrixSimulator`. + +For the compressed :class:`TrajectorySimulator` we reconstruct the simulated +density matrix as the Monte-Carlo average of the pure trajectory projectors, + + rho_est = (1 / N) * sum_i |psi_i>ij", m, jnp.conj(m)) / n + return qx.DensityMatrix.from_matrix(rho, dims) + + +def _oracle_density(program, qubits, noise_model=None): + """Ground-truth density matrix via the uncompressed density-matrix simulator.""" + sim = DensityMatrixSimulator( + program, qubits=qubits, noise_model=noise_model, max_subsystem_size=0 + ) + return sim.compute(sim.linearize({})) + + +def _assert_real_compression(program, qubits, noise_model=None): + """Assert the compressor actually merges ops at ``MAX_SUBSYSTEM_SIZE``. + + A test of compression correctness is meaningless if no merging happens, so + every test first confirms the compressed op count is strictly smaller than + the resolved op count. + """ + sim = TrajectorySimulator( + program, qubits=qubits, noise_model=noise_model, + max_subsystem_size=MAX_SUBSYSTEM_SIZE, + ) + params = sim.linearize({}) + resolved = sim.resolve(params) + compressed = sim.compress(resolved) + assert len(compressed) < len(resolved), ( + f"compression did not merge any ops " + f"({len(resolved)} resolved -> {len(compressed)} compressed)" + ) + + +def _trajectory_density(program, qubits, dims, n_traj, *, noise_model=None, + seed=0, max_subsystem_size=MAX_SUBSYSTEM_SIZE): + """Run ``n_traj`` trajectories and reconstruct the simulated density matrix. + + The compressor preserves program order for measurement nodes, so the columns + of ``outcomes`` correspond to the ``MEASURE`` instructions in program order + regardless of ``max_subsystem_size``. No re-ordering is applied here — the + raw column order is part of what these tests verify (see + :func:`test_measurement_outcome_column_order_under_compression`). + + :return: ``(rho_est, outcomes)`` where ``rho_est`` is the Monte-Carlo + density matrix and ``outcomes`` has shape ``(n_traj, n_measurements)``. + """ + sim = TrajectorySimulator( + program, qubits=qubits, noise_model=noise_model, + max_subsystem_size=max_subsystem_size, + ) + params = sim.linearize({}) + keys = jax.random.split(jax.random.key(seed), n_traj) + psi, outcomes = sim.compute(params, keys) + return _density_from_states(psi.matrix, dims), np.asarray(outcomes) + + +def _outcome_distribution(outcomes, dims): + """Empirical joint distribution of measurement outcomes as a length-``d`` vector. + + Each trajectory's per-measurement outcomes (in program / qubit-slot order) + are interpreted as digits in a row-major mixed-radix number with the given + ``dims`` and histogrammed. The resulting index ordering matches the + canonical basis ordering of the density matrix, so the distribution can be + compared directly against the oracle diagonal. + """ + out = np.asarray(outcomes) + n_traj, n_meas = out.shape + radices = np.asarray(dims[:n_meas]) + # Mixed-radix encoding: index = sum_k outcome_k * prod(radices[k+1:]). + weights = np.ones(n_meas, dtype=np.int64) + for k in range(n_meas - 2, -1, -1): + weights[k] = weights[k + 1] * radices[k + 1] + indices = (out * weights).sum(axis=1) + counts = np.bincount(indices, minlength=int(np.prod(radices))) + return counts / n_traj + + +def _total_variation(p, q): + """Total-variation distance between two probability vectors.""" + return 0.5 * float(np.abs(np.asarray(p) - np.asarray(q)).sum()) + + +def _sampler_total_channel(program, qubits, *, noise_model=None, max_subsystem_size): + """Full superoperator the trajectory *sampler* implements, as a dense matrix. + + This composes the per-operation channels exactly as the trajectory sampler + sees them: each operation is converted to its padded Kraus matrices via + :func:`pyquil.simulation._simulator._op_to_kraus_matrix` (the very matrices + fed into the Monte-Carlo sampling loop), promoted to a superoperator, and + embedded into the full register before being composed. + + Building the channel from ``_op_to_kraus_matrix`` (rather than from the + high-level operator objects) means this exercises the padding, outcome/Kraus + axis flattening, and dimension handling that only the trajectory path uses. + The result is deterministic and independent of any sampling, so it is a + far more sensitive probe of compression correctness than a Monte-Carlo + fidelity: a *subtly* wrong error rate at ``max_subsystem_size=2`` shows up + here as a non-zero channel difference even though it would hide under the + statistical noise of a fidelity estimate. + + :return: A dense ``(D**2, D**2)`` superoperator matrix. + """ + sim = TrajectorySimulator( + program, qubits=qubits, noise_model=noise_model, + max_subsystem_size=max_subsystem_size, + ) + operations = sim.adapt(sim.compress(sim.resolve(sim.linearize({})))) + dims = tuple(sim.dims) + dimension = int(np.prod(dims)) + + channel = np.eye(dimension * dimension, dtype=complex) + for op, subsystem in operations: + matrix, _divisor, _is_measure = _op_to_kraus_matrix(op) + kraus = qx.KrausMap.from_matrix( + np.asarray(matrix), (tuple(dims[i] for i in subsystem),) * 2 + ) + embedded = qx.embed( + qx.to_superop(kraus), target_dims=dims, positions=tuple(subsystem) + ) + channel = np.asarray(embedded.matrix) @ channel + return channel + + + +# ══════════════════════════════════════════════════════════ +# Circuit builders +# +# Each builder returns ``(program, qubits, dims)`` and (optionally) a matching +# noise model. Single-qudit gates flanking the entanglers are absorbable by +# the compressor, which is what forces real 2-qudit merges to occur. +# ══════════════════════════════════════════════════════════ + + +def _qubit_circuit_2(): + """2-qubit gate-only circuit on non-sequential qubits [5, 2]. + + ``CNOT(2, 5)`` is intentionally control>target so the two-qubit gate is + embedded at a non-sorted position inside the merged subsystem. + """ + p = Program() + p += H(5) + p += CNOT(2, 5) # control on the high index -> non-sorted subsystem + p += X(5) + p += H(2) + return p, QUBITS_2, (2, 2) + + +def _qubit_circuit_5(): + """5-qubit gate-only circuit on non-sequential qubits [7, 2, 9, 4, 0].""" + p = Program() + for q in QUBITS_5: + p += H(q) + p += CZ(7, 2) + p += CZ(9, 4) + for q, a in zip(QUBITS_5, (0.3, 0.7, 1.1, 0.5, 0.9)): + p += RX(a, q) + p += CNOT(2, 9) # non-sorted relative to qubit ordering + p += CNOT(4, 0) + for q, a in zip(QUBITS_5, (0.2, 0.6, 0.4, 0.8, 1.0)): + p += RZ(a, q) + p += CNOT(0, 7) # spans the two ends -> forces another merge + return p, QUBITS_5, (2, 2, 2, 2, 2) + + +def _qutrit_circuit_2(): + """2-qutrit gate-only circuit on non-sequential qubits [5, 2]. + + ``TSWAP(2, 5)`` is non-sorted, which is the case the user reports as broken + under compression. + """ + p = Program() + p += Gate("TH", [], [5]) + p += Gate("TSWAP", [], [2, 5]) # non-sorted two-qutrit gate + p += Gate("TRX01", [0.9], [2]) + p += Gate("TX", [], [5]) + return p, QUBITS_2, (3, 3) + + +def _qutrit_circuit_5(): + """5-qutrit gate-only circuit on non-sequential qubits [7, 2, 9, 4, 0].""" + p = Program() + for q in QUBITS_5: + p += Gate("TH", [], [q]) + p += Gate("TSWAP", [], [2, 7]) # non-sorted + p += Gate("TSWAP", [], [9, 4]) + for q, a in zip(QUBITS_5, (0.3, 0.7, 1.1, 0.5, 0.9)): + p += Gate("TRX01", [a], [q]) + p += Gate("TSWAP", [], [4, 0]) + p += Gate("TX", [], [7]) + p += Gate("TX", [], [9]) + return p, QUBITS_5, (3, 3, 3, 3, 3) + + +def _depolarizing_model(insts, fidelity): + """Build a depolarizing noise model for the given instructions.""" + return NoiseModel( + channels=[Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) for inst in insts] + ) + + +# ══════════════════════════════════════════════════════════ +# 1-2. Gate-only, no noise +# ══════════════════════════════════════════════════════════ + + +def test_compression_two_qubit_gates_no_noise(): + """2-qubit gate-only circuit: compressed trajectory == uncompressed oracle.""" + program, qubits, dims = _qubit_circuit_2() + _assert_real_compression(program, qubits) + + oracle = _oracle_density(program, qubits) + rho_est, _ = _trajectory_density(program, qubits, dims, n_traj=8) + assert float(qx.fidelity(rho_est, oracle)) > 0.9999 + + # Compressed and uncompressed trajectories must agree exactly (noiseless). + rho_uncompressed, _ = _trajectory_density( + program, qubits, dims, n_traj=8, max_subsystem_size=0 + ) + assert float(qx.fidelity(rho_est, rho_uncompressed)) > 0.9999 + + +def test_compression_five_qubit_gates_no_noise(): + """5-qubit gate-only circuit: compressed trajectory == uncompressed oracle.""" + program, qubits, dims = _qubit_circuit_5() + _assert_real_compression(program, qubits) + + oracle = _oracle_density(program, qubits) + rho_est, _ = _trajectory_density(program, qubits, dims, n_traj=8) + assert float(qx.fidelity(rho_est, oracle)) > 0.9999 + + rho_uncompressed, _ = _trajectory_density( + program, qubits, dims, n_traj=8, max_subsystem_size=0 + ) + assert float(qx.fidelity(rho_est, rho_uncompressed)) > 0.9999 + + +# ══════════════════════════════════════════════════════════ +# 3-4. Gate-only, with noise +# ══════════════════════════════════════════════════════════ + + +def test_compression_two_qubit_gates_with_noise(): + """2-qubit noisy circuit: reconstructed density matrix matches oracle.""" + program, qubits, dims = _qubit_circuit_2() + noise_model = _depolarizing_model( + [CNOT(2, 5), X(5), H(2), H(5)], fidelity=0.97 + ) + _assert_real_compression(program, qubits, noise_model) + + oracle = _oracle_density(program, qubits, noise_model) + rho_est, _ = _trajectory_density( + program, qubits, dims, n_traj=8000, noise_model=noise_model, seed=1 + ) + assert float(qx.fidelity(rho_est, oracle)) > 0.99 + + +def test_compression_five_qubit_gates_with_noise(): + """5-qubit noisy circuit: reconstructed density matrix matches oracle.""" + program, qubits, dims = _qubit_circuit_5() + noise_model = _depolarizing_model( + [CZ(7, 2), CZ(9, 4), CNOT(2, 9), CNOT(4, 0), CNOT(0, 7)], fidelity=0.99 + ) + _assert_real_compression(program, qubits, noise_model) + + oracle = _oracle_density(program, qubits, noise_model) + rho_est, _ = _trajectory_density( + program, qubits, dims, n_traj=12000, noise_model=noise_model, seed=2 + ) + assert float(qx.fidelity(rho_est, oracle)) > 0.98 + + +# ══════════════════════════════════════════════════════════ +# 5-6. Qutrit gate-only, no noise +# ══════════════════════════════════════════════════════════ + + +def test_compression_two_qutrit_gates_no_noise(): + """2-qutrit gate-only circuit: the qutrit compression case under test.""" + program, qubits, dims = _qutrit_circuit_2() + _assert_real_compression(program, qubits) + + oracle = _oracle_density(program, qubits) + rho_est, _ = _trajectory_density(program, qubits, dims, n_traj=8) + assert float(qx.fidelity(rho_est, oracle)) > 0.9999 + + rho_uncompressed, _ = _trajectory_density( + program, qubits, dims, n_traj=8, max_subsystem_size=0 + ) + assert float(qx.fidelity(rho_est, rho_uncompressed)) > 0.9999 + + +def test_compression_five_qutrit_gates_no_noise(): + """5-qutrit gate-only circuit: compressed trajectory == uncompressed oracle.""" + program, qubits, dims = _qutrit_circuit_5() + _assert_real_compression(program, qubits) + + oracle = _oracle_density(program, qubits) + rho_est, _ = _trajectory_density(program, qubits, dims, n_traj=8) + assert float(qx.fidelity(rho_est, oracle)) > 0.9999 + + rho_uncompressed, _ = _trajectory_density( + program, qubits, dims, n_traj=8, max_subsystem_size=0 + ) + assert float(qx.fidelity(rho_est, rho_uncompressed)) > 0.9999 + + +# ══════════════════════════════════════════════════════════ +# 7-8. Qutrit gate-only, with noise +# ══════════════════════════════════════════════════════════ + + +def test_compression_two_qutrit_gates_with_noise(): + """2-qutrit noisy circuit: reconstructed density matrix matches oracle.""" + program, qubits, dims = _qutrit_circuit_2() + noise_model = _depolarizing_model( + [Gate("TSWAP", [], [2, 5]), Gate("TX", [], [5]), Gate("TH", [], [5])], + fidelity=0.98, + ) + _assert_real_compression(program, qubits, noise_model) + + oracle = _oracle_density(program, qubits, noise_model) + rho_est, _ = _trajectory_density( + program, qubits, dims, n_traj=8000, noise_model=noise_model, seed=3 + ) + assert float(qx.fidelity(rho_est, oracle)) > 0.99 + + +def test_compression_five_qutrit_gates_with_noise(): + """5-qutrit noisy circuit: reconstructed density matrix matches oracle. + + Noise is kept light (per-gate fidelity 0.99 on two gates only) so the + output state stays close to pure and is reconstructable from a tractable + number of trajectories despite the d=243 Hilbert space. + """ + program, qubits, dims = _qutrit_circuit_5() + noise_model = _depolarizing_model( + [Gate("TSWAP", [], [2, 7]), Gate("TSWAP", [], [9, 4])], fidelity=0.99 + ) + _assert_real_compression(program, qubits, noise_model) + + oracle = _oracle_density(program, qubits, noise_model) + rho_est, _ = _trajectory_density( + program, qubits, dims, n_traj=8000, noise_model=noise_model, seed=4 + ) + assert float(qx.fidelity(rho_est, oracle)) > 0.97 + + +# ══════════════════════════════════════════════════════════ +# 9-10. Gates + measurements, no noise +# ══════════════════════════════════════════════════════════ + + +def test_compression_two_qubit_measurements_no_noise(): + """2-qubit Bell circuit with measurements: outcomes are perfectly correlated.""" + program = Program() + program += H(5) + program += CNOT(5, 2) + program += Measurement(qubit=Qubit(5), classical_reg=None) + program += Measurement(qubit=Qubit(2), classical_reg=None) + qubits, dims = QUBITS_2, (2, 2) + + _assert_real_compression(program, qubits) + + oracle = _oracle_density(program, qubits) + rho_est, outcomes = _trajectory_density(program, qubits, dims, n_traj=6000, seed=5) + # Trajectory-averaged (post-measurement) state reproduces the oracle mixture. + assert float(qx.fidelity(rho_est, oracle)) > 0.99 + + # Bell state -> only 00 and 11 ever occur, each ~50%. + dist = _outcome_distribution(outcomes, dims) + oracle_diag = np.real(np.diag(np.asarray(oracle.matrix))) + assert _total_variation(dist, oracle_diag) < 0.05 + assert dist[1] < 1e-9 and dist[2] < 1e-9 # |01> and |10> never measured + + +def test_compression_five_qubit_measurements_no_noise(): + """5-qubit circuit with terminal measurements on all qubits.""" + program, qubits, dims = _qubit_circuit_5() + for q in qubits: + program += Measurement(qubit=Qubit(q), classical_reg=None) + + _assert_real_compression(program, qubits) + + oracle = _oracle_density(program, qubits) + rho_est, outcomes = _trajectory_density(program, qubits, dims, n_traj=8000, seed=6) + assert float(qx.fidelity(rho_est, oracle)) > 0.98 + + dist = _outcome_distribution(outcomes, dims) + oracle_diag = np.real(np.diag(np.asarray(oracle.matrix))) + assert _total_variation(dist, oracle_diag) < 0.06 + + +# ══════════════════════════════════════════════════════════ +# 11-12. Gates + measurements, with noise +# ══════════════════════════════════════════════════════════ + + +def test_compression_two_qubit_measurements_with_noise(): + """2-qubit Bell circuit with gate noise and readout error.""" + program = Program() + program += H(5) + program += CNOT(5, 2) + meas5 = Measurement(qubit=Qubit(5), classical_reg=None) + meas2 = Measurement(qubit=Qubit(2), classical_reg=None) + program += meas5 + program += meas2 + qubits, dims = QUBITS_2, (2, 2) + + noise_model = NoiseModel( + channels=[ + Channel.from_gate_fidelity(inst=CNOT(5, 2), fidelity=0.97), + Channel.from_gate_fidelity(inst=H(5), fidelity=0.98), + MeasurementChannel.from_readout_fidelity(inst=meas5, fidelity=0.95), + MeasurementChannel.from_readout_fidelity(inst=meas2, fidelity=0.95), + ] + ) + _assert_real_compression(program, qubits, noise_model) + + oracle = _oracle_density(program, qubits, noise_model) + rho_est, outcomes = _trajectory_density( + program, qubits, dims, n_traj=8000, noise_model=noise_model, seed=7 + ) + assert float(qx.fidelity(rho_est, oracle)) > 0.99 + + # Readout confusion error perturbs the reported outcomes (not the QND + # post-measurement state), so the outcome distribution is compared against + # the uncompressed run rather than the density-matrix diagonal. + dist = _outcome_distribution(outcomes, dims) + _, outcomes_uncompressed = _trajectory_density( + program, qubits, dims, n_traj=8000, noise_model=noise_model, seed=7, + max_subsystem_size=0, + ) + dist_uncompressed = _outcome_distribution(outcomes_uncompressed, dims) + assert _total_variation(dist, dist_uncompressed) < 0.04 + + +def test_compression_five_qubit_measurements_with_noise(): + """5-qubit circuit with gate noise, readout error, and terminal measurements.""" + program, qubits, dims = _qubit_circuit_5() + measurements = [] + for q in qubits: + m = Measurement(qubit=Qubit(q), classical_reg=None) + measurements.append(m) + program += m + + noise_model = NoiseModel( + channels=[ + Channel.from_gate_fidelity(inst=CZ(7, 2), fidelity=0.99), + Channel.from_gate_fidelity(inst=CZ(9, 4), fidelity=0.99), + Channel.from_gate_fidelity(inst=CNOT(2, 9), fidelity=0.99), + Channel.from_gate_fidelity(inst=CNOT(4, 0), fidelity=0.99), + Channel.from_gate_fidelity(inst=CNOT(0, 7), fidelity=0.99), + ] + + [MeasurementChannel.from_readout_fidelity(inst=m, fidelity=0.96) for m in measurements] + ) + _assert_real_compression(program, qubits, noise_model) + + oracle = _oracle_density(program, qubits, noise_model) + rho_est, outcomes = _trajectory_density( + program, qubits, dims, n_traj=12000, noise_model=noise_model, seed=8 + ) + assert float(qx.fidelity(rho_est, oracle)) > 0.97 + + # Readout confusion error perturbs the reported outcomes (not the QND + # post-measurement state), so the outcome distribution is compared against + # the uncompressed run rather than the density-matrix diagonal. + dist = _outcome_distribution(outcomes, dims) + _, outcomes_uncompressed = _trajectory_density( + program, qubits, dims, n_traj=12000, noise_model=noise_model, seed=8, + max_subsystem_size=0, + ) + dist_uncompressed = _outcome_distribution(outcomes_uncompressed, dims) + assert _total_variation(dist, dist_uncompressed) < 0.05 + + +# ══════════════════════════════════════════════════════════ +# Regression: measurement-outcome column order under compression +# ══════════════════════════════════════════════════════════ + + +def test_measurement_outcome_column_order_under_compression(): + """Outcome columns follow ``MEASURE`` program order even when gates merge. + + The compressor merges gates into multi-qudit groups, which reorders the + topological emission of operations. Measurement nodes must nonetheless be + emitted in program order so that ``outcomes[:, i]`` corresponds to the + *i*-th ``MEASURE`` instruction. This pins down that invariant directly by + preparing a distinct, deterministic basis state on every qubit and checking + each column independently — at ``max_subsystem_size=2`` (compressed) against + both the analytic expectation and the uncompressed (``max=0``) run. + """ + qubits = QUBITS_5 # [7, 2, 9, 4, 0] + dims = (2, 2, 2, 2, 2) + + # X on qubits 7, 9, 0 -> per-slot states [1, 0, 1, 0, 1] for slots 0..4. + program = Program() + program += X(7) + program += X(9) + program += X(0) + # Single-qubit gates flanking an entangler force real merges around the + # measurement barriers without changing the deterministic outcomes. + program += H(2) + program += H(2) # H*H = I on slot 1, but creates a mergeable group + program += CNOT(7, 9) # both already |1>: CNOT leaves 7->1, 9 flips 1->0 + program += CNOT(7, 9) # flip back: 9 -> 1, restoring [1,0,1,0,1] + # Measure in program order: qubits 7, 2, 9, 4, 0 == slots 0, 1, 2, 3, 4. + for q in qubits: + program += Measurement(qubit=Qubit(q), classical_reg=None) + + _assert_real_compression(program, qubits) + + expected = np.array([1, 0, 1, 0, 1]) + + _, outcomes_compressed = _trajectory_density( + program, qubits, dims, n_traj=16, seed=0, max_subsystem_size=2 + ) + _, outcomes_uncompressed = _trajectory_density( + program, qubits, dims, n_traj=16, seed=0, max_subsystem_size=0 + ) + + # Every trajectory is deterministic; every column must match program order. + assert outcomes_compressed.shape == (16, 5) + assert np.all(outcomes_compressed == expected) + assert np.all(outcomes_uncompressed == expected) + # Compressed and uncompressed must agree column-for-column. + assert np.array_equal(outcomes_compressed, outcomes_uncompressed) + + +# ══════════════════════════════════════════════════════════ +# Tightened: deterministic total-channel equality across sizes +# ══════════════════════════════════════════════════════════ + + +def _noisy_channel_cases(): + """Yield ``(id, program, qubits, noise_model)`` for every noisy circuit. + + These reuse the noisy circuits exercised by the Monte-Carlo tests above for + a deterministic, sampling-free channel comparison. The 5-qutrit circuit is + deliberately omitted: its dense superoperator is ``243**2 x 243**2`` (~52 + GiB), which is intractable. That case stays covered by the Monte-Carlo + fidelity test; the cases below already span qubit and qutrit registers, + non-sorted multi-qudit gates, and 2- and 5-register sizes. + """ + p2, q2, _ = _qubit_circuit_2() + yield ( + "qubit2", + p2, + q2, + _depolarizing_model([CNOT(2, 5), X(5), H(2), H(5)], fidelity=0.97), + ) + + p5, q5, _ = _qubit_circuit_5() + yield ( + "qubit5", + p5, + q5, + _depolarizing_model( + [CZ(7, 2), CZ(9, 4), CNOT(2, 9), CNOT(4, 0), CNOT(0, 7)], fidelity=0.99 + ), + ) + + pq2, qq2, _ = _qutrit_circuit_2() + yield ( + "qutrit2", + pq2, + qq2, + _depolarizing_model( + [Gate("TSWAP", [], [2, 5]), Gate("TX", [], [5]), Gate("TH", [], [5])], + fidelity=0.98, + ), + ) + + +@pytest.mark.parametrize( + "case", _noisy_channel_cases(), ids=lambda c: c[0] +) +def test_compression_preserves_total_channel_exactly(case): + """Compression must not change the *channel* the sampler implements. + + The Monte-Carlo fidelity tests above can only resolve an error rate to + within their statistical noise (~1e-2). A *subtly* wrong error rate at + ``max_subsystem_size=2`` — exactly the symptom under investigation — would + slip beneath that floor. This test removes the sampling entirely: it builds + the exact dense superoperator the trajectory sampler implements at each + ``max_subsystem_size`` and asserts the compressed channel equals the + uncompressed one to machine precision. + + Because the channel is reconstructed from + :func:`pyquil.simulation._simulator._op_to_kraus_matrix`, this directly + exercises the merged-Kraus padding/embedding path that only compression + triggers, for both qubit (d=2) and qutrit (d=3) registers including + non-sorted multi-qudit gates. + """ + _id, program, qubits, noise_model = case + + reference = _sampler_total_channel( + program, qubits, noise_model=noise_model, max_subsystem_size=0 + ) + for size in (1, 2): + channel = _sampler_total_channel( + program, qubits, noise_model=noise_model, max_subsystem_size=size + ) + max_abs_diff = float(np.abs(channel - reference).max()) + assert max_abs_diff < 1e-10, ( + f"compression at max_subsystem_size={size} changed the channel " + f"(max |ΔS| = {max_abs_diff:.2e})" + ) + + +# ══════════════════════════════════════════════════════════ +# Tightened: per-qubit error rate is mapped to the correct column +# ══════════════════════════════════════════════════════════ + + +def test_asymmetric_readout_error_rate_column_mapping_under_compression(): + """Each qubit's readout error rate stays on its own outcome column. + + A column-permutation bug under compression would swap which qubit a measured + error rate is attributed to. A *symmetric* readout model cannot detect such + a swap, so this test gives the two measured qubits **very different** error + rates (one ~30%, one ~1%) and interleaves a mergeable gate block between the + two ``MEASURE`` instructions. At ``max_subsystem_size=2`` the gate block + collapses to a single merged operator sitting between the measurement + barriers — the configuration most likely to disturb measurement emission + order — yet each column must still report its own qubit's error rate. + """ + qubits = QUBITS_2 # [5, 2] + + program = Program() + program += X(2) # slot 1 (qubit 2) -> |1>, slot 0 (qubit 5) -> |0> + meas2 = Measurement(qubit=Qubit(2), classical_reg=None) # program-order col 0 + meas5 = Measurement(qubit=Qubit(5), classical_reg=None) # program-order col 1 + program += meas2 + # Mergeable gate block between the two measurements (identity on the state: + # H(5) H(5) = I, CNOT(2,5) CNOT(2,5) = I) but it forces a real 2-qubit merge. + program += H(5) + program += CNOT(2, 5) + program += CNOT(2, 5) + program += H(5) + program += meas5 + + # qubit 2 measured |1> with 30% flip (fidelity 0.70); qubit 5 measured |0> + # with 1% flip (fidelity 0.99). The two error rates are deliberately far + # apart so any column swap is unmistakable. + noise_model = NoiseModel( + channels=[ + MeasurementChannel.from_readout_fidelity(inst=meas2, fidelity=0.70), + MeasurementChannel.from_readout_fidelity(inst=meas5, fidelity=0.99), + ] + ) + _assert_real_compression(program, qubits, noise_model) + + n_traj = 20000 + dims = (2, 2) + for size in (0, 1, 2): + _, outcomes = _trajectory_density( + program, qubits, dims, n_traj, noise_model=noise_model, + seed=0, max_subsystem_size=size, + ) + # Column 0 == qubit 2 (|1>, 30% flip to 0) -> P(=1) ~ 0.70. + # Column 1 == qubit 5 (|0>, 1% flip to 1) -> P(=1) ~ 0.01. + p_col0_one = outcomes[:, 0].mean() + p_col1_one = outcomes[:, 1].mean() + assert abs(p_col0_one - 0.70) < 0.02, ( + f"size {size}: qubit-2 error rate landed on the wrong column " + f"(col0 P(=1)={p_col0_one:.3f}, expected ~0.70)" + ) + assert abs(p_col1_one - 0.01) < 0.01, ( + f"size {size}: qubit-5 error rate landed on the wrong column " + f"(col1 P(=1)={p_col1_one:.3f}, expected ~0.01)" + ) + + +# ══════════════════════════════════════════════════════════ +# Regression: compression must not reorder mid-circuit measurements +# ══════════════════════════════════════════════════════════ + + +def test_compression_does_not_merge_gates_across_mid_circuit_measurement(): + """Gates straddling a mid-circuit measurement must not be fused. + + This pins down the bug that produced a *different logical error rate* under + compression in repetition-code experiments. Two gates that act on the same + qubits but sit on opposite sides of a mid-circuit ``MEASURE`` form a DAG + edge (they share a qubit), so a size-only merge check happily fuses them — + which silently moves the measurement to *after* both gates. In a circuit + with repeated syndrome extraction this corrupts every round and inflates the + logical error rate. + + The compressor must keep the merged group *convex*: it may only fuse two + operations if no barrier (or any other operation) lies on a dependency path + between them. Here a non-trivial unitary surrounds a mid-circuit + measurement, so the merged ``emit_order`` at ``max_subsystem_size=2`` must + still place the measurement between the two gate groups, and the sampled + outcome distribution must match the uncompressed run. + """ + qubits = QUBITS_2 # [5, 2] + dims = (2, 2) + + program = Program() + program += H(5) + program += CNOT(5, 2) # entangle 5 and 2 + mid = Measurement(qubit=Qubit(2), classical_reg=None) # collapse slot 1 + program += mid + program += H(2) + program += CNOT(5, 2) # acts on the same pair again, *after* the measurement + program += Measurement(qubit=Qubit(5), classical_reg=None) + program += Measurement(qubit=Qubit(2), classical_reg=None) + + _assert_real_compression(program, qubits) + + # The mid-circuit measurement must remain sandwiched between the two + # two-qubit gate groups; the two CNOTs must NOT be fused into one operator. + sim = TrajectorySimulator( + program, qubits=qubits, max_subsystem_size=MAX_SUBSYSTEM_SIZE + ) + emitted = sim.compress(sim.resolve(sim.linearize({}))) + op_types = [type(op).__name__ for op, _ in emitted] + # Exactly three QuantumInstruments (one mid-circuit + two terminal) and the + # first must appear before the second gate group — i.e. there is a gate + # operation emitted *after* the first measurement. + first_measure = op_types.index("QuantumInstrument") + assert any( + name != "QuantumInstrument" for name in op_types[first_measure + 1 :] + ), f"a gate group must follow the mid-circuit measurement, got {op_types}" + + # The sampled outcome statistics must be independent of compression. There + # are three measurements (mid-circuit on slot 1, then terminal on slots 0 + # and 1), so the joint distribution is histogrammed over 2**3 = 8 patterns. + def _joint(max_subsystem_size): + _, outcomes = _trajectory_density( + program, qubits, dims, 40000, seed=0, + max_subsystem_size=max_subsystem_size, + ) + codes = outcomes[:, 0] * 4 + outcomes[:, 1] * 2 + outcomes[:, 2] + return np.bincount(codes, minlength=8) / len(codes) + + assert _total_variation(_joint(0), _joint(2)) < 0.02 + + + + From dec685140345feee0a9841552fcb06b20af90d15 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 12:26:27 +0000 Subject: [PATCH 20/37] Small fixes --- poetry.lock | 10 +-- pyproject.toml | 2 +- pyquil/simulation/_resolver.py | 63 +++++++++++---- pyquil/simulation/_simulator.py | 116 +++++++++++++++------------- test/unit/test_qutrit_simulation.py | 23 ++++-- test/unit/test_resolver.py | 16 ++-- test/unit/test_state_vector.py | 30 ++++++- 7 files changed, 173 insertions(+), 87 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2517fa8b6..c2f1fba90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -3270,14 +3270,14 @@ httpx = ">=0.21.0" [[package]] name = "rigetti-quax" -version = "0.6.3" +version = "0.6.4" description = "A high-performance library for quantum information science built on top of JAX" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ - {file = "rigetti_quax-0.6.3-py3-none-any.whl", hash = "sha256:caa46c36c805a45ee4572a3d6fd167a2d2353ad1ec95d13931ab941c1e249fa5"}, - {file = "rigetti_quax-0.6.3.tar.gz", hash = "sha256:d1cbb64f9095a78e5f596c48c7b1a2ca38c841cfd81ccc960c369a85391a82d0"}, + {file = "rigetti_quax-0.6.4-py3-none-any.whl", hash = "sha256:8d18d0ccfda2b2469f71b94f58c6dd936928e0656aff2f065123816e948eaaf0"}, + {file = "rigetti_quax-0.6.4.tar.gz", hash = "sha256:c1f46381f70c7ee2d25128fc88ead0984846c9c31f5f3d170c0c8efe7324e9f3"}, ] [package.dependencies] @@ -4113,4 +4113,4 @@ latex = ["ipython"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.13" -content-hash = "3ede6a989f05bee1b80907269ff0cd6a2a3e14707124c3b0ff561e3f95f8610f" +content-hash = "44463a850522ab243601e2c1e21773a37e88e4e0308e8d45faeae0132cb6b2a8" diff --git a/pyproject.toml b/pyproject.toml index 66e1f05f7..6cef5700d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ pandoc = {version = "2.4b0", optional = true} matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} -rigetti-quax = ">=0.6.2" +rigetti-quax = ">=0.6.4" [tool.poetry.extras] latex = ["ipython"] diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index be1f77db0..d5c44d2b0 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -33,10 +33,11 @@ import heapq import logging -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterator, Mapping from copy import deepcopy from typing import Any, TypeAlias, cast +import jax.numpy as jnp import networkx as nx import quax as qx from jax import Array @@ -92,7 +93,7 @@ def __init__( def __call__(self, params: Array) -> qx.Unitary: resolved: list[Any] = [] - for pi, cv in zip(self.param_indices, self.concrete_values): + for pi, cv in zip(self.param_indices, self.concrete_values, strict=False): if pi >= 0: resolved.append(params[pi]) else: @@ -184,6 +185,7 @@ def _measure_registers(program: Program) -> set[str]: def expand_program( program: Program, noise_model: NoiseModelLike | None = None, + qubit_dimensions: Mapping[int, int] | None = None, ) -> tuple[list[ExpandedOp], list[tuple[int, ...]], list[tuple[str, int]]]: """Expand a program into operators and physical qubit tuples. @@ -204,6 +206,9 @@ def expand_program( :param program: Quil program (may contain DEFCIRCUITs). :param noise_model: Optional noise model. + :param qubit_dimensions: Optional mapping from physical qubit id to its + Hilbert-space dimension. Used for ideal measurement and reset operators, + whose quax constructors otherwise default to qubit dimension. :return: Tuple of ``(ops, qubit_tuples, param_refs)`` where each op is either a concrete quax operator or a ``Callable[[Array], Unitary]`` for parameterized gates, each qubit tuple contains physical qubit @@ -270,13 +275,16 @@ def _resolve_gate(inst: Gate) -> tuple[ExpandedOp, tuple[int, ...]]: unitary = get_instruction_unitary(inst, custom_gates=custom_gates) return unitary, qubits + def _dimension_for(qubit: int) -> int: + return qubit_dimensions.get(qubit, 2) if qubit_dimensions is not None else 2 + def _resolve_measurement(inst: Measurement) -> tuple[FixedOp, tuple[int, ...]]: """Resolve a measurement instruction.""" qubits = tuple(inst.get_qubit_indices()) channel = noise_model.get_channel(inst) if noise_model is not None else None if isinstance(channel, MeasurementChannel): return channel.process, qubits - return qx.gates.MEASURE(), qubits + return qx.gates.MEASURE(dim=_dimension_for(qubits[0])), qubits def _resolve_reset_qubit(inst: ResetQubit) -> tuple[FixedOp, tuple[int, ...]]: """Resolve a targeted reset instruction.""" @@ -284,7 +292,7 @@ def _resolve_reset_qubit(inst: ResetQubit) -> tuple[FixedOp, tuple[int, ...]]: channel = noise_model.get_channel(inst) if noise_model is not None else None if isinstance(channel, ResetChannel): return channel.process, qubits - return qx.gates.RESET(), qubits + return qx.gates.RESET(dim=_dimension_for(qubits[0])), qubits def _emit_instruction(inst: Gate | Measurement | ResetQubit | Reset) -> None: """Resolve and emit a single instruction.""" @@ -300,7 +308,7 @@ def _emit_instruction(inst: Gate | Measurement | ResetQubit | Reset) -> None: _emit_op(op, qubits) case Reset(): for q in all_qubits: - _emit_op(qx.gates.RESET(), (q,)) + _emit_op(qx.gates.RESET(dim=_dimension_for(q)), (q,)) for inst in program.instructions: if isinstance(inst, DefCircuit): @@ -365,6 +373,15 @@ def build_dag(qubit_tuples: list[tuple[int, ...]]) -> nx.DiGraph: return dag +def _infer_dims(resolved: list[ResolvedOp], n_qubits: int) -> tuple[int, ...]: + """Infer per-qudit dimensions from resolved operators.""" + dims = [2] * n_qubits + for op, subsystem in resolved: + for q, d in zip(subsystem, op.dims[1], strict=False): + dims[q] = max(dims[q], d) + return tuple(dims) + + # ══════════════════════════════════════════════════════════ # Resolver # ══════════════════════════════════════════════════════════ @@ -420,19 +437,31 @@ def resolver_from_program( don't appear in the program. :return: Tuple of ``(Resolver, dag)``. """ - # Phase 1: Expand into flat operators + physical qubit tuples. - expanded_ops, phys_qubits, _param_refs = expand_program(program, noise_model) - - # Phase 2: Remap physical qubits to 0-based indices. if qubits is None: qubits = sorted(program.get_qubit_indices()) qubit_indices = {q: i for i, q in enumerate(qubits)} - mapped_qubits = remap_qubits(phys_qubits, qubit_indices) + n_qubits = len(qubits) - # Phase 3: Build dependency DAG. - dag = build_dag(mapped_qubits) + initial_expanded_ops, initial_phys_qubits, initial_param_refs = expand_program(program, noise_model) + initial_mapped_qubits = remap_qubits(initial_phys_qubits, qubit_indices) + initial_frozen_ops = list(zip(initial_expanded_ops, initial_mapped_qubits, strict=False)) + + def initial_resolve(params: Array) -> list[ResolvedOp]: + return [ + (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) + for item, subsystem in initial_frozen_ops + ] + + dims = _infer_dims(initial_resolve(jnp.zeros(len(initial_param_refs))), n_qubits) - # Phase 4: Build the resolve closure. + qubit_dimensions = {q: dims[i] for q, i in qubit_indices.items()} + expanded_ops, phys_qubits, _param_refs = expand_program( + program, + noise_model, + qubit_dimensions=qubit_dimensions, + ) + mapped_qubits = remap_qubits(phys_qubits, qubit_indices) + dag = build_dag(mapped_qubits) frozen_ops = list(zip(expanded_ops, mapped_qubits, strict=False)) def resolve(params: Array) -> list[ResolvedOp]: @@ -441,9 +470,6 @@ def resolve(params: Array) -> list[ResolvedOp]: for item, subsystem in frozen_ops ] - n_qubits = len(qubits) - dims = (2,) * n_qubits - return Resolver(resolve, dims=dims), dag @@ -622,10 +648,15 @@ def compressor_from_dag( n_original = dag.number_of_nodes() if max_subsystem_size == 0 or n_original == 0: + emit_order = [ + (nk, [nk], tuple(dag.nodes[nk]["qubits"])) + for nk in nx.lexicographical_topological_sort(dag) + ] def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: return ops + compress_passthrough.emit_order = emit_order # type: ignore[attr-defined] logger.info( "Compressor: %d ops (no merging), max_subsystem_size=0", n_original, diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 0e9eadb24..8b6bb7d4d 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -173,14 +173,38 @@ def __init__( self.qubits = qubits self.n_qubits = len(qubits) - # Build resolver from the expanded program. - expanded_ops, phys_qubits, param_refs = expand_program(program, noise_model) - - # Remap the qudits from 0 to n qubit_indices = {q: i for i, q in enumerate(qubits)} + + # First expand with default ideal measurement/reset dimensions so we can + # infer the register dimensions from gates and noisy channels. + if dims is None: + initial_expanded_ops, initial_phys_qubits, initial_param_refs = expand_program( + program, + noise_model, + ) + initial_mapped_qubits = remap_qubits(initial_phys_qubits, qubit_indices) + initial_frozen_ops = list(zip(initial_expanded_ops, initial_mapped_qubits, strict=False)) + + def initial_resolve(params: Array) -> list[ResolvedOp]: + return [ + (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) + for item, subsystem in initial_frozen_ops + ] + + self.dims = _infer_dims(initial_resolve(jnp.zeros(len(initial_param_refs))), self.n_qubits) + else: + self.dims = dims + + # Expand again with inferred dimensions so ideal measurement/reset + # instruments are not stuck at qubit dimension. + qubit_dimensions = {q: self.dims[i] for q, i in qubit_indices.items()} + expanded_ops, phys_qubits, param_refs = expand_program( + program, + noise_model, + qubit_dimensions=qubit_dimensions, + ) mapped_qubits = remap_qubits(phys_qubits, qubit_indices) dag = build_dag(mapped_qubits) - frozen_ops = list(zip(expanded_ops, mapped_qubits, strict=False)) def resolve(params: Array) -> list[ResolvedOp]: @@ -201,17 +225,6 @@ def linearize(memory_map: MemoryMap) -> Array: self._expanded_ops = tuple(expanded_ops) self._raw_subsystems = tuple(mapped_qubits) - # Per-qudit dimensions: use the explicit override when provided, - # otherwise infer each slot's dimension from the operators acting on it - # (e.g. a qutrit gate upgrades its slots to dimension 3). Inference - # resolves the ops once with zero parameters; the matrix *shapes* are - # parameter-independent, so this yields the correct dimensions for any - # parameter values. - if dims is not None: - self.dims = dims - else: - self.dims = _infer_dims(resolve(jnp.zeros(len(param_refs))), self.n_qubits) - # Whether any gate matrix depends on a runtime parameter. When it does # not, the compressed operator stack is a compile-time constant and can # be materialised eagerly (outside the traced graph), which avoids XLA @@ -315,60 +328,59 @@ def _make_embed_fn( if op_subsystem == group_subsystem: D = math.prod(dims[q] for q in op_subsystem) pad_w = ((0, d_max - D),) * 2 + def _identity_embed(mat: Array) -> Array: return jnp.pad(mat, pad_w) + return _identity_embed - # General case: embed via Kronecker products. - # The group_subsystem is ordered; figure out which positions in the group - # the op occupies, and insert identities for the remaining positions. + # General case: embed a tensor-format operator by placing its output/input + # axes at the requested positions and identity tensors on untouched axes. target_dims = tuple(dims[q] for q in group_subsystem) + op_dims = tuple(dims[q] for q in op_subsystem) positions = tuple(group_subsystem.index(q) for q in op_subsystem) n_group = len(group_subsystem) D = math.prod(target_dims) pad_w = ((0, d_max - D),) * 2 - - # Precompute which group positions are "identity" positions. - # Strategy: reshape the op matrix into a tensor, then embed into the full - # group tensor, then reshape back. This avoids the overhead of qx objects. - op_dims = tuple(dims[q] for q in op_subsystem) n_op = len(op_subsystem) - # Build a permutation: the full group tensor has axes for each qubit. - # We place op axes at their positions, then the remaining axes carry identity. - # Result = I ⊗ ... ⊗ op ⊗ ... ⊗ I (with op spread across `positions`). - # For the common case: 1-qubit gate in a 2-qubit group if n_op == 1 and n_group == 2 and all(d == 2 for d in target_dims): pos = positions[0] I2 = jnp.eye(2, dtype=complex) if pos == 0: + def _embed(mat: Array) -> Array: return jnp.pad(jnp.kron(mat, I2), pad_w) + return _embed else: + def _embed(mat: Array) -> Array: return jnp.pad(jnp.kron(I2, mat), pad_w) + return _embed - # General fallback: use einsum-based embedding - # Build the full unitary as a tensor product with identities. non_op_positions = [i for i in range(n_group) if i not in positions] - non_op_dims = [target_dims[i] for i in non_op_positions] - identity_factors = [jnp.eye(d, dtype=complex) for d in non_op_dims] + identity_factors = [jnp.eye(target_dims[i], dtype=complex) for i in non_op_positions] + + # Example for op positions (0, 2) in a 3-qudit group: + # op tensor axes are out0,out2,in0,in2; identity axes are out1,in1; + # output order must be out0,out1,out2,in0,in1,in2. + labels = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + if 2 * n_group > len(labels): + raise ValueError(f"Cannot build an einsum embedding for {n_group} subsystems.") + out_labels = labels[:n_group] + in_labels = labels[n_group : 2 * n_group] + op_subscript = "".join(out_labels[p] for p in positions) + "".join(in_labels[p] for p in positions) + identity_subscripts = [out_labels[p] + in_labels[p] for p in non_op_positions] + embedded_subscript = out_labels + in_labels + einsum_spec = ",".join([op_subscript, *identity_subscripts]) + f"->{embedded_subscript}" def _embed_general(mat: Array) -> Array: - # Reshape op into a tensor - op_tensor = mat.reshape([op_dims[i] for i in range(n_op)] * 2) - # Build full group tensor via einsum - # Start with the op tensor, then kron with identities for non-op positions - result = mat - for i, nop in enumerate(non_op_positions): - # Determine if this identity goes before or after - d = non_op_dims[i] - # Simple sequential kron (ordering may need adjustment) - result = jnp.kron(result, identity_factors[i]) if nop > max(positions) else jnp.kron(identity_factors[i], result) - return jnp.pad(result, pad_w) + op_tensor = mat.reshape(op_dims + op_dims) + embedded = jnp.einsum(einsum_spec, op_tensor, *identity_factors) + return jnp.pad(embedded.reshape(D, D), pad_w) return _embed_general @@ -453,7 +465,7 @@ def _build_vectorized_unitary_constructor( pos_arr = np.array(positions) parametric_slots = [j for j, pi in enumerate(template_pi) if pi >= 0] - concrete_slots = [(j, cv) for j, (pi, cv) in enumerate(zip(template_pi, template_cv)) if pi < 0] + concrete_slots = [(j, cv) for j, (pi, cv) in enumerate(zip(template_pi, template_cv, strict=False)) if pi < 0] n_total = len(template_pi) def _make_batch(gf: Callable, ps: list[int], cs: list[tuple[int, float]], @@ -872,15 +884,13 @@ def _op_to_kraus_matrix( case qx.KrausMap(): return op.matrix, 1, False case qx.QuantumInstrument(): - kraus_map = qx.superop_to_kraus(qx.SuperOp(op.data, op.num_qubits)) - data = kraus_map.data - n_ens_i = len(op.ensemble_size) - shape = data.shape - n_kraus_per_outcome = shape[n_ens_i + 1] - n_total_kraus = op.num_outcomes * n_kraus_per_outcome - data = data.reshape(shape[:n_ens_i] + (n_total_kraus,) + shape[n_ens_i + 2 :]) - merged = qx.KrausMap(data=data, num_qubits=kraus_map.num_qubits) - return merged.matrix, n_kraus_per_outcome, True + kraus_mats = [ + qx.superop_to_kraus(op.outcome_superop(i)[0]).matrix + for i in range(op.num_outcomes) + ] + n_kraus_per_outcome = kraus_mats[0].shape[-3] + merged = jnp.concatenate(kraus_mats, axis=-3) + return merged, n_kraus_per_outcome, True case _: raise TypeError(f"Unsupported operator type: {type(op)}") diff --git a/test/unit/test_qutrit_simulation.py b/test/unit/test_qutrit_simulation.py index f5be64067..4df91be31 100644 --- a/test/unit/test_qutrit_simulation.py +++ b/test/unit/test_qutrit_simulation.py @@ -6,16 +6,15 @@ import pytest import quax as qx -from pyquil.gates import H, MEASURE, X +from pyquil.gates import H, X +from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel +from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program from pyquil.quilatom import MemoryReference, Qubit from pyquil.quilbase import Declare, DefGate, Gate, Measurement - -from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel -from pyquil.noise._noise_model import NoiseModel from pyquil.simulation._simulator import ( - PureStateVectorSimulator, DensityMatrixSimulator, + PureStateVectorSimulator, TrajectorySimulator, ) @@ -431,6 +430,20 @@ def test_qutrit_measure_second_excited(self): ) assert jnp.all(outcomes == 1) + def test_qutrit_ideal_reset(self): + """An ideal qutrit reset returns any qutrit level to |0>.""" + from pyquil.quilbase import ResetQubit + + p = Program() + p += Gate("TX", [], [0]) + p += ResetQubit(Qubit(0)) + p += Measurement(qubit=Qubit(0), classical_reg=None) + + outcomes = _sample( + p, qubits=[0], num_trajectories=100, random_seed=42 + ) + assert jnp.all(outcomes == 0) + def test_qutrit_measure_superposition_statistics(self): """TH|0> = (|0>+|1>+|2>)/sqrt(3) gives uniform measurement distribution.""" p = Program() diff --git a/test/unit/test_resolver.py b/test/unit/test_resolver.py index 9a04dc3a0..62847abf7 100644 --- a/test/unit/test_resolver.py +++ b/test/unit/test_resolver.py @@ -2,11 +2,10 @@ import jax.numpy as jnp import numpy as np -import pytest import quax as qx -from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, RZ, H, X -from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel +from pyquil.gates import CNOT, H, MEASURE, RESET, RX, RZ, X +from pyquil.noise._channels import Channel, CycleChannel from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program from pyquil.quilatom import FormalArgument, MemoryReference, Qubit @@ -15,11 +14,8 @@ DefCircuit, Gate, Measurement, - Reset, - ResetQubit, ) from pyquil.simulation._resolver import ( - _measure_registers, build_dag, expand_program, remap_qubits, @@ -181,3 +177,11 @@ def test_measurement_and_reset(self): assert len(ops) == 2 assert isinstance(ops[0][0], qx.Unitary) assert isinstance(ops[1][0], qx.QuantumInstrument) + + def test_qutrit_measurement_dimensions(self): + p = Program(Gate("TX", [], [0]), Measurement(Qubit(0), None)) + resolver, _ = resolver_from_program(p) + ops = resolver(_EMPTY_PARAMS) + assert resolver.dims == (3,) + assert isinstance(ops[1][0], qx.QuantumInstrument) + assert ops[1][0].dims == ((3,), (3,)) diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index 5f85e0b78..d2aef07d4 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -6,7 +6,7 @@ import pytest import quax as qx -from pyquil.gates import CNOT, CZ, MEASURE, RESET, RX, RY, RZ, H, X +from pyquil.gates import CNOT, CPHASE, CZ, MEASURE, RESET, RX, RY, RZ, H, X from pyquil.noise._channels import Channel, CycleChannel, MeasurementChannel, ResetChannel from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program @@ -610,6 +610,34 @@ class TestCompressor: # ── max_subsystem_size=0 (no merging) ── + def test_no_merge_pure_state_compute_matches_compressed(self): + """PureStateVectorSimulator should support disabling compression.""" + p = Program(H(0), CNOT(0, 1), RZ(0.5, 0)) + compressed = PureStateVectorSimulator(p) + uncompressed = PureStateVectorSimulator(p, max_subsystem_size=0) + + psi_compressed = compressed.compute(_EMPTY_PARAMS) + psi_uncompressed = uncompressed.compute(_EMPTY_PARAMS) + + assert qx.fidelity(psi_uncompressed, psi_compressed) > 0.9999 + + def test_parametric_non_contiguous_gate_embeds_in_larger_group(self): + """Parametric multi-qubit gates should embed correctly into larger groups.""" + p = Program() + p += Declare("theta", "REAL", 1) + p += H(0) + p += H(1) + p += H(2) + p += CPHASE(MemoryReference("theta", 0), 0, 2) + p += CPHASE(0.37, 0, 1) + + compressed = PureStateVectorSimulator(p, qubits=[0, 1, 2], max_subsystem_size=3) + uncompressed = PureStateVectorSimulator(p, qubits=[0, 1, 2], max_subsystem_size=0) + params = compressed.linearize({"theta": [0.91]}) + + assert compressed.bases == [(0, 1, 2)] + assert qx.fidelity(compressed.compute(params), uncompressed.compute(params)) > 0.9999 + def test_no_merge_noiseless_matches_direct(self): """max_subsystem_size=0 compressor output should match direct computation.""" p = Program(H(0), CNOT(0, 1), RZ(0.5, 0)) From 9cd6cd347423d55f96589bc23bad54f6459a71e0 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 12:45:25 +0000 Subject: [PATCH 21/37] Update native format of noise model --- pyquil/noise/_noise_model.py | 89 ++++++++++++++++------- test/benchmarks/test_state_vector.py | 4 +- test/unit/test_noise_model.py | 91 +++++++++++++++++++++--- test/unit/test_qutrit_simulation.py | 10 +-- test/unit/test_resolver.py | 6 +- test/unit/test_state_vector.py | 26 +++---- test/unit/test_trajectory_compression.py | 17 +++-- 7 files changed, 176 insertions(+), 67 deletions(-) diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index d47f69bcc..496dbf0a9 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -33,10 +33,11 @@ import json import logging -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass from functools import cached_property, reduce from operator import mul +from types import MappingProxyType from typing import ( TYPE_CHECKING, Protocol, @@ -60,6 +61,7 @@ # Channel union type returned by get_channel ChannelType = Channel | MeasurementChannel | ResetChannel | CycleChannel +NoiseInstruction = Gate | Measurement | ResetQubit @runtime_checkable @@ -99,29 +101,36 @@ class NoiseModel: This includes gate channels, measurement channels, reset channels, and cycle channels. - The constructor accepts any iterable of channels (list, tuple, set, generator, etc.) - which is coerced to a tuple for immutable storage. + The constructor accepts a mapping from instruction to channel. Use + :meth:`from_channels` when constructing from a list, tuple, set, generator, + or other channel iterable. """ - channels: tuple[Channel | MeasurementChannel | ResetChannel | CycleChannel, ...] - """Immutable tuple of all noise channels in the model.""" + channels: Mapping[NoiseInstruction, ChannelType] + """Immutable mapping from instruction to its noise channel.""" def __init__( self, - channels: Iterable[Channel | MeasurementChannel | ResetChannel | CycleChannel] = (), + channels: Mapping[NoiseInstruction, ChannelType] | None = None, ) -> None: - # Accept any iterable, coerce to tuple for immutable storage. - if isinstance(channels, tuple): - object.__setattr__(self, "channels", channels) - else: - object.__setattr__(self, "channels", tuple(channels)) + if channels is None: + channels = {} + if not isinstance(channels, Mapping): + raise TypeError("NoiseModel channels must be a mapping. Use NoiseModel.from_channels(...) for iterables.") + + channel_map: dict[NoiseInstruction, ChannelType] = {} + for inst, channel in channels.items(): + if channel.inst != inst: + raise ValueError(f"NoiseModel channel key {inst!r} does not match channel instruction {channel.inst!r}.") + channel_map[inst] = channel + object.__setattr__(self, "channels", MappingProxyType(channel_map)) @cached_property def _channel_map( self, - ) -> dict[Gate | Measurement | ResetQubit, Channel | MeasurementChannel | ResetChannel | CycleChannel]: + ) -> Mapping[NoiseInstruction, ChannelType]: """Map from instruction to channel for fast lookup.""" - return {ch.inst: ch for ch in self.channels} + return self.channels @overload def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... @@ -146,6 +155,21 @@ def get_channel( # Constructors # ────────────────────────────────────────────── + @classmethod + def from_channels(cls: type[NoiseModel], channels: Iterable[ChannelType] = ()) -> NoiseModel: + """Create a noise model from an iterable of channels. + + :param channels: Noise channels to include in the model. + :return: A NoiseModel keyed by each channel's instruction. + :raises ValueError: If more than one channel targets the same instruction. + """ + channel_map: dict[NoiseInstruction, ChannelType] = {} + for channel in channels: + if channel.inst in channel_map: + raise ValueError(f"Duplicate noise channel for instruction {channel.inst!r}.") + channel_map[channel.inst] = channel + return cls(channels=channel_map) + @classmethod def from_isa(cls: type[NoiseModel], compiler_isa: CompilerISA) -> NoiseModel: """Create a noise model from an instruction set architecture. @@ -218,7 +242,7 @@ def from_isa(cls: type[NoiseModel], compiler_isa: CompilerISA) -> NoiseModel: if fidelity is not None and fidelity < 1.0: channels[inst] = Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) - return cls(channels=channels.values()) + return cls(channels=channels) # ────────────────────────────────────────────── # Serialization @@ -230,7 +254,7 @@ def to_json(self) -> str: :return: JSON string representation. """ channel_data = [] - for ch in self.channels: + for ch in self.channels.values(): if isinstance(ch, (Channel, MeasurementChannel, ResetChannel, CycleChannel)): channel_data.append({"type": type(ch).__name__, "data": ch.to_json()}) else: @@ -257,7 +281,7 @@ def from_json(cls: type[NoiseModel], json_str: str) -> NoiseModel: if ch_cls is None: raise ValueError(f"Unknown channel type: {ch_data['type']}") channels.append(ch_cls.from_json(ch_data["data"])) # type: ignore[attr-defined] - return cls(channels=channels) + return cls.from_channels(channels) # ────────────────────────────────────────────── # Dunder methods @@ -267,7 +291,7 @@ def __eq__(self, other: object) -> bool: """Check if two NoiseModels contain equivalent channel maps.""" if not isinstance(other, NoiseModel): return False - return self._channel_map == other._channel_map + return dict(self.channels) == dict(other.channels) def __hash__(self) -> int: """Hash based on id (NoiseModel is not value-hashable due to array contents).""" @@ -282,24 +306,35 @@ def __add__(self, other: NoiseModel) -> NoiseModel: if not isinstance(other, NoiseModel): return NotImplemented - my_channels = {ch.inst: ch for ch in self.channels} - other_channels = {ch.inst: ch for ch in other.channels} - - combined: list[Channel | MeasurementChannel | ResetChannel | CycleChannel] = [] - all_insts = list(dict.fromkeys(list(my_channels) + list(other_channels))) + combined: dict[NoiseInstruction, ChannelType] = {} + all_insts = list(dict.fromkeys(list(self.channels) + list(other.channels))) for inst in all_insts: - mine = my_channels.get(inst) - theirs = other_channels.get(inst) + mine = self.channels.get(inst) + theirs = other.channels.get(inst) if mine is not None and theirs is not None: # Both have a channel for this instruction — compose them # (only same-type composition is defined) composed = mine @ theirs # type: ignore[operator] - combined.append(composed) + combined[inst] = composed elif mine is not None: - combined.append(mine) + combined[inst] = mine elif theirs is not None: - combined.append(theirs) + combined[inst] = theirs + + return NoiseModel(channels=combined) + def with_channels(self, channels: Iterable[ChannelType]) -> NoiseModel: + """Return a new model with additional channels. + + :param channels: New channels to add. + :return: A NoiseModel containing the existing and new channels. + :raises ValueError: If a new channel targets an instruction already in the model. + """ + combined = dict(self.channels) + for channel in channels: + if channel.inst in combined: + raise ValueError(f"Duplicate noise channel for instruction {channel.inst!r}.") + combined[channel.inst] = channel return NoiseModel(channels=combined) diff --git a/test/benchmarks/test_state_vector.py b/test/benchmarks/test_state_vector.py index 76fa35775..55e881053 100644 --- a/test/benchmarks/test_state_vector.py +++ b/test/benchmarks/test_state_vector.py @@ -73,7 +73,7 @@ def _build_noisy_program_and_model(num_qubits, num_layers, seed=4867): Channel.from_coherence_times(RX(np.pi / 2, q), gate_duration=0.04, t1s=[t1s[q]], t2s=[t2s[q]]) for q in range(num_qubits) ] - noise_model = NoiseModel(channels=channels) + noise_model = NoiseModel.from_channels(channels) program = Program() for _ in range(num_layers): @@ -165,7 +165,7 @@ def _build_surface17_cycle_noise_model(program, depolarizing_constant=0.99, read cycle_channels.append(CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels))) - return NoiseModel(channels=cycle_channels) + return NoiseModel.from_channels(cycle_channels) def _prepare_trajectory_operations(program, noise_model, max_subsystem_size=0): diff --git a/test/unit/test_noise_model.py b/test/unit/test_noise_model.py index 212abd691..c36e94b47 100644 --- a/test/unit/test_noise_model.py +++ b/test/unit/test_noise_model.py @@ -210,14 +210,44 @@ def test_json_roundtrip_preserves_qutrit_dims(self): class TestNoiseModel: def test_empty_model(self): """An empty NoiseModel has no channels.""" - nm = NoiseModel(channels=()) + nm = NoiseModel() assert nm.get_channel(RX(0.5, 0)) is None + def test_constructor_accepts_instruction_mapping(self): + """NoiseModel stores channels keyed by instruction.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + nm = NoiseModel(channels={inst: ch}) + assert nm.channels[inst] is ch + assert nm.get_channel(inst) is ch + + def test_constructor_rejects_channel_iterable(self): + """Sequence construction should go through from_channels.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + with pytest.raises(TypeError, match="from_channels"): + NoiseModel(channels=[ch]) # type: ignore[arg-type] + + def test_constructor_rejects_mismatched_mapping_key(self): + """Mapping keys must match the instruction stored on each channel.""" + inst = RX(np.pi / 4, 0) + ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + with pytest.raises(ValueError, match="does not match"): + NoiseModel(channels={RY(np.pi / 2, 0): ch}) + + def test_from_channels_rejects_duplicates(self): + """Duplicate instruction channels are ambiguous and rejected.""" + inst = RX(np.pi / 4, 0) + ch1 = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + ch2 = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.97) + with pytest.raises(ValueError, match="Duplicate noise channel"): + NoiseModel.from_channels([ch1, ch2]) + def test_get_channel_gate(self): """NoiseModel.get_channel returns the correct Channel for a gate.""" inst = RX(np.pi / 4, 0) ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) - nm = NoiseModel(channels=[ch]) + nm = NoiseModel.from_channels([ch]) retrieved = nm.get_channel(inst) assert retrieved is ch @@ -225,7 +255,7 @@ def test_get_channel_returns_none_for_missing(self): """get_channel returns None for instructions not in the model.""" inst = RX(np.pi / 4, 0) ch = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) - nm = NoiseModel(channels=[ch]) + nm = NoiseModel.from_channels([ch]) other_inst = RY(np.pi / 2, 1) assert nm.get_channel(other_inst) is None @@ -237,11 +267,56 @@ def test_multiple_channels(self): ch1 = Channel.from_depolarizing_constant(inst=inst1, depolarizing_constant=0.99) ch2 = Channel.from_depolarizing_constant(inst=inst2, depolarizing_constant=0.97) ch3 = Channel.from_depolarizing_constant(inst=inst3, depolarizing_constant=0.95) - nm = NoiseModel(channels=[ch1, ch2, ch3]) + nm = NoiseModel.from_channels([ch1, ch2, ch3]) assert nm.get_channel(inst1) is ch1 assert nm.get_channel(inst2) is ch2 assert nm.get_channel(inst3) is ch3 + def test_json_roundtrip(self): + """NoiseModel JSON keeps the existing channel-list wire format.""" + ch = Channel.from_depolarizing_constant(inst=RX(np.pi / 4, 0), depolarizing_constant=0.98) + meas_ch = MeasurementChannel.from_readout_fidelity(inst=MEASURE(1, None), fidelity=0.95) + nm = NoiseModel.from_channels([ch, meas_ch]) + + restored = NoiseModel.from_json(nm.to_json()) + + assert restored == nm + assert set(restored.channels) == {ch.inst, meas_ch.inst} + + def test_add_combines_disjoint_channels(self): + """NoiseModel addition preserves disjoint channels from both operands.""" + ch1 = Channel.from_depolarizing_constant(inst=RX(np.pi / 4, 0), depolarizing_constant=0.98) + ch2 = Channel.from_depolarizing_constant(inst=RY(np.pi / 4, 1), depolarizing_constant=0.97) + + combined = NoiseModel.from_channels([ch1]) + NoiseModel.from_channels([ch2]) + + assert combined.get_channel(ch1.inst) == ch1 + assert combined.get_channel(ch2.inst) == ch2 + + def test_add_composes_overlapping_channels(self): + """NoiseModel addition composes channels with the same instruction.""" + inst = RX(np.pi / 4, 0) + ch1 = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) + ch2 = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.97) + + combined = NoiseModel.from_channels([ch1]) + NoiseModel.from_channels([ch2]) + + assert combined.get_channel(inst) == (ch1 @ ch2) + + def test_with_channels_returns_extended_model(self): + """with_channels returns a new model and rejects duplicate instructions.""" + ch1 = Channel.from_depolarizing_constant(inst=RX(np.pi / 4, 0), depolarizing_constant=0.98) + ch2 = Channel.from_depolarizing_constant(inst=RY(np.pi / 4, 1), depolarizing_constant=0.97) + nm = NoiseModel.from_channels([ch1]) + + extended = nm.with_channels([ch2]) + + assert nm.get_channel(ch2.inst) is None + assert extended.get_channel(ch1.inst) is ch1 + assert extended.get_channel(ch2.inst) is ch2 + with pytest.raises(ValueError, match="Duplicate noise channel"): + nm.with_channels([ch1]) + # ────────────────────────────────────────────────────────── # get_instruction_unitary tests @@ -300,7 +375,7 @@ def test_ideal_reset_maps_excited_to_ground(self): """An ideal reset on an excited qubit should produce |0><0|.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=[ch]) + noise_model = NoiseModel.from_channels([ch]) # Prepare |1> then reset program = Program(X(0), RESET(0)) rho = _dm(program, noise_model=noise_model) @@ -311,7 +386,7 @@ def test_ideal_reset_maps_superposition_to_ground(self): """An ideal reset on a superposition state should produce |0><0|.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=[ch]) + noise_model = NoiseModel.from_channels([ch]) # Prepare |+> then reset program = Program(RX(np.pi / 2, 0), RESET(0)) rho = _dm(program, noise_model=noise_model) @@ -322,7 +397,7 @@ def test_noisy_reset_reduces_fidelity(self): """A noisy reset should produce a state with fidelity < 1 relative to |0><0|.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=0.90) - noise_model = NoiseModel(channels=[ch]) + noise_model = NoiseModel.from_channels([ch]) program = Program(X(0), RESET(0)) rho = _dm(program, noise_model=noise_model) target_rho = qx.zero_state_matrix(1) @@ -334,7 +409,7 @@ def test_reset_in_multi_qubit_circuit(self): """Reset on one qubit should not affect the other qubit.""" inst = RESET(0) ch = ResetChannel.from_reset_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=[ch]) + noise_model = NoiseModel.from_channels([ch]) # Prepare |11> then reset qubit 0 program = Program(X(0), X(1), RESET(0)) rho = _dm(program, noise_model=noise_model) diff --git a/test/unit/test_qutrit_simulation.py b/test/unit/test_qutrit_simulation.py index 4df91be31..22dac99f7 100644 --- a/test/unit/test_qutrit_simulation.py +++ b/test/unit/test_qutrit_simulation.py @@ -465,7 +465,7 @@ def test_qutrit_noisy_measurement_channel(self): meas_ch = MeasurementChannel.from_readout_fidelity( inst=meas_inst, fidelity=0.9, dim=3 ) - noise_model = NoiseModel(channels=[meas_ch]) + noise_model = NoiseModel.from_channels([meas_ch]) # Prepare |2> (TX|0>=|2>) and measure with noise p = Program() @@ -604,7 +604,7 @@ def test_qutrit_depolarizing_channel(self): """A depolarizing channel on a qutrit gate mixes the state.""" inst = Gate("TX", [], [0]) channel = Channel.from_gate_fidelity(inst=inst, fidelity=0.8) - noise_model = NoiseModel(channels=[channel]) + noise_model = NoiseModel.from_channels([channel]) # Density matrix should show mixed state p = Program(Gate("TX", [], [0])) @@ -618,7 +618,7 @@ def test_qutrit_depolarizing_trajectory(self): """Trajectory simulation with qutrit depolarizing noise.""" inst = Gate("TX", [], [0]) channel = Channel.from_gate_fidelity(inst=inst, fidelity=0.9) - noise_model = NoiseModel(channels=[channel]) + noise_model = NoiseModel.from_channels([channel]) p = Program() p += Gate("TX", [], [0]) @@ -639,7 +639,7 @@ def test_qutrit_reset_channel(self): reset_inst = ResetQubit(Qubit(0)) reset_ch = ResetChannel.from_reset_fidelity(inst=reset_inst, fidelity=0.9, dim=3) - noise_model = NoiseModel(channels=[reset_ch]) + noise_model = NoiseModel.from_channels([reset_ch]) # Prepare |1> (TX^2|0>=|1>), then reset — should mostly go to |0> p = Program() @@ -667,7 +667,7 @@ def test_mixed_noise_qubit_and_qutrit(self): ch_qutrit = Channel.from_gate_fidelity( inst=Gate("TX", [], [1]), fidelity=0.95 ) - noise_model = NoiseModel(channels=[ch_qubit, ch_qutrit]) + noise_model = NoiseModel.from_channels([ch_qubit, ch_qutrit]) p = Program() p += X(0) diff --git a/test/unit/test_resolver.py b/test/unit/test_resolver.py index 62847abf7..70656ad55 100644 --- a/test/unit/test_resolver.py +++ b/test/unit/test_resolver.py @@ -64,7 +64,7 @@ def test_reset_emitted(self): def test_noise_channel_resolved(self): p = Program(X(0)) ch = Channel.from_gate_fidelity(inst=X(0), fidelity=0.99) - nm = NoiseModel(channels=[ch]) + nm = NoiseModel.from_channels([ch]) ops, _, _ = expand_program(p, nm) assert isinstance(ops[0], qx.SuperOp) @@ -83,7 +83,7 @@ def test_cycle_channel_expansion(self): channels = tuple( Channel.from_depolarizing_constant(inst, depolarizing_constant=0.99) for inst in (RX(0.1, 0), RZ(0.2, 0)) ) - nm = NoiseModel(channels=[CycleChannel(inst=cycle_inst, defcircuit=dc, channels=channels)]) + nm = NoiseModel.from_channels([CycleChannel(inst=cycle_inst, defcircuit=dc, channels=channels)]) p = Program(dc, cycle_inst) ops, qubit_tuples, _ = expand_program(p, nm) assert len(ops) == 2 @@ -149,7 +149,7 @@ def test_basic_roundtrip(self): def test_with_noise(self): p = Program(X(0), H(1)) ch = Channel.from_gate_fidelity(inst=X(0), fidelity=0.99) - nm = NoiseModel(channels=[ch]) + nm = NoiseModel.from_channels([ch]) resolver, dag = resolver_from_program(p, nm) ops = resolver(_EMPTY_PARAMS) assert len(ops) == 2 diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index d2aef07d4..885845e8d 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -225,13 +225,13 @@ def _make_bitflip_noise_model(self, p_error: float, qubit: int = 0) -> NoiseMode # Use a Pauli channel: p_I = 1-p, p_X = p, p_Y = 0, p_Z = 0 pauli_probs = {"X": p_error} channel = Channel.from_pauli_noise(inst=inst, pauli_noise=pauli_probs) - return NoiseModel(channels=[channel]) + return NoiseModel.from_channels([channel]) def _make_depolarizing_noise_model(self, fidelity: float, qubit: int = 0) -> NoiseModel: """Create a noise model with depolarizing noise on X gate.""" inst = X(qubit) channel = Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) - return NoiseModel(channels=[channel]) + return NoiseModel.from_channels([channel]) def test_noiseless_gate_with_noise_model(self): """A noise model that doesn't cover the applied gate should leave it noiseless.""" @@ -299,7 +299,7 @@ def test_two_qubit_noise(self): inst_q1 = X(1) ch0 = Channel.from_pauli_noise(inst=inst_q0, pauli_noise={"X": p_error}) ch1 = Channel.from_pauli_noise(inst=inst_q1, pauli_noise={"X": p_error}) - noise_model = NoiseModel(channels=[ch0, ch1]) + noise_model = NoiseModel.from_channels([ch0, ch1]) prog = Program(X(0), X(1)) num_traj = 2048 @@ -364,7 +364,7 @@ def test_noisy_measurement(self): qubit = Qubit(0) m_inst = QuilMeasurement(qubit=qubit, classical_reg=None) meas_ch = MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=0.8) - noise_model = NoiseModel(channels=[meas_ch]) + noise_model = NoiseModel.from_channels([meas_ch]) p = Program(MEASURE(0, None)) psi, outcomes = _simulate_trajectories( @@ -412,7 +412,7 @@ def test_noisy_reset(self): qubit = Qubit(0) reset_inst = ResetQubit(qubit) reset_ch = ResetChannel.from_reset_fidelity(inst=reset_inst, fidelity=0.9) - noise_model = NoiseModel(channels=[reset_ch]) + noise_model = NoiseModel.from_channels([reset_ch]) # Start in |1⟩, apply noisy reset p = Program(X(0), ResetQubit(Qubit(0))) @@ -477,7 +477,7 @@ def test_noise_model_single_trajectory(self): """With noise_model provided, runs a single trajectory.""" inst = X(0) channel = Channel.from_gate_fidelity(inst=inst, fidelity=1.0) - noise_model = NoiseModel(channels=[channel]) + noise_model = NoiseModel.from_channels([channel]) p = Program(X(0)) sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0]) psi, _ = sim.compute(_EMPTY_PARAMS, jax.random.key(0)) @@ -517,7 +517,7 @@ def test_bitflip_statistics(self): p_error = 0.3 inst = X(0) ch = Channel.from_pauli_noise(inst=inst, pauli_noise={"X": p_error}) - noise_model = NoiseModel(channels=[ch]) + noise_model = NoiseModel.from_channels([ch]) p = Program(X(0), MEASURE(0, None)) sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0]) @@ -684,7 +684,7 @@ def test_no_merge_noisy_ops_count(self): channels = [ Channel.from_coherence_times(RX(np.pi / 2, 0), gate_duration=0.04, t1s=[30.0], t2s=[20.0]), ] - noise_model = NoiseModel(channels=channels) + noise_model = NoiseModel.from_channels(channels) sim = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) @@ -777,7 +777,7 @@ def test_noisy_1q_merge(self): channels = [ Channel.from_coherence_times(RX(np.pi / 2, 0), gate_duration=0.04, t1s=[30.0], t2s=[20.0]), ] - noise_model = NoiseModel(channels=channels) + noise_model = NoiseModel.from_channels(channels) sim0 = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) sim1 = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=1) @@ -832,7 +832,7 @@ def test_noisy_trajectory_via_simulator(self): channels = [ Channel.from_coherence_times(CNOT(0, 1), gate_duration=0.1, t1s=[30.0, 30.0], t2s=[20.0, 20.0]), ] - noise_model = NoiseModel(channels=channels) + noise_model = NoiseModel.from_channels(channels) sim = TrajectorySimulator(p, noise_model=noise_model, max_subsystem_size=0) ops = sim.adapt(sim.compress(sim.resolve(_EMPTY_PARAMS))) @@ -894,7 +894,7 @@ def test_cycle_channel_expands_and_compresses(self): Channel.from_depolarizing_constant(inst, depolarizing_constant=0.99) for inst in (RX(0.1, 0), RZ(0.2, 0), RX(0.3, 0)) ) - noise_model = NoiseModel(channels=[CycleChannel(inst=cycle_inst, defcircuit=defcircuit, channels=channels)]) + noise_model = NoiseModel.from_channels([CycleChannel(inst=cycle_inst, defcircuit=defcircuit, channels=channels)]) program = Program(defcircuit, cycle_inst) sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=1) @@ -914,7 +914,7 @@ def test_expanded_cycle_without_cycle_channel_uses_gate_channels(self): [RX(0.1, formal_qubit), RZ(0.2, formal_qubit)], ) cycle_inst = QuilGate("INDIVIDUAL_NOISE_CYCLE", [], [0]) - noise_model = NoiseModel(channels=[Channel.from_depolarizing_constant(RX(0.1, 0), 0.99)]) + noise_model = NoiseModel.from_channels([Channel.from_depolarizing_constant(RX(0.1, 0), 0.99)]) program = Program(defcircuit, cycle_inst) sim = TrajectorySimulator(program, noise_model=noise_model, max_subsystem_size=0) @@ -1161,7 +1161,7 @@ def test_noisy_sample_with_devices(self): """Multi-device path should work with noise models.""" p_error = 0.3 ch = Channel.from_pauli_noise(inst=X(0), pauli_noise={"X": p_error}) - noise_model = NoiseModel(channels=[ch]) + noise_model = NoiseModel.from_channels([ch]) p = Program(X(0), MEASURE(0, None)) sim = TrajectorySimulator(p, noise_model=noise_model, qubits=[0], devices=jax.devices()) outcomes = sim.sample(_EMPTY_PARAMS, num_trajectories=1024, batch_size=256, random_seed=7) diff --git a/test/unit/test_trajectory_compression.py b/test/unit/test_trajectory_compression.py index 0dab0a9c2..e6299e51d 100644 --- a/test/unit/test_trajectory_compression.py +++ b/test/unit/test_trajectory_compression.py @@ -281,8 +281,8 @@ def _qutrit_circuit_5(): def _depolarizing_model(insts, fidelity): """Build a depolarizing noise model for the given instructions.""" - return NoiseModel( - channels=[Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) for inst in insts] + return NoiseModel.from_channels( + [Channel.from_gate_fidelity(inst=inst, fidelity=fidelity) for inst in insts] ) @@ -494,8 +494,8 @@ def test_compression_two_qubit_measurements_with_noise(): program += meas2 qubits, dims = QUBITS_2, (2, 2) - noise_model = NoiseModel( - channels=[ + noise_model = NoiseModel.from_channels( + [ Channel.from_gate_fidelity(inst=CNOT(5, 2), fidelity=0.97), Channel.from_gate_fidelity(inst=H(5), fidelity=0.98), MeasurementChannel.from_readout_fidelity(inst=meas5, fidelity=0.95), @@ -531,8 +531,8 @@ def test_compression_five_qubit_measurements_with_noise(): measurements.append(m) program += m - noise_model = NoiseModel( - channels=[ + noise_model = NoiseModel.from_channels( + [ Channel.from_gate_fidelity(inst=CZ(7, 2), fidelity=0.99), Channel.from_gate_fidelity(inst=CZ(9, 4), fidelity=0.99), Channel.from_gate_fidelity(inst=CNOT(2, 9), fidelity=0.99), @@ -730,8 +730,8 @@ def test_asymmetric_readout_error_rate_column_mapping_under_compression(): # qubit 2 measured |1> with 30% flip (fidelity 0.70); qubit 5 measured |0> # with 1% flip (fidelity 0.99). The two error rates are deliberately far # apart so any column swap is unmistakable. - noise_model = NoiseModel( - channels=[ + noise_model = NoiseModel.from_channels( + [ MeasurementChannel.from_readout_fidelity(inst=meas2, fidelity=0.70), MeasurementChannel.from_readout_fidelity(inst=meas5, fidelity=0.99), ] @@ -827,4 +827,3 @@ def _joint(max_subsystem_size): - From f8f5cf4928f9c94f38a13c4609f57dec3409f989 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 13:12:33 +0000 Subject: [PATCH 22/37] Fix CI failures for noise model PR --- poetry.lock | 219 +++++++++++---------- pyproject.toml | 8 +- pyquil/noise/_channels.py | 3 +- pyquil/noise/_noise_model.py | 6 +- pyquil/simulation/_resolver.py | 7 +- pyquil/simulation/_simulator.py | 60 +++--- test/benchmarks/test_state_vector.py | 8 +- test/unit/__snapshots__/test_compiler.ambr | 3 +- test/unit/test_trajectory_compression.py | 5 +- 9 files changed, 169 insertions(+), 150 deletions(-) diff --git a/poetry.lock b/poetry.lock index c2f1fba90..da732f6e8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -104,23 +104,23 @@ lxml = ["lxml"] [[package]] name = "bleach" -version = "6.3.0" +version = "6.4.0" description = "An easy safelist-based HTML-sanitizing tool." optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6"}, - {file = "bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22"}, + {file = "bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081"}, + {file = "bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452"}, ] [package.dependencies] -tinycss2 = {version = ">=1.1.0,<1.5", optional = true, markers = "extra == \"css\""} +tinycss2 = {version = ">=1.1.0", optional = true, markers = "extra == \"css\""} webencodings = "*" [package.extras] -css = ["tinycss2 (>=1.1.0,<1.5)"] +css = ["tinycss2 (>=1.1.0)"] [[package]] name = "certifi" @@ -948,14 +948,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.13" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"}, - {file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] @@ -1674,6 +1674,7 @@ description = "Inline Matplotlib backend for Jupyter" optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"latex\" or extra == \"docs\"" files = [ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, @@ -1718,15 +1719,15 @@ files = [ [[package]] name = "mistune" -version = "3.2.0" +version = "3.3.2" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1"}, - {file = "mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a"}, + {file = "mistune-3.3.2-py3-none-any.whl", hash = "sha256:a678a56387d487db7368ede4647cb2ba1deff22ce61f92343e4ebe0ddfce4f2d"}, + {file = "mistune-3.3.2.tar.gz", hash = "sha256:e12ee4f1e74336e91aa1141e35f913b337c40bdf7c0cc49f21fb853a27e8b62f"}, ] [[package]] @@ -1789,74 +1790,78 @@ dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"] [[package]] name = "msgpack" -version = "1.1.2" +version = "1.2.1" description = "MessagePack serializer" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2"}, - {file = "msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87"}, - {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251"}, - {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a"}, - {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f"}, - {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f"}, - {file = "msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}, - {file = "msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}, - {file = "msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c"}, - {file = "msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0"}, - {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296"}, - {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef"}, - {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c"}, - {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e"}, - {file = "msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e"}, - {file = "msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68"}, - {file = "msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406"}, - {file = "msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa"}, - {file = "msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb"}, - {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f"}, - {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42"}, - {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9"}, - {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620"}, - {file = "msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029"}, - {file = "msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b"}, - {file = "msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69"}, - {file = "msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf"}, - {file = "msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7"}, - {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999"}, - {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e"}, - {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162"}, - {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794"}, - {file = "msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c"}, - {file = "msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9"}, - {file = "msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84"}, - {file = "msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00"}, - {file = "msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939"}, - {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e"}, - {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931"}, - {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014"}, - {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2"}, - {file = "msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717"}, - {file = "msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b"}, - {file = "msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af"}, - {file = "msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a"}, - {file = "msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b"}, - {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245"}, - {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90"}, - {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20"}, - {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27"}, - {file = "msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b"}, - {file = "msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff"}, - {file = "msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46"}, - {file = "msgpack-1.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e"}, - {file = "msgpack-1.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844"}, - {file = "msgpack-1.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23"}, - {file = "msgpack-1.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7"}, - {file = "msgpack-1.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8"}, - {file = "msgpack-1.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833"}, - {file = "msgpack-1.1.2-cp39-cp39-win32.whl", hash = "sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c"}, - {file = "msgpack-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030"}, - {file = "msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e"}, + {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, + {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, + {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, + {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, + {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, + {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, + {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, + {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, + {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, + {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, + {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, + {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, + {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, + {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, + {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, + {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, + {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, + {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, ] [[package]] @@ -2647,20 +2652,20 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.4.2" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1" -packaging = ">=20" +iniconfig = ">=1.0.1" +packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" @@ -3839,18 +3844,18 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "syrupy" -version = "4.9.1" +version = "5.3.3" description = "Pytest Snapshot Test Utility" optional = false -python-versions = ">=3.8.1" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda"}, - {file = "syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4"}, + {file = "syrupy-5.3.3-py3-none-any.whl", hash = "sha256:94b7c41192295fa03ec6de281d8075900e03739f45943935f7854d61824670d7"}, + {file = "syrupy-5.3.3.tar.gz", hash = "sha256:6c065cd1ba9374e810ad3826feb466f4d43e6f1a7168703178e56b47902c736f"}, ] [package.dependencies] -pytest = ">=7.0.0,<9.0.0" +pytest = ">=8.0.0" [[package]] name = "tinycss2" @@ -3876,9 +3881,10 @@ test = ["pytest", "ruff"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -optional = false +optional = true python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -3886,23 +3892,23 @@ files = [ [[package]] name = "tornado" -version = "6.5.5" +version = "6.5.7" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa"}, - {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521"}, - {file = "tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5"}, - {file = "tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07"}, - {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e"}, - {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca"}, - {file = "tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7"}, - {file = "tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b"}, - {file = "tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6"}, - {file = "tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9"}, + {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"}, + {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"}, + {file = "tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972"}, + {file = "tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b"}, + {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92"}, + {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5"}, + {file = "tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4"}, + {file = "tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4"}, + {file = "tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796"}, + {file = "tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2"}, ] [[package]] @@ -3912,6 +3918,7 @@ description = "Traitlets Python configuration system" optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"latex\" or extra == \"docs\"" files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, @@ -3960,15 +3967,15 @@ files = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] @@ -4107,10 +4114,10 @@ files = [ dev = ["pytest", "setuptools"] [extras] -docs = ["Sphinx", "matplotlib", "myst-parser", "nbsphinx", "pandoc", "seaborn", "sphinx-rtd-theme"] +docs = ["Sphinx", "matplotlib", "matplotlib-inline", "myst-parser", "nbsphinx", "pandoc", "seaborn", "sphinx-rtd-theme", "toml"] latex = ["ipython"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.13" -content-hash = "44463a850522ab243601e2c1e21773a37e88e4e0308e8d45faeae0132cb6b2a8" +content-hash = "9c5b1b0f8f39ead4cf829f29e5bb6379e5c1b309a0b884df1aa3cd63ce69bb88" diff --git a/pyproject.toml b/pyproject.toml index 6cef5700d..1e573a1a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,27 +43,27 @@ pandoc = {version = "2.4b0", optional = true} matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} +toml = {version = "^0.10.2", optional = true} rigetti-quax = ">=0.6.4" [tool.poetry.extras] latex = ["ipython"] -docs = ["Sphinx", "sphinx-rtd-theme", "nbsphinx", "myst-parser", "pandoc", "matplotlib", "matlotlib-inline", "seaborn", "toml"] +docs = ["Sphinx", "sphinx-rtd-theme", "nbsphinx", "myst-parser", "pandoc", "matplotlib", "matplotlib-inline", "seaborn", "toml"] [tool.poetry.group.dev.dependencies] typing-extensions = "^4.12.0" setuptools = {version = "^78.0.0", python = ">=3.12"} ruff = "^0.4.6" -pytest = "^8.2.0" +pytest = ">=8.2.0,<10" pytest-cov = "^5.0.0" mypy = "^1.10.0" -toml = "^0.10.2" pytest-xdist = "^3.6.1" pytest-rerunfailures = "^14.0.0" pytest-timeout = "^2.3.1" pytest-mock = "^3.14.0" pytest-benchmark = "4.0.0" respx = "^0.21.1" -syrupy = "^4.6.1" +syrupy = "^5.3.3" jinja2 = {version = ">=3.1.6", optional = true} # see: https://osv.dev/vulnerability/GHSA-gmj6-6f8f-6699 and https://osv.dev/vulnerability/GHSA-q2x7-8rv6-6q7h and https://osv.dev/GHSA-cpwx-vrp4-4pq7 h11 = {version = ">=0.16.0", optional = true} # see: https://osv.dev/vulnerability/GHSA-vqfr-h8mv-ghfj diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index f0ba21062..2edce166e 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -36,7 +36,6 @@ import numpy as np import quax as qx from jax import Array -from plotly.graph_objs import Figure from quil.expression import Expression as QuilExpression from quil.program import Program as RSProgram from scipy.linalg import logm as scipy_logm @@ -45,6 +44,8 @@ from pyquil.quilbase import DefCircuit, DefGate, Gate, Measurement, Reset, ResetQubit if TYPE_CHECKING: + from plotly.graph_objs import Figure + from pyquil import Program logger = logging.getLogger(__name__) diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index 496dbf0a9..cf188c7b7 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -121,7 +121,9 @@ def __init__( channel_map: dict[NoiseInstruction, ChannelType] = {} for inst, channel in channels.items(): if channel.inst != inst: - raise ValueError(f"NoiseModel channel key {inst!r} does not match channel instruction {channel.inst!r}.") + raise ValueError( + f"NoiseModel channel key {inst!r} does not match channel instruction {channel.inst!r}." + ) channel_map[inst] = channel object.__setattr__(self, "channels", MappingProxyType(channel_map)) @@ -184,7 +186,7 @@ def from_isa(cls: type[NoiseModel], compiler_isa: CompilerISA) -> NoiseModel: from pyquil.external.rpcq import GateInfo, MeasureInfo from pyquil.quilatom import Qubit as QuilQubit - channels: dict[Gate | Measurement, Channel | MeasurementChannel | ResetChannel | CycleChannel] = {} + channels: dict[NoiseInstruction, ChannelType] = {} seen_measure_qubits: set[int] = set() for qubit_label, qubit in compiler_isa.qubits.items(): diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index d5c44d2b0..6607d710f 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -648,15 +648,14 @@ def compressor_from_dag( n_original = dag.number_of_nodes() if max_subsystem_size == 0 or n_original == 0: - emit_order = [ - (nk, [nk], tuple(dag.nodes[nk]["qubits"])) - for nk in nx.lexicographical_topological_sort(dag) + passthrough_emit_order = [ + (nk, [nk], tuple(dag.nodes[nk]["qubits"])) for nk in nx.lexicographical_topological_sort(dag) ] def compress_passthrough(ops: list[ResolvedOp]) -> list[ResolvedOp]: return ops - compress_passthrough.emit_order = emit_order # type: ignore[attr-defined] + compress_passthrough.emit_order = passthrough_emit_order # type: ignore[attr-defined] logger.info( "Compressor: %d ops (no merging), max_subsystem_size=0", n_original, diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 8b6bb7d4d..a24dfe9e1 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -112,7 +112,7 @@ def _make_kraus_trajectory_branch( def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: kraus_map = qx.KrausMap.from_matrix(op_mat[:, :db, :db], (base_dims, base_dims)) - return _sample_kraus_map_trajectory(kraus_map, psi, key, base) + return cast(tuple[qx.StateVector, Array], _sample_kraus_map_trajectory(kraus_map, psi, key, base)) return branch @@ -153,9 +153,22 @@ class ProgramSimulator: Instances are immutable after construction. """ - __slots__ = ("n_qubits", "qubits", "dims", "_linearize_fn", "_resolve_fn", "_compress_fn", - "bases", "op_index", "base_dims", "base_total_dim", "d_max", "_has_params", - "_expanded_ops", "_raw_subsystems") + __slots__ = ( + "n_qubits", + "qubits", + "dims", + "_linearize_fn", + "_resolve_fn", + "_compress_fn", + "bases", + "op_index", + "base_dims", + "base_total_dim", + "d_max", + "_has_params", + "_expanded_ops", + "_raw_subsystems", + ) def __init__( self, @@ -252,7 +265,7 @@ def linearize(memory_map: MemoryMap) -> Array: # (rather than the number of operations) determines the size of the # traced/compiled graph. probe = self._compress_fn(self._resolve_fn(jnp.zeros(len(param_refs)))) - self.bases = [] + self.bases: list[tuple[int, ...]] = [] sub_to_branch: dict[tuple[int, ...], int] = {} op_index: list[int] = [] for _, subsystem in probe: @@ -294,9 +307,9 @@ def compute(self, params: Array, **kwargs: Any) -> Any: # ══════════════════════════════════════════════════════════ -def _embed_matrix_np(mat: np.ndarray, op_subsystem: tuple[int, ...], - group_subsystem: tuple[int, ...], dims: tuple[int, ...], - d_max: int) -> np.ndarray: +def _embed_matrix_np( + mat: np.ndarray, op_subsystem: tuple[int, ...], group_subsystem: tuple[int, ...], dims: tuple[int, ...], d_max: int +) -> np.ndarray: """Embed a gate matrix into a larger subsystem (numpy, for constant ops). Computes the d_max×d_max padded matrix that applies ``mat`` on @@ -305,6 +318,7 @@ def _embed_matrix_np(mat: np.ndarray, op_subsystem: tuple[int, ...], if op_subsystem == group_subsystem: return np.pad(mat, [(0, d_max - s) for s in mat.shape]) import quax as qx # noqa: F811 — local re-import for clarity + target_dims = tuple(dims[q] for q in group_subsystem) positions = tuple(group_subsystem.index(q) for q in op_subsystem) op_dims = tuple(dims[q] for q in op_subsystem) @@ -449,8 +463,7 @@ def _build_vectorized_unitary_constructor( key = (id(eop.gate_fn), concrete_mask, concrete_vals, embed_key) if key not in param_groups: embed_fn = _make_embed_fn(op_sub, grp_sub, dims, d_max) - param_groups[key] = (eop.gate_fn, eop.param_indices, eop.concrete_values, - [], [], embed_fn) + param_groups[key] = (eop.gate_fn, eop.param_indices, eop.concrete_values, [], [], embed_fn) param_groups[key][3].append(sorted_pos) param_groups[key][4].append([pi for pi in eop.param_indices if pi >= 0]) else: @@ -468,15 +481,16 @@ def _build_vectorized_unitary_constructor( concrete_slots = [(j, cv) for j, (pi, cv) in enumerate(zip(template_pi, template_cv, strict=False)) if pi < 0] n_total = len(template_pi) - def _make_batch(gf: Callable, ps: list[int], cs: list[tuple[int, float]], - nt: int, ef: Callable, pidx: Array) -> Callable[[Array], Array]: + def _make_batch( + gf: Callable, ps: list[int], cs: list[tuple[int, float]], nt: int, ef: Callable, pidx: Array + ) -> Callable[[Array], Array]: def _single(parametric_values: Array) -> Array: args: list[Any] = [None] * nt for slot, val in cs: args[slot] = val for k, slot in enumerate(ps): args[slot] = parametric_values[k] - return ef(gf(*args).matrix) + return cast(Array, ef(gf(*args).matrix)) batched = jax.vmap(_single) @@ -486,8 +500,7 @@ def build(params: Array) -> Array: return build pidx_arr = jnp.array(pidx_lists) - builder = _make_batch(gate_fn, parametric_slots, concrete_slots, - n_total, embed_fn, pidx_arr) + builder = _make_batch(gate_fn, parametric_slots, concrete_slots, n_total, embed_fn, pidx_arr) vmapped_specs.append((pos_arr, builder)) # ── Pre-build constant embedded matrices ── @@ -533,6 +546,7 @@ def build_compressed_stack(params: Array) -> Array: def group_product(mats: Array) -> Array: def body(acc: Array, mat: Array) -> tuple[Array, None]: return mat @ acc, None + final, _ = jax.lax.scan(body, eye_mat, mats) return final @@ -575,7 +589,11 @@ def __init__( # op count in the state-evolution scan). emit_order = getattr(self._compress_fn, "emit_order", []) build_fn, _sort_order, _group_bounds = _build_vectorized_unitary_constructor( - self._expanded_ops, self._raw_subsystems, emit_order, self.dims, self.d_max, + self._expanded_ops, + self._raw_subsystems, + emit_order, + self.dims, + self.d_max, ) self._vmapped_build_fn = build_fn @@ -622,7 +640,6 @@ def body(psi: qx.StateVector, xs: tuple[Array, Array]) -> tuple[qx.StateVector, psi, _ = jax.lax.scan(body, self._psi0, (op_stack, self._idx_arr)) return psi - def __call__(self, params: Array) -> qx.StateVector: return self.compute(params) @@ -690,9 +707,7 @@ def __init__( # graph to just the scan over a concrete array. self._const_op_stack: Array | None = None if not self._has_params and self.op_index: - self._const_op_stack = jax.block_until_ready( - self._stack_superops(self.resolve(jnp.zeros(0))) - ) + self._const_op_stack = self._stack_superops(self.resolve(jnp.zeros(0))).block_until_ready() def _stack_superops(self, resolved: list[ResolvedOp]) -> Array: """Compress, promote each op to a SuperOp, and stack.""" @@ -884,10 +899,7 @@ def _op_to_kraus_matrix( case qx.KrausMap(): return op.matrix, 1, False case qx.QuantumInstrument(): - kraus_mats = [ - qx.superop_to_kraus(op.outcome_superop(i)[0]).matrix - for i in range(op.num_outcomes) - ] + kraus_mats = [qx.superop_to_kraus(op.outcome_superop(i)[0]).matrix for i in range(op.num_outcomes)] n_kraus_per_outcome = kraus_mats[0].shape[-3] merged = jnp.concatenate(kraus_mats, axis=-3) return merged, n_kraus_per_outcome, True diff --git a/test/benchmarks/test_state_vector.py b/test/benchmarks/test_state_vector.py index 55e881053..8916881d6 100644 --- a/test/benchmarks/test_state_vector.py +++ b/test/benchmarks/test_state_vector.py @@ -139,11 +139,13 @@ def _surface17_program_variant(variant="full"): def _build_surface17_cycle_noise_model(program, depolarizing_constant=0.99, readout_fidelity=1.0): defcircuits = _surface17_defcircuits(program) - cycle_channels = [] + cycle_channels = {} for inst in program.instructions: if not isinstance(inst, QuilGate) or inst.name not in defcircuits: continue + if inst in cycle_channels: + continue defcircuit = defcircuits[inst.name] qubit_map = dict(zip(defcircuit.qubit_variables, inst.qubits)) @@ -163,9 +165,9 @@ def _build_surface17_cycle_noise_model(program, depolarizing_constant=0.99, read MeasurementChannel.from_readout_fidelity(concrete_measurement, fidelity=readout_fidelity) ) - cycle_channels.append(CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels))) + cycle_channels[inst] = CycleChannel(inst=inst, defcircuit=defcircuit, channels=tuple(channels)) - return NoiseModel.from_channels(cycle_channels) + return NoiseModel(channels=cycle_channels) def _prepare_trajectory_operations(program, noise_model, max_subsystem_size=0): diff --git a/test/unit/__snapshots__/test_compiler.ambr b/test/unit/__snapshots__/test_compiler.ambr index fc4c291a0..9e46b575e 100644 --- a/test/unit/__snapshots__/test_compiler.ambr +++ b/test/unit/__snapshots__/test_compiler.ambr @@ -2,9 +2,8 @@ # name: test_transpile_qasm_2 ''' DECLARE ro BIT[2] - MEASURE 0 ro[0] MEASURE 1 ro[1] - HALT + MEASURE 0 ro[0] ''' # --- diff --git a/test/unit/test_trajectory_compression.py b/test/unit/test_trajectory_compression.py index e6299e51d..303ba6a78 100644 --- a/test/unit/test_trajectory_compression.py +++ b/test/unit/test_trajectory_compression.py @@ -659,9 +659,7 @@ def _noisy_channel_cases(): ) -@pytest.mark.parametrize( - "case", _noisy_channel_cases(), ids=lambda c: c[0] -) +@pytest.mark.parametrize("case", tuple(_noisy_channel_cases()), ids=lambda c: c[0]) def test_compression_preserves_total_channel_exactly(case): """Compression must not change the *channel* the sampler implements. @@ -826,4 +824,3 @@ def _joint(max_subsystem_size): assert _total_variation(_joint(0), _joint(2)) < 0.02 - From 4a7f622aea7235a52ecadadbf3f39e8c21c68534 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 13:20:13 +0000 Subject: [PATCH 23/37] Fix documentation doctest expectations --- docs/source/compiler.rst | 1 - docs/source/getting_started.rst | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/source/compiler.rst b/docs/source/compiler.rst index 9571f958d..1042c2886 100644 --- a/docs/source/compiler.rst +++ b/docs/source/compiler.rst @@ -365,7 +365,6 @@ For example, consider running a ``CZ`` on non-neighboring qubits on a linear dev .. testoutput:: swaps CZ 2 1 - HALT We see that the resulting program has only a single ``CZ`` even though the original program would usually require the insertion of a ``SWAP`` gate. The compiler instead opted to just relabel (or diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index ceb22506e..c0d33e820 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -40,7 +40,7 @@ If you would like to stay up to date with the latest changes and bug fixes, you .. note:: - pyQuil requires Python 3.9 or later. + pyQuil requires Python 3.11 or later and supports Python versions earlier than 3.13. .. testcode:: verify-min-version :hide: @@ -58,7 +58,7 @@ If you would like to stay up to date with the latest changes and bug fixes, you .. testoutput:: verify-min-version :hide: - ^3.9... + >=3.11, <3.13 .. note:: @@ -165,7 +165,7 @@ the terminal windows where your servers are running, you should see output print pyQuil also provides the :py:func:`~pyquil.api.local_forest_runtime()` context manager to ensure both ``quilc`` and ``qvm`` servers are running by starting them as subprocesses if they aren't already. - .. testcode:: first-program + .. code:: python from pyquil import get_qc, Program from pyquil.gates import CNOT, Z, MEASURE From e9fb8e9042a2b713ab7a361502520e94c8306ab7 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 13:24:38 +0000 Subject: [PATCH 24/37] Make compiler doctest tolerate quilc HALT output --- docs/source/compiler.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/compiler.rst b/docs/source/compiler.rst index 1042c2886..0fad65457 100644 --- a/docs/source/compiler.rst +++ b/docs/source/compiler.rst @@ -364,7 +364,7 @@ For example, consider running a ``CZ`` on non-neighboring qubits on a linear dev .. testoutput:: swaps - CZ 2 1 + CZ 2 1... We see that the resulting program has only a single ``CZ`` even though the original program would usually require the insertion of a ``SWAP`` gate. The compiler instead opted to just relabel (or From d0c6b2d29a81877790694086f40c81f6c144a38a Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 13:33:56 +0000 Subject: [PATCH 25/37] Stabilize QASM transpilation unit test --- test/unit/__snapshots__/test_compiler.ambr | 9 --------- test/unit/test_compiler.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 test/unit/__snapshots__/test_compiler.ambr diff --git a/test/unit/__snapshots__/test_compiler.ambr b/test/unit/__snapshots__/test_compiler.ambr deleted file mode 100644 index 9e46b575e..000000000 --- a/test/unit/__snapshots__/test_compiler.ambr +++ /dev/null @@ -1,9 +0,0 @@ -# serializer version: 1 -# name: test_transpile_qasm_2 - ''' - DECLARE ro BIT[2] - MEASURE 1 ro[1] - MEASURE 0 ro[0] - - ''' -# --- diff --git a/test/unit/test_compiler.py b/test/unit/test_compiler.py index c44dea0f2..611154681 100644 --- a/test/unit/test_compiler.py +++ b/test/unit/test_compiler.py @@ -3,7 +3,6 @@ import pytest from qcs_sdk.qpu.translation import TranslationBackend -from syrupy.assertion import SnapshotAssertion from pyquil import Program from pyquil.api._compiler import ( @@ -36,10 +35,16 @@ def test_compile_with_quilt_calibrations(compiler: QPUCompiler): assert compilation_result == program -def test_transpile_qasm_2(compiler: QPUCompiler, snapshot: SnapshotAssertion): +def test_transpile_qasm_2(compiler: QPUCompiler): qasm = 'OPENQASM 2.0;\nqreg q[3];\ncreg ro[2];\nmeasure q[0] -> ro[0];\nmeasure q[1] -> ro[1];' program = compiler.transpile_qasm_2(qasm) - assert program.out() == snapshot + lines = [line for line in program.out().splitlines() if line] + assert lines[0] == "DECLARE ro BIT[2]" + instructions = lines[1:] + if instructions[-1:] == ["HALT"]: + instructions = instructions[:-1] + assert len(instructions) == 2 + assert set(instructions) == {"MEASURE 0 ro[0]", "MEASURE 1 ro[1]"} @pytest.mark.parametrize( @@ -60,4 +65,3 @@ def test_translation_backend_validation(quantum_processor_id: str, backend: Opti else: actual = select_backend_for_quantum_processor_id(quantum_processor_id, backend) assert actual == expected - From 26cfd94f0fc4086181322f0ddc20852c3dacef25 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 25 Jun 2026 16:34:49 +0000 Subject: [PATCH 26/37] Update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e573a1a9..0ac993607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyquil" -version = "4.17.1-rc.0" +version = "4.19.0-rc.0" description = "A Python library for creating Quantum Instruction Language (Quil) programs." authors = ["Rigetti Computing "] readme = "README.md" From 0d16ac368a33fb9830043b357a15e921094a0881 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sun, 28 Jun 2026 12:56:54 +0000 Subject: [PATCH 27/37] Add docs --- docs/source/index.rst | 1 + docs/source/simulation_architecture.rst | 439 +++++++++++++++++++++++ poetry.lock | 8 +- pyproject.toml | 2 +- pyquil/noise/_channels.py | 93 ++++- pyquil/noise/_noise_model.py | 75 ++-- pyquil/simulation/_resolver.py | 203 ++++++----- pyquil/simulation/_simulator.py | 442 +++++++++++------------- test/unit/test_noise_model.py | 207 ++++++++++- test/unit/test_resolver.py | 34 +- 10 files changed, 1087 insertions(+), 417 deletions(-) create mode 100644 docs/source/simulation_architecture.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 5f0282e5b..c41a46e62 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,6 +32,7 @@ If you’re new to pyQuil, head to the `getting started `_ guid wavefunction_simulator compiler noise + simulation_architecture advanced_usage troubleshooting introducing_v4 diff --git a/docs/source/simulation_architecture.rst b/docs/source/simulation_architecture.rst new file mode 100644 index 000000000..777c2434c --- /dev/null +++ b/docs/source/simulation_architecture.rst @@ -0,0 +1,439 @@ +.. _simulation_architecture: + +========================================= +Noisy simulation architecture +========================================= + +.. note:: + + The simulators described here live in the experimental, private modules + ``pyquil.simulation._simulator`` and ``pyquil.simulation._resolver`` (and the + noise model in ``pyquil.noise._noise_model`` / ``pyquil.noise._channels``). + The API is not yet stable and the import paths are private. It is documented + here because the design is intended to become the default simulation backend + in a future major release, replacing the NumPy reference simulators. + + These modules depend on `JAX `_ (via the + ``rigetti-quax`` package), which provides the operator algebra and the + ``jit``/``grad``/``vmap`` machinery the simulators are built on. + + +Goal of the module +================== + +The module simulates the action of a (possibly noisy) Quil program on a quantum +register and returns the resulting quantum state or measurement statistics. It +is designed to solve two problems with the existing simulators simultaneously: + +* **Expressiveness.** Device-realistic noise is not limited to a fixed menu of + Kraus channels. The module represents noise as arbitrary completely-positive, + trace-preserving (CPTP) maps attached to individual instructions — coherent + errors, stochastic Pauli channels, thermal relaxation, leakage to higher + levels (qutrits and beyond), readout confusion, and reset infidelity — and + composes them exactly. + +* **Performance and differentiability.** Every stage is expressed in JAX so that + the entire forward simulation is a single traceable function. It can be + ``jax.jit``-compiled (amortizing compilation across a parameter sweep) and + ``jax.grad``-differentiated (exact gradients of an output observable with + respect to gate parameters), and trajectories can be vectorized with + ``jax.vmap`` and sharded across devices. + +The unit of noise is a **channel** keyed to a program instruction. A +:class:`~pyquil.noise._noise_model.NoiseModel` is, conceptually, a partial map +from instructions to channels, + +.. math:: + + \mathcal{N} : \text{instruction} \longmapsto \mathcal{E}, + +queried during simulation via ``NoiseModel.get_channel(inst)``. A channel's +``process`` is a superoperator that *includes* the ideal gate, so the channel +**replaces** the instruction rather than being appended after it: + +.. math:: + + \mathcal{E} \;=\; \Lambda \circ \mathcal{U}, + +where :math:`\mathcal{U}(\rho) = U \rho U^\dagger` is the ideal gate and +:math:`\Lambda` is the noise. An instruction with no channel is simulated +ideally. + + +Operator vocabulary +=================== + +The pipeline manipulates a small set of ``quax`` operator types. Each carries +explicit per-qudit dimensions (e.g. ``((2, 2), (2, 2))`` for a two-qubit +operator, ``((3,), (3,))`` for a qutrit), so qubit and qudit systems are treated +uniformly. + +.. list-table:: + :header-rows: 1 + :widths: 22 78 + + * - Type + - Meaning + * - ``Unitary`` + - An ideal gate :math:`U`, acting as :math:`\rho \mapsto U \rho U^\dagger`. + * - ``SuperOp`` + - A general linear map on density operators in column-stacking + (Liouville) form, the canonical representation of a noisy ``Channel``. + * - ``KrausMap`` + - A channel as a set of Kraus operators :math:`\{K_i\}` with + :math:`\sum_i K_i^\dagger K_i = I`; the form a state-vector trajectory + samples from. + * - ``QuantumInstrument`` + - A measurement: a collection of outcome-labelled CP maps whose sum is + trace-preserving. Models classification (confusion) error and + post-measurement back-action. + +Conversions (``to_superop``, ``to_kraus``, ``to_choi``, ``to_pauli_liouville``) +are provided by ``quax`` and are used by the adapters described below. + + +The simulation pipeline +======================= + +A simulator is an **object constructed from a program**, not a function called +on one. The reason is that efficient simulation requires several closures whose +structure is fixed by the program (and noise model) but whose inputs are the +runtime parameters. Building them once, at construction time, lets the +expensive analysis (circuit expansion, dependency analysis, operator merging, +trace/compile) be shared across every subsequent evaluation — a parameter sweep, +a gradient computation, or a batch of Monte-Carlo trajectories. + +Construction runs four conceptual stages, each materialized as a closure: + +.. code-block:: text + + Program (+ NoiseModel) + │ + ▼ + ┌──────────────┐ MemoryMap ──► flat parameter vector θ + │ Linearizer │ + │ + ▼ + ┌──────────────┐ θ ──► [(operator, subsystem), ...] + │ Resolver │ (consults the noise model; ideal gates stay parametric) + │ + ▼ + ┌──────────────┐ merge adjacent operators on the program DAG + │ Compressor │ up to `max_subsystem_size` qubits + │ + ▼ + ┌──────────────┐ apply the operator stack to the initial state + │ Calculator │ (jit/grad/vmap-friendly) + │ + ▼ + StateVector / DensityMatrix / (StateVector, outcomes) + +The first three stages are shared infrastructure in ``_resolver.py``; the last +is specialized per simulator in ``_simulator.py``. + +Linearizer +---------- + +A Quil program references classical memory by name and offset (e.g. +``theta[0]``). The linearizer flattens a :class:`~pyquil.api.MemoryMap` into the +dense parameter vector :math:`\theta \in \mathbb{R}^{n}` that the rest of the +pipeline (and ``jax.grad``) operates on. The layout — which ``(register, +offset)`` pair occupies each slot — is discovered during expansion and fixed for +the life of the object, so ``linearize`` is a cheap gather. + +Resolver +-------- + +The resolver turns :math:`\theta` into an ordered list of +``(operator, subsystem)`` pairs, one per operation, where ``subsystem`` is the +tuple of (zero-based) qudit indices the operator acts on. It is produced by +:func:`~pyquil.simulation._resolver.resolve_program`, which returns a +``Resolution`` bundling the inferred dimensions, the expanded operators, their +subsystems, the parameter layout, and the resolve closure. + +Expansion does several things at once: + +* **Noise resolution.** Each instruction is looked up in the noise model. A + noisy gate becomes its ``SuperOp``; a noisy measurement becomes a + ``QuantumInstrument``; a noisy reset becomes a ``SuperOp``. Instructions with + no channel resolve to their ideal operator. + +* **Most-specific typing.** Operators are kept in their tightest native type — + ideal gates as ``Unitary``, channels as ``SuperOp``, measurements as + ``QuantumInstrument``. This lets the cheapest backend (pure state vector) + avoid density-matrix arithmetic whenever a program happens to be noiseless, + and lets each backend choose how to adapt the rest (see *Adapters*). + +* **Parametric closures.** A gate whose angle is a runtime memory reference is + *not* resolved to a number. It is wrapped in a ``ParametricGate`` that, given + :math:`\theta`, constructs the gate matrix. This keeps gate construction + inside the traced/differentiated graph, which is what makes ``jax.grad`` with + respect to gate angles work. + +* **DEFCIRCUIT and cycle expansion.** ``DEFCIRCUIT`` bodies are expanded with + formal-argument substitution. When a circuit invocation matches a + :class:`~pyquil.noise._channels.CycleChannel` in the noise model — a single + channel describing the joint noise of a whole parallel cycle — the cycle is + replaced by the channel's constituent operators directly. + +* **Dimension inference.** The register dimension of each qudit is inferred from + the operators that act on it (e.g. a ``TX`` gate or a qutrit channel promotes a + line to dimension 3). The program is expanded twice: once with default qubit + dimensions to infer the true dimensions, then again with those dimensions so + that *ideal* measurement and reset operators are built at the correct size. + Passing ``dims`` explicitly skips the first pass. + +Dependency DAG +-------------- + +The subsystem list induces a dependency DAG +(:func:`~pyquil.simulation._resolver.build_dag`): one node per operation, with an +edge :math:`u \to v` whenever :math:`u` and :math:`v` share a qubit and :math:`u` +precedes :math:`v` in program order. The DAG encodes exactly the orderings that +must be preserved; everything else is free to be reordered or merged. + +Compressor +---------- + +Applying operators one at a time is wasteful: a depth-:math:`D`, +:math:`N`-qubit program issues many small one- and two-qubit operators, and +under ``jit`` each distinct operator shape becomes a distinct branch in the +compiled graph. The compressor +(:func:`~pyquil.simulation._resolver.compressor_from_dag`) performs **greedy edge +contraction** on the DAG, fusing adjacent operators into a single operator on the +union of their qubits, up to a cap of ``max_subsystem_size`` qubits. + +Key properties: + +* **Small-first priority.** Candidate merges are taken from a priority queue + ordered by the size of the resulting subsystem, so one-qubit gates are + absorbed into neighbouring multi-qubit groups first. This reduces the number + of *distinct* subsystem shapes, which is what governs compile time. + +* **Convexity / barrier safety.** A merge is rejected if it would create a cycle + in the contracted (quotient) graph — i.e. if some operation lies on a + dependency path *between* the two groups. This is what prevents two gates that + straddle a mid-circuit measurement from being fused, which would silently + reorder the measurement. Measurements (and any explicit barrier) are marked as + non-mergeable. + +* **Order-preserving emission.** The merged groups are emitted in a + lexicographic topological order keyed by program index, guaranteeing that + measurement operators appear in the compressed list in the same order as the + ``MEASURE`` instructions in the program, regardless of how gates were fused. + +Setting ``max_subsystem_size=0`` disables merging entirely (useful for +debugging or for exact per-instruction inspection). + +Adapters +-------- + +The resolver is backend-agnostic: it yields each operator in its most specific +type. Each simulator then adapts the compressed list to the representation it +evolves: + +* **Density matrix** (:func:`~pyquil.simulation._resolver.adapt_for_density_matrix`): + everything becomes a ``SuperOp`` (a ``QuantumInstrument`` is collapsed to its + total channel, since the density-matrix backend does not branch on outcomes). + +* **Trajectory** (:func:`~pyquil.simulation._resolver.adapt_for_trajectory`): a + ``SuperOp`` is converted to a (truncated) ``KrausMap``; ``Unitary``, + ``KrausMap``, and ``QuantumInstrument`` pass through unchanged. + +Calculator +---------- + +The calculator applies the operator stack to the initial state. Two strategies +appear, both designed so that the compiled graph scales with the number of +*distinct subsystem shapes* rather than the number of operations: + +* **Scan + switch.** Operators are stacked into one array and applied with a + :func:`jax.lax.scan`; the loop body dispatches each operator to a + :func:`jax.lax.switch` branch selected by its base subsystem. Only one branch + per distinct subsystem is traced. + +* **Vectorized construction.** For state-vector evolution, gate matrices of the + same *kind* (same constructor, constant arguments, and embedding) are built in + a single ``jax.vmap`` and then folded within each merge group by a segmented + matrix-product scan. The traced graph is then proportional to the number of + gate kinds, not the number of gates. For parameter-free programs the operator + stack is a compile-time constant and is materialized once and reused. + +Because the whole calculator is a pure JAX function of :math:`\theta` (and, for +trajectories, a PRNG key), ``jax.jit`` and ``jax.grad`` compose with it directly. + + +The three simulators +==================== + +All three share the pipeline above and differ only in the state they evolve and +the operations they admit. + +.. list-table:: + :header-rows: 1 + :widths: 26 30 14 14 16 + + * - Simulator + - Use case + - Noise + - Measurements / resets + - Differentiable + * - ``PureStateVectorSimulator`` + - Gate-only programs + - No + - No + - ``jit`` + ``grad`` + * - ``DensityMatrixSimulator`` + - Any program, optional noise + - Yes + - Resets (measurements as total channel) + - ``jit`` + ``grad`` + * - ``TrajectorySimulator`` + - Monte-Carlo sampling + - Yes + - Yes + - ``jit`` (per batch) + +The qubit ceilings are set by memory: a state vector holds :math:`2^{N}` +amplitudes, so pure-state and trajectory simulation are practical to roughly +:math:`N \lesssim 26`; a density matrix holds :math:`4^{N}` entries, limiting the +density-matrix backend to roughly :math:`N \lesssim 13`. + +All simulators take the program (and, where relevant, a ``noise_model`` and +``max_subsystem_size``) at construction, and expose ``linearize``, ``resolve``, +``compress``, and ``compute``. ``compute`` is the entry point and takes the flat +parameter vector from ``linearize``. + +Pure state vector +----------------- + +For unitary, noiseless, measurement-free programs, evolve a pure state +:math:`|\psi\rangle = U_D \cdots U_1 |0\rangle`. This is the cheapest backend and +the natural target for gradient-based circuit optimization, and it can return the +full program unitary in addition to the state. + +.. code-block:: python + + import jax + import jax.numpy as jnp + from pyquil import Program + from pyquil.gates import H, CNOT, RX + from pyquil.simulation._simulator import PureStateVectorSimulator + + # A Bell state (no runtime parameters). + sim = PureStateVectorSimulator(Program(H(0), CNOT(0, 1))) + psi = sim.compute(jnp.array([])) # final state vector + + # The full 4x4 program unitary. + U = sim.unitary(jnp.array([])) + + # A parametric program, jit-compiled and differentiated. + from pyquil.quilatom import MemoryReference + from pyquil.quilbase import Declare + + p = Program(Declare("theta", "REAL", 1), RX(MemoryReference("theta", 0), 0)) + sim = PureStateVectorSimulator(p) + + def excited_pop(theta): + psi = sim.compute(jnp.array([theta])) + amps = psi.matrix.reshape(-1) + return jnp.abs(amps[1]) ** 2 # P(|1>) + + grad_pop = jax.jit(jax.grad(excited_pop)) + print(grad_pop(0.3)) # exact d P(|1>) / d theta + +Density matrix +-------------- + +For noisy, deterministic evolution, propagate the density matrix +:math:`\rho \mapsto \mathcal{E}_D \circ \cdots \circ \mathcal{E}_1 (\rho)` exactly. +This is the backend to use for expectation values and process metrics under +noise, since it tracks the full mixed state without sampling. Measurements are +applied as their total (outcome-averaged) channel. + +.. code-block:: python + + import jax.numpy as jnp + from pyquil import Program + from pyquil.gates import RX + from pyquil.noise._channels import Channel + from pyquil.noise._noise_model import NoiseModel + from pyquil.simulation._simulator import DensityMatrixSimulator + + gate = RX(jnp.pi, 0) + noise = NoiseModel.from_channels([ + Channel.from_gate_fidelity(inst=gate, fidelity=0.99), + ]) + + sim = DensityMatrixSimulator(Program(gate), noise_model=noise) + rho = sim.compute(jnp.array([])) # final density matrix (a quax DensityMatrix) + +A device-realistic model can be built directly from an instruction set +architecture with :meth:`NoiseModel.from_isa `, +which converts per-gate fidelities to depolarizing channels and per-qubit +readout fidelities to symmetric confusion. + +Trajectory +---------- + +For programs with mid-circuit measurements, resets, and feed-forward-style +sampling, unravel the dynamics into pure-state **quantum trajectories**: each +trajectory samples a Kraus operator (or measurement outcome) at every noisy step +and evolves a single state vector, so the cost is that of a state vector rather +than a density matrix. Averaging over trajectories recovers the density-matrix +result; the individual trajectories *are* the sampled measurement records. + +The number of trajectories is set by the shape of the PRNG key: a scalar key +runs one trajectory, while a batch of keys (from ``jax.random.split``) runs that +many in parallel via ``vmap``. A measurement is handled by flattening its +``QuantumInstrument`` into a single Kraus axis, so sampling a Kraus index also +selects the outcome. + +.. code-block:: python + + import jax + import jax.numpy as jnp + from pyquil import Program + from pyquil.gates import H, MEASURE + from pyquil.quilatom import MemoryReference + from pyquil.quilbase import Declare + from pyquil.simulation._simulator import TrajectorySimulator + + p = Program(Declare("ro", "BIT", 1), H(0), MEASURE(0, MemoryReference("ro", 0))) + sim = TrajectorySimulator(p) + params = jnp.array([]) + + # A batch of 1000 trajectories in parallel. + keys = jax.random.split(jax.random.key(0), 1000) + psi_batch, outcomes = sim.compute(params, keys) + # outcomes has shape (1000, n_measurements); ~50/50 for an H gate. + + # Or, scalable sampling that streams batches and keeps only the outcomes: + shots = sim.sample(params, num_trajectories=100_000, batch_size=2_000) + +``sample`` runs trajectories in fixed-size batches, discarding state vectors +between batches so the total number of shots is unbounded by memory. When +multiple JAX devices are available, each batch is sharded across them along the +trajectory axis. + + +Choosing a simulator +==================== + +* Use **``PureStateVectorSimulator``** for ideal, measurement-free circuits — + variational ansätze, unitary verification, gradient-based optimization. It is + the fastest and supports ``jax.grad`` and the full-unitary readout. + +* Use **``DensityMatrixSimulator``** when you need the *exact* noisy state or a + noise-averaged expectation value at modest qubit count (:math:`\lesssim 13`), + with no sampling noise. It is also differentiable. + +* Use **``TrajectorySimulator``** when the program contains mid-circuit + measurements or resets, when you want sampled bitstrings rather than a state, + or when the qubit count is too large for a density matrix but a state vector + still fits. Increase the trajectory count to reduce sampling error. + +In all cases, ``max_subsystem_size`` trades compile time against runtime: larger +groups mean fewer, denser operator applications (faster steady-state runtime) at +the cost of larger merged matrices and longer compilation. The default (2) is a +reasonable balance for circuits dominated by one- and two-qubit gates. diff --git a/poetry.lock b/poetry.lock index da732f6e8..ea1d28672 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3275,14 +3275,14 @@ httpx = ">=0.21.0" [[package]] name = "rigetti-quax" -version = "0.6.4" +version = "0.6.5" description = "A high-performance library for quantum information science built on top of JAX" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ - {file = "rigetti_quax-0.6.4-py3-none-any.whl", hash = "sha256:8d18d0ccfda2b2469f71b94f58c6dd936928e0656aff2f065123816e948eaaf0"}, - {file = "rigetti_quax-0.6.4.tar.gz", hash = "sha256:c1f46381f70c7ee2d25128fc88ead0984846c9c31f5f3d170c0c8efe7324e9f3"}, + {file = "rigetti_quax-0.6.5-py3-none-any.whl", hash = "sha256:a6fa3e046e715afed42dd13f1c252214e56708a7c5c6c1888f2d5f491dd9b8be"}, + {file = "rigetti_quax-0.6.5.tar.gz", hash = "sha256:63c2120eb295ab407f7e2d9d79ea3ce271ba3780a26bce3bb2f7cd71db4cdcc3"}, ] [package.dependencies] @@ -4120,4 +4120,4 @@ latex = ["ipython"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.13" -content-hash = "9c5b1b0f8f39ead4cf829f29e5bb6379e5c1b309a0b884df1aa3cd63ce69bb88" +content-hash = "bdb98b172f015c293126930752e9c7648e92da5034633cee480c32940a0aa14d" diff --git a/pyproject.toml b/pyproject.toml index 0ac993607..111d24288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} toml = {version = "^0.10.2", optional = true} -rigetti-quax = ">=0.6.4" +rigetti-quax = ">=0.6.5" [tool.poetry.extras] latex = ["ipython"] diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index 2edce166e..e9405b213 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -38,6 +38,8 @@ from jax import Array from quil.expression import Expression as QuilExpression from quil.program import Program as RSProgram +from scipy.linalg import expm as scipy_expm +from scipy.linalg import fractional_matrix_power from scipy.linalg import logm as scipy_logm from pyquil.quilatom import Expression, FormalArgument, Parameter, substitute @@ -381,16 +383,16 @@ def from_pauli_noise( raise ValueError("Pauli error rates plus the implicit identity rate must sum to 1.0.") pauli_error_rates = list(reversed(pauli_error_rates)) - # Build Pauli Kraus operators using quax ensembles - single_paulis = qx.ensembles.PAULIS # ensemble of (I, X, Y, Z) - if num_qubits == 1: - pauli_ops = single_paulis - else: - pauli_ops = reduce(lambda a, b: a | b, [single_paulis for _ in range(num_qubits)]) + # Build the 4**num_qubits Pauli operators as tensor (Kronecker) products of the + # single-qubit Paulis, in lexicographic (I, X, Y, Z) order matching pauli_error_rates. + single_pauli_matrices = qx.ensembles.PAULIS.matrix # (4, 2, 2): I, X, Y, Z + pauli_op_matrices = jnp.stack( + [reduce(jnp.kron, paulis) for paulis in product(single_pauli_matrices, repeat=num_qubits)] + ) # (4**num_qubits, 2**num_qubits, 2**num_qubits) # Scale each Pauli by sqrt(probability) to form Kraus operators coeffs = jnp.sqrt(jnp.array(pauli_error_rates, dtype=float)) - kraus_matrices = coeffs[:, None, None] * pauli_ops.matrix + kraus_matrices = coeffs[:, None, None] * pauli_op_matrices kraus_map = qx.KrausMap.from_matrix(kraus_matrices, unitary.dims) process_superop = qx.to_superop(kraus_map @ unitary) @@ -807,12 +809,20 @@ def __str__(self) -> str: return f"<{self.inst.out()} ~ ({100 * self.pauli_fidelity:.2f}%)>" def __eq__(self, other: object) -> bool: - """Check equality based on instruction and process fidelity.""" + """Check equality by instruction and exact process and ideal-gate matrices. + + Equality is exact (no fidelity tolerance): two channels are equal only if they + share the same instruction and bit-for-bit identical process and target-unitary + matrices. Making tolerance decisions on the user's behalf is deliberately avoided. + """ if not isinstance(other, Channel): return False if self.inst != other.inst: return False - return bool(jnp.isclose(float(qx.process_fidelity(self.process, other.process)), 1.0, atol=1e-9)) + return bool( + jnp.array_equal(self.process.matrix, other.process.matrix) + and jnp.array_equal(self.target_unitary.matrix, other.target_unitary.matrix) + ) __hash__ = None # type: ignore[assignment] @@ -837,6 +847,22 @@ def __matmul__(self, other: Channel) -> Channel: composed_superop = qx.to_superop(self.process @ u_dag_superop @ other.process) return replace(self, process=composed_superop) + def __pow__(self, power: float) -> Channel: + r"""Raise the channel's noise to a fractional ``power``, preserving the gate. + + With the channel written :math:`\\mathcal{E} = \\Lambda \\circ \\mathcal{U}`, this returns + :math:`\\Lambda^{power} \\circ \\mathcal{U}`: only the noise :math:`\\Lambda` is raised to the + fractional matrix power, while the ideal gate is kept. So ``power = 0`` yields the noiseless + gate, ``1`` leaves the channel unchanged, and ``> 1`` strengthens the noise. This is the knob + used to sweep noise strength. + """ + if not isinstance(power, (int, float)): + return NotImplemented + powered_noise_matrix = fractional_matrix_power(np.asarray(self.noise_process.matrix), power) + powered_noise = qx.SuperOp.from_matrix(jnp.asarray(powered_noise_matrix), self.noise_process.dims) + process = qx.to_superop(powered_noise @ qx.to_superop(self.unitary)) + return replace(self, process=process) + def __or__(self, other: Channel | MeasurementChannel) -> CycleChannel: """Tensor product of two channels on disjoint qubits, producing a CycleChannel. @@ -1173,12 +1199,12 @@ def __str__(self) -> str: return f"" def __eq__(self, other: object) -> bool: - """Check equality based on instruction and operator.""" + """Check equality by instruction and exact instrument matrix (no tolerance).""" if not isinstance(other, MeasurementChannel): return False if self.inst != other.inst: return False - return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) + return bool(jnp.array_equal(self.process.matrix, other.process.matrix)) __hash__ = None # type: ignore[assignment] @@ -1197,6 +1223,47 @@ def __matmul__(self, other: MeasurementChannel) -> MeasurementChannel: composed = self.process @ other.process return replace(self, process=composed) + def __pow__(self, power: float) -> MeasurementChannel: + r"""Raise the measurement's classification noise to a fractional ``power``. + + The confusion and transition matrices are column-stochastic, so a fractional power is + only well-defined through their *generator*: with :math:`M = e^{G}` (where :math:`G` has + zero column sums), the principled power is :math:`M^{p} = e^{p G}`, which is again + column-stochastic. This avoids the non-physical (e.g. negative) entries that a naive + ``fractional_matrix_power`` of a stochastic matrix can produce. + + So ``power = 0`` is an ideal (noiseless) measurement, ``1`` leaves it unchanged, and + ``> 1`` degrades it. Mirrors :meth:`Channel.__pow__` for sweeping readout-noise strength. + + :raises ValueError: If the matrix is not embeddable (no real generator) so that the + powered matrix is not a valid stochastic matrix. + """ + if not isinstance(power, (int, float)): + return NotImplemented + + def _powered_stochastic(matrix: Array) -> Array: + m = np.asarray(matrix, dtype=complex) + # Generator G = log(M); M**power = exp(power * G). Zero column sums of G are + # preserved under scaling, so exp(power * G) stays column-stochastic. + powered = scipy_expm(power * scipy_logm(m)) + if np.max(np.abs(powered.imag)) > 1e-9: + raise ValueError( + f"MeasurementChannel ** {power} has no real generator; the matrix is not " + "embeddable, so the powered measurement is not a valid stochastic matrix." + ) + powered = powered.real + if np.any(powered < -1e-9) or not np.allclose(powered.sum(axis=0), 1.0, atol=1e-6): + raise ValueError( + f"MeasurementChannel ** {power} is not a valid (non-negative, column-stochastic) " + "matrix; the underlying confusion/transition matrix is not embeddable for this power." + ) + # Clip away sub-tolerance numerical negatives validated above. + return jnp.asarray(np.clip(powered, 0.0, None)) + + confusion = _powered_stochastic(self.confusion_matrix) + transition = _powered_stochastic(self.transition_matrix) + return MeasurementChannel.from_confusion_and_transition(self.inst, confusion, transition) + def __or__(self, other: Channel | MeasurementChannel) -> CycleChannel: """Tensor product of two channels on disjoint qubits, producing a CycleChannel. @@ -1374,12 +1441,12 @@ def __str__(self) -> str: return f"" def __eq__(self, other: object) -> bool: - """Check equality based on instruction and process matrix.""" + """Check equality by instruction and exact process matrix (no tolerance).""" if not isinstance(other, ResetChannel): return False if self.inst != other.inst: return False - return bool(jnp.allclose(self.process.matrix, other.process.matrix, atol=1e-9)) + return bool(jnp.array_equal(self.process.matrix, other.process.matrix)) __hash__ = None # type: ignore[assignment] diff --git a/pyquil/noise/_noise_model.py b/pyquil/noise/_noise_model.py index cf188c7b7..5bd1682ae 100644 --- a/pyquil/noise/_noise_model.py +++ b/pyquil/noise/_noise_model.py @@ -25,7 +25,6 @@ depolarizing channel for any gate. - ``CompositeNoiseModel``: Chains multiple noise models, returning the first non-None channel. -- ``NO_NOISE``: A sentinel noise model that always returns ``None``. - Program-level fidelity estimation utilities. """ @@ -35,7 +34,7 @@ import logging from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass -from functools import cached_property, reduce +from functools import reduce from operator import mul from types import MappingProxyType from typing import ( @@ -127,12 +126,12 @@ def __init__( channel_map[inst] = channel object.__setattr__(self, "channels", MappingProxyType(channel_map)) - @cached_property - def _channel_map( - self, - ) -> Mapping[NoiseInstruction, ChannelType]: - """Map from instruction to channel for fast lookup.""" - return self.channels + def __getstate__(self) -> dict[str, dict[NoiseInstruction, ChannelType]]: + # ``channels`` is a MappingProxyType (unpicklable); serialize it as a plain dict. + return {"channels": dict(self.channels)} + + def __setstate__(self, state: Mapping[str, Mapping[NoiseInstruction, ChannelType]]) -> None: + object.__setattr__(self, "channels", MappingProxyType(dict(state["channels"]))) @overload def get_channel(self, inst: Gate) -> Channel | CycleChannel | None: ... @@ -151,7 +150,7 @@ def get_channel( :param inst: The instruction (gate, measurement, or reset) for which to retrieve the noise channel. :return: The noise channel associated with the instruction, or None if no channel is found. """ - return self._channel_map.get(inst) + return self.channels.get(inst) # ────────────────────────────────────────────── # Constructors @@ -180,6 +179,13 @@ def from_isa(cls: type[NoiseModel], compiler_isa: CompilerISA) -> NoiseModel: errors are symmetric. Only gates with concrete numeric parameters are included. + .. note:: + Two-qubit gate channels are keyed by the ISA edge's operand order + (e.g. ``CZ 0 1``). A program that issues the gate with the operands + reversed (``CZ 1 0``) will not match this channel, even for symmetric + gates. Issue gates in the ISA operand order, or build channels + explicitly for both orderings. + :param compiler_isa: The compiler ISA. :return: A NoiseModel with channels according to the provided fidelities. """ @@ -220,9 +226,11 @@ def from_isa(cls: type[NoiseModel], compiler_isa: CompilerISA) -> NoiseModel: fidelity = op_info.fidelity if qubit_idx in seen_measure_qubits: continue - seen_measure_qubits.add(qubit_idx) if fidelity is None: + # Don't mark the qubit seen on a fidelity-less entry; a later + # MeasureInfo for the same qubit may carry a usable fidelity. continue + seen_measure_qubits.add(qubit_idx) m_inst = Measurement(qubit=QuilQubit(qubit_idx), classical_reg=None) channels[m_inst] = MeasurementChannel.from_readout_fidelity(inst=m_inst, fidelity=fidelity) @@ -295,35 +303,32 @@ def __eq__(self, other: object) -> bool: return False return dict(self.channels) == dict(other.channels) - def __hash__(self) -> int: - """Hash based on id (NoiseModel is not value-hashable due to array contents).""" - return id(self) + # Unhashable: its channels hold jax arrays and are themselves unhashable, and an + # ``id``-based hash would be inconsistent with the value-based ``__eq__``. + __hash__ = None # type: ignore[assignment] def __add__(self, other: NoiseModel) -> NoiseModel: - """Combine two NoiseModels. + """Combine two NoiseModels into their disjoint union. - For channels with matching instructions, compose them (``channel_A @ channel_B``). - For non-overlapping channels, include both. + The two models must not both define a channel for the same instruction. + Addition is a union of channels, not a composition: overlapping channels are + a conflict, not an "addition". Compose channels explicitly + (``channel_a @ channel_b``) if that is what you intend. + + :raises ValueError: If both models define a channel for the same instruction. """ if not isinstance(other, NoiseModel): return NotImplemented - combined: dict[NoiseInstruction, ChannelType] = {} - all_insts = list(dict.fromkeys(list(self.channels) + list(other.channels))) - for inst in all_insts: - mine = self.channels.get(inst) - theirs = other.channels.get(inst) - if mine is not None and theirs is not None: - # Both have a channel for this instruction — compose them - # (only same-type composition is defined) - composed = mine @ theirs # type: ignore[operator] - combined[inst] = composed - elif mine is not None: - combined[inst] = mine - elif theirs is not None: - combined[inst] = theirs + overlap = set(self.channels) & set(other.channels) + if overlap: + insts = ", ".join(repr(inst) for inst in overlap) + raise ValueError( + f"Cannot add NoiseModels: both define a channel for the same instruction(s): {insts}. " + "Addition is a disjoint union; compose the channels explicitly if that is intended." + ) - return NoiseModel(channels=combined) + return NoiseModel(channels={**self.channels, **other.channels}) def with_channels(self, channels: Iterable[ChannelType]) -> NoiseModel: """Return a new model with additional channels. @@ -345,10 +350,6 @@ def with_channels(self, channels: Iterable[ChannelType]) -> NoiseModel: # ────────────────────────────────────────────────────────── -NOISELESS: NoiseModelLike = NoiseModel() -"""Sentinel noise model that applies no noise to any instruction.""" - - @dataclass(frozen=True) class DepolarizingNoiseModel: r"""A noise model that applies uniform depolarizing noise to every gate. @@ -431,9 +432,7 @@ def estimate_program_fidelity(program: Program, noise_model: NoiseModelLike) -> for inst in program.instructions: if isinstance(inst, Gate): channel = noise_model.get_channel(inst) - if channel is not None and isinstance(channel, Channel): - gate_fidelities.append(channel.pauli_fidelity) - elif channel is not None and isinstance(channel, CycleChannel): + if isinstance(channel, (Channel, CycleChannel)): gate_fidelities.append(channel.pauli_fidelity) return reduce(mul, gate_fidelities) diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 6607d710f..59f72377f 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -35,7 +35,7 @@ import logging from collections.abc import Callable, Iterator, Mapping from copy import deepcopy -from typing import Any, TypeAlias, cast +from typing import Any, NamedTuple, TypeAlias, cast import jax.numpy as jnp import networkx as nx @@ -44,6 +44,7 @@ from pyquil.noise._channels import ( Channel, + CustomGateMap, CycleChannel, MeasurementChannel, ResetChannel, @@ -182,10 +183,37 @@ def _measure_registers(program: Program) -> set[str]: return regs +class _ExpansionContext(NamedTuple): + """Program-level derivations that are independent of qubit dimensions. + + Computing these once lets :func:`resolve_program` expand a program twice + (dimension inference, then final expansion) without re-deriving the custom + gates, circuit definitions, etc. on each pass. + """ + + circuit_definitions: dict[str, DefCircuit] + custom_gates: CustomGateMap | None + measure_regs: set[str] + all_qubits: list[int] + + +def _build_expansion_context(program: Program) -> _ExpansionContext: + """Derive the dimension-independent expansion context for a program.""" + circuit_definitions: dict[str, DefCircuit] = { + inst.name: inst for inst in program.instructions if isinstance(inst, DefCircuit) + } + custom_gates = get_custom_gates_from_program(program) or None + measure_regs = _measure_registers(program) + all_qubits = sorted(program.get_qubit_indices()) + return _ExpansionContext(circuit_definitions, custom_gates, measure_regs, all_qubits) + + def expand_program( program: Program, noise_model: NoiseModelLike | None = None, qubit_dimensions: Mapping[int, int] | None = None, + *, + context: _ExpansionContext | None = None, ) -> tuple[list[ExpandedOp], list[tuple[int, ...]], list[tuple[str, int]]]: """Expand a program into operators and physical qubit tuples. @@ -209,22 +237,22 @@ def expand_program( :param qubit_dimensions: Optional mapping from physical qubit id to its Hilbert-space dimension. Used for ideal measurement and reset operators, whose quax constructors otherwise default to qubit dimension. + :param context: Optional precomputed :class:`_ExpansionContext`. When omitted + it is derived from the program; pass it to avoid recomputing the + dimension-independent derivations across multiple expansion passes. :return: Tuple of ``(ops, qubit_tuples, param_refs)`` where each op is either a concrete quax operator or a ``Callable[[Array], Unitary]`` for parameterized gates, each qubit tuple contains physical qubit IDs, and ``param_refs`` is a list of ``(register_name, offset)`` pairs for each scalar parameter in program order. """ - # Derive circuit definitions and custom gates from the program. - circuit_definitions: dict[str, DefCircuit] = {} - for inst in program.instructions: - if isinstance(inst, DefCircuit): - circuit_definitions[inst.name] = inst - - custom_gates = get_custom_gates_from_program(program) or None - - measure_regs = _measure_registers(program) - all_qubits = sorted(program.get_qubit_indices()) + # Derive (or reuse) the dimension-independent program context. + if context is None: + context = _build_expansion_context(program) + circuit_definitions = context.circuit_definitions + custom_gates = context.custom_gates + measure_regs = context.measure_regs + all_qubits = context.all_qubits ops: list[ExpandedOp] = [] qubit_tuples: list[tuple[int, ...]] = [] @@ -387,90 +415,95 @@ def _infer_dims(resolved: list[ResolvedOp], n_qubits: int) -> tuple[int, ...]: # ══════════════════════════════════════════════════════════ -class Resolver: - """Resolves a flat parameter vector into a list of (operator, subsystem) pairs. +def _freeze_resolver( + ops: list[ExpandedOp], + subsystems: list[tuple[int, ...]], +) -> Callable[[Array], list[ResolvedOp]]: + """Build the closure that turns a parameter vector into resolved operations. - Constructed via :func:`resolver_from_program`. Call instances directly:: + Fixed operators pass straight through; ``ParametricGate`` items are called + with the parameter vector to produce a concrete ``Unitary``. + """ + frozen = list(zip(ops, subsystems, strict=False)) - resolver = resolver_from_program(program, ...) - ops = resolver(params) + def resolve(params: Array) -> list[ResolvedOp]: + return [ + (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) + for item, subsystem in frozen + ] + + return resolve - :param dims: Inferred per-qudit dimensions (e.g. ``(2, 2, 3)``). - """ - __slots__ = ("_resolve_fn", "dims") +class Resolution(NamedTuple): + """Everything the simulators need from a program after expansion. - def __init__(self, resolve_fn: Callable[[Array], list[ResolvedOp]], dims: tuple[int, ...]) -> None: - self._resolve_fn = resolve_fn - self.dims = dims + :param dims: Inferred per-qudit dimensions (e.g. ``(2, 2, 3)``). + :param ops: Expanded operators, one per DAG node, in program order. + :param subsystems: 0-based qubit tuple each operator acts on. + :param param_refs: ``(register_name, offset)`` for each scalar parameter. + :param resolve: Closure mapping a parameter vector to ``(operator, subsystem)`` pairs. + """ - def __call__(self, params: Array) -> list[ResolvedOp]: - return self._resolve_fn(params) + dims: tuple[int, ...] + ops: list[ExpandedOp] + subsystems: list[tuple[int, ...]] + param_refs: list[tuple[str, int]] + resolve: Callable[[Array], list[ResolvedOp]] -def resolver_from_program( +def resolve_program( program: Program, noise_model: NoiseModelLike | None = None, qubits: list[int] | None = None, -) -> tuple[Resolver, nx.DiGraph]: - """Build a :class:`Resolver` and dependency DAG from a program. - - The resolver accepts a flat parameter vector and produces one - ``(operator, subsystem)`` pair per operation, in program order. + dims: tuple[int, ...] | None = None, +) -> Resolution: + """Expand a program and build its parameter-resolving closure. Operators are returned in their most specific native type: - * Ideal gates → ``qx.Unitary`` + * Ideal gates → ``qx.Unitary`` (parametric gates as a ``ParametricGate`` callable) * Noisy gates (``Channel``) → ``qx.SuperOp`` * Expanded cycle gates with ``CycleChannel`` noise → constituent ``qx.SuperOp`` * Measurements → ``qx.QuantumInstrument`` - * Noisy resets (``ResetChannel``) → ``qx.SuperOp`` - * Ideal resets → ``qx.SuperOp`` + * Noisy/ideal resets → ``qx.SuperOp`` - Custom gate definitions (DEFGATE) and circuit definitions (DEFCIRCUIT) - are derived from the program automatically. + The program is expanded twice: first with default (qubit) register + dimensions to infer each register's true dimension from the gates and noisy + channels, then again with those dimensions so ideal measurement/reset + instruments use the correct dimension. Passing *dims* skips the first pass. :param program: Quil program (may contain DEFCIRCUITs and DEFGATEs). :param noise_model: Optional noise model. - :param qubits: Optional explicit qubit list. If ``None``, inferred from - the program. Use this when the simulator knows about qubits that - don't appear in the program. - :return: Tuple of ``(Resolver, dag)``. + :param qubits: Optional explicit qubit list. If ``None``, inferred from the + program. Use this when the simulator knows about qubits that don't + appear in the program. + :param dims: Optional pre-determined per-qudit dimensions. + :return: A :class:`Resolution`. """ if qubits is None: qubits = sorted(program.get_qubit_indices()) qubit_indices = {q: i for i, q in enumerate(qubits)} - n_qubits = len(qubits) - initial_expanded_ops, initial_phys_qubits, initial_param_refs = expand_program(program, noise_model) - initial_mapped_qubits = remap_qubits(initial_phys_qubits, qubit_indices) - initial_frozen_ops = list(zip(initial_expanded_ops, initial_mapped_qubits, strict=False)) + # Derive the dimension-independent context once and reuse it across both passes. + context = _build_expansion_context(program) - def initial_resolve(params: Array) -> list[ResolvedOp]: - return [ - (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) - for item, subsystem in initial_frozen_ops - ] + def expand( + qubit_dimensions: Mapping[int, int] | None, + ) -> tuple[list[ExpandedOp], list[tuple[int, ...]], list[tuple[str, int]]]: + ops, phys_qubits, param_refs = expand_program( + program, noise_model, qubit_dimensions=qubit_dimensions, context=context + ) + return ops, remap_qubits(phys_qubits, qubit_indices), param_refs - dims = _infer_dims(initial_resolve(jnp.zeros(len(initial_param_refs))), n_qubits) + if dims is None: + ops, subsystems, param_refs = expand(None) + resolve = _freeze_resolver(ops, subsystems) + dims = _infer_dims(resolve(jnp.zeros(len(param_refs))), len(qubits)) qubit_dimensions = {q: dims[i] for q, i in qubit_indices.items()} - expanded_ops, phys_qubits, _param_refs = expand_program( - program, - noise_model, - qubit_dimensions=qubit_dimensions, - ) - mapped_qubits = remap_qubits(phys_qubits, qubit_indices) - dag = build_dag(mapped_qubits) - frozen_ops = list(zip(expanded_ops, mapped_qubits, strict=False)) - - def resolve(params: Array) -> list[ResolvedOp]: - return [ - (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) - for item, subsystem in frozen_ops - ] - - return Resolver(resolve, dims=dims), dag + ops, subsystems, param_refs = expand(qubit_dimensions) + return Resolution(dims, ops, subsystems, param_refs, _freeze_resolver(ops, subsystems)) # ══════════════════════════════════════════════════════════ @@ -500,13 +533,11 @@ def adapt_for_density_matrix( """ result: list[DensityMatrixOp] = [] for op, subsystem in ops: - match op: - case qx.SuperOp(): - result.append((op, subsystem)) - case qx.QuantumInstrument(): - result.append((qx.to_superop(op.total_channel()), subsystem)) - case qx.Unitary() | qx.KrausMap(): - result.append((qx.to_superop(op), subsystem)) + # ``qx.to_superop`` is single-dispatch and idempotent on SuperOp, so it + # covers Unitary/SuperOp/KrausMap directly; only an instrument needs its + # total channel taken first. + channel = op.total_channel() if isinstance(op, qx.QuantumInstrument) else op + result.append((qx.to_superop(channel), subsystem)) return result @@ -533,6 +564,8 @@ def adapt_for_trajectory( result.append((km, subsystem)) case qx.Unitary() | qx.KrausMap() | qx.QuantumInstrument(): result.append((op, subsystem)) + case _: + raise TypeError(f"Cannot adapt operator of type {type(op).__name__} for trajectory simulation.") return result @@ -548,15 +581,12 @@ def _merge_ops( ) -> ResolvedOp: """Merge a sequence of operators into a single operator on the union subsystem. - Each operator is embedded into the merged Hilbert space and then composed - sequentially using the ``@`` operator, which handles type promotion - automatically (Unitary, SuperOp, KrausMap). - - For groups containing only ``Unitary`` operators, the result is a ``Unitary``. - For groups containing any noisy operator (``SuperOp``, ``KrausMap``), all - operators are promoted to ``SuperOp``, composed, and the result is returned - as a ``SuperOp``. Downstream adapters handle final conversion (e.g. to - ``KrausMap`` for trajectories). + Each operator is embedded into the merged Hilbert space with :func:`quax.embed` + and composed sequentially with ``@``. Quax's operator ``@`` promotes mixed + types automatically (requires ``rigetti-quax >= 0.6.5``), so an all-``Unitary`` + group yields a ``Unitary`` while a group containing any channel promotes to a + ``SuperOp``. Downstream adapters handle final conversion (e.g. to ``KrausMap`` + for trajectories). :param ops_with_subsystems: Ordered list of ``(operator, subsystem)`` pairs to merge (applied in order: first element is applied first). @@ -564,19 +594,12 @@ def _merge_ops( :param dims: Global per-qudit dimensions tuple. :return: A single ``(operator, merged_subsystem)`` pair. """ - has_noisy = any(isinstance(op, (qx.KrausMap, qx.SuperOp)) for op, _ in ops_with_subsystems) - target_dims = tuple(dims[q] for q in merged_subsystem) - accumulated = None + accumulated: FixedOp | None = None for op, subsystem in ops_with_subsystems: positions = tuple(merged_subsystem.index(q) for q in subsystem) - - if has_noisy: - embedded = qx.embed(qx.to_superop(op), target_dims=target_dims, positions=positions) - else: - embedded = qx.embed(op, target_dims=target_dims, positions=positions) - + embedded = qx.embed(op, target_dims=target_dims, positions=positions) accumulated = embedded if accumulated is None else embedded @ accumulated if accumulated is None: diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index a24dfe9e1..acd4f3422 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -34,6 +34,7 @@ import math from collections.abc import Callable +from dataclasses import dataclass, field from typing import Any, cast import jax @@ -56,8 +57,7 @@ adapt_for_trajectory, build_dag, compressor_from_dag, - expand_program, - remap_qubits, + resolve_program, ) @@ -117,26 +117,6 @@ def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVect return branch -def _infer_dims(resolved: list[ResolvedOp], n_qubits: int) -> tuple[int, ...]: - """Infer per-qudit dimensions from a resolved operation list. - - Each operator carries its own per-qudit input dimensions via - ``op.dims[1]``. A slot's dimension is the maximum dimension seen across - every operation acting on it, defaulting to ``2`` (qubit) for slots that - no operation touches. - - :param resolved: Resolved ``(operator, subsystem)`` pairs. - :param n_qubits: Number of qudit slots. - :return: Inferred per-qudit dimensions tuple. - """ - dims = [2] * n_qubits - for op, subsystem in resolved: - op_dims = op.dims[1] - for q, d in zip(subsystem, op_dims, strict=False): - dims[q] = max(dims[q], d) - return tuple(dims) - - # ══════════════════════════════════════════════════════════ # Base class # ══════════════════════════════════════════════════════════ @@ -186,45 +166,14 @@ def __init__( self.qubits = qubits self.n_qubits = len(qubits) - qubit_indices = {q: i for i, q in enumerate(qubits)} - - # First expand with default ideal measurement/reset dimensions so we can - # infer the register dimensions from gates and noisy channels. - if dims is None: - initial_expanded_ops, initial_phys_qubits, initial_param_refs = expand_program( - program, - noise_model, - ) - initial_mapped_qubits = remap_qubits(initial_phys_qubits, qubit_indices) - initial_frozen_ops = list(zip(initial_expanded_ops, initial_mapped_qubits, strict=False)) - - def initial_resolve(params: Array) -> list[ResolvedOp]: - return [ - (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) - for item, subsystem in initial_frozen_ops - ] - - self.dims = _infer_dims(initial_resolve(jnp.zeros(len(initial_param_refs))), self.n_qubits) - else: - self.dims = dims - - # Expand again with inferred dimensions so ideal measurement/reset - # instruments are not stuck at qubit dimension. - qubit_dimensions = {q: self.dims[i] for q, i in qubit_indices.items()} - expanded_ops, phys_qubits, param_refs = expand_program( - program, - noise_model, - qubit_dimensions=qubit_dimensions, - ) - mapped_qubits = remap_qubits(phys_qubits, qubit_indices) - dag = build_dag(mapped_qubits) - frozen_ops = list(zip(expanded_ops, mapped_qubits, strict=False)) - - def resolve(params: Array) -> list[ResolvedOp]: - return [ - (cast(Callable[[Array], qx.Unitary], item)(params) if callable(item) else item, subsystem) - for item, subsystem in frozen_ops - ] + # Expand the program into operators, inferring register dimensions when + # not supplied. See :func:`resolve_program`. + res = resolve_program(program, noise_model, qubits, dims=dims) + self.dims = res.dims + self._resolve_fn = res.resolve + self._expanded_ops = tuple(res.ops) + self._raw_subsystems = tuple(res.subsystems) + param_refs = res.param_refs # Build linearizer from parameter references discovered during expansion. def linearize(memory_map: MemoryMap) -> Array: @@ -234,9 +183,8 @@ def linearize(memory_map: MemoryMap) -> Array: return jnp.array(values, dtype=float) self._linearize_fn = linearize - self._resolve_fn = resolve - self._expanded_ops = tuple(expanded_ops) - self._raw_subsystems = tuple(mapped_qubits) + + dag = build_dag(res.subsystems) # Whether any gate matrix depends on a runtime parameter. When it does # not, the compressed operator stack is a compile-time constant and can @@ -247,7 +195,7 @@ def linearize(memory_map: MemoryMap) -> Array: # Derive barrier nodes: measurements (QuantumInstrument) should not # be merged by the compressor. - barrier_nodes = {i for i, op in enumerate(expanded_ops) if isinstance(op, qx.QuantumInstrument)} + barrier_nodes = {i for i, op in enumerate(res.ops) if isinstance(op, qx.QuantumInstrument)} self._compress_fn = compressor_from_dag( dag, @@ -301,31 +249,48 @@ def compute(self, params: Array, **kwargs: Any) -> Any: """Compute the simulation result. Subclasses must override.""" raise NotImplementedError + def _evolve(self, state: Any, op_stack: Array) -> Any: + """Apply a stack of operator matrices to *state* via a scan + switch. + + Each operator is dispatched to the switch branch for its base subsystem + (``self._branches``, keyed by ``self._idx_arr``), so the compiled graph + size scales with the number of distinct base subsystems rather than the + number of operations. Used by the state-vector and density-matrix + simulators, which differ only in their branch and state types. + """ + branches = self._branches # type: ignore[attr-defined] + + def body(state: Any, xs: tuple[Array, Array]) -> tuple[Any, None]: + op_mat, sidx = xs + return jax.lax.switch(sidx, branches, op_mat, state), None + + state, _ = jax.lax.scan(body, state, (op_stack, self._idx_arr)) # type: ignore[attr-defined] + return state + # ══════════════════════════════════════════════════════════ # Vectorized gate construction # ══════════════════════════════════════════════════════════ -def _embed_matrix_np( - mat: np.ndarray, op_subsystem: tuple[int, ...], group_subsystem: tuple[int, ...], dims: tuple[int, ...], d_max: int -) -> np.ndarray: - """Embed a gate matrix into a larger subsystem (numpy, for constant ops). +def _embed_constant_matrix( + mat: Array, op_subsystem: tuple[int, ...], group_subsystem: tuple[int, ...], dims: tuple[int, ...], d_max: int +) -> Array: + """Embed a constant gate matrix into its merge group, padded to ``d_max``. - Computes the d_max×d_max padded matrix that applies ``mat`` on - ``op_subsystem`` within the Hilbert space of ``group_subsystem``. + Computes the ``d_max × d_max`` matrix that applies ``mat`` on ``op_subsystem`` + within the Hilbert space of ``group_subsystem`` via :func:`quax.embed`. This + runs eagerly (once per parameter-free gate, outside any ``jit``); the result + is closed over as a compile-time constant. The final pad to ``d_max`` — the + uniform stack width across all groups — is plain array padding, not a + tensor-product embedding, so it has no quax equivalent. """ - if op_subsystem == group_subsystem: - return np.pad(mat, [(0, d_max - s) for s in mat.shape]) - import quax as qx # noqa: F811 — local re-import for clarity - + op_dims = tuple(dims[q] for q in op_subsystem) target_dims = tuple(dims[q] for q in group_subsystem) positions = tuple(group_subsystem.index(q) for q in op_subsystem) - op_dims = tuple(dims[q] for q in op_subsystem) - op = qx.Unitary.from_matrix(jnp.array(mat), (op_dims, op_dims)) - embedded = qx.embed(op, target_dims=target_dims, positions=positions) - result = np.asarray(embedded.matrix) - return np.pad(result, [(0, d_max - s) for s in result.shape]) + op = qx.Unitary.from_matrix(jnp.asarray(mat), (op_dims, op_dims)) + embedded = qx.embed(op, target_dims=target_dims, positions=positions).matrix + return jnp.pad(embedded, [(0, d_max - s) for s in embedded.shape]) def _make_embed_fn( @@ -399,160 +364,155 @@ def _embed_general(mat: Array) -> Array: return _embed_general +@dataclass +class _GateBatch: + """A set of gates sharing one constructor, concrete layout, and embedding. + + Members differ only in which entries of the parameter vector feed their + free arguments, so all of them are built with a single ``jax.vmap``. This + keeps the traced graph proportional to the number of distinct gate *kinds* + rather than the number of gates. + """ + + gate_fn: Callable[..., qx.Unitary] + n_args: int + #: ``(slot, value)`` for each compile-time-constant argument. + concrete_args: tuple[tuple[int, float], ...] + #: Embeds a raw gate matrix into its merge group, padded to ``d_max``. + embed_fn: Callable[[Array], Array] + #: Sorted-array positions this batch fills, one per member. + positions: list[int] = field(default_factory=list) + #: Parameter-vector index for each free argument, one list per member. + param_indices: list[list[int]] = field(default_factory=list) + + def builder(self) -> Callable[[Array], Array]: + """Return ``params -> (n_members, d_max, d_max)`` embedded gate matrices.""" + concrete = {slot for slot, _ in self.concrete_args} + free_slots = [j for j in range(self.n_args) if j not in concrete] + gate_fn, embed_fn, n_args, concrete_args = self.gate_fn, self.embed_fn, self.n_args, self.concrete_args + param_indices = jnp.asarray(self.param_indices) # (n_members, n_free) + + def single(free_values: Array) -> Array: + args: list[Any] = [None] * n_args + for slot, val in concrete_args: + args[slot] = val + for k, slot in enumerate(free_slots): + args[slot] = free_values[k] + return embed_fn(gate_fn(*args).matrix) + + batched = jax.vmap(single) + return lambda params: batched(params[param_indices]) + + +def _make_group_fold(group_start: list[int], n_ops: int, d_max: int) -> Callable[[Array], Array]: + """Build the per-group matrix-product fold. + + ``fold(raw)`` takes ``(n_ops, d_max, d_max)`` embedded gate matrices laid out + in group order and returns ``(n_groups, d_max, d_max)`` — the ordered matrix + product of each group's gates. Groups are gathered into a padded + ``(n_groups, max_size, ...)`` array (short groups padded with an identity + sentinel) so every group folds under a single ``jax.vmap``. + """ + n_groups = len(group_start) - 1 + sizes = np.diff(group_start) + max_size = int(sizes.max()) if n_groups else 1 + + # gather[g, k] = sorted position of group g's k-th gate, or n_ops (the + # identity sentinel appended in ``fold``) for padding. + gather = np.full((n_groups, max_size), n_ops, dtype=np.int32) + for g in range(n_groups): + gather[g, : sizes[g]] = np.arange(group_start[g], group_start[g + 1]) + gather_jax = jnp.asarray(gather) + eye = jnp.eye(d_max, dtype=complex) + + def group_product(mats: Array) -> Array: + final, _ = jax.lax.scan(lambda acc, m: (m @ acc, None), eye, mats) + return final + + def fold(raw: Array) -> Array: + padded = jnp.concatenate([raw, eye[None]], axis=0)[gather_jax] # (n_groups, max_size, d, d) + return jax.vmap(group_product)(padded) + + return fold + + def _build_vectorized_unitary_constructor( - expanded_ops: tuple, + expanded_ops: tuple[Any, ...], raw_subsystems: tuple[tuple[int, ...], ...], - emit_order: list, + emit_order: list[tuple[int, list[int], tuple[int, ...]]], dims: tuple[int, ...], d_max: int, -) -> tuple[Callable[[Array], Array], np.ndarray, np.ndarray]: - """Build a vectorized constructor that produces *embedded* gate matrices. - - Each raw gate matrix is embedded into its merge group's subsystem so that - compression can be performed as a simple segmented matmul scan. - - Returns ``(build_fn, sort_order, group_boundaries)`` where: - - ``build_fn(params)`` → ``(N_raw, d_max, d_max)`` array of embedded matrices - in group-sorted order (ops within each group are consecutive). - - ``sort_order[i]`` = raw op index for position *i* in the sorted array. - - ``group_boundaries[g]`` = start index of group *g* in the sorted array. - The final compressed op for group *g* is the product of sorted ops from - ``group_boundaries[g]`` to ``group_boundaries[g+1]``. +) -> Callable[[Array], Array]: + """Build a JIT-friendly constructor for the compressed unitary stack. + + Returns ``build(params) -> (n_groups, d_max, d_max)``: one matrix per merge + group, equal to ``compress(resolve(params))`` but assembled so the traced + graph scales with the number of distinct gate *kinds* rather than the number + of gates. Each gate is embedded into its merge group's Hilbert space, then + the gates of every group are folded together via :func:`_make_group_fold`. """ n_ops = len(expanded_ops) - # ── Compute group membership and embedding for each raw op ── - # raw_to_group[i] = which compressed group raw op i belongs to - # raw_to_embed_key[i] = (id(gate_fn_or_None), concrete_key, embed_key) for grouping - raw_to_group = np.empty(n_ops, dtype=np.int32) - raw_to_group_sub: list[tuple[int, ...]] = [() for _ in range(n_ops)] - - sorted_raw_indices: list[int] = [] # raw op indices in group-then-topo order - group_boundaries: list[int] = [0] - for group_idx, (_, nodes, subsystem) in enumerate(emit_order): + # Lay raw ops out in group order: group g occupies sorted positions + # [group_start[g], group_start[g + 1]). + sorted_indices: list[int] = [] + group_subsystems: list[tuple[int, ...]] = [] # merge subsystem per sorted position + group_start: list[int] = [0] + for _, nodes, subsystem in emit_order: for nk in nodes: - raw_to_group[nk] = group_idx - raw_to_group_sub[nk] = subsystem - sorted_raw_indices.append(nk) - group_boundaries.append(len(sorted_raw_indices)) - - sort_order = np.array(sorted_raw_indices, dtype=np.int32) - group_bounds = np.array(group_boundaries, dtype=np.int32) - - # ── Group ops by (gate_fn, concrete_layout, embedding_config) for vmap ── - # The embedding_config is (op_subsystem, group_subsystem) which determines - # how the raw matrix is expanded into the group's Hilbert space. - param_groups: dict[tuple, tuple[Callable, tuple, tuple, list[int], list[list[int]], Callable]] = {} - const_entries: list[tuple[int, np.ndarray]] = [] # (sorted_position, embedded_matrix) - - for sorted_pos, raw_idx in enumerate(sorted_raw_indices): - eop = expanded_ops[raw_idx] + sorted_indices.append(nk) + group_subsystems.append(subsystem) + group_start.append(len(sorted_indices)) + + # Plan how each op's embedded matrix is produced: parametric gates are + # collected into vmapped batches; constant gates are embedded eagerly. + batches: dict[tuple, _GateBatch] = {} + const_positions: list[int] = [] + const_mats: list[Array] = [] + for pos, raw_idx in enumerate(sorted_indices): + op = expanded_ops[raw_idx] op_sub = raw_subsystems[raw_idx] - grp_sub = raw_to_group_sub[raw_idx] - - if isinstance(eop, ParametricGate): - concrete_mask = tuple(j for j, pi in enumerate(eop.param_indices) if pi < 0) - concrete_vals = tuple(eop.concrete_values[j] for j in concrete_mask) - # Group by embedding TYPE not physical qubits: all embeddings with - # the same (op_dims, target_dims, positions_within_group) produce - # identical traced graphs, so they can share a single vmap. - op_dims_key = tuple(dims[q] for q in op_sub) - target_dims_key = tuple(dims[q] for q in grp_sub) - positions_key = tuple(grp_sub.index(q) for q in op_sub) - embed_key = (op_dims_key, target_dims_key, positions_key) - key = (id(eop.gate_fn), concrete_mask, concrete_vals, embed_key) - if key not in param_groups: - embed_fn = _make_embed_fn(op_sub, grp_sub, dims, d_max) - param_groups[key] = (eop.gate_fn, eop.param_indices, eop.concrete_values, [], [], embed_fn) - param_groups[key][3].append(sorted_pos) - param_groups[key][4].append([pi for pi in eop.param_indices if pi >= 0]) + grp_sub = group_subsystems[pos] + if isinstance(op, ParametricGate): + # Key by embedding *type* (dims + positions within the group), not + # physical qubits: embeddings that trace to the same graph share a vmap. + embed_key = ( + tuple(dims[q] for q in op_sub), + tuple(dims[q] for q in grp_sub), + tuple(grp_sub.index(q) for q in op_sub), + ) + concrete_args = tuple((j, op.concrete_values[j]) for j, pi in enumerate(op.param_indices) if pi < 0) + key = (id(op.gate_fn), concrete_args, embed_key) + batch = batches.get(key) + if batch is None: + batch = _GateBatch( + gate_fn=op.gate_fn, + n_args=len(op.param_indices), + concrete_args=concrete_args, + embed_fn=_make_embed_fn(op_sub, grp_sub, dims, d_max), + ) + batches[key] = batch + batch.positions.append(pos) + batch.param_indices.append([pi for pi in op.param_indices if pi >= 0]) else: - mat = np.asarray(eop.matrix) - embedded = _embed_matrix_np(mat, op_sub, grp_sub, dims, d_max) - const_entries.append((sorted_pos, embedded)) - - # ── Build vmapped constructors ── - vmapped_specs: list[tuple[np.ndarray, Callable[[Array], Array]]] = [] - - for gate_fn, template_pi, template_cv, positions, pidx_lists, embed_fn in param_groups.values(): - pos_arr = np.array(positions) - - parametric_slots = [j for j, pi in enumerate(template_pi) if pi >= 0] - concrete_slots = [(j, cv) for j, (pi, cv) in enumerate(zip(template_pi, template_cv, strict=False)) if pi < 0] - n_total = len(template_pi) - - def _make_batch( - gf: Callable, ps: list[int], cs: list[tuple[int, float]], nt: int, ef: Callable, pidx: Array - ) -> Callable[[Array], Array]: - def _single(parametric_values: Array) -> Array: - args: list[Any] = [None] * nt - for slot, val in cs: - args[slot] = val - for k, slot in enumerate(ps): - args[slot] = parametric_values[k] - return cast(Array, ef(gf(*args).matrix)) - - batched = jax.vmap(_single) - - def build(params: Array) -> Array: - return batched(params[pidx]) - - return build - - pidx_arr = jnp.array(pidx_lists) - builder = _make_batch(gate_fn, parametric_slots, concrete_slots, n_total, embed_fn, pidx_arr) - vmapped_specs.append((pos_arr, builder)) - - # ── Pre-build constant embedded matrices ── - if const_entries: - const_positions = np.array([p for p, _ in const_entries]) - const_stack = jnp.array(np.stack([m for _, m in const_entries])) - else: - const_positions = None - const_stack = None - - # ── Build gather index for padded parallel composition ── - n_groups = len(group_boundaries) - 1 - group_sizes = np.diff(group_boundaries) - max_group_size = int(group_sizes.max()) if n_groups > 0 else 1 + const_positions.append(pos) + const_mats.append(_embed_constant_matrix(op.matrix, op_sub, grp_sub, dims, d_max)) - # gather_idx[g, k] = sorted position of the k-th op in group g, - # or n_ops (the identity sentinel position) for padding. - gather_idx = np.full((n_groups, max_group_size), n_ops, dtype=np.int32) - for g in range(n_groups): - start, end = group_boundaries[g], group_boundaries[g + 1] - size = end - start - gather_idx[g, :size] = np.arange(start, end) - gather_idx_jax = jnp.array(gather_idx) - eye_mat = jnp.eye(d_max, dtype=complex) + builders = [(np.asarray(b.positions), b.builder()) for b in batches.values()] + const_pos_arr = np.asarray(const_positions) if const_positions else None + const_stack = jnp.stack(const_mats) if const_mats else None - def build_compressed_stack(params: Array) -> Array: - """Build all gate matrices and compose within each merge group. + fold = _make_group_fold(group_start, n_ops, d_max) - Returns ``(n_groups, d_max, d_max)`` — one compressed matrix per group. - """ - # 1. Build all embedded gate matrices in group-sorted order. - raw_mats = jnp.zeros((n_ops, d_max, d_max), dtype=complex) - for pos_arr, builder in vmapped_specs: - raw_mats = raw_mats.at[pos_arr].set(builder(params)) + def build(params: Array) -> Array: + raw = jnp.zeros((n_ops, d_max, d_max), dtype=complex) + for positions, builder in builders: + raw = raw.at[positions].set(builder(params)) if const_stack is not None: - raw_mats = raw_mats.at[const_positions].set(const_stack) - - # 2. Append identity sentinel and gather into (n_groups, max_size, d, d). - raw_mats_plus = jnp.concatenate([raw_mats, eye_mat[None]], axis=0) - padded = raw_mats_plus[gather_idx_jax] # (n_groups, max_size, d, d) - - # 3. Parallel fold: vmap a scan over all groups simultaneously. - def group_product(mats: Array) -> Array: - def body(acc: Array, mat: Array) -> tuple[Array, None]: - return mat @ acc, None - - final, _ = jax.lax.scan(body, eye_mat, mats) - return final + raw = raw.at[const_pos_arr].set(const_stack) + return fold(raw) - return jax.vmap(group_product)(padded) # (n_groups, d, d) - - return build_compressed_stack, sort_order, group_bounds + return build # ══════════════════════════════════════════════════════════ @@ -588,14 +548,13 @@ def __init__( # compilation (small traced graph) AND fast runtime (compressed # op count in the state-evolution scan). emit_order = getattr(self._compress_fn, "emit_order", []) - build_fn, _sort_order, _group_bounds = _build_vectorized_unitary_constructor( + self._vmapped_build_fn = _build_vectorized_unitary_constructor( self._expanded_ops, self._raw_subsystems, emit_order, self.dims, self.d_max, ) - self._vmapped_build_fn = build_fn self._branches = [ _make_unitary_branch(base, base_dims, db) @@ -630,15 +589,7 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] # Vectorized construction: build embedded matrices via vmap, then # compose within each merge group via a parallel fold. op_stack = self._vmapped_build_fn(params) - branches = self._branches - - def body(psi: qx.StateVector, xs: tuple[Array, Array]) -> tuple[qx.StateVector, None]: - op_mat, sidx = xs - psi = jax.lax.switch(sidx, branches, op_mat, psi) - return psi, None - - psi, _ = jax.lax.scan(body, self._psi0, (op_stack, self._idx_arr)) - return psi + return self._evolve(self._psi0, op_stack) def __call__(self, params: Array) -> qx.StateVector: return self.compute(params) @@ -661,9 +612,7 @@ def unitary(self, params: Array) -> qx.Unitary: accumulated = embedded @ accumulated if accumulated is None: - d = 1 - for dim in self.dims: - d *= dim + d = math.prod(self.dims) return qx.Unitary.from_matrix(jnp.eye(d, dtype=complex), (self.dims, self.dims)) return accumulated @@ -703,11 +652,11 @@ def __init__( self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) # See :class:`PureStateVectorSimulator`: for parameter-free programs the - # superoperator stack is constant, so build it eagerly to keep the traced - # graph to just the scan over a concrete array. + # superoperator stack is constant, so it can be built once and reused to keep + # the traced graph to just the scan over a concrete array. It is materialised + # lazily on the first :meth:`compute` call rather than eagerly here, so + # constructing the simulator stays cheap. self._const_op_stack: Array | None = None - if not self._has_params and self.op_index: - self._const_op_stack = self._stack_superops(self.resolve(jnp.zeros(0))).block_until_ready() def _stack_superops(self, resolved: list[ResolvedOp]) -> Array: """Compress, promote each op to a SuperOp, and stack.""" @@ -728,22 +677,17 @@ def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] :param params: Flat parameter vector from :meth:`linearize`. :return: The final density matrix. """ - if self._const_op_stack is not None: + if not self._has_params and self.op_index: + # Parameter-free program: build the constant superop stack once, then reuse. + if self._const_op_stack is None: + self._const_op_stack = self._stack_superops(self.resolve(jnp.zeros(0))) op_stack = self._const_op_stack else: resolved = self.resolve(params) if not resolved: return self._rho0 op_stack = self._stack_superops(resolved) - branches = self._branches - - def body(rho: qx.DensityMatrix, xs: tuple[Array, Array]) -> tuple[qx.DensityMatrix, None]: - op_mat, sidx = xs - rho = jax.lax.switch(sidx, branches, op_mat, rho) - return rho, None - - rho, _ = jax.lax.scan(body, self._rho0, (op_stack, self._idx_arr)) - return rho + return self._evolve(self._rho0, op_stack) def __call__(self, params: Array) -> qx.DensityMatrix: return self.compute(params) @@ -895,7 +839,7 @@ def _op_to_kraus_matrix( """ match op: case qx.Unitary(): - return op.matrix[jnp.newaxis, :, :], 1, False + return qx.to_kraus(op).matrix, 1, False case qx.KrausMap(): return op.matrix, 1, False case qx.QuantumInstrument(): @@ -1042,7 +986,7 @@ def _run_batched_trajectories( n_devices = len(mesh.devices.flat) if mesh is not None else 1 key = jax.random.key(random_seed) - all_psis: list[qx.StateVector] = [] if keep_states else [] + all_psis: list[qx.StateVector] = [] all_outcomes: list[Array] = [] remaining = num_trajectories diff --git a/test/unit/test_noise_model.py b/test/unit/test_noise_model.py index c36e94b47..223075cc9 100644 --- a/test/unit/test_noise_model.py +++ b/test/unit/test_noise_model.py @@ -19,10 +19,12 @@ import pytest import quax as qx +from pyquil.external.rpcq import CompilerISA from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, X from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel, get_instruction_unitary from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program +from pyquil.quilatom import Qubit from pyquil.quilbase import Gate, Measurement, ResetQubit from pyquil.simulation._simulator import DensityMatrixSimulator @@ -137,6 +139,29 @@ def test_from_pauli_noise_rejects_invalid_probabilities(self): with pytest.raises(ValueError, match="at most 1.0"): Channel.from_pauli_noise(inst=RX(0.5, 0), pauli_noise={"X": 0.6, "Z": 0.5}) + def test_from_pauli_noise_two_qubit(self): + """from_pauli_noise builds the correct 16-term Pauli channel for a 2Q gate (regression).""" + pauli_noise = {"IX": 0.01, "XI": 0.005, "ZZ": 0.02} + ch = Channel.from_pauli_noise(inst=CNOT(0, 1), pauli_noise=pauli_noise) + pv = np.asarray(ch.pauli_vector) + assert pv.size == 16 + assert float(jnp.sum(ch.pauli_vector)) == pytest.approx(1.0, abs=1e-3) + terms = [a + b for a in "IXYZ" for b in "IXYZ"] + rates = dict(zip(terms, pv, strict=True)) + for term, rate in pauli_noise.items(): + assert rates[term] == pytest.approx(rate, abs=1e-3) + assert rates["II"] == pytest.approx(1.0 - sum(pauli_noise.values()), abs=1e-3) + + def test_pow_scales_noise(self): + """Channel ** power scales the noise while preserving the gate.""" + ch = Channel.from_depolarizing_constant(inst=RX(np.pi / 2, 0), depolarizing_constant=0.99) + assert (ch**0.0).pauli_infidelity == pytest.approx(0.0, abs=1e-3) + assert (ch**1.0).pauli_infidelity == pytest.approx(ch.pauli_infidelity, abs=1e-3) + assert (ch**2.0).pauli_infidelity > ch.pauli_infidelity + # The ideal gate is preserved. + assert (ch**2.0).qubits == ch.qubits + assert jnp.allclose((ch**2.0).target_unitary.matrix, ch.target_unitary.matrix) + def test_json_roundtrip_preserves_qutrit_dims(self): """Channel JSON includes explicit dims for non-qubit operators.""" qutrit_x = jnp.array( @@ -188,6 +213,35 @@ def test_qubits(self): ch = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.99) assert ch.qubits == [5] + @pytest.mark.parametrize("asymmetry", [0.0, 0.5]) + def test_pow_scales_readout_noise(self, asymmetry): + """MeasurementChannel ** power scales readout noise via the stochastic generator.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + ch = MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.95, asymmetry=asymmetry) + + def bitflip(channel): + cm = np.asarray(channel.confusion_matrix) + return 1.0 - 0.5 * (float(cm[0, 0]) + float(cm[1, 1])) + + assert bitflip(ch**0.0) == pytest.approx(0.0, abs=1e-3) + assert bitflip(ch**1.0) == pytest.approx(bitflip(ch), abs=1e-3) + assert bitflip(ch**2.0) > bitflip(ch) + # The generator construction keeps the result exactly column-stochastic and non-negative. + powered = np.asarray((ch**1.5).confusion_matrix) + assert np.all(powered >= -1e-9) + assert np.allclose(powered.sum(axis=0), 1.0, atol=1e-6) + + def test_pow_rejects_non_embeddable_measurement(self): + """A confusion matrix with no real generator cannot be fractionally powered.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + # Near-complete bit flip: eigenvalue ~ -0.8, so the matrix is not embeddable. + confusion = jnp.array([[0.1, 0.9], [0.9, 0.1]]) + ch = MeasurementChannel.from_confusion_and_transition(meas_inst, confusion, jnp.eye(2)) + with pytest.raises(ValueError, match="not embeddable|not a valid"): + _ = ch**0.5 + def test_json_roundtrip_preserves_qutrit_dims(self): """MeasurementChannel JSON includes explicit dims for non-qubit instruments.""" prog = Program(MEASURE(0, None)) @@ -228,6 +282,28 @@ def test_constructor_rejects_channel_iterable(self): with pytest.raises(TypeError, match="from_channels"): NoiseModel(channels=[ch]) # type: ignore[arg-type] + def test_pickle_roundtrip(self): + """NoiseModel survives pickling (its MappingProxyType channels would otherwise block it). + + This is what lets a model be shipped to multiprocessing workers. + """ + import pickle + + gate = RX(np.pi / 4, 0) + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + nm = NoiseModel.from_channels( + [ + Channel.from_depolarizing_constant(inst=gate, depolarizing_constant=0.98), + MeasurementChannel.from_readout_fidelity(inst=meas_inst, fidelity=0.95), + ] + ) + restored = pickle.loads(pickle.dumps(nm)) + assert set(restored.channels) == set(nm.channels) + assert isinstance(restored.get_channel(gate), Channel) + # Channels survive the round-trip intact (exercises value-based __eq__). + assert restored == nm + def test_constructor_rejects_mismatched_mapping_key(self): """Mapping keys must match the instruction stored on each channel.""" inst = RX(np.pi / 4, 0) @@ -293,15 +369,14 @@ def test_add_combines_disjoint_channels(self): assert combined.get_channel(ch1.inst) == ch1 assert combined.get_channel(ch2.inst) == ch2 - def test_add_composes_overlapping_channels(self): - """NoiseModel addition composes channels with the same instruction.""" + def test_add_rejects_overlapping_channels(self): + """Addition is a disjoint union; a shared instruction is a conflict, not a composition.""" inst = RX(np.pi / 4, 0) ch1 = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.98) ch2 = Channel.from_depolarizing_constant(inst=inst, depolarizing_constant=0.97) - combined = NoiseModel.from_channels([ch1]) + NoiseModel.from_channels([ch2]) - - assert combined.get_channel(inst) == (ch1 @ ch2) + with pytest.raises(ValueError, match="same instruction"): + _ = NoiseModel.from_channels([ch1]) + NoiseModel.from_channels([ch2]) def test_with_channels_returns_extended_model(self): """with_channels returns a new model and rejects duplicate instructions.""" @@ -317,6 +392,12 @@ def test_with_channels_returns_extended_model(self): with pytest.raises(ValueError, match="Duplicate noise channel"): nm.with_channels([ch1]) + def test_noise_model_is_unhashable(self): + """NoiseModel is unhashable (consistent with value-based equality).""" + nm = NoiseModel.from_channels([Channel.from_depolarizing_constant(RX(0.5, 0), 0.98)]) + with pytest.raises(TypeError): + hash(nm) + # ────────────────────────────────────────────────────────── # get_instruction_unitary tests @@ -438,3 +519,119 @@ def test_json_roundtrip_preserves_qutrit_dims(self): assert restored.inst == channel.inst assert restored.process.dims == ((3,), (3,)) assert jnp.allclose(restored.process.matrix, channel.process.matrix) + + +# ────────────────────────────────────────────────────────── +# Channel equality / hashing semantics +# ────────────────────────────────────────────────────────── + + +class TestChannelEqualityAndHashing: + def test_channels_are_unhashable(self): + """Channels hold jax arrays and are intentionally unhashable.""" + ch = Channel.from_depolarizing_constant(RX(0.5, 0), 0.98) + with pytest.raises(TypeError): + hash(ch) + + def test_channel_equality_is_exact(self): + """Channel equality is exact: identical builds are equal, different ones are not.""" + ch1 = Channel.from_depolarizing_constant(RX(0.5, 0), 0.98) + ch2 = Channel.from_depolarizing_constant(RX(0.5, 0), 0.98) + ch3 = Channel.from_depolarizing_constant(RX(0.5, 0), 0.97) + assert ch1 == ch2 + assert ch1 != ch3 + assert ch1 != "not a channel" + + def test_channel_inequality_on_different_instruction(self): + """Channels on different instructions are never equal.""" + ch_a = Channel.from_depolarizing_constant(RX(0.5, 0), 0.98) + ch_b = Channel.from_depolarizing_constant(RX(0.5, 1), 0.98) + assert ch_a != ch_b + + +# ────────────────────────────────────────────────────────── +# Channel construction / analysis coverage +# ────────────────────────────────────────────────────────── + + +class TestChannelAnalysis: + def test_from_mixture(self): + """from_mixture builds a noisy channel from unitary errors with probabilities.""" + z = qx.Unitary.from_matrix(jnp.array([[1, 0], [0, -1]], dtype=complex), ((2,), (2,))) + ch = Channel.from_mixture(X(0), constituents=[z], probabilities=[0.1]) + assert isinstance(ch.process, qx.SuperOp) + assert ch.pauli_infidelity > 0.0 + + def test_coherent_and_stochastic_decomposition(self): + """to_coherent_channel / to_stochastic_channel split the noise into components.""" + ch = Channel.from_random_coherent_error(X(0), process_fidelity=0.95, rng=np.random.default_rng(0)) + coherent = ch.to_coherent_channel() + stochastic = ch.to_stochastic_channel() + assert isinstance(coherent, Channel) + assert isinstance(stochastic, Channel) + # Coherent + stochastic infidelity decomposition is non-negative and finite. + assert np.isfinite(ch.coherent_infidelity) + assert np.isfinite(ch.stochastic_infidelity) + + def test_pauli_twirl_is_pauli(self): + """Twirling a coherent-error channel yields a stochastic Pauli channel.""" + ch = Channel.from_random_coherent_error(X(0), process_fidelity=0.95, rng=np.random.default_rng(1)) + twirled = ch.pauli_twirl() + assert twirled.is_pauli() + + +# ────────────────────────────────────────────────────────── +# NoiseModel.from_isa +# ────────────────────────────────────────────────────────── + + +class TestFromIsa: + @staticmethod + def _isa() -> CompilerISA: + return CompilerISA.parse_obj( + { + "1Q": { + "0": { + "id": 0, + "gates": [ + { + "operator_type": "gate", + "operator": "RX", + "parameters": [1.5707963267948966], + "arguments": ["_"], + "fidelity": 0.99, + }, + # A fidelity-less measurement entry must not mask the real one below. + {"operator_type": "measure", "qubit": "0", "fidelity": None}, + {"operator_type": "measure", "qubit": "0", "fidelity": 0.95}, + ], + }, + "1": {"id": 1, "gates": []}, + }, + "2Q": { + "0-1": { + "ids": [0, 1], + "gates": [ + { + "operator_type": "gate", + "operator": "CZ", + "parameters": [], + "arguments": ["_", "_"], + "fidelity": 0.9, + } + ], + } + }, + } + ) + + def test_builds_gate_and_edge_channels(self): + nm = NoiseModel.from_isa(self._isa()) + assert isinstance(nm.get_channel(Gate("RX", [1.5707963267948966], [0])), Channel) + assert isinstance(nm.get_channel(Gate("CZ", [], [0, 1])), Channel) + + def test_measurement_dedup_prefers_real_fidelity(self): + """A None-fidelity measure entry must not block a later usable one (dedup ordering).""" + nm = NoiseModel.from_isa(self._isa()) + channel = nm.get_channel(Measurement(qubit=Qubit(0), classical_reg=None)) + assert isinstance(channel, MeasurementChannel) diff --git a/test/unit/test_resolver.py b/test/unit/test_resolver.py index 70656ad55..cfc436bc4 100644 --- a/test/unit/test_resolver.py +++ b/test/unit/test_resolver.py @@ -4,7 +4,7 @@ import numpy as np import quax as qx -from pyquil.gates import CNOT, H, MEASURE, RESET, RX, RZ, X +from pyquil.gates import CNOT, MEASURE, RESET, RX, RZ, H, X from pyquil.noise._channels import Channel, CycleChannel from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program @@ -19,7 +19,7 @@ build_dag, expand_program, remap_qubits, - resolver_from_program, + resolve_program, ) _EMPTY_PARAMS = jnp.array([], dtype=float) @@ -133,55 +133,55 @@ def test_build_dag_multi_qubit(self): # ────────────────────────────────────────────────────────── -# resolver_from_program (integration) +# resolve_program (integration) # ────────────────────────────────────────────────────────── -class TestResolverFromProgram: +class TestResolveProgram: def test_basic_roundtrip(self): p = Program(H(0), CNOT(0, 1), X(1)) - resolver, dag = resolver_from_program(p) - ops = resolver(_EMPTY_PARAMS) + res = resolve_program(p) + ops = res.resolve(_EMPTY_PARAMS) assert len(ops) == 3 assert all(isinstance(op, qx.Unitary) for op, _ in ops) - assert resolver.dims == (2, 2) + assert res.dims == (2, 2) def test_with_noise(self): p = Program(X(0), H(1)) ch = Channel.from_gate_fidelity(inst=X(0), fidelity=0.99) nm = NoiseModel.from_channels([ch]) - resolver, dag = resolver_from_program(p, nm) - ops = resolver(_EMPTY_PARAMS) + res = resolve_program(p, nm) + ops = res.resolve(_EMPTY_PARAMS) assert len(ops) == 2 assert isinstance(ops[0][0], qx.SuperOp) assert isinstance(ops[1][0], qx.Unitary) def test_parameterized(self): p = Program(Declare("theta", "REAL", 1), RZ(MemoryReference("theta", 0), 0)) - resolver, _ = resolver_from_program(p) + res = resolve_program(p) params = jnp.array([np.pi / 4]) - ops = resolver(params) + ops = res.resolve(params) assert len(ops) == 1 assert isinstance(ops[0][0], qx.Unitary) def test_dag_structure(self): p = Program(H(0), X(0), CNOT(0, 1)) - _, dag = resolver_from_program(p) + dag = build_dag(resolve_program(p).subsystems) assert dag.has_edge(0, 1) assert dag.has_edge(1, 2) def test_measurement_and_reset(self): p = Program(Declare("ro", "BIT", 1), H(0), MEASURE(0, MemoryReference("ro", 0))) - resolver, _ = resolver_from_program(p) - ops = resolver(_EMPTY_PARAMS) + res = resolve_program(p) + ops = res.resolve(_EMPTY_PARAMS) assert len(ops) == 2 assert isinstance(ops[0][0], qx.Unitary) assert isinstance(ops[1][0], qx.QuantumInstrument) def test_qutrit_measurement_dimensions(self): p = Program(Gate("TX", [], [0]), Measurement(Qubit(0), None)) - resolver, _ = resolver_from_program(p) - ops = resolver(_EMPTY_PARAMS) - assert resolver.dims == (3,) + res = resolve_program(p) + ops = res.resolve(_EMPTY_PARAMS) + assert res.dims == (3,) assert isinstance(ops[1][0], qx.QuantumInstrument) assert ops[1][0].dims == ((3,), (3,)) From 72a85dbaa533f0181823940307bd6b09be7dd771 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Wed, 1 Jul 2026 09:54:30 +0000 Subject: [PATCH 28/37] Add dynamic trajectory simulator --- docs/source/index.rst | 1 + pyquil/simulation/_simulator.py | 168 +++++++++++++++++++++++ test/unit/test_dynamic_trajectory.py | 130 ++++++++++++++++++ test/unit/test_trajectory_compression.py | 2 +- 4 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 test/unit/test_dynamic_trajectory.py diff --git a/docs/source/index.rst b/docs/source/index.rst index c41a46e62..ee9758bb6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -33,6 +33,7 @@ If you’re new to pyQuil, head to the `getting started `_ guid compiler noise simulation_architecture + dynamic_simulator_benchmark advanced_usage troubleshooting introducing_v4 diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index acd4f3422..c165a2f9d 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -908,6 +908,14 @@ def _apply_trajectory_operations( measure_positions: list[int] = [] branch_index: list[int] = [] for i, (op, subsystem) in enumerate(operations): + # Promote each operator to the state's dimension on its subsystem (identity on the + # higher levels). Without this, an op authored at a lower dimension than the register + # (e.g. a qubit-dimension channel on a register promoted to qutrits by a leakage model) + # would be zero-padded to ``d_max`` instead — silently wrong on the high levels, and a + # reshape error when ``d_max`` is below the branch's dimension. + target_dims = tuple(psi.dims[q] for q in subsystem) + if op.dims[0] != target_dims: + op = qx.promote(op, target_dims) mat, divisor, is_measure = _op_to_kraus_matrix(op) kraus_mats.append(mat) divisors.append(divisor) @@ -1041,3 +1049,163 @@ def _run_batched_trajectories( remaining -= this_batch return (all_psis if keep_states else None), all_outcomes + + +# ══════════════════════════════════════════════════════════ +# Dynamic-shape trajectory simulator +# ══════════════════════════════════════════════════════════ + + +def _dyn_apply( + op: qx.Unitary | qx.KrausMap | qx.QuantumInstrument, + psi: qx.StateVector, + subsystem: tuple[int, ...], + key: Array, + squeeze_tol: float, +) -> tuple[qx.StateVector, Array | None]: + """Apply one trajectory operator with dynamic per-subsystem dimensions. + + The reconciliation is *grow state → apply → squeeze state*: + + 1. The state is grown via :func:`quax.promote` only where the operator exceeds it. + Operators are applied at their authored dimension — they are never squeezed, since + squeezing an operator is ill-defined: it acts non-trivially on levels the state may + never populate (see :mod:`quax._squeeze`). + 2. The matching ``quax`` kernel is applied; it promotes the operator up to the state's + dimensions, never shrinking it. + 3. The *state* is squeezed (a well-defined operation) to reclaim any leakage level the + operator left empty. So an ideal gate authored on a qutrit register transiently + grows the state and then squeezes straight back, while a genuine leakage op leaves + population behind that survives the squeeze. The squeeze is skipped while every qudit + is already at the qubit floor, so a purely no-leakage trajectory pays nothing for it. + + :param squeeze_tol: Tolerance for squeezing emptied leakage levels out of the state. + :return: ``(state, outcome)`` where ``outcome`` is the sampled measurement + result for an instrument, else ``None``. + """ + current = tuple(psi.dims[q] for q in subsystem) + target = tuple(max(c, e) for c, e in zip(current, op.dims[0], strict=True)) + if target != current: + grown = list(psi.dims) + for q, t in zip(subsystem, target, strict=True): + grown[q] = t + psi = qx.promote(psi, tuple(grown)) + + if isinstance(op, qx.Unitary): + psi, outcome = qx.targeted_apply_unitary(op, psi, subsystem), None + elif isinstance(op, qx.KrausMap): + psi, _ = _sample_kraus_map_trajectory(op, psi, key, subsystem) + outcome = None + elif isinstance(op, qx.QuantumInstrument): + psi, outcome = qx.targeted_apply_instrument_to_state_vector(op, psi, key, subsystem) + else: + raise TypeError(f"DynamicTrajectorySimulator cannot apply operator of type {type(op).__name__}.") + + # Reclaim any leakage level the operator left empty by squeezing the *state*. No qudit + # can shrink below the qubit floor, so skip the work entirely while none has leaked. + if any(d > 2 for d in psi.dims): + psi = cast(qx.StateVector, qx.squeeze(psi, squeeze_tol)) + return psi, outcome + + +class DynamicTrajectorySimulator(ProgramSimulator): + """Single-trajectory simulator with dynamically-sized qudit dimensions. + + Targets the **largest** leakage-aware registers. Where the other simulators + fix a global Hilbert-space shape, this one keeps a per-subsystem dimension + vector that drifts at runtime: a qudit is grown to dimension 3 only when an + operator can populate its leakage level, and squeezed back to 2 once that + level empties (via :func:`quax.squeeze`). In the realistic low-leakage + regime only a handful of qudits occupy ``|2>`` at once, so the stored state + stays far below the full ``3**n``. + + The simulation is **eager** — it applies one operator at a time and cannot be + ``jax.jit``/``jax.grad``-compiled (the shapes are data-dependent) — and runs a + single trajectory per :meth:`compute` call (scalar PRNG key, no ensemble). + Squeeze is tolerance-based, so results carry a bounded truncation error set by + ``squeeze_tol``. + + ``max_subsystem_size`` defaults to 1: chains of single-qudit gates on one line + are still fused (no extra qudits pinned), but multi-qudit gates are left + un-merged so that a leakage channel never pins its neighbours to dimension 3. + + Example:: + + sim = DynamicTrajectorySimulator(program, noise_model=leakage_model) + params = sim.linearize(memory_map) + psi, outcomes = sim.compute(params, jax.random.key(0)) + shots = sim.sample(params, num_trajectories=1000) + """ + + __slots__ = ("_kraus_truncation_threshold", "_squeeze_tol") + + def __init__( + self, + program: Program, + qubits: list[int] | None = None, + *, + noise_model: NoiseModelLike | None = None, + max_subsystem_size: int = 1, + kraus_truncation_threshold: float = 1e-6, + squeeze_tol: float = 1e-9, + ) -> None: + super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) + self._kraus_truncation_threshold = kraus_truncation_threshold + self._squeeze_tol = squeeze_tol + + def _validate(self, program: Program) -> None: + """Measurements, resets, and noise are all supported (like TrajectorySimulator).""" + + def compute( # type: ignore[override] + self, + params: Array, + key: Array, + ) -> tuple[qx.StateVector, Array]: + """Run a single dynamic-shape trajectory. + + :param params: Flat parameter vector from :meth:`linearize`. + :param key: Scalar JAX PRNG key for this trajectory. + :return: ``(state_vector, measurement_outcomes)``. The state's per-qudit + dimensions reflect whatever leakage survived squeezing; outcomes has + shape ``(n_measurements,)`` in program order. + """ + operations = adapt_for_trajectory(self.compress(self.resolve(params)), self._kraus_truncation_threshold) + + psi = qx.zero_state_vector(dims=(2,) * self.n_qubits) + outcomes: list[Array] = [] + # The state's shape changes as qudits grow and squeeze, so quax's jitted apply kernels + # compile once per distinct (shape, subsystem) they see. In the low-leakage regime that + # set is small and bounded — the deterministic no-leak shape sequence plus a few + # one-qubit-leaked shapes — so the compilation is a fixed upfront cost that amortizes + # over the many trajectories a sampling run takes. We therefore keep jit enabled. + for idx, (op, subsystem) in enumerate(operations): + psi, outcome = _dyn_apply(op, psi, tuple(subsystem), jax.random.fold_in(key, idx), self._squeeze_tol) + if outcome is not None: + outcomes.append(outcome) + + if outcomes: + return psi, jnp.stack(outcomes, axis=-1).astype(jnp.int32) + return psi, jnp.empty((0,), dtype=jnp.int32) + + def __call__(self, params: Array, key: Array) -> tuple[qx.StateVector, Array]: + return self.compute(params, key) + + def sample( + self, + params: Array, + num_trajectories: int = 1000, + random_seed: int = 0, + ) -> Array: + """Run trajectories sequentially, returning only measurement outcomes. + + Dynamic per-trajectory shapes preclude ``vmap`` batching, so trajectories + run one at a time. + + :param params: Flat parameter vector from :meth:`linearize`. + :param num_trajectories: Number of trajectories to simulate. + :param random_seed: Seed for the JAX PRNG. + :return: Measurement outcomes with shape ``(num_trajectories, n_measurements)``. + """ + key = jax.random.key(random_seed) + outcomes = [self.compute(params, jax.random.fold_in(key, t))[1] for t in range(num_trajectories)] + return jnp.stack(outcomes, axis=0) diff --git a/test/unit/test_dynamic_trajectory.py b/test/unit/test_dynamic_trajectory.py new file mode 100644 index 000000000..9e0ce3202 --- /dev/null +++ b/test/unit/test_dynamic_trajectory.py @@ -0,0 +1,130 @@ +# Copyright 2026 Rigetti Computing +# +# 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. + +"""Unit tests for the dynamic-shape trajectory simulator.""" + +import jax +import jax.numpy as jnp +import numpy as np +import quax as qx + +from pyquil.gates import CNOT, MEASURE, RX, H, X +from pyquil.noise._channels import Channel +from pyquil.noise._noise_model import NoiseModel +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference +from pyquil.quilbase import Declare +from pyquil.simulation._simulator import ( + DensityMatrixSimulator, + DynamicTrajectorySimulator, + PureStateVectorSimulator, + _dyn_apply, +) + +_EMPTY = jnp.array([], dtype=float) + + +def _overlap(a: qx.StateVector, b: qx.StateVector) -> float: + return float(jnp.abs(jnp.vdot(a.matrix, b.matrix)) ** 2) + + +# -------------------------------------------------------------------------- +# Deterministic correctness against the exact simulators +# -------------------------------------------------------------------------- + + +def test_matches_pure_state_vector_and_stays_qubit(): + """A noiseless gate-only program reproduces the exact state and never grows.""" + program = Program(X(0), CNOT(0, 1), RX(0.7, 2)) + reference = PureStateVectorSimulator(program).compute(_EMPTY) + + sim = DynamicTrajectorySimulator(program) + psi, outcomes = sim.compute(_EMPTY, jax.random.key(0)) + + assert psi.dims == (2, 2, 2) # no leakage -> no growth + assert outcomes.shape == (0,) + assert _overlap(psi, reference) > 1 - 1e-6 + + +def test_bell_measurement_is_correlated(): + """Bell-state measurements are perfectly correlated and roughly balanced.""" + program = Program( + Declare("ro", "BIT", 2), + H(0), + CNOT(0, 1), + MEASURE(0, MemoryReference("ro", 0)), + MEASURE(1, MemoryReference("ro", 1)), + ) + + shots = DynamicTrajectorySimulator(program).sample(_EMPTY, num_trajectories=200, random_seed=1) + assert shots.shape == (200, 2) + # the two qubits always agree + assert np.all(np.asarray(shots)[:, 0] == np.asarray(shots)[:, 1]) + # and both outcomes occur + assert set(np.asarray(shots)[:, 0].tolist()) == {0, 1} + + +# -------------------------------------------------------------------------- +# Dynamic growth / squeeze of the live state +# -------------------------------------------------------------------------- + + +def test_dyn_apply_grows_then_squeezes(): + """A 1<->2 raising gate grows the state to dim 3; returning empties and squeezes it.""" + psi = qx.StateVector.from_matrix(jnp.array([0.0, 1.0], dtype=complex), (2,)) # |1> + swap_12 = qx.Unitary.from_matrix(jnp.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]], dtype=complex), ((3,), (3,))) + + grown, outcome = _dyn_apply(swap_12, psi, (0,), jax.random.key(0), 1e-9) + assert outcome is None + assert grown.dims == (3,) # state was grown to host |2> + assert float(jnp.abs(grown.matrix[2]) ** 2) > 0.99 + assert qx.squeeze(grown, 1e-9).dims == (3,) # population in |2> is retained + + # Apply the raising gate again: population returns to |1>, so squeeze shrinks back. + returned, _ = _dyn_apply(swap_12, grown, (0,), jax.random.key(1), 1e-9) + squeezed = qx.squeeze(returned, 1e-9) + assert squeezed.dims == (2,) + assert float(jnp.abs(squeezed.matrix[1]) ** 2) > 0.99 + + +# -------------------------------------------------------------------------- +# Noise: trajectory average reproduces the density matrix +# -------------------------------------------------------------------------- + + +def test_depolarizing_trajectory_average_matches_density_matrix(): + """Averaging |psi> Date: Wed, 1 Jul 2026 10:06:52 +0000 Subject: [PATCH 29/37] Require rigetti-quax >=0.6.6 for states-only squeeze quax 0.6.6 narrows squeeze to states only (raising TypeError on operators). The dynamic trajectory simulator now squeezes the state after applying each operator, so it needs a quax that actually exports squeeze. The lock still resolved rigetti-quax to 0.6.5 (no squeeze), causing test_dyn_apply_grows_then_squeezes to fail with AttributeError: module 'quax' has no attribute 'squeeze'. Co-Authored-By: Claude Opus 4.8 (1M context) --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index ea1d28672..4c6135858 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3275,14 +3275,14 @@ httpx = ">=0.21.0" [[package]] name = "rigetti-quax" -version = "0.6.5" +version = "0.6.6" description = "A high-performance library for quantum information science built on top of JAX" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ - {file = "rigetti_quax-0.6.5-py3-none-any.whl", hash = "sha256:a6fa3e046e715afed42dd13f1c252214e56708a7c5c6c1888f2d5f491dd9b8be"}, - {file = "rigetti_quax-0.6.5.tar.gz", hash = "sha256:63c2120eb295ab407f7e2d9d79ea3ce271ba3780a26bce3bb2f7cd71db4cdcc3"}, + {file = "rigetti_quax-0.6.6-py3-none-any.whl", hash = "sha256:9ea85ba2f5703c39010332ceec70fde4421ff601eb371b4403921dda3460e6fb"}, + {file = "rigetti_quax-0.6.6.tar.gz", hash = "sha256:bba5a9a2e7f567e1abfcd4507413a98155410cf7792eae98f7a5c88ef3b19b21"}, ] [package.dependencies] @@ -4120,4 +4120,4 @@ latex = ["ipython"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.13" -content-hash = "bdb98b172f015c293126930752e9c7648e92da5034633cee480c32940a0aa14d" +content-hash = "2b96409c96f43f18016b7fe078e8dff2b5f2851903018e4f359f01c1f158d582" diff --git a/pyproject.toml b/pyproject.toml index 111d24288..eb38eb7fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ matplotlib = {version = "^3.9.0", optional = true} matplotlib-inline = {version = "^0.1.7", optional = true} seaborn = {version = "^0.13.2", optional = true} toml = {version = "^0.10.2", optional = true} -rigetti-quax = ">=0.6.5" +rigetti-quax = ">=0.6.6" [tool.poetry.extras] latex = ["ipython"] From 0ab60879a1f4d285fda5ac9db1c531e3e400864d Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Wed, 1 Jul 2026 10:15:18 +0000 Subject: [PATCH 30/37] Bump dev version to 4.19.0-rc.1 Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb38eb7fa..26f99c6e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyquil" -version = "4.19.0-rc.0" +version = "4.19.0-rc.1" description = "A Python library for creating Quantum Instruction Language (Quil) programs." authors = ["Rigetti Computing "] readme = "README.md" From 2d11e73bb32dad7f9232ab34e7fcc5b4153e8c64 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Wed, 1 Jul 2026 18:43:29 +0000 Subject: [PATCH 31/37] Fix emit order --- pyquil/simulation/_resolver.py | 48 ++++++++++++-------- test/unit/test_trajectory_compression.py | 56 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index 59f72377f..cad97858c 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -787,16 +787,9 @@ def _contract_quotient(keep: int, drop: int) -> None: heapq.heappush(heap, (new_union_size, u_node, neighbour)) # --- Build merge plan --- - # Use a *lexicographical* topological sort keyed by node index (= program - # order). Any topological order is physically valid, but breaking ties by - # program index guarantees that barrier nodes — measurements in particular, - # which are never merged and therefore each form a singleton group — are - # emitted in program order. Because a measurement's predecessors all have - # smaller indices, this sort can never emit a later measurement before an - # earlier one, so the order in which ``QuantumInstrument`` ops appear in the - # compressed list (and hence the order of measurement-outcome columns in - # :func:`_apply_trajectory_operations`) matches the order of ``MEASURE`` - # instructions in the program, independent of how gates are merged. + # Order the *members within each group* by a lexicographical topological + # sort of the original DAG (= program order), so ``_merge_ops`` composes + # them in the order they appear in the program. topo_order = list(nx.lexicographical_topological_sort(dag)) root_to_nodes: dict[int, list[int]] = {} @@ -808,15 +801,34 @@ def _contract_quotient(keep: int, drop: int) -> None: for root, qubits in group_qubits.items(): root_to_subsystem[root] = tuple(sorted(qubits)) - emit_order: list[tuple[int, list[int], tuple[int, ...]]] = [] - emitted_roots: set[int] = set() - for nk in topo_order: + # Emit the *groups* in a topological order of the **quotient** graph rather + # than the original DAG. A merged group can legitimately contain an op that + # precedes a barrier (e.g. a measurement) in program order *together* with + # an op that depends on that barrier — the merge is valid because the + # pre-barrier op commutes with the barrier, so it may be applied after it. + # But emitting the group at its earliest member's position (as an earlier + # version did, by walking the original DAG) would place the *whole* group — + # including the post-barrier op — before the barrier, silently applying + # post-measurement gates before the measurement and corrupting its outcome. + # A quotient topological sort respects every inter-group dependency, so a + # group is emitted only after all groups it depends on. The lexicographic + # key — each group's minimum original node index — keeps barrier singletons + # (measurements are never merged) emitted in program order, so the order of + # ``QuantumInstrument`` ops in the compressed list (and hence the + # measurement-outcome columns in :func:`_apply_trajectory_operations`) + # matches the order of ``MEASURE`` instructions in the program: any ancestor + # group of a measurement has a member preceding it in program order and thus + # a strictly smaller minimum index, so a later measurement can never be + # emitted before an earlier one. + group_min_index: dict[int, int] = {} + for nk in dag.nodes: root = uf.find(nk) - if root not in emitted_roots: - emitted_roots.add(root) - nodes = root_to_nodes[root] - subsystem = root_to_subsystem[root] - emit_order.append((root, nodes, subsystem)) + if root not in group_min_index or nk < group_min_index[root]: + group_min_index[root] = nk + + emit_order: list[tuple[int, list[int], tuple[int, ...]]] = [] + for root in nx.lexicographical_topological_sort(quotient, key=lambda r: group_min_index[r]): + emit_order.append((root, root_to_nodes[root], root_to_subsystem[root])) # --- Log the compression statistics --- n_groups = len(emit_order) diff --git a/test/unit/test_trajectory_compression.py b/test/unit/test_trajectory_compression.py index 665a6c160..073008054 100644 --- a/test/unit/test_trajectory_compression.py +++ b/test/unit/test_trajectory_compression.py @@ -824,3 +824,59 @@ def _joint(max_subsystem_size): assert _total_variation(_joint(0), _joint(2)) < 0.02 +def test_compression_does_not_move_pre_measurement_gate_across_a_measurement(): + """A pre-measurement gate must not be pulled *after* a measurement it precedes. + + This is the subtler sibling of the convexity bug above. A gate that sits + *before* a mid-circuit ``MEASURE`` but acts on a *disjoint* qubit can be + validly fused with a gate that sits *after* the measurement (they share a + qubit and the pre-measurement gate commutes with the barrier). The merge + itself is legal, but the merged group must be *emitted after* the barrier — + otherwise the post-measurement gate is applied before the measurement, + corrupting its outcome. + + An earlier emit order walked the *original* DAG and emitted each group at + its earliest member, which placed such a group before the barrier. The + correct emit order is a topological sort of the *quotient* (contracted) + graph. Here ``X`` on the data qubit precedes the ancilla ``MEASURE`` and is + fused with the ``CNOT`` that follows it; the circuit is fully deterministic, + so every measurement column must equal the uncompressed result at every + ``max_subsystem_size``. + """ + ancilla, data = QUBITS_2 # [5, 2]; measure the ancilla mid-circuit + qubits = QUBITS_2 + + program = Program() + program += X(data) # data -> |1> + program += Measurement(qubit=Qubit(ancilla), classical_reg=None) # ancilla |0> -> 0 + program += CNOT(data, ancilla) # control |1> flips ancilla -> |1> + program += Measurement(qubit=Qubit(ancilla), classical_reg=None) # -> 1 + program += Measurement(qubit=Qubit(data), classical_reg=None) # -> 1 + + _assert_real_compression(program, qubits) + + # X(data) and CNOT(data, ancilla) share the data qubit and fuse into one + # group; that group must still be emitted *after* the first ancilla + # measurement, so a gate op appears after the first QuantumInstrument. + sim = TrajectorySimulator(program, qubits=qubits, max_subsystem_size=MAX_SUBSYSTEM_SIZE) + op_types = [type(op).__name__ for op, _ in sim.compress(sim.resolve(sim.linearize({})))] + first_measure = op_types.index("QuantumInstrument") + assert any(name != "QuantumInstrument" for name in op_types[first_measure + 1 :]), ( + f"a gate group must follow the first mid-circuit measurement, got {op_types}" + ) + + # Fully deterministic: columns are [ancilla(round 1), ancilla(round 2), data]. + expected = np.array([0, 1, 1]) + dims = (2, 2) + reference = None + for size in (0, 1, 2): + _, outcomes = _trajectory_density(program, qubits, dims, 4, seed=0, max_subsystem_size=size) + assert np.all(outcomes == expected), ( + f"size {size}: deterministic outcome {outcomes[0].tolist()} != {expected.tolist()}" + ) + if reference is None: + reference = outcomes + else: + assert np.array_equal(outcomes, reference) + + From 570da778f617056e4c12845601f6b279725b92c7 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Wed, 1 Jul 2026 20:06:26 +0100 Subject: [PATCH 32/37] Bump version from 4.19.0-rc.1 to 4.19.0-rc.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26f99c6e1..a77714b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyquil" -version = "4.19.0-rc.1" +version = "4.19.0-rc.2" description = "A Python library for creating Quantum Instruction Language (Quil) programs." authors = ["Rigetti Computing "] readme = "README.md" From 4736511a551295ed13eb9d53e87687cb026e2dd5 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 2 Jul 2026 12:03:43 +0000 Subject: [PATCH 33/37] Fixes to noise model --- pyquil/noise/_channels.py | 74 +++-- pyquil/simulation/_simulator.py | 508 ++++++++++++++------------------ test/unit/test_noise_model.py | 78 ++++- 3 files changed, 347 insertions(+), 313 deletions(-) diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index e9405b213..d62b0f53a 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -1023,21 +1023,25 @@ def from_binary_discriminator( ) -> MeasurementChannel: """Create a MeasurementChannel for a binary discriminator. - Models a measurement that confuses each state at or above ``threshold`` with - the state one level below it. This is useful for measurements calibrated as - binary discriminators between groups of energy levels. - - For example, ``threshold=2, dim=3`` always confuses state 2 for state 1 - (discriminates ``{0, 1}`` vs ``{2}``). ``threshold=1, dim=3`` confuses - state 1 for state 0 and state 2 for state 1 (discriminates ``{0}`` vs ``{1, 2}``). + Models a measurement that reports a single classical *bit* for a + ``dim``-level system by thresholding: levels ``[0, threshold)`` yield + outcome ``0`` and levels ``[threshold, dim)`` yield outcome ``1``. The + resulting instrument therefore always has exactly two outcomes, so leaked + levels are lumped in with whichever side of the threshold they fall on + (the usual case being a "dark" ground state vs. everything "bright"). + + For example, ``threshold=1, dim=2`` is an ordinary qubit readout + (``{0}`` -> 0, ``{1}`` -> 1); ``threshold=1, dim=3`` discriminates + ``{0}`` vs ``{1, 2}`` (ground vs. excited-or-leaked); ``threshold=2, + dim=3`` discriminates ``{0, 1}`` vs ``{2}`` (i.e. flags leakage only). An optional ``fidelity`` parameter degrades the ideal discriminator with uniform classification noise. :param inst: The measurement instruction. :param dim: The dimension of the measured system. - :param threshold: States at or above this level are confused with the level below. - Must satisfy ``1 <= threshold < dim``. + :param threshold: The split point: levels below it report 0, levels at or + above it report 1. Must satisfy ``1 <= threshold < dim``. :param fidelity: Additional classification fidelity applied on top of the discrimination (1.0 = perfect discriminator). :return: A MeasurementChannel instance. @@ -1045,19 +1049,17 @@ def from_binary_discriminator( if not (1 <= threshold < dim): raise ValueError(f"threshold must satisfy 1 <= threshold < dim, got threshold={threshold}, dim={dim}") - # Build the ideal binary discriminator confusion matrix: - # states below threshold are classified correctly, - # states at or above threshold are classified as the state one below. - confusion = jnp.zeros((dim, dim)) + # Ideal two-outcome confusion matrix of shape (num_outcomes=2, dim): + # column j (prepared level j) puts all its weight on outcome 0 if + # j < threshold, else on outcome 1. Two rows so the instrument has + # exactly two outcomes (never a phantom, zero-probability outcome). + confusion = jnp.zeros((2, dim)) for j in range(dim): - if j < threshold: - confusion = confusion.at[j, j].set(1.0) - else: - confusion = confusion.at[j - 1, j].set(1.0) + confusion = confusion.at[int(j >= threshold), j].set(1.0) - # Optionally degrade with uniform noise + # Optionally degrade with uniform noise across the two outcomes. if fidelity < 1.0: - confusion = fidelity * confusion + (1 - fidelity) * jnp.ones((dim, dim)) / dim + confusion = fidelity * confusion + (1 - fidelity) * jnp.ones((2, dim)) / 2 transition = jnp.eye(dim) instrument = qx.instrument_from_confusion_and_transition( @@ -1468,6 +1470,40 @@ class CycleChannel: channels: tuple[Channel | MeasurementChannel, ...] """Constituent channels (one per operation in the cycle) on disjoint qubits.""" + def __post_init__(self) -> None: + """Validate that every instruction in the cycle body has a corresponding channel. + + Downstream consumers (the resolver, the stim converter) use only ``channels`` and + ignore ``defcircuit``; a missing channel would silently drop that operation's noise. + Operations are matched by identity (name, params, concrete qubits), independent of + the DefCircuit's formal-argument naming. + """ + qarg_to_qubit = dict(zip(self.defcircuit.qubit_variables, self.inst.get_qubit_indices(), strict=False)) + + def _resolve(qubit: object) -> int: + if qubit in qarg_to_qubit: + return qarg_to_qubit[qubit] # type: ignore[index] + return qubit.index if hasattr(qubit, "index") else int(qubit) # type: ignore[union-attr,arg-type] + + def _body_key(inst: Gate | Measurement) -> tuple[str, tuple, tuple[int, ...]]: + if isinstance(inst, Measurement): + return ("MEASURE", (), (_resolve(inst.qubit),)) + return (inst.name, tuple(inst.params), tuple(_resolve(q) for q in inst.qubits)) + + def _channel_key(channel: Channel | MeasurementChannel) -> tuple[str, tuple, tuple[int, ...]]: + if isinstance(channel, MeasurementChannel): + return ("MEASURE", (), tuple(channel.qubits)) + return (channel.inst.name, tuple(channel.inst.params), tuple(channel.inst.get_qubit_indices())) + + expected = sorted(repr(_body_key(inst)) for inst in self.defcircuit.instructions) + provided = sorted(repr(_channel_key(ch)) for ch in self.channels) + if expected != provided: + raise ValueError( + "CycleChannel is incomplete: every instruction in the cycle's DefCircuit " + "body must have a corresponding channel. " + f"DefCircuit body: {expected}; channels: {provided}." + ) + # ────────────────────────────────────────────── # Derived properties # ────────────────────────────────────────────── diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index c165a2f9d..3c4093b62 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -75,48 +75,6 @@ def _pad_matrix(mat: Array, *target: int) -> Array: return jnp.pad(mat, pad) -def _make_unitary_branch( - base: tuple[int, ...], - base_dims: tuple[int, ...], - db: int, -) -> Callable[[Array, qx.StateVector], qx.StateVector]: - """Build a ``jax.lax.switch`` branch that applies a unitary on *base*.""" - - def branch(op_mat: Array, psi: qx.StateVector) -> qx.StateVector: - unitary = qx.Unitary.from_matrix(op_mat[:db, :db], (base_dims, base_dims)) - return qx.targeted_apply_unitary(unitary, psi, base) - - return branch - - -def _make_superop_branch( - base: tuple[int, ...], - base_dims: tuple[int, ...], - db2: int, -) -> Callable[[Array, qx.DensityMatrix], qx.DensityMatrix]: - """Build a ``jax.lax.switch`` branch that applies a superoperator on *base*.""" - - def branch(op_mat: Array, rho: qx.DensityMatrix) -> qx.DensityMatrix: - superop = qx.SuperOp.from_matrix(op_mat[:db2, :db2], (base_dims, base_dims)) - return qx.targeted_apply_superop(superop, rho, base) - - return branch - - -def _make_kraus_trajectory_branch( - base: tuple[int, ...], - base_dims: tuple[int, ...], - db: int, -) -> Callable[[Array, qx.StateVector, Array], tuple[qx.StateVector, Array]]: - """Build a ``jax.lax.switch`` branch that samples a Kraus trajectory on *base*.""" - - def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: - kraus_map = qx.KrausMap.from_matrix(op_mat[:, :db, :db], (base_dims, base_dims)) - return cast(tuple[qx.StateVector, Array], _sample_kraus_map_trajectory(kraus_map, psi, key, base)) - - return branch - - # ══════════════════════════════════════════════════════════ # Base class # ══════════════════════════════════════════════════════════ @@ -130,26 +88,9 @@ class ProgramSimulator: Subclasses override :meth:`_validate` and :meth:`compute`. - Instances are immutable after construction. + Instances are treated as immutable after construction. """ - __slots__ = ( - "n_qubits", - "qubits", - "dims", - "_linearize_fn", - "_resolve_fn", - "_compress_fn", - "bases", - "op_index", - "base_dims", - "base_total_dim", - "d_max", - "_has_params", - "_expanded_ops", - "_raw_subsystems", - ) - def __init__( self, program: Program, @@ -237,6 +178,15 @@ def linearize(self, memory_map: MemoryMap) -> Array: """Convert a memory map to a flat JAX parameter vector.""" return self._linearize_fn(memory_map) + def _default_params(self, params: Array | None) -> Array: + """Return *params*, or the empty-memory-map vector when ``None``. + + Lets callers omit ``params`` for parameter-free programs (where the + vector is empty). For a parametric program the empty memory map raises + on the missing register, which is clearer than silently using zeros. + """ + return self.linearize({}) if params is None else params + def resolve(self, params: Array) -> list[ResolvedOp]: """Resolve parameters into one operator per DAG node.""" return self._resolve_fn(params) @@ -245,11 +195,11 @@ def compress(self, resolved: list[ResolvedOp]) -> list[ResolvedOp]: """Merge operators via greedy edge contraction.""" return self._compress_fn(resolved) - def compute(self, params: Array, **kwargs: Any) -> Any: + def compute(self, params: Array | None = None, **kwargs: Any) -> Any: """Compute the simulation result. Subclasses must override.""" raise NotImplementedError - def _evolve(self, state: Any, op_stack: Array) -> Any: + def apply(self, state: Any, op_stack: Array) -> Any: """Apply a stack of operator matrices to *state* via a scan + switch. Each operator is dispatched to the switch branch for its base subsystem @@ -273,97 +223,24 @@ def body(state: Any, xs: tuple[Array, Array]) -> tuple[Any, None]: # ══════════════════════════════════════════════════════════ -def _embed_constant_matrix( - mat: Array, op_subsystem: tuple[int, ...], group_subsystem: tuple[int, ...], dims: tuple[int, ...], d_max: int +def _embed_unitary_to_group( + op: qx.Unitary, + target_dims: tuple[int, ...], + positions: tuple[int, ...], + d_max: int, ) -> Array: - """Embed a constant gate matrix into its merge group, padded to ``d_max``. - - Computes the ``d_max × d_max`` matrix that applies ``mat`` on ``op_subsystem`` - within the Hilbert space of ``group_subsystem`` via :func:`quax.embed`. This - runs eagerly (once per parameter-free gate, outside any ``jit``); the result - is closed over as a compile-time constant. The final pad to ``d_max`` — the - uniform stack width across all groups — is plain array padding, not a - tensor-product embedding, so it has no quax equivalent. + """Embed *op* into a merge group and pad to the uniform stack width ``d_max``. + + :func:`quax.embed` places ``op`` (whose qudits map to ``positions`` within the + group) into the group Hilbert space ``target_dims``; the trailing pad to + ``d_max`` — the stack width shared by every group — is plain array padding with + no quax equivalent. This is traceable, so it serves both the eager + constant-gate path and the vmapped parametric path. """ - op_dims = tuple(dims[q] for q in op_subsystem) - target_dims = tuple(dims[q] for q in group_subsystem) - positions = tuple(group_subsystem.index(q) for q in op_subsystem) - op = qx.Unitary.from_matrix(jnp.asarray(mat), (op_dims, op_dims)) embedded = qx.embed(op, target_dims=target_dims, positions=positions).matrix return jnp.pad(embedded, [(0, d_max - s) for s in embedded.shape]) -def _make_embed_fn( - op_subsystem: tuple[int, ...], - group_subsystem: tuple[int, ...], - dims: tuple[int, ...], - d_max: int, -) -> Callable[[Array], Array]: - """Return a JIT-friendly function that embeds a gate matrix into a group subsystem. - - Uses simple Kronecker products rather than the full qx.embed machinery - to minimize the traced graph size. - """ - if op_subsystem == group_subsystem: - D = math.prod(dims[q] for q in op_subsystem) - pad_w = ((0, d_max - D),) * 2 - - def _identity_embed(mat: Array) -> Array: - return jnp.pad(mat, pad_w) - - return _identity_embed - - # General case: embed a tensor-format operator by placing its output/input - # axes at the requested positions and identity tensors on untouched axes. - target_dims = tuple(dims[q] for q in group_subsystem) - op_dims = tuple(dims[q] for q in op_subsystem) - positions = tuple(group_subsystem.index(q) for q in op_subsystem) - n_group = len(group_subsystem) - D = math.prod(target_dims) - pad_w = ((0, d_max - D),) * 2 - n_op = len(op_subsystem) - - # For the common case: 1-qubit gate in a 2-qubit group - if n_op == 1 and n_group == 2 and all(d == 2 for d in target_dims): - pos = positions[0] - I2 = jnp.eye(2, dtype=complex) - if pos == 0: - - def _embed(mat: Array) -> Array: - return jnp.pad(jnp.kron(mat, I2), pad_w) - - return _embed - else: - - def _embed(mat: Array) -> Array: - return jnp.pad(jnp.kron(I2, mat), pad_w) - - return _embed - - non_op_positions = [i for i in range(n_group) if i not in positions] - identity_factors = [jnp.eye(target_dims[i], dtype=complex) for i in non_op_positions] - - # Example for op positions (0, 2) in a 3-qudit group: - # op tensor axes are out0,out2,in0,in2; identity axes are out1,in1; - # output order must be out0,out1,out2,in0,in1,in2. - labels = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - if 2 * n_group > len(labels): - raise ValueError(f"Cannot build an einsum embedding for {n_group} subsystems.") - out_labels = labels[:n_group] - in_labels = labels[n_group : 2 * n_group] - op_subscript = "".join(out_labels[p] for p in positions) + "".join(in_labels[p] for p in positions) - identity_subscripts = [out_labels[p] + in_labels[p] for p in non_op_positions] - embedded_subscript = out_labels + in_labels - einsum_spec = ",".join([op_subscript, *identity_subscripts]) + f"->{embedded_subscript}" - - def _embed_general(mat: Array) -> Array: - op_tensor = mat.reshape(op_dims + op_dims) - embedded = jnp.einsum(einsum_spec, op_tensor, *identity_factors) - return jnp.pad(embedded.reshape(D, D), pad_w) - - return _embed_general - - @dataclass class _GateBatch: """A set of gates sharing one constructor, concrete layout, and embedding. @@ -378,8 +255,12 @@ class _GateBatch: n_args: int #: ``(slot, value)`` for each compile-time-constant argument. concrete_args: tuple[tuple[int, float], ...] - #: Embeds a raw gate matrix into its merge group, padded to ``d_max``. - embed_fn: Callable[[Array], Array] + #: Per-qudit dimensions of the merge group each member embeds into. + target_dims: tuple[int, ...] + #: Positions within the group occupied by the gate's qudits. + group_positions: tuple[int, ...] + #: Uniform stack width every embedded matrix is padded to. + d_max: int #: Sorted-array positions this batch fills, one per member. positions: list[int] = field(default_factory=list) #: Parameter-vector index for each free argument, one list per member. @@ -389,7 +270,8 @@ def builder(self) -> Callable[[Array], Array]: """Return ``params -> (n_members, d_max, d_max)`` embedded gate matrices.""" concrete = {slot for slot, _ in self.concrete_args} free_slots = [j for j in range(self.n_args) if j not in concrete] - gate_fn, embed_fn, n_args, concrete_args = self.gate_fn, self.embed_fn, self.n_args, self.concrete_args + gate_fn, n_args, concrete_args = self.gate_fn, self.n_args, self.concrete_args + target_dims, group_positions, d_max = self.target_dims, self.group_positions, self.d_max param_indices = jnp.asarray(self.param_indices) # (n_members, n_free) def single(free_values: Array) -> Array: @@ -398,7 +280,7 @@ def single(free_values: Array) -> Array: args[slot] = val for k, slot in enumerate(free_slots): args[slot] = free_values[k] - return embed_fn(gate_fn(*args).matrix) + return _embed_unitary_to_group(gate_fn(*args), target_dims, group_positions, d_max) batched = jax.vmap(single) return lambda params: batched(params[param_indices]) @@ -473,14 +355,13 @@ def _build_vectorized_unitary_constructor( op = expanded_ops[raw_idx] op_sub = raw_subsystems[raw_idx] grp_sub = group_subsystems[pos] + # Where the op's qudits sit within the merge group, and the group's dims. + target_dims = tuple(dims[q] for q in grp_sub) + group_positions = tuple(grp_sub.index(q) for q in op_sub) if isinstance(op, ParametricGate): - # Key by embedding *type* (dims + positions within the group), not + # Key by embedding *type* (op dims + group dims + positions), not # physical qubits: embeddings that trace to the same graph share a vmap. - embed_key = ( - tuple(dims[q] for q in op_sub), - tuple(dims[q] for q in grp_sub), - tuple(grp_sub.index(q) for q in op_sub), - ) + embed_key = (tuple(dims[q] for q in op_sub), target_dims, group_positions) concrete_args = tuple((j, op.concrete_values[j]) for j, pi in enumerate(op.param_indices) if pi < 0) key = (id(op.gate_fn), concrete_args, embed_key) batch = batches.get(key) @@ -489,14 +370,16 @@ def _build_vectorized_unitary_constructor( gate_fn=op.gate_fn, n_args=len(op.param_indices), concrete_args=concrete_args, - embed_fn=_make_embed_fn(op_sub, grp_sub, dims, d_max), + target_dims=target_dims, + group_positions=group_positions, + d_max=d_max, ) batches[key] = batch batch.positions.append(pos) batch.param_indices.append([pi for pi in op.param_indices if pi >= 0]) else: const_positions.append(pos) - const_mats.append(_embed_constant_matrix(op.matrix, op_sub, grp_sub, dims, d_max)) + const_mats.append(_embed_unitary_to_group(op, target_dims, group_positions, d_max)) builders = [(np.asarray(b.positions), b.builder()) for b in batches.values()] const_pos_arr = np.asarray(const_positions) if const_positions else None @@ -531,8 +414,6 @@ class PureStateVectorSimulator(ProgramSimulator): U = jax.jit(sim.unitary)(params) """ - __slots__ = ("_psi0", "_branches", "_idx_arr", "_vmapped_build_fn") - def __init__( self, program: Program, @@ -556,8 +437,19 @@ def __init__( self.d_max, ) + # One switch branch per distinct base subsystem: it rebuilds a Unitary + # from the padded matrix slice for its base and applies it to the state. + def unitary_branch( + base: tuple[int, ...], base_dims: tuple[int, ...], db: int + ) -> Callable[[Array, qx.StateVector], qx.StateVector]: + def branch(op_mat: Array, psi: qx.StateVector) -> qx.StateVector: + unitary = qx.Unitary.from_matrix(op_mat[:db, :db], (base_dims, base_dims)) + return qx.targeted_apply_unitary(unitary, psi, base) + + return branch + self._branches = [ - _make_unitary_branch(base, base_dims, db) + unitary_branch(base, base_dims, db) for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) ] self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) @@ -569,7 +461,7 @@ def _validate(self, program: Program) -> None: if isinstance(inst, (Reset, ResetQubit)): raise ValueError(f"PureStateVectorSimulator does not support resets. Found: {inst}") - def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] + def compute(self, params: Array | None = None) -> qx.StateVector: # type: ignore[override] """Compute the final state vector. Operators are stacked into a single array and applied with a @@ -579,7 +471,8 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] the number of operations, dramatically reducing JIT compilation time for large programs. - :param params: Flat parameter vector from :meth:`linearize`. + :param params: Flat parameter vector from :meth:`linearize`. Omit (or + pass ``None``) for a parameter-free program. :return: The final state vector. """ # No operations (e.g. empty program) → the initial state is the result. @@ -588,19 +481,20 @@ def compute(self, params: Array) -> qx.StateVector: # type: ignore[override] # Vectorized construction: build embedded matrices via vmap, then # compose within each merge group via a parallel fold. - op_stack = self._vmapped_build_fn(params) - return self._evolve(self._psi0, op_stack) + op_stack = self._vmapped_build_fn(self._default_params(params)) + return self.apply(self._psi0, op_stack) - def __call__(self, params: Array) -> qx.StateVector: + def __call__(self, params: Array | None = None) -> qx.StateVector: return self.compute(params) - def unitary(self, params: Array) -> qx.Unitary: + def unitary(self, params: Array | None = None) -> qx.Unitary: """Compute the full program unitary. - :param params: Flat parameter vector from :meth:`linearize`. + :param params: Flat parameter vector from :meth:`linearize`. Omit (or + pass ``None``) for a parameter-free program. :return: The full unitary matrix. """ - resolved = self.resolve(params) + resolved = self.resolve(self._default_params(params)) compressed = self.compress(resolved) accumulated: qx.Unitary | None = None @@ -633,8 +527,6 @@ class DensityMatrixSimulator(ProgramSimulator): rho = jax.jit(sim.compute)(params) """ - __slots__ = ("_rho0", "_branches", "_idx_arr", "_const_op_stack") - def __init__( self, program: Program, @@ -645,8 +537,20 @@ def __init__( ) -> None: super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) self._rho0 = qx.zero_state_matrix(dims=self.dims) + + # One switch branch per distinct base subsystem: it rebuilds a SuperOp + # from the padded matrix slice for its base and applies it to the state. + def superop_branch( + base: tuple[int, ...], base_dims: tuple[int, ...], db2: int + ) -> Callable[[Array, qx.DensityMatrix], qx.DensityMatrix]: + def branch(op_mat: Array, rho: qx.DensityMatrix) -> qx.DensityMatrix: + superop = qx.SuperOp.from_matrix(op_mat[:db2, :db2], (base_dims, base_dims)) + return qx.targeted_apply_superop(superop, rho, base) + + return branch + self._branches = [ - _make_superop_branch(base, base_dims, db * db) + superop_branch(base, base_dims, db * db) for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) ] self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) @@ -666,7 +570,7 @@ def _stack_superops(self, resolved: list[ResolvedOp]) -> Array: mats = [_pad_matrix(superop.matrix, d_max2, d_max2) for superop, _ in superops] return jnp.stack(mats, axis=0) - def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] + def compute(self, params: Array | None = None) -> qx.DensityMatrix: # type: ignore[override] """Compute the final density matrix. Superoperators are stacked and applied with a :func:`jax.lax.scan` @@ -674,7 +578,8 @@ def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] :func:`jax.lax.switch`, keeping the compiled graph size proportional to the number of distinct base subsystems. - :param params: Flat parameter vector from :meth:`linearize`. + :param params: Flat parameter vector from :meth:`linearize`. Omit (or + pass ``None``) for a parameter-free program. :return: The final density matrix. """ if not self._has_params and self.op_index: @@ -683,13 +588,13 @@ def compute(self, params: Array) -> qx.DensityMatrix: # type: ignore[override] self._const_op_stack = self._stack_superops(self.resolve(jnp.zeros(0))) op_stack = self._const_op_stack else: - resolved = self.resolve(params) + resolved = self.resolve(self._default_params(params)) if not resolved: return self._rho0 op_stack = self._stack_superops(resolved) - return self._evolve(self._rho0, op_stack) + return self.apply(self._rho0, op_stack) - def __call__(self, params: Array) -> qx.DensityMatrix: + def __call__(self, params: Array | None = None) -> qx.DensityMatrix: return self.compute(params) @@ -722,8 +627,6 @@ class TrajectorySimulator(ProgramSimulator): outcomes. """ - __slots__ = ("_kraus_truncation_threshold", "_devices") - def __init__( self, program: Program, @@ -745,19 +648,20 @@ def adapt(self, compressed: list[ResolvedOp]) -> list[TrajectoryOp]: def compute( # type: ignore[override] self, - params: Array, - key: Array, + params: Array | None = None, + key: Array | None = None, ) -> tuple[qx.StateVector, Array]: """Run trajectory simulation. - :param params: Flat parameter vector from :meth:`linearize`. - :param key: JAX PRNG key. Scalar key → single trajectory. + :param params: Flat parameter vector from :meth:`linearize`. Omit (or + pass ``None``) for a parameter-free program. + :param key: JAX PRNG key (required). Scalar key → single trajectory. Batch of keys (from ``jax.random.split``) → batched trajectories. :return: Tuple of ``(state_vector, measurement_outcomes)``. """ - resolved = self.resolve(params) - compressed = self.compress(resolved) - operations = self.adapt(compressed) + if key is None: + raise ValueError("TrajectorySimulator.compute requires a JAX PRNG key.") + operations = self.adapt(self.compress(self.resolve(self._default_params(params)))) if key.ndim == 0: psi = qx.zero_state_vector(dims=self.dims) @@ -765,14 +669,14 @@ def compute( # type: ignore[override] n_traj = key.shape[0] psi = qx.zero_state_vector(dims=self.dims, ensemble_size=(n_traj,)) - return _apply_trajectory_operations(operations, psi, key) + return _build_trajectory_kernel(operations, self.dims)(psi, key) - def __call__(self, params: Array, key: Array) -> tuple[qx.StateVector, Array]: + def __call__(self, params: Array | None = None, key: Array | None = None) -> tuple[qx.StateVector, Array]: return self.compute(params, key) def sample( self, - params: Array, + params: Array | None = None, num_trajectories: int = 1000, batch_size: int = 250, random_seed: int = 0, @@ -784,16 +688,15 @@ def sample( available, each batch is sharded across them so that every device processes ``batch_size // n_devices`` trajectories concurrently. - :param params: Flat parameter vector from :meth:`linearize`. + :param params: Flat parameter vector from :meth:`linearize`. Omit (or + pass ``None``) for a parameter-free program. :param num_trajectories: Total number of trajectories to simulate. :param batch_size: Maximum number of trajectories per batch (total across all devices). :param random_seed: Seed for the JAX PRNG. :return: Measurement outcomes with shape ``(num_trajectories, n_measurements)``. """ - resolved = self.resolve(params) - compressed = self.compress(resolved) - operations = self.adapt(compressed) + operations = self.adapt(self.compress(self.resolve(self._default_params(params)))) _, all_outcomes = _run_batched_trajectories( operations, @@ -851,41 +754,48 @@ def _op_to_kraus_matrix( raise TypeError(f"Unsupported operator type: {type(op)}") -def _apply_trajectory_operations( - operations: list[TrajectoryOp], - psi: qx.StateVector, - key: Array, -) -> tuple[qx.StateVector, Array]: - """Apply trajectory operations to a (batched) state vector via a JAX loop. +TrajectoryRun = Callable[[qx.StateVector, Array], tuple[qx.StateVector, Array]] - Every operator is converted to a (zero-padded) Kraus map and stacked into a - single array. A :func:`jax.lax.fori_loop` then iterates over the stack, - dispatching each operator to the correct base subsystem with a - :func:`jax.lax.switch`. Because only one loop body and one switch branch - per distinct subsystem are traced, the compiled graph size scales with the - number of distinct subsystems rather than the number of operations. - Measurements are handled uniformly: a quantum instrument is flattened so - that sampling a Kraus index also selects an outcome (``index // divisor``). - Zero-padded Kraus operators have zero Born probability and are therefore - never sampled. +def _build_trajectory_kernel(operations: list[TrajectoryOp], dims: tuple[int, ...]) -> TrajectoryRun: + """Build a reusable, jitted trajectory kernel from *operations*. - Key generation is sharding-friendly: per-operation keys are derived lazily - via ``jax.random.fold_in`` so the key array is never materialised in full. + All batch-invariant work happens once, here: enumerating distinct subsystems, + building the per-subsystem switch branches, and converting every operator to a + padded Kraus stack. The returned ``run(psi, key)`` wraps the scan over that + stack in :func:`jax.jit`, so repeated calls with matching ``psi``/``key`` + shapes (e.g. one per sampling batch) reuse a single compilation instead of + re-tracing — this is what turns the previously spiky, recompile-per-batch GPU + usage into a single upfront compile. + + Every operator is expressed as a (zero-padded) Kraus map and stacked into one + array indexed by the scan; measurements are handled uniformly by flattening a + quantum instrument so that sampling a Kraus index also selects an outcome + (``index // divisor``). Zero-padded Kraus operators have zero Born + probability and are therefore never sampled. Per-operation keys are derived + lazily via ``jax.random.fold_in`` so the key array is never materialised in + full (sharding-friendly). :param operations: Ordered list of ``(operator, subsystem)`` pairs. - :param psi: Initial state vector, optionally batched via ensemble dimension. - :param key: JAX PRNG key (scalar) or per-trajectory key vector. - :return: Tuple of ``(final_state_vector, measurement_outcomes)`` where - measurement_outcomes has shape ``(*ensemble, n_measurements)`` with - dtype int32. + :param dims: Per-qudit register dimensions (must match the ``psi`` passed to + ``run``). + :return: ``run(psi, key) -> (final_state_vector, measurement_outcomes)`` where + ``measurement_outcomes`` has shape ``(*ensemble, n_measurements)``, + dtype int32. ``key`` is a scalar PRNG key or a per-trajectory key vector. """ - ensemble_size = psi.ensemble_size - if not operations: - return psi, jnp.empty((*ensemble_size, 0), dtype=jnp.int32) - # 1. Enumerate distinct subsystems → one switch branch each. + def run_empty(psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: + return psi, jnp.empty((*psi.ensemble_size, 0), dtype=jnp.int32) + + return run_empty + + # 1. Enumerate distinct subsystems → one switch branch each. A branch is + # handed a padded matrix (not a KrausMap) because the scan must index a + # single homogeneous op stack: operators live on different subsystems with + # different Kraus counts and so cannot be stacked as one KrausMap, whose + # dims are static per-branch metadata. Each branch rebuilds its KrausMap + # from the slice, using the base dims it closes over. distinct_subsystems: list[tuple[int, ...]] = [] sub_to_branch: dict[tuple[int, ...], int] = {} for _, subsystem in operations: @@ -893,14 +803,17 @@ def _apply_trajectory_operations( sub_to_branch[subsystem] = len(distinct_subsystems) distinct_subsystems.append(subsystem) - branches = [ - _make_kraus_trajectory_branch( - subsystem, - tuple(psi.dims[q] for q in subsystem), - math.prod(psi.dims[q] for q in subsystem), - ) - for subsystem in distinct_subsystems - ] + def make_branch(base: tuple[int, ...]) -> Callable[[Array, qx.StateVector, Array], tuple[qx.StateVector, Array]]: + base_dims = tuple(dims[q] for q in base) + db = math.prod(base_dims) + + def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: + kraus_map = qx.KrausMap.from_matrix(op_mat[:, :db, :db], (base_dims, base_dims)) + return cast(tuple[qx.StateVector, Array], _sample_kraus_map_trajectory(kraus_map, psi, key, base)) + + return branch + + branches = [make_branch(subsystem) for subsystem in distinct_subsystems] # 2. Convert every operator to a padded Kraus matrix and stack. kraus_mats: list[Array] = [] @@ -908,12 +821,12 @@ def _apply_trajectory_operations( measure_positions: list[int] = [] branch_index: list[int] = [] for i, (op, subsystem) in enumerate(operations): - # Promote each operator to the state's dimension on its subsystem (identity on the + # Promote each operator to the register dimension on its subsystem (identity on the # higher levels). Without this, an op authored at a lower dimension than the register # (e.g. a qubit-dimension channel on a register promoted to qutrits by a leakage model) # would be zero-padded to ``d_max`` instead — silently wrong on the high levels, and a # reshape error when ``d_max`` is below the branch's dimension. - target_dims = tuple(psi.dims[q] for q in subsystem) + target_dims = tuple(dims[q] for q in subsystem) if op.dims[0] != target_dims: op = qx.promote(op, target_dims) mat, divisor, is_measure = _op_to_kraus_matrix(op) @@ -927,33 +840,48 @@ def _apply_trajectory_operations( d_max = max(mat.shape[-1] for mat in kraus_mats) op_stack = jnp.stack([_pad_matrix(mat, max_k, d_max, d_max) for mat in kraus_mats], axis=0) branch_arr = jnp.asarray(branch_index, dtype=jnp.int32) + op_indices = jnp.arange(len(operations), dtype=jnp.int32) - # 3. Per-trajectory base keys. - if ensemble_size: - per_traj_keys = key if key.ndim > 0 else jax.random.split(key, ensemble_size[0]) - else: - per_traj_keys = None + @jax.jit + def run(psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: + ensemble_size = psi.ensemble_size + if ensemble_size: + per_traj_keys = key if key.ndim > 0 else jax.random.split(key, ensemble_size[0]) + else: + per_traj_keys = None + + def body(psi_c: qx.StateVector, xs: tuple[Array, Array, Array]) -> tuple[qx.StateVector, Array]: + op_mat, bidx, i = xs + if per_traj_keys is not None: + op_key = jax.vmap(lambda k: jax.random.fold_in(k, i))(per_traj_keys) + else: + op_key = jax.random.fold_in(key, i) + psi_c, sampled_idx = jax.lax.switch(bidx, branches, op_mat, psi_c, op_key) + return psi_c, sampled_idx.astype(jnp.int32) - n_ops = len(operations) - sampled_init = jnp.zeros((n_ops, *ensemble_size), dtype=jnp.int32) + psi_out, sampled = jax.lax.scan(body, psi, (op_stack, branch_arr, op_indices)) - def body(i: Array, carry: tuple[qx.StateVector, Array]) -> tuple[qx.StateVector, Array]: - psi_c, sampled = carry - if per_traj_keys is not None: - op_key = jax.vmap(lambda k: jax.random.fold_in(k, i))(per_traj_keys) + if measure_positions: + outcomes = jnp.stack([sampled[p] // divisors[p] for p in measure_positions], axis=-1) else: - op_key = jax.random.fold_in(key, i) - psi_c, sampled_idx = jax.lax.switch(branch_arr[i], branches, op_stack[i], psi_c, op_key) - return psi_c, sampled.at[i].set(sampled_idx.astype(jnp.int32)) + outcomes = jnp.empty((*ensemble_size, 0), dtype=jnp.int32) + return psi_out, outcomes - psi, sampled = jax.lax.fori_loop(0, n_ops, body, (psi, sampled_init)) + return run - if measure_positions: - outcomes = jnp.stack([sampled[p] // divisors[p] for p in measure_positions], axis=-1) - else: - outcomes = jnp.empty((*ensemble_size, 0), dtype=jnp.int32) - return psi, outcomes +def _apply_trajectory_operations( + operations: list[TrajectoryOp], + psi: qx.StateVector, + key: Array, +) -> tuple[qx.StateVector, Array]: + """Build a one-off trajectory kernel for *operations* and apply it to *psi*. + + Convenience wrapper around :func:`_build_trajectory_kernel` for callers that + run a single (batch of) trajectories; batched sampling reuses one kernel + across batches instead — see :func:`_run_batched_trajectories`. + """ + return _build_trajectory_kernel(operations, psi.dims)(psi, key) def _make_mesh(devices: list[jax.Device] | None) -> Mesh | None: @@ -986,12 +914,26 @@ def _run_batched_trajectories( is constructed and both the initial state vector and PRNG keys are sharded along the trajectory (ensemble) axis. XLA's SPMD partitioner then distributes the work so that each device processes its own slice. + + Every batch runs at the same width (:func:`_build_trajectory_kernel` compiled + once, reused across batches) so the GPU sees one upfront compile rather than a + recompile spike per batch; the final short batch is padded up to that width + and its extra rows are sliced off. """ if dims is None: dims = (2,) * n_qubits mesh = _make_mesh(devices) n_devices = len(mesh.devices.flat) if mesh is not None else 1 + sharding = NamedSharding(mesh, PartitionSpec("traj")) if mesh is not None else None # type: ignore[no-untyped-call] + + # Build the jitted trajectory kernel once; reuse it for every batch. + run = _build_trajectory_kernel(operations, dims) + + # Uniform per-batch width: full batches and the padded tail all share it, so + # the kernel compiles exactly once. Capped at the total so single-batch runs + # don't pad past what's asked for; rounded to n_devices for even sharding. + batch_width = _round_up_to(min(num_trajectories, batch_size), n_devices) key = jax.random.key(random_seed) all_psis: list[qx.StateVector] = [] @@ -1000,47 +942,30 @@ def _run_batched_trajectories( remaining = num_trajectories while remaining > 0: this_batch = min(remaining, batch_size) - - # Pad to a multiple of n_devices so the shard split is even. - padded_batch = _round_up_to(this_batch, n_devices) if n_devices > 1 else this_batch - n_pad = padded_batch - this_batch - key, batch_key = jax.random.split(key) - if padded_batch == 1: + if batch_width == 1: psi = qx.zero_state_vector(dims=dims) + batch_keys = batch_key else: - psi = qx.zero_state_vector(dims=dims, ensemble_size=(padded_batch,)) + psi = qx.zero_state_vector(dims=dims, ensemble_size=(batch_width,)) + batch_keys = batch_key # Shard state and key across devices when a mesh is available. - if mesh is not None: - sharding = NamedSharding(mesh, PartitionSpec("traj")) # type: ignore[no-untyped-call] - psi = qx.StateVector.from_matrix( - jax.device_put(psi.matrix, sharding), - psi.dims, - ) - # Split a per-trajectory key vector and shard it. - batch_keys = jax.random.split(batch_key, padded_batch) - batch_keys = jax.device_put(batch_keys, sharding) - else: - batch_keys = batch_key + if sharding is not None: + psi = qx.StateVector.from_matrix(jax.device_put(psi.matrix, sharding), psi.dims) + batch_keys = jax.device_put(jax.random.split(batch_key, batch_width), sharding) - psi_out, outcomes = _apply_trajectory_operations(operations, psi, batch_keys) + psi_out, outcomes = run(psi, batch_keys) psi_out.matrix.block_until_ready() - # Strip padding rows. - if n_pad > 0: - psi_out = qx.StateVector.from_matrix( - psi_out.matrix[:this_batch], - psi_out.dims, - ) + # Strip padding rows down to this batch's real trajectory count. + if batch_width > 1 and this_batch < batch_width: + psi_out = qx.StateVector.from_matrix(psi_out.matrix[:this_batch], psi_out.dims) outcomes = outcomes[:this_batch] - if this_batch == 1 and padded_batch == 1: - psi_out = qx.StateVector.from_matrix( - psi_out.matrix[jnp.newaxis], - psi_out.dims, - ) + if this_batch == 1 and batch_width == 1: + psi_out = qx.StateVector.from_matrix(psi_out.matrix[jnp.newaxis], psi_out.dims) outcomes = outcomes[jnp.newaxis] if keep_states: @@ -1137,8 +1062,6 @@ class DynamicTrajectorySimulator(ProgramSimulator): shots = sim.sample(params, num_trajectories=1000) """ - __slots__ = ("_kraus_truncation_threshold", "_squeeze_tol") - def __init__( self, program: Program, @@ -1158,18 +1081,23 @@ def _validate(self, program: Program) -> None: def compute( # type: ignore[override] self, - params: Array, - key: Array, + params: Array | None = None, + key: Array | None = None, ) -> tuple[qx.StateVector, Array]: """Run a single dynamic-shape trajectory. - :param params: Flat parameter vector from :meth:`linearize`. - :param key: Scalar JAX PRNG key for this trajectory. + :param params: Flat parameter vector from :meth:`linearize`. Omit (or + pass ``None``) for a parameter-free program. + :param key: Scalar JAX PRNG key for this trajectory (required). :return: ``(state_vector, measurement_outcomes)``. The state's per-qudit dimensions reflect whatever leakage survived squeezing; outcomes has shape ``(n_measurements,)`` in program order. """ - operations = adapt_for_trajectory(self.compress(self.resolve(params)), self._kraus_truncation_threshold) + if key is None: + raise ValueError("DynamicTrajectorySimulator.compute requires a JAX PRNG key.") + operations = adapt_for_trajectory( + self.compress(self.resolve(self._default_params(params))), self._kraus_truncation_threshold + ) psi = qx.zero_state_vector(dims=(2,) * self.n_qubits) outcomes: list[Array] = [] @@ -1187,12 +1115,12 @@ def compute( # type: ignore[override] return psi, jnp.stack(outcomes, axis=-1).astype(jnp.int32) return psi, jnp.empty((0,), dtype=jnp.int32) - def __call__(self, params: Array, key: Array) -> tuple[qx.StateVector, Array]: + def __call__(self, params: Array | None = None, key: Array | None = None) -> tuple[qx.StateVector, Array]: return self.compute(params, key) def sample( self, - params: Array, + params: Array | None = None, num_trajectories: int = 1000, random_seed: int = 0, ) -> Array: diff --git a/test/unit/test_noise_model.py b/test/unit/test_noise_model.py index 223075cc9..ba53170b5 100644 --- a/test/unit/test_noise_model.py +++ b/test/unit/test_noise_model.py @@ -20,12 +20,19 @@ import quax as qx from pyquil.external.rpcq import CompilerISA -from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, X -from pyquil.noise._channels import Channel, MeasurementChannel, ResetChannel, get_instruction_unitary +from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, RZ, X +from pyquil.noise._channels import ( + Channel, + CycleChannel, + MeasurementChannel, + ResetChannel, + _build_cycle_channel, + get_instruction_unitary, +) from pyquil.noise._noise_model import NoiseModel from pyquil.quil import Program -from pyquil.quilatom import Qubit -from pyquil.quilbase import Gate, Measurement, ResetQubit +from pyquil.quilatom import FormalArgument, Qubit +from pyquil.quilbase import DefCircuit, Gate, Measurement, ResetQubit from pyquil.simulation._simulator import DensityMatrixSimulator _EMPTY_PARAMS = jnp.array([], dtype=float) @@ -242,6 +249,48 @@ def test_pow_rejects_non_embeddable_measurement(self): with pytest.raises(ValueError, match="not embeddable|not a valid"): _ = ch**0.5 + def test_from_binary_discriminator_qubit_is_faithful_readout(self): + """Regression: dim=2/threshold=1 is a real qubit readout, not an always-0 collapse. + + The previous implementation mapped both |0> and |1> to outcome 0 for a qubit, + silently erasing all measurement information. + """ + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + ch = MeasurementChannel.from_binary_discriminator(inst=meas_inst, dim=2, threshold=1) + cm = np.asarray(ch.confusion_matrix) + assert cm.shape == (2, 2) # two reachable outcomes + assert np.allclose(cm, np.eye(2)) # |0> -> 0, |1> -> 1 + + def test_from_binary_discriminator_qutrit_split(self): + """dim=3 splits into exactly two outcomes at the threshold.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + # threshold=2: {0,1} -> 0, {2} -> 1 (flag leakage only) + cm2 = np.asarray(MeasurementChannel.from_binary_discriminator(inst=meas_inst, dim=3, threshold=2).confusion_matrix) + assert np.allclose(cm2, [[1, 1, 0], [0, 0, 1]]) + # threshold=1: {0} -> 0, {1,2} -> 1 (ground vs excited-or-leaked) + cm1 = np.asarray(MeasurementChannel.from_binary_discriminator(inst=meas_inst, dim=3, threshold=1).confusion_matrix) + assert np.allclose(cm1, [[1, 0, 0], [0, 1, 1]]) + + def test_from_binary_discriminator_fidelity_degrades(self): + """Sub-unit fidelity stays column-stochastic and keeps both outcomes reachable.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + cm = np.asarray( + MeasurementChannel.from_binary_discriminator(inst=meas_inst, dim=2, threshold=1, fidelity=0.9).confusion_matrix + ) + assert cm.shape == (2, 2) + assert np.allclose(cm.sum(axis=0), 1.0) + assert cm[1, 1] > cm[0, 1] # |1> still most likely reads as outcome 1 + + def test_from_binary_discriminator_rejects_bad_threshold(self): + """threshold must satisfy 1 <= threshold < dim.""" + prog = Program(MEASURE(0, None)) + meas_inst = [i for i in prog if isinstance(i, Measurement)][0] + with pytest.raises(ValueError, match="threshold"): + MeasurementChannel.from_binary_discriminator(inst=meas_inst, dim=2, threshold=2) + def test_json_roundtrip_preserves_qutrit_dims(self): """MeasurementChannel JSON includes explicit dims for non-qubit instruments.""" prog = Program(MEASURE(0, None)) @@ -635,3 +684,24 @@ def test_measurement_dedup_prefers_real_fidelity(self): nm = NoiseModel.from_isa(self._isa()) channel = nm.get_channel(Measurement(qubit=Qubit(0), classical_reg=None)) assert isinstance(channel, MeasurementChannel) + + +class TestCycleChannel: + def test_complete_cycle_constructs(self): + """A CycleChannel whose channels cover every DefCircuit body instruction is valid.""" + channels = tuple( + Channel.from_depolarizing_constant(inst, depolarizing_constant=0.99) for inst in (RX(0.1, 0), RZ(0.2, 1)) + ) + cycle = _build_cycle_channel(list(channels)) + assert cycle.channels == channels + + def test_incomplete_cycle_rejected(self): + """A body instruction with no corresponding channel is a footgun and must raise.""" + q0, q1 = FormalArgument("q0"), FormalArgument("q1") + # DefCircuit body has two gates, but only one channel is supplied for q0. + defcircuit = DefCircuit("CYCLE", [], [q0, q1], [RX(0.1, q0), RZ(0.2, q1)]) + cycle_inst = Gate("CYCLE", [], [Qubit(0), Qubit(1)]) + channels = (Channel.from_depolarizing_constant(RX(0.1, 0), depolarizing_constant=0.99),) + + with pytest.raises(ValueError, match="incomplete"): + _ = CycleChannel(inst=cycle_inst, defcircuit=defcircuit, channels=channels) From 329f83d22d4ee59e4e94221fb3295d45713f4b34 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 2 Jul 2026 14:52:51 +0000 Subject: [PATCH 34/37] Clean up --- pyquil/noise/_channels.py | 49 +++-- pyquil/simulation/_resolver.py | 102 ++++++++- pyquil/simulation/_simulator.py | 367 ++++++++++++++++++++++---------- test/unit/test_resolver.py | 22 ++ test/unit/test_state_vector.py | 27 ++- 5 files changed, 422 insertions(+), 145 deletions(-) diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index d62b0f53a..2aefe1d61 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -1478,36 +1478,43 @@ def __post_init__(self) -> None: Operations are matched by identity (name, params, concrete qubits), independent of the DefCircuit's formal-argument naming. """ - qarg_to_qubit = dict(zip(self.defcircuit.qubit_variables, self.inst.get_qubit_indices(), strict=False)) - - def _resolve(qubit: object) -> int: - if qubit in qarg_to_qubit: - return qarg_to_qubit[qubit] # type: ignore[index] - return qubit.index if hasattr(qubit, "index") else int(qubit) # type: ignore[union-attr,arg-type] - - def _body_key(inst: Gate | Measurement) -> tuple[str, tuple, tuple[int, ...]]: - if isinstance(inst, Measurement): - return ("MEASURE", (), (_resolve(inst.qubit),)) - return (inst.name, tuple(inst.params), tuple(_resolve(q) for q in inst.qubits)) - - def _channel_key(channel: Channel | MeasurementChannel) -> tuple[str, tuple, tuple[int, ...]]: - if isinstance(channel, MeasurementChannel): - return ("MEASURE", (), tuple(channel.qubits)) - return (channel.inst.name, tuple(channel.inst.params), tuple(channel.inst.get_qubit_indices())) - - expected = sorted(repr(_body_key(inst)) for inst in self.defcircuit.instructions) - provided = sorted(repr(_channel_key(ch)) for ch in self.channels) - if expected != provided: + if len(self.expanded_instructions) != len(self.channels): raise ValueError( "CycleChannel is incomplete: every instruction in the cycle's DefCircuit " "body must have a corresponding channel. " - f"DefCircuit body: {expected}; channels: {provided}." + f"\nDefCircuit body: {self.expanded_instructions}" + f"\nChannels: {self.channels}" ) + for instruction, channel in zip(self.expanded_instructions, self.channels): + if str(instruction) != str(channel.inst): + raise ValueError( + "CycleChannel is incomplete: every instruction in the cycle's DefCircuit " + "body must have a corresponding channel. " + f"\nDefCircuit body: {instruction}" + f"\nChannels: {channel.inst}" + ) # ────────────────────────────────────────────── # Derived properties # ────────────────────────────────────────────── + @cached_property + def expanded_instructions(self) -> list[Gate | Measurement | ResetQubit]: + """Return the expanded instructions of the defcircuit.""" + qarg_to_qubit = dict(zip(self.defcircuit.qubit_variables, self.inst.get_qubit_indices(), strict=False)) + instructions = [] + for inst in self.defcircuit.instructions: + match inst: + case Measurement(): + instructions.append(Measurement(qubit=qarg_to_qubit[inst.qubit], classical_reg=inst.classical_reg)) # type: ignore[arg-type] + case ResetQubit(): + instructions.append(ResetQubit(qarg_to_qubit[inst.qubit])) # type: ignore[arg-type] + case Gate(): + instructions.append(Gate(inst.name, inst.params, [qarg_to_qubit[q] for q in inst.qubits])) # type: ignore[arg-type] + case _: + raise TypeError(f"Unsupported instruction type in defcircuit: {type(inst).__name__}") + return instructions + @cached_property def operator(self) -> tuple[qx.SuperOp | qx.QuantumInstrument, ...]: """Tuple of process superoperators, one per constituent channel.""" diff --git a/pyquil/simulation/_resolver.py b/pyquil/simulation/_resolver.py index cad97858c..b1dfa1971 100644 --- a/pyquil/simulation/_resolver.py +++ b/pyquil/simulation/_resolver.py @@ -35,7 +35,7 @@ import logging from collections.abc import Callable, Iterator, Mapping from copy import deepcopy -from typing import Any, NamedTuple, TypeAlias, cast +from typing import Any, Literal, NamedTuple, TypeAlias, cast import jax.numpy as jnp import networkx as nx @@ -67,6 +67,13 @@ # A fixed (non-parameterized) operator — the most specific native quax type. FixedOp: TypeAlias = qx.Unitary | qx.SuperOp | qx.KrausMap | qx.QuantumInstrument +# How ``MEASURE`` is represented during expansion. The grad-able simulators +# (state-vector, density-matrix) treat a measurement as a plain dephasing +# ``SuperOp`` (``"superop"``); the trajectory simulators keep it as a sampled +# ``QuantumInstrument`` (``"instrument"``). See :func:`resolve_for_gradable` and +# :func:`resolve_for_trajectory`. +MeasurementMode: TypeAlias = Literal["superop", "instrument"] + class ParametricGate: """A parametric gate whose matrix depends on runtime parameters. @@ -214,6 +221,7 @@ def expand_program( qubit_dimensions: Mapping[int, int] | None = None, *, context: _ExpansionContext | None = None, + measurement: MeasurementMode = "instrument", ) -> tuple[list[ExpandedOp], list[tuple[int, ...]], list[tuple[str, int]]]: """Expand a program into operators and physical qubit tuples. @@ -240,6 +248,11 @@ def expand_program( :param context: Optional precomputed :class:`_ExpansionContext`. When omitted it is derived from the program; pass it to avoid recomputing the dimension-independent derivations across multiple expansion passes. + :param measurement: How to represent ``MEASURE`` instructions. ``"instrument"`` + (default) keeps a sampled ``QuantumInstrument``; ``"superop"`` emits the + measurement's dephasing total channel as a ``SuperOp`` so no instrument + (and hence no compressor barrier) is produced — used by the grad-able + simulators. :return: Tuple of ``(ops, qubit_tuples, param_refs)`` where each op is either a concrete quax operator or a ``Callable[[Array], Unitary]`` for parameterized gates, each qubit tuple contains physical qubit @@ -307,12 +320,23 @@ def _dimension_for(qubit: int) -> int: return qubit_dimensions.get(qubit, 2) if qubit_dimensions is not None else 2 def _resolve_measurement(inst: Measurement) -> tuple[FixedOp, tuple[int, ...]]: - """Resolve a measurement instruction.""" + """Resolve a measurement instruction. + + Under ``measurement="instrument"`` the result is a ``QuantumInstrument`` + (sampled by the trajectory simulators). Under ``measurement="superop"`` + the instrument's dephasing total channel is returned as a ``SuperOp`` so + the grad-able pipeline never has to carry — or merge — an instrument. + """ qubits = tuple(inst.get_qubit_indices()) channel = noise_model.get_channel(inst) if noise_model is not None else None - if isinstance(channel, MeasurementChannel): - return channel.process, qubits - return qx.gates.MEASURE(dim=_dimension_for(qubits[0])), qubits + instrument = ( + channel.process + if isinstance(channel, MeasurementChannel) + else qx.gates.MEASURE(dim=_dimension_for(qubits[0])) + ) + if measurement == "superop": + return instrument.total_channel(), qubits + return instrument, qubits def _resolve_reset_qubit(inst: ResetQubit) -> tuple[FixedOp, tuple[int, ...]]: """Resolve a targeted reset instruction.""" @@ -457,6 +481,8 @@ def resolve_program( noise_model: NoiseModelLike | None = None, qubits: list[int] | None = None, dims: tuple[int, ...] | None = None, + *, + measurement: MeasurementMode = "instrument", ) -> Resolution: """Expand a program and build its parameter-resolving closure. @@ -479,6 +505,9 @@ def resolve_program( program. Use this when the simulator knows about qubits that don't appear in the program. :param dims: Optional pre-determined per-qudit dimensions. + :param measurement: Measurement representation — see :func:`expand_program`. + Prefer the :func:`resolve_for_gradable` / :func:`resolve_for_trajectory` + entry points, which pin the correct mode for each simulator family. :return: A :class:`Resolution`. """ if qubits is None: @@ -492,7 +521,7 @@ def expand( qubit_dimensions: Mapping[int, int] | None, ) -> tuple[list[ExpandedOp], list[tuple[int, ...]], list[tuple[str, int]]]: ops, phys_qubits, param_refs = expand_program( - program, noise_model, qubit_dimensions=qubit_dimensions, context=context + program, noise_model, qubit_dimensions=qubit_dimensions, context=context, measurement=measurement ) return ops, remap_qubits(phys_qubits, qubit_indices), param_refs @@ -506,6 +535,67 @@ def expand( return Resolution(dims, ops, subsystems, param_refs, _freeze_resolver(ops, subsystems)) +def resolve_for_gradable( + program: Program, + noise_model: NoiseModelLike | None = None, + qubits: list[int] | None = None, + dims: tuple[int, ...] | None = None, +) -> Resolution: + """Resolve a program for the grad-able (state-vector / density-matrix) simulators. + + Measurements become dephasing ``SuperOp`` operators, so the resolved program + contains only ``Unitary``/``SuperOp`` operators and no compressor barriers are + needed. Thin wrapper over :func:`resolve_program` with ``measurement="superop"``. + """ + return resolve_program(program, noise_model, qubits, dims, measurement="superop") + + +def resolve_for_trajectory( + program: Program, + noise_model: NoiseModelLike | None = None, + qubits: list[int] | None = None, + dims: tuple[int, ...] | None = None, +) -> Resolution: + """Resolve a program for the trajectory simulators. + + Measurements remain sampled ``QuantumInstrument`` operators (kept out of merges + by compressor barriers). Thin wrapper over :func:`resolve_program` with + ``measurement="instrument"``. + """ + return resolve_program(program, noise_model, qubits, dims, measurement="instrument") + + +def enumerate_bases( + emit_order: list[tuple[int, list[int], tuple[int, ...]]], +) -> tuple[list[tuple[int, ...]], tuple[int, ...]]: + """Enumerate the distinct base subsystems produced by a compressor. + + The compressor's ``emit_order`` (see :func:`compressor_from_dag`) lists one + ``(root, nodes, subsystem)`` entry per emitted group, in application order. The + merge structure depends only on the DAG, not on parameter values, so the base + subsystems can be read straight off ``emit_order`` — no ``resolve``/``compress`` + probe is required. + + The grad-able simulators dispatch each compressed operation through a + ``jax.lax.switch`` keyed by its base, so the number of *distinct* bases (rather + than the number of operations) sets the size of the compiled graph. + + :param emit_order: The ``emit_order`` attribute of a compressor closure. + :return: ``(bases, op_index)`` where ``bases`` is the distinct subsystems in + first-seen order and ``op_index[k]`` is the base index of the ``k``-th + emitted operation. + """ + bases: list[tuple[int, ...]] = [] + sub_to_branch: dict[tuple[int, ...], int] = {} + op_index: list[int] = [] + for _, _, subsystem in emit_order: + if subsystem not in sub_to_branch: + sub_to_branch[subsystem] = len(bases) + bases.append(subsystem) + op_index.append(sub_to_branch[subsystem]) + return bases, tuple(op_index) + + # ══════════════════════════════════════════════════════════ # Adapters # diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 3c4093b62..0dbeb3793 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -13,21 +13,28 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -"""Unified program simulators backed by quax. +"""Program simulators backed by quax. -Three simulators share a common preprocessing pipeline: +All simulators share the preprocessing in :class:`ProgramSimulator` (expansion, +dimension inference, ``linearize``/``resolve``/``compress``) and then split into +two families with different execution models: -* :class:`PureStateVectorSimulator` — gate-only programs (no noise, - measurements, or resets). Jit- and grad-friendly. -* :class:`DensityMatrixSimulator` — any program, optionally with noise. - Jit- and grad-friendly. -* :class:`TrajectorySimulator` — Monte Carlo trajectory simulation for - programs with measurements and resets, optionally with noise. +* **Grad-able** (:class:`_GradableSimulator`) — jit/grad-friendly evolution of a + compressed ``Unitary``/``SuperOp`` stack; measurements are dephasing SuperOps and + there are no compressor barriers: -Each simulator is constructed from a :class:`~pyquil.quil.Program` and -exposes ``linearize``, ``resolve``, ``compress``, and ``compute`` methods. -The ``compute`` method is the main entry point and can be passed directly -to ``jax.jit`` or ``jax.grad``. + * :class:`PureStateVectorSimulator` — gate-only programs (no noise, measurements, + or resets). + * :class:`DensityMatrixSimulator` — any program, optionally with noise. + +* **Trajectory** (:class:`_TrajectorySimulator`) — Monte Carlo sampling of programs + with measurements and resets; measurements stay as sampled QuantumInstruments: + + * :class:`TrajectorySimulator` — fixed-dimension, vectorized/batched trajectories. + * :class:`DynamicTrajectorySimulator` — eager, per-trajectory dynamic qudit dims. + +The ``compute`` method is the main entry point; for the grad-able family it can be +passed directly to ``jax.jit`` or ``jax.grad``. """ from __future__ import annotations @@ -50,6 +57,7 @@ from pyquil.quil import Program from pyquil.quilbase import Measurement, Reset, ResetQubit from pyquil.simulation._resolver import ( + MeasurementMode, ParametricGate, ResolvedOp, TrajectoryOp, @@ -57,7 +65,9 @@ adapt_for_trajectory, build_dag, compressor_from_dag, - resolve_program, + enumerate_bases, + resolve_for_gradable, + resolve_for_trajectory, ) @@ -81,10 +91,13 @@ def _pad_matrix(mat: Array, *target: int) -> Array: class ProgramSimulator: - """Base class for program simulators. + """Shared preprocessing base for program simulators. - Handles all shared preprocessing: circuit expansion, qubit ordering, - building the linearizer, resolver, and compressor closures. + Handles the pipeline common to every backend: circuit expansion, qubit + ordering, dimension inference, and building the ``linearize``/``resolve``/ + ``compress`` closures. Two family bases specialise it — :class:`_GradableSimulator` + (state-vector, density-matrix) and :class:`_TrajectorySimulator` (trajectory, + dynamic trajectory) — supplying only the execution machinery each needs. Subclasses override :meth:`_validate` and :meth:`compute`. @@ -99,6 +112,7 @@ def __init__( noise_model: NoiseModelLike | None = None, max_subsystem_size: int = 2, dims: tuple[int, ...] | None = None, + measurement: MeasurementMode = "instrument", ) -> None: self._validate(program) @@ -107,13 +121,19 @@ def __init__( self.qubits = qubits self.n_qubits = len(qubits) - # Expand the program into operators, inferring register dimensions when - # not supplied. See :func:`resolve_program`. - res = resolve_program(program, noise_model, qubits, dims=dims) + # Expand the program into operators, inferring register dimensions when not + # supplied. The measurement mode selects the resolver family: the grad-able + # simulators resolve measurements to dephasing SuperOps, the trajectory + # simulators keep them as sampled QuantumInstruments. + if measurement == "superop": + res = resolve_for_gradable(program, noise_model, qubits, dims) + else: + res = resolve_for_trajectory(program, noise_model, qubits, dims) self.dims = res.dims self._resolve_fn = res.resolve self._expanded_ops = tuple(res.ops) self._raw_subsystems = tuple(res.subsystems) + self._n_params = len(res.param_refs) param_refs = res.param_refs # Build linearizer from parameter references discovered during expansion. @@ -127,15 +147,9 @@ def linearize(memory_map: MemoryMap) -> Array: dag = build_dag(res.subsystems) - # Whether any gate matrix depends on a runtime parameter. When it does - # not, the compressed operator stack is a compile-time constant and can - # be materialised eagerly (outside the traced graph), which avoids XLA - # constant-folding/autotuning a large ``compose_operator`` subgraph — the - # dominant JIT cost on accelerators for deep, literal-angle programs. - self._has_params = bool(param_refs) - - # Derive barrier nodes: measurements (QuantumInstrument) should not - # be merged by the compressor. + # Measurements (QuantumInstrument) must not be merged by the compressor. + # Under the grad-able ("superop") mode no instruments are produced, so this + # is naturally empty and the compressor is free to merge every operation. barrier_nodes = {i for i, op in enumerate(res.ops) if isinstance(op, qx.QuantumInstrument)} self._compress_fn = compressor_from_dag( @@ -145,28 +159,6 @@ def linearize(memory_map: MemoryMap) -> Array: barrier_nodes=barrier_nodes, ) - # Enumerate the *base subsystems* produced by the compressor. The merge - # structure depends only on the DAG (not on parameter values), so a - # structural probe with zero parameters yields exactly the subsystem - # sequence that ``compress`` will produce for any parameters. The - # lax-loop ``compute`` methods dispatch each compressed operation through - # a ``jax.lax.switch`` keyed by its base, so the number of distinct bases - # (rather than the number of operations) determines the size of the - # traced/compiled graph. - probe = self._compress_fn(self._resolve_fn(jnp.zeros(len(param_refs)))) - self.bases: list[tuple[int, ...]] = [] - sub_to_branch: dict[tuple[int, ...], int] = {} - op_index: list[int] = [] - for _, subsystem in probe: - if subsystem not in sub_to_branch: - sub_to_branch[subsystem] = len(self.bases) - self.bases.append(subsystem) - op_index.append(sub_to_branch[subsystem]) - self.op_index = tuple(op_index) - self.base_dims = [tuple(self.dims[q] for q in base) for base in self.bases] - self.base_total_dim = [math.prod(d) for d in self.base_dims] - self.d_max = max(self.base_total_dim) if self.base_total_dim else 1 - # -- hook for subclass validation --------------------- def _validate(self, program: Program) -> None: @@ -199,14 +191,68 @@ def compute(self, params: Array | None = None, **kwargs: Any) -> Any: """Compute the simulation result. Subclasses must override.""" raise NotImplementedError + +# ══════════════════════════════════════════════════════════ +# Grad-able family base (state-vector / density-matrix) +# ══════════════════════════════════════════════════════════ + + +class _GradableSimulator(ProgramSimulator): + """Base for the jit/grad-friendly state-vector and density-matrix simulators. + + Adds the compressed-stack evolution machinery. It enumerates the distinct + *base subsystems* the compressor emits (:func:`enumerate_bases`) and applies the + operator stack with a :func:`jax.lax.scan` whose body dispatches each operator to + the :func:`jax.lax.switch` branch for its base (``self._branches``, keyed by + ``self._idx_arr``), so the compiled graph size scales with the number of distinct + base subsystems rather than the number of operations. + + Measurements are dephasing SuperOps (``measurement="superop"``) and the compressor + runs with no barriers, so former-measurement operators merge freely. + """ + + def __init__( + self, + program: Program, + qubits: list[int] | None = None, + *, + noise_model: NoiseModelLike | None = None, + max_subsystem_size: int = 2, + dims: tuple[int, ...] | None = None, + ) -> None: + super().__init__( + program, + qubits, + noise_model=noise_model, + max_subsystem_size=max_subsystem_size, + dims=dims, + measurement="superop", + ) + + # The merge structure depends only on the DAG (not on parameter values), so + # the base subsystems can be read straight off the compressor's emit order — + # no ``resolve``/``compress`` probe is required. + self.bases, self.op_index = enumerate_bases(self._compress_fn.emit_order) # type: ignore[attr-defined] + self.base_dims = [tuple(self.dims[q] for q in base) for base in self.bases] + self.base_total_dim = [math.prod(d) for d in self.base_dims] + self.d_max = max(self.base_total_dim) if self.base_total_dim else 1 + self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) + + # Whether any gate matrix depends on a runtime parameter. When it does not, + # the compressed operator stack is a compile-time constant and can be + # materialised eagerly (outside the traced graph), which avoids XLA + # constant-folding/autotuning a large ``compose_operator`` subgraph — the + # dominant JIT cost on accelerators for deep, literal-angle programs. + self._has_params = self._n_params > 0 + def apply(self, state: Any, op_stack: Array) -> Any: """Apply a stack of operator matrices to *state* via a scan + switch. Each operator is dispatched to the switch branch for its base subsystem - (``self._branches``, keyed by ``self._idx_arr``), so the compiled graph - size scales with the number of distinct base subsystems rather than the - number of operations. Used by the state-vector and density-matrix - simulators, which differ only in their branch and state types. + (``self._branches``, keyed by ``self._idx_arr``), so the compiled graph size + scales with the number of distinct base subsystems rather than the number of + operations. The state-vector and density-matrix simulators differ only in + their branch and state types. """ branches = self._branches # type: ignore[attr-defined] @@ -214,7 +260,7 @@ def body(state: Any, xs: tuple[Array, Array]) -> tuple[Any, None]: op_mat, sidx = xs return jax.lax.switch(sidx, branches, op_mat, state), None - state, _ = jax.lax.scan(body, state, (op_stack, self._idx_arr)) # type: ignore[attr-defined] + state, _ = jax.lax.scan(body, state, (op_stack, self._idx_arr)) return state @@ -403,7 +449,7 @@ def build(params: Array) -> Array: # ══════════════════════════════════════════════════════════ -class PureStateVectorSimulator(ProgramSimulator): +class PureStateVectorSimulator(_GradableSimulator): """Simulator for gate-only programs (no noise, measurements, or resets). All methods are jit- and grad-friendly:: @@ -452,7 +498,6 @@ def branch(op_mat: Array, psi: qx.StateVector) -> qx.StateVector: unitary_branch(base, base_dims, db) for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) ] - self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) def _validate(self, program: Program) -> None: for inst in program.instructions: @@ -517,7 +562,7 @@ def unitary(self, params: Array | None = None) -> qx.Unitary: # ══════════════════════════════════════════════════════════ -class DensityMatrixSimulator(ProgramSimulator): +class DensityMatrixSimulator(_GradableSimulator): """Density-matrix simulator for any program, optionally with noise. All methods are jit- and grad-friendly:: @@ -553,7 +598,6 @@ def branch(op_mat: Array, rho: qx.DensityMatrix) -> qx.DensityMatrix: superop_branch(base, base_dims, db * db) for base, base_dims, db in zip(self.bases, self.base_dims, self.base_total_dim, strict=True) ] - self._idx_arr = jnp.asarray(self.op_index, dtype=jnp.int32) # See :class:`PureStateVectorSimulator`: for parameter-free programs the # superoperator stack is constant, so it can be built once and reused to keep @@ -598,12 +642,53 @@ def __call__(self, params: Array | None = None) -> qx.DensityMatrix: return self.compute(params) +# ══════════════════════════════════════════════════════════ +# Trajectory family base +# ══════════════════════════════════════════════════════════ + + +class _TrajectorySimulator(ProgramSimulator): + """Base for the Monte-Carlo trajectory simulators. + + Resolves measurements as sampled ``QuantumInstrument`` operators + (``measurement="instrument"``), keeps them out of merges via compressor + barriers, and adapts the compressed operators to the trajectory-native + ``Unitary``/``KrausMap``/``QuantumInstrument`` types. Unlike the grad-able + family it builds no base-subsystem switch table — each concrete simulator + builds its own trajectory kernel. + """ + + def __init__( + self, + program: Program, + qubits: list[int] | None = None, + *, + noise_model: NoiseModelLike | None = None, + max_subsystem_size: int = 2, + kraus_truncation_threshold: float = 1e-6, + dims: tuple[int, ...] | None = None, + ) -> None: + super().__init__( + program, + qubits, + noise_model=noise_model, + max_subsystem_size=max_subsystem_size, + dims=dims, + measurement="instrument", + ) + self._kraus_truncation_threshold = kraus_truncation_threshold + + def adapt(self, compressed: list[ResolvedOp]) -> list[TrajectoryOp]: + """Convert compressed ops to trajectory-compatible types.""" + return adapt_for_trajectory(compressed, self._kraus_truncation_threshold) + + # ══════════════════════════════════════════════════════════ # Trajectory simulator # ══════════════════════════════════════════════════════════ -class TrajectorySimulator(ProgramSimulator): +class TrajectorySimulator(_TrajectorySimulator): """Monte Carlo trajectory simulator for programs with measurements and resets. The ``compute`` method requires a JAX PRNG key. The number of @@ -638,14 +723,16 @@ def __init__( devices: list[jax.Device] | None = None, dims: tuple[int, ...] | None = None, ) -> None: - super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size, dims=dims) - self._kraus_truncation_threshold = kraus_truncation_threshold + super().__init__( + program, + qubits, + noise_model=noise_model, + max_subsystem_size=max_subsystem_size, + kraus_truncation_threshold=kraus_truncation_threshold, + dims=dims, + ) self._devices = devices if devices is not None else jax.devices() - def adapt(self, compressed: list[ResolvedOp]) -> list[TrajectoryOp]: - """Convert compressed ops to trajectory-compatible types.""" - return adapt_for_trajectory(compressed, self._kraus_truncation_threshold) - def compute( # type: ignore[override] self, params: Array | None = None, @@ -700,7 +787,6 @@ def sample( _, all_outcomes = _run_batched_trajectories( operations, - self.n_qubits, num_trajectories, batch_size, random_seed, @@ -756,46 +842,51 @@ def _op_to_kraus_matrix( TrajectoryRun = Callable[[qx.StateVector, Array], tuple[qx.StateVector, Array]] +#: A ``run(op_mat, psi, key) -> (psi, sampled_index)`` switch branch for one subsystem. +KrausBranch = Callable[[Array, qx.StateVector, Array], tuple[qx.StateVector, Array]] -def _build_trajectory_kernel(operations: list[TrajectoryOp], dims: tuple[int, ...]) -> TrajectoryRun: - """Build a reusable, jitted trajectory kernel from *operations*. - - All batch-invariant work happens once, here: enumerating distinct subsystems, - building the per-subsystem switch branches, and converting every operator to a - padded Kraus stack. The returned ``run(psi, key)`` wraps the scan over that - stack in :func:`jax.jit`, so repeated calls with matching ``psi``/``key`` - shapes (e.g. one per sampling batch) reuse a single compilation instead of - re-tracing — this is what turns the previously spiky, recompile-per-batch GPU - usage into a single upfront compile. - - Every operator is expressed as a (zero-padded) Kraus map and stacked into one - array indexed by the scan; measurements are handled uniformly by flattening a - quantum instrument so that sampling a Kraus index also selects an outcome - (``index // divisor``). Zero-padded Kraus operators have zero Born - probability and are therefore never sampled. Per-operation keys are derived - lazily via ``jax.random.fold_in`` so the key array is never materialised in - full (sharding-friendly). - :param operations: Ordered list of ``(operator, subsystem)`` pairs. - :param dims: Per-qudit register dimensions (must match the ``psi`` passed to - ``run``). - :return: ``run(psi, key) -> (final_state_vector, measurement_outcomes)`` where - ``measurement_outcomes`` has shape ``(*ensemble, n_measurements)``, - dtype int32. ``key`` is a scalar PRNG key or a per-trajectory key vector. +@dataclass +class _KrausOpStack: + """The batch-invariant, scan-ready form of a trajectory operation sequence. + + Every operation is expressed as a zero-padded Kraus matrix so the scan can index + a single homogeneous stack (quax has no ragged/heterogeneous KrausMap stacking; + operators live on different subsystems with different Kraus counts). The padding + to a uniform ``(max_k, d_max, d_max)`` is inherent to that single-``lax.scan`` + design — zero-padded Kraus operators carry zero Born probability and are simply + never sampled — and each :data:`KrausBranch` re-slices ``[:, :db, :db]`` back to + its subsystem before rebuilding a ``KrausMap``. """ - if not operations: - def run_empty(psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: - return psi, jnp.empty((*psi.ensemble_size, 0), dtype=jnp.int32) + #: ``(n_ops, max_k, d_max, d_max)`` padded Kraus matrices, in application order. + op_stack: Array + #: ``(n_ops,)`` int32 — the :attr:`branches` index for each operation. + branch_arr: Array + #: One switch branch per distinct subsystem (Kraus trajectory sampling). + branches: list[KrausBranch] + #: Per-operation Kraus-count divisor; measurement outcome = ``sampled_index // divisor``. + divisors: list[int] + #: Indices (into the op sequence) of the measurement operations, in program order. + measure_positions: list[int] + + @property + def n_ops(self) -> int: + return len(self.divisors) - return run_empty - # 1. Enumerate distinct subsystems → one switch branch each. A branch is - # handed a padded matrix (not a KrausMap) because the scan must index a - # single homogeneous op stack: operators live on different subsystems with - # different Kraus counts and so cannot be stacked as one KrausMap, whose - # dims are static per-branch metadata. Each branch rebuilds its KrausMap - # from the slice, using the base dims it closes over. +def _build_kraus_op_stack(operations: list[TrajectoryOp], dims: tuple[int, ...]) -> _KrausOpStack: + """Convert a trajectory operation sequence into a scan-ready :class:`_KrausOpStack`. + + This is all the batch-invariant work: enumerating distinct subsystems (one switch + branch each), promoting each operator to its register dimension, converting it to a + Kraus matrix (:func:`_op_to_kraus_matrix`), and padding everything to one uniform + stack. Separated from :func:`_build_trajectory_kernel` so the kernel itself only + holds the scan / key-folding / outcome-decoding logic. + + :param operations: Ordered list of ``(operator, subsystem)`` pairs. + :param dims: Per-qudit register dimensions (must match the ``psi`` passed to the kernel). + """ distinct_subsystems: list[tuple[int, ...]] = [] sub_to_branch: dict[tuple[int, ...], int] = {} for _, subsystem in operations: @@ -803,7 +894,7 @@ def run_empty(psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: sub_to_branch[subsystem] = len(distinct_subsystems) distinct_subsystems.append(subsystem) - def make_branch(base: tuple[int, ...]) -> Callable[[Array, qx.StateVector, Array], tuple[qx.StateVector, Array]]: + def make_branch(base: tuple[int, ...]) -> KrausBranch: base_dims = tuple(dims[q] for q in base) db = math.prod(base_dims) @@ -815,7 +906,6 @@ def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVect branches = [make_branch(subsystem) for subsystem in distinct_subsystems] - # 2. Convert every operator to a padded Kraus matrix and stack. kraus_mats: list[Array] = [] divisors: list[int] = [] measure_positions: list[int] = [] @@ -839,8 +929,51 @@ def branch(op_mat: Array, psi: qx.StateVector, key: Array) -> tuple[qx.StateVect max_k = max(mat.shape[0] for mat in kraus_mats) d_max = max(mat.shape[-1] for mat in kraus_mats) op_stack = jnp.stack([_pad_matrix(mat, max_k, d_max, d_max) for mat in kraus_mats], axis=0) - branch_arr = jnp.asarray(branch_index, dtype=jnp.int32) - op_indices = jnp.arange(len(operations), dtype=jnp.int32) + return _KrausOpStack( + op_stack=op_stack, + branch_arr=jnp.asarray(branch_index, dtype=jnp.int32), + branches=branches, + divisors=divisors, + measure_positions=measure_positions, + ) + + +def _build_trajectory_kernel(operations: list[TrajectoryOp], dims: tuple[int, ...]) -> TrajectoryRun: + """Build a reusable, jitted trajectory kernel from *operations*. + + All batch-invariant work happens once in :func:`_build_kraus_op_stack`. The + returned ``run(psi, key)`` wraps the scan over that padded Kraus stack in + :func:`jax.jit`, so repeated calls with matching ``psi``/``key`` shapes (e.g. one + per sampling batch) reuse a single compilation instead of re-tracing — this is + what turns the previously spiky, recompile-per-batch GPU usage into a single + upfront compile. + + Measurements are handled uniformly by flattening a quantum instrument so that + sampling a Kraus index also selects an outcome (``index // divisor``). + Per-operation keys are derived lazily via ``jax.random.fold_in`` so the key array + is never materialised in full (sharding-friendly). + + :param operations: Ordered list of ``(operator, subsystem)`` pairs. + :param dims: Per-qudit register dimensions (must match the ``psi`` passed to + ``run``). + :return: ``run(psi, key) -> (final_state_vector, measurement_outcomes)`` where + ``measurement_outcomes`` has shape ``(*ensemble, n_measurements)``, + dtype int32. ``key`` is a scalar PRNG key or a per-trajectory key vector. + """ + if not operations: + + def run_empty(psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: + return psi, jnp.empty((*psi.ensemble_size, 0), dtype=jnp.int32) + + return run_empty + + stack = _build_kraus_op_stack(operations, dims) + branches = stack.branches + op_stack = stack.op_stack + branch_arr = stack.branch_arr + divisors = stack.divisors + measure_positions = stack.measure_positions + op_indices = jnp.arange(stack.n_ops, dtype=jnp.int32) @jax.jit def run(psi: qx.StateVector, key: Array) -> tuple[qx.StateVector, Array]: @@ -900,12 +1033,12 @@ def _round_up_to(n: int, divisor: int) -> int: def _run_batched_trajectories( operations: list[TrajectoryOp], - n_qubits: int, num_trajectories: int, batch_size: int, random_seed: int, keep_states: bool = True, - dims: tuple[int, ...] | None = None, + *, + dims: tuple[int, ...], devices: list[jax.Device] | None = None, ) -> tuple[list[qx.StateVector] | None, list[Array]]: """Run trajectory simulation in batches, optionally sharded across devices. @@ -919,10 +1052,9 @@ def _run_batched_trajectories( once, reused across batches) so the GPU sees one upfront compile rather than a recompile spike per batch; the final short batch is padded up to that width and its extra rows are sliced off. - """ - if dims is None: - dims = (2,) * n_qubits + :param dims: Per-qudit register dimensions of the simulated system. + """ mesh = _make_mesh(devices) n_devices = len(mesh.devices.flat) if mesh is not None else 1 sharding = NamedSharding(mesh, PartitionSpec("traj")) if mesh is not None else None # type: ignore[no-untyped-call] @@ -1033,7 +1165,7 @@ def _dyn_apply( return psi, outcome -class DynamicTrajectorySimulator(ProgramSimulator): +class DynamicTrajectorySimulator(_TrajectorySimulator): """Single-trajectory simulator with dynamically-sized qudit dimensions. Targets the **largest** leakage-aware registers. Where the other simulators @@ -1072,8 +1204,13 @@ def __init__( kraus_truncation_threshold: float = 1e-6, squeeze_tol: float = 1e-9, ) -> None: - super().__init__(program, qubits, noise_model=noise_model, max_subsystem_size=max_subsystem_size) - self._kraus_truncation_threshold = kraus_truncation_threshold + super().__init__( + program, + qubits, + noise_model=noise_model, + max_subsystem_size=max_subsystem_size, + kraus_truncation_threshold=kraus_truncation_threshold, + ) self._squeeze_tol = squeeze_tol def _validate(self, program: Program) -> None: @@ -1095,9 +1232,7 @@ def compute( # type: ignore[override] """ if key is None: raise ValueError("DynamicTrajectorySimulator.compute requires a JAX PRNG key.") - operations = adapt_for_trajectory( - self.compress(self.resolve(self._default_params(params))), self._kraus_truncation_threshold - ) + operations = self.adapt(self.compress(self.resolve(self._default_params(params)))) psi = qx.zero_state_vector(dims=(2,) * self.n_qubits) outcomes: list[Array] = [] diff --git a/test/unit/test_resolver.py b/test/unit/test_resolver.py index cfc436bc4..e6e8bfacc 100644 --- a/test/unit/test_resolver.py +++ b/test/unit/test_resolver.py @@ -91,6 +91,28 @@ def test_cycle_channel_expansion(self): for op in ops: assert isinstance(op, qx.SuperOp) + def test_cycle_channel_applies_process_faithfully(self): + """A ``CycleChannel`` constituent is simulated as its ``process`` verbatim. + + ``Channel.process`` is the source of truth for a cycle constituent: it must + carry the gate (composed with any noise). This pins that contract — a + noiseless gate channel's ``process`` is emitted unchanged and equals the gate + superoperator — so a noise model that puts an identity in ``process`` for a + real gate (which silently drops it) is a *noise-model* error, caught at the + point of construction, not something the resolver second-guesses. + """ + q0 = FormalArgument("q") + dc = DefCircuit("VCYC", [], [q0], [X(q0)]) + cycle_inst = Gate("VCYC", [], [Qubit(0)]) + # Noiseless X channel: the gate lives in the process (target_unitary == process action). + gate_channel = Channel(X(0), qx.to_superop(qx.gates.X), target_unitary=qx.gates.X) + nm = NoiseModel.from_channels([CycleChannel(inst=cycle_inst, defcircuit=dc, channels=(gate_channel,))]) + p = Program(dc, cycle_inst) + ops, _, _ = expand_program(p, nm) + assert len(ops) == 1 + # Emitted verbatim: the process (an X superoperator) is what gets applied. + assert jnp.allclose(qx.to_superop(ops[0]).matrix, qx.to_superop(qx.gates.X).matrix, atol=1e-6) + def test_parameterized_gate_produces_callable(self): p = Program(Declare("theta", "REAL", 1), RZ(MemoryReference("theta", 0), 0)) ops, _, _ = expand_program(p) diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index 885845e8d..7b3f4df42 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -55,7 +55,6 @@ def _simulate_trajectories(program, noise_model=None, qubits=None, num_trajector operations = sim.adapt(compressed) all_psis, all_outcomes = _run_batched_trajectories( operations, - sim.n_qubits, num_trajectories, batch_size, random_seed, @@ -319,6 +318,31 @@ def test_two_qubit_noise(self): assert abs(avg_prob_11 - expected_prob_11) < 0.05 +class TestCycleChannelGateApplication: + """A ``CycleChannel`` gate constituent is applied as its ``process``. + + ``Channel.process`` carries the gate (composed with any noise); the resolver + expands a cycle by applying each constituent's ``process`` verbatim. This + guards, end to end, that a cycle gate whose unitary lives in its ``process`` + actually acts on the state — the surface-code kraus/stim mismatch was caused by + a noise model that put an identity in ``process`` for a real gate, dropping it, + which is a noise-model error rather than a simulator one. + """ + + def test_trajectory_applies_cycle_gate(self): + """The cycle's ``X`` (carried in the channel's process) flips the qubit.""" + q = FormalArgument("q") + dc = DefCircuit("CX", [], [q], [X(q)]) + cyc = QuilGate("CX", [], [Qubit(0)]) + program = Program(dc, cyc, Declare("ro", "BIT", 1), QuilMeasurement(Qubit(0), MemoryReference("ro", 0))) + gate_channel = Channel(X(0), qx.to_superop(qx.gates.X), target_unitary=qx.gates.X) + nm = NoiseModel.from_channels([CycleChannel(inst=cyc, defcircuit=dc, channels=(gate_channel,))]) + sim = TrajectorySimulator(program, noise_model=nm, qubits=[0]) + outcomes = np.asarray(sim.sample(sim.linearize({}), num_trajectories=16, batch_size=16, random_seed=0)) + assert outcomes.shape == (16, 1) + assert np.all(outcomes == 1) + + class TestTrajectoryMeasurement: """Test mid-circuit measurement in trajectory simulation.""" @@ -1146,7 +1170,6 @@ def test_batched_trajectories_with_devices(self): _, outcomes = _run_batched_trajectories( operations, - sim.n_qubits, num_trajectories=20, batch_size=8, random_seed=42, From 74d8c58f41871854d62bc449b9aabe01d4676fb9 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Thu, 2 Jul 2026 14:59:31 +0000 Subject: [PATCH 35/37] Fix lint and type errors in CycleChannel validation Satisfy CI's ruff (0.4.10) and mypy checks for the CycleChannel `__post_init__`/`expanded_instructions` code: - add explicit `strict=True` to the body/channels `zip` (B905); lengths are validated equal immediately above - annotate `instructions` as `list[Gate | Measurement | ResetQubit]` (fixes the invariant-list return-value error) - correct the `# type: ignore` codes from `[arg-type]` to `[index]` (the actual error is the dict index type) and fix the two-space formatting before the comment Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01WGJX5EQDKdok4ELkRmowT5 --- pyquil/noise/_channels.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyquil/noise/_channels.py b/pyquil/noise/_channels.py index 2aefe1d61..597057c67 100644 --- a/pyquil/noise/_channels.py +++ b/pyquil/noise/_channels.py @@ -1485,7 +1485,7 @@ def __post_init__(self) -> None: f"\nDefCircuit body: {self.expanded_instructions}" f"\nChannels: {self.channels}" ) - for instruction, channel in zip(self.expanded_instructions, self.channels): + for instruction, channel in zip(self.expanded_instructions, self.channels, strict=True): if str(instruction) != str(channel.inst): raise ValueError( "CycleChannel is incomplete: every instruction in the cycle's DefCircuit " @@ -1502,15 +1502,15 @@ def __post_init__(self) -> None: def expanded_instructions(self) -> list[Gate | Measurement | ResetQubit]: """Return the expanded instructions of the defcircuit.""" qarg_to_qubit = dict(zip(self.defcircuit.qubit_variables, self.inst.get_qubit_indices(), strict=False)) - instructions = [] + instructions: list[Gate | Measurement | ResetQubit] = [] for inst in self.defcircuit.instructions: match inst: case Measurement(): - instructions.append(Measurement(qubit=qarg_to_qubit[inst.qubit], classical_reg=inst.classical_reg)) # type: ignore[arg-type] + instructions.append(Measurement(qubit=qarg_to_qubit[inst.qubit], classical_reg=inst.classical_reg)) # type: ignore[index] case ResetQubit(): - instructions.append(ResetQubit(qarg_to_qubit[inst.qubit])) # type: ignore[arg-type] + instructions.append(ResetQubit(qarg_to_qubit[inst.qubit])) # type: ignore[index] case Gate(): - instructions.append(Gate(inst.name, inst.params, [qarg_to_qubit[q] for q in inst.qubits])) # type: ignore[arg-type] + instructions.append(Gate(inst.name, inst.params, [qarg_to_qubit[q] for q in inst.qubits])) # type: ignore[index] case _: raise TypeError(f"Unsupported instruction type in defcircuit: {type(inst).__name__}") return instructions From 372947fdbf90815144293f5b529965599054150f Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Fri, 3 Jul 2026 21:44:04 +0000 Subject: [PATCH 36/37] Fix sharding --- docs/source/simulation_architecture.rst | 7 +- pyquil/simulation/_simulator.py | 120 +++++++++++------------- test/unit/test_state_vector.py | 27 +----- 3 files changed, 60 insertions(+), 94 deletions(-) diff --git a/docs/source/simulation_architecture.rst b/docs/source/simulation_architecture.rst index 777c2434c..056e96a22 100644 --- a/docs/source/simulation_architecture.rst +++ b/docs/source/simulation_architecture.rst @@ -413,8 +413,11 @@ selects the outcome. ``sample`` runs trajectories in fixed-size batches, discarding state vectors between batches so the total number of shots is unbounded by memory. When -multiple JAX devices are available, each batch is sharded across them along the -trajectory axis. +multiple JAX devices are available, each batch is run data-parallel via +:func:`jax.pmap` — one independent kernel replica per device, with no +cross-device communication. In that case ``batch_size`` is interpreted **per +device**, so ``n`` devices run ``n * batch_size`` trajectories per batch and +each device's memory footprint matches a single-device run. Choosing a simulator diff --git a/pyquil/simulation/_simulator.py b/pyquil/simulation/_simulator.py index 0dbeb3793..0c2c6e7a5 100644 --- a/pyquil/simulation/_simulator.py +++ b/pyquil/simulation/_simulator.py @@ -49,7 +49,6 @@ import numpy as np import quax as qx from jax import Array -from jax.sharding import Mesh, NamedSharding, PartitionSpec from quax._apply import _sample_kraus_map_trajectory from pyquil.api import MemoryMap @@ -772,14 +771,16 @@ def sample( State vectors are discarded after each batch, making this scalable to arbitrarily many trajectories. When multiple devices are - available, each batch is sharded across them so that every device - processes ``batch_size // n_devices`` trajectories concurrently. + available the batch is run **data-parallel** via :func:`jax.pmap`: each + device runs an independent replica of the trajectory kernel on its own + slice of trajectories, with no cross-device communication. :param params: Flat parameter vector from :meth:`linearize`. Omit (or pass ``None``) for a parameter-free program. :param num_trajectories: Total number of trajectories to simulate. - :param batch_size: Maximum number of trajectories per batch - (total across all devices). + :param batch_size: Trajectories per device per batch. With ``n`` devices + each batch runs ``n * batch_size`` trajectories concurrently; on a + single device this is simply the batch size. :param random_seed: Seed for the JAX PRNG. :return: Measurement outcomes with shape ``(num_trajectories, n_measurements)``. """ @@ -1017,20 +1018,6 @@ def _apply_trajectory_operations( return _build_trajectory_kernel(operations, psi.dims)(psi, key) -def _make_mesh(devices: list[jax.Device] | None) -> Mesh | None: - """Build a 1-D ``Mesh`` over *devices*, or ``None`` for single-device.""" - if devices is None: - devices = jax.devices() - if len(devices) <= 1: - return None - return Mesh(np.array(devices), axis_names=("traj",)) - - -def _round_up_to(n: int, divisor: int) -> int: - """Round *n* up to the nearest multiple of *divisor*.""" - return ((n + divisor - 1) // divisor) * divisor - - def _run_batched_trajectories( operations: list[TrajectoryOp], num_trajectories: int, @@ -1041,31 +1028,44 @@ def _run_batched_trajectories( dims: tuple[int, ...], devices: list[jax.Device] | None = None, ) -> tuple[list[qx.StateVector] | None, list[Array]]: - """Run trajectory simulation in batches, optionally sharded across devices. - - When *devices* contains more than one device a :class:`jax.sharding.Mesh` - is constructed and both the initial state vector and PRNG keys are sharded - along the trajectory (ensemble) axis. XLA's SPMD partitioner then - distributes the work so that each device processes its own slice. - - Every batch runs at the same width (:func:`_build_trajectory_kernel` compiled - once, reused across batches) so the GPU sees one upfront compile rather than a - recompile spike per batch; the final short batch is padded up to that width - and its extra rows are sliced off. + """Run trajectory simulation in batches, data-parallel across devices. + + When *devices* contains more than one device the batch is run **data-parallel** + via :func:`jax.pmap`: each device runs an independent replica of the trajectory + kernel on its own slice of trajectories. Trajectories are statistically + independent, so no cross-device communication is required — per-device memory + equals a single-device run and the broken-collectives / all-gather failure modes + of SPMD sharding are avoided entirely. + + ``batch_size`` is interpreted **per device**: with ``n`` devices each call runs + ``n * batch_size`` trajectories. Every call runs at the same width + (:func:`_build_trajectory_kernel` compiled once, wrapped in a single ``pmap``, + reused across calls) so the GPU sees one upfront compile rather than a recompile + spike per batch; the final short call is padded up to that width and its extra + rows are sliced off. + + Each device's zero state is built *inside* the mapped function so the full + ``(n_devices, per_device, hilbert)`` array is never allocated on one device. + When ``keep_states`` is false only the measurement outcomes are returned from the + mapped function, so the large final state vectors are freeable intermediates and + are never gathered back to the host. :param dims: Per-qudit register dimensions of the simulated system. """ - mesh = _make_mesh(devices) - n_devices = len(mesh.devices.flat) if mesh is not None else 1 - sharding = NamedSharding(mesh, PartitionSpec("traj")) if mesh is not None else None # type: ignore[no-untyped-call] + devices = devices if devices is not None else jax.devices() + n_devices = len(devices) + per_device = batch_size + per_call = n_devices * per_device - # Build the jitted trajectory kernel once; reuse it for every batch. - run = _build_trajectory_kernel(operations, dims) + kernel = _build_trajectory_kernel(operations, dims) - # Uniform per-batch width: full batches and the padded tail all share it, so - # the kernel compiles exactly once. Capped at the total so single-batch runs - # don't pad past what's asked for; rounded to n_devices for even sharding. - batch_width = _round_up_to(min(num_trajectories, batch_size), n_devices) + def run_replica(device_keys: Array) -> Any: + # Build only this device's slice; keeps per-device memory at a single-device run. + psi = qx.zero_state_vector(dims=dims, ensemble_size=(per_device,)) + psi_out, outcomes = kernel(psi, device_keys) + return (psi_out, outcomes) if keep_states else outcomes + + pkernel = jax.pmap(run_replica, devices=devices) key = jax.random.key(random_seed) all_psis: list[qx.StateVector] = [] @@ -1073,37 +1073,23 @@ def _run_batched_trajectories( remaining = num_trajectories while remaining > 0: - this_batch = min(remaining, batch_size) + this_call = min(remaining, per_call) key, batch_key = jax.random.split(key) - - if batch_width == 1: - psi = qx.zero_state_vector(dims=dims) - batch_keys = batch_key - else: - psi = qx.zero_state_vector(dims=dims, ensemble_size=(batch_width,)) - batch_keys = batch_key - - # Shard state and key across devices when a mesh is available. - if sharding is not None: - psi = qx.StateVector.from_matrix(jax.device_put(psi.matrix, sharding), psi.dims) - batch_keys = jax.device_put(jax.random.split(batch_key, batch_width), sharding) - - psi_out, outcomes = run(psi, batch_keys) - psi_out.matrix.block_until_ready() - - # Strip padding rows down to this batch's real trajectory count. - if batch_width > 1 and this_batch < batch_width: - psi_out = qx.StateVector.from_matrix(psi_out.matrix[:this_batch], psi_out.dims) - outcomes = outcomes[:this_batch] - - if this_batch == 1 and batch_width == 1: - psi_out = qx.StateVector.from_matrix(psi_out.matrix[jnp.newaxis], psi_out.dims) - outcomes = outcomes[jnp.newaxis] + # Fixed width every call (tail padded, extra rows sliced) → compile once. + batch_keys = jax.random.split(batch_key, per_call).reshape(n_devices, per_device) + + result = pkernel(batch_keys) + outcomes = result[1] if keep_states else result + # pmap re-adds the leading device axis: (n_devices, per_device, n_meas). + # Flatten it back to a 1-D ensemble to preserve the return contract. + outcomes = outcomes.reshape(per_call, -1)[:this_call] + all_outcomes.append(outcomes) if keep_states: - all_psis.append(psi_out) - all_outcomes.append(outcomes) - remaining -= this_batch + mat = result[0].matrix.reshape(per_call, -1)[:this_call] + all_psis.append(qx.StateVector.from_matrix(mat, dims)) + + remaining -= this_call return (all_psis if keep_states else None), all_outcomes diff --git a/test/unit/test_state_vector.py b/test/unit/test_state_vector.py index 7b3f4df42..ace424401 100644 --- a/test/unit/test_state_vector.py +++ b/test/unit/test_state_vector.py @@ -27,8 +27,6 @@ PureStateVectorSimulator, TrajectorySimulator, _run_batched_trajectories, - _make_mesh, - _round_up_to, ) from pyquil.simulation._simulator import ( _apply_trajectory_operations as apply_trajectory_operations, @@ -1103,33 +1101,12 @@ def test_random_circuit_compression_summary(self, capsys): # ────────────────────────────────────────────────────────────────────────────── -class TestMultiDeviceHelpers: - def test_round_up_to(self): - assert _round_up_to(7, 4) == 8 - assert _round_up_to(8, 4) == 8 - assert _round_up_to(1, 3) == 3 - assert _round_up_to(0, 5) == 0 - - def test_make_mesh_single_device_returns_none(self): - """A single device should return None (no mesh needed).""" - devices = jax.devices()[:1] - assert _make_mesh(devices) is None - - def test_make_mesh_none_uses_default(self): - """Passing None should query jax.devices().""" - mesh = _make_mesh(None) - if len(jax.devices()) <= 1: - assert mesh is None - else: - assert mesh is not None - - class TestMultiDeviceTrajectory: """Tests that exercise the multi-device code paths. On a single-CPU host these still validate the padding/unpadding logic - and the ``devices`` parameter plumbing. On a multi-GPU host they - exercise real cross-device sharding. + and the ``devices`` parameter plumbing. On a multi-device host they + exercise real data-parallel ``jax.pmap`` execution (one replica per device). """ def test_devices_parameter_accepted(self): From ecd9cffa7c758747f54e9d7b4942aad630069286 Mon Sep 17 00:00:00 2001 From: Bram Evert Date: Sat, 4 Jul 2026 08:11:50 +0000 Subject: [PATCH 37/37] Dump to rc3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a77714b7c..4d9342571 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyquil" -version = "4.19.0-rc.2" +version = "4.19.0-rc.3" description = "A Python library for creating Quantum Instruction Language (Quil) programs." authors = ["Rigetti Computing "] readme = "README.md"