diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/README.md b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/README.md new file mode 100644 index 0000000..e0987bf --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/README.md @@ -0,0 +1,25 @@ +# Bivariate Bicycle Quantum Code Discovery + +This challenge adapts the original CSS bivariate-bicycle code-discovery fitness from [qiskit-community/qcode-discovery](https://github.com/qiskit-community/qcode-discovery) at commit `4e828d0bc74066df9484e80f751a52674af7251f`. + +The source project accompanied IBM Research's [AI for quantum error correction](https://research.ibm.com/blog/ai-for-qec) work and the paper [Discovering Quantum Error Correction Codes with AI](https://arxiv.org/abs/2606.02418). The adapted upstream evaluator code remains available under the Apache License 2.0. + +## Challenge Shape + +Submitted projects implement a generator for CSS bivariate-bicycle polynomial pairs. Agentics invokes the generator separately for each published lattice and asks it to write an ordered JSON candidate list. The trusted separated-evaluator reconstructs and scores every selected code; it never imports or executes participant source code. + +Public validation reproduces the source evaluator's two-lattice, k-only cascade screen. Official evaluation reproduces the original eight-lattice BP-OSD fitness and averages three independent full passes to reduce leaderboard noise. + +At the pinned commit, the executable stage-2 adapter wires `refine_trials=1000` into the initial BP-OSD `quick_trials` argument and leaves `evaluate_candidate`'s refinement default at 500. Each selected candidate therefore receives a 1,000-trial initial estimate and, when its preliminary FOM reaches 6, three 500-trial refinements. This challenge preserves that executable behavior even though nearby source prose describes the trial counts differently. + +No private benchmark data is used. The lattices, evaluator, and scoring formula are public by design, as they were during the original evolutionary campaign. Published candidates are legitimate baselines; the goal is to improve the generator rather than demonstrate hidden-case generalization. + +## Scientific Limitation + +BP-OSD returns stochastic upper bounds on code distance. The source evaluator's `d / sqrt(n)` trust filter is heuristic, and a high challenge score is not proof of a code's exact distance, novelty, or inequivalence to known codes. Agentics reports a mean and spread across three source-faithful passes, but that wrapper does not turn the estimates into certificates. + +The source file comments mention an OSD-CS verification step, but the pinned default stage-2 path sets `fom_threshold_exact` to infinity and therefore runs neither OSD-CS nor exact distance. This challenge follows the executable source behavior. A future MILP-backed port must use a new challenge handle, proposed as `milp-verified-bivariate-bicycle-code-discovery-qcode-discovery`, rather than changing this immutable ranking contract. + +## Provenance And License + +The vendored upstream files and baseline retain their Apache-2.0 license and attribution. See `v1/separated-evaluator/vendor/qcode-discovery/UPSTREAM.md` and `v1/separated-evaluator/vendor/qcode-discovery/LICENSE`. diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/agentics.challenge.json b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/agentics.challenge.json new file mode 100644 index 0000000..339335d --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/agentics.challenge.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "request": "new_challenge", + "challenge_name": "bivariate-bicycle-code-discovery-qcode-discovery", + "title": "Bivariate Bicycle Quantum Code Discovery", + "summary": { + "en": "Design generators for bivariate-bicycle quantum LDPC codes and maximize IBM's trust-filtered aggregate figure of merit.", + "zh": "为双变量自行车量子 LDPC 码设计候选生成器,并最大化 IBM 的可信度过滤聚合品质因数。" + }, + "keywords": [ + "quantum error correction", + "qLDPC", + "code discovery", + "optimization" + ], + "readme_path": "README.md", + "bundle_path": "v1", + "private_assets": [], + "ci": { + "validate_manifest": true, + "validate_public_bundle": true, + "smoke_test_public_validation": false + } +} diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/public/lattices.json b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/public/lattices.json new file mode 100644 index 0000000..b085a86 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/public/lattices.json @@ -0,0 +1,19 @@ +{ + "schema_version": 1, + "max_candidates": 5000, + "official_repetitions": 3, + "validation_lattices": [ + [6, 6], + [12, 6] + ], + "official_lattices": [ + [12, 6], + [6, 12], + [12, 12], + [24, 6], + [15, 12], + [30, 6], + [16, 9], + [18, 8] + ] +} diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/resources/evaluator-env/pyproject.toml b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/resources/evaluator-env/pyproject.toml new file mode 100644 index 0000000..6558baf --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/resources/evaluator-env/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "agentics-qcode-discovery-evaluator" +version = "0.1.0" +requires-python = ">=3.12,<3.13" +dependencies = [ + "galois==0.4.10", + "ldpc==2.4.1", + "numpy==2.3.5", + "qldpc==0.2.6", + "scipy==1.17.0", + "sympy==1.14.0", +] + +[tool.uv] +package = false diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/resources/evaluator-env/uv.lock b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/resources/evaluator-env/uv.lock new file mode 100644 index 0000000..a725aed --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/resources/evaluator-env/uv.lock @@ -0,0 +1,689 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[[package]] +name = "agentics-qcode-discovery-evaluator" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "galois" }, + { name = "ldpc" }, + { name = "numpy" }, + { name = "qldpc" }, + { name = "scipy" }, + { name = "sympy" }, +] + +[package.metadata] +requires-dist = [ + { name = "galois", specifier = "==0.4.10" }, + { name = "ldpc", specifier = "==2.4.1" }, + { name = "numpy", specifier = "==2.3.5" }, + { name = "qldpc", specifier = "==0.2.6" }, + { name = "scipy", specifier = "==1.17.0" }, + { name = "sympy", specifier = "==1.14.0" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, +] + +[[package]] +name = "clarabel" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/e2/47f692161779dbd98876015de934943effb667a014e6f79a6d746b3e4c2a/clarabel-0.11.1.tar.gz", hash = "sha256:e7c41c47f0e59aeab99aefff9e58af4a8753ee5269bbeecbd5526fc6f41b9598", size = 253949, upload-time = "2025-06-11T16:49:05.864Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/f7/f82698b6d00a40a80c67e9a32b2628886aadfaf7f7b32daa12a463e44571/clarabel-0.11.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c39160e4222040f051f2a0598691c4f9126b4d17f5b9e7678f76c71d611e12d8", size = 1039511, upload-time = "2025-06-11T16:48:58.525Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8f/13650cfe25762b51175c677330e6471d5d2c5851a6fbd6df77f0681bb34e/clarabel-0.11.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8963687ee250d27310d139eea5a6816f9c3ae31f33691b56579ca4f0f0b64b63", size = 935135, upload-time = "2025-06-11T16:48:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/7af10d2b540b39f1a05d1ebba604fce933cc9bc0e65e88ec3b7a84976425/clarabel-0.11.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4837b9d0db01e98239f04b1e3526a6cf568529d3c19a8b3f591befdc467f9bb", size = 1079226, upload-time = "2025-06-11T16:49:00.987Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a9/c76edf781ca3283186ff4b54a9a4fb51367fd04313a68e2b09f062407439/clarabel-0.11.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8c41aaa6f3f8c0f3bd9d86c3e568dcaee079562c075bd2ec9fb3a80287380ef", size = 1164345, upload-time = "2025-06-11T16:49:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/4eee3062088c221e5a18b054e51c69f616e0bb0dc1b0a1a5e0fe90dfa18e/clarabel-0.11.1-cp39-abi3-win_amd64.whl", hash = "sha256:557d5148a4377ae1980b65d00605ae870a8f34f95f0f6a41e04aa6d3edf67148", size = 887310, upload-time = "2025-06-11T16:49:04.277Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, +] + +[[package]] +name = "cvxpy" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clarabel" }, + { name = "highspy" }, + { name = "numpy" }, + { name = "osqp" }, + { name = "qdldl" }, + { name = "scipy" }, + { name = "scs" }, + { name = "sparsediffpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/b7/209c6df38f3621fc2f32298c93e7d0310b8a1ee4ef15e5fa59d90492fa6f/cvxpy-1.9.2.tar.gz", hash = "sha256:b2e939f197a7081a300d5a95812fec8643fabaf23a149abf7e67ca7f89671d92", size = 1916772, upload-time = "2026-06-22T04:37:31.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/43/929526a0801cdd56fbb8350f2200cec49739344d12f310f0aafea1f3506e/cvxpy-1.9.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc9082110ac7f9d9a121f4dea73936f9b65ec043af91562677478edf4962905b", size = 1635177, upload-time = "2026-06-22T04:32:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0b/3be49d69c0a90e22572b5ed1c568d4f2357f14f013597a4314f4ea98a2da/cvxpy-1.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba59da053c4d15fffa54921dc4b8b929d5a11937692703133858c2be6329107b", size = 1425036, upload-time = "2026-06-22T04:32:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/80e2ec67a9588cf6e2b99f3e528c4508457cba16c5f5a2a8195c255d043e/cvxpy-1.9.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa6e86a6ecd19a63751b470a3ed44d5fba6663ab9e05d6040bb954e80a4f7551", size = 4379465, upload-time = "2026-06-22T04:37:29.121Z" }, + { url = "https://files.pythonhosted.org/packages/56/6b/f0f804c7b626917edfe1801baddb85076cdb2fc0e52ed49ff112db82f980/cvxpy-1.9.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:074897ae8f2378174099c082feef5a93b44f4c8dc9b3e960397c31fa3cebf722", size = 4427785, upload-time = "2026-06-22T04:37:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/54/81871ee6599d33e3a60907829aafff78e713e1c3a34ce57d1e4ef26fdb5d/cvxpy-1.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:1cc56d04515201a3e2f58a16ea59812a0126dd1419f0726f8322e6dc227a9062", size = 1386030, upload-time = "2026-06-22T04:35:23.856Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "galois" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/54/ede3562049f1bb7f9546cbd86af8794b49733a835b0d1c40fedfbbcdbcc5/galois-0.4.10.tar.gz", hash = "sha256:94a371c995193a66798fceb079320dff154c21a0e3e0de388c06ba26f8eb574a", size = 7401288, upload-time = "2026-01-02T02:59:04.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/16/91e376aa4cad58203448510c907c267cce524d1fd2dd7f9f875f75b8584e/galois-0.4.10-py3-none-any.whl", hash = "sha256:9badf669915c92d9f298c7799cadad112baa21f53d816f427f55c4ec6ee9a2dc", size = 4196886, upload-time = "2026-01-02T02:59:02.332Z" }, +] + +[[package]] +name = "highspy" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/02/c6b658f79911fee921721da728b9ab8f5e19ff06121fff36f90f77127f4d/highspy-1.15.1.tar.gz", hash = "sha256:20ed2fbf1cb64bf3044ee6632364b7e2653d93e6901e2b19fd3d5df10702e8c5", size = 1703256, upload-time = "2026-07-02T12:03:25.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/59/b79a7b1711ddfcca36674ddb41759e98eb1797f4a94513e7dd215e32e94d/highspy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a781dc8432568ea990fcdcc8d6e4365e67aa4848ca1f99275db096645b27cae3", size = 4878738, upload-time = "2026-07-02T12:02:03.82Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e4/ae08124f71187628471a177e6db1ed2c1c45e9dceadc45f7111dfd7c2254/highspy-1.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9499d631edeb9642fc08dee59ca6c5815be1764c13a336c58ab7ba063011aa24", size = 4473938, upload-time = "2026-07-02T12:02:05.754Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/185b8c9579a9e4ef88eda45d1fdaf8d23a3a640f73c403a7b29fc0f0c4be/highspy-1.15.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef048fa722cdeb80062d271b8ba211cd6650ab73419762d80da7642bbd4a8420", size = 4636755, upload-time = "2026-07-02T12:02:07.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/6b/18bec60d8585df860b8d33d310e99e7893eaabe3c8e9ebfa7e387ba9d2a4/highspy-1.15.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9730647160a6481426729f46d9989a0507d05f3cf96f9fb180f4ab9891bea67b", size = 5034168, upload-time = "2026-07-02T12:02:09.89Z" }, + { url = "https://files.pythonhosted.org/packages/d4/51/e43f06e64e994ccb41a336ff78802c0dae63aed46c17acd52167b5ca3d76/highspy-1.15.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6a6a2f21ee31a9205a928fbbc3f8c054893c1aec34f6a7c56588317e2800e673", size = 5861937, upload-time = "2026-07-02T12:02:11.801Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2a/5501a23cac55926e4b0554352b4285734b417dbec385c593f2ae405ea637/highspy-1.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a6760962b3e813814dc5e88301890d7cce975de5ce97cc3aed589cfdd461811", size = 6192004, upload-time = "2026-07-02T12:02:14.544Z" }, + { url = "https://files.pythonhosted.org/packages/94/08/fb7d30ea0e6c83fb943b16bf31951ba13a5be01a638ec13962a477009b91/highspy-1.15.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:787c92d5ff274256ba8848ab174cfc65d5af696f51bffe87423c85b2ea25c3fe", size = 7233098, upload-time = "2026-07-02T12:02:16.609Z" }, + { url = "https://files.pythonhosted.org/packages/23/77/9a07df7181834cfb61dafa5594e5eedc78369797c6487806bd3221d20667/highspy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ee8e139e7260ec1306a48e30f1bd7937d9cfb8cb201d25da10e1099e5129b", size = 6636717, upload-time = "2026-07-02T12:02:18.72Z" }, + { url = "https://files.pythonhosted.org/packages/9a/25/5083d8e3d5cf5ff5edf5bcc03e3f693630ab59142c9d0a0bcbb2d315c50e/highspy-1.15.1-cp312-cp312-win32.whl", hash = "sha256:01c6585e83938ecf4139248b074b2ee736816d63716a20dc608b1d2fc9637b66", size = 2306753, upload-time = "2026-07-02T12:02:20.837Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/05521ca6b38e34e68d707888c378d3bcac34e62715b739e7c0c9b9887993/highspy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:8c548165270608a40147a7ea6d985fd62a65fabf0f075b3c0c59ea910b724223", size = 2711114, upload-time = "2026-07-02T12:02:22.621Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, +] + +[[package]] +name = "ldpc" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pymatching" }, + { name = "pytest" }, + { name = "scipy" }, + { name = "sinter" }, + { name = "stim" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/3d/1642f2456dc4c494ab1209b8da8a886cd4e82280839c8ef434294492738d/ldpc-2.4.1.tar.gz", hash = "sha256:729376026af4e46e550c333152f073dcf42224589380d309f38923e00736ceba", size = 1077057, upload-time = "2025-12-08T15:10:52.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/3b/11c487a402c58e646ce5736412ff4aa06ff0cbf20c4e228e1c942c473b08/ldpc-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:035ceb48f9d8c0d2ab37eee67f87692a935ba586ebc380a0f7ad7bac3192a872", size = 1834836, upload-time = "2025-12-08T15:10:31.333Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ee/157f674b39807af9a16a2d682b6993a3696709278c83fe9039356cbd5460/ldpc-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4010c9ac9eff008c058dfa9f225325d5aed8b7a0b55ec0975a737fc7352aa301", size = 1795380, upload-time = "2025-12-08T15:10:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b3/491228c3ee52889be6dbc9d3dbc1cff7f81d7174154dff564307fa76d694/ldpc-2.4.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6701b042f8d505f205c7fe39820566513f1191ed04413e0968bb54bfbc6f0086", size = 8122834, upload-time = "2025-12-08T15:10:33.59Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b0/a480113901c689b73087406f0a09a2206de2c71e2b791257802362bd5b12/ldpc-2.4.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49b8b14413ec0d4c68c4f5a9bcaff0232363aed71652a75ed6af1234c58031b", size = 8266025, upload-time = "2025-12-08T15:10:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/39/05/6b2cab8a9417d4de79bd67f281f420dd5862c1fe7b6f12b0910001d05938/ldpc-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d7965b6c3dc53ac0bc852e3769516a4b4a2756f26c6a683ea675c1e1771da42", size = 1736143, upload-time = "2025-12-08T15:10:36.837Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/a2/28696a9e61e245d1a79816d29d106692a90a2b6e7d78c98b326db70827af/llvmlite-0.48.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d66c3beb4209087ddd4cf4ed2a0856b6887e6a913bdcf1aacfec9851cf2cba4e", size = 40480651, upload-time = "2026-07-01T18:41:35.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/72409351db66d0a317ec5087e076f31fb7b773a640db8a90ce6b5cac9edd/llvmlite-0.48.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:416fa4c2c66c2c6dc6d0a402648c19206e548efa0aa1eff01ad5cdad0af8217d", size = 59890118, upload-time = "2026-07-01T18:41:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/5ae2f3722606360480707adb47f001ad89df8251d06b14ee80336e660b66/llvmlite-0.48.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5e5a5131045b72345c71062ea1a91910dde913792b6c9b28ebb2c1c0a712e98", size = 58343459, upload-time = "2026-07-01T18:41:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/78/d824ffff7521cd140dc2006e44ce2bc82e64b48d1b32e90e956308c85a74/llvmlite-0.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:d45c7541a80934ec6d8ab0defe67439494ecd2193cbf852a44ba827808976ac1", size = 41865022, upload-time = "2026-07-01T18:41:48.663Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a3/70deb7f88461c1cd5d16aa990c2380604102661a427667b8950dcdccc27f/numba-0.66.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53ca5900b7cab15109796030113a6b28576bae5ad7bb507ad6dd1360ddd81ba4", size = 2727264, upload-time = "2026-07-01T23:12:18.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/25c319845e9a4e08f16611ddbda56a192eb7b6ed13e1a2bff2da272ffb97/numba-0.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0999e3ee1b18c48e1fb51d11af35ef59852c7f4f50569c9550c25faef0616ad1", size = 3866252, upload-time = "2026-07-01T23:12:20.429Z" }, + { url = "https://files.pythonhosted.org/packages/71/ef/a82d6fd6bf1b0fe461651e924d3647eeec9ac17f8eee4896264bf7480930/numba-0.66.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe0d2d5099790df945e0cb6e1b3104bd965d7bbfac50d62f1d5d1d6ade0825d", size = 3566974, upload-time = "2026-07-01T23:12:22.116Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/9e6171e378822ab191c7abcfd3d8cfc8644516f6c7834c22e210e4acc070/numba-0.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:b075a4e7ebc43dc6294f223e2821659656209fd5e0ce53245877c23d66d6e1a9", size = 2797403, upload-time = "2026-07-01T23:12:23.724Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, +] + +[[package]] +name = "osqp" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/7f/b441062e4766851fdf1d066857134c4c3bcf7e0089e4a1d007b6114cecd5/osqp-1.1.3.tar.gz", hash = "sha256:48f53ef5ec89e6ce99ffa955bc6ea0cf2eec09ea3d40905f0c9fadc939609907", size = 57816, upload-time = "2026-06-12T16:59:27.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d2/f21ddf1b41ecc14784186129125c4e9c19d8f6f7ade0f2e0efd4a44dded6/osqp-1.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2b59e8094fe29d928d568cad0156a42daa44257ce142fb7808400016a62dc28c", size = 328512, upload-time = "2026-06-12T16:58:49.796Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/1f99a9f25dc9534323b8ef9e578dc93acb33e2b762d5be267e1380b2dda5/osqp-1.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5620abbdb259190da6f0e5421b9fdfd9386b46690210ce82aea6102cc85c67f", size = 308891, upload-time = "2026-06-12T16:58:51.07Z" }, + { url = "https://files.pythonhosted.org/packages/d5/25/176dbb33c3c3605367c0de8720945c6bdd8f9b0d411e0a94ef747085f034/osqp-1.1.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e44ad08234cfbd6d9f2a823118547e683b038676887e096533935b9fcc15fd3c", size = 328135, upload-time = "2026-06-12T16:58:52.185Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/96e2e4de3b89e7363931c70eb58bded22656f5512bde18eb7a9272ce48d8/osqp-1.1.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ceef7fb4f332892b6e0bbc17323d5c9e028c3f9db726b62a15d876d0f81cc06", size = 354077, upload-time = "2026-06-12T16:58:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/4b/6a1e4f5aa28117bc0a78be5e9f4071b6a11c00a33a01bc673c3faf2a0a65/osqp-1.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:e7e9bd3939e47e726673218a7b04b9784ecbf9ef0b8bc69107d20cc659836ac1", size = 316733, upload-time = "2026-06-12T16:58:54.397Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymatching" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/19/39472c89d532e576890320eff5e9738e4b4d7828d027315f7e198ba0be8c/pymatching-2.4.0.tar.gz", hash = "sha256:0c30dafa506b086f50d15530debd7b7b39228597d22c52d5ccd114bcbb0a2326", size = 347169, upload-time = "2026-05-22T18:10:07.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/bc/4983d49570d97b4489de8ed1580faea58e63a16e6c7046e45aaa636ea054/pymatching-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:881f3410975b5a2d393561d44fa4513c878653d0e718e55930b08f52e9a04fc5", size = 380199, upload-time = "2026-05-22T18:09:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0e/00cd2b9c2da28fb3dafb77da8e2da18ceedda42de2298c682fbb016a9189/pymatching-2.4.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:af03cdd7389e7b9f93069f1dd1bb9931c3836a2bfc0d01952993f394b0bfa10d", size = 412898, upload-time = "2026-05-22T18:09:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/0f/03/b3186c28500a5aa751f7fc37b52fe6129ba24092604a8118faea456f482c/pymatching-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15e6d73153713a8f383f44ba4497d478fb4c4d765fbdd30f9fc1e1d47af75760", size = 626230, upload-time = "2026-05-22T18:09:38.547Z" }, + { url = "https://files.pythonhosted.org/packages/60/c8/d9abe73cc6dbaddf61d3c5875a9687aaa9591a976b109cc7cb6b7595cccb/pymatching-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:eb4428581a2578a8cc77f695ab267c184f444d9de5cb25301fe9f3f893214ea4", size = 347707, upload-time = "2026-05-22T18:09:40.805Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "qdldl" +version = "0.1.9.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/452984a63df9421cf8e7d25e8e6a44832cf0247a5e7b65e437cd516a0f8f/qdldl-0.1.9.post1.tar.gz", hash = "sha256:da2016d541c26cefc79bca4d8b5bebfa00f35db19704abb20efbd1c08df3b4c7", size = 76295, upload-time = "2026-02-19T16:48:36.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ed/2ae64314f84211cb963136168fb119e595d1aea42d1f03a1e84ef7d04d68/qdldl-0.1.9.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25525962e90f1b9bfe3c4fb565222daf2bd77249bb6ffb9ae20b7da551f73538", size = 122478, upload-time = "2026-02-19T16:47:51.279Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/767d8a6da3a04ee3c7262f897c1dd81e92d156c60eb5b43bcab4f2bc5af5/qdldl-0.1.9.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a2c005c9365dea6389a9feacf636028453790cace114b54e40fd302d6f4bf91c", size = 117691, upload-time = "2026-02-19T16:47:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/1f/58/cb80bb5d379a7e570a160a198ff6bff2398d6c61b5514462736348218013/qdldl-0.1.9.post1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee5b35c1dd419cbf388664d26d410c89a3e0c7e055e25223b6d28bd920349c8a", size = 1449354, upload-time = "2026-02-19T16:47:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/8d89f71d4e1cb3152e4e3262db667282e0df6788d21800725195ee2384c0/qdldl-0.1.9.post1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa0e9721d272467c95a9748e6acea8911a12041ef3d8b20176aff829388ce57d", size = 1476880, upload-time = "2026-02-19T16:47:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/d4/19/b30374cd37f145c4ee086642eca46c1a0b29d16027e02417b825d3708602/qdldl-0.1.9.post1-cp312-cp312-win_amd64.whl", hash = "sha256:f5a9bcda38dd19f75d72e47558f5132a99e5443238e5153a984c6f552bc4f4ac", size = 104542, upload-time = "2026-02-19T16:47:55.558Z" }, + { url = "https://files.pythonhosted.org/packages/22/51/2a683eddd1f0cf50440e0d173ccc4cb500775a8449e27651963873cdb4df/qdldl-0.1.9.post1-cp312-cp312-win_arm64.whl", hash = "sha256:ba3e19399553821b5ceee0c082fdb4453d00a38bd420b76dddec92ce2a5a065c", size = 98901, upload-time = "2026-02-19T16:47:56.553Z" }, +] + +[[package]] +name = "qldpc" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cvxpy" }, + { name = "diskcache" }, + { name = "galois" }, + { name = "ldpc" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "platformdirs" }, + { name = "pymatching" }, + { name = "scipy" }, + { name = "stim" }, + { name = "sympy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/4d/0036670900534879517b1d4262bf61358e0ced84666f2ee27f3865324a88/qldpc-0.2.6.tar.gz", hash = "sha256:86085a59a9afaaecdb538f063e8890255fdd2d79ac24cf0a3e55fb9209f3a73f", size = 172508, upload-time = "2025-10-24T19:57:29.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/10/7229f185dc4b6a8f45bc7a1926da02582ebc3241feb636589344e6504f73/qldpc-0.2.6-py3-none-any.whl", hash = "sha256:aa44f5fb3e9ea2ebf20b13ab3d5242e7a4ecb4c19fea8ea3168515af229a40ea", size = 205092, upload-time = "2025-10-24T19:57:27.893Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, +] + +[[package]] +name = "scs" +version = "3.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/59/5cb7f9612a5a3ff6efd4ab2d899902a536cc5974a7edb589084c5577291c/scs-3.2.11.tar.gz", hash = "sha256:2a5455cf2093d07f84f2f848c199faed52e79cdb3a11fe250b5622b6bbac4913", size = 1691825, upload-time = "2026-01-09T17:53:54.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/74/87a97e5fc2aac7ab3661c2555a25115121734c51eb4ebbabc2127f53bd83/scs-3.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad646848375b5cf2d3e45a9ebefd87ccc37a53da9c32f2bf30ea5ad0e84d9e5b", size = 96302, upload-time = "2026-01-09T17:53:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/24/0c/e34764a320249465dc6c11e67a6d34e2e53a9186a64f21759e94dfb043ee/scs-3.2.11-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f30821521a74f6930924b13e731e9455b6bdcfc964f66d5623d3c8d3fdd98126", size = 5071418, upload-time = "2026-01-09T17:53:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/db/3d/dd17a1c1890ce25efd3908f7ab67a56b208e89c5a5d60a2dedaf99394dcb/scs-3.2.11-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a89c71ebacd4790c461d3032a47e59ed4759e11c0f03fa79b5b84086ef9c7bc", size = 12079957, upload-time = "2026-01-09T17:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/6f83bfa17e074c92b17e16a9bd897aedeec64f10f9200c86588d7fc583c2/scs-3.2.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4e37dc60081dd742bdcd63eeb5b260db116b3803162bcf6084eb203ebedcb080", size = 11973936, upload-time = "2026-01-09T17:53:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/1a/fe/5d8f6048a90abc3aa053b5ac2acf3885dc46af94c3baf7d9ccf201a1ce19/scs-3.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:2504266ff8e6a226f7ecb987567c93e6e996534cbf479a60a5a886549446205e", size = 7478461, upload-time = "2026-01-09T17:53:11.899Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "sinter" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "stim" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/6b/b546cc9240fb1b64417c8ca00038ce1927ffd6e8a0d4e46ad237242dbcd8/sinter-1.16.0.tar.gz", hash = "sha256:fd0f7bff97cb951345893f53b347d76f99b53977590054abc3d057150f82037b", size = 177330, upload-time = "2026-05-22T05:41:49.762Z" } + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sparsediffpy" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/e7/6a3227a25a79a440e5ec5eff1e90f5911515a7c219ee95fc6dfe5d74ec30/sparsediffpy-0.3.0.tar.gz", hash = "sha256:fdd9115db63ee228d09e1917365b263a16811645c6d32ee7dce50ada09b3d5a5", size = 180927, upload-time = "2026-05-14T06:57:48.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/82/c2eb05d191fdeaee1f03d7ed5bc7f796256fd975eceb344a2a23fe6225e2/sparsediffpy-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22848d97852554c8814cd5a0bffc4f4f41930aa7155f2193e99332e8a9cf6f6d", size = 207399, upload-time = "2026-05-14T06:57:18.379Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/31b924d6756469af09e44ac93507e6cf9b80c3de3cae63e7185a89f0605f/sparsediffpy-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc49c774a44776afd728aca4ee1f062756416ee25366acbf32ad6416bdfb2630", size = 138478, upload-time = "2026-05-14T06:57:20.077Z" }, + { url = "https://files.pythonhosted.org/packages/23/4b/46834436ee28b2aa1404ac0b9fa2a3fcef4ee249d5fa624b0b16c50bee42/sparsediffpy-0.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31560dc28732401d922cb910d997adabed72f26338dbf03409e1a3c6639d1908", size = 5091184, upload-time = "2026-05-14T06:57:21.78Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/e23622b87b93bdcca8ac3bd0ffe825a670ab7710ccafda7f4ca0d7d0af1b/sparsediffpy-0.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:614ba5d5fbdd64c3f53fa9b6dbd37d6794c55176093eba4b58f3618cf20d62bf", size = 12099967, upload-time = "2026-05-14T06:57:24.009Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a7/487fcf2157472235411e30795f1b8270e311db73e1bb58b3fe73d70c19d3/sparsediffpy-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:695be68fedc2c1b6fc258d05e37e20b4081c38e020ec5c9d862e42266d1ece84", size = 130146, upload-time = "2026-05-14T06:57:26.56Z" }, +] + +[[package]] +name = "stim" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/bd/c0f5d6241cdb806400a63e0c7febe22982ddf1c8761142bb0ea6b51d3f50/stim-1.16.0.tar.gz", hash = "sha256:9092a996429dcf616d8d4ca01fec886b7b310caf9425a39fcfe499005636bd52", size = 882497, upload-time = "2026-05-22T05:41:56.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/d9/8b70c39860e67e233178a9ef595c15a7ec718f0b88bc424ae2c6ad8f97da/stim-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3e2883216fb4f57411531c99dfa1838c0d8201c619ccd01b197b9afd54d326cf", size = 2067905, upload-time = "2026-05-22T05:41:34.9Z" }, + { url = "https://files.pythonhosted.org/packages/51/e3/45a8c0889f4629027cbdd8ed8e9f5824b5bd8b12b0f5d24be27ec0c2a3dc/stim-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b8580dfbe5ff27594dd15a83b24262630b82cf089d1463d29dbee90ffbc6898b", size = 1902644, upload-time = "2026-05-22T05:41:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/db565f5bd0f273414e90d85551eed1c75f7b689847bd605a7cf0c4c0386d/stim-1.16.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883b24c3c95fa69818e931e8b60daf4fc8195d159d321e7548d8706dbac16558", size = 5307850, upload-time = "2026-05-22T05:41:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/bb/65/0ddfa6f76c4a137aefeee4d51a65945e0ec77cc2221322f13a5768f4c488/stim-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:85b9c544c65377a42c6b5c9a57616a5ed5580e1cabb286a604800a045c7a12fb", size = 2760794, upload-time = "2026-05-22T05:41:39.731Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tqdm" +version = "4.69.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/run.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/run.py new file mode 100644 index 0000000..2251799 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/run.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import argparse +import json +import os +import sys +from enum import Enum +from pathlib import Path +from typing import Any + + +ENV_PROJECT_DIR = "evaluator-env" +ENV_ACTIVE = "AGENTICS_QCODE_EVALUATOR_ENV_ACTIVE" + + +class EvaluationMode(str, Enum): + VALIDATION = "validation" + OFFICIAL = "official" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Score qcode-discovery generator outputs") + parser.add_argument("--challenge-dir", required=True) + parser.add_argument("--solution-runs-dir", required=True) + parser.add_argument("--output-path", required=True) + parser.add_argument("--mode", choices=[mode.value for mode in EvaluationMode], required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--setup-dir", required=True) + parser.add_argument("--runs-file", required=True) + return parser.parse_args() + + +def maybe_reexec(args: argparse.Namespace) -> None: + if os.environ.get(ENV_ACTIVE) == "1": + return + python = Path(args.setup_dir) / ENV_PROJECT_DIR / ".venv" / "bin" / "python" + if not python.is_file(): + raise RuntimeError(f"missing evaluator environment at {python}") + env = os.environ.copy() + env[ENV_ACTIVE] = "1" + env["HOME"] = str(Path(args.output_path).parent) + env["TMPDIR"] = str(Path(args.output_path).parent / "tmp") + env["PYTHONDONTWRITEBYTECODE"] = "1" + Path(env["TMPDIR"]).mkdir(parents=True, exist_ok=True) + os.execve(str(python), [str(python), *sys.argv], env) + + +def metric(name: str, value: float | int) -> dict[str, Any]: + return {"metric_name": name, "value": float(value)} + + +def lattice_metric_name(ell: int, m: int) -> str: + return f"credible_fom_{ell}x{m}" + + +def write_result(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + +def validation_payload(result: dict[str, Any], lattices: list[tuple[int, int]], warnings: tuple[str, ...]) -> dict[str, Any]: + covered = int(result["covered_lattices"]) + passed = covered == len(lattices) + run_metrics = [] + public_results = [] + for lattice in lattices: + stats = result["per_lattice"].get(lattice, {"valid_codes": 0.0, "high_k_codes": 0.0, "best_rate": 0.0}) + run_name = f"lattice-{lattice[0]}x{lattice[1]}" + valid = int(stats["valid_codes"]) + run_metrics.append( + { + "run_name": run_name, + "metrics": [ + metric("valid_codes", valid), + metric("high_k_codes", stats["high_k_codes"]), + ], + } + ) + public_results.append( + { + "case_name": run_name, + "status": "passed" if valid > 0 else "failed", + "score": float(stats["best_rate"]), + "message": f"valid_codes={valid}, high_k_codes={int(stats['high_k_codes'])}, best_encoding_rate={stats['best_rate']:.6f}", + } + ) + return { + "status": "passed" if passed else "failed", + "mode": EvaluationMode.VALIDATION.value, + "aggregate_metrics": [ + metric("stage1_score", result["stage1_score"]), + metric("valid_codes", result["valid_codes"]), + metric("high_k_codes", result["high_k_codes"]), + metric("lattices_with_high_k", result["lattices_with_high_k"]), + metric("total_candidates", result["total_candidates"]), + ], + "run_metrics": run_metrics, + "public_results": public_results, + "validation_summary": { + "score": float(result["stage1_score"]), + "passed": covered, + "total": len(lattices), + }, + "logs": [ + f"IBM stage-1 score={result['stage1_score']:.6f}; covered_lattices={covered}/{len(lattices)}", + *warnings[:40], + ], + } + + +def official_payload(result: dict[str, Any], lattices: list[tuple[int, int]], warnings: tuple[str, ...]) -> dict[str, Any]: + aggregate_metrics = [ + metric("score", result["score"]), + metric("score_stddev", result["score_stddev"]), + metric("score_min", result["score_min"]), + metric("score_max", result["score_max"]), + metric("mean_best_fom", result["mean_best_fom"]), + metric("valid_codes", result["valid_codes"]), + metric("high_k_codes", result["high_k_codes"]), + metric("lattices_with_high_k", result["lattices_with_high_k"]), + metric("total_candidates", result["total_candidates"]), + ] + for ell, m in lattices: + aggregate_metrics.append(metric(lattice_metric_name(ell, m), result["per_lattice"][(ell, m)])) + return { + "status": "passed", + "mode": EvaluationMode.OFFICIAL.value, + "aggregate_metrics": aggregate_metrics, + "run_metrics": [], + "public_results": [], + "official_summary": { + "score": float(result["score"]), + "passed": int(result["lattices_with_high_k"]), + "total": len(lattices), + }, + "logs": [ + "three independent IBM-style BP-OSD passes completed", + "pass_scores=" + ",".join(f"{score:.6f}" for score in result["pass_scores"]), + *warnings[:40], + ], + } + + +def error_payload(mode: EvaluationMode, message: str, total: int) -> dict[str, Any]: + payload: dict[str, Any] = { + "status": "error", + "mode": mode.value, + "aggregate_metrics": [metric("score", 0.0)] if mode is EvaluationMode.OFFICIAL else [], + "run_metrics": [], + "public_results": [], + "logs": [message if mode is EvaluationMode.VALIDATION else "evaluation failed before all repetitions completed"], + } + summary = {"score": 0.0, "passed": 0, "total": total} + if mode is EvaluationMode.VALIDATION: + payload["validation_summary"] = summary + else: + payload["official_summary"] = summary + return payload + + +def run() -> int: + args = parse_args() + maybe_reexec(args) + mode = EvaluationMode(args.mode) + output_path = Path(args.output_path) + try: + from scoring import evaluate_official, evaluate_stage1, load_candidate_collection + + runs_payload = json.loads(Path(args.runs_file).read_text(encoding="utf-8")) + runs = runs_payload.get("runs") + if not isinstance(runs, list): + raise ValueError("runs manifest must contain a runs array") + lattices = [] + for run_spec in runs: + metadata = run_spec.get("metadata") if isinstance(run_spec, dict) else None + if not isinstance(metadata, dict): + raise ValueError("run metadata must contain lattice dimensions") + lattices.append((int(metadata["ell"]), int(metadata["m"]))) + collection = load_candidate_collection(runs_payload, Path(args.solution_runs_dir)) + if mode is EvaluationMode.VALIDATION: + payload = validation_payload(evaluate_stage1(collection, lattices), lattices, collection.warnings) + else: + payload = official_payload(evaluate_official(collection, lattices), lattices, collection.warnings) + except Exception as error: # noqa: BLE001 - evaluator must always explain its terminal state. + payload = error_payload(mode, str(error), 2 if mode is EvaluationMode.VALIDATION else 8) + write_result(output_path, payload) + return 0 + + +if __name__ == "__main__": + raise SystemExit(run()) diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/scoring.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/scoring.py new file mode 100644 index 0000000..1e31e5e --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/scoring.py @@ -0,0 +1,322 @@ +from __future__ import annotations + +import json +import math +import multiprocessing +import os +import statistics +import sys +from concurrent.futures import ProcessPoolExecutor +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + + +MAX_CANDIDATE_FILE_BYTES = 8 * 1024 * 1024 +MAX_CANDIDATES = 5000 +OFFICIAL_REPETITIONS = 3 +TRUST_FULL = 1.3 +TRUST_NONE = 2.0 + +Lattice = tuple[int, int] +Terms = list[tuple[int, int]] +Candidate = tuple[Terms, Terms] + + +@dataclass(frozen=True) +class CandidateCollection: + candidates: dict[Lattice, list[Candidate]] + submitted_counts: dict[Lattice, int] + warnings: tuple[str, ...] + + +def _bounded_json(path: Path) -> Any: + size = path.stat().st_size + if size > MAX_CANDIDATE_FILE_BYTES: + raise ValueError(f"candidates.json exceeds {MAX_CANDIDATE_FILE_BYTES} bytes") + try: + return json.loads(path.read_text(encoding="utf-8")) + except UnicodeDecodeError as error: + raise ValueError("candidates.json must be UTF-8") from error + except json.JSONDecodeError as error: + raise ValueError(f"candidates.json is not valid JSON: {error.msg}") from error + + +def _term_list(raw: Any) -> Terms | None: + if not isinstance(raw, list) or not 2 <= len(raw) <= 6: + return None + terms: Terms = [] + for pair in raw: + if ( + not isinstance(pair, list) + or len(pair) != 2 + or any(isinstance(value, bool) or not isinstance(value, int) for value in pair) + ): + return None + terms.append((pair[0], pair[1])) + if len(set(terms)) != len(terms): + return None + return terms + + +def _terms_in_lattice(terms: Terms, lattice: Lattice) -> bool: + ell, m = lattice + return all(0 <= x < ell and 0 <= y < m for x, y in terms) + + +def parse_candidate_document(payload: Any, lattice: Lattice) -> tuple[list[Candidate], int, list[str]]: + if not isinstance(payload, dict) or set(payload) != {"candidates"}: + raise ValueError("output must be an object containing only candidates") + raw_candidates = payload["candidates"] + if not isinstance(raw_candidates, list): + raise ValueError("candidates must be an array") + submitted_count = len(raw_candidates) + warnings: list[str] = [] + parsed: list[Candidate] = [] + for index, raw in enumerate(raw_candidates[:MAX_CANDIDATES]): + if not isinstance(raw, dict) or set(raw) != {"a_terms", "b_terms"}: + parsed.append(([], [])) + warnings.append(f"candidate {index} has an invalid object shape") + continue + a_terms = _term_list(raw["a_terms"]) + b_terms = _term_list(raw["b_terms"]) + if ( + a_terms is None + or b_terms is None + or not _terms_in_lattice(a_terms, lattice) + or not _terms_in_lattice(b_terms, lattice) + ): + parsed.append(([], [])) + warnings.append(f"candidate {index} has invalid or out-of-range polynomial terms") + continue + parsed.append((a_terms, b_terms)) + if submitted_count > MAX_CANDIDATES: + warnings.append(f"candidate list was truncated from {submitted_count} to {MAX_CANDIDATES}") + return parsed, submitted_count, warnings + + +def _positive_int(value: Any, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise ValueError(f"{field} must be a positive integer") + return value + + +def _run_lattice(run: dict[str, Any]) -> Lattice: + metadata = run.get("metadata") + if not isinstance(metadata, dict): + raise ValueError("run metadata must be an object") + return _positive_int(metadata.get("ell"), "ell"), _positive_int(metadata.get("m"), "m") + + +def _run_completed(run_dir: Path) -> tuple[bool, str | None]: + metadata_path = run_dir / "agentics-run.json" + if not metadata_path.is_file(): + return False, "missing agentics-run.json" + try: + metadata = _bounded_json(metadata_path) + except (OSError, ValueError) as error: + return False, str(error) + if not isinstance(metadata, dict): + return False, "agentics-run.json must be an object" + if metadata.get("timed_out") is True: + return False, "solution timed out" + exit_code = metadata.get("exit_code") + if isinstance(exit_code, bool) or not isinstance(exit_code, int) or exit_code != 0: + return False, f"solution exited with code {exit_code!r}" + return True, None + + +def load_candidate_collection(runs_payload: Any, solution_runs_dir: Path) -> CandidateCollection: + if not isinstance(runs_payload, dict) or not isinstance(runs_payload.get("runs"), list): + raise ValueError("runs manifest must contain a runs array") + candidates: dict[Lattice, list[Candidate]] = {} + submitted_counts: dict[Lattice, int] = {} + warnings: list[str] = [] + for run in runs_payload["runs"]: + if not isinstance(run, dict) or not isinstance(run.get("run_name"), str): + raise ValueError("every run must have a string run_name") + run_name = run["run_name"] + lattice = _run_lattice(run) + if lattice in candidates: + raise ValueError(f"duplicate lattice {lattice}") + run_dir = solution_runs_dir / run_name + completed, failure = _run_completed(run_dir) + if not completed: + candidates[lattice] = [] + submitted_counts[lattice] = 0 + warnings.append(f"{run_name}: {failure}") + continue + output_path = run_dir / "output" / "candidates.json" + try: + parsed, submitted_count, parse_warnings = parse_candidate_document(_bounded_json(output_path), lattice) + except (OSError, ValueError) as error: + candidates[lattice] = [] + submitted_counts[lattice] = 0 + warnings.append(f"{run_name}: {error}") + continue + candidates[lattice] = parsed + submitted_counts[lattice] = submitted_count + warnings.extend(f"{run_name}: {warning}" for warning in parse_warnings[:20]) + return CandidateCollection(candidates, submitted_counts, tuple(warnings)) + + +def credible_fom(n: int, k: int, distance: int) -> float: + if n <= 0 or k <= 0: + return 0.0 + fallback = k / n + if distance <= 0: + return fallback + ratio = distance / math.sqrt(n) + raw_fom = k * distance * distance / n + if ratio <= TRUST_FULL: + return raw_fom + if ratio >= TRUST_NONE: + return fallback + alpha = (TRUST_NONE - ratio) / (TRUST_NONE - TRUST_FULL) + return alpha * raw_fom + (1.0 - alpha) * fallback + + +def score_stage1_metrics(metrics: dict[str, Any], lattice_count: int) -> dict[str, Any]: + valid = [result for result in metrics.get("all_results", []) if result.get("k", 0) > 0] + covered = {(result["ell"], result["m"]) for result in valid} + if len(covered) < lattice_count: + score = len(covered) * 0.001 + else: + best_rate = max((float(result.get("encoding_rate", 0.0)) for result in valid), default=0.0) + score = 0.1 + best_rate + math.log1p(float(metrics.get("num_high_k", 0))) / 10.0 + per_lattice: dict[Lattice, dict[str, float]] = {} + for result in valid: + lattice = (int(result["ell"]), int(result["m"])) + stats = per_lattice.setdefault(lattice, {"valid_codes": 0.0, "high_k_codes": 0.0, "best_rate": 0.0}) + stats["valid_codes"] += 1.0 + if result.get("k", 0) >= 8: + stats["high_k_codes"] += 1.0 + stats["best_rate"] = max(stats["best_rate"], float(result.get("encoding_rate", 0.0))) + return { + "stage1_score": score, + "covered_lattices": len(covered), + "valid_codes": len(valid), + "high_k_codes": int(metrics.get("num_high_k", 0)), + "lattices_with_high_k": int(metrics.get("lattices_with_high_k", 0)), + "total_candidates": int(metrics.get("total_candidates", 0)), + "per_lattice": per_lattice, + } + + +def score_stage2_metrics(metrics: dict[str, Any], lattices: list[Lattice]) -> dict[str, Any]: + per_lattice = {lattice: 0.0 for lattice in lattices} + for result in metrics.get("all_results", []): + lattice = (int(result.get("ell", 0)), int(result.get("m", 0))) + if lattice not in per_lattice: + continue + value = credible_fom( + int(result.get("n", 0)), + int(result.get("k", 0)), + int(result.get("d", 0)), + ) + per_lattice[lattice] = max(per_lattice[lattice], value) + return { + "combined_score": sum(per_lattice.values()), + "best_fom": float(metrics.get("best_fom", 0.0)), + "valid_codes": int(metrics.get("num_valid", 0)), + "high_k_codes": int(metrics.get("num_high_k", 0)), + "lattices_with_high_k": int(metrics.get("lattices_with_high_k", 0)), + "total_candidates": int(metrics.get("total_candidates", 0)), + "per_lattice": per_lattice, + } + + +def _load_upstream(): + vendor_root = Path(__file__).resolve().parent / "vendor" / "qcode-discovery" + if str(vendor_root) not in sys.path: + sys.path.insert(0, str(vendor_root)) + from evolve import openevolve_evaluator as upstream + + upstream._log_code_jsonl = lambda *_args, **_kwargs: None + upstream.save_code = lambda *_args, **_kwargs: None + upstream.update_pareto_front = lambda *_args, **_kwargs: [] + return upstream + + +def _generator(candidate_map: dict[Lattice, list[Candidate]]) -> Callable[[int, int], list[Candidate]]: + def generate(ell: int, m: int) -> list[Candidate]: + return candidate_map.get((ell, m), []) + + return generate + + +def evaluate_stage1(collection: CandidateCollection, lattices: list[Lattice]) -> dict[str, Any]: + upstream = _load_upstream() + metrics = upstream._run_evaluation(_generator(collection.candidates), lattices, quick=True) + metrics["total_candidates"] = sum(collection.submitted_counts.get(lattice, 0) for lattice in lattices) + return score_stage1_metrics(metrics, len(lattices)) + + +def require_complete_upstream_pass(metrics: dict[str, Any]) -> None: + upstream_errors = metrics.get("errors") + if not isinstance(upstream_errors, list): + raise RuntimeError("upstream evaluation returned an invalid errors field") + if upstream_errors: + details = "; ".join(str(error) for error in upstream_errors[:8]) + raise RuntimeError(f"upstream evaluation did not complete every lattice: {details}") + + +def evaluate_official_pass( + candidate_map: dict[Lattice, list[Candidate]], + submitted_counts: dict[Lattice, int], + lattices: list[Lattice], +) -> dict[str, Any]: + upstream = _load_upstream() + # Preserve the pinned adapter's executable behavior: this value becomes + # evaluate_candidate.quick_trials, while its refinement default stays 500. + metrics = upstream._run_evaluation( + _generator(candidate_map), + lattices, + quick=False, + refine_trials=1000, + ) + require_complete_upstream_pass(metrics) + metrics["total_candidates"] = sum(submitted_counts.get(lattice, 0) for lattice in lattices) + return score_stage2_metrics(metrics, lattices) + + +def aggregate_official_passes(pass_results: list[dict[str, Any]], lattices: list[Lattice]) -> dict[str, Any]: + if len(pass_results) != OFFICIAL_REPETITIONS: + raise RuntimeError(f"expected {OFFICIAL_REPETITIONS} complete passes, found {len(pass_results)}") + scores = [float(result["combined_score"]) for result in pass_results] + per_lattice = { + lattice: statistics.fmean(float(result["per_lattice"][lattice]) for result in pass_results) + for lattice in lattices + } + return { + "score": statistics.fmean(scores), + "score_stddev": statistics.pstdev(scores), + "score_min": min(scores), + "score_max": max(scores), + "mean_best_fom": statistics.fmean(float(result["best_fom"]) for result in pass_results), + "valid_codes": int(pass_results[0]["valid_codes"]), + "high_k_codes": int(pass_results[0]["high_k_codes"]), + "lattices_with_high_k": int(pass_results[0]["lattices_with_high_k"]), + "total_candidates": int(pass_results[0]["total_candidates"]), + "per_lattice": per_lattice, + "pass_scores": scores, + } + + +def evaluate_official(collection: CandidateCollection, lattices: list[Lattice]) -> dict[str, Any]: + os.environ.setdefault("OMP_NUM_THREADS", "1") + os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") + os.environ.setdefault("MKL_NUM_THREADS", "1") + context = multiprocessing.get_context("spawn") + with ProcessPoolExecutor(max_workers=OFFICIAL_REPETITIONS, mp_context=context) as executor: + futures = [ + executor.submit( + evaluate_official_pass, + collection.candidates, + collection.submitted_counts, + lattices, + ) + for _ in range(OFFICIAL_REPETITIONS) + ] + pass_results = [future.result() for future in futures] + return aggregate_official_passes(pass_results, lattices) diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/setup.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/setup.py new file mode 100644 index 0000000..62aa0d9 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/setup.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +from enum import Enum +from pathlib import Path +from typing import Any + + +ENV_PROJECT_DIR = "evaluator-env" +PYTHON_INSTALL_DIR = "uv-python" +PYTHON_REQUEST = "3.12" +SUPPORTED_TARGET = "linux-arm64-cpu" + + +class EvaluationMode(str, Enum): + VALIDATION = "validation" + OFFICIAL = "official" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Set up qcode-discovery evaluation") + parser.add_argument("--challenge-dir", required=True) + parser.add_argument("--setup-dir", required=True) + parser.add_argument("--mode", choices=[mode.value for mode in EvaluationMode], required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--runs-file", required=True) + return parser.parse_args() + + +def load_config(challenge_dir: Path) -> dict[str, Any]: + path = challenge_dir / "public" / "lattices.json" + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict) or payload.get("schema_version") != 1: + raise RuntimeError("public/lattices.json must be a schema-version-1 object") + return payload + + +def lattice_pairs(raw: Any, field: str) -> list[tuple[int, int]]: + if not isinstance(raw, list) or not raw: + raise RuntimeError(f"{field} must be a non-empty array") + lattices: list[tuple[int, int]] = [] + for value in raw: + if ( + not isinstance(value, list) + or len(value) != 2 + or any(isinstance(item, bool) or not isinstance(item, int) or item <= 0 for item in value) + ): + raise RuntimeError(f"{field} entries must be positive [ell, m] integer pairs") + lattices.append((value[0], value[1])) + if len(set(lattices)) != len(lattices): + raise RuntimeError(f"{field} must not contain duplicate lattices") + return lattices + + +def find_managed_python(setup_dir: Path, env: dict[str, str]) -> Path: + install_dir = setup_dir / PYTHON_INSTALL_DIR + subprocess.run( + ["uv", "python", "install", PYTHON_REQUEST, "--install-dir", str(install_dir)], + check=True, + env=env, + timeout=900, + ) + candidates = sorted(install_dir.glob("*/bin/python")) + if not candidates: + raise RuntimeError(f"uv did not install managed Python under {install_dir}") + return candidates[0] + + +def copy_and_sync_environment(challenge_dir: Path, setup_dir: Path) -> None: + source = challenge_dir / "resources" / ENV_PROJECT_DIR + project_dir = setup_dir / ENV_PROJECT_DIR + shutil.copytree(source, project_dir) + env = os.environ.copy() + env["UV_CACHE_DIR"] = str(setup_dir / "uv-cache") + env["UV_LINK_MODE"] = "copy" + env["UV_PROJECT_ENVIRONMENT"] = str(project_dir / ".venv") + python = find_managed_python(setup_dir, env) + subprocess.run( + [ + "uv", + "sync", + "--frozen", + "--project", + str(project_dir), + "--python", + str(python), + "--no-dev", + "--no-install-project", + ], + check=True, + env=env, + timeout=1200, + ) + shutil.rmtree(setup_dir / "uv-cache", ignore_errors=True) + + +def build_runs(lattices: list[tuple[int, int]], max_candidates: int) -> dict[str, Any]: + runs = [] + for ell, m in lattices: + runs.append( + { + "run_name": f"lattice-{ell}x{m}", + "interface": "file_system", + "stdin_json": None, + "stdin_text": None, + "input_files": [ + { + "path": "instance.json", + "content_json": { + "schema_version": 1, + "ell": ell, + "m": m, + "max_candidates": max_candidates, + }, + } + ], + "output_files": ["candidates.json"], + "metadata": {"ell": ell, "m": m}, + } + ) + return {"runs": runs} + + +def checked_runs_path(setup_dir: Path, raw_path: str) -> Path: + path = Path(raw_path).resolve() + try: + path.relative_to(setup_dir.resolve()) + except ValueError as error: + raise RuntimeError("--runs-file must be located under --setup-dir") from error + return path + + +def main() -> int: + args = parse_args() + mode = EvaluationMode(args.mode) + if args.target != SUPPORTED_TARGET: + raise RuntimeError(f"unsupported target {args.target!r}") + challenge_dir = Path(args.challenge_dir).resolve() + setup_dir = Path(args.setup_dir).resolve() + setup_dir.mkdir(parents=True, exist_ok=True) + config = load_config(challenge_dir) + max_candidates = config.get("max_candidates") + if isinstance(max_candidates, bool) or not isinstance(max_candidates, int) or max_candidates <= 0: + raise RuntimeError("max_candidates must be a positive integer") + field = "validation_lattices" if mode is EvaluationMode.VALIDATION else "official_lattices" + lattices = lattice_pairs(config.get(field), field) + copy_and_sync_environment(challenge_dir, setup_dir) + runs_path = checked_runs_path(setup_dir, args.runs_file) + runs_path.parent.mkdir(parents=True, exist_ok=True) + runs_path.write_text(json.dumps(build_runs(lattices, max_candidates), indent=2), encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/LICENSE b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/UPSTREAM.md b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/UPSTREAM.md new file mode 100644 index 0000000..4f9ad59 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/UPSTREAM.md @@ -0,0 +1,11 @@ +# Vendored qcode-discovery Sources + +- Repository: https://github.com/qiskit-community/qcode-discovery +- Commit: `4e828d0bc74066df9484e80f751a52674af7251f` +- Commit date: 2026-06-02 +- License: Apache License 2.0 +- Citation: https://arxiv.org/abs/2606.02418 + +The `evaluation` package and `evolve/openevolve_evaluator.py` are copied without behavioral edits from the pinned source. The Agentics adapter calls the upstream in-memory evaluation cascade, disables its result-persistence side effects, and supplies candidate values parsed from participant JSON instead of dynamically importing participant Python. + +The upstream default CSS evaluator imports its later MILP helpers at module load time, so the corresponding evaluator source is included even though this challenge invokes only the original BP-OSD path. OpenEvolve, LiteLLM, tracking integrations, result datasets, and campaign scripts are not vendored or installed. diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/__init__.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/__init__.py new file mode 100644 index 0000000..e9c3e68 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/__init__.py @@ -0,0 +1,24 @@ +"""Evaluation pipeline for bivariate bicycle quantum LDPC codes. + +This package implements the full evaluation cascade that takes a candidate +polynomial pair ``(A_terms, B_terms)`` and produces code parameters +``[[n, k, d]]`` along with a figure of merit ``FOM = k * d^2 / n``. + +Modules +------- +bb_code + BB code construction -- converts exponent tuples to ``qldpc.BBCode`` + objects and provides fast ``(n, k)`` extraction. +distance + Distance estimation -- BP-OSD upper bounds (OSD_0 and OSD-CS order 10) + and exact brute-force distance with timeout. +evaluator + Multi-stage evaluation cascade -- the central ``evaluate_candidate()`` + function that orchestrates validation, k computation, and progressive + distance refinement. +results + JSON persistence -- discovered codes file and Pareto front tracking. +tracking + Run logging -- structured JSONL files for per-evaluation and + per-generation metrics, plus run metadata. +""" diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/bb_code.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/bb_code.py new file mode 100644 index 0000000..f23dd07 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/bb_code.py @@ -0,0 +1,112 @@ +"""Bivariate bicycle code construction and parameter computation. + +Thin wrapper around ``qldpc.codes.BBCode`` that converts exponent-based +representations to sympy polynomials and provides a unified interface +for the evaluation pipeline. + +A bivariate bicycle (BB) code is defined by two trinomials over the ring +``F_2[x, y] / (x^ell - 1, y^m - 1)``. Each trinomial is represented in +this module as a list of three ``(x_exp, y_exp)`` tuples -- for example, +``[(3, 0), (0, 1), (0, 2)]`` encodes the polynomial ``x^3 + y + y^2``. + +The module exposes four public helpers used throughout the project: + +* :func:`terms_to_poly` -- convert exponent tuples to a sympy expression. +* :func:`validate_terms` -- check that a term list is a well-formed trinomial + for a given ``(ell, m)`` lattice. +* :func:`build_bb_code` -- construct a :class:`qldpc.codes.BBCode` object. +* :func:`get_code_params_fast` -- return ``(n, k)`` without computing distance. + +Typical usage:: + + code = build_bb_code(12, 6, + [(3, 0), (0, 1), (0, 2)], # A = x^3 + y + y^2 + [(0, 3), (1, 0), (2, 0)]) # B = y^3 + x + x^2 + n, k = get_code_params_fast(code) # (144, 12) +""" + +from __future__ import annotations + +import sympy +from sympy.abc import x, y +from qldpc import codes + + +def terms_to_poly(terms: list[tuple[int, int]]) -> sympy.Expr: + """Convert exponent pairs to a sympy polynomial. + + Args: + terms: List of (x_exp, y_exp) tuples, e.g. [(3,0), (1,0), (0,1)]. + + Returns: + Sympy expression like x**3 + x + y. + """ + monomials = [] + for x_exp, y_exp in terms: + monomials.append(x ** x_exp * y ** y_exp) + return sum(monomials) + + +def validate_terms( + ell: int, m: int, terms: list[tuple[int, int]], name: str = "polynomial", + min_terms: int = 2, max_terms: int = 6, +) -> None: + """Validate that terms define a proper polynomial for the given lattice. + + Raises ValueError if: + - Term count outside [min_terms, max_terms] + - Duplicate monomials (after reducing mod ell, m) + - Exponents out of range + """ + if not (min_terms <= len(terms) <= max_terms): + raise ValueError( + f"{name} must have {min_terms}-{max_terms} terms, got {len(terms)}" + ) + + reduced = set() + for x_exp, y_exp in terms: + if not (0 <= x_exp < ell): + raise ValueError( + f"{name}: x-exponent {x_exp} out of range [0, {ell})" + ) + if not (0 <= y_exp < m): + raise ValueError( + f"{name}: y-exponent {y_exp} out of range [0, {m})" + ) + monomial = (x_exp % ell, y_exp % m) + if monomial in reduced: + raise ValueError( + f"{name}: duplicate monomial x^{x_exp}*y^{y_exp}" + ) + reduced.add(monomial) + + +def build_bb_code( + ell: int, + m: int, + A_terms: list[tuple[int, int]], + B_terms: list[tuple[int, int]], +) -> codes.BBCode: + """Construct a BBCode from lattice dimensions and exponent lists. + + Args: + ell: Cyclic group order for x. + m: Cyclic group order for y. + A_terms: 3 monomials [(a1,b1), (a2,b2), (a3,b3)] for polynomial A. + B_terms: 3 monomials [(b1,c1), (b2,c2), (b3,c3)] for polynomial B. + + Returns: + A qldpc BBCode instance. + """ + poly_a = terms_to_poly(A_terms) + poly_b = terms_to_poly(B_terms) + return codes.BBCode({x: ell, y: m}, poly_a, poly_b) + + +def get_code_params_fast(code: codes.BBCode) -> tuple[int, int]: + """Get (n, k) quickly -- no distance computation. + + Returns: + (n, k) where n = num_qubits, k = dimension. + """ + return code.num_qudits, code.dimension diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/distance.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/distance.py new file mode 100644 index 0000000..549cc8d --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/distance.py @@ -0,0 +1,188 @@ +"""Distance estimation for bivariate bicycle codes. + +Provides three estimation strategies, listed from fastest to slowest: + +1. **BP-OSD with OSD_0** (:func:`estimate_distance`) -- default fast estimator. + Uses belief-propagation (product-sum) followed by ordered-statistics + decoding at order 0. Returns an upper bound on code distance by + splitting random trials evenly between X and Z Pauli types and taking + the minimum. ~100-500 trials give a workable bound in seconds. + +2. **BP-OSD with OSD-CS order 10** (:func:`estimate_distance_osd_cs`) -- + tighter bounds at ~2-5x the cost per trial. OSD-CS (combination + sweep) systematically searches order-*w* subsets of reliable bit + positions after Gaussian elimination, finding lower-weight logical + operators that OSD_0 misses. Empirically tightens bounds on 7 of 9 + tested codes (4-12 point improvements on high-*k* codes). + +3. **Exact distance** (:func:`compute_distance_exact`) -- brute-force + enumeration via ``code.get_distance_exact()``. Uses ``SIGALRM`` on + Unix for timeout or ``multiprocessing`` on Windows. Returns ``None`` + when called from non-main threads (qldpc objects are not picklable). + +Important caveat: BP-OSD is **stochastic** -- the same code can yield +distance estimates ranging from 6 to 18 across independent batches +(observed on [[144,32,?]]). For publication-quality claims, use the +multi-decoder soak test (``tests/soak_test.py``) with at least 150,000 +total trials across three decoder configurations. + +All distance values returned by this module are **upper bounds** and +should be reported as ``d <= X``, never ``d = X``. +""" + +from __future__ import annotations + +import multiprocessing +import signal +import threading +from contextlib import contextmanager +from qldpc.codes import CSSCode +from qldpc.objects import Pauli + + +def _safe_int(d, default=0) -> int: + """Convert distance to int, returning default if NaN.""" + if d != d: # NaN check + return default + return int(d) + +_HAS_SIGALRM = hasattr(signal, "SIGALRM") + + +def _can_use_sigalrm() -> bool: + """Check if SIGALRM is available and we're in the main thread.""" + return _HAS_SIGALRM and threading.current_thread() is threading.main_thread() + + +class DistanceTimeout(Exception): + pass + + +@contextmanager +def timeout(seconds: int): + """Context manager that raises DistanceTimeout after `seconds`. + + Uses SIGALRM on Unix. On platforms without it (Windows), falls back + to running the block without a timeout -- use compute_distance_exact() + which has its own multiprocessing-based timeout. + """ + if not _can_use_sigalrm(): + yield + return + + def handler(signum, frame): + raise DistanceTimeout(f"Distance computation timed out after {seconds}s") + + old_handler = signal.signal(signal.SIGALRM, handler) + signal.alarm(seconds) + try: + yield + finally: + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) + + +def estimate_distance(code: CSSCode, num_trials: int = 100) -> int: + """Estimate code distance using decoder-based upper bound (BP-OSD). + + Calls get_distance_bound_with_decoder directly to bypass GAP/QDistRnd + checks (which require interactive input when GAP is not installed). + + Args: + code: A CSSCode (typically BBCode). + num_trials: Number of randomized trials. More trials -> tighter bound. + + Returns: + Upper bound on code distance. + """ + if code.dimension == 0: + return 0 + + trials_x = num_trials // 2 + trials_z = (num_trials + 1) // 2 + d_x = code.get_distance_bound_with_decoder( + Pauli.X, trials_x, bp_method="product_sum" + ) + d_z = code.get_distance_bound_with_decoder( + Pauli.Z, trials_z, bp_method="product_sum" + ) + d = min(d_x, d_z) + return _safe_int(d) + + +def estimate_distance_osd_cs(code: CSSCode, num_trials: int = 200) -> int: + """Estimate distance using OSD-CS order 10 (tighter bounds than OSD_0). + + OSD-CS systematically searches order-w subsets of reliable bit positions + after Gaussian elimination, finding lower-weight logical operators that + OSD_0 misses. Empirically finds tighter bounds for 7 of 9 tested codes. + + ~2-5x slower per trial than OSD_0, so use fewer trials. + """ + if code.dimension == 0: + return 0 + + trials_x = num_trials // 2 + trials_z = (num_trials + 1) // 2 + d_x = code.get_distance_bound_with_decoder( + Pauli.X, trials_x, + bp_method="product_sum", osd_method="osd_cs", osd_order=10, + ) + d_z = code.get_distance_bound_with_decoder( + Pauli.Z, trials_z, + bp_method="product_sum", osd_method="osd_cs", osd_order=10, + ) + d = min(d_x, d_z) + return _safe_int(d) + + +def _exact_distance_worker(code: CSSCode, result_queue: multiprocessing.Queue): + """Worker for multiprocessing-based exact distance with timeout.""" + try: + d = code.get_distance_exact() + result_queue.put(_safe_int(d) if d == d else None) + except Exception: + result_queue.put(None) + + +def compute_distance_exact(code: CSSCode, timeout_seconds: int = 300) -> int | None: + """Compute exact code distance via brute-force. + + Uses SIGALRM on Unix for timeout. Falls back to multiprocessing on + platforms without SIGALRM (Windows). Returns None (skips) when called + from a non-main thread since qldpc objects can't be pickled for + multiprocessing and SIGALRM isn't available. + + Args: + code: A CSSCode (typically BBCode). + timeout_seconds: Maximum time allowed (default 5 minutes). + + Returns: + Exact distance, or None if computation timed out or unavailable. + """ + if _can_use_sigalrm(): + try: + with timeout(timeout_seconds): + d = code.get_distance_exact() + return _safe_int(d) or None + except DistanceTimeout: + return None + + if threading.current_thread() is not threading.main_thread(): + # Can't use SIGALRM or multiprocessing (qldpc objects aren't + # picklable) from worker threads. Return None to keep the + # BP-OSD estimate instead. + return None + + # Fallback: multiprocessing-based timeout (main thread, no SIGALRM) + queue: multiprocessing.Queue = multiprocessing.Queue() + proc = multiprocessing.Process( + target=_exact_distance_worker, args=(code, queue) + ) + proc.start() + proc.join(timeout=timeout_seconds) + if proc.is_alive(): + proc.terminate() + proc.join(timeout=5) + return None + return queue.get_nowait() if not queue.empty() else None diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/distance_milp.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/distance_milp.py new file mode 100644 index 0000000..4b08d9c --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/distance_milp.py @@ -0,0 +1,598 @@ +"""MILP-based exact distance computation for quantum codes. + +Uses the integer-programming formulation introduced by Landahl, Anderson, and +Rice (arXiv:1108.5738, 2011) and reused by Bravyi et al. (arXiv:2308.07915, +which cites Landahl-Anderson-Rice as the source of the method) with +optimizations for use during evolutionary search: + + - Early exit when d drops to ``early_stop`` threshold + - Cross-type early exit: skip d_X if d_Z already ≤ early_stop + - Per-code total timeout (not just per-logical) + - d_Z computed before d_X (cheaper to determine d is low) + +Supports both CSS codes (``ilp_min_weight``, ``compute_distance_milp``) +and non-CSS codes (``ilp_min_weight_symplectic``, +``compute_distance_milp_symplectic``). + +Core solver: HiGHS via ``scipy.optimize.milp``. Thread-safe (no SIGALRM). +""" + +from __future__ import annotations + +import logging +import time + +import numpy as np +from scipy.optimize import milp, LinearConstraint, Bounds + +from qldpc.objects import Pauli + +logger = logging.getLogger(__name__) + + +def get_code_matrices(code): + """Extract check matrices and logical operators from a qldpc BBCode. + + Returns (hx, hz, lx, lz) -- all binary numpy arrays. + """ + hx = np.array(code.matrix_x, dtype=int) % 2 + hz = np.array(code.matrix_z, dtype=int) % 2 + lx = np.array(code.get_logical_ops(Pauli.X), dtype=int) % 2 + lz = np.array(code.get_logical_ops(Pauli.Z), dtype=int) % 2 + return hx, hz, lx, lz + + +def symplectic_weight_bound(code): + """Upper bound on code distance from symplectic logical operator weights. + + Returns the minimum Hamming weight across all logical operators obtained + via Gaussian elimination (the initial symplectic basis). This is a valid + upper bound on d because any logical operator witnesses d ≤ weight. + + Cost: milliseconds (no solver, just GF(2) linear algebra already done + by qldpc internally). + + Returns: + (d_upper, d_x_upper, d_z_upper) -- ints. + """ + lx = np.array(code.get_logical_ops(Pauli.X), dtype=int) % 2 + lz = np.array(code.get_logical_ops(Pauli.Z), dtype=int) % 2 + + d_x_upper = int(np.min(np.sum(lx, axis=1))) if lx.size > 0 else code.num_qudits + d_z_upper = int(np.min(np.sum(lz, axis=1))) if lz.size > 0 else code.num_qudits + d_upper = min(d_x_upper, d_z_upper) + + return d_upper, d_x_upper, d_z_upper + + +def ilp_min_weight(check_matrix, logical_op, timeout=30): + """Find minimum-weight operator orthogonal to checks, anticommuting with logical_op. + + Formulation: binary variables x_j for each of n qubits, with mod-2 + constraints encoded as integer equalities using slack variables. + + Args: + check_matrix: (m, n) binary matrix of stabilizer checks. + logical_op: (n,) binary vector of a logical operator. + timeout: solver time limit in seconds. + + Returns: + (weight, optimal) tuple: weight is the minimum weight found (int), + optimal is True if proven optimal. Returns (None, False) when no + feasible solution was found at all (infeasibility or no incumbent). + """ + m, n = check_matrix.shape + num_vars = n + m + 1 # [x_0..x_{n-1}, s_0..s_{m-1}, t] + + # Objective: minimize Hamming weight + c = np.zeros(num_vars) + c[:n] = 1.0 + + # Constraint matrix: stabilizer orthogonality + logical anticommutation + rows = [] + for r in range(m): + row = np.zeros(num_vars) + row[:n] = check_matrix[r] + row[n + r] = -2 + rows.append(row) + + row = np.zeros(num_vars) + row[:n] = logical_op + row[n + m] = -2 + rows.append(row) + + A = np.array(rows) + + b_lb = np.zeros(m + 1) + b_ub = np.zeros(m + 1) + b_lb[m] = 1 + b_ub[m] = 1 + + constraints = LinearConstraint(A, b_lb, b_ub) + + # Bounds + lb = np.zeros(num_vars) + ub = np.ones(num_vars) + for r in range(m): + ub[n + r] = np.ceil(np.sum(check_matrix[r]) / 2) + ub[n + m] = np.ceil(np.sum(logical_op) / 2) + + bounds = Bounds(lb, ub) + integrality = np.ones(num_vars) + + opts = {"presolve": True} + if 0 < timeout < 1e9: + opts["time_limit"] = timeout + + result = milp( + c=c, + constraints=constraints, + integrality=integrality, + bounds=bounds, + options=opts, + ) + + if result.x is not None: + w = int(round(result.fun)) + return w, result.success # success=True means proven optimal + return None, False + + +def compute_distance_milp( + code, + *, + timeout_per_logical: int = 30, + total_timeout: int = 120, + early_stop: int | None = 4, + verbose: bool = False, +) -> tuple[int, dict]: + """Compute exact code distance via MILP with early-exit optimizations. + + Optimizations over the basic ILP approach: + 1. Stops as soon as d drops to ``early_stop`` (most bad codes have d=2-4). + 2. Computes d_Z first; skips d_X if d_Z ≤ early_stop. + 3. Respects a total time budget across all logicals. + 4. Adapts per-logical timeout to ensure broad coverage: when k is large, + uses shorter per-logical timeouts to check more logicals (coverage + matters more than per-logical optimality for finding min-weight). + + Args: + code: qldpc BBCode instance. + timeout_per_logical: Max seconds per individual ILP solve. + total_timeout: Max total seconds for the entire distance computation. + early_stop: Stop immediately when d ≤ this value. Pass ``None`` to + disable early stopping and iterate over every logical (required + when the goal is to certify an exact distance). + verbose: Log progress. + + Returns: + (d, details) where d is the exact distance (or best upper bound on + timeout) and details contains d_x, d_z, num_logicals_checked, time_s, + and exact (bool indicating whether the result is provably exact). + """ + # Convert 0 = unlimited to effectively infinite budget + if timeout_per_logical <= 0: + timeout_per_logical = float("inf") + if total_timeout <= 0: + total_timeout = float("inf") + + n = code.num_qudits + k = code.dimension + if k == 0: + return n, {"d_x": n, "d_z": n, "k": 0, "exact": True, + "num_logicals_checked": 0, "total_logicals": 0, + "time_s": 0.0} + + hx, hz, lx, lz = get_code_matrices(code) + t_start = time.monotonic() + logicals_checked = 0 + logicals_optimal = 0 # Proven optimal by solver + logicals_incumbent = 0 # Feasible solution found but not proven optimal + all_solved = True # Track whether all logicals were solved (no timeouts) + + # Per-logical timeout is passed through unmodified. The caller + # (evaluator) sets the budget; total_timeout is enforced via the + # _remaining() check before each logical. This avoids the old + # adaptive formula that starved per-logical time when k was large + # (e.g. k=24 with total=300s → only 6s/logical, far too low). + + def _remaining(): + return max(0, total_timeout - (time.monotonic() - t_start)) + + # --- Z-distance: min-weight Z-op commuting with X-checks --- + d_z = n + any_z_found = False # Any feasible solution (optimal or incumbent) + for i in range(k): + remaining = _remaining() + if remaining <= 0: + all_solved = False + break + timeout = min(timeout_per_logical, remaining) + w, optimal = ilp_min_weight(hx, lx[i], timeout=timeout) + logicals_checked += 1 + if w is not None: + d_z = min(d_z, w) + any_z_found = True + if optimal: + logicals_optimal += 1 + else: + logicals_incumbent += 1 + all_solved = False + if verbose: + tag = "" if optimal else " (incumbent)" + logger.info("Z[%d]: d=%d%s (%.1fs)", i, w, tag, + time.monotonic() - t_start) + else: + all_solved = False + if verbose: + logger.info("Z[%d]: no solution (%.1fs)", i, + time.monotonic() - t_start) + if early_stop is not None and d_z <= early_stop: + break + + # --- Cross-type early exit --- + # d = min(d_X, d_Z). If d_Z is already very low, no point computing d_X. + if early_stop is not None and d_z <= early_stop: + d = d_z + elapsed = time.monotonic() - t_start + # d_z ≤ early_stop was found as a feasible solution (optimal or + # incumbent). Either way, d ≤ d_z is a valid upper bound. + # exact=False because d_x was not computed -- we cannot prove d_x >= d_z. + return d, { + "d_x": n, # Not computed + "d_z": d_z, + "k": k, + "exact": False, + "d_x_computed": False, + "num_logicals_checked": logicals_checked, + "logicals_optimal": logicals_optimal, + "logicals_incumbent": logicals_incumbent, + "total_logicals": 2 * k, + "time_s": elapsed, + "timeout_per_logical": timeout_per_logical, + } + + # --- X-distance: min-weight X-op commuting with Z-checks --- + d_x = n + any_x_found = False + for i in range(k): + remaining = _remaining() + if remaining <= 0: + all_solved = False + break + timeout = min(timeout_per_logical, remaining) + w, optimal = ilp_min_weight(hz, lz[i], timeout=timeout) + logicals_checked += 1 + if w is not None: + d_x = min(d_x, w) + any_x_found = True + if optimal: + logicals_optimal += 1 + else: + logicals_incumbent += 1 + all_solved = False + if verbose: + tag = "" if optimal else " (incumbent)" + logger.info("X[%d]: d=%d%s (%.1fs)", i, w, tag, + time.monotonic() - t_start) + else: + all_solved = False + if verbose: + logger.info("X[%d]: no solution (%.1fs)", i, + time.monotonic() - t_start) + # Early exit: d_X already below d_Z, no need to check more + if early_stop is not None and d_x <= early_stop: + break + + elapsed = time.monotonic() - t_start + + # If no feasible solution was found on either side, distance is unknown. + # Use d = early_stop + 1 as a conservative lower bound: if d ≤ early_stop, + # the solver would have found it near-instantly, so d > early_stop. + if not any_z_found and not any_x_found: + # No logical solved on either side. See compute_distance_milp_symplectic + # for the rationale: early_stop+1 is a real lower bound when the caller + # supplied a threshold; otherwise return the vacuous d=n. + d_lower = (early_stop + 1) if early_stop is not None else n + return d_lower, { + "d_x": 0, + "d_z": 0, + "k": k, + "exact": False, + "d_x_computed": True, + "num_logicals_checked": logicals_checked, + "logicals_optimal": logicals_optimal, + "logicals_incumbent": logicals_incumbent, + "total_logicals": 2 * k, + "time_s": elapsed, + "timeout_per_logical": timeout_per_logical, + "all_timeout": True, + "d_is_lower_bound": early_stop is not None, + } + + # Use the best feasible values found. Unsolved sides stay at n + # (trivially valid upper bound). + d = min(d_x, d_z) + + return d, { + "d_x": d_x if any_x_found else 0, + "d_z": d_z if any_z_found else 0, + "k": k, + "exact": all_solved, + "d_x_computed": True, + "num_logicals_checked": logicals_checked, + "logicals_optimal": logicals_optimal, + "logicals_incumbent": logicals_incumbent, + "total_logicals": 2 * k, + "time_s": elapsed, + "timeout_per_logical": timeout_per_logical, + } + + +# --------------------------------------------------------------------------- +# Non-CSS (symplectic) MILP formulation +# --------------------------------------------------------------------------- + + +def ilp_min_weight_symplectic(stabilizer_matrix, logical_op, timeout=30): + """Find minimum symplectic-weight Pauli in the coset logical_op + stabilizers. + + Uses the symplectic ILP of Landahl, Anderson, and Rice (arXiv:1108.5738, + 2011) with the standard linear encoding of the per-qubit binary OR + (w_j = x_j OR z_j) via w_j >= x_j and w_j >= z_j; the upper-bound + constraint w_j <= x_j + z_j is omitted because the minimization objective + drives w_j down to max(x_j, z_j) on its own. (Note: this is the convex-hull + description of binary OR, not McCormick relaxation -- McCormick envelopes + apply to bilinear products of continuous variables.) + + For non-CSS codes, each Pauli operator is (x_1..x_n, z_1..z_n) and its + symplectic weight is the number of qubits i where x_i OR z_i is nonzero. + + Variables: + - x_j, z_j: binary, the Pauli operator on qubit j (2n vars) + - w_j: binary, 1 if qubit j has nontrivial support (n vars) + - s_r: integer slack for mod-2 commutation constraints (num_stabs vars) + - t: integer slack for the anticommutation constraint (1 var) + + Objective: minimize sum(w_j) + + Constraints: + - w_j >= x_j and w_j >= z_j (symplectic weight linearization) + - For each stabilizer s: sum_j(s_xj * z_j + s_zj * x_j) - 2*s_r = 0 + (commutation, mod-2 encoded via integer slack) + - For the target logical L: sum_j(L_xj * z_j + L_zj * x_j) - 2*t = 1 + (anticommutation) + + Args: + stabilizer_matrix: (num_stabs, 2n) binary symplectic matrix. + logical_op: (2n,) binary vector of a logical operator. + timeout: solver time limit in seconds. + + Returns: + (weight, optimal) tuple. Returns (None, False) if no feasible + solution found. + """ + num_stabs, two_n = stabilizer_matrix.shape + n = two_n // 2 + + # Variable layout: [x_0..x_{n-1}, z_0..z_{n-1}, w_0..w_{n-1}, + # s_0..s_{num_stabs-1}, t] + num_vars = 2 * n + n + num_stabs + 1 + idx_x = slice(0, n) + idx_z = slice(n, 2 * n) + idx_w = slice(2 * n, 3 * n) + # Stabilizer slack vars (s_0..s_{num_stabs-1}) live at indices + # [3n, 3n + num_stabs); they are indexed directly below. + idx_t = 3 * n + num_stabs + + # Objective: minimize sum(w_j) + c = np.zeros(num_vars) + c[idx_w] = 1.0 + + # --- Constraints --- + rows = [] + row_lb = [] + row_ub = [] + + # 1. w_j >= x_j => w_j - x_j >= 0 + for j in range(n): + row = np.zeros(num_vars) + row[2 * n + j] = 1 # w_j + row[j] = -1 # -x_j + rows.append(row) + row_lb.append(0) + row_ub.append(np.inf) + + # 2. w_j >= z_j => w_j - z_j >= 0 + for j in range(n): + row = np.zeros(num_vars) + row[2 * n + j] = 1 # w_j + row[n + j] = -1 # -z_j + rows.append(row) + row_lb.append(0) + row_ub.append(np.inf) + + # 3. Commutation with each stabilizer: + # sum_j(s_xj * z_j + s_zj * x_j) - 2*s_r = 0 + for r in range(num_stabs): + row = np.zeros(num_vars) + s_x = stabilizer_matrix[r, :n] # X-part of stabilizer + s_z = stabilizer_matrix[r, n:] # Z-part of stabilizer + # symplectic inner product: s_x . z + s_z . x + row[idx_z] = s_x # s_xj * z_j + row[idx_x] = s_z # s_zj * x_j + row[3 * n + r] = -2 # -2 * s_r (slack) + rows.append(row) + row_lb.append(0) + row_ub.append(0) + + # 4. Anticommutation with target logical: + # sum_j(L_xj * z_j + L_zj * x_j) - 2*t = 1 + row = np.zeros(num_vars) + L_x = logical_op[:n] + L_z = logical_op[n:] + row[idx_z] = L_x + row[idx_x] = L_z + row[idx_t] = -2 + rows.append(row) + row_lb.append(1) + row_ub.append(1) + + A_mat = np.array(rows) + constraints = LinearConstraint(A_mat, row_lb, row_ub) + + # Bounds + lb = np.zeros(num_vars) + ub = np.ones(num_vars) + # Slack vars for stabilizer commutation: s_r can be up to ceil(weight/2) + for r in range(num_stabs): + ub[3 * n + r] = np.ceil(np.sum(stabilizer_matrix[r]) / 2) + # Slack for anticommutation + ub[idx_t] = np.ceil(np.sum(logical_op) / 2) + + bounds = Bounds(lb, ub) + integrality = np.ones(num_vars) + + opts = {"presolve": True} + if 0 < timeout < 1e9: + opts["time_limit"] = timeout + + result = milp( + c=c, + constraints=constraints, + integrality=integrality, + bounds=bounds, + options=opts, + ) + + if result.x is not None: + w = int(round(result.fun)) + return w, result.success + return None, False + + +def compute_distance_milp_symplectic( + code, + *, + timeout_per_logical: int = 30, + total_timeout: int = 120, + early_stop: int | None = 4, + verbose: bool = False, +) -> tuple[int, dict]: + """Compute code distance via symplectic MILP for non-CSS codes. + + Unlike the CSS version which separates d_X and d_Z, this formulation + works with the full symplectic representation and minimizes symplectic + weight directly. + + Args: + code: A qubit stabilizer code, non-CSS (qldpc's ``QuditCode``). + timeout_per_logical: Max seconds per individual ILP solve. + total_timeout: Max total seconds for the entire computation. + early_stop: Stop immediately when d <= this value. Pass ``None`` to + disable early stopping and iterate over every logical (required + when the goal is to certify an exact distance). + verbose: Log progress. + + Returns: + (d, details) where d is the distance (or best upper bound on + timeout) and details dict. + """ + if timeout_per_logical <= 0: + timeout_per_logical = float("inf") + if total_timeout <= 0: + total_timeout = float("inf") + + n = code.num_qudits + k = code.dimension + if k == 0: + return n, {"k": 0, "exact": True, "num_logicals_checked": 0, + "total_logicals": 0, "time_s": 0.0} + + # Get stabilizer matrix and logical operators + # Use our own GF(2) computation for logicals -- qldpc's get_logical_ops() + # has bugs for some non-CSS codes (singular matrix errors). + from evaluation.pbb_code import get_symplectic_logicals + stab_matrix = np.array(code.matrix, dtype=int) % 2 + logicals = get_symplectic_logicals(code) + num_logicals = logicals.shape[0] # 2k logicals + + t_start = time.monotonic() + logicals_checked = 0 + logicals_optimal = 0 + logicals_incumbent = 0 + all_solved = True + d_best = n + any_found = False + + def _remaining(): + return max(0, total_timeout - (time.monotonic() - t_start)) + + for i in range(num_logicals): + remaining = _remaining() + if remaining <= 0: + all_solved = False + break + timeout = min(timeout_per_logical, remaining) + w, optimal = ilp_min_weight_symplectic(stab_matrix, logicals[i], + timeout=timeout) + logicals_checked += 1 + if w is not None: + d_best = min(d_best, w) + any_found = True + if optimal: + logicals_optimal += 1 + else: + logicals_incumbent += 1 + all_solved = False + if verbose: + tag = "" if optimal else " (incumbent)" + logger.info("L[%d]: d=%d%s (%.1fs)", i, w, tag, + time.monotonic() - t_start) + else: + all_solved = False + if verbose: + logger.info("L[%d]: no solution (%.1fs)", i, + time.monotonic() - t_start) + if early_stop is not None and d_best <= early_stop: + break + + elapsed = time.monotonic() - t_start + + if not any_found: + # No logical was solved. If the caller supplied an early-stop + # threshold, "no solution found" implies d > early_stop (the solver + # would have hit a small d quickly), so report early_stop+1 as a + # lower bound. If early_stop is None we have no informative bound; + # report d = n (vacuous upper bound) so the standard + # ``milp_worked = d_milp < n`` check at call sites correctly rejects + # the result. + if early_stop is not None: + d_lower = early_stop + 1 + d_is_lb = True + else: + d_lower = n + d_is_lb = False + return d_lower, { + "k": k, + "exact": False, + "num_logicals_checked": logicals_checked, + "logicals_optimal": logicals_optimal, + "logicals_incumbent": logicals_incumbent, + "total_logicals": num_logicals, + "time_s": elapsed, + "timeout_per_logical": timeout_per_logical, + "all_timeout": True, + "d_is_lower_bound": d_is_lb, + } + + return d_best, { + "k": k, + "exact": all_solved, + "num_logicals_checked": logicals_checked, + "logicals_optimal": logicals_optimal, + "logicals_incumbent": logicals_incumbent, + "total_logicals": num_logicals, + "time_s": elapsed, + "timeout_per_logical": timeout_per_logical, + } diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/evaluator.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/evaluator.py new file mode 100644 index 0000000..9a22a00 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/evaluator.py @@ -0,0 +1,744 @@ +"""Multi-stage evaluation cascade for bivariate bicycle code candidates. + +This is the central evaluation module. Every candidate polynomial pair +is assessed through a five-stage cascade that progressively invests more +compute in more promising candidates: + +1. **Validate** (microseconds) -- Checks polynomial structure: 2-6 + distinct terms with exponents in range for the target lattice. + See :func:`evaluation.bb_code.validate_terms`. + +2. **Build & compute k** (milliseconds) -- Constructs a + :class:`qldpc.codes.BBCode` and reads ``(n, k)`` via GF(2) rank. + Rejects codes with ``k = 0`` (stage ``k_zero``) or ``k < 4`` + (stage ``k_low``, score ``-1000 + k``). + +3. **Quick distance estimate** (seconds) -- Runs + :func:`evaluation.distance.estimate_distance` with ``quick_trials`` + BP-OSD trials (default 100). Codes with ``d <= 2`` are tagged + ``trivial_distance`` and exit early. The result carries a + ``distance_trusted`` flag based on ``d / sqrt(n)`` vs + :data:`DISTANCE_TRUST_RATIO`. + +4. **Refined distance estimate** (seconds) -- Triggered when preliminary + ``FOM >= fom_threshold_refine`` (default 6.0). Runs 3 independent + BP-OSD batches of ``refine_trials`` each and keeps the minimum. + An additional OSD-CS order-10 verification pass + (:func:`evaluation.distance.estimate_distance_osd_cs`) fires when + ``FOM >= fom_threshold_exact``. + +5. **Exact distance** (minutes) -- Triggered when refined + ``FOM >= fom_threshold_exact`` (default 8.0). Calls + :func:`evaluation.distance.compute_distance_exact` with a configurable + timeout. Returns ``None`` on timeout (stage ``exact_timeout``). + +The cascade is orchestrated by :func:`evaluate_candidate`, which returns +a dict with keys ``n, k, d, d_is_exact, distance_trusted, fom, +encoding_rate, ell, m, A_terms, B_terms, score, stage``. + +:func:`evaluate_batch` and :func:`evaluate_lattices` provide batch +wrappers for evaluating lists of candidates across one or many lattices. + +Constants +--------- +SCORE_REJECTED : float + Score assigned to all rejected candidates (``-inf``). +DISTANCE_TRUST_RATIO : float + ``d / sqrt(n)`` threshold below which BP-OSD distance is fully trusted + (1.3, just above the highest verified ratio of 1.26). +DISTANCE_UNTRUST_RATIO : float + ``d / sqrt(n)`` threshold above which BP-OSD distance is discarded + entirely (2.0, well below the lowest observed degenerate ratio of 2.5). + Used by :mod:`evolve.openevolve_evaluator` to compute credible FOM + with linear interpolation in the gap between the two thresholds. +""" + +from __future__ import annotations + +import logging +import math + +from evaluation.bb_code import build_bb_code, validate_terms, get_code_params_fast +from evaluation.distance import estimate_distance, estimate_distance_osd_cs, compute_distance_exact +from evaluation.distance_milp import compute_distance_milp, symplectic_weight_bound + +logger = logging.getLogger(__name__) + +SCORE_REJECTED = float("-inf") + +# Trust boundaries for BP-OSD distance estimates (d/sqrt(n) ratio). +# +# Empirical observations on BB codes: +# Known good codes: d/√n = 0.71 ([[72,12,6]]), 1.0 ([[144,12,12]]), +# 1.06 ([[288,12,18]]), 1.26 ([[360,12,24]]) +# Degenerate codes: d/√n = 2.5-4.0 (BP-OSD wildly overestimates) +# Gap: No observed code has d/√n in (1.26, 2.5) +# +# DISTANCE_TRUST_RATIO: below this, BP-OSD d is fully trusted. +# Set to 1.3, just above the highest verified d/√n (1.26), so all known +# good codes get full FOM while leaving room for near-miss discoveries. +# +# DISTANCE_UNTRUST_RATIO: above this, BP-OSD d is discarded (use k/n only). +# Set to 2.0, well below the lowest observed degenerate ratio (2.5). The +# wide 1.3-2.0 decay zone provides a smooth gradient for borderline cases. +# Tightening to e.g. √2 ≈ 1.41 would penalize plausible discoveries in +# the uncharted 1.3-1.5 regime without empirical justification. +DISTANCE_TRUST_RATIO = 1.3 +DISTANCE_UNTRUST_RATIO = 2.0 + + +MIN_K_THRESHOLD = 4 +SCORE_K_LOW_PENALTY = -1000.0 + + +def compute_fom(n: int, k: int, d: int) -> float: + """Compute figure of merit kd²/n.""" + if n == 0 or k == 0 or d == 0: + return 0.0 + return k * d * d / n + + +def _make_result_template( + ell: int, m: int, A_terms: list, B_terms: list +) -> dict: + """Create a default result dict for a candidate.""" + return { + "ell": ell, + "m": m, + "A_terms": A_terms, + "B_terms": B_terms, + "n": 2 * ell * m, + "k": 0, + "d": 0, + "d_is_exact": False, + "distance_trusted": False, + "fom": 0.0, + "encoding_rate": 0.0, + "score": SCORE_REJECTED, + "stage": "rejected", + } + + +def _validate_and_build( + ell: int, m: int, A_terms: list, B_terms: list, result: dict +) -> tuple | None: + """Validate terms, build code, compute k, apply early-exit rules. + + Returns (code, n, k) on success, or None if the candidate was rejected + (result dict is updated in place with the rejection reason). + """ + # Stage 1: Validate inputs + try: + validate_terms(ell, m, A_terms, "A") + validate_terms(ell, m, B_terms, "B") + except ValueError as e: + logger.debug("Validation failed: %s", e) + result["stage"] = "invalid" + return None + + # Stage 2: Build code, compute k + try: + code = build_bb_code(ell, m, A_terms, B_terms) + n, k = get_code_params_fast(code) + except Exception as e: + logger.debug("Construction failed: %s", e) + result["stage"] = "construction_error" + return None + + result["n"] = n + result["k"] = k + result["encoding_rate"] = k / n if n > 0 else 0.0 + + if k == 0: + result["stage"] = "k_zero" + return None + if k < MIN_K_THRESHOLD: + result["stage"] = "k_low" + result["score"] = SCORE_K_LOW_PENALTY + k + return None + + # Self-dual gate: BB codes with A=B always have d=2 (proven). + # BP-OSD misses this in 29/30 batches, so we hard-code it. + if sorted(tuple(t) for t in A_terms) == sorted(tuple(t) for t in B_terms): + result["d"] = 2 + result["d_is_exact"] = True + result["distance_trusted"] = True + result["fom"] = k * 4 / n + result["score"] = result["fom"] + result["stage"] = "self_dual_d2" + return None + + return code, n, k + + +def evaluate_candidate( + ell: int, + m: int, + A_terms: list[tuple[int, int]], + B_terms: list[tuple[int, int]], + *, + quick: bool = False, + fom_threshold_refine: float = 6.0, + fom_threshold_exact: float = 8.0, + quick_trials: int = 100, + refine_trials: int = 500, + exact_timeout: int = 300, +) -> dict: + """Evaluate a BB code candidate through the full cascade. + + Args: + ell: Cyclic group order for x. + m: Cyclic group order for y. + A_terms: 3 exponent pairs for polynomial A. + B_terms: 3 exponent pairs for polynomial B. + quick: If True, only compute k (skip distance). + fom_threshold_refine: FOM threshold to trigger refined distance estimation. + fom_threshold_exact: FOM threshold to trigger exact distance computation. + quick_trials: Number of BP-OSD trials for initial estimate. + refine_trials: Number of BP-OSD trials for refined estimate. + exact_timeout: Timeout in seconds for exact distance computation. + + Returns: + Dict with keys: n, k, d, d_is_exact, fom, encoding_rate, + ell, m, A_terms, B_terms, score, stage. + """ + result = _make_result_template(ell, m, A_terms, B_terms) + + built = _validate_and_build(ell, m, A_terms, B_terms, result) + if built is None: + return result + code, n, k = built + + if quick: + result["stage"] = "quick_k_only" + result["score"] = k / n # use encoding rate as proxy + return result + + # Stage 3: Quick distance estimate + d_upper = estimate_distance(code, num_trials=quick_trials) + if d_upper <= 2: + result["d"] = d_upper + result["distance_trusted"] = True # d≤2 is always reliable + result["fom"] = compute_fom(n, k, d_upper) + result["score"] = result["fom"] + result["stage"] = "trivial_distance" + return result + + result["d"] = d_upper + result["distance_trusted"] = d_upper <= DISTANCE_TRUST_RATIO * math.sqrt(n) + fom = compute_fom(n, k, d_upper) + result["fom"] = fom + result["score"] = fom + result["stage"] = "quick_estimate" + + # Stage 4: Refined distance estimate for promising candidates. + # Run 3 independent BP-OSD batches (OSD_0) and take the minimum to reduce + # variance. BP-OSD is an upper bound, so min of multiple runs is + # tighter and more stable. + if fom >= fom_threshold_refine: + for _ in range(3): + d_refined = estimate_distance(code, num_trials=refine_trials) + d_upper = min(d_upper, d_refined) + result["d"] = d_upper + result["distance_trusted"] = d_upper <= DISTANCE_TRUST_RATIO * math.sqrt(n) + fom = compute_fom(n, k, d_upper) + result["fom"] = fom + result["score"] = fom + result["stage"] = "refined_estimate" + + # Stage 4b: OSD-CS verification for top candidates. + # OSD-CS order=10 finds tighter bounds than OSD_0 for 7 of 9 tested + # codes (4-12 point improvements on high-k codes). One batch of 200 + # trials catches the worst overestimates before they pollute fitness. + if fom >= fom_threshold_exact: + d_cs = estimate_distance_osd_cs(code, num_trials=200) + if d_cs < d_upper: + d_upper = d_cs + result["d"] = d_upper + result["distance_trusted"] = d_upper <= DISTANCE_TRUST_RATIO * math.sqrt(n) + fom = compute_fom(n, k, d_upper) + result["fom"] = fom + result["score"] = fom + result["stage"] = "osd_cs_verified" + + # Stage 5: Exact distance for top candidates + if fom >= fom_threshold_exact: + d_exact = compute_distance_exact(code, timeout_seconds=exact_timeout) + if d_exact is not None: + result["d"] = d_exact + result["d_is_exact"] = True + result["distance_trusted"] = True + result["fom"] = compute_fom(n, k, d_exact) + result["score"] = result["fom"] + result["stage"] = "exact" + else: + result["stage"] = "exact_timeout" + + return result + + +def evaluate_batch( + ell: int, + m: int, + candidates: list[tuple[list[tuple[int, int]], list[tuple[int, int]]]], + **kwargs, +) -> list[dict]: + """Evaluate a batch of (A_terms, B_terms) candidates for a given lattice. + + Args: + ell: Cyclic group order for x. + m: Cyclic group order for y. + candidates: List of (A_terms, B_terms) tuples, as returned by + generate_candidates(ell, m). + **kwargs: Passed to evaluate_candidate. + + Returns: + List of result dicts, sorted by score descending. + """ + results = [] + for A_terms, B_terms in candidates: + result = evaluate_candidate(ell, m, A_terms, B_terms, **kwargs) + results.append(result) + if result["score"] > 0: + logger.info( + "[[%d, %d, %d]] FOM=%.2f (stage=%s)", + result["n"], result["k"], result["d"], + result["fom"], result["stage"], + ) + results.sort(key=lambda r: r["score"], reverse=True) + return results + + +def evaluate_lattices( + lattices: list[tuple[int, int]], + generate_fn, + **kwargs, +) -> list[dict]: + """Run evaluation across multiple lattice dimensions. + + Args: + lattices: List of (ell, m) pairs to search. + generate_fn: A function (ell, m) -> list of (A_terms, B_terms). + **kwargs: Passed to evaluate_candidate. + + Returns: + All results across all lattices, sorted by score descending. + """ + all_results = [] + for ell, m in lattices: + candidates = generate_fn(ell, m) + logger.info( + "Evaluating %d candidates for lattice (%d, %d), n=%d", + len(candidates), ell, m, 2 * ell * m, + ) + results = evaluate_batch(ell, m, candidates, **kwargs) + all_results.extend(results) + all_results.sort(key=lambda r: r["score"], reverse=True) + return all_results + + +# ── MILP-based evaluation (Campaign 4+) ───────────────────────── + + +def evaluate_candidate_milp( + ell: int, + m: int, + A_terms: list[tuple[int, int]], + B_terms: list[tuple[int, int]], + *, + quick: bool = False, + milp_timeout_per_logical: int = 30, + milp_total_timeout: int = 120, + milp_early_stop: int = 4, +) -> dict: + """Evaluate a BB code candidate using MILP for exact distance. + + Simplified 3-stage cascade (vs 5-stage BP-OSD cascade): + 1. Validate + build + compute k (microseconds) + 2. Quick k-only return if quick=True (microseconds) + 3. MILP exact distance (sub-second for d≤4, seconds to minutes for d≥6) + + All distances are exact -- no trust ratio filtering needed. + + Args: + ell: Cyclic group order for x. + m: Cyclic group order for y. + A_terms: 3 exponent pairs for polynomial A. + B_terms: 3 exponent pairs for polynomial B. + quick: If True, only compute k (skip distance). + milp_timeout_per_logical: Timeout per individual ILP solve. + milp_total_timeout: Total timeout for all logicals combined. + milp_early_stop: Stop immediately when d ≤ this value. + + Returns: + Dict with keys: n, k, d, d_is_exact, distance_trusted, fom, + encoding_rate, ell, m, A_terms, B_terms, score, stage, milp_details. + """ + result = _make_result_template(ell, m, A_terms, B_terms) + + built = _validate_and_build(ell, m, A_terms, B_terms, result) + if built is None: + return result + code, n, k = built + + # Symplectic weight: instant upper bound on d from Gaussian elimination + d_symp, _, _ = symplectic_weight_bound(code) + result["d_symplectic"] = d_symp + + if quick: + result["stage"] = "quick_k_only" + result["score"] = k / n + return result + + # Pre-filter using symplectic weight bound (instant, no MILP needed). + # d_symp is an upper bound on d from Gaussian elimination. + # - d_symp ≤ 2: provably exact (BB codes with k>0 have d ≥ 2) + # - d_symp ≤ early_stop: MILP would solve in <1s anyway (d ≤ d_symp ≤ 4), + # but we can report d_symp directly as a valid upper bound and skip MILP. + # This saves hundreds of MILP calls per iteration. + if d_symp <= milp_early_stop: + result["d"] = d_symp + result["d_is_exact"] = d_symp <= 2 # Only d≤2 is provably exact + result["distance_trusted"] = True # Valid upper bound + result["fom"] = compute_fom(n, k, d_symp) + result["score"] = result["fom"] + result["stage"] = "symplectic_low_d" + return result + + # Stage 3: MILP exact distance + d, details = compute_distance_milp( + code, + timeout_per_logical=milp_timeout_per_logical, + total_timeout=milp_total_timeout, + early_stop=milp_early_stop, + ) + + result["milp_details"] = details + + if details.get("all_timeout"): + # No feasible solution at all -- solver couldn't even find an + # incumbent. d > early_stop is a valid lower bound, but we have + # NO upper bound. Don't report phantom FOM from a lower bound; + # it would inflate combined_score with fictitious values. + result["d"] = 0 + result["d_lower_bound"] = milp_early_stop + 1 + result["d_is_exact"] = False + result["distance_trusted"] = False + result["fom"] = 0.0 + result["score"] = 0.01 # Tiny positive: promising (d > early_stop) + result["stage"] = "milp_promising_timeout" + else: + result["d"] = d + result["d_is_exact"] = details["exact"] + result["distance_trusted"] = True # Incumbent or optimal -- valid upper bound + result["fom"] = compute_fom(n, k, d) + result["score"] = result["fom"] + if d <= milp_early_stop: + result["stage"] = "milp_low_d" + elif details["exact"]: + result["stage"] = "milp_exact" + else: + # Incumbents found but not all proven optimal -- d is an upper + # bound on true distance, so FOM is an upper bound too. + result["stage"] = "milp_incumbent" + + return result + + +def evaluate_batch_milp( + ell: int, + m: int, + candidates: list[tuple[list[tuple[int, int]], list[tuple[int, int]]]], + **kwargs, +) -> list[dict]: + """Evaluate a batch of candidates using MILP distance. + + Args: + ell: Cyclic group order for x. + m: Cyclic group order for y. + candidates: List of (A_terms, B_terms) tuples. + **kwargs: Passed to evaluate_candidate_milp. + + Returns: + List of result dicts, sorted by score descending. + """ + results = [] + for A_terms, B_terms in candidates: + result = evaluate_candidate_milp(ell, m, A_terms, B_terms, **kwargs) + results.append(result) + if result["score"] > 0: + logger.info( + "MILP [[%d, %d, %d]] FOM=%.2f (stage=%s, %.1fs)", + result["n"], result["k"], result["d"], + result["fom"], result["stage"], + result.get("milp_details", {}).get("time_s", 0), + ) + results.sort(key=lambda r: r["score"], reverse=True) + return results + + +# ── Parallel k-screening ───────────────────────────────────────── + + +def evaluate_batch_milp_parallel( + tasks: list[tuple[int, int, list, list]], + *, + max_workers: int | None = None, + **kwargs, +) -> list[dict]: + """Evaluate multiple candidates in parallel using ProcessPoolExecutor. + + Designed for the k-only screening phase where each candidate takes + ~20-70ms (depending on lattice size) and there are 100k+ candidates. + With 10 workers, this gives ~10x speedup over sequential evaluation. + + Args: + tasks: List of (ell, m, A_terms, B_terms) tuples. + max_workers: Number of parallel processes. Defaults to + min(cpu_count - 2, 10). + **kwargs: Passed to evaluate_candidate_milp (e.g. quick=True). + + Returns: + List of result dicts (same order as tasks). + """ + import os + from concurrent.futures import ProcessPoolExecutor + + if not tasks: + return [] + + if max_workers is None: + max_workers = min((os.cpu_count() or 4) - 2, 10) + max_workers = max(1, max_workers) + + worker_args = [(ell, m, A, B, kwargs) for ell, m, A, B in tasks] + + # chunksize=200 reduces IPC overhead: each worker processes 200 + # candidates per round-trip instead of 1. + logger.info( + "Parallel k-screening: %d tasks across %d workers", + len(tasks), max_workers, + ) + with ProcessPoolExecutor(max_workers=max_workers) as pool: + results = list(pool.map(_milp_worker, worker_args, chunksize=200)) + + n_valid = sum(1 for r in results if r.get("k", 0) > 0) + logger.info( + "Parallel k-screening done: %d/%d with k>0", n_valid, len(results), + ) + return results + + +# ── Parallel MILP evaluation ───────────────────────────────────── + + +def _milp_worker(args): + """Worker function for parallel evaluation (k-screening or MILP). + + Runs in a separate process via ProcessPoolExecutor. + Args is a tuple: (ell, m, A_terms, B_terms, kwargs). + """ + ell, m, A_terms, B_terms, kwargs = args + return evaluate_candidate_milp(ell, m, A_terms, B_terms, **kwargs) + + +def _milp_cache_key(ell: int, m: int, A_terms, B_terms) -> tuple: + """Canonical cache key for a BB code (lattice + sorted polynomial terms).""" + a = tuple(sorted(tuple(t) for t in A_terms)) + b = tuple(sorted(tuple(t) for t in B_terms)) + return (ell, m, a, b) + + +def _load_milp_cache(path: str | None) -> dict[tuple, dict]: + """Load MILP results cache from a JSONL file. + + Returns a dict mapping cache keys to result dicts. Only caches + results with d > 0 (successful solves). For duplicate keys, keeps + the best result: exact beats non-exact; among same exactness, lower + d (tighter upper bound) wins. + """ + import json + from pathlib import Path + + cache: dict[tuple, dict] = {} + if not path: + return cache + p = Path(path) + if not p.exists(): + return cache + try: + for line in p.read_text().splitlines(): + if not line.strip(): + continue + try: + r = json.loads(line) + except json.JSONDecodeError: + continue # Skip corrupted lines, don't abort + if r.get("d", 0) <= 0: + continue + key = _milp_cache_key(r["ell"], r["m"], r["A_terms"], r["B_terms"]) + existing = cache.get(key) + if existing is None: + cache[key] = r + elif r.get("d_is_exact") and not existing.get("d_is_exact"): + # Exact result always beats non-exact + cache[key] = r + elif not r.get("d_is_exact") and existing.get("d_is_exact"): + pass # Keep the exact result + elif r.get("d", 0) < existing.get("d", 0): + # Same exactness: lower d is tighter (better upper bound) + cache[key] = r + except OSError as e: + logger.warning("MILP cache load error: %s", e) + return cache + + +def evaluate_milp_parallel( + tasks: list[tuple[int, int, list, list]], + *, + milp_timeout_per_logical: int = 300, + milp_total_timeout: int = 7200, + milp_early_stop: int = 4, + max_workers: int | None = None, + save_path: str | None = None, +) -> list[dict]: + """Evaluate multiple (ell, m, A_terms, B_terms) tasks in parallel using MILP. + + Uses ProcessPoolExecutor for true parallelism (HiGHS is single-threaded, + so each worker gets one core). Results are saved incrementally to + ``save_path`` (JSONL format) as each completes -- no code loss even if + the parent process is killed. + + Includes a disk-based cache: results from previous iterations (loaded from + ``save_path``) are reused without re-solving. This avoids the dominant + bottleneck where known seed codes are MILP-verified every iteration. + + Args: + tasks: List of (ell, m, A_terms, B_terms) tuples. + milp_timeout_per_logical: Per-logical ILP solve timeout (seconds). + milp_total_timeout: Total timeout per code (seconds, 0=unlimited). + milp_early_stop: Stop when d ≤ this value. + max_workers: Number of parallel processes. Defaults to + min(cpu_count - 2, 10). + save_path: Path to JSONL file for incremental persistence. + Each result is appended as one JSON line immediately after solving. + + Returns: + List of result dicts for all tasks, sorted by score descending. + """ + import json + import os + import time as _time + from concurrent.futures import ProcessPoolExecutor, as_completed + from pathlib import Path + + if not tasks: + return [] + + if max_workers is None: + max_workers = min((os.cpu_count() or 4) - 2, 10) + max_workers = max(1, max_workers) + + # ── Load MILP cache from previous results ────────────────────── + cache = _load_milp_cache(save_path) + + # Separate cached vs uncached tasks + cached_results = [] + uncached_tasks = [] + for ell, m, A_terms, B_terms in tasks: + key = _milp_cache_key(ell, m, A_terms, B_terms) + cached = cache.get(key) + if cached is not None: + cached_results.append(cached) + else: + uncached_tasks.append((ell, m, A_terms, B_terms)) + + if cached_results: + logger.info( + "MILP cache: %d/%d tasks cached, %d to solve", + len(cached_results), len(tasks), len(uncached_tasks), + ) + + kwargs = { + "milp_timeout_per_logical": milp_timeout_per_logical, + "milp_total_timeout": milp_total_timeout, + "milp_early_stop": milp_early_stop, + } + + # Prepare worker arguments for uncached tasks only + worker_args = [ + (ell, m, A_terms, B_terms, kwargs) + for ell, m, A_terms, B_terms in uncached_tasks + ] + + results = list(cached_results) + save_file = Path(save_path) if save_path else None + if save_file: + save_file.parent.mkdir(parents=True, exist_ok=True) + + if not worker_args: + logger.info( + "MILP cache: all %d tasks cached, skipping solver", + len(tasks), + ) + results.sort(key=lambda r: r.get("score", float("-inf")), reverse=True) + return results + + t0 = _time.monotonic() + with ProcessPoolExecutor(max_workers=max_workers) as executor: + futures = { + executor.submit(_milp_worker, args): i + for i, args in enumerate(worker_args) + } + + for future in as_completed(futures): + idx = futures[future] + try: + result = future.result() + except Exception as e: + ell, m, A_terms, B_terms, _ = worker_args[idx] + logger.error( + "MILP worker error for (%d,%d) A=%s: %s", + ell, m, A_terms, e, + ) + result = { + "ell": ell, "m": m, + "A_terms": A_terms, "B_terms": B_terms, + "n": 2 * ell * m, "k": 0, "d": 0, + "d_is_exact": False, "fom": 0.0, + "score": float("-inf"), + "stage": "worker_error", + "error": str(e), + } + + results.append(result) + + # Incremental save: append to JSONL immediately + if save_file and result.get("d", 0) > 0: + result["_saved_at"] = _time.time() + try: + with open(save_file, "a") as f: + f.write(json.dumps(result, default=str) + "\n") + except OSError: + pass # Don't fail evaluation over persistence + + # Log progress + d = result.get("d", 0) + if d > 0: + logger.info( + "MILP [%d/%d] [[%d,%d,%d]] FOM=%.2f (%s, %.1fs)", + len(results) - len(cached_results), len(uncached_tasks), + result["n"], result["k"], d, + result.get("fom", 0), result.get("stage", "?"), + result.get("milp_details", {}).get("time_s", 0), + ) + + elapsed = _time.monotonic() - t0 + logger.info( + "Parallel MILP: %d new + %d cached = %d total, %d workers, %.1fs", + len(uncached_tasks), len(cached_results), len(results), + max_workers, elapsed, + ) + + results.sort(key=lambda r: r.get("score", float("-inf")), reverse=True) + return results diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/results.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/results.py new file mode 100644 index 0000000..af13bef --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evaluation/results.py @@ -0,0 +1,181 @@ +"""JSON persistence for discovered codes and Pareto front tracking. + +Discovered codes are appended to a JSON file (default +``results/discovered_codes.json``) and deduplicated by their defining +parameters ``(ell, m, A_terms, B_terms)``. + +A Pareto front is maintained in a separate file (default +``results/pareto_front.json``). A code is Pareto-optimal if no other +code dominates it in the three-dimensional space of ``(k/n, d, 1/n)`` -- +i.e., no other code has both higher encoding rate *and* higher distance +at the same or smaller block size. The front is incrementally updated: +each call merges new results with the existing front on disk, deduplicates, +and recomputes the non-dominated set. + +Public API +---------- +* :func:`save_code` -- append a single code to the discovered-codes file. +* :func:`load_codes` -- read all codes from the file. +* :func:`update_pareto_front` -- merge new results into the Pareto front. +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +logger = logging.getLogger(__name__) + +RESULTS_DIR = Path(__file__).parent.parent / "results" + + +def save_code(result: dict, filepath: Path | str | None = None) -> None: + """Append a discovered code to the results JSON file. + + Args: + result: Evaluation result dict from evaluate_candidate. + filepath: Path to JSON file. Defaults to results/discovered_codes.json. + """ + filepath = Path(filepath or RESULTS_DIR / "discovered_codes.json") + filepath.parent.mkdir(parents=True, exist_ok=True) + + existing = load_codes(filepath) + + # Avoid duplicates by (ell, m, A_terms, B_terms) key + key = _code_key(result) + existing_keys = {_code_key(r) for r in existing} + if key not in existing_keys: + existing.append(result) + + with open(filepath, "w") as f: + json.dump(existing, f, indent=2, default=str) + + +def load_codes(filepath: Path | str | None = None) -> list[dict]: + """Load discovered codes from JSON file. + + Args: + filepath: Path to JSON file. Defaults to results/discovered_codes.json. + + Returns: + List of result dicts. + """ + filepath = Path(filepath or RESULTS_DIR / "discovered_codes.json") + if not filepath.exists(): + return [] + try: + with open(filepath) as f: + return json.load(f) + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Corrupted codes file %s: %s", filepath, e) + return [] + + +def update_pareto_front( + results: list[dict], filepath: Path | str | None = None +) -> list[dict]: + """Update the Pareto front of discovered codes. + + Merges new results with the existing front on disk, then recomputes. + A code is Pareto-optimal if no other code has both higher k/n AND + higher d (at the same or smaller n). + + Args: + results: List of evaluation result dicts. + filepath: Path to save the front. Defaults to results/pareto_front.json. + + Returns: + The Pareto-optimal codes. + """ + filepath = Path(filepath or RESULTS_DIR / "pareto_front.json") + filepath.parent.mkdir(parents=True, exist_ok=True) + + # Merge with existing front on disk + existing_front = [] + if filepath.exists(): + try: + with open(filepath) as f: + existing_front = json.load(f) + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Corrupted pareto front file %s: %s", filepath, e) + + # Deduplicate by code key before computing front + all_results = existing_front + results + seen_keys = set() + deduped = [] + for r in all_results: + key = _code_key(r) + if key not in seen_keys: + seen_keys.add(key) + deduped.append(r) + + # Filter to valid codes only + valid = [r for r in deduped if r.get("k", 0) > 0 and r.get("d", 0) > 0] + + # Sort by FOM descending + valid.sort(key=lambda r: r.get("fom", 0), reverse=True) + + # Compute Pareto front: non-dominated in (k/n, d, 1/n) space + front = [] + for candidate in valid: + dominated = False + n = candidate["n"] + k = candidate["k"] + d = candidate["d"] + rate = k / n if n > 0 else 0 + + for existing in front: + en, ek, ed = existing["n"], existing["k"], existing["d"] + e_rate = ek / en if en > 0 else 0 + # existing dominates candidate if it's at least as good in all + # dimensions and strictly better in at least one + if e_rate >= rate and ed >= d and en <= n: + if e_rate > rate or ed > d or en < n: + dominated = True + break + + if not dominated: + # Remove any codes in front that this candidate dominates + front = [ + existing for existing in front + if not ( + rate >= (existing["k"] / existing["n"]) and + d >= existing["d"] and + n <= existing["n"] and + (rate > (existing["k"] / existing["n"]) or + d > existing["d"] or n < existing["n"]) + ) + ] + front.append(candidate) + + with open(filepath, "w") as f: + json.dump(front, f, indent=2, default=str) + + return front + + +def _code_key(result: dict) -> tuple: + """Unique key for a code based on its defining parameters. + + For PBB (non-CSS) codes, also includes C_terms and D_terms so that + codes with the same (A, B) base but different perturbations are + stored separately. + """ + def canonical_terms(terms): + return tuple(sorted(tuple(t) for t in (terms or []))) + + key = ( + result.get("ell"), + result.get("m"), + canonical_terms(result.get("A_terms", [])), + canonical_terms(result.get("B_terms", [])), + ) + c_terms = result.get("C_terms") + d_terms = result.get("D_terms") + if c_terms or d_terms: + key += ( + canonical_terms(c_terms), + canonical_terms(d_terms), + ) + return key diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evolve/__init__.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evolve/__init__.py new file mode 100644 index 0000000..d346251 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evolve/__init__.py @@ -0,0 +1,34 @@ +"""LLM-guided evolutionary search for bivariate bicycle codes. + +This package implements the evolutionary loop that discovers novel BB code +constructions. Rather than evolving individual polynomial pairs, it +evolves the **strategy** -- a Python function ``generate_candidates(ell, m)`` +that produces batches of candidate polynomials for evaluation. + +The evolutionary loop is powered by `OpenEvolve +`_, which mutates the code between +``# EVOLVE-BLOCK-START`` and ``# EVOLVE-BLOCK-END`` markers in the seed +solution using LLM-generated diffs. + +Modules +------- +seed_solution + Seed program containing known benchmark codes (``KNOWN_CODES``), + target lattice dimensions (``TARGET_LATTICES``), and the initial + ``generate_candidates()`` function that OpenEvolve mutates. +openevolve_evaluator + Adapter between OpenEvolve's ``evaluate(program_path)`` interface and + the project's evaluation cascade. Implements a two-stage cascade: + stage 1 (quick k-only screening) gates stage 2 (full distance + estimation with trust filtering). +run_evolution + CLI entry point for launching evolutionary runs. Handles LiteLLM + proxy connection, model ensemble configuration, W&B integration, and + checkpoint resume. +config.yaml + OpenEvolve configuration: population size, island migration, + MAP-Elites feature dimensions, LLM ensemble, and cascade thresholds. +prompt_context.md + Domain knowledge document fed to the LLM during mutations -- describes + BB code algebra, known patterns, and strategies to explore. +""" diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evolve/openevolve_evaluator.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evolve/openevolve_evaluator.py new file mode 100644 index 0000000..0f8d849 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/evolve/openevolve_evaluator.py @@ -0,0 +1,1161 @@ +"""OpenEvolve evaluator adapter for BB code discovery. + +This module bridges OpenEvolve's ``evaluate(program_path)`` interface with +the project's multi-stage evaluation cascade. OpenEvolve calls +``evaluate(program_path)`` (or ``evaluate_stage1`` / ``evaluate_stage2`` +when cascade evaluation is enabled) with a temporary ``.py`` file containing +the evolved ``generate_candidates`` function. This adapter dynamically +loads that function, runs it across target lattices, and returns a score +dict compatible with OpenEvolve's MAP-Elites population database. + +Two-stage cascade +----------------- +**Stage 1** -- Quick k-only screening (~2 s) + Evaluates the evolved program on 2 small lattices (``(6,6)`` and + ``(12,6)``). Programs must produce valid codes (``k > 0``) at + **all** stage-1 lattices to advance past the cascade threshold. + Score: ``0.1 base + best_encoding_rate + log1p(num_high_k) / 10``. + +**Stage 2** -- Full evaluation with distance estimation (~30-60 s) + Evaluates on 8 lattices with BP-OSD distance (1000 OSD_0 trials + + 200 OSD-CS order-10 trials for top candidates). The primary fitness + metric is ``combined_score`` -- the sum of the best *credible* FOM per + lattice, where credibility is determined by a trust filter on + ``d / sqrt(n)``: + + * ``d / sqrt(n) <= 1.3`` -- full trust: use raw FOM. + * ``d / sqrt(n) >= 2.0`` -- no trust: use encoding rate (``k/n``) only. + * Between -- linear interpolation (smooth decay, no cliff). + + Credible codes are persisted to ``results/discovered_codes.json`` and + the Pareto front. Metrics are written to a shared JSONL file for + W&B background sync (since ``wandb.run`` is ``None`` in subprocess + workers). + +MAP-Elites feature dimensions +------------------------------ +* ``lattices_with_high_k`` -- number of distinct lattices with at least one + code having ``k >= 8``. +* ``num_high_k`` -- total count of codes with ``k >= 8``. + +These features encourage behavioral diversity in the population: programs +that find high-k codes at many lattices occupy different niches from +programs that find a single exceptional code. + +Constants +--------- +STAGE1_LATTICES : list[tuple[int, int]] + ``[(6, 6), (12, 6)]`` -- quick screening lattices. +STAGE2_LATTICES : list[tuple[int, int]] + 8 lattices for full evaluation. Excludes ``(9,8)``, ``(10,10)``, + ``(18,10)`` which produce zero ``k > 0`` codes. +""" + +from __future__ import annotations + +import importlib.util +import logging +import math +import sys +from pathlib import Path + +# Ensure the project root is on sys.path so we can import evaluation.* +_PROJECT_ROOT = str(Path(__file__).resolve().parent.parent) +if _PROJECT_ROOT not in sys.path: + sys.path.insert(0, _PROJECT_ROOT) + +from evaluation.evaluator import ( + evaluate_batch, + evaluate_batch_milp, + evaluate_batch_milp_parallel, + evaluate_milp_parallel, + DISTANCE_TRUST_RATIO, + DISTANCE_UNTRUST_RATIO, +) +from evaluation.results import save_code, update_pareto_front + +logger = logging.getLogger(__name__) + +# Lattice subsets for staged evaluation +# Stage 1: small/fast lattices for quick screening +STAGE1_LATTICES = [(6, 6), (12, 6)] +# Stage 2: full set +STAGE2_LATTICES = [ + (12, 6), (6, 12), + (12, 12), (24, 6), + (15, 12), (30, 6), + (16, 9), (18, 8), +] +# Stage 2 for MILP: drop (16,9) and (18,8) which produce 0 valid x/y-swap codes +STAGE2_LATTICES_MILP = [ + (12, 6), (6, 12), + (12, 12), (24, 6), + (15, 12), (30, 6), +] + + +def _classify_pattern(A_terms, B_terms) -> float: + """Classify polynomial structure for MAP-Elites. + + 0.0 = univariate (A=f(y), B=g(x) or vice versa, incl. constant-monomial) + 1.0 = x/y-swap (pure terms, no constant, each poly mixes x+y axes) + 2.0 = self-dual (A=B) + 3.0 = mixed monomials (has x^a*y^b with both a,b > 0) + 4.0 = multi-term pure (4+ terms, all pure) + 5.0 = hybrid/non-standard pure (has constant term, not univariate -- + e.g. 1+x+y type, or constant-mono A + x/y-swap B) + """ + a_set = sorted(tuple(t) for t in A_terms) + b_set = sorted(tuple(t) for t in B_terms) + if a_set == b_set: + return 2.0 + has_mixed = any(x > 0 and y > 0 for x, y in A_terms) or \ + any(x > 0 and y > 0 for x, y in B_terms) + if has_mixed: + return 3.0 + a_y_only = all(x == 0 for x, y in A_terms) + a_x_only = all(y == 0 for x, y in A_terms) + b_y_only = all(x == 0 for x, y in B_terms) + b_x_only = all(y == 0 for x, y in B_terms) + if (a_y_only and b_x_only) or (a_x_only and b_y_only): + return 0.0 + # 4+ term pure polynomials get their own niche + if len(A_terms) >= 4 or len(B_terms) >= 4: + return 4.0 + # Hybrid / non-standard pure: has a constant term (0,0) but isn't + # univariate. Separates novel structures (1+x+y, hybrid cross-family) + # from classic x/y-swap (which never has a constant term). + a_has_const = any(x == 0 and y == 0 for x, y in A_terms) + b_has_const = any(x == 0 and y == 0 for x, y in B_terms) + if a_has_const or b_has_const: + return 5.0 + return 1.0 + + +def _count_terms(A_terms, B_terms) -> float: + """Return max term count across A and B (for MAP-Elites feature).""" + return float(max(len(A_terms), len(B_terms))) + + +def _structural_feedback(result: dict) -> str: + """Generate structural feedback string for a code with d >= 4. + + Reports mixed vs pure term counts, shift direction vectors, + and axis coupling -- simple properties that help the LLM reason + about WHY a code worked. + """ + A = result.get("A_terms", []) + B = result.get("B_terms", []) + + def _classify_terms(terms, name): + pure_x = sum(1 for x, y in terms if x > 0 and y == 0) + pure_y = sum(1 for x, y in terms if x == 0 and y > 0) + const = sum(1 for x, y in terms if x == 0 and y == 0) + mixed = sum(1 for x, y in terms if x > 0 and y > 0) + return f"{name}: {pure_x} pure-x, {pure_y} pure-y, {const} const, {mixed} mixed" + + lines = [ + _classify_terms(A, "A"), + _classify_terms(B, "B"), + f"A shifts: {A}", + f"B shifts: {B}", + f"Terms: |A|={len(A)}, |B|={len(B)}", + ] + + a_has_mixed = any(x > 0 and y > 0 for x, y in A) + b_has_mixed = any(x > 0 and y > 0 for x, y in B) + if a_has_mixed and b_has_mixed: + lines.append("Axis coupling: both A and B have mixed terms") + elif a_has_mixed: + lines.append("Axis coupling: only A has mixed terms") + elif b_has_mixed: + lines.append("Axis coupling: only B has mixed terms") + else: + lines.append("Axis coupling: none (pure-term code)") + + return "\n ".join(lines) + + +def _log_code_jsonl(result: dict, run_name: str | None = None) -> None: + """Append a code result to the run-specific all_codes.jsonl file. + + Logs ALL codes with d > 0 (not just high-FOM codes), providing the + full dataset for Phase D pattern extraction. Each line is ~200 bytes. + + The run_name is resolved from (in priority order): + 1. Explicit ``run_name`` argument + 2. ``QCODE_RUN_NAME`` environment variable (set by run_evolution.py) + 3. Fallback to ``results/evolution/all_codes.jsonl`` + """ + import json + import os + import time + + d = result.get("d", 0) + if d <= 0: + return + + # Resolve run_name from argument or environment + if not run_name: + run_name = os.environ.get("QCODE_RUN_NAME") + + record = { + "ell": result.get("ell"), + "m": result.get("m"), + "A_terms": result.get("A_terms"), + "B_terms": result.get("B_terms"), + "n": result.get("n"), + "k": result.get("k"), + "d": d, + "fom": result.get("fom", 0.0), + "stage": result.get("stage", ""), + "pattern_type": _classify_pattern( + result.get("A_terms", []), result.get("B_terms", []) + ), + "term_count": _count_terms( + result.get("A_terms", []), result.get("B_terms", []) + ), + "timestamp": time.time(), + } + + if run_name: + log_dir = Path(_PROJECT_ROOT) / "results" / "evolution" / run_name + else: + log_dir = Path(_PROJECT_ROOT) / "results" / "evolution" + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / "all_codes.jsonl" + + try: + with open(log_file, "a") as f: + f.write(json.dumps(record, default=str) + "\n") + except OSError: + pass # Don't fail evaluation over logging + + +def _error_result(error: str) -> dict: + """Return an error result that includes all feature dimensions. + + MAP-Elites requires the feature dimensions to be present in every + result, including errors. + """ + return { + "combined_score": 0.0, + "error": error, + "lattices_with_high_k": 0.0, + "num_high_k": 0.0, + "term_count": 0.0, + "pattern_type": 0.0, + } + + +def _load_generate_candidates(program_path: str): + """Load generate_candidates from an evolved program file.""" + if not Path(program_path).exists(): + raise FileNotFoundError(f"Evolved program not found: {program_path}") + spec = importlib.util.spec_from_file_location("evolved_program", program_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if not hasattr(module, "generate_candidates"): + raise AttributeError("Evolved program missing generate_candidates function") + + return module.generate_candidates + + +def _run_evaluation( + generate_fn, + lattices: list[tuple[int, int]], + quick: bool = False, + quick_trials: int = 100, + refine_trials: int = 500, + max_distance_per_lattice: int = 10, + use_milp: bool = False, + milp_timeout_per_logical: int = 30, + milp_total_timeout: int = 120, + milp_early_stop: int = 4, + run_name: str | None = None, +) -> dict: + """Run evaluation across lattices and compute aggregate metrics. + + When quick=False, uses a two-pass approach per lattice: + 1. Quick k-only screen of all candidates + 2. Distance estimation for the top `max_distance_per_lattice` candidates, + using either BP-OSD (default) or MILP (when use_milp=True). + """ + all_results = [] + total_candidates = 0 + errors = [] + + for ell, m in lattices: + try: + candidates = generate_fn(ell, m) + if not isinstance(candidates, list): + errors.append(f"({ell},{m}): generate_candidates returned {type(candidates)}, not list") + continue + + total_candidates += len(candidates) + + # Cap candidates per lattice to avoid runaway generation + if len(candidates) > 5000: + errors.append(f"({ell},{m}): {len(candidates)} candidates, capped to 5000") + candidates = candidates[:5000] + + if quick: + results = evaluate_batch( + ell, m, candidates, + quick=True, + quick_trials=quick_trials, + fom_threshold_refine=6.0, + fom_threshold_exact=8.0, + ) + else: + # Two-pass: quick screen, then distance on top candidates. + # MILP path uses evaluate_batch_milp(quick=True) to get + # symplectic weight bounds for smarter top-k ranking. + if use_milp: + quick_results = evaluate_batch_milp( + ell, m, candidates, quick=True, + ) + else: + quick_results = evaluate_batch( + ell, m, candidates, quick=True, + ) + # Select diverse candidates for distance estimation. + # Diversify on BOTH k value AND polynomial A -- prevents + # wasting MILP budget on near-duplicate codes (e.g. 5 codes + # with same A at (15,12) all giving k=16). + # When symplectic weight is available (MILP path), rank by + # approximate FOM = k * d_symp^2 / n instead of k alone. + promising = [ + r for r in quick_results if r.get("k", 0) >= 8 + ] + n_code = 2 * ell * m + def _rank_key(r): + d_s = r.get("d_symplectic", 0) + if d_s > 0 and use_milp: + return r["k"] * d_s * d_s / n_code + return r["k"] + promising.sort(key=_rank_key, reverse=True) + + # Pass 1: one per distinct k value + seen_k: set[int] = set() + top: list[dict] = [] + for r in promising: + if r["k"] not in seen_k and len(top) < max_distance_per_lattice: + seen_k.add(r["k"]) + top.append(r) + # Pass 2: one per distinct A polynomial (among same-k codes) + seen_a: set[tuple] = {tuple(map(tuple, r["A_terms"])) for r in top} + for r in promising: + if len(top) >= max_distance_per_lattice: + break + a_key = tuple(map(tuple, r["A_terms"])) + if a_key not in seen_a and r not in top: + seen_a.add(a_key) + top.append(r) + # Pass 3: fill remaining slots + for r in promising: + if len(top) >= max_distance_per_lattice: + break + if r not in top: + top.append(r) + + top_candidates = [ + (r["A_terms"], r["B_terms"]) for r in top + ] + + if use_milp: + # MILP: scale timeout with n. The adaptive per-logical + # timeout in distance_milp.py uses max(8s, total/2k), + # so the total budget directly determines coverage. + # k=24 at n=288 with 240s: 8s/logical × 30 logicals + # (63% of 48). Most codes solve instantly (d=2-4), + # so only 1-2 codes per lattice use the full budget. + n_code = 2 * ell * m + if n_code <= 200: + lat_timeout = min(milp_total_timeout, 120) + lat_per_log = min(milp_timeout_per_logical, 20) + elif n_code <= 300: + lat_timeout = min(milp_total_timeout, 240) + lat_per_log = min(milp_timeout_per_logical, 45) + else: + lat_timeout = milp_total_timeout + lat_per_log = milp_timeout_per_logical + results = evaluate_batch_milp( + ell, m, top_candidates, + milp_timeout_per_logical=lat_per_log, + milp_total_timeout=lat_timeout, + milp_early_stop=milp_early_stop, + ) + else: + # BP-OSD: use refine_trials for tighter upper bounds. + # Skip exact distance (stage 5) -- requires SIGALRM which + # isn't available in OpenEvolve's worker threads. + results = evaluate_batch( + ell, m, top_candidates, + quick=False, + quick_trials=refine_trials, + fom_threshold_refine=6.0, + fom_threshold_exact=float("inf"), + ) + # Include quick-only results for aggregate counting + quick_only = [ + r for r in quick_results if r.get("k", 0) > 0 + and r not in top + ] + results.extend(quick_only) + + all_results.extend(results) + + # Log ALL codes with d > 0 to JSONL for Phase D pattern extraction + for r in results: + _log_code_jsonl(r, run_name=run_name) + except Exception as e: + errors.append(f"({ell},{m}): {type(e).__name__}: {e}") + + # Compute aggregate metrics using encoding rate (exact) and FOM (approximate) + valid = [r for r in all_results if r.get("k", 0) > 0] + foms = [r.get("fom", 0.0) for r in valid if r.get("fom", 0.0) > 0] + encoding_rates = [r.get("encoding_rate", 0.0) for r in valid] + + best_fom = max(foms) if foms else 0.0 + mean_fom = sum(foms) / len(foms) if foms else 0.0 + num_above_6 = sum(1 for f in foms if f >= 6.0) + num_above_12 = sum(1 for f in foms if f >= 12.0) + best_encoding_rate = max(encoding_rates) if encoding_rates else 0.0 + + # Count high-k codes (k >= 8) -- exact metric, not affected by BP-OSD + high_k_codes = [r for r in valid if r.get("k", 0) >= 8] + # Count lattices with at least one high-k code (breadth across lattices) + lattices_with_high_k = len(set( + (r["ell"], r["m"]) for r in high_k_codes + )) + + # Find the best code for reporting + best_code = None + if all_results: + best_result = max(all_results, key=lambda r: r.get("fom", 0.0)) + if best_result.get("fom", 0.0) > 0: + best_code = best_result + + return { + "best_fom": best_fom, + "mean_fom": mean_fom, + "num_valid": len(valid), + "num_above_6": num_above_6, + "num_above_12": num_above_12, + "total_candidates": total_candidates, + "best_encoding_rate": best_encoding_rate, + "num_high_k": len(high_k_codes), + "lattices_with_high_k": lattices_with_high_k, + "best_code": best_code, + "all_results": all_results, + "errors": errors, + } + + +def evaluate_stage1(program_path: str) -> dict: + """Stage 1: Quick screening on small lattices (k-only, ~2s). + + Programs must produce valid codes (k > 0) at ALL stage-1 lattices to + advance. (6,6) and (12,6) are the most forgiving lattices -- any + reasonable x/y-swap program produces k>0 at both. Failing either + signals a fundamentally broken strategy, not worth ~5 min of stage-2 + evaluation. + + Score components (all lattices covered): + - 0.1 base: guarantees passing cascade threshold (0.01) + - best_rate: peak encoding rate (up to ~0.17 for k=12 at n=72) + - high_k_quality: log-scaled count of k≥8 codes (diminishing returns, + prevents gaming by sparse generators that score high on ratios) + Total range: ~0.11 (barely alive) to ~0.8 (excellent), giving + MAP-Elites ~7x differentiation vs the prior 15% spread. + """ + try: + generate_fn = _load_generate_candidates(program_path) + except Exception as e: + return _error_result(str(e)) + + metrics = _run_evaluation(generate_fn, STAGE1_LATTICES, quick=True) + + if metrics["total_candidates"] == 0: + return { + "combined_score": 0.0, + "num_valid": 0.0, + "total_candidates": 0.0, + "lattices_with_high_k": 0.0, + "num_high_k": 0.0, + "term_count": 0.0, + "pattern_type": 0.0, + } + + valid = [r for r in metrics.get("all_results", []) if r.get("k", 0) > 0] + + # Require valid codes at ALL stage-1 lattices + lattices_with_valid = set( + (r["ell"], r["m"]) for r in valid + ) + lattice_coverage = len(lattices_with_valid) / len(STAGE1_LATTICES) + + if lattice_coverage < 1.0: + # Partial coverage: below cascade threshold (0.01) + score = len(lattices_with_valid) * 0.001 + else: + # All lattices covered. Score by quality. + best_rate = max(r.get("encoding_rate", 0.0) for r in valid) + # Log-scaled count: rewards breadth without letting sparse + # generators game the score (2 codes → 0.11, 259 codes → 0.56) + high_k_quality = math.log1p(metrics["num_high_k"]) / 10.0 + score = 0.1 + best_rate + high_k_quality + + # MAP-Elites features: compute from best valid code + best_valid = max(valid, key=lambda r: r.get("k", 0)) if valid else None + if best_valid: + best_tc = _count_terms(best_valid["A_terms"], best_valid["B_terms"]) + best_pattern = _classify_pattern(best_valid["A_terms"], best_valid["B_terms"]) + else: + best_tc = 0.0 + best_pattern = 0.0 + + return { + "combined_score": score, + "num_valid": float(metrics["num_valid"]), + "total_candidates": float(metrics["total_candidates"]), + "lattices_with_high_k": float(metrics["lattices_with_high_k"]), + "num_high_k": float(metrics["num_high_k"]), + "term_count": best_tc, + "pattern_type": best_pattern, + } + + +def evaluate_stage2(program_path: str) -> dict: + """Stage 2: Full evaluation with distance estimation (~30-60s). + + Runs across all target lattices with distance estimation. + """ + try: + generate_fn = _load_generate_candidates(program_path) + except Exception as e: + return _error_result(str(e)) + + metrics = _run_evaluation( + generate_fn, STAGE2_LATTICES, + quick=False, refine_trials=1000, + ) + + # --- Combined score --- + # Sum of best *credible* FOM per lattice. + # + # BP-OSD with 1000 trials gives exact d for well-structured codes + # (verified on [[72,12,6]], [[144,12,12]], [[288,12,18]]). But for + # degenerate high-k codes it wildly overestimates d. + # + # We FILTER rather than cap: only trust BP-OSD estimates where + # d ≤ TRUST_FULL * sqrt(n). Known best BB codes have d/sqrt(n) ≤ 1.26. + # Degenerate high-k codes have d/sqrt(n) ≥ 2.5, leaving a wide gap. + # Codes failing the filter get a small encoding-rate bonus (k/n) + # instead, so they're not completely invisible but can't dominate. + + # Trust boundaries (imported from evaluation.evaluator -- single source of truth). + TRUST_FULL = DISTANCE_TRUST_RATIO # d/sqrt(n) ≤ 1.3: fully trust FOM + TRUST_NONE = DISTANCE_UNTRUST_RATIO # d/sqrt(n) ≥ 2.0: discard FOM, use k/n only + # Between TRUST_FULL and TRUST_NONE: linear interpolation (soft decay, no cliff) + best_fom = metrics["best_fom"] + + per_lattice_best: dict[tuple[int, int], float] = {} + for r in metrics["all_results"]: + d_raw = r.get("d", 0) + k = r.get("k", 0) + n = r.get("n", 0) + if k <= 0 or n <= 0: + continue + key = (r["ell"], r["m"]) + fallback = k / n # encoding rate, always available + if d_raw <= 0: + credible_fom = fallback + else: + ratio = d_raw / math.sqrt(n) + raw_fom = k * d_raw * d_raw / n + if ratio <= TRUST_FULL: + credible_fom = raw_fom + elif ratio >= TRUST_NONE: + credible_fom = fallback + else: + # Linear decay: 100% FOM at TRUST_FULL, 0% at TRUST_NONE + alpha = (TRUST_NONE - ratio) / (TRUST_NONE - TRUST_FULL) + credible_fom = alpha * raw_fom + (1 - alpha) * fallback + per_lattice_best[key] = max(per_lattice_best.get(key, 0.0), credible_fom) + + combined = sum(per_lattice_best.values()) + + # Build artifacts for LLM feedback. + # Show the best code by CREDIBLE FOM (d-filtered), not raw BP-OSD FOM, + # so the LLM learns from genuine patterns, not degenerate codes. + artifacts = {} + credible_codes = [] + for r in metrics["all_results"]: + d_val = r.get("d", 0) + n_val = r.get("n", 0) + if d_val > 0 and n_val > 0 and d_val <= TRUST_FULL * math.sqrt(n_val): + credible_codes.append(r) + if credible_codes: + bc = max(credible_codes, key=lambda r: r.get("fom", 0.0)) + artifacts["best_code"] = ( + f"[[{bc['n']},{bc['k']},{bc['d']}]] FOM={bc['fom']:.2f} " + f"at ({bc['ell']},{bc['m']})\n" + f" A={bc['A_terms']}\n" + f" B={bc['B_terms']}" + ) + elif metrics["best_code"]: + bc = metrics["best_code"] + artifacts["best_code"] = ( + f"[[{bc['n']},{bc['k']},{bc.get('d', '?')}]] " + f"(d estimate unreliable) at ({bc['ell']},{bc['m']})\n" + f" A={bc['A_terms']}\n" + f" B={bc['B_terms']}" + ) + + if metrics["errors"]: + artifacts["errors"] = "\n".join(metrics["errors"][:5]) + + # Report top 5 codes by credible FOM (reusing list from above) + top5 = sorted(credible_codes, key=lambda r: r.get("fom", 0), reverse=True)[:5] + if top5: + top5_lines = [] + for r in top5: + top5_lines.append( + f" [[{r['n']},{r['k']},{r['d']}]] FOM={r['fom']:.1f} " + f"rate={r.get('encoding_rate', 0):.3f} ({r['ell']},{r['m']})" + ) + artifacts["top_codes"] = "\n".join(top5_lines) + + # Structural feedback for codes with d >= 4 (helps LLM reason about patterns) + d4_codes = [r for r in credible_codes if r.get("d", 0) >= 4] + if d4_codes: + struct_lines = [] + for r in sorted(d4_codes, key=lambda r: r.get("fom", 0), reverse=True)[:3]: + struct_lines.append( + f" [[{r['n']},{r['k']},{r['d']}]] FOM={r['fom']:.1f}:\n" + f" {_structural_feedback(r)}" + ) + artifacts["structural_analysis"] = ( + "Structural analysis of top codes with d>=4:\n" + "\n".join(struct_lines) + ) + + # Per-lattice breakdown for the LLM + lattice_lines = [] + for key in sorted(per_lattice_best.keys()): + lattice_lines.append( + f" ({key[0]},{key[1]}): credible FOM={per_lattice_best[key]:.1f}" + ) + + artifacts["summary"] = ( + f"Evaluated {metrics['total_candidates']} candidates across " + f"{len(STAGE2_LATTICES)} lattices.\n" + f"Valid codes (k>0): {metrics['num_valid']}\n" + f"High-k codes (k>=8): {metrics['num_high_k']}\n" + f"Lattices with high-k: {metrics['lattices_with_high_k']}/{len(STAGE2_LATTICES)}\n" + f"Best raw FOM (BP-OSD, 1000 trials + OSD-CS check): {best_fom:.2f}\n" + f"Combined score: {combined:.1f} = sum of best credible FOM per lattice " + f"(full trust d/sqrt(n) <= {TRUST_FULL}, soft decay to {TRUST_NONE})\n" + f"Per-lattice breakdown:\n" + "\n".join(lattice_lines) + ) + + # Save only codes with trusted distances (d in fully-trusted zone) + credible_to_save = [ + r for r in metrics["all_results"] + if r.get("fom", 0) > 0 + and r.get("d", 0) > 0 + and r.get("n", 0) > 0 + and r["d"] <= TRUST_FULL * math.sqrt(r["n"]) + ] + if credible_to_save: + best_credible = max(credible_to_save, key=lambda r: r["fom"]) + if best_credible["fom"] > 6.0: + try: + save_code(best_credible) + update_pareto_front(credible_to_save) + except Exception: + pass # Don't fail evaluation over persistence + + # Write metrics to shared JSONL file for W&B sync from main process. + # (wandb.run is None in subprocess workers, so direct wandb.log doesn't work.) + _write_metrics_jsonl(metrics) + + # MAP-Elites features from best credible code + best_credible_code = max(credible_codes, key=lambda r: r.get("fom", 0)) if credible_codes else None + if best_credible_code: + s2_tc = _count_terms(best_credible_code["A_terms"], best_credible_code["B_terms"]) + s2_pattern = _classify_pattern(best_credible_code["A_terms"], best_credible_code["B_terms"]) + else: + s2_tc = 0.0 + s2_pattern = 0.0 + + result = { + "combined_score": combined, + "best_fom": best_fom, + "mean_fom": metrics["mean_fom"], + "num_valid": float(metrics["num_valid"]), + "num_high_k": float(metrics["num_high_k"]), + "lattices_with_high_k": float(metrics["lattices_with_high_k"]), + "best_encoding_rate": metrics["best_encoding_rate"], + "num_above_6": float(metrics["num_above_6"]), + "num_above_12": float(metrics["num_above_12"]), + "total_candidates": float(metrics["total_candidates"]), + "term_count": s2_tc, + "pattern_type": s2_pattern, + } + + try: + from openevolve.evaluation_result import EvaluationResult + return EvaluationResult(metrics=result, artifacts=artifacts) + except ImportError: + return result + + +def evaluate_stage2_milp(program_path: str) -> dict: + """Stage 2 with parallel MILP distance verification. + + Two-phase approach: + 1. Quick k-only screening on all lattices (sequential, ~3s) + 2. Parallel MILP verification for top candidates across all lattices + + Key improvements over Campaign 4: + - Parallel MILP evaluation using ProcessPoolExecutor (10 workers) + - 300s per-logical timeout (proven sufficient for n≤288 exact) + - 7200s total timeout per code (covers k=12-24 fully) + - Incremental code persistence to JSONL after each solve + - Scoring: only codes with d≥6 contribute FOM (d≤4 are irrelevant) + - Symplectic weight pre-filter: codes with d_symp≤4 skip MILP entirely + - Better LLM feedback with clear signal about what works vs doesn't + """ + try: + generate_fn = _load_generate_candidates(program_path) + except Exception as e: + return _error_result(str(e)) + + # MILP budget parameters + MILP_TIMEOUT_PER_LOGICAL = 300 # 300s proven sufficient (milp_optimality_audit.json) + MILP_TOTAL_TIMEOUT = 7200 # 2h per code -- covers k≤24 fully + MILP_EARLY_STOP = 4 # Exit immediately if d≤4 + MAX_DISTANCE_PER_LATTICE = 5 # Top-5 per lattice → ~30 MILP tasks + MIN_RELEVANT_D = 6 # Only d≥6 codes contribute to score + + # Save path for incremental MILP persistence + codes_jsonl = str(Path(_PROJECT_ROOT) / "results" / "evolution_codes.jsonl") + + # ── Phase 1: k-only screening (parallel) ─────────────────────── + # Collect all candidates across lattices, then evaluate in parallel + # using ProcessPoolExecutor. ~10x faster than sequential (~3 min + # vs ~30 min for 120k candidates). + all_quick_tasks = [] # (ell, m, A_terms, B_terms) + total_candidates = 0 + errors = [] + + for ell, m in STAGE2_LATTICES_MILP: + try: + candidates = generate_fn(ell, m) + if not isinstance(candidates, list): + errors.append(f"({ell},{m}): returned {type(candidates)}, not list") + continue + total_candidates += len(candidates) + # Cap at 20000 per lattice. + # Priority candidates (perturbations, simultaneous perturbations) + # are generated first in the seed, so they survive the cap. + # Strategy 4's exhaustive search fills remaining slots. + if len(candidates) > 20000: + errors.append(f"({ell},{m}): {len(candidates)} candidates, capped to 20000") + candidates = candidates[:20000] + + for A_terms, B_terms in candidates: + all_quick_tasks.append((ell, m, A_terms, B_terms)) + except Exception as e: + errors.append(f"({ell},{m}): {type(e).__name__}: {e}") + + all_quick_results = evaluate_batch_milp_parallel( + all_quick_tasks, quick=True, + ) + + # ── Phase 2: Select top candidates for MILP ──────────────────── + # Use d_symplectic to filter and rank -- only codes with d_symp ≥ 5 + # are worth MILP verification (d_symp ≤ 4 already resolved by + # evaluate_candidate_milp's pre-filter). Among qualifying codes, + # rank by approximate FOM = k * d_symp² / n. + milp_tasks = [] # (ell, m, A_terms, B_terms) + milp_skipped_low_d_symp = 0 + + for ell, m in STAGE2_LATTICES_MILP: + lattice_results = [ + r for r in all_quick_results + if r.get("ell") == ell and r.get("m") == m + and r.get("k", 0) >= 4 + ] + + # Filter: only consider codes with d_symplectic high enough to + # potentially have d ≥ MIN_RELEVANT_D. This is the key optimization + # from the symplectic basis pre-filter. + n_code = 2 * ell * m + promising = [] + for r in lattice_results: + d_s = r.get("d_symplectic", 0) + if d_s > MILP_EARLY_STOP: + promising.append(r) + else: + milp_skipped_low_d_symp += 1 + + # Rank by approximate FOM using symplectic weight + def _rank_key(r): + d_s = r.get("d_symplectic", 0) + if d_s > 0: + return r["k"] * d_s * d_s / n_code + return r["k"] + promising.sort(key=_rank_key, reverse=True) + + # Diversify: pick top candidates by k, A-polynomial diversity + seen_k: set[int] = set() + top: list[dict] = [] + for r in promising: + if r["k"] not in seen_k and len(top) < MAX_DISTANCE_PER_LATTICE: + seen_k.add(r["k"]) + top.append(r) + seen_a: set[tuple] = {tuple(map(tuple, r["A_terms"])) for r in top} + for r in promising: + if len(top) >= MAX_DISTANCE_PER_LATTICE: + break + a_key = tuple(map(tuple, r["A_terms"])) + if a_key not in seen_a and r not in top: + seen_a.add(a_key) + top.append(r) + for r in promising: + if len(top) >= MAX_DISTANCE_PER_LATTICE: + break + if r not in top: + top.append(r) + + for r in top: + milp_tasks.append((ell, m, r["A_terms"], r["B_terms"])) + + # ── Phase 3: Parallel MILP verification ──────────────────────── + # Run all MILP tasks in parallel using ProcessPoolExecutor. + # Each result is saved to JSONL immediately after solving. + milp_results = [] + if milp_tasks: + milp_results = evaluate_milp_parallel( + milp_tasks, + milp_timeout_per_logical=MILP_TIMEOUT_PER_LOGICAL, + milp_total_timeout=MILP_TOTAL_TIMEOUT, + milp_early_stop=MILP_EARLY_STOP, + save_path=codes_jsonl, + ) + + # Combine: MILP results + k-only results (for codes that didn't get MILP) + all_results = list(milp_results) + milp_keys = { + (r["ell"], r["m"], tuple(map(tuple, r["A_terms"])), tuple(map(tuple, r["B_terms"]))) + for r in milp_results + } + for r in all_quick_results: + key = (r["ell"], r["m"], tuple(map(tuple, r["A_terms"])), tuple(map(tuple, r["B_terms"]))) + if key not in milp_keys and r.get("k", 0) > 0: + all_results.append(r) + + # ── Phase 4: Scoring ─────────────────────────────────────────── + # Only codes with d ≥ MIN_RELEVANT_D contribute FOM to combined_score. + # This sends a clear signal: d≤4 codes are worthless. + per_lattice_best: dict[tuple[int, int], float] = {} + for r in all_results: + d = r.get("d", 0) + fom = r.get("fom", 0.0) + k = r.get("k", 0) + if k <= 0: + continue + key = (r["ell"], r["m"]) + if d >= MIN_RELEVANT_D and fom > 0: + per_lattice_best[key] = max(per_lattice_best.get(key, 0.0), fom) + elif d > 0 and d < MIN_RELEVANT_D: + # Low-d codes: tiny contribution (like encoding rate) + per_lattice_best[key] = max(per_lattice_best.get(key, 0.0), 0.01) + + combined = sum(per_lattice_best.values()) + + # Aggregate metrics + valid = [r for r in all_results if r.get("k", 0) > 0] + foms = [r.get("fom", 0.0) for r in valid if r.get("fom", 0.0) > 0] + best_fom = max(foms) if foms else 0.0 + mean_fom = sum(foms) / len(foms) if foms else 0.0 + high_k_codes = [r for r in valid if r.get("k", 0) >= 8] + lattices_with_high_k = len(set( + (r["ell"], r["m"]) for r in high_k_codes + )) + num_above_6 = sum(1 for f in foms if f >= 6.0) + num_above_12 = sum(1 for f in foms if f >= 12.0) + + # ── Phase 5: LLM feedback artifacts ──────────────────────────── + artifacts = {} + + def _stage_tag(r): + """Human-readable label for MILP result quality.""" + stage = r.get("stage", "") + if stage == "milp_exact": + return "exact" + elif stage == "milp_incumbent": + return "d\u2264" + str(r["d"]) + elif stage == "milp_promising_timeout": + return "d>" + str(r.get("milp_details", {}).get("early_stop", 4)) + elif stage in ("milp_low_d", "symplectic_low_d"): + return "exact" if r.get("d", 0) <= 2 else f"d\u2264{r['d']}" + return "" + + # Reference codes for perturbation gradient analysis + _REFERENCE_CODES = [ + {"ell": 12, "m": 6, "A": [(3,0),(0,1),(0,2)], "B": [(0,3),(1,0),(2,0)], + "d": 12, "k": 12, "name": "[[144,12,12]] gross"}, + {"ell": 12, "m": 12, "A": [(3,0),(0,2),(0,7)], "B": [(0,3),(1,0),(2,0)], + "d": 18, "k": 12, "name": "[[288,12,18]] bravyi"}, + {"ell": 12, "m": 12, "A": [(6,0),(0,1),(0,2)], "B": [(0,3),(2,0),(4,0)], + "d": 12, "k": 24, "name": "[[288,24,12]]"}, + {"ell": 12, "m": 12, "A": [(3,0),(0,1),(0,2)], "B": [(0,3),(1,0),(2,0)], + "d": 12, "k": 16, "name": "[[288,16,12]] gross-scaled"}, + {"ell": 15, "m": 12, "A": [(3,0),(0,2),(0,4)], "B": [(0,6),(2,0),(4,0)], + "d": 14, "k": 16, "name": "[[360,16,14]]"}, + {"ell": 30, "m": 6, "A": [(9,0),(0,1),(0,2)], "B": [(0,3),(25,0),(26,0)], + "d": 24, "k": 12, "name": "[[360,12,24]] bravyi"}, + ] + + def _exponent_diff(terms1, terms2): + """Count changed exponent positions between sorted term lists.""" + s1 = sorted(tuple(t) for t in terms1) + s2 = sorted(tuple(t) for t in terms2) + return sum(1 for a, b in zip(s1, s2) if a != b) + + def _describe_diff(terms1, terms2, poly_name): + """Describe exponent changes between two polynomials.""" + s1 = sorted(tuple(t) for t in terms1) + s2 = sorted(tuple(t) for t in terms2) + changes = [] + for a, b in zip(s1, s2): + if a != b: + changes.append(f"{poly_name}: {a}->{b}") + return changes + + # Codes with d reported (MILP-verified) + codes_with_d = [ + r for r in all_results + if r.get("d", 0) > 0 and r.get("fom", 0) > 0 + ] + + # Best code + if codes_with_d: + bc = max(codes_with_d, key=lambda r: r["fom"]) + tag = _stage_tag(bc) + # Check if it beats any Bravyi baseline + bravyi_foms = {144: 12.0, 288: 13.5, 360: 19.2} + beat_bravyi = bc["fom"] > bravyi_foms.get(bc["n"], float("inf")) + beat_tag = " ← BEATS BRAVYI!" if beat_bravyi else "" + artifacts["best_code"] = ( + f"[[{bc['n']},{bc['k']},{bc['d']}]] FOM={bc['fom']:.2f} " + f"({tag}) at ({bc['ell']},{bc['m']}){beat_tag}\n" + f" A={bc['A_terms']}\n" + f" B={bc['B_terms']}" + ) + + if errors: + artifacts["errors"] = "\n".join(errors[:5]) + + # Top codes with d ≥ MIN_RELEVANT_D (the useful ones) + relevant_codes = [r for r in codes_with_d if r.get("d", 0) >= MIN_RELEVANT_D] + top5 = sorted(relevant_codes, key=lambda r: r.get("fom", 0), reverse=True)[:5] + if top5: + top5_lines = [] + for r in top5: + tag = _stage_tag(r) + tag_str = f" ({tag})" if tag else "" + top5_lines.append( + f" [[{r['n']},{r['k']},{r['d']}]] FOM={r['fom']:.1f}" + f"{tag_str} ({r['ell']},{r['m']})" + ) + artifacts["top_codes"] = "Top codes with d>=" + str(MIN_RELEVANT_D) + ":\n" + "\n".join(top5_lines) + + # Low-d summary (to teach the LLM what's bad) + low_d_count = sum(1 for r in codes_with_d if r.get("d", 0) < MIN_RELEVANT_D) + if low_d_count > 0: + max_k_low_d = max( + (r.get("k", 0) for r in codes_with_d if r.get("d", 0) < MIN_RELEVANT_D), + default=0, + ) + artifacts["low_d_warning"] = ( + f"{low_d_count} codes with d<{MIN_RELEVANT_D} (max k={max_k_low_d}) -- " + f"these DON'T count toward score. " + f"Avoid univariate (A=f(y),B=g(x)) and self-dual (A=B)." + ) + + # Distance gradient analysis: compare MILP results to reference codes + # to show the LLM how exponent changes affect d. + gradient_lines = [] + for ref in _REFERENCE_CODES: + ref_lattice = (ref["ell"], ref["m"]) + sorted(tuple(t) for t in ref["A"]) + sorted(tuple(t) for t in ref["B"]) + # Find MILP results at the same lattice that are close perturbations + perturbations = [] + for r in codes_with_d: + if (r["ell"], r["m"]) != ref_lattice: + continue + diff_a = _exponent_diff(r["A_terms"], ref["A"]) + diff_b = _exponent_diff(r["B_terms"], ref["B"]) + total_diff = diff_a + diff_b + if 0 < total_diff <= 3: # close perturbation, not identical + changes = _describe_diff(ref["A"], r["A_terms"], "A") + changes += _describe_diff(ref["B"], r["B_terms"], "B") + d_delta = r["d"] - ref["d"] + sign = "+" if d_delta > 0 else "" + tag = _stage_tag(r) + perturbations.append(( + r["d"], d_delta, r.get("k", 0), + f" d={r['d']} ({tag}) k={r['k']} [{', '.join(changes)}] " + f"d_change={sign}{d_delta}" + )) + if perturbations: + # Sort: best d first + perturbations.sort(key=lambda x: -x[0]) + gradient_lines.append(f" Perturbations of {ref['name']} (d={ref['d']}, k={ref['k']}):") + for _, _, _, line in perturbations[:5]: # top 5 + gradient_lines.append(line) + + if gradient_lines: + artifacts["distance_gradients"] = ( + "Distance gradients (how exponent changes affect d):\n" + + "\n".join(gradient_lines) + ) + + # Per-lattice breakdown + lattice_lines = [] + for key in sorted(per_lattice_best.keys()): + lattice_lines.append( + f" ({key[0]},{key[1]}): best FOM={per_lattice_best[key]:.1f}" + ) + + artifacts["summary"] = ( + f"Evaluated {total_candidates} candidates across " + f"{len(STAGE2_LATTICES_MILP)} lattices.\n" + f"MILP verified: {len(milp_tasks)} codes ({milp_skipped_low_d_symp} " + f"skipped by symplectic pre-filter d_symp<={MILP_EARLY_STOP}).\n" + f"Valid codes (k>0): {len(valid)}\n" + f"Codes with d>={MIN_RELEVANT_D}: {len(relevant_codes)} (these count toward score)\n" + f"Best FOM: {best_fom:.2f}\n" + f"Combined score: {combined:.1f} = sum of best FOM per lattice (d>={MIN_RELEVANT_D} only)\n" + f"Per-lattice breakdown:\n" + "\n".join(lattice_lines) + ) + + # ── Phase 6: Persistence ─────────────────────────────────────── + # Individual codes are already saved to JSONL by evaluate_milp_parallel. + # Also save to discovered_codes.json and pareto_front.json for compat. + to_save = [ + r for r in all_results + if r.get("fom", 0) > 6.0 and r.get("d", 0) > 0 + ] + if to_save: + best_to_save = max(to_save, key=lambda r: r["fom"]) + try: + save_code(best_to_save) + update_pareto_front(to_save) + except Exception: + pass + + _write_metrics_jsonl({ + "best_fom": best_fom, + "mean_fom": mean_fom, + "num_valid": len(valid), + "num_high_k": len(high_k_codes), + "lattices_with_high_k": lattices_with_high_k, + "best_encoding_rate": max((r.get("encoding_rate", 0) for r in valid), default=0), + "num_above_6": num_above_6, + "num_above_12": num_above_12, + "total_candidates": total_candidates, + "all_results": all_results, + }) + + # MAP-Elites features from best MILP-verified code + milp_best = max( + (r for r in all_results if r.get("d", 0) > 0), + key=lambda r: r.get("fom", 0), default=None, + ) + if milp_best: + milp_tc = _count_terms(milp_best["A_terms"], milp_best["B_terms"]) + milp_pattern = _classify_pattern(milp_best["A_terms"], milp_best["B_terms"]) + else: + milp_tc = 0.0 + milp_pattern = 0.0 + + result = { + "combined_score": combined, + "best_fom": best_fom, + "mean_fom": mean_fom, + "num_valid": float(len(valid)), + "num_high_k": float(len(high_k_codes)), + "lattices_with_high_k": float(lattices_with_high_k), + "best_encoding_rate": max((r.get("encoding_rate", 0) for r in valid), default=0), + "num_above_6": float(num_above_6), + "num_above_12": float(num_above_12), + "total_candidates": float(total_candidates), + "term_count": milp_tc, + "pattern_type": milp_pattern, + } + + try: + from openevolve.evaluation_result import EvaluationResult + return EvaluationResult(metrics=result, artifacts=artifacts) + except ImportError: + return result + + +def evaluate(program_path: str) -> dict: + """Full evaluation (fallback when cascade is disabled).""" + return evaluate_stage2(program_path) + + +def _write_metrics_jsonl(metrics: dict) -> None: + """Append evaluation metrics to a shared JSONL file. + + This is called from subprocess workers where wandb.run is None. + The main process reads this file and syncs to W&B. + """ + import json + import time + + metrics_dir = Path(_PROJECT_ROOT) / "results" + metrics_dir.mkdir(parents=True, exist_ok=True) + metrics_file = metrics_dir / "evolution_metrics.jsonl" + + # Build per-lattice best FOM for detailed tracking + per_lattice: dict[tuple[int, int], float] = {} + for r in metrics.get("all_results", []): + d_raw = r.get("d", 0) + k = r.get("k", 0) + n = r.get("n", 0) + if k > 0 and n > 0 and d_raw > 0: + fom = k * d_raw * d_raw / n + key = (r["ell"], r["m"]) + per_lattice[key] = max(per_lattice.get(key, 0.0), fom) + + record = { + "timestamp": time.time(), + "best_fom": metrics.get("best_fom", 0), + "mean_fom": metrics.get("mean_fom", 0), + "num_valid": metrics.get("num_valid", 0), + "num_high_k": metrics.get("num_high_k", 0), + "lattices_with_high_k": metrics.get("lattices_with_high_k", 0), + "best_encoding_rate": metrics.get("best_encoding_rate", 0), + "num_above_6": metrics.get("num_above_6", 0), + "num_above_12": metrics.get("num_above_12", 0), + "total_candidates": metrics.get("total_candidates", 0), + "per_lattice_best_fom": { + f"{k[0]}x{k[1]}": v for k, v in per_lattice.items() + }, + } + + try: + with open(metrics_file, "a") as f: + f.write(json.dumps(record) + "\n") + except OSError: + pass # Don't fail evaluation over metrics logging diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/spec.json b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/spec.json new file mode 100644 index 0000000..c3bbfdc --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/spec.json @@ -0,0 +1,287 @@ +{ + "schema_version": 1, + "challenge_name": "bivariate-bicycle-code-discovery-qcode-discovery", + "challenge_title": "Bivariate Bicycle Quantum Code Discovery", + "summary": { + "en": "Design generators for bivariate-bicycle quantum LDPC codes and maximize IBM's trust-filtered aggregate figure of merit.", + "zh": "为双变量自行车量子 LDPC 码设计候选生成器,并最大化 IBM 的可信度过滤聚合品质因数。" + }, + "keywords": [ + "quantum error correction", + "qLDPC", + "code discovery", + "optimization" + ], + "solution": { + "protocol": "zip_project", + "manifest_file": "agentics.solution.json" + }, + "targets": [ + { + "name": "linux-arm64-cpu", + "docker_platform": "linux/arm64", + "accelerator": null, + "validation_enabled": true, + "resource_profile": { + "name": "agentics-cpu-large", + "solution_image": { + "source": "registry", + "reference": "ghcr.io/agentic-science/agentics-linux-arm64-cpu:ubuntu26.04-v0.2.5@sha256:7ba1dbfb4de62ce7c8716fbdf6fa9e840004cc2d231ac9c0adfd655cd275a537" + }, + "evaluator_image": { + "source": "registry", + "reference": "ghcr.io/agentic-science/agentics-linux-arm64-cpu:ubuntu26.04-v0.2.5@sha256:7ba1dbfb4de62ce7c8716fbdf6fa9e840004cc2d231ac9c0adfd655cd275a537" + }, + "solution": { + "setup": { + "timeout_sec": 300, + "memory_limit_mb": 2048, + "cpu_limit_millis": 2000, + "disk_limit_mb": 4096, + "network_access": "enabled" + }, + "build": { + "timeout_sec": 300, + "memory_limit_mb": 2048, + "cpu_limit_millis": 2000, + "disk_limit_mb": 4096, + "network_access": "disabled" + }, + "run": { + "timeout_sec": 120, + "memory_limit_mb": 2048, + "cpu_limit_millis": 2000, + "disk_limit_mb": 2048, + "network_access": "disabled" + } + }, + "evaluator": { + "setup": { + "timeout_sec": 1200, + "memory_limit_mb": 6144, + "cpu_limit_millis": 4000, + "disk_limit_mb": 16384, + "network_access": "enabled" + }, + "run": { + "timeout_sec": 3600, + "memory_limit_mb": 8192, + "cpu_limit_millis": 8000, + "disk_limit_mb": 8192, + "network_access": "disabled" + } + }, + "resource_description": "ARM64 CPU profile for three parallel IBM-style qLDPC fitness passes.", + "hardware_metadata": { + "kind": "cpu", + "gpu_model": null, + "gpu_count": null, + "gpu_memory_gb": null, + "cuda_variant": null, + "cuda_version": null, + "driver_minimum": null + } + } + } + ], + "starts_at": "2026-01-01T00:00:00Z", + "closes_at": null, + "eligibility": { + "type": "open" + }, + "validation_submission_limit": 2, + "official_submission_limit": 1, + "visibility": { + "leaderboard": "public_live", + "score_distribution": "public_live", + "result_detail": "submitter_live_public_live" + }, + "solution_publication": "public", + "execution": { + "mode": "separated_evaluator", + "separated_evaluator": { + "command": [ + "python", + "separated-evaluator/run.py" + ], + "result_file": "result.json" + }, + "validation_runs": null, + "official_runs": null, + "validation_setup": { + "command": [ + "python", + "separated-evaluator/setup.py" + ], + "result_runs_file": "generated/runs.json", + "reproducibility_notes": "Installs the pinned qcode-discovery numerical environment with uv and generates IBM's published stage-1 lattices from public/lattices.json." + }, + "official_evaluation_setup": { + "command": [ + "python", + "separated-evaluator/setup.py" + ], + "result_runs_file": "generated/runs.json", + "reproducibility_notes": "Installs the same pinned environment and generates IBM's published eight-lattice official suite; no private benchmark material is used." + } + }, + "datasets": { + "public_dir": "public", + "private_benchmark_dir": null, + "public_policy": "full", + "private_benchmark_policy": "score_only", + "private_benchmark_enabled": false + }, + "metric_schema": { + "metrics": [ + { + "name": "score", + "label": "Mean Combined Score", + "direction": "maximize", + "visibility": "public", + "metric_description": "Arithmetic mean of three independent IBM-style eight-lattice combined scores.", + "unit": null + }, + { + "name": "score_stddev", + "label": "Score Standard Deviation", + "direction": "minimize", + "visibility": "public", + "metric_description": "Population standard deviation of the three independent combined scores.", + "unit": null + }, + { + "name": "score_min", + "label": "Minimum Pass Score", + "direction": "maximize", + "visibility": "public", + "metric_description": "Lowest combined score among the three official passes.", + "unit": null + }, + { + "name": "score_max", + "label": "Maximum Pass Score", + "direction": "maximize", + "visibility": "public", + "metric_description": "Highest combined score among the three official passes.", + "unit": null + }, + { + "name": "mean_best_fom", + "label": "Mean Best Raw FOM", + "direction": "maximize", + "visibility": "public", + "metric_description": "Mean of each pass's best raw BP-OSD figure of merit; this is an upper-bound estimate, not a distance certificate.", + "unit": null + }, + { + "name": "stage1_score", + "label": "Stage 1 Score", + "direction": "maximize", + "visibility": "public", + "metric_description": "IBM's public k-only cascade score across the two validation lattices.", + "unit": null + }, + { + "name": "valid_codes", + "label": "Valid Codes", + "direction": "maximize", + "visibility": "public", + "metric_description": "Number of evaluated candidates with positive logical dimension.", + "unit": "codes" + }, + { + "name": "high_k_codes", + "label": "High-k Codes", + "direction": "maximize", + "visibility": "public", + "metric_description": "Number of evaluated candidates with at least eight logical qubits.", + "unit": "codes" + }, + { + "name": "lattices_with_high_k", + "label": "Lattices With High-k Codes", + "direction": "maximize", + "visibility": "public", + "metric_description": "Number of lattices containing at least one candidate with k at least eight.", + "unit": "lattices" + }, + { + "name": "total_candidates", + "label": "Submitted Candidates", + "direction": "maximize", + "visibility": "public", + "metric_description": "Total candidate count reported across evaluated lattices before the per-lattice source cap.", + "unit": "candidates" + }, + { + "name": "credible_fom_12x6", + "label": "Credible FOM (12,6)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (12,6).", + "unit": null + }, + { + "name": "credible_fom_6x12", + "label": "Credible FOM (6,12)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (6,12).", + "unit": null + }, + { + "name": "credible_fom_12x12", + "label": "Credible FOM (12,12)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (12,12).", + "unit": null + }, + { + "name": "credible_fom_24x6", + "label": "Credible FOM (24,6)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (24,6).", + "unit": null + }, + { + "name": "credible_fom_15x12", + "label": "Credible FOM (15,12)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (15,12).", + "unit": null + }, + { + "name": "credible_fom_30x6", + "label": "Credible FOM (30,6)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (30,6).", + "unit": null + }, + { + "name": "credible_fom_16x9", + "label": "Credible FOM (16,9)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (16,9).", + "unit": null + }, + { + "name": "credible_fom_18x8", + "label": "Credible FOM (18,8)", + "direction": "maximize", + "visibility": "public", + "metric_description": "Three-pass mean of the best trust-filtered FOM on lattice (18,8).", + "unit": null + } + ], + "ranking": { + "primary_metric_name": "score", + "tie_breaker_metric_names": null + } + } +} diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/statement.md b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/statement.md new file mode 100644 index 0000000..451bbe2 --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/statement.md @@ -0,0 +1,92 @@ +# Bivariate Bicycle Quantum Code Discovery + +Design a program that generates candidate CSS bivariate-bicycle quantum LDPC codes. Your program is evaluated with the original trust-filtered fitness used by IBM's `qcode-discovery` evolutionary campaign. + +## Submission Interface + +Submit a `zip_project` containing `agentics.solution.json`. Agentics invokes the declared run command once per lattice with these environment variables: + +- `AGENTICS_INPUT_DIR`: read-only input directory. +- `AGENTICS_OUTPUT_DIR`: writable output directory. + +Read `AGENTICS_INPUT_DIR/instance.json`: + +```json +{"schema_version": 1, "ell": 12, "m": 6, "max_candidates": 5000} +``` + +Write `AGENTICS_OUTPUT_DIR/candidates.json`: + +```json +{ + "candidates": [ + { + "a_terms": [[3, 0], [0, 1], [0, 2]], + "b_terms": [[0, 3], [1, 0], [2, 0]] + } + ] +} +``` + +Each exponent pair represents the monomial `x^a y^b`. Every polynomial must contain 2–6 distinct pairs with `0 <= a < ell` and `0 <= b < m`. Candidate order is significant. The evaluator reads at most 8 MiB and evaluates only the first 5,000 candidates for each lattice. + +Your run command has no network access. It may use any language and does not need `qldpc`; the trusted evaluator constructs and scores the codes independently. + +## Public Validation + +Validation uses lattices `(6,6)` and `(12,6)` and computes only the logical dimension `k`. Both lattices must have at least one candidate with `k > 0`. + +When both lattices are covered, the validation score is: + +$$ +S_1 = 0.1 + \max(k/n) + \frac{\log(1 + N_{k\ge8})}{10}. +$$ + +Partial coverage receives `0.001` per covered lattice and remains below the original cascade threshold. + +## Official Evaluation + +Official scoring uses these published lattices: + +```text +(12,6), (6,12), (12,12), (24,6), +(15,12), (30,6), (16,9), (18,8) +``` + +For each lattice, the evaluator computes `n` and `k` for every candidate, keeps candidates with `k >= 8`, and selects at most ten for distance estimation. Selection first preserves distinct `k` values, then distinct `A` polynomials, and finally fills remaining slots in stable score and submission order. + +Selected candidates receive a 1,000-trial BP-OSD distance estimate. Candidates whose preliminary figure of merit is at least 6 receive three additional 500-trial BP-OSD batches, with the smallest observed upper bound retained. These counts follow the pinned adapter's executable argument wiring; nearby source prose describes different defaults. + +For a candidate with parameters `[[n,k,d]]`, the raw figure of merit is: + +$$ +F = \frac{k d^2}{n}. +$$ + +The source evaluator applies a credibility filter using `r = d / sqrt(n)`: + +- `r <= 1.3`: use the full raw FOM. +- `r >= 2.0`: discard the distance contribution and use the encoding rate `k/n`. +- Between the thresholds: linearly interpolate between raw FOM and `k/n`. + +One pass score is the sum of the best credible value on each lattice. Agentics runs three independent passes in parallel and ranks by their arithmetic mean: + +$$ +\text{score} = \frac{S^{(1)} + S^{(2)} + S^{(3)}}{3}. +$$ + +Higher `score` is better. `score_stddev`, `score_min`, `score_max`, the per-lattice credible FOM means, and aggregate candidate statistics are reported for diagnosis but do not break ties. + +## Interpretation And Limitations + +BP-OSD produces stochastic upper bounds on distance. A reported `d` or FOM is not a proof of exact distance. The `d / sqrt(n)` filter is an empirical heuristic intended to prevent obviously inflated upper bounds from dominating evolution. + +The pinned source comments mention OSD-CS, but its executable default stage-2 path sets `fom_threshold_exact` to infinity and therefore runs neither OSD-CS nor exact distance. This challenge follows that executable behavior. + +The official lattices and published candidates are intentionally public. Reusing known codes is a legitimate baseline, but a high leaderboard score does not establish novelty or inequivalence. Exact scientific claims require independent verification. + +A future MILP-backed challenge will use the separate handle `milp-verified-bivariate-bicycle-code-discovery-qcode-discovery`. Its stronger verification must not replace this challenge's published scoring contract. + +## Provenance + +This challenge adapts [qiskit-community/qcode-discovery](https://github.com/qiskit-community/qcode-discovery) commit `4e828d0bc74066df9484e80f751a52674af7251f`, released under Apache-2.0. See IBM Research's [AI for quantum error correction](https://research.ibm.com/blog/ai-for-qec) and [arXiv:2606.02418](https://arxiv.org/abs/2606.02418). diff --git a/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/tests/test_scoring.py b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/tests/test_scoring.py new file mode 100644 index 0000000..98c035a --- /dev/null +++ b/challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/tests/test_scoring.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import json +import math +import tempfile +import unittest +from pathlib import Path + +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "separated-evaluator")) + +from scoring import ( # noqa: E402 + MAX_CANDIDATES, + aggregate_official_passes, + credible_fom, + load_candidate_collection, + parse_candidate_document, + require_complete_upstream_pass, + score_stage1_metrics, + score_stage2_metrics, +) + + +class CandidateParsingTests(unittest.TestCase): + def test_preserves_order_and_replaces_invalid_candidates(self) -> None: + payload = { + "candidates": [ + {"a_terms": [[1, 0], [0, 1]], "b_terms": [[0, 0], [2, 0]]}, + {"a_terms": [[1, 0]], "b_terms": [[0, 0], [2, 0]]}, + {"a_terms": [[3, 0], [0, 2]], "b_terms": [[0, 3], [1, 0]]}, + ] + } + parsed, submitted, warnings = parse_candidate_document(payload, (6, 6)) + self.assertEqual(submitted, 3) + self.assertEqual(parsed[0][0], [(1, 0), (0, 1)]) + self.assertEqual(parsed[1], ([], [])) + self.assertEqual(parsed[2][0], [(3, 0), (0, 2)]) + self.assertEqual(len(warnings), 1) + + def test_rejects_terms_outside_the_lattice_without_reordering(self) -> None: + payload = { + "candidates": [ + {"a_terms": [[1, 0], [0, 1]], "b_terms": [[0, 0], [2, 0]]}, + {"a_terms": [[6, 0], [0, 1]], "b_terms": [[0, 0], [2, 0]]}, + {"a_terms": [[1, 0], [0, 1]], "b_terms": [[0, -1], [2, 0]]}, + ] + } + parsed, submitted, warnings = parse_candidate_document(payload, (6, 6)) + self.assertEqual(submitted, 3) + self.assertNotEqual(parsed[0], ([], [])) + self.assertEqual(parsed[1:], [([], []), ([], [])]) + self.assertEqual(len(warnings), 2) + + def test_truncates_after_source_cap(self) -> None: + candidate = {"a_terms": [[1, 0], [0, 1]], "b_terms": [[0, 0], [2, 0]]} + parsed, submitted, warnings = parse_candidate_document( + {"candidates": [candidate] * (MAX_CANDIDATES + 2)}, + (6, 6), + ) + self.assertEqual(submitted, MAX_CANDIDATES + 2) + self.assertEqual(len(parsed), MAX_CANDIDATES) + self.assertIn("truncated", warnings[-1]) + + def test_failed_solution_run_contributes_no_candidates(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + run_dir = root / "lattice-6x6" + run_dir.mkdir() + (run_dir / "agentics-run.json").write_text( + json.dumps({"exit_code": 1, "timed_out": False}), encoding="utf-8" + ) + runs = {"runs": [{"run_name": "lattice-6x6", "metadata": {"ell": 6, "m": 6}}]} + collection = load_candidate_collection(runs, root) + self.assertEqual(collection.candidates[(6, 6)], []) + self.assertEqual(collection.submitted_counts[(6, 6)], 0) + self.assertIn("exited with code", collection.warnings[0]) + + +class FitnessTests(unittest.TestCase): + def test_trust_filter_boundaries_and_interpolation(self) -> None: + n = 100 + k = 10 + self.assertEqual(credible_fom(n, k, 13), 16.9) + self.assertEqual(credible_fom(n, k, 20), 0.1) + middle = credible_fom(n, k, 16) + alpha = (2.0 - 1.6) / (2.0 - 1.3) + self.assertAlmostEqual(middle, alpha * 25.6 + (1.0 - alpha) * 0.1) + + def test_stage1_requires_complete_lattice_coverage(self) -> None: + metrics = { + "all_results": [ + {"ell": 6, "m": 6, "k": 12, "encoding_rate": 1 / 6}, + ], + "num_high_k": 1, + "lattices_with_high_k": 1, + "total_candidates": 1, + } + partial = score_stage1_metrics(metrics, 2) + self.assertEqual(partial["stage1_score"], 0.001) + metrics["all_results"].append({"ell": 12, "m": 6, "k": 12, "encoding_rate": 1 / 12}) + metrics["num_high_k"] = 2 + complete = score_stage1_metrics(metrics, 2) + self.assertAlmostEqual(complete["stage1_score"], 0.1 + 1 / 6 + math.log1p(2) / 10) + + def test_stage2_takes_best_credible_fom_per_lattice(self) -> None: + metrics = { + "all_results": [ + {"ell": 6, "m": 6, "n": 72, "k": 12, "d": 6}, + {"ell": 6, "m": 6, "n": 72, "k": 8, "d": 4}, + {"ell": 12, "m": 6, "n": 144, "k": 12, "d": 12}, + ], + "best_fom": 12.0, + "num_valid": 3, + "num_high_k": 3, + "lattices_with_high_k": 2, + "total_candidates": 3, + } + result = score_stage2_metrics(metrics, [(6, 6), (12, 6)]) + self.assertEqual(result["per_lattice"][(6, 6)], 6.0) + self.assertEqual(result["per_lattice"][(12, 6)], 12.0) + self.assertEqual(result["combined_score"], 18.0) + + def test_three_pass_aggregate_uses_population_spread(self) -> None: + lattices = [(6, 6)] + passes = [ + { + "combined_score": score, + "best_fom": score + 1, + "valid_codes": 10, + "high_k_codes": 5, + "lattices_with_high_k": 1, + "total_candidates": 20, + "per_lattice": {(6, 6): score}, + } + for score in (2.0, 4.0, 6.0) + ] + result = aggregate_official_passes(passes, lattices) + self.assertEqual(result["score"], 4.0) + self.assertAlmostEqual(result["score_stddev"], math.sqrt(8 / 3)) + self.assertEqual(result["score_min"], 2.0) + self.assertEqual(result["score_max"], 6.0) + self.assertEqual(result["per_lattice"][(6, 6)], 4.0) + + def test_three_pass_aggregate_rejects_partial_completion(self) -> None: + with self.assertRaisesRegex(RuntimeError, "expected 3 complete passes"): + aggregate_official_passes([], [(6, 6)]) + + def test_official_pass_rejects_upstream_lattice_errors(self) -> None: + with self.assertRaisesRegex(RuntimeError, "did not complete every lattice"): + require_complete_upstream_pass({"errors": ["(12,6): dependency failure"]}) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/README.md b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/README.md new file mode 100644 index 0000000..15677e2 --- /dev/null +++ b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/README.md @@ -0,0 +1,7 @@ +# Bivariate Bicycle Quantum Code Discovery Baseline + +This meaningful baseline adapts `evolve/seed_solution.py` from [qiskit-community/qcode-discovery](https://github.com/qiskit-community/qcode-discovery) commit `4e828d0bc74066df9484e80f751a52674af7251f` to the Agentics file-system run interface. + +The generator combines known published codes, perturbations, self-similar constructions, and bounded x/y-swap searches. It was the public starting point for the upstream evolutionary campaign, so it is an intended baseline rather than a hidden-evaluator shortcut. + +The copied `seed_solution.py` remains licensed under the upstream Apache License 2.0. The license text is included with the challenge evaluator at `challenges/bivariate-bicycle-code-discovery-qcode-discovery/v1/separated-evaluator/vendor/qcode-discovery/LICENSE`. diff --git a/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/agentics.solution.json b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/agentics.solution.json new file mode 100644 index 0000000..ccfb7f2 --- /dev/null +++ b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/agentics.solution.json @@ -0,0 +1,10 @@ +{ + "protocol": "zip_project", + "protocol_version": 1, + "note": "Published IBM qcode-discovery seed generator adapted to the Agentics JSON interface.", + "commands": { + "setup": null, + "build": null, + "run": "run.sh" + } +} diff --git a/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/run.sh b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/run.sh new file mode 100755 index 0000000..55baf8c --- /dev/null +++ b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +python3 solution.py diff --git a/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/seed_solution.py b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/seed_solution.py new file mode 100644 index 0000000..3310d96 --- /dev/null +++ b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/seed_solution.py @@ -0,0 +1,304 @@ +"""Seed solution for the evolutionary search. + +This is the file that OpenEvolve loads and mutates. It contains three +top-level objects: + +``KNOWN_CODES`` : list[dict] + 14 verified BB codes -- 9 from Bravyi et al. 2024 (arXiv:2308.07915) + and 5 original discoveries verified with the 150k-trial multi-decoder + soak test. Each entry has ``ell, m, A_terms, B_terms, expected, + name, source``. These serve as regression baselines and as seeds for + the perturbation strategy inside ``generate_candidates``. + +``TARGET_LATTICES`` : list[tuple[int, int]] + 18 target ``(ell, m)`` lattice dimensions covering code sizes + ``n = 144, 200, 288, 360``. Each size includes several factorizations + (e.g. ``n=144`` has ``(12,6), (6,12), (9,8), (8,9), (24,3), (36,2)``). + +``generate_candidates(ell, m)`` : function + The function that OpenEvolve evolves -- enclosed between + ``# EVOLVE-BLOCK-START`` and ``# EVOLVE-BLOCK-END`` markers. + The seed version uses four generation strategies: + + 1. **x/y-swap symmetric** -- ``A = x^a + y^b + y^(2b)``, + ``B = y^d + x^e + x^(2e)``. Matches the gross code pattern. + 2. **Perturbation of known codes** -- shifts each exponent of + benchmark codes by ±1 and ±2. + 3. **Self-similar scaling** -- applies canonical polynomial pairs at + whatever lattice size they fit. + 4. **Full x/y-swap search** -- independent y-exponents for A and + x-exponents for B with small bound (``min(dim, 5)``). + + All strategies share a deduplication set to avoid evaluating the + same candidate twice. + +Sources: + - Bravyi et al. 2024 (arXiv:2308.07915, Table 3) + - qldpc examples (bivariate_bicycle_codes.ipynb) + - Chengyu et al. 2026 (arXiv:2601.18562) +""" + +from __future__ import annotations + +# --------------------------------------------------------------------------- +# Known BB codes -- verified against qldpc (n, k match exactly) +# --------------------------------------------------------------------------- +# Each entry: ell, m, A_terms, B_terms, (expected_n, expected_k, expected_d, fom) +# +# A_terms / B_terms are lists of (x_exp, y_exp) exponent pairs. +# Polynomial: A = sum of x^a * y^b for each (a,b) in A_terms. +# +# Pattern key: +# "xy_swap" = A uses x^a + y-terms, B uses y^d + x-terms +# "mixed" = A and B don't follow the swap pattern + +KNOWN_CODES = [ + # --- arXiv:2308.07915, Table 3 --- + # [[72, 12, 6]] A = x^3 + y + y^2, B = y^3 + x + x^2 + { + "ell": 6, "m": 6, + "A_terms": [(3, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (72, 12, 6, 6.0), + "name": "[[72,12,6]]", + "source": "arXiv:2308.07915", + }, + # [[90, 8, 10]] A = x^9 + y + y^2, B = 1 + x^2 + x^7 + { + "ell": 15, "m": 3, + "A_terms": [(9, 0), (0, 1), (0, 2)], + "B_terms": [(0, 0), (2, 0), (7, 0)], + "expected": (90, 8, 10, 8.89), + "name": "[[90,8,10]]", + "source": "arXiv:2308.07915", + }, + # [[108, 8, 10]] A = x^3 + y + y^2, B = y^3 + x + x^2 + { + "ell": 9, "m": 6, + "A_terms": [(3, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (108, 8, 10, 7.41), + "name": "[[108,8,10]]", + "source": "arXiv:2308.07915", + }, + # [[144, 12, 12]] A = x^3 + y + y^2, B = y^3 + x + x^2 (the "gross code") + { + "ell": 12, "m": 6, + "A_terms": [(3, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (144, 12, 12, 12.0), + "name": "[[144,12,12]] gross", + "source": "arXiv:2308.07915", + }, + # [[144, 12, 12]] Same polynomials, different lattice (6,12) + { + "ell": 6, "m": 12, + "A_terms": [(3, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (144, 12, 12, 12.0), + "name": "[[144,12,12]] (6,12)", + "source": "arXiv:2308.07915", + }, + # [[216, 12, ?]] A = x^3 + y + y^2, B = y^3 + x + x^2 + { + "ell": 18, "m": 6, + "A_terms": [(3, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (216, 12, None, None), + "name": "[[216,12,?]]", + "source": "arXiv:2308.07915", + }, + # [[288, 12, 18]] A = x^3 + y^2 + y^7, B = y^3 + x + x^2 + { + "ell": 12, "m": 12, + "A_terms": [(3, 0), (0, 2), (0, 7)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (288, 12, 18, 13.5), + "name": "[[288,12,18]]", + "source": "arXiv:2308.07915", + }, + # [[288, 12, ?]] A = x^3 + y + y^2, B = y^3 + x + x^2 at (24,6) + { + "ell": 24, "m": 6, + "A_terms": [(3, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (1, 0), (2, 0)], + "expected": (288, 12, None, None), + "name": "[[288,12,?]] (24,6)", + "source": "arXiv:2308.07915", + }, + # [[360, 12, <=24]] A = x^9 + y + y^2, B = y^3 + x^25 + x^26 + { + "ell": 30, "m": 6, + "A_terms": [(9, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (25, 0), (26, 0)], + "expected": (360, 12, 24, 19.2), + "name": "[[360,12,<=24]]", + "source": "arXiv:2308.07915", + }, + # --- Verified discoveries (150k-trial multi-decoder protocol) --- + # [[360, 40, <=20]] Constant-monomial: A = 1+y+y^2, B = 1+x^5+x^10 + { + "ell": 15, "m": 12, + "A_terms": [(0, 0), (0, 1), (0, 2)], + "B_terms": [(0, 0), (5, 0), (10, 0)], + "expected": (360, 40, 20, 44.4), + "name": "[[360,40,<=20]]", + "source": "this work", + }, + # [[288, 32, <=20]] x/y-swap: A = x^3+y^2+y^10, B = y^6+x+x^11 + { + "ell": 12, "m": 12, + "A_terms": [(3, 0), (0, 2), (0, 10)], + "B_terms": [(0, 6), (1, 0), (11, 0)], + "expected": (288, 32, 20, 44.4), + "name": "[[288,32,<=20]]", + "source": "this work", + }, + # [[360, 32, <=16]] Constant-monomial: A = 1+y^2+y^4, B = 1+x^3+x^4 + { + "ell": 15, "m": 12, + "A_terms": [(0, 0), (0, 2), (0, 4)], + "B_terms": [(0, 0), (3, 0), (4, 0)], + "expected": (360, 32, 16, 22.8), + "name": "[[360,32,<=16]]", + "source": "this work", + }, + # [[288, 32, <=12]] Constant-monomial: A = 1+y^2+y^4, B = 1+x^2+x^4 + { + "ell": 12, "m": 12, + "A_terms": [(0, 0), (0, 2), (0, 4)], + "B_terms": [(0, 0), (2, 0), (4, 0)], + "expected": (288, 32, 12, 16.0), + "name": "[[288,32,<=12]]", + "source": "this work", + }, + # [[288, 24, <=12]] x/y-swap: A = x^6+y+y^2, B = y^3+x^2+x^4 + { + "ell": 24, "m": 6, + "A_terms": [(6, 0), (0, 1), (0, 2)], + "B_terms": [(0, 3), (2, 0), (4, 0)], + "expected": (288, 24, 12, 12.0), + "name": "[[288,24,<=12]]", + "source": "this work", + }, +] + + +# Target lattice dimensions for the search +TARGET_LATTICES = [ + # n=144: ell*m = 72 + (12, 6), (6, 12), (9, 8), (8, 9), (24, 3), (36, 2), + # n=200: ell*m = 100 + (10, 10), (20, 5), (25, 4), (50, 2), + # n=288: ell*m = 144 + (12, 12), (16, 9), (18, 8), (24, 6), + # n=360: ell*m = 180 + (15, 12), (18, 10), (20, 9), (30, 6), +] + + +# EVOLVE-BLOCK-START +def generate_candidates( + ell: int, m: int +) -> list[tuple[list[tuple[int, int]], list[tuple[int, int]]]]: + """Generate candidate (A_terms, B_terms) pairs for given lattice dimensions. + + This is the function that gets evolved by the LLM. The initial version + uses algebraic heuristics based on known good code patterns. + + Args: + ell: Cyclic group order for x. + m: Cyclic group order for y. + + Returns: + List of (A_terms, B_terms) pairs to evaluate. + """ + candidates = [] + seen = set() + + def _add(a_terms, b_terms): + """Add candidate if not a duplicate.""" + key = (tuple(sorted(a_terms)), tuple(sorted(b_terms))) + if key not in seen: + seen.add(key) + candidates.append((list(a_terms), list(b_terms))) + + # Strategy 1: x/y-swap symmetric construction + # Pattern: A = x^a + y^b + y^(2b), B = y^d + x^e + x^(2e) + # Matches the gross code structure. Restrict to small exponents + # (good codes use exponents ≤ ℓ/2). The evolutionary loop will + # expand the search beyond this initial neighborhood. + max_a = ell // 2 + 1 + max_y = m // 2 + 1 + for a in range(1, max_a): + for b in range(1, max_y): + c = (2 * b) % m + A = [(a, 0), (0, b), (0, c)] + if len(set(A)) != 3: + continue + for d in range(1, max_y): + for e in range(1, max_a): + f = (2 * e) % ell + B = [(0, d), (e, 0), (f, 0)] + if len(set(B)) == 3: + _add(A, B) + + # Strategy 2: Perturbations of known good codes at this lattice + for code_spec in KNOWN_CODES: + if code_spec["ell"] == ell and code_spec["m"] == m: + base_A = code_spec["A_terms"] + base_B = code_spec["B_terms"] + _add(base_A, base_B) + + # Perturb each exponent by ±1, ±2 + for delta in [-2, -1, 1, 2]: + for i in range(3): + for coord in [0, 1]: # x or y exponent + new_A = [list(t) for t in base_A] + limit = ell if coord == 0 else m + new_A[i][coord] = (new_A[i][coord] + delta) % limit + new_A_tuples = [tuple(t) for t in new_A] + if len(set(new_A_tuples)) == 3: + _add(new_A_tuples, base_B) + + new_B = [list(t) for t in base_B] + limit = ell if coord == 0 else m + new_B[i][coord] = (new_B[i][coord] + delta) % limit + new_B_tuples = [tuple(t) for t in new_B] + if len(set(new_B_tuples)) == 3: + _add(base_A, new_B_tuples) + + # Strategy 3: Self-similar scaling + # If A=x^3+y+y^2, B=y^3+x+x^2 works at (6,6) and (12,6), + # try the same polynomials at the current lattice. + canonical_polys = [ + ([(3, 0), (0, 1), (0, 2)], [(0, 3), (1, 0), (2, 0)]), + ([(9, 0), (0, 1), (0, 2)], [(0, 3), (1, 0), (2, 0)]), + ] + for A, B in canonical_polys: + # Check exponents are in range + if all(a < ell and b < m for a, b in A) and \ + all(a < ell and b < m for a, b in B): + _add(A, B) + + # Strategy 4: Full x/y-swap search without the doubling constraint. + # Strategy 1 uses c=2b, f=2e. Here we try all independent y-exponents + # for A and x-exponents for B with small exponent bound. + # Pattern: A = x^a + y^b + y^c (b ≠ c), B = y^d + x^e + x^f (e ≠ f) + max_x = min(ell, 5) + max_yy = min(m, 5) + for a in range(1, max_x): + for b in range(0, max_yy): + for c in range(b + 1, max_yy): + A = [(a, 0), (0, b), (0, c)] + if len(set(A)) != 3: + continue + for d in range(1, max_yy): + for e in range(0, max_x): + for f in range(e + 1, max_x): + B = [(0, d), (e, 0), (f, 0)] + if len(set(B)) == 3: + _add(A, B) + + return candidates +# EVOLVE-BLOCK-END diff --git a/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/solution.py b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/solution.py new file mode 100644 index 0000000..6f0317c --- /dev/null +++ b/test-solutions/bivariate-bicycle-code-discovery-qcode-discovery/solution.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +from seed_solution import generate_candidates + + +def positive_int(value: Any, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise ValueError(f"{field} must be a positive integer") + return value + + +def main() -> int: + input_dir = Path(os.environ["AGENTICS_INPUT_DIR"]) + output_dir = Path(os.environ["AGENTICS_OUTPUT_DIR"]) + instance = json.loads((input_dir / "instance.json").read_text(encoding="utf-8")) + if not isinstance(instance, dict) or instance.get("schema_version") != 1: + raise ValueError("instance.json must be a schema-version-1 object") + ell = positive_int(instance.get("ell"), "ell") + m = positive_int(instance.get("m"), "m") + max_candidates = positive_int(instance.get("max_candidates"), "max_candidates") + generated = generate_candidates(ell, m)[:max_candidates] + candidates = [ + { + "a_terms": [[int(x), int(y)] for x, y in a_terms], + "b_terms": [[int(x), int(y)] for x, y in b_terms], + } + for a_terms, b_terms in generated + ] + output_dir.mkdir(parents=True, exist_ok=True) + (output_dir / "candidates.json").write_text( + json.dumps({"candidates": candidates}, separators=(",", ":")), + encoding="utf-8", + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())