October 2026 main drop: mirror from GitHub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- .gitignore +15 -0
- CHANGELOG.md +18 -0
- CITATION.cff +21 -0
- LICENSE +23 -0
- LICENSES/AGPL-3.0.txt +661 -0
- LICENSES/CPSC-ESCL-1.0.txt +149 -0
- NOTICE +19 -0
- README.md +19 -0
- REUSE.toml +13 -0
- docs/.nojekyll +0 -0
- docs/CHANNEL_PRUNING.md +34 -0
- docs/OPEN_PROBLEMS.md +35 -0
- docs/SPECIFICATION.md +76 -0
- docs/index.html +66 -0
- docs/mercury-circuit-demo.gif +3 -0
- docs/mercury-circuit-demo.mp4 +3 -0
- docs/mercury_bundle.rb +845 -0
- docs/qsim.html +307 -0
- frontend/README.md +104 -0
- frontend/lib/mercury.rb +21 -0
- frontend/lib/mercury/circuit.rb +115 -0
- frontend/lib/mercury/cpsc.rb +159 -0
- frontend/lib/mercury/gates.rb +126 -0
- frontend/lib/mercury/mini_json.rb +164 -0
- frontend/lib/mercury/simulator.rb +164 -0
- frontend/lib/mercury/version.rb +16 -0
- frontend/test/test_mercury.rb +261 -0
- frontend/web/app.rb +87 -0
- frontend/web/build.sh +31 -0
- frontend/web/index.html +307 -0
- frontend/web/mercury_bundle.rb +845 -0
- frontend/web/qsim.html +307 -0
- lean/CPSC.lean +8 -0
- lean/CPSC/Axioms.lean +89 -0
- lean/CPSC/Certificate.lean +155 -0
- lean/CPSC/Emitted.lean +16 -0
- lean/CPSC/Lemmas.lean +111 -0
- lean/lake-manifest.json +5 -0
- lean/lakefile.toml +16 -0
- lean/lean-toolchain +1 -0
- paper/ABSTRACT.md +10 -0
- paper/PAPER.md +115 -0
- pyproject.toml +35 -0
- qsharp/Axioms.qs +72 -0
- qsharp/ConservationPreservingScattering.qs +104 -0
- qsharp/Lemmas.qs +116 -0
- src/cpsc/__init__.py +31 -0
- src/cpsc/analyzer.py +120 -0
- src/cpsc/certificate.py +104 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
docs/mercury-circuit-demo.gif filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docs/mercury-circuit-demo.mp4 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
.venv/
|
| 4 |
+
venv/
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.mypy_cache/
|
| 7 |
+
*.egg-info/
|
| 8 |
+
dist/
|
| 9 |
+
build/
|
| 10 |
+
.lake/
|
| 11 |
+
lean/CPSC/.lake/
|
| 12 |
+
*.olean
|
| 13 |
+
.DS_Store
|
| 14 |
+
|
| 15 |
+
lean/.lake/
|
CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
-->
|
| 5 |
+
|
| 6 |
+
# Changelog
|
| 7 |
+
|
| 8 |
+
## 0.3.0 — 2026-10-07
|
| 9 |
+
|
| 10 |
+
Production release of the research prototype.
|
| 11 |
+
|
| 12 |
+
- Mercury Ruby frontend: circuit DSL, statevector simulator, CPSC bridge, WASM console.
|
| 13 |
+
- GitHub Pages site in `docs/`, console at `qsim.html`.
|
| 14 |
+
- Lean Boolean lemmas proved; operator commutation held as axioms; `lake build` passes.
|
| 15 |
+
- Q# axiom/lemma interpreter fails closed on a rejected factor.
|
| 16 |
+
- Dual strict copyleft: `LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only`.
|
| 17 |
+
|
| 18 |
+
Known limit: the browser analyzer mirrors Z-parity admission. The Python compiler is the path that switches to spin-flip mode at λ ≠ 0.
|
CITATION.cff
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cff-version: 1.2.0
|
| 2 |
+
message: "If you use this scaffold, cite it as software, not as a discharged formalization."
|
| 3 |
+
title: "CPSC: Conservation-Preserving Scattering Compiler"
|
| 4 |
+
abstract: >
|
| 5 |
+
Research scaffold for compilation-time enforcement of lattice momentum and
|
| 6 |
+
charge in 1+1D qubit-lattice scattering, with a Lean 4 certificate interface
|
| 7 |
+
and an executable symmetry analyzer. Proof obligations are open.
|
| 8 |
+
authors:
|
| 9 |
+
- family-names: Parr
|
| 10 |
+
given-names: Ahmad Ali
|
| 11 |
+
version: 0.1.0
|
| 12 |
+
date-released: 2026-10-07
|
| 13 |
+
license: AGPL-3.0-only
|
| 14 |
+
repository-code: "https://github.com/AHMADALIPARR/cpsc-qft"
|
| 15 |
+
keywords:
|
| 16 |
+
- quantum-field-theory
|
| 17 |
+
- formal-verification
|
| 18 |
+
- lean4
|
| 19 |
+
- scattering
|
| 20 |
+
- symmetry
|
| 21 |
+
license-url: "https://github.com/AHMADALIPARR/cpsc-qft/blob/main/LICENSE"
|
LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CPSC Eclipse Strict Copyleft License, Version 1.0
|
| 2 |
+
or, at your option, the GNU Affero General Public License, Version 3 only.
|
| 3 |
+
|
| 4 |
+
Copyright (c) 2026 Ahmad Ali Parr
|
| 5 |
+
|
| 6 |
+
This repository is dual-licensed under:
|
| 7 |
+
|
| 8 |
+
(A) the CPSC Eclipse Strict Copyleft License, Version 1.0
|
| 9 |
+
(LicenseRef-CPSC-ESCL-1.0), in LICENSES/CPSC-ESCL-1.0.txt
|
| 10 |
+
|
| 11 |
+
(B) the GNU Affero General Public License, Version 3 only
|
| 12 |
+
(AGPL-3.0-only), in LICENSES/AGPL-3.0.txt
|
| 13 |
+
|
| 14 |
+
You may use the work under either license. Both are strict copyleft.
|
| 15 |
+
Neither option is the Eclipse Public License, the Apache License, or a
|
| 16 |
+
permissive grant. There is no classpath exception. A recipient may not
|
| 17 |
+
relicense the work, or a modified version of it, under MIT, Apache-2.0,
|
| 18 |
+
BSD, EPL-2.0, or any other non-strict-copyleft license.
|
| 19 |
+
|
| 20 |
+
File headers are part of the license notice. Keep them.
|
| 21 |
+
|
| 22 |
+
This custom instrument is not issued by the Eclipse Foundation and grants
|
| 23 |
+
no right to Eclipse trademarks.
|
LICENSES/AGPL-3.0.txt
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
| 2 |
+
Version 3, 19 November 2007
|
| 3 |
+
|
| 4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
| 5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
| 6 |
+
of this license document, but changing it is not allowed.
|
| 7 |
+
|
| 8 |
+
Preamble
|
| 9 |
+
|
| 10 |
+
The GNU Affero General Public License is a free, copyleft license for
|
| 11 |
+
software and other kinds of works, specifically designed to ensure
|
| 12 |
+
cooperation with the community in the case of network server software.
|
| 13 |
+
|
| 14 |
+
The licenses for most software and other practical works are designed
|
| 15 |
+
to take away your freedom to share and change the works. By contrast,
|
| 16 |
+
our General Public Licenses are intended to guarantee your freedom to
|
| 17 |
+
share and change all versions of a program--to make sure it remains free
|
| 18 |
+
software for all its users.
|
| 19 |
+
|
| 20 |
+
When we speak of free software, we are referring to freedom, not
|
| 21 |
+
price. Our General Public Licenses are designed to make sure that you
|
| 22 |
+
have the freedom to distribute copies of free software (and charge for
|
| 23 |
+
them if you wish), that you receive source code or can get it if you
|
| 24 |
+
want it, that you can change the software or use pieces of it in new
|
| 25 |
+
free programs, and that you know you can do these things.
|
| 26 |
+
|
| 27 |
+
Developers that use our General Public Licenses protect your rights
|
| 28 |
+
with two steps: (1) assert copyright on the software, and (2) offer
|
| 29 |
+
you this License which gives you legal permission to copy, distribute
|
| 30 |
+
and/or modify the software.
|
| 31 |
+
|
| 32 |
+
A secondary benefit of defending all users' freedom is that
|
| 33 |
+
improvements made in alternate versions of the program, if they
|
| 34 |
+
receive widespread use, become available for other developers to
|
| 35 |
+
incorporate. Many developers of free software are heartened and
|
| 36 |
+
encouraged by the resulting cooperation. However, in the case of
|
| 37 |
+
software used on network servers, this result may fail to come about.
|
| 38 |
+
The GNU General Public License permits making a modified version and
|
| 39 |
+
letting the public access it on a server without ever releasing its
|
| 40 |
+
source code to the public.
|
| 41 |
+
|
| 42 |
+
The GNU Affero General Public License is designed specifically to
|
| 43 |
+
ensure that, in such cases, the modified source code becomes available
|
| 44 |
+
to the community. It requires the operator of a network server to
|
| 45 |
+
provide the source code of the modified version running there to the
|
| 46 |
+
users of that server. Therefore, public use of a modified version, on
|
| 47 |
+
a publicly accessible server, gives the public access to the source
|
| 48 |
+
code of the modified version.
|
| 49 |
+
|
| 50 |
+
An older license, called the Affero General Public License and
|
| 51 |
+
published by Affero, was designed to accomplish similar goals. This is
|
| 52 |
+
a different license, not a version of the Affero GPL, but Affero has
|
| 53 |
+
released a new version of the Affero GPL which permits relicensing under
|
| 54 |
+
this license.
|
| 55 |
+
|
| 56 |
+
The precise terms and conditions for copying, distribution and
|
| 57 |
+
modification follow.
|
| 58 |
+
|
| 59 |
+
TERMS AND CONDITIONS
|
| 60 |
+
|
| 61 |
+
0. Definitions.
|
| 62 |
+
|
| 63 |
+
"This License" refers to version 3 of the GNU Affero General Public License.
|
| 64 |
+
|
| 65 |
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
| 66 |
+
works, such as semiconductor masks.
|
| 67 |
+
|
| 68 |
+
"The Program" refers to any copyrightable work licensed under this
|
| 69 |
+
License. Each licensee is addressed as "you". "Licensees" and
|
| 70 |
+
"recipients" may be individuals or organizations.
|
| 71 |
+
|
| 72 |
+
To "modify" a work means to copy from or adapt all or part of the work
|
| 73 |
+
in a fashion requiring copyright permission, other than the making of an
|
| 74 |
+
exact copy. The resulting work is called a "modified version" of the
|
| 75 |
+
earlier work or a work "based on" the earlier work.
|
| 76 |
+
|
| 77 |
+
A "covered work" means either the unmodified Program or a work based
|
| 78 |
+
on the Program.
|
| 79 |
+
|
| 80 |
+
To "propagate" a work means to do anything with it that, without
|
| 81 |
+
permission, would make you directly or secondarily liable for
|
| 82 |
+
infringement under applicable copyright law, except executing it on a
|
| 83 |
+
computer or modifying a private copy. Propagation includes copying,
|
| 84 |
+
distribution (with or without modification), making available to the
|
| 85 |
+
public, and in some countries other activities as well.
|
| 86 |
+
|
| 87 |
+
To "convey" a work means any kind of propagation that enables other
|
| 88 |
+
parties to make or receive copies. Mere interaction with a user through
|
| 89 |
+
a computer network, with no transfer of a copy, is not conveying.
|
| 90 |
+
|
| 91 |
+
An interactive user interface displays "Appropriate Legal Notices"
|
| 92 |
+
to the extent that it includes a convenient and prominently visible
|
| 93 |
+
feature that (1) displays an appropriate copyright notice, and (2)
|
| 94 |
+
tells the user that there is no warranty for the work (except to the
|
| 95 |
+
extent that warranties are provided), that licensees may convey the
|
| 96 |
+
work under this License, and how to view a copy of this License. If
|
| 97 |
+
the interface presents a list of user commands or options, such as a
|
| 98 |
+
menu, a prominent item in the list meets this criterion.
|
| 99 |
+
|
| 100 |
+
1. Source Code.
|
| 101 |
+
|
| 102 |
+
The "source code" for a work means the preferred form of the work
|
| 103 |
+
for making modifications to it. "Object code" means any non-source
|
| 104 |
+
form of a work.
|
| 105 |
+
|
| 106 |
+
A "Standard Interface" means an interface that either is an official
|
| 107 |
+
standard defined by a recognized standards body, or, in the case of
|
| 108 |
+
interfaces specified for a particular programming language, one that
|
| 109 |
+
is widely used among developers working in that language.
|
| 110 |
+
|
| 111 |
+
The "System Libraries" of an executable work include anything, other
|
| 112 |
+
than the work as a whole, that (a) is included in the normal form of
|
| 113 |
+
packaging a Major Component, but which is not part of that Major
|
| 114 |
+
Component, and (b) serves only to enable use of the work with that
|
| 115 |
+
Major Component, or to implement a Standard Interface for which an
|
| 116 |
+
implementation is available to the public in source code form. A
|
| 117 |
+
"Major Component", in this context, means a major essential component
|
| 118 |
+
(kernel, window system, and so on) of the specific operating system
|
| 119 |
+
(if any) on which the executable work runs, or a compiler used to
|
| 120 |
+
produce the work, or an object code interpreter used to run it.
|
| 121 |
+
|
| 122 |
+
The "Corresponding Source" for a work in object code form means all
|
| 123 |
+
the source code needed to generate, install, and (for an executable
|
| 124 |
+
work) run the object code and to modify the work, including scripts to
|
| 125 |
+
control those activities. However, it does not include the work's
|
| 126 |
+
System Libraries, or general-purpose tools or generally available free
|
| 127 |
+
programs which are used unmodified in performing those activities but
|
| 128 |
+
which are not part of the work. For example, Corresponding Source
|
| 129 |
+
includes interface definition files associated with source files for
|
| 130 |
+
the work, and the source code for shared libraries and dynamically
|
| 131 |
+
linked subprograms that the work is specifically designed to require,
|
| 132 |
+
such as by intimate data communication or control flow between those
|
| 133 |
+
subprograms and other parts of the work.
|
| 134 |
+
|
| 135 |
+
The Corresponding Source need not include anything that users
|
| 136 |
+
can regenerate automatically from other parts of the Corresponding
|
| 137 |
+
Source.
|
| 138 |
+
|
| 139 |
+
The Corresponding Source for a work in source code form is that
|
| 140 |
+
same work.
|
| 141 |
+
|
| 142 |
+
2. Basic Permissions.
|
| 143 |
+
|
| 144 |
+
All rights granted under this License are granted for the term of
|
| 145 |
+
copyright on the Program, and are irrevocable provided the stated
|
| 146 |
+
conditions are met. This License explicitly affirms your unlimited
|
| 147 |
+
permission to run the unmodified Program. The output from running a
|
| 148 |
+
covered work is covered by this License only if the output, given its
|
| 149 |
+
content, constitutes a covered work. This License acknowledges your
|
| 150 |
+
rights of fair use or other equivalent, as provided by copyright law.
|
| 151 |
+
|
| 152 |
+
You may make, run and propagate covered works that you do not
|
| 153 |
+
convey, without conditions so long as your license otherwise remains
|
| 154 |
+
in force. You may convey covered works to others for the sole purpose
|
| 155 |
+
of having them make modifications exclusively for you, or provide you
|
| 156 |
+
with facilities for running those works, provided that you comply with
|
| 157 |
+
the terms of this License in conveying all material for which you do
|
| 158 |
+
not control copyright. Those thus making or running the covered works
|
| 159 |
+
for you must do so exclusively on your behalf, under your direction
|
| 160 |
+
and control, on terms that prohibit them from making any copies of
|
| 161 |
+
your copyrighted material outside their relationship with you.
|
| 162 |
+
|
| 163 |
+
Conveying under any other circumstances is permitted solely under
|
| 164 |
+
the conditions stated below. Sublicensing is not allowed; section 10
|
| 165 |
+
makes it unnecessary.
|
| 166 |
+
|
| 167 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
| 168 |
+
|
| 169 |
+
No covered work shall be deemed part of an effective technological
|
| 170 |
+
measure under any applicable law fulfilling obligations under article
|
| 171 |
+
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
| 172 |
+
similar laws prohibiting or restricting circumvention of such
|
| 173 |
+
measures.
|
| 174 |
+
|
| 175 |
+
When you convey a covered work, you waive any legal power to forbid
|
| 176 |
+
circumvention of technological measures to the extent such circumvention
|
| 177 |
+
is effected by exercising rights under this License with respect to
|
| 178 |
+
the covered work, and you disclaim any intention to limit operation or
|
| 179 |
+
modification of the work as a means of enforcing, against the work's
|
| 180 |
+
users, your or third parties' legal rights to forbid circumvention of
|
| 181 |
+
technological measures.
|
| 182 |
+
|
| 183 |
+
4. Conveying Verbatim Copies.
|
| 184 |
+
|
| 185 |
+
You may convey verbatim copies of the Program's source code as you
|
| 186 |
+
receive it, in any medium, provided that you conspicuously and
|
| 187 |
+
appropriately publish on each copy an appropriate copyright notice;
|
| 188 |
+
keep intact all notices stating that this License and any
|
| 189 |
+
non-permissive terms added in accord with section 7 apply to the code;
|
| 190 |
+
keep intact all notices of the absence of any warranty; and give all
|
| 191 |
+
recipients a copy of this License along with the Program.
|
| 192 |
+
|
| 193 |
+
You may charge any price or no price for each copy that you convey,
|
| 194 |
+
and you may offer support or warranty protection for a fee.
|
| 195 |
+
|
| 196 |
+
5. Conveying Modified Source Versions.
|
| 197 |
+
|
| 198 |
+
You may convey a work based on the Program, or the modifications to
|
| 199 |
+
produce it from the Program, in the form of source code under the
|
| 200 |
+
terms of section 4, provided that you also meet all of these conditions:
|
| 201 |
+
|
| 202 |
+
a) The work must carry prominent notices stating that you modified
|
| 203 |
+
it, and giving a relevant date.
|
| 204 |
+
|
| 205 |
+
b) The work must carry prominent notices stating that it is
|
| 206 |
+
released under this License and any conditions added under section
|
| 207 |
+
7. This requirement modifies the requirement in section 4 to
|
| 208 |
+
"keep intact all notices".
|
| 209 |
+
|
| 210 |
+
c) You must license the entire work, as a whole, under this
|
| 211 |
+
License to anyone who comes into possession of a copy. This
|
| 212 |
+
License will therefore apply, along with any applicable section 7
|
| 213 |
+
additional terms, to the whole of the work, and all its parts,
|
| 214 |
+
regardless of how they are packaged. This License gives no
|
| 215 |
+
permission to license the work in any other way, but it does not
|
| 216 |
+
invalidate such permission if you have separately received it.
|
| 217 |
+
|
| 218 |
+
d) If the work has interactive user interfaces, each must display
|
| 219 |
+
Appropriate Legal Notices; however, if the Program has interactive
|
| 220 |
+
interfaces that do not display Appropriate Legal Notices, your
|
| 221 |
+
work need not make them do so.
|
| 222 |
+
|
| 223 |
+
A compilation of a covered work with other separate and independent
|
| 224 |
+
works, which are not by their nature extensions of the covered work,
|
| 225 |
+
and which are not combined with it such as to form a larger program,
|
| 226 |
+
in or on a volume of a storage or distribution medium, is called an
|
| 227 |
+
"aggregate" if the compilation and its resulting copyright are not
|
| 228 |
+
used to limit the access or legal rights of the compilation's users
|
| 229 |
+
beyond what the individual works permit. Inclusion of a covered work
|
| 230 |
+
in an aggregate does not cause this License to apply to the other
|
| 231 |
+
parts of the aggregate.
|
| 232 |
+
|
| 233 |
+
6. Conveying Non-Source Forms.
|
| 234 |
+
|
| 235 |
+
You may convey a covered work in object code form under the terms
|
| 236 |
+
of sections 4 and 5, provided that you also convey the
|
| 237 |
+
machine-readable Corresponding Source under the terms of this License,
|
| 238 |
+
in one of these ways:
|
| 239 |
+
|
| 240 |
+
a) Convey the object code in, or embodied in, a physical product
|
| 241 |
+
(including a physical distribution medium), accompanied by the
|
| 242 |
+
Corresponding Source fixed on a durable physical medium
|
| 243 |
+
customarily used for software interchange.
|
| 244 |
+
|
| 245 |
+
b) Convey the object code in, or embodied in, a physical product
|
| 246 |
+
(including a physical distribution medium), accompanied by a
|
| 247 |
+
written offer, valid for at least three years and valid for as
|
| 248 |
+
long as you offer spare parts or customer support for that product
|
| 249 |
+
model, to give anyone who possesses the object code either (1) a
|
| 250 |
+
copy of the Corresponding Source for all the software in the
|
| 251 |
+
product that is covered by this License, on a durable physical
|
| 252 |
+
medium customarily used for software interchange, for a price no
|
| 253 |
+
more than your reasonable cost of physically performing this
|
| 254 |
+
conveying of source, or (2) access to copy the
|
| 255 |
+
Corresponding Source from a network server at no charge.
|
| 256 |
+
|
| 257 |
+
c) Convey individual copies of the object code with a copy of the
|
| 258 |
+
written offer to provide the Corresponding Source. This
|
| 259 |
+
alternative is allowed only occasionally and noncommercially, and
|
| 260 |
+
only if you received the object code with such an offer, in accord
|
| 261 |
+
with subsection 6b.
|
| 262 |
+
|
| 263 |
+
d) Convey the object code by offering access from a designated
|
| 264 |
+
place (gratis or for a charge), and offer equivalent access to the
|
| 265 |
+
Corresponding Source in the same way through the same place at no
|
| 266 |
+
further charge. You need not require recipients to copy the
|
| 267 |
+
Corresponding Source along with the object code. If the place to
|
| 268 |
+
copy the object code is a network server, the Corresponding Source
|
| 269 |
+
may be on a different server (operated by you or a third party)
|
| 270 |
+
that supports equivalent copying facilities, provided you maintain
|
| 271 |
+
clear directions next to the object code saying where to find the
|
| 272 |
+
Corresponding Source. Regardless of what server hosts the
|
| 273 |
+
Corresponding Source, you remain obligated to ensure that it is
|
| 274 |
+
available for as long as needed to satisfy these requirements.
|
| 275 |
+
|
| 276 |
+
e) Convey the object code using peer-to-peer transmission, provided
|
| 277 |
+
you inform other peers where the object code and Corresponding
|
| 278 |
+
Source of the work are being offered to the general public at no
|
| 279 |
+
charge under subsection 6d.
|
| 280 |
+
|
| 281 |
+
A separable portion of the object code, whose source code is excluded
|
| 282 |
+
from the Corresponding Source as a System Library, need not be
|
| 283 |
+
included in conveying the object code work.
|
| 284 |
+
|
| 285 |
+
A "User Product" is either (1) a "consumer product", which means any
|
| 286 |
+
tangible personal property which is normally used for personal, family,
|
| 287 |
+
or household purposes, or (2) anything designed or sold for incorporation
|
| 288 |
+
into a dwelling. In determining whether a product is a consumer product,
|
| 289 |
+
doubtful cases shall be resolved in favor of coverage. For a particular
|
| 290 |
+
product received by a particular user, "normally used" refers to a
|
| 291 |
+
typical or common use of that class of product, regardless of the status
|
| 292 |
+
of the particular user or of the way in which the particular user
|
| 293 |
+
actually uses, or expects or is expected to use, the product. A product
|
| 294 |
+
is a consumer product regardless of whether the product has substantial
|
| 295 |
+
commercial, industrial or non-consumer uses, unless such uses represent
|
| 296 |
+
the only significant mode of use of the product.
|
| 297 |
+
|
| 298 |
+
"Installation Information" for a User Product means any methods,
|
| 299 |
+
procedures, authorization keys, or other information required to install
|
| 300 |
+
and execute modified versions of a covered work in that User Product from
|
| 301 |
+
a modified version of its Corresponding Source. The information must
|
| 302 |
+
suffice to ensure that the continued functioning of the modified object
|
| 303 |
+
code is in no case prevented or interfered with solely because
|
| 304 |
+
modification has been made.
|
| 305 |
+
|
| 306 |
+
If you convey an object code work under this section in, or with, or
|
| 307 |
+
specifically for use in, a User Product, and the conveying occurs as
|
| 308 |
+
part of a transaction in which the right of possession and use of the
|
| 309 |
+
User Product is transferred to the recipient in perpetuity or for a
|
| 310 |
+
fixed term (regardless of how the transaction is characterized), the
|
| 311 |
+
Corresponding Source conveyed under this section must be accompanied
|
| 312 |
+
by the Installation Information. But this requirement does not apply
|
| 313 |
+
if neither you nor any third party retains the ability to install
|
| 314 |
+
modified object code on the User Product (for example, the work has
|
| 315 |
+
been installed in ROM).
|
| 316 |
+
|
| 317 |
+
The requirement to provide Installation Information does not include a
|
| 318 |
+
requirement to continue to provide support service, warranty, or updates
|
| 319 |
+
for a work that has been modified or installed by the recipient, or for
|
| 320 |
+
the User Product in which it has been modified or installed. Access to a
|
| 321 |
+
network may be denied when the modification itself materially and
|
| 322 |
+
adversely affects the operation of the network or violates the rules and
|
| 323 |
+
protocols for communication across the network.
|
| 324 |
+
|
| 325 |
+
Corresponding Source conveyed, and Installation Information provided,
|
| 326 |
+
in accord with this section must be in a format that is publicly
|
| 327 |
+
documented (and with an implementation available to the public in
|
| 328 |
+
source code form), and must require no special password or key for
|
| 329 |
+
unpacking, reading or copying.
|
| 330 |
+
|
| 331 |
+
7. Additional Terms.
|
| 332 |
+
|
| 333 |
+
"Additional permissions" are terms that supplement the terms of this
|
| 334 |
+
License by making exceptions from one or more of its conditions.
|
| 335 |
+
Additional permissions that are applicable to the entire Program shall
|
| 336 |
+
be treated as though they were included in this License, to the extent
|
| 337 |
+
that they are valid under applicable law. If additional permissions
|
| 338 |
+
apply only to part of the Program, that part may be used separately
|
| 339 |
+
under those permissions, but the entire Program remains governed by
|
| 340 |
+
this License without regard to the additional permissions.
|
| 341 |
+
|
| 342 |
+
When you convey a copy of a covered work, you may at your option
|
| 343 |
+
remove any additional permissions from that copy, or from any part of
|
| 344 |
+
it. (Additional permissions may be written to require their own
|
| 345 |
+
removal in certain cases when you modify the work.) You may place
|
| 346 |
+
additional permissions on material, added by you to a covered work,
|
| 347 |
+
for which you have or can give appropriate copyright permission.
|
| 348 |
+
|
| 349 |
+
Notwithstanding any other provision of this License, for material you
|
| 350 |
+
add to a covered work, you may (if authorized by the copyright holders of
|
| 351 |
+
that material) supplement the terms of this License with terms:
|
| 352 |
+
|
| 353 |
+
a) Disclaiming warranty or limiting liability differently from the
|
| 354 |
+
terms of sections 15 and 16 of this License; or
|
| 355 |
+
|
| 356 |
+
b) Requiring preservation of specified reasonable legal notices or
|
| 357 |
+
author attributions in that material or in the Appropriate Legal
|
| 358 |
+
Notices displayed by works containing it; or
|
| 359 |
+
|
| 360 |
+
c) Prohibiting misrepresentation of the origin of that material, or
|
| 361 |
+
requiring that modified versions of such material be marked in
|
| 362 |
+
reasonable ways as different from the original version; or
|
| 363 |
+
|
| 364 |
+
d) Limiting the use for publicity purposes of names of licensors or
|
| 365 |
+
authors of the material; or
|
| 366 |
+
|
| 367 |
+
e) Declining to grant rights under trademark law for use of some
|
| 368 |
+
trade names, trademarks, or service marks; or
|
| 369 |
+
|
| 370 |
+
f) Requiring indemnification of licensors and authors of that
|
| 371 |
+
material by anyone who conveys the material (or modified versions of
|
| 372 |
+
it) with contractual assumptions of liability to the recipient, for
|
| 373 |
+
any liability that these contractual assumptions directly impose on
|
| 374 |
+
those licensors and authors.
|
| 375 |
+
|
| 376 |
+
All other non-permissive additional terms are considered "further
|
| 377 |
+
restrictions" within the meaning of section 10. If the Program as you
|
| 378 |
+
received it, or any part of it, contains a notice stating that it is
|
| 379 |
+
governed by this License along with a term that is a further
|
| 380 |
+
restriction, you may remove that term. If a license document contains
|
| 381 |
+
a further restriction but permits relicensing or conveying under this
|
| 382 |
+
License, you may add to a covered work material governed by the terms
|
| 383 |
+
of that license document, provided that the further restriction does
|
| 384 |
+
not survive such relicensing or conveying.
|
| 385 |
+
|
| 386 |
+
If you add terms to a covered work in accord with this section, you
|
| 387 |
+
must place, in the relevant source files, a statement of the
|
| 388 |
+
additional terms that apply to those files, or a notice indicating
|
| 389 |
+
where to find the applicable terms.
|
| 390 |
+
|
| 391 |
+
Additional terms, permissive or non-permissive, may be stated in the
|
| 392 |
+
form of a separately written license, or stated as exceptions;
|
| 393 |
+
the above requirements apply either way.
|
| 394 |
+
|
| 395 |
+
8. Termination.
|
| 396 |
+
|
| 397 |
+
You may not propagate or modify a covered work except as expressly
|
| 398 |
+
provided under this License. Any attempt otherwise to propagate or
|
| 399 |
+
modify it is void, and will automatically terminate your rights under
|
| 400 |
+
this License (including any patent licenses granted under the third
|
| 401 |
+
paragraph of section 11).
|
| 402 |
+
|
| 403 |
+
However, if you cease all violation of this License, then your
|
| 404 |
+
license from a particular copyright holder is reinstated (a)
|
| 405 |
+
provisionally, unless and until the copyright holder explicitly and
|
| 406 |
+
finally terminates your license, and (b) permanently, if the copyright
|
| 407 |
+
holder fails to notify you of the violation by some reasonable means
|
| 408 |
+
prior to 60 days after the cessation.
|
| 409 |
+
|
| 410 |
+
Moreover, your license from a particular copyright holder is
|
| 411 |
+
reinstated permanently if the copyright holder notifies you of the
|
| 412 |
+
violation by some reasonable means, this is the first time you have
|
| 413 |
+
received notice of violation of this License (for any work) from that
|
| 414 |
+
copyright holder, and you cure the violation prior to 30 days after
|
| 415 |
+
your receipt of the notice.
|
| 416 |
+
|
| 417 |
+
Termination of your rights under this section does not terminate the
|
| 418 |
+
licenses of parties who have received copies or rights from you under
|
| 419 |
+
this License. If your rights have been terminated and not permanently
|
| 420 |
+
reinstated, you do not qualify to receive new licenses for the same
|
| 421 |
+
material under section 10.
|
| 422 |
+
|
| 423 |
+
9. Acceptance Not Required for Having Copies.
|
| 424 |
+
|
| 425 |
+
You are not required to accept this License in order to receive or
|
| 426 |
+
run a copy of the Program. Ancillary propagation of a covered work
|
| 427 |
+
occurring solely as a consequence of using peer-to-peer transmission
|
| 428 |
+
to receive a copy likewise does not require acceptance. However,
|
| 429 |
+
nothing other than this License grants you permission to propagate or
|
| 430 |
+
modify any covered work. These actions infringe copyright if you do
|
| 431 |
+
not accept this License. Therefore, by modifying or propagating a
|
| 432 |
+
covered work, you indicate your acceptance of this License to do so.
|
| 433 |
+
|
| 434 |
+
10. Automatic Licensing of Downstream Recipients.
|
| 435 |
+
|
| 436 |
+
Each time you convey a covered work, the recipient automatically
|
| 437 |
+
receives a license from the original licensors, to run, modify and
|
| 438 |
+
propagate that work, subject to this License. You are not responsible
|
| 439 |
+
for enforcing compliance by third parties with this License.
|
| 440 |
+
|
| 441 |
+
An "entity transaction" is a transaction transferring control of an
|
| 442 |
+
organization, or substantially all assets of one, or subdividing an
|
| 443 |
+
organization, or merging organizations. If propagation of a covered
|
| 444 |
+
work results from an entity transaction, each party to that
|
| 445 |
+
transaction who receives a copy of the work also receives whatever
|
| 446 |
+
licenses to the work the party's predecessor in interest had or could
|
| 447 |
+
give under the previous paragraph, plus a right to possession of the
|
| 448 |
+
Corresponding Source of the work from the predecessor in interest, if
|
| 449 |
+
the predecessor has it or can get it with reasonable efforts.
|
| 450 |
+
|
| 451 |
+
You may not impose any further restrictions on the exercise of the
|
| 452 |
+
rights granted or affirmed under this License. For example, you may
|
| 453 |
+
not impose a license fee, royalty, or other charge for exercise of
|
| 454 |
+
rights granted under this License, and you may not initiate litigation
|
| 455 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
| 456 |
+
any patent claim is infringed by making, using, selling, offering for
|
| 457 |
+
sale, or importing the Program or any portion of it.
|
| 458 |
+
|
| 459 |
+
11. Patents.
|
| 460 |
+
|
| 461 |
+
A "contributor" is a copyright holder who authorizes use under this
|
| 462 |
+
License of the Program or a work on which the Program is based. The
|
| 463 |
+
work thus licensed is called the contributor's "contributor version".
|
| 464 |
+
|
| 465 |
+
A contributor's "essential patent claims" are all patent claims
|
| 466 |
+
owned or controlled by the contributor, whether already acquired or
|
| 467 |
+
hereafter acquired, that would be infringed by some manner, permitted
|
| 468 |
+
by this License, of making, using, or selling its contributor version,
|
| 469 |
+
but do not include claims that would be infringed only as a
|
| 470 |
+
consequence of further modification of the contributor version. For
|
| 471 |
+
purposes of this definition, "control" includes the right to grant
|
| 472 |
+
patent sublicenses in a manner consistent with the requirements of
|
| 473 |
+
this License.
|
| 474 |
+
|
| 475 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
| 476 |
+
patent license under the contributor's essential patent claims, to
|
| 477 |
+
make, use, sell, offer for sale, import and otherwise run, modify and
|
| 478 |
+
propagate the contents of its contributor version.
|
| 479 |
+
|
| 480 |
+
In the following three paragraphs, a "patent license" is any express
|
| 481 |
+
agreement or commitment, however denominated, not to enforce a patent
|
| 482 |
+
(such as an express permission to practice a patent or covenant not to
|
| 483 |
+
sue for patent infringement). To "grant" such a patent license to a
|
| 484 |
+
party means to make such an agreement or commitment not to enforce a
|
| 485 |
+
patent against the party.
|
| 486 |
+
|
| 487 |
+
If you convey a covered work, knowingly relying on a patent license,
|
| 488 |
+
and the Corresponding Source of the work is not available for anyone
|
| 489 |
+
to copy, free of charge and under the terms of this License, through a
|
| 490 |
+
publicly available network server or other readily accessible means,
|
| 491 |
+
then you must either (1) cause the Corresponding Source to be so
|
| 492 |
+
available, or (2) arrange to deprive yourself of the benefit of the
|
| 493 |
+
patent license for this particular work, or (3) arrange, in a manner
|
| 494 |
+
consistent with the requirements of this License, to extend the patent
|
| 495 |
+
license to downstream recipients. "Knowingly relying" means you have
|
| 496 |
+
actual knowledge that, but for the patent license, your conveying the
|
| 497 |
+
covered work in a country, or your recipient's use of the covered work
|
| 498 |
+
in a country, would infringe one or more identifiable patents in that
|
| 499 |
+
country that you have reason to believe are valid.
|
| 500 |
+
|
| 501 |
+
If, pursuant to or in connection with a single transaction or
|
| 502 |
+
arrangement, you convey, or propagate by procuring conveyance of, a
|
| 503 |
+
covered work, and grant a patent license to some of the parties
|
| 504 |
+
receiving the covered work authorizing them to use, propagate, modify
|
| 505 |
+
or convey a specific copy of the covered work, then the patent license
|
| 506 |
+
you grant is automatically extended to all recipients of the covered
|
| 507 |
+
work and works based on it.
|
| 508 |
+
|
| 509 |
+
A patent license is "discriminatory" if it does not include within
|
| 510 |
+
the scope of its coverage, prohibits the exercise of, or is
|
| 511 |
+
conditioned on the non-exercise of one or more of the rights that are
|
| 512 |
+
specifically granted under this License. You may not convey a covered
|
| 513 |
+
work if you are a party to an arrangement with a third party that is
|
| 514 |
+
in the business of distributing software, under which you make payment
|
| 515 |
+
to the third party based on the extent of your activity of conveying
|
| 516 |
+
the work, and under which the third party grants, to any of the
|
| 517 |
+
parties who would receive the covered work from you, a discriminatory
|
| 518 |
+
patent license (a) in connection with copies of the covered work
|
| 519 |
+
conveyed by you (or copies made from those copies), or (b) primarily
|
| 520 |
+
for and in connection with specific products or compilations that
|
| 521 |
+
contain the covered work, unless you entered into that arrangement,
|
| 522 |
+
or that patent license was granted, prior to 28 March 2007.
|
| 523 |
+
|
| 524 |
+
Nothing in this License shall be construed as excluding or limiting
|
| 525 |
+
any implied license or other defenses to infringement that may
|
| 526 |
+
otherwise be available to you under applicable patent law.
|
| 527 |
+
|
| 528 |
+
12. No Surrender of Others' Freedom.
|
| 529 |
+
|
| 530 |
+
If conditions are imposed on you (whether by court order, agreement or
|
| 531 |
+
otherwise) that contradict the conditions of this License, they do not
|
| 532 |
+
excuse you from the conditions of this License. If you cannot convey a
|
| 533 |
+
covered work so as to satisfy simultaneously your obligations under this
|
| 534 |
+
License and any other pertinent obligations, then as a consequence you may
|
| 535 |
+
not convey it at all. For example, if you agree to terms that obligate you
|
| 536 |
+
to collect a royalty for further conveying from those to whom you convey
|
| 537 |
+
the Program, the only way you could satisfy both those terms and this
|
| 538 |
+
License would be to refrain entirely from conveying the Program.
|
| 539 |
+
|
| 540 |
+
13. Remote Network Interaction; Use with the GNU General Public License.
|
| 541 |
+
|
| 542 |
+
Notwithstanding any other provision of this License, if you modify the
|
| 543 |
+
Program, your modified version must prominently offer all users
|
| 544 |
+
interacting with it remotely through a computer network (if your version
|
| 545 |
+
supports such interaction) an opportunity to receive the Corresponding
|
| 546 |
+
Source of your version by providing access to the Corresponding Source
|
| 547 |
+
from a network server at no charge, through some standard or customary
|
| 548 |
+
means of facilitating copying of software. This Corresponding Source
|
| 549 |
+
shall include the Corresponding Source for any work covered by version 3
|
| 550 |
+
of the GNU General Public License that is incorporated pursuant to the
|
| 551 |
+
following paragraph.
|
| 552 |
+
|
| 553 |
+
Notwithstanding any other provision of this License, you have
|
| 554 |
+
permission to link or combine any covered work with a work licensed
|
| 555 |
+
under version 3 of the GNU General Public License into a single
|
| 556 |
+
combined work, and to convey the resulting work. The terms of this
|
| 557 |
+
License will continue to apply to the part which is the covered work,
|
| 558 |
+
but the work with which it is combined will remain governed by version
|
| 559 |
+
3 of the GNU General Public License.
|
| 560 |
+
|
| 561 |
+
14. Revised Versions of this License.
|
| 562 |
+
|
| 563 |
+
The Free Software Foundation may publish revised and/or new versions of
|
| 564 |
+
the GNU Affero General Public License from time to time. Such new versions
|
| 565 |
+
will be similar in spirit to the present version, but may differ in detail to
|
| 566 |
+
address new problems or concerns.
|
| 567 |
+
|
| 568 |
+
Each version is given a distinguishing version number. If the
|
| 569 |
+
Program specifies that a certain numbered version of the GNU Affero General
|
| 570 |
+
Public License "or any later version" applies to it, you have the
|
| 571 |
+
option of following the terms and conditions either of that numbered
|
| 572 |
+
version or of any later version published by the Free Software
|
| 573 |
+
Foundation. If the Program does not specify a version number of the
|
| 574 |
+
GNU Affero General Public License, you may choose any version ever published
|
| 575 |
+
by the Free Software Foundation.
|
| 576 |
+
|
| 577 |
+
If the Program specifies that a proxy can decide which future
|
| 578 |
+
versions of the GNU Affero General Public License can be used, that proxy's
|
| 579 |
+
public statement of acceptance of a version permanently authorizes you
|
| 580 |
+
to choose that version for the Program.
|
| 581 |
+
|
| 582 |
+
Later license versions may give you additional or different
|
| 583 |
+
permissions. However, no additional obligations are imposed on any
|
| 584 |
+
author or copyright holder as a result of your choosing to follow a
|
| 585 |
+
later version.
|
| 586 |
+
|
| 587 |
+
15. Disclaimer of Warranty.
|
| 588 |
+
|
| 589 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
| 590 |
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
| 591 |
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
| 592 |
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
| 593 |
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
| 594 |
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
| 595 |
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
| 596 |
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
| 597 |
+
|
| 598 |
+
16. Limitation of Liability.
|
| 599 |
+
|
| 600 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
| 601 |
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
| 602 |
+
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
| 603 |
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
| 604 |
+
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
| 605 |
+
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
| 606 |
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
| 607 |
+
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
| 608 |
+
SUCH DAMAGES.
|
| 609 |
+
|
| 610 |
+
17. Interpretation of Sections 15 and 16.
|
| 611 |
+
|
| 612 |
+
If the disclaimer of warranty and limitation of liability provided
|
| 613 |
+
above cannot be given local legal effect according to their terms,
|
| 614 |
+
reviewing courts shall apply local law that most closely approximates
|
| 615 |
+
an absolute waiver of all civil liability in connection with the
|
| 616 |
+
Program, unless a warranty or assumption of liability accompanies a
|
| 617 |
+
copy of the Program in return for a fee.
|
| 618 |
+
|
| 619 |
+
END OF TERMS AND CONDITIONS
|
| 620 |
+
|
| 621 |
+
How to Apply These Terms to Your New Programs
|
| 622 |
+
|
| 623 |
+
If you develop a new program, and you want it to be of the greatest
|
| 624 |
+
possible use to the public, the best way to achieve this is to make it
|
| 625 |
+
free software which everyone can redistribute and change under these terms.
|
| 626 |
+
|
| 627 |
+
To do so, attach the following notices to the program. It is safest
|
| 628 |
+
to attach them to the start of each source file to most effectively
|
| 629 |
+
state the exclusion of warranty; and each file should have at least
|
| 630 |
+
the "copyright" line and a pointer to where the full notice is found.
|
| 631 |
+
|
| 632 |
+
<one line to give the program's name and a brief idea of what it does.>
|
| 633 |
+
Copyright (C) <year> <name of author>
|
| 634 |
+
|
| 635 |
+
This program is free software: you can redistribute it and/or modify
|
| 636 |
+
it under the terms of the GNU Affero General Public License as published by
|
| 637 |
+
the Free Software Foundation, either version 3 of the License, or
|
| 638 |
+
(at your option) any later version.
|
| 639 |
+
|
| 640 |
+
This program is distributed in the hope that it will be useful,
|
| 641 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 642 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 643 |
+
GNU Affero General Public License for more details.
|
| 644 |
+
|
| 645 |
+
You should have received a copy of the GNU Affero General Public License
|
| 646 |
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 647 |
+
|
| 648 |
+
Also add information on how to contact you by electronic and paper mail.
|
| 649 |
+
|
| 650 |
+
If your software can interact with users remotely through a computer
|
| 651 |
+
network, you should also make sure that it provides a way for users to
|
| 652 |
+
get its source. For example, if your program is a web application, its
|
| 653 |
+
interface could display a "Source" link that leads users to an archive
|
| 654 |
+
of the code. There are many ways you could offer source, and different
|
| 655 |
+
solutions will be better for different programs; see section 13 for the
|
| 656 |
+
specific requirements.
|
| 657 |
+
|
| 658 |
+
You should also get your employer (if you work as a programmer) or school,
|
| 659 |
+
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
| 660 |
+
For more information on this, and how to apply and follow the GNU AGPL, see
|
| 661 |
+
<https://www.gnu.org/licenses/>.
|
LICENSES/CPSC-ESCL-1.0.txt
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CPSC Eclipse Strict Copyleft License
|
| 2 |
+
Version 1.0, 7 October 2026
|
| 3 |
+
|
| 4 |
+
Copyright (c) 2026 Ahmad Ali Parr
|
| 5 |
+
|
| 6 |
+
This license is a custom instrument for the Conservation-Preserving
|
| 7 |
+
Scattering Compiler. It uses the Eclipse project notice style (copyright
|
| 8 |
+
header, SPDX identifier, accompanying-materials clause). It is not the
|
| 9 |
+
Eclipse Public License 1.0 or 2.0, it is not issued by the Eclipse
|
| 10 |
+
Foundation, and it grants no license to any Eclipse trademark.
|
| 11 |
+
|
| 12 |
+
The copyleft in this instrument is strict. It is taken from the dual
|
| 13 |
+
offer of the GNU Affero General Public License, Version 3 only
|
| 14 |
+
("AGPL-3.0-only"), including that license's network-use source obligation.
|
| 15 |
+
Where this text and the AGPL both apply by the recipient's choice, the
|
| 16 |
+
chosen text governs. Where a downstream distributor offers the work
|
| 17 |
+
onward, both options must remain available.
|
| 18 |
+
|
| 19 |
+
0. Definitions.
|
| 20 |
+
|
| 21 |
+
"This License" means version 1.0 of the CPSC Eclipse Strict Copyleft
|
| 22 |
+
License.
|
| 23 |
+
|
| 24 |
+
"The Program" means any copyrightable work licensed under this License.
|
| 25 |
+
|
| 26 |
+
"You" means the licensee.
|
| 27 |
+
|
| 28 |
+
"Modify" means copy from or adapt all or part of the Program in a fashion
|
| 29 |
+
requiring copyright permission, other than making an exact copy.
|
| 30 |
+
|
| 31 |
+
"Covered work" means the unmodified Program or a work based on it,
|
| 32 |
+
including a translation, a hardware mapping, a gate-list rewrite, a
|
| 33 |
+
certificate binding, and a library linked so as to form a larger program.
|
| 34 |
+
|
| 35 |
+
"Corresponding Source" means the preferred form for modifying the work,
|
| 36 |
+
including scripts required to build and run it, and excluding only system
|
| 37 |
+
libraries and unmodified general-purpose tools.
|
| 38 |
+
|
| 39 |
+
"Convey" means any propagation that enables other parties to make or
|
| 40 |
+
receive copies. Remote interaction by users with a modified covered work
|
| 41 |
+
is treated as conveying for the source obligation in section 5.
|
| 42 |
+
|
| 43 |
+
1. Grant.
|
| 44 |
+
|
| 45 |
+
You receive a worldwide, royalty-free, non-exclusive copyright license to
|
| 46 |
+
run, study, modify, and convey the Program, and a patent license under
|
| 47 |
+
the contributors' essential patent claims, only if you comply with this
|
| 48 |
+
License. These grants are irrevocable for the term of copyright if you
|
| 49 |
+
remain in compliance.
|
| 50 |
+
|
| 51 |
+
2. Dual offer.
|
| 52 |
+
|
| 53 |
+
The Program is licensed under this License or, at your option, under the
|
| 54 |
+
GNU Affero General Public License, Version 3 only. The AGPL text is
|
| 55 |
+
included as LICENSES/AGPL-3.0.txt and may not be modified. Choosing the
|
| 56 |
+
AGPL does not extinguish the obligation, when you convey a modified
|
| 57 |
+
version, to keep the dual offer available to your recipients.
|
| 58 |
+
|
| 59 |
+
3. Strict copyleft.
|
| 60 |
+
|
| 61 |
+
You may convey a modified covered work only under this same dual offer,
|
| 62 |
+
as a whole, to anyone who receives a copy. You may not:
|
| 63 |
+
|
| 64 |
+
(a) relicense the work, or any part of it, under a permissive license
|
| 65 |
+
or a weak-copyleft license, including MIT, BSD, Apache-2.0,
|
| 66 |
+
EPL-1.0, EPL-2.0, LGPL, or MPL;
|
| 67 |
+
|
| 68 |
+
(b) add a classpath exception, linking exception, or other term that
|
| 69 |
+
lets a proprietary work incorporate the Program without itself
|
| 70 |
+
becoming a covered work;
|
| 71 |
+
|
| 72 |
+
(c) impose a further restriction that would be forbidden by AGPL-3.0
|
| 73 |
+
section 10;
|
| 74 |
+
|
| 75 |
+
(d) remove or alter the copyright header, the SPDX identifier
|
| 76 |
+
"LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only", or the statement that
|
| 77 |
+
both options are strict copyleft.
|
| 78 |
+
|
| 79 |
+
A compilation on a storage medium with independent works is an aggregate
|
| 80 |
+
only if the compilation does not limit users' rights beyond what this
|
| 81 |
+
License allows. Inclusion in an aggregate does not extend this License
|
| 82 |
+
to independent works.
|
| 83 |
+
|
| 84 |
+
4. Source with binaries.
|
| 85 |
+
|
| 86 |
+
If you convey a covered work in object form, you must also convey the
|
| 87 |
+
Corresponding Source under this dual offer, on a durable medium, by a
|
| 88 |
+
written offer valid for at least three years, or by equivalent network
|
| 89 |
+
access at no further charge, in the manner of AGPL-3.0 section 6.
|
| 90 |
+
|
| 91 |
+
5. Network use.
|
| 92 |
+
|
| 93 |
+
If you modify a covered work and users interact with it remotely through
|
| 94 |
+
a computer network, you must offer those users Corresponding Source of
|
| 95 |
+
the modified work under this dual offer, at no charge, by a prominent
|
| 96 |
+
network offer. This is the AGPL-3.0 section 13 obligation, and it applies
|
| 97 |
+
whether the recipient elects this License or the AGPL.
|
| 98 |
+
|
| 99 |
+
6. Headers.
|
| 100 |
+
|
| 101 |
+
Every source file of a covered work must carry a notice substantially
|
| 102 |
+
in the Eclipse header form:
|
| 103 |
+
|
| 104 |
+
Copyright (c) <year> <holder> and others.
|
| 105 |
+
Made available under the CPSC Eclipse Strict Copyleft License,
|
| 106 |
+
Version 1.0, or, at your option, the GNU AGPL Version 3 only.
|
| 107 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 108 |
+
|
| 109 |
+
Files that cannot carry an inline comment must be listed, with the same
|
| 110 |
+
SPDX expression, in REUSE.toml.
|
| 111 |
+
|
| 112 |
+
7. Notices and marks.
|
| 113 |
+
|
| 114 |
+
Keep intact all notices of this License, of the AGPL, and of the absence
|
| 115 |
+
of warranty. Do not use the names Eclipse, Eclipse Foundation, or CPSC
|
| 116 |
+
to endorse a derivative without written permission. No trademark is
|
| 117 |
+
granted.
|
| 118 |
+
|
| 119 |
+
8. Termination.
|
| 120 |
+
|
| 121 |
+
If you violate this License, your rights end. They reinstate if you cure
|
| 122 |
+
the violation within 30 days of notice, or permanently if the holder
|
| 123 |
+
fails to notify you within 60 days of ceasing the violation, following
|
| 124 |
+
the pattern of AGPL-3.0 section 8. Downstream recipients who received
|
| 125 |
+
copies while you were in compliance keep their licenses.
|
| 126 |
+
|
| 127 |
+
9. No surrender of others' freedom.
|
| 128 |
+
|
| 129 |
+
If conditions are imposed on you that contradict this License, you may
|
| 130 |
+
not convey the work at all.
|
| 131 |
+
|
| 132 |
+
10. Disclaimer of warranty.
|
| 133 |
+
|
| 134 |
+
THE PROGRAM IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
| 135 |
+
OR IMPLIED, INCLUDING FITNESS FOR A PARTICULAR PURPOSE AND
|
| 136 |
+
NON-INFRINGEMENT. THE ENTIRE RISK AS TO QUALITY AND PERFORMANCE IS WITH
|
| 137 |
+
YOU.
|
| 138 |
+
|
| 139 |
+
11. Limitation of liability.
|
| 140 |
+
|
| 141 |
+
IN NO EVENT WILL A COPYRIGHT HOLDER OR CONTRIBUTOR BE LIABLE FOR DAMAGES
|
| 142 |
+
ARISING FROM THE PROGRAM, UNDER ANY THEORY, UNLESS REQUIRED BY LAW OR
|
| 143 |
+
AGREED IN WRITING.
|
| 144 |
+
|
| 145 |
+
12. Versioning.
|
| 146 |
+
|
| 147 |
+
Only Ahmad Ali Parr may publish a new version of this License. A later
|
| 148 |
+
version applies to the Program only if a notice says so. The dual AGPL
|
| 149 |
+
option remains AGPL-3.0-only unless a later notice says otherwise.
|
NOTICE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CPSC — Conservation-Preserving Scattering Compiler
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
|
| 4 |
+
This program and the accompanying materials are made available under the
|
| 5 |
+
terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
LICENSES/AGPL-3.0.txt.
|
| 9 |
+
|
| 10 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 11 |
+
|
| 12 |
+
This is not an Eclipse Foundation project. The notice form follows the
|
| 13 |
+
Eclipse accompanying-materials convention. The copyleft is the strict
|
| 14 |
+
network copyleft of the dual AGPL-3.0-only offer. No classpath exception
|
| 15 |
+
is granted.
|
| 16 |
+
|
| 17 |
+
Third-party: LICENSES/AGPL-3.0.txt is the GNU Affero General Public
|
| 18 |
+
License, Version 3, Copyright (C) 2007 Free Software Foundation, Inc.
|
| 19 |
+
Verbatim copies of that license text are permitted; changing it is not.
|
README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: cpsc-escl-1.0-or-agpl-3.0-only
|
| 4 |
+
license_link: https://huggingface.co/Snapkitty/cpsc-qft/blob/main/LICENSE
|
| 5 |
+
tags:
|
| 6 |
+
- snapkitty
|
| 7 |
+
- october-2026-drop
|
| 8 |
+
- ruby
|
| 9 |
+
- python
|
| 10 |
+
- lean4
|
| 11 |
+
- qsharp
|
| 12 |
+
- quantum-field-theory
|
| 13 |
+
- lattice
|
| 14 |
+
- compiler
|
| 15 |
+
- quantum-computing
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
> Mirrored from [https://github.com/SNAPKITTYWEST/cpsc-qft](https://github.com/SNAPKITTYWEST/cpsc-qft) at commit `8ebd1d5`. Part of the **SnapKitty October 2026 main drop**.
|
| 19 |
+
|
REUSE.toml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version = 1
|
| 2 |
+
|
| 3 |
+
[[annotations]]
|
| 4 |
+
path = ["LICENSE", "NOTICE", "LICENSES/CPSC-ESCL-1.0.txt", "lean/lean-toolchain", ".gitignore"]
|
| 5 |
+
precedence = "aggregate"
|
| 6 |
+
SPDX-FileCopyrightText = "2026 Ahmad Ali Parr"
|
| 7 |
+
SPDX-License-Identifier = "LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only"
|
| 8 |
+
|
| 9 |
+
[[annotations]]
|
| 10 |
+
path = ["LICENSES/AGPL-3.0.txt"]
|
| 11 |
+
precedence = "aggregate"
|
| 12 |
+
SPDX-FileCopyrightText = "2007 Free Software Foundation, Inc."
|
| 13 |
+
SPDX-License-Identifier = "FSFAP"
|
docs/.nojekyll
ADDED
|
File without changes
|
docs/CHANNEL_PRUNING.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
This file is dual-licensed under the CPSC Eclipse Strict Copyleft License v1.0
|
| 5 |
+
or the GNU AGPL v3 only. Both are strict copyleft.
|
| 6 |
+
-->
|
| 7 |
+
|
| 8 |
+
# Channel pruning for a 1+1 → 1+2 process
|
| 9 |
+
|
| 10 |
+
The draft asked for an inelastic $1+1 \to 1+2$ collision whose output lies strictly in the input energy manifold.
|
| 11 |
+
|
| 12 |
+
On this lattice that sentence has to be weakened.
|
| 13 |
+
|
| 14 |
+
## What the process is
|
| 15 |
+
|
| 16 |
+
There is no particle-number superselection rule. The transverse field creates and destroys domain walls in pairs, which is the lattice shadow of $\phi \to \phi\phi\phi$ near the Ising fixed point, not a Fock-space $2 \to 3$ amplitude with continuum kinematics.
|
| 17 |
+
|
| 18 |
+
Operationally the channel check is:
|
| 19 |
+
|
| 20 |
+
- Input: a two-domain-wall state, or a computational bitstring with a declared parity and (if $\lambda = 0$) magnetization.
|
| 21 |
+
- Allowed output: any state with the same $P_Z$, the same lattice momentum if the input was a momentum eigenstate, and — only as a diagnostic — an energy expectation within a stated Trotter tolerance of $\langle H\rangle_{\mathrm{in}}$.
|
| 22 |
+
- Forbidden output: the opposite parity sector. A single spin flip is the generator of that forbidden move.
|
| 23 |
+
|
| 24 |
+
Energy expectation is not a sector label for a superposition. The sector label is the eigenvalue of an operator that commutes with $H$. $H$ commutes with itself, but its eigenvalues are not known in closed form for $\lambda \neq 0$. Filtering on $\langle H\rangle$ after a Trotter step measures Trotter error, not a conservation-law violation.
|
| 25 |
+
|
| 26 |
+
## Pruning rule implemented
|
| 27 |
+
|
| 28 |
+
`cpsc.channel.allowed_basis` keeps computational basis states whose parity equals the input parity. If `conserve_magnetization` is set, it also keeps only states with the same Hamming weight. The evolution helper applies the exact diagonalization of $H$ restricted to that basis. Amplitude that would have left the basis is removed before exponentiation, which is the compilation constraint.
|
| 29 |
+
|
| 30 |
+
For $\lambda \neq 0$ and magnetization pruning, this restriction is *not* completeness-preserving: the true $e^{-iHt}$ leaves a fixed-weight subspace. The demo therefore enables magnetization pruning only at $\lambda = 0$. Parity pruning is completeness-preserving for every $\lambda$.
|
| 31 |
+
|
| 32 |
+
## Adversarial gate
|
| 33 |
+
|
| 34 |
+
A one-qubit $X$ changes parity and is rejected by `SymmetryAnalyzer`. A $ZZ$ rotation and a translated pair of $X$ gates (still an even number of $X$s, hence parity-even, and shiftable) are accepted. Acceptance is a gate-set check, not a proof that the Trotter product equals $e^{-iHt}$.
|
docs/OPEN_PROBLEMS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
This file is dual-licensed under the CPSC Eclipse Strict Copyleft License v1.0
|
| 5 |
+
or the GNU AGPL v3 only. Both are strict copyleft.
|
| 6 |
+
-->
|
| 7 |
+
|
| 8 |
+
# Open problems
|
| 9 |
+
|
| 10 |
+
Marked `POSSIBLY_NOVEL` in the draft. The honest status is: executable prototype with a Boolean certificate. Operator commutation is not formally verified, and prior art is not cleared.
|
| 11 |
+
|
| 12 |
+
## Discharged in the prototype
|
| 13 |
+
|
| 14 |
+
1. Boolean admission. `lean/CPSC/Certificate.lean` reduces the 4-site ring and both adversarial extensions by `rfl`.
|
| 15 |
+
2. Digest binding. `cpsc.certificate` hashes the schedule. Editing a gate makes `Certificate.matches` false.
|
| 16 |
+
3. Mode switch. λ = 0 rejects a single X. λ ≠ 0 rejects a lone Z. Translation orbits must be closed.
|
| 17 |
+
|
| 18 |
+
## Obligations not discharged
|
| 19 |
+
|
| 20 |
+
1. Operator commutation. The Boolean predicate is not yet proved equal to `[U, Π] = 0` on `(ℂ²)⊗N`.
|
| 21 |
+
2. Leakage under hardware noise. Ideal leakage on N = 4 is zero; device leakage is unmodeled.
|
| 22 |
+
3. Momentum as an eigenspace, rather than orbit closure of the gate support.
|
| 23 |
+
4. A non-trivial φ⁴ encoding. `X^4 = I` on qubits. The next model is a truncated oscillator per site.
|
| 24 |
+
5. A Lean binary in CI. The emitted theorem is generated; `lake build` has not been run here.
|
| 25 |
+
|
| 26 |
+
## Collisions
|
| 27 |
+
|
| 28 |
+
- Post-selection on symmetry bits (symmetry verification, symmetry-protected mitigation) is the method this repo refuses to call a compiler.
|
| 29 |
+
- Subspace-preserving compilation and Pauli twirling / sector projection in Qiskit and PennyLane already build the block-diagonal evolution for a declared symmetry.
|
| 30 |
+
- Verified circuit compilers (VOQC, SQIR, CertiQ-style stacks) already attach machine-checked semantics to gate lists. They do not know about $\phi^4$.
|
| 31 |
+
- SPT phases are a property of a ground state, not of a synthesizer. The analogy in the draft is motivational only.
|
| 32 |
+
|
| 33 |
+
## What would make the novelty claim true
|
| 34 |
+
|
| 35 |
+
A released artifact where (i) the channel projector is derived from a stated lattice QFT, (ii) every hardware rewrite is rejected or proved inside the commutant, and (iii) a Lean checker fails closed on an edited circuit. The prototype now rejects illegal gates and binds a digest. It still does not fail a Lean checker on an edited operator identity. That gap is obligation 1.
|
docs/SPECIFICATION.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
This file is dual-licensed under the CPSC Eclipse Strict Copyleft License v1.0
|
| 5 |
+
or the GNU AGPL v3 only. Both are strict copyleft.
|
| 6 |
+
-->
|
| 7 |
+
|
| 8 |
+
# Specification
|
| 9 |
+
|
| 10 |
+
## 1. Objects
|
| 11 |
+
|
| 12 |
+
A QFT specification is a tuple $\mathcal{S} = (\mathcal{H}, H, \mathcal{G})$.
|
| 13 |
+
|
| 14 |
+
- $\mathcal{H} = (\mathbb{C}^2)^{\otimes N}$, computational basis ordered little-endian.
|
| 15 |
+
- $H$ is the periodic transverse-field Ising Hamiltonian in Section 2.
|
| 16 |
+
- $\mathcal{G}$ is generated by the cyclic shift $T$ and by global $Z$ parity. Continuous $U(1)$ charge is not a symmetry of this $H$, because the transverse field flips $Q = \sum_i Z_i$ by $\pm 2$. The conserved charge used by the compiler is the $\mathbb{Z}_2$ parity $P_Z = \prod_i Z_i$, which does commute with every $X_i$ and every $ZZ$ term. Total magnetization is conserved only at $\lambda = 0$.
|
| 17 |
+
|
| 18 |
+
This corrects the draft spec, which listed $\Pi_Q = \sum_i Z_i$ as conserved for the interacting theory. It is not, once $\lambda \neq 0$.
|
| 19 |
+
|
| 20 |
+
A conservation invariant is a Hermitian operator $\Pi$ with $[H, \Pi] = 0$. A scattering channel is a pair of joint eigenspaces of the commuting family $\{\Pi_\chi\}$ with the same eigenvalues. A compiled circuit is a gate list intended to approximate $e^{-iHt}$ while remaining in the commutant of that family. A proof certificate is a Lean witness of that commutation, sector by sector. The witness is not yet constructed.
|
| 21 |
+
|
| 22 |
+
## 2. Hamiltonian
|
| 23 |
+
|
| 24 |
+
$$
|
| 25 |
+
H = -J \sum_{i=0}^{N-1} Z_i Z_{i+1} + \lambda \sum_{i=0}^{N-1} X_i
|
| 26 |
+
+ \frac{g}{24} \sum_{i=0}^{N-1} X_i^4
|
| 27 |
+
$$
|
| 28 |
+
|
| 29 |
+
with $Z_N = Z_0$. Since $X^4 = I$, the last sum is $(g/24) N \, I$ and does not affect scattering. The implemented operator omits it and records the identity.
|
| 30 |
+
|
| 31 |
+
At $\lambda = 0$, $H$ is diagonal and conserves magnetization $Q = \sum_i Z_i$. For $\lambda \neq 0$ the compiler must not claim magnetization conservation. It claims parity conservation, which holds for all $\lambda, J$.
|
| 32 |
+
|
| 33 |
+
## 3. Symmetry-constrained manifold
|
| 34 |
+
|
| 35 |
+
$$
|
| 36 |
+
\mathcal{M}_{\mathrm{sym}}
|
| 37 |
+
= \{ U \in U(2^N) \mid [U, P_Z] = 0,\ [U, T] = 0 \}
|
| 38 |
+
$$
|
| 39 |
+
|
| 40 |
+
Translation invariance of $H$ puts $e^{-iHt}$ in the commutant of $T$. Parity does the same for $P_Z$. Completeness (obligation 3) is the statement that exact evolution already lies in $\mathcal{M}_{\mathrm{sym}}$. The compiler's job is to refuse gates that leave it, including gates introduced by a later hardware rewrite.
|
| 41 |
+
|
| 42 |
+
The group average
|
| 43 |
+
|
| 44 |
+
$$
|
| 45 |
+
\mathcal{P}(O) = \frac{1}{N} \sum_{k=0}^{N-1} T^{-k} O T^{k}
|
| 46 |
+
$$
|
| 47 |
+
|
| 48 |
+
projects onto translation-invariant operators. It is a synthesis constraint, not a post-selection on shots. Applying it to a Trotter factor that already came from a translation-invariant $H$ changes nothing. Applying it to an asymmetric gate (a defect, a boundary drive, a mis-routed CNOT pattern that is not shift-equivalent) either restores invariance or, in the analyzer, rejects the gate if the projection is not unitary on the code subspace.
|
| 49 |
+
|
| 50 |
+
Charge projection for $\mathbb{Z}_2$ is the average $(O + P_Z O P_Z)/2$.
|
| 51 |
+
|
| 52 |
+
## 4. Channel-space form
|
| 53 |
+
|
| 54 |
+
Let $\lambda$ run over joint eigenvalues of $(P_Z, P)$, where $P$ is lattice momentum, $T\lvert p\rangle = e^{-ip}\lvert p\rangle$, $p = 2\pi n / N$. Then
|
| 55 |
+
|
| 56 |
+
$$
|
| 57 |
+
U_{\mathrm{QFT}} = \bigoplus_\lambda U_\lambda
|
| 58 |
+
$$
|
| 59 |
+
|
| 60 |
+
on the exact evolution. The compiled operator is the direct sum of the blocks the synthesizer actually built. Blocks with $\lambda \neq \lambda_{\mathrm{in}}$ are absent from the gate list, not zeroed after measurement.
|
| 61 |
+
|
| 62 |
+
## 5. Algorithm (PCSS)
|
| 63 |
+
|
| 64 |
+
1. Symmetry analysis. From $\mathcal{S}$, take the commutant generators that are cheap to test: $P_Z$ always; $Q = \sum Z_i$ only if $\lambda = 0$; $T$ always.
|
| 65 |
+
2. Channel generation. Read $\lambda_{\mathrm{in}}$ from the input. The allowed subspace is the matching joint eigenspace. For a computational-basis input this is a single parity (and, if $\lambda = 0$, a single magnetization).
|
| 66 |
+
3. Constrained synthesis. Trotterize only terms that commute with the active generators. Map to hardware only through rewrites that are themselves certified, or reject the rewrite.
|
| 67 |
+
4. Certificate. Emit the Lean statement for the concrete gate list. Until the proof is filled, the certificate is an interface plus the Python rejection log.
|
| 68 |
+
5. Execution. Hardware noise can still leak. The certificate does not claim otherwise. Shot filters remain a diagnostic for the device, not a substitute for the compiler check.
|
| 69 |
+
|
| 70 |
+
## 6. Proof obligations
|
| 71 |
+
|
| 72 |
+
See `lean/CPSC/Certificate.lean` and `docs/OPEN_PROBLEMS.md`.
|
| 73 |
+
|
| 74 |
+
- Commutation: each emitted gate $G$ satisfies $[G, \Pi_\chi] = 0$.
|
| 75 |
+
- Leakage: the ideal compiled unitary has zero amplitude between distinct $\lambda$ sectors.
|
| 76 |
+
- Completeness: $e^{-iHt} \in \mathcal{M}_{\mathrm{sym}}$ whenever $[H, \Pi_\chi] = 0$.
|
docs/index.html
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<!--
|
| 3 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 4 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 5 |
+
-->
|
| 6 |
+
<html lang="en">
|
| 7 |
+
<head>
|
| 8 |
+
<meta charset="utf-8">
|
| 9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 10 |
+
<title>CPSC-QFT — Conservation-Preserving Scattering Compiler</title>
|
| 11 |
+
<meta name="description" content="Compilation-time symmetry constraints for a 1+1D lattice shadow of phi^4. Mercury console, Lean certificate, Q# emission.">
|
| 12 |
+
<style>
|
| 13 |
+
:root { --bg:#0a0e14; --panel:#111722; --line:#1e2a3a; --txt:#d6e2f0; --dim:#7d8ea8; --acc:#5cc8ff; }
|
| 14 |
+
* { box-sizing:border-box; }
|
| 15 |
+
body { margin:0; background:var(--bg); color:var(--txt); font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
| 16 |
+
header, main, footer { max-width:880px; margin:0 auto; padding:28px 20px; }
|
| 17 |
+
header { border-bottom:1px solid var(--line); }
|
| 18 |
+
h1 { margin:0 0 8px; letter-spacing:2px; color:var(--acc); font-size:22px; }
|
| 19 |
+
p { line-height:1.55; color:var(--dim); }
|
| 20 |
+
a { color:var(--acc); }
|
| 21 |
+
.grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
| 22 |
+
@media (max-width:700px) { .grid { grid-template-columns:1fr; } }
|
| 23 |
+
.card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:14px; }
|
| 24 |
+
.card h2 { margin:0 0 8px; font-size:13px; letter-spacing:1px; color:var(--txt); }
|
| 25 |
+
code { color:var(--txt); }
|
| 26 |
+
footer { color:var(--dim); font-size:12px; border-top:1px solid var(--line); }
|
| 27 |
+
</style>
|
| 28 |
+
</head>
|
| 29 |
+
<body>
|
| 30 |
+
<header>
|
| 31 |
+
<h1>CPSC-QFT</h1>
|
| 32 |
+
<p>Conservation-preserving scattering compiler for a 1+1D lattice shadow of φ⁴. Version 0.3.0. Dual strict copyleft.</p>
|
| 33 |
+
</header>
|
| 34 |
+
<main>
|
| 35 |
+
<div class="grid">
|
| 36 |
+
<div class="card">
|
| 37 |
+
<h2>Mercury console</h2>
|
| 38 |
+
<p>Ruby 3.2 in the browser. Build a circuit, run the statevector, and see the compile-time symmetry verdict.</p>
|
| 39 |
+
<p><a href="qsim.html">Open the console</a></p>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="card">
|
| 42 |
+
<h2>Paper</h2>
|
| 43 |
+
<p>What is proved, what is measured, and which operator is actually conserved.</p>
|
| 44 |
+
<p><a href="https://github.com/AHMADALIPARR/cpsc-qft/blob/main/paper/PAPER.md">paper/PAPER.md</a></p>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="card">
|
| 47 |
+
<h2>Certificate</h2>
|
| 48 |
+
<p>Boolean lemmas are proved. Operator commutation is an explicit axiom set. <code>lake build</code> passes on Lean 4.22.0.</p>
|
| 49 |
+
<p><a href="https://github.com/AHMADALIPARR/cpsc-qft/blob/main/lean/CPSC/Lemmas.lean">Lemmas.lean</a></p>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="card">
|
| 52 |
+
<h2>Source</h2>
|
| 53 |
+
<p>Python compiler, Q# emission, Mercury library. Tests live next to the code.</p>
|
| 54 |
+
<p><a href="https://github.com/AHMADALIPARR/cpsc-qft">AHMADALIPARR/cpsc-qft</a></p>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="card" style="margin-top:12px">
|
| 58 |
+
<h2>Release check</h2>
|
| 59 |
+
<p>Ising mode (λ = 0) rejects a single X. Spin-flip mode (λ ≠ 0) rejects a lone Z. The console analyzer still mirrors the Z-parity bridge in <code>frontend/lib/mercury/cpsc.rb</code>; the Python compiler is the mode-accurate path. WASM is loaded from jsDelivr, so the console needs network.</p>
|
| 60 |
+
</div>
|
| 61 |
+
</main>
|
| 62 |
+
<footer>
|
| 63 |
+
LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only. Not the Eclipse Public License.
|
| 64 |
+
</footer>
|
| 65 |
+
</body>
|
| 66 |
+
</html>
|
docs/mercury-circuit-demo.gif
ADDED
|
Git LFS Details
|
docs/mercury-circuit-demo.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0292673a14f213e7702309efbb01e7aa9323e3d32d416495f223edaf62661f1d
|
| 3 |
+
size 258045
|
docs/mercury_bundle.rb
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---- ../lib/mercury/version.rb ----
|
| 2 |
+
# *******************************************************************************
|
| 3 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 4 |
+
#
|
| 5 |
+
# This program and the accompanying materials are made available under the
|
| 6 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 7 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 8 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 9 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 10 |
+
# classpath exception and no permissive relicensing.
|
| 11 |
+
#
|
| 12 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 13 |
+
# *******************************************************************************
|
| 14 |
+
|
| 15 |
+
module Mercury
|
| 16 |
+
VERSION = "0.1.0"
|
| 17 |
+
end
|
| 18 |
+
|
| 19 |
+
# ---- ../lib/mercury/gates.rb ----
|
| 20 |
+
# *******************************************************************************
|
| 21 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 22 |
+
#
|
| 23 |
+
# This program and the accompanying materials are made available under the
|
| 24 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 25 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 26 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 27 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 28 |
+
# classpath exception and no permissive relicensing.
|
| 29 |
+
#
|
| 30 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 31 |
+
# *******************************************************************************
|
| 32 |
+
|
| 33 |
+
# Gate matrices for Mercury. Little-endian convention: in a k-qubit gate
|
| 34 |
+
# applied to qubits [q0, q1, ...], matrix row/column index bit j (value
|
| 35 |
+
# 2**j) is the computational-basis state of qubit q_j. For a two-qubit
|
| 36 |
+
# gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0).
|
| 37 |
+
module Mercury
|
| 38 |
+
module Gates
|
| 39 |
+
ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze
|
| 40 |
+
TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze
|
| 41 |
+
THREE_QUBIT = %i[ccx].freeze
|
| 42 |
+
|
| 43 |
+
PARAM_COUNTS = {
|
| 44 |
+
i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0,
|
| 45 |
+
rx: 1, ry: 1, rz: 1, u: 3,
|
| 46 |
+
cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0,
|
| 47 |
+
ccx: 0
|
| 48 |
+
}.freeze
|
| 49 |
+
|
| 50 |
+
def self.arity(name)
|
| 51 |
+
return 1 if ONE_QUBIT.include?(name)
|
| 52 |
+
return 2 if TWO_QUBIT.include?(name)
|
| 53 |
+
return 3 if THREE_QUBIT.include?(name)
|
| 54 |
+
raise ArgumentError, "unknown gate #{name}"
|
| 55 |
+
end
|
| 56 |
+
|
| 57 |
+
def self.param_count(name)
|
| 58 |
+
PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" }
|
| 59 |
+
end
|
| 60 |
+
|
| 61 |
+
def self.matrix(name, params = [])
|
| 62 |
+
need = param_count(name)
|
| 63 |
+
raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need
|
| 64 |
+
|
| 65 |
+
case name
|
| 66 |
+
when :i then [[c(1), c(0)], [c(0), c(1)]]
|
| 67 |
+
when :x then [[c(0), c(1)], [c(1), c(0)]]
|
| 68 |
+
when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]]
|
| 69 |
+
when :z then [[c(1), c(0)], [c(0), c(-1)]]
|
| 70 |
+
when :h
|
| 71 |
+
s = Math.sqrt(0.5)
|
| 72 |
+
[[c(s), c(s)], [c(s), c(-s)]]
|
| 73 |
+
when :s then [[c(1), c(0)], [c(0), c(0, 1)]]
|
| 74 |
+
when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]]
|
| 75 |
+
when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]]
|
| 76 |
+
when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]]
|
| 77 |
+
when :rx
|
| 78 |
+
th = params[0] / 2.0
|
| 79 |
+
co = c(Math.cos(th)); si = c(0, -Math.sin(th))
|
| 80 |
+
[[co, si], [si, co]]
|
| 81 |
+
when :ry
|
| 82 |
+
th = params[0] / 2.0
|
| 83 |
+
[[c(Math.cos(th)), c(-Math.sin(th))],
|
| 84 |
+
[c(Math.sin(th)), c(Math.cos(th))]]
|
| 85 |
+
when :rz
|
| 86 |
+
th = params[0] / 2.0
|
| 87 |
+
[[phase(-th), c(0)], [c(0), phase(th)]]
|
| 88 |
+
when :u
|
| 89 |
+
th, ph, la = params
|
| 90 |
+
co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0))
|
| 91 |
+
[[co, c(-1) * phase(la) * si],
|
| 92 |
+
[phase(ph) * si, phase(ph + la) * co]]
|
| 93 |
+
when :cnot
|
| 94 |
+
# qubits [target, control]; flips target iff control == 1
|
| 95 |
+
[[c(1), c(0), c(0), c(0)],
|
| 96 |
+
[c(0), c(1), c(0), c(0)],
|
| 97 |
+
[c(0), c(0), c(0), c(1)],
|
| 98 |
+
[c(0), c(0), c(1), c(0)]]
|
| 99 |
+
when :cz
|
| 100 |
+
[[c(1), c(0), c(0), c(0)],
|
| 101 |
+
[c(0), c(1), c(0), c(0)],
|
| 102 |
+
[c(0), c(0), c(1), c(0)],
|
| 103 |
+
[c(0), c(0), c(0), c(-1)]]
|
| 104 |
+
when :cp
|
| 105 |
+
[[c(1), c(0), c(0), c(0)],
|
| 106 |
+
[c(0), c(1), c(0), c(0)],
|
| 107 |
+
[c(0), c(0), c(1), c(0)],
|
| 108 |
+
[c(0), c(0), c(0), phase(params[0])]]
|
| 109 |
+
when :swap
|
| 110 |
+
[[c(1), c(0), c(0), c(0)],
|
| 111 |
+
[c(0), c(0), c(1), c(0)],
|
| 112 |
+
[c(0), c(1), c(0), c(0)],
|
| 113 |
+
[c(0), c(0), c(0), c(1)]]
|
| 114 |
+
when :rzz
|
| 115 |
+
# exp(-i*theta/2 * ZxZ)
|
| 116 |
+
a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0)
|
| 117 |
+
[[a, c(0), c(0), c(0)],
|
| 118 |
+
[c(0), b, c(0), c(0)],
|
| 119 |
+
[c(0), c(0), b, c(0)],
|
| 120 |
+
[c(0), c(0), c(0), a]]
|
| 121 |
+
when :xx
|
| 122 |
+
# XxX: parity-even pair used by the CPSC analyzer
|
| 123 |
+
[[c(0), c(0), c(0), c(1)],
|
| 124 |
+
[c(0), c(0), c(1), c(0)],
|
| 125 |
+
[c(0), c(1), c(0), c(0)],
|
| 126 |
+
[c(1), c(0), c(0), c(0)]]
|
| 127 |
+
when :ccx
|
| 128 |
+
# qubits [target, control1, control2]; flips target iff both controls == 1
|
| 129 |
+
m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } }
|
| 130 |
+
m[6][6] = c(0); m[7][7] = c(0)
|
| 131 |
+
m[6][7] = c(1); m[7][6] = c(1)
|
| 132 |
+
m
|
| 133 |
+
end
|
| 134 |
+
end
|
| 135 |
+
|
| 136 |
+
def self.c(re, im = 0)
|
| 137 |
+
Complex(re, im)
|
| 138 |
+
end
|
| 139 |
+
|
| 140 |
+
def self.phase(angle)
|
| 141 |
+
Complex(Math.cos(angle), Math.sin(angle))
|
| 142 |
+
end
|
| 143 |
+
private_class_method :c, :phase
|
| 144 |
+
end
|
| 145 |
+
end
|
| 146 |
+
|
| 147 |
+
# ---- ../lib/mercury/circuit.rb ----
|
| 148 |
+
# *******************************************************************************
|
| 149 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 150 |
+
#
|
| 151 |
+
# This program and the accompanying materials are made available under the
|
| 152 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 153 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 154 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 155 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 156 |
+
# classpath exception and no permissive relicensing.
|
| 157 |
+
#
|
| 158 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 159 |
+
# *******************************************************************************
|
| 160 |
+
|
| 161 |
+
module Mercury
|
| 162 |
+
# A quantum circuit: an ordered list of gate operations on n qubits.
|
| 163 |
+
#
|
| 164 |
+
# c = Mercury::Circuit.new(2) do |c|
|
| 165 |
+
# c.h 0
|
| 166 |
+
# c.cnot 0, 1
|
| 167 |
+
# c.measure_all
|
| 168 |
+
# end
|
| 169 |
+
#
|
| 170 |
+
# Multi-qubit gate qubit order follows the Gates convention: for
|
| 171 |
+
# cnot(control, target) the DSL takes (control, target) and stores
|
| 172 |
+
# [target, control] so matrix bit 0 is the target. Same for ccx:
|
| 173 |
+
# ccx(c1, c2, target) stores [target, c1, c2].
|
| 174 |
+
class Circuit
|
| 175 |
+
Op = Struct.new(:gate, :qubits, :params, :classical)
|
| 176 |
+
|
| 177 |
+
attr_reader :n, :ops
|
| 178 |
+
|
| 179 |
+
DSL_REORDER = { cnot: true, ccx: true }.freeze
|
| 180 |
+
|
| 181 |
+
def initialize(n)
|
| 182 |
+
raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1
|
| 183 |
+
@n = n
|
| 184 |
+
@ops = []
|
| 185 |
+
yield self if block_given?
|
| 186 |
+
end
|
| 187 |
+
|
| 188 |
+
def add(gate, *qubits, params: [])
|
| 189 |
+
gate = gate.to_sym
|
| 190 |
+
arity = Gates.arity(gate)
|
| 191 |
+
raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity
|
| 192 |
+
raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length
|
| 193 |
+
qubits.each do |q|
|
| 194 |
+
raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n
|
| 195 |
+
end
|
| 196 |
+
stored = DSL_REORDER[gate] ? qubits.reverse : qubits
|
| 197 |
+
@ops << Op.new(gate, stored, params, nil)
|
| 198 |
+
self
|
| 199 |
+
end
|
| 200 |
+
|
| 201 |
+
# Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry.
|
| 202 |
+
(Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname|
|
| 203 |
+
define_method(gname) do |*args|
|
| 204 |
+
need = Gates.param_count(gname)
|
| 205 |
+
arity = Gates.arity(gname)
|
| 206 |
+
raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity
|
| 207 |
+
params = args.first(need)
|
| 208 |
+
qubits = args.last(arity)
|
| 209 |
+
add(gname, *qubits, params: params)
|
| 210 |
+
end
|
| 211 |
+
end
|
| 212 |
+
|
| 213 |
+
def measure(qubit, clbit = nil)
|
| 214 |
+
raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n
|
| 215 |
+
@ops << Op.new(:measure, [qubit], [], clbit || qubit)
|
| 216 |
+
self
|
| 217 |
+
end
|
| 218 |
+
|
| 219 |
+
def measure_all
|
| 220 |
+
@n.times { |q| measure(q, q) }
|
| 221 |
+
self
|
| 222 |
+
end
|
| 223 |
+
|
| 224 |
+
def depth
|
| 225 |
+
@ops.length
|
| 226 |
+
end
|
| 227 |
+
|
| 228 |
+
# Minimal Unicode circuit diagram.
|
| 229 |
+
def to_s
|
| 230 |
+
cols = @ops.map { |op| column(op) }
|
| 231 |
+
rows = (0...@n).map do |q|
|
| 232 |
+
label = "q#{q}: "
|
| 233 |
+
body = cols.map { |col| col[q] }.join("")
|
| 234 |
+
label + body
|
| 235 |
+
end
|
| 236 |
+
rows.join("\n")
|
| 237 |
+
end
|
| 238 |
+
|
| 239 |
+
private
|
| 240 |
+
|
| 241 |
+
def column(op)
|
| 242 |
+
cells = Array.new(@n, "────────")
|
| 243 |
+
case op.gate
|
| 244 |
+
when :measure
|
| 245 |
+
cells[op.qubits[0]] = "───╫M───"
|
| 246 |
+
else
|
| 247 |
+
qs = op.qubits
|
| 248 |
+
tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase
|
| 249 |
+
if qs.length == 1
|
| 250 |
+
cells[qs[0]] = "───#{tag[0, 2]}───"
|
| 251 |
+
else
|
| 252 |
+
# multi-qubit: control dots on qs[1..], box on qs[0], rail between
|
| 253 |
+
lo, hi = qs.minmax
|
| 254 |
+
(lo..hi).each { |q| cells[q] = "───┼────" }
|
| 255 |
+
cells[qs[0]] = "───#{tag[0, 2]}───"
|
| 256 |
+
qs[1..].each { |q| cells[q] = "───●────" }
|
| 257 |
+
end
|
| 258 |
+
end
|
| 259 |
+
cells
|
| 260 |
+
end
|
| 261 |
+
end
|
| 262 |
+
end
|
| 263 |
+
|
| 264 |
+
# ---- ../lib/mercury/simulator.rb ----
|
| 265 |
+
# *******************************************************************************
|
| 266 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 267 |
+
#
|
| 268 |
+
# This program and the accompanying materials are made available under the
|
| 269 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 270 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 271 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 272 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 273 |
+
# classpath exception and no permissive relicensing.
|
| 274 |
+
#
|
| 275 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 276 |
+
# *******************************************************************************
|
| 277 |
+
|
| 278 |
+
module Mercury
|
| 279 |
+
# Full statevector simulator. Little-endian: qubit q is bit q of the
|
| 280 |
+
# basis index, matching the Gates matrix convention.
|
| 281 |
+
class Statevector
|
| 282 |
+
attr_reader :n, :amps
|
| 283 |
+
|
| 284 |
+
# initial: integer basis index, or MSB-first bitstring like "0011".
|
| 285 |
+
def initialize(n, initial: 0)
|
| 286 |
+
raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1
|
| 287 |
+
initial = Integer(initial, 2) if initial.is_a?(String)
|
| 288 |
+
raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1)
|
| 289 |
+
@n = n
|
| 290 |
+
@dim = 1 << n
|
| 291 |
+
@amps = Array.new(@dim, Complex(0, 0))
|
| 292 |
+
@amps[initial] = Complex(1, 0)
|
| 293 |
+
end
|
| 294 |
+
|
| 295 |
+
def dim
|
| 296 |
+
@dim
|
| 297 |
+
end
|
| 298 |
+
|
| 299 |
+
def norm
|
| 300 |
+
Math.sqrt(@amps.sum { |a| a.abs**2 })
|
| 301 |
+
end
|
| 302 |
+
|
| 303 |
+
def apply(gate, qubits, params = [])
|
| 304 |
+
matrix = Gates.matrix(gate, params)
|
| 305 |
+
case qubits.length
|
| 306 |
+
when 1 then apply_one(matrix, qubits[0])
|
| 307 |
+
when 2 then apply_two(matrix, qubits[0], qubits[1])
|
| 308 |
+
when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2])
|
| 309 |
+
else raise ArgumentError, "unsupported arity #{qubits.length}"
|
| 310 |
+
end
|
| 311 |
+
self
|
| 312 |
+
end
|
| 313 |
+
|
| 314 |
+
def probabilities
|
| 315 |
+
@amps.map { |a| a.abs**2 }
|
| 316 |
+
end
|
| 317 |
+
|
| 318 |
+
# Sample `shots` bitstrings (MSB-first strings) from the distribution.
|
| 319 |
+
def sample(shots, rng = Random.new)
|
| 320 |
+
probs = probabilities
|
| 321 |
+
cumulative = []
|
| 322 |
+
total = 0.0
|
| 323 |
+
probs.each { |p| total += p; cumulative << total }
|
| 324 |
+
Array.new(shots) do
|
| 325 |
+
r = rng.rand * total
|
| 326 |
+
idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1)
|
| 327 |
+
bitstring(idx)
|
| 328 |
+
end
|
| 329 |
+
end
|
| 330 |
+
|
| 331 |
+
# Projective Z measurement of one qubit; collapses the state in place.
|
| 332 |
+
def measure!(qubit, rng = Random.new)
|
| 333 |
+
raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1)
|
| 334 |
+
step = 1 << qubit
|
| 335 |
+
p0 = 0.0
|
| 336 |
+
@amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? }
|
| 337 |
+
outcome = rng.rand < p0 ? 0 : 1
|
| 338 |
+
renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0))
|
| 339 |
+
raise "measurement of zero-probability branch" if renorm.zero?
|
| 340 |
+
@amps.each_with_index do |a, s|
|
| 341 |
+
bit = (s & step).zero? ? 0 : 1
|
| 342 |
+
@amps[s] = bit == outcome ? a / renorm : Complex(0, 0)
|
| 343 |
+
end
|
| 344 |
+
outcome
|
| 345 |
+
end
|
| 346 |
+
|
| 347 |
+
def bitstring(index)
|
| 348 |
+
# MSB-first for display: qubit n-1 ... qubit 0
|
| 349 |
+
(0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join
|
| 350 |
+
end
|
| 351 |
+
|
| 352 |
+
private
|
| 353 |
+
|
| 354 |
+
def apply_one(m, q)
|
| 355 |
+
step = 1 << q
|
| 356 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 357 |
+
(0...@dim).each do |s|
|
| 358 |
+
next unless (s & step).zero?
|
| 359 |
+
i0 = s
|
| 360 |
+
i1 = s | step
|
| 361 |
+
a0 = @amps[i0]
|
| 362 |
+
a1 = @amps[i1]
|
| 363 |
+
nxt[i0] = m[0][0] * a0 + m[0][1] * a1
|
| 364 |
+
nxt[i1] = m[1][0] * a0 + m[1][1] * a1
|
| 365 |
+
end
|
| 366 |
+
@amps = nxt
|
| 367 |
+
end
|
| 368 |
+
|
| 369 |
+
def apply_two(m, q0, q1)
|
| 370 |
+
raise ArgumentError, "q0 == q1" if q0 == q1
|
| 371 |
+
s0 = 1 << q0
|
| 372 |
+
s1 = 1 << q1
|
| 373 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 374 |
+
(0...@dim).each do |s|
|
| 375 |
+
next unless (s & s0).zero? && (s & s1).zero?
|
| 376 |
+
idx = [s, s | s0, s | s1, s | s0 | s1]
|
| 377 |
+
old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])]
|
| 378 |
+
4.times do |r|
|
| 379 |
+
nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3]
|
| 380 |
+
end
|
| 381 |
+
end
|
| 382 |
+
@amps = nxt
|
| 383 |
+
end
|
| 384 |
+
|
| 385 |
+
def apply_three(m, q0, q1, q2)
|
| 386 |
+
raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3
|
| 387 |
+
s0 = 1 << q0
|
| 388 |
+
s1 = 1 << q1
|
| 389 |
+
s2 = 1 << q2
|
| 390 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 391 |
+
(0...@dim).each do |s|
|
| 392 |
+
next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero?
|
| 393 |
+
idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) }
|
| 394 |
+
old = idx.map { |i| @amps[i] }
|
| 395 |
+
8.times do |r|
|
| 396 |
+
acc = Complex(0, 0)
|
| 397 |
+
8.times { |cidx| acc += m[r][cidx] * old[cidx] }
|
| 398 |
+
nxt[idx[r]] = acc
|
| 399 |
+
end
|
| 400 |
+
end
|
| 401 |
+
@amps = nxt
|
| 402 |
+
end
|
| 403 |
+
end
|
| 404 |
+
|
| 405 |
+
# Runs a Circuit. :measure ops collapse immediately; `shots` samples the
|
| 406 |
+
# final distribution.
|
| 407 |
+
class Simulator
|
| 408 |
+
Result = Struct.new(:counts, :statevector, :probabilities, :measured)
|
| 409 |
+
|
| 410 |
+
def run(circuit, shots: 0, seed: nil, initial: 0)
|
| 411 |
+
rng = seed.nil? ? Random.new : Random.new(seed)
|
| 412 |
+
sv = Statevector.new(circuit.n, initial: initial)
|
| 413 |
+
measured = {}
|
| 414 |
+
circuit.ops.each do |op|
|
| 415 |
+
if op.gate == :measure
|
| 416 |
+
measured[op.classical] = sv.measure!(op.qubits[0], rng)
|
| 417 |
+
else
|
| 418 |
+
sv.apply(op.gate, op.qubits, op.params)
|
| 419 |
+
end
|
| 420 |
+
end
|
| 421 |
+
counts = {}
|
| 422 |
+
if shots.positive?
|
| 423 |
+
sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 }
|
| 424 |
+
end
|
| 425 |
+
Result.new(counts, sv.amps.dup, sv.probabilities, measured)
|
| 426 |
+
end
|
| 427 |
+
end
|
| 428 |
+
end
|
| 429 |
+
|
| 430 |
+
# ---- ../lib/mercury/mini_json.rb ----
|
| 431 |
+
# *******************************************************************************
|
| 432 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 433 |
+
#
|
| 434 |
+
# This program and the accompanying materials are made available under the
|
| 435 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 436 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 437 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 438 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 439 |
+
# classpath exception and no permissive relicensing.
|
| 440 |
+
#
|
| 441 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 442 |
+
# *******************************************************************************
|
| 443 |
+
|
| 444 |
+
# Minimal JSON parser/generator covering exactly what the Mercury web
|
| 445 |
+
# bridge needs (objects, arrays, strings, numbers, booleans, null).
|
| 446 |
+
# Pure Ruby, no dependencies — safe inside Ruby WASM.
|
| 447 |
+
module Mercury
|
| 448 |
+
module MiniJSON
|
| 449 |
+
class ParseError < StandardError; end
|
| 450 |
+
|
| 451 |
+
def self.parse(str)
|
| 452 |
+
@src = str
|
| 453 |
+
@pos = 0
|
| 454 |
+
skip_ws
|
| 455 |
+
val = parse_value
|
| 456 |
+
skip_ws
|
| 457 |
+
raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length
|
| 458 |
+
val
|
| 459 |
+
end
|
| 460 |
+
|
| 461 |
+
def self.generate(obj)
|
| 462 |
+
case obj
|
| 463 |
+
when Hash
|
| 464 |
+
"{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}"
|
| 465 |
+
when Array
|
| 466 |
+
"[" + obj.map { |v| generate(v) }.join(",") + "]"
|
| 467 |
+
when String
|
| 468 |
+
'"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"'
|
| 469 |
+
when Integer, Float
|
| 470 |
+
raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite?
|
| 471 |
+
obj.to_s
|
| 472 |
+
when true then "true"
|
| 473 |
+
when false then "false"
|
| 474 |
+
when nil then "null"
|
| 475 |
+
else
|
| 476 |
+
raise ParseError, "cannot serialize #{obj.class}"
|
| 477 |
+
end
|
| 478 |
+
end
|
| 479 |
+
|
| 480 |
+
def self.skip_ws
|
| 481 |
+
@pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/
|
| 482 |
+
end
|
| 483 |
+
|
| 484 |
+
def self.parse_value
|
| 485 |
+
case @src[@pos]
|
| 486 |
+
when '"' then parse_string
|
| 487 |
+
when "{" then parse_object
|
| 488 |
+
when "[" then parse_array
|
| 489 |
+
when "t" then literal("true", true)
|
| 490 |
+
when "f" then literal("false", false)
|
| 491 |
+
when "n" then literal("null", nil)
|
| 492 |
+
else parse_number
|
| 493 |
+
end
|
| 494 |
+
end
|
| 495 |
+
|
| 496 |
+
def self.literal(word, val)
|
| 497 |
+
raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word
|
| 498 |
+
@pos += word.length
|
| 499 |
+
val
|
| 500 |
+
end
|
| 501 |
+
|
| 502 |
+
def self.parse_string
|
| 503 |
+
@pos += 1 # opening quote
|
| 504 |
+
out = +""
|
| 505 |
+
while @pos < @src.length
|
| 506 |
+
ch = @src[@pos]
|
| 507 |
+
case ch
|
| 508 |
+
when '"'
|
| 509 |
+
@pos += 1
|
| 510 |
+
return out
|
| 511 |
+
when "\\"
|
| 512 |
+
@pos += 1
|
| 513 |
+
esc = @src[@pos]
|
| 514 |
+
out << case esc
|
| 515 |
+
when '"' then '"'
|
| 516 |
+
when "\\" then "\\"
|
| 517 |
+
when "/" then "/"
|
| 518 |
+
when "b" then "\b"
|
| 519 |
+
when "f" then "\f"
|
| 520 |
+
when "n" then "\n"
|
| 521 |
+
when "r" then "\r"
|
| 522 |
+
when "t" then "\t"
|
| 523 |
+
when "u"
|
| 524 |
+
hex = @src[@pos + 1, 4]
|
| 525 |
+
@pos += 4
|
| 526 |
+
[hex.to_i(16)].pack("U")
|
| 527 |
+
else raise ParseError, "bad escape at #{@pos}"
|
| 528 |
+
end
|
| 529 |
+
@pos += 1
|
| 530 |
+
else
|
| 531 |
+
out << ch
|
| 532 |
+
@pos += 1
|
| 533 |
+
end
|
| 534 |
+
end
|
| 535 |
+
raise ParseError, "unterminated string"
|
| 536 |
+
end
|
| 537 |
+
|
| 538 |
+
def self.parse_object
|
| 539 |
+
@pos += 1
|
| 540 |
+
obj = {}
|
| 541 |
+
skip_ws
|
| 542 |
+
if @src[@pos] == "}"
|
| 543 |
+
@pos += 1
|
| 544 |
+
return obj
|
| 545 |
+
end
|
| 546 |
+
loop do
|
| 547 |
+
skip_ws
|
| 548 |
+
raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"'
|
| 549 |
+
key = parse_string
|
| 550 |
+
skip_ws
|
| 551 |
+
raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":"
|
| 552 |
+
@pos += 1
|
| 553 |
+
skip_ws
|
| 554 |
+
obj[key] = parse_value
|
| 555 |
+
skip_ws
|
| 556 |
+
case @src[@pos]
|
| 557 |
+
when "," then @pos += 1
|
| 558 |
+
when "}" then @pos += 1; break
|
| 559 |
+
else raise ParseError, "expected , or } at #{@pos}"
|
| 560 |
+
end
|
| 561 |
+
end
|
| 562 |
+
obj
|
| 563 |
+
end
|
| 564 |
+
|
| 565 |
+
def self.parse_array
|
| 566 |
+
@pos += 1
|
| 567 |
+
arr = []
|
| 568 |
+
skip_ws
|
| 569 |
+
if @src[@pos] == "]"
|
| 570 |
+
@pos += 1
|
| 571 |
+
return arr
|
| 572 |
+
end
|
| 573 |
+
loop do
|
| 574 |
+
skip_ws
|
| 575 |
+
arr << parse_value
|
| 576 |
+
skip_ws
|
| 577 |
+
case @src[@pos]
|
| 578 |
+
when "," then @pos += 1
|
| 579 |
+
when "]" then @pos += 1; break
|
| 580 |
+
else raise ParseError, "expected , or ] at #{@pos}"
|
| 581 |
+
end
|
| 582 |
+
end
|
| 583 |
+
arr
|
| 584 |
+
end
|
| 585 |
+
|
| 586 |
+
def self.parse_number
|
| 587 |
+
m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)
|
| 588 |
+
raise ParseError, "bad number at #{@pos}" unless m
|
| 589 |
+
@pos += m[0].length
|
| 590 |
+
s = m[0]
|
| 591 |
+
(s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i
|
| 592 |
+
end
|
| 593 |
+
end
|
| 594 |
+
end
|
| 595 |
+
|
| 596 |
+
# ---- ../lib/mercury/cpsc.rb ----
|
| 597 |
+
# *******************************************************************************
|
| 598 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 599 |
+
#
|
| 600 |
+
# This program and the accompanying materials are made available under the
|
| 601 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 602 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 603 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 604 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 605 |
+
# classpath exception and no permissive relicensing.
|
| 606 |
+
#
|
| 607 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 608 |
+
# *******************************************************************************
|
| 609 |
+
|
| 610 |
+
# Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a
|
| 611 |
+
# Trotterized scattering evolution built as a Mercury circuit.
|
| 612 |
+
#
|
| 613 |
+
# Gate-kind semantics (identical to the Python original):
|
| 614 |
+
# - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z.
|
| 615 |
+
# - "parity_even": preserves prod Z (Z2 parity) but may change sum Z.
|
| 616 |
+
# - "illegal": odd X/Y support; changes Z-parity. Rejected.
|
| 617 |
+
module Mercury
|
| 618 |
+
module CPSC
|
| 619 |
+
# Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds.
|
| 620 |
+
KINDS = {
|
| 621 |
+
i: "magnetization", z: "magnetization", s: "magnetization",
|
| 622 |
+
t: "magnetization", sdg: "magnetization", tdg: "magnetization",
|
| 623 |
+
rz: "magnetization", cz: "magnetization", rzz: "magnetization",
|
| 624 |
+
cp: "magnetization",
|
| 625 |
+
xx: "parity_even",
|
| 626 |
+
x: "illegal", y: "illegal", h: "illegal",
|
| 627 |
+
rx: "illegal", ry: "illegal", u: "illegal",
|
| 628 |
+
cnot: "illegal", swap: "illegal", ccx: "illegal"
|
| 629 |
+
}.freeze
|
| 630 |
+
|
| 631 |
+
GateSpec = Struct.new(:name, :qubits, :kind)
|
| 632 |
+
Rejection = Struct.new(:spec, :reason)
|
| 633 |
+
|
| 634 |
+
class CompilationRejected < StandardError; end
|
| 635 |
+
|
| 636 |
+
class SymmetryAnalyzer
|
| 637 |
+
def initialize(n, conserve_magnetization: false)
|
| 638 |
+
raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1
|
| 639 |
+
@n = n
|
| 640 |
+
@conserve_magnetization = conserve_magnetization
|
| 641 |
+
end
|
| 642 |
+
|
| 643 |
+
def check(specs)
|
| 644 |
+
specs.flat_map do |spec|
|
| 645 |
+
reason = reject_reason(spec)
|
| 646 |
+
reason ? [Rejection.new(spec, reason)] : []
|
| 647 |
+
end
|
| 648 |
+
end
|
| 649 |
+
|
| 650 |
+
def admit(specs)
|
| 651 |
+
rejected = check(specs)
|
| 652 |
+
unless rejected.empty?
|
| 653 |
+
detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ")
|
| 654 |
+
raise CompilationRejected, detail
|
| 655 |
+
end
|
| 656 |
+
specs.dup
|
| 657 |
+
end
|
| 658 |
+
|
| 659 |
+
private
|
| 660 |
+
|
| 661 |
+
def reject_reason(spec)
|
| 662 |
+
spec.qubits.each do |q|
|
| 663 |
+
return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1)
|
| 664 |
+
end
|
| 665 |
+
case spec.kind
|
| 666 |
+
when "illegal"
|
| 667 |
+
"odd X/Y support changes Z-parity; forbidden sector"
|
| 668 |
+
when "parity_even", "magnetization"
|
| 669 |
+
nil
|
| 670 |
+
else
|
| 671 |
+
"unknown gate kind #{spec.kind}"
|
| 672 |
+
end
|
| 673 |
+
end
|
| 674 |
+
end
|
| 675 |
+
|
| 676 |
+
# Classify a Mercury::Circuit::Op into a GateSpec for the analyzer.
|
| 677 |
+
def self.classify(op)
|
| 678 |
+
kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" }
|
| 679 |
+
label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})"
|
| 680 |
+
GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind)
|
| 681 |
+
end
|
| 682 |
+
|
| 683 |
+
# Run the analyzer over a whole circuit. Returns
|
| 684 |
+
# { admitted: bool, rejections: [{gate, reason}] }.
|
| 685 |
+
def self.analyze(circuit, conserve_magnetization: false)
|
| 686 |
+
analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization)
|
| 687 |
+
specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) }
|
| 688 |
+
rejections = analyzer.check(specs)
|
| 689 |
+
{
|
| 690 |
+
admitted: rejections.empty?,
|
| 691 |
+
rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } }
|
| 692 |
+
}
|
| 693 |
+
end
|
| 694 |
+
|
| 695 |
+
# First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i
|
| 696 |
+
# (periodic), matching src/cpsc/hamiltonian.py. Per step dt:
|
| 697 |
+
# prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i).
|
| 698 |
+
def self.trotter_circuit(n, j:, lam:, time:, steps:)
|
| 699 |
+
raise ArgumentError, "steps must be >= 1" unless steps >= 1
|
| 700 |
+
dt = time.to_f / steps
|
| 701 |
+
Circuit.new(n) do |c|
|
| 702 |
+
steps.times do
|
| 703 |
+
(0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) }
|
| 704 |
+
(0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero?
|
| 705 |
+
end
|
| 706 |
+
end
|
| 707 |
+
end
|
| 708 |
+
|
| 709 |
+
def self.parity_of(basis, n)
|
| 710 |
+
basis.to_s(2).count("1") & 1
|
| 711 |
+
end
|
| 712 |
+
|
| 713 |
+
def self.magnetization_of(basis, n)
|
| 714 |
+
mag = 0
|
| 715 |
+
n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 }
|
| 716 |
+
mag
|
| 717 |
+
end
|
| 718 |
+
|
| 719 |
+
# Probability mass outside the input Z2-parity sector.
|
| 720 |
+
def self.leakage(amps, n, parity)
|
| 721 |
+
wrong = 0.0
|
| 722 |
+
amps.each_with_index do |a, b|
|
| 723 |
+
wrong += a.abs**2 if parity_of(b, n) != parity
|
| 724 |
+
end
|
| 725 |
+
wrong
|
| 726 |
+
end
|
| 727 |
+
|
| 728 |
+
# <psi|H|psi> for the transverse-field Ising H, computed directly.
|
| 729 |
+
def self.energy_expectation(amps, n, j:, lam:)
|
| 730 |
+
dim = 1 << n
|
| 731 |
+
e = 0.0
|
| 732 |
+
dim.times do |s|
|
| 733 |
+
p = amps[s].abs**2
|
| 734 |
+
next if p.zero?
|
| 735 |
+
zz = 0.0
|
| 736 |
+
n.times do |i|
|
| 737 |
+
zi = ((s >> i) & 1).zero? ? 1.0 : -1.0
|
| 738 |
+
zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0
|
| 739 |
+
zz += zi * zj
|
| 740 |
+
end
|
| 741 |
+
e += p * (-j * zz)
|
| 742 |
+
end
|
| 743 |
+
unless lam.zero?
|
| 744 |
+
x = 0.0
|
| 745 |
+
n.times do |i|
|
| 746 |
+
bit = 1 << i
|
| 747 |
+
dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real }
|
| 748 |
+
end
|
| 749 |
+
e += lam * x
|
| 750 |
+
end
|
| 751 |
+
e
|
| 752 |
+
end
|
| 753 |
+
|
| 754 |
+
end
|
| 755 |
+
end
|
| 756 |
+
|
| 757 |
+
# ---- app.rb ----
|
| 758 |
+
# *******************************************************************************
|
| 759 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 760 |
+
#
|
| 761 |
+
# This program and the accompanying materials are made available under the
|
| 762 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 763 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 764 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 765 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 766 |
+
# classpath exception and no permissive relicensing.
|
| 767 |
+
#
|
| 768 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 769 |
+
# *******************************************************************************
|
| 770 |
+
|
| 771 |
+
# Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into
|
| 772 |
+
# mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript
|
| 773 |
+
# drives it via vm.eval("handle_request('<json>')").
|
| 774 |
+
#
|
| 775 |
+
# Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]],
|
| 776 |
+
# "shots":1024,"seed":42,"cpsc":true,"magnetization":false,
|
| 777 |
+
# "j":1.0,"lam":0.0}
|
| 778 |
+
# Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...},
|
| 779 |
+
# "statevector":[[bits,re,im],...],"norm":1.0,
|
| 780 |
+
# "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}}
|
| 781 |
+
|
| 782 |
+
def bitstring_of(index, n)
|
| 783 |
+
(0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join
|
| 784 |
+
end
|
| 785 |
+
|
| 786 |
+
def handle_request(json_str)
|
| 787 |
+
req = Mercury::MiniJSON.parse(json_str)
|
| 788 |
+
n = req["n"].to_i
|
| 789 |
+
raise "n must be 1..12" unless n.between?(1, 12)
|
| 790 |
+
|
| 791 |
+
c = Mercury::Circuit.new(n)
|
| 792 |
+
(req["ops"] || []).each do |op|
|
| 793 |
+
name = op[0].to_s.to_sym
|
| 794 |
+
qubits = op[1].map(&:to_i)
|
| 795 |
+
params = (op[2] || []).map(&:to_f)
|
| 796 |
+
c.add(name, *qubits, params: params)
|
| 797 |
+
end
|
| 798 |
+
|
| 799 |
+
initial = req.fetch("initial", "0" * n).to_s
|
| 800 |
+
raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/)
|
| 801 |
+
init_idx = Integer(initial, 2)
|
| 802 |
+
|
| 803 |
+
shots = req.fetch("shots", 0).to_i
|
| 804 |
+
seed = req["seed"]
|
| 805 |
+
|
| 806 |
+
res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx)
|
| 807 |
+
|
| 808 |
+
probs = []
|
| 809 |
+
res.probabilities.each_with_index do |p, i|
|
| 810 |
+
probs << [bitstring_of(i, n), p] if p > 1e-9
|
| 811 |
+
end
|
| 812 |
+
probs.sort_by! { |_, p| -p }
|
| 813 |
+
|
| 814 |
+
sv = []
|
| 815 |
+
res.statevector.each_with_index do |a, i|
|
| 816 |
+
sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9
|
| 817 |
+
end
|
| 818 |
+
sv.sort_by! { |_, re, im| -(re * re + im * im) }
|
| 819 |
+
|
| 820 |
+
out = {
|
| 821 |
+
"diagram" => c.to_s,
|
| 822 |
+
"probs" => probs.first(24),
|
| 823 |
+
"counts" => res.counts,
|
| 824 |
+
"statevector" => sv.first(24),
|
| 825 |
+
"norm" => Math.sqrt(res.probabilities.sum)
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
if req["cpsc"]
|
| 829 |
+
verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"])
|
| 830 |
+
out["cpsc"] = {
|
| 831 |
+
"admitted" => verdict[:admitted],
|
| 832 |
+
"rejections" => verdict[:rejections],
|
| 833 |
+
"leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)),
|
| 834 |
+
"energy" => Mercury::CPSC.energy_expectation(
|
| 835 |
+
res.statevector, n,
|
| 836 |
+
j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f
|
| 837 |
+
)
|
| 838 |
+
}
|
| 839 |
+
end
|
| 840 |
+
|
| 841 |
+
Mercury::MiniJSON.generate(out)
|
| 842 |
+
rescue => e
|
| 843 |
+
Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" })
|
| 844 |
+
end
|
| 845 |
+
|
docs/qsim.html
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<!--
|
| 3 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 4 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 5 |
+
-->
|
| 6 |
+
<html lang="en">
|
| 7 |
+
<head>
|
| 8 |
+
<meta charset="utf-8">
|
| 9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 10 |
+
<title>Mercury — Quantum Circuit Console · CPSC-QFT</title>
|
| 11 |
+
<style>
|
| 12 |
+
:root { --bg:#0a0e14; --panel:#111722; --line:#1e2a3a; --txt:#d6e2f0; --dim:#7d8ea8;
|
| 13 |
+
--acc:#5cc8ff; --ok:#5df08a; --bad:#ff6b6b; --warn:#ffd479; }
|
| 14 |
+
* { box-sizing:border-box; }
|
| 15 |
+
body { margin:0; background:var(--bg); color:var(--txt);
|
| 16 |
+
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px; }
|
| 17 |
+
header { padding:14px 20px; border-bottom:1px solid var(--line); display:flex;
|
| 18 |
+
align-items:baseline; gap:14px; flex-wrap:wrap; }
|
| 19 |
+
header h1 { margin:0; font-size:17px; letter-spacing:2px; color:var(--acc); }
|
| 20 |
+
header span { color:var(--dim); font-size:12px; }
|
| 21 |
+
#status { margin-left:auto; font-size:12px; color:var(--warn); }
|
| 22 |
+
#status.ready { color:var(--ok); }
|
| 23 |
+
main { display:grid; grid-template-columns:minmax(340px,430px) 1fr; gap:14px; padding:14px 20px; }
|
| 24 |
+
@media (max-width:900px){ main{ grid-template-columns:1fr; } }
|
| 25 |
+
.card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:12px; margin-bottom:14px; }
|
| 26 |
+
.card h2 { margin:0 0 10px; font-size:12px; letter-spacing:1.5px; color:var(--dim); text-transform:uppercase; }
|
| 27 |
+
label { display:block; color:var(--dim); font-size:11px; margin:8px 0 3px; }
|
| 28 |
+
input[type=text], input[type=number], textarea, select {
|
| 29 |
+
width:100%; background:#0a0e14; color:var(--txt); border:1px solid var(--line);
|
| 30 |
+
border-radius:5px; padding:7px 9px; font:inherit; font-size:12.5px; }
|
| 31 |
+
textarea { min-height:150px; resize:vertical; line-height:1.5; }
|
| 32 |
+
.row { display:flex; gap:8px; } .row > div { flex:1; }
|
| 33 |
+
button { background:#16202f; color:var(--txt); border:1px solid var(--line); border-radius:5px;
|
| 34 |
+
padding:7px 10px; font:inherit; font-size:12px; cursor:pointer; }
|
| 35 |
+
button:hover { border-color:var(--acc); color:var(--acc); }
|
| 36 |
+
button.primary { background:#12324a; border-color:var(--acc); color:var(--acc); font-weight:bold; }
|
| 37 |
+
button.primary:hover { background:#17405e; }
|
| 38 |
+
.palette { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
|
| 39 |
+
.palette button { padding:5px 9px; }
|
| 40 |
+
.presets { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
| 41 |
+
pre.diagram { background:#0a0e14; border:1px solid var(--line); border-radius:5px;
|
| 42 |
+
padding:10px; overflow-x:auto; font-size:12.5px; line-height:1.7; }
|
| 43 |
+
table { width:100%; border-collapse:collapse; font-size:12px; }
|
| 44 |
+
th, td { text-align:right; padding:4px 8px; border-bottom:1px solid var(--line); }
|
| 45 |
+
th { color:var(--dim); font-weight:normal; } td:first-child, th:first-child { text-align:left; }
|
| 46 |
+
.verdict-ok { color:var(--ok); font-weight:bold; } .verdict-bad { color:var(--bad); font-weight:bold; }
|
| 47 |
+
.rej { color:var(--bad); } .muted { color:var(--dim); }
|
| 48 |
+
canvas { width:100%; height:220px; background:#0a0e14; border:1px solid var(--line); border-radius:5px; }
|
| 49 |
+
.kv { display:flex; justify-content:space-between; padding:3px 0; }
|
| 50 |
+
.kv b { font-weight:normal; color:var(--dim); }
|
| 51 |
+
footer { padding:10px 20px 20px; color:var(--dim); font-size:11px; }
|
| 52 |
+
a { color:var(--acc); }
|
| 53 |
+
</style>
|
| 54 |
+
</head>
|
| 55 |
+
<body>
|
| 56 |
+
<header>
|
| 57 |
+
<h1>MERCURY</h1>
|
| 58 |
+
<a href="./" style="color:#7d8ea8;text-decoration:none">site</a>
|
| 59 |
+
<span>quantum circuit console · CPSC-QFT frontend · Ruby/WASM statevector simulator</span>
|
| 60 |
+
<span id="status">● loading Ruby…</span>
|
| 61 |
+
</header>
|
| 62 |
+
<main>
|
| 63 |
+
<div>
|
| 64 |
+
<div class="card">
|
| 65 |
+
<h2>Circuit</h2>
|
| 66 |
+
<div class="row">
|
| 67 |
+
<div><label>Qubits (1–12)</label><input type="number" id="n" value="2" min="1" max="12"></div>
|
| 68 |
+
<div><label>Initial state (bitstring)</label><input type="text" id="initial" value="00"></div>
|
| 69 |
+
</div>
|
| 70 |
+
<label>Script — one op per line: <span class="muted">h 0 · rx 1.57 1 · cnot 0 1 · cp 0.79 1 0 · rzz 0.5 0 1 · xx 0 1 · measure 0</span></label>
|
| 71 |
+
<textarea id="dsl">h 0
|
| 72 |
+
cnot 0 1</textarea>
|
| 73 |
+
<label>Gate palette (appends)</label>
|
| 74 |
+
<div class="palette" id="palette"></div>
|
| 75 |
+
<label>Presets</label>
|
| 76 |
+
<div class="presets">
|
| 77 |
+
<button data-preset="bell">Bell pair</button>
|
| 78 |
+
<button data-preset="ghz">GHZ-3</button>
|
| 79 |
+
<button data-preset="qft">QFT-3</button>
|
| 80 |
+
<button data-preset="trotter">Trotter scattering</button>
|
| 81 |
+
<button data-preset="adversarial">Adversarial X</button>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="card">
|
| 85 |
+
<h2>Run</h2>
|
| 86 |
+
<div class="row">
|
| 87 |
+
<div><label>Shots (0 = statevector only)</label><input type="number" id="shots" value="1024" min="0" max="100000"></div>
|
| 88 |
+
<div><label>Seed (blank = random)</label><input type="text" id="seed" value="42"></div>
|
| 89 |
+
</div>
|
| 90 |
+
<label style="margin-top:10px"><input type="checkbox" id="cpsc" checked style="width:auto"> CPSC conservation check</label>
|
| 91 |
+
<div class="row">
|
| 92 |
+
<div><label>J (coupling)</label><input type="text" id="pj" value="1.0"></div>
|
| 93 |
+
<div><label>λ (transverse field)</label><input type="text" id="plam" value="0.0"></div>
|
| 94 |
+
<div><label style="margin-top:22px"><input type="checkbox" id="pmag" style="width:auto"> conserve ΣZ</label></div>
|
| 95 |
+
</div>
|
| 96 |
+
<div style="margin-top:10px"><button class="primary" id="run" style="width:100%; padding:10px">▶ Run circuit</button></div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="card">
|
| 99 |
+
<h2>Conservation (CPSC)</h2>
|
| 100 |
+
<div id="cpscOut"><span class="muted">Run a circuit to check symmetry-sector admission.</span></div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div>
|
| 104 |
+
<div class="card">
|
| 105 |
+
<h2>Circuit diagram</h2>
|
| 106 |
+
<pre class="diagram" id="diagram"><span class="muted">—</span></pre>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="card">
|
| 109 |
+
<h2>Measurement histogram</h2>
|
| 110 |
+
<canvas id="hist" width="800" height="220"></canvas>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="card">
|
| 113 |
+
<h2>Statevector (top amplitudes)</h2>
|
| 114 |
+
<table><thead><tr><th>basis</th><th>Re</th><th>Im</th><th>P</th></tr></thead>
|
| 115 |
+
<tbody id="svBody"><tr><td colspan="4" class="muted">—</td></tr></tbody></table>
|
| 116 |
+
</div>
|
| 117 |
+
<div class="card">
|
| 118 |
+
<h2>Probabilities</h2>
|
| 119 |
+
<table><thead><tr><th>basis</th><th>P</th></tr></thead>
|
| 120 |
+
<tbody id="probBody"><tr><td colspan="2" class="muted">—</td></tr></tbody></table>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</main>
|
| 124 |
+
<footer>
|
| 125 |
+
Mercury is a dependency-free Ruby statevector simulator (up to 12 qubits in-browser). The CPSC panel mirrors
|
| 126 |
+
<span class="muted">src/cpsc/analyzer.py</span>: gates are admitted only if they preserve the Z<sub>2</sub> parity sector —
|
| 127 |
+
a compile-time rejection rule, not post-selection. Trotter preset evolves the transverse-field Ising H under test.
|
| 128 |
+
</footer>
|
| 129 |
+
|
| 130 |
+
<script src="https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.3.0/dist/browser.umd.js"></script>
|
| 131 |
+
<script>
|
| 132 |
+
/* Mercury WASM boot — same pattern as the Intentsim console. */
|
| 133 |
+
const W = { vm: null, ready: false };
|
| 134 |
+
const RUBY_WASM_URL = "https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.3.0/dist/ruby+stdlib.wasm";
|
| 135 |
+
function setBoot(t, ok) {
|
| 136 |
+
const el = document.getElementById("status");
|
| 137 |
+
el.textContent = t;
|
| 138 |
+
el.classList.toggle("ready", !!ok);
|
| 139 |
+
document.getElementById("run").disabled = !ok;
|
| 140 |
+
}
|
| 141 |
+
function rbEscape(s) { return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); }
|
| 142 |
+
function mercuryRun(req) {
|
| 143 |
+
const code = `handle_request('${rbEscape(JSON.stringify(req))}')`;
|
| 144 |
+
return JSON.parse(W.vm.eval(code).toString());
|
| 145 |
+
}
|
| 146 |
+
async function bootRuby() {
|
| 147 |
+
try {
|
| 148 |
+
if (!window["ruby-wasm-wasi"]) throw new Error("ruby-wasm-wasi bundle missing");
|
| 149 |
+
setBoot("● loading ruby 3.2 webassembly (≈32 mb)…");
|
| 150 |
+
const { DefaultRubyVM } = window["ruby-wasm-wasi"];
|
| 151 |
+
const response = await fetch(RUBY_WASM_URL);
|
| 152 |
+
if (!response.ok) throw new Error("wasm fetch failed: " + response.status);
|
| 153 |
+
setBoot("● compiling webassembly…");
|
| 154 |
+
const module = await WebAssembly.compile(await response.arrayBuffer());
|
| 155 |
+
setBoot("● starting ruby vm…");
|
| 156 |
+
const { vm } = await DefaultRubyVM(module);
|
| 157 |
+
setBoot("● evaluating mercury bundle…");
|
| 158 |
+
const src = await (await fetch("mercury_bundle.rb")).text();
|
| 159 |
+
if (!src.includes("module Mercury")) throw new Error("mercury bundle not found");
|
| 160 |
+
vm.eval(src);
|
| 161 |
+
W.vm = vm; W.ready = true;
|
| 162 |
+
setBoot("● ruby ready — live simulation", true);
|
| 163 |
+
} catch (e) {
|
| 164 |
+
setBoot("● wasm unreachable (" + (e.message || e) + ")");
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
const PARAM_N = {rx:1, ry:1, rz:1, cp:1, rzz:1, u:3};
|
| 169 |
+
const GATE_LIST = ["h","x","y","z","s","t","rx","ry","rz","cnot","cz","cp","swap","rzz","xx","ccx"];
|
| 170 |
+
|
| 171 |
+
function parseDSL(text) {
|
| 172 |
+
const ops = [];
|
| 173 |
+
for (const raw of text.split("\n")) {
|
| 174 |
+
const line = raw.trim();
|
| 175 |
+
if (!line || line.startsWith("#")) continue;
|
| 176 |
+
const t = line.split(/\s+/);
|
| 177 |
+
const gate = t[0].toLowerCase();
|
| 178 |
+
let idx = 1, params = [];
|
| 179 |
+
const pn = PARAM_N[gate] || 0;
|
| 180 |
+
for (let k = 0; k < pn; k++) {
|
| 181 |
+
if (idx >= t.length) throw new Error("missing param for " + gate);
|
| 182 |
+
const v = parseFloat(t[idx++]);
|
| 183 |
+
if (!isFinite(v)) throw new Error("bad param " + t[idx-1]);
|
| 184 |
+
params.push(v);
|
| 185 |
+
}
|
| 186 |
+
const qubits = [];
|
| 187 |
+
while (idx < t.length) {
|
| 188 |
+
const q = parseInt(t[idx++], 10);
|
| 189 |
+
if (!isFinite(q) || q < 0) throw new Error("bad qubit");
|
| 190 |
+
qubits.push(q);
|
| 191 |
+
}
|
| 192 |
+
ops.push([gate, qubits, params]);
|
| 193 |
+
}
|
| 194 |
+
return ops;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
document.getElementById("run").disabled = true;
|
| 198 |
+
document.addEventListener("DOMContentLoaded", bootRuby);
|
| 199 |
+
|
| 200 |
+
// gate palette
|
| 201 |
+
const pal = document.getElementById("palette");
|
| 202 |
+
for (const g of GATE_LIST) {
|
| 203 |
+
const b = document.createElement("button");
|
| 204 |
+
b.textContent = g.toUpperCase();
|
| 205 |
+
b.onclick = () => {
|
| 206 |
+
const dsl = document.getElementById("dsl");
|
| 207 |
+
const pn = PARAM_N[g] || 0;
|
| 208 |
+
dsl.value += (dsl.value.endsWith("\n") || dsl.value === "" ? "" : "\n") + g + " " + "0.5 ".repeat(pn) + "0\n";
|
| 209 |
+
dsl.focus();
|
| 210 |
+
};
|
| 211 |
+
pal.appendChild(b);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
const PRESETS = {
|
| 215 |
+
bell: { n:"2", initial:"00", dsl:"h 0\ncnot 0 1" },
|
| 216 |
+
ghz: { n:"3", initial:"000", dsl:"h 0\ncnot 0 1\ncnot 1 2" },
|
| 217 |
+
qft: { n:"3", initial:"000",
|
| 218 |
+
dsl:"h 0\ncp 1.5708 1 0\ncp 0.7854 2 0\nh 1\ncp 1.5708 2 1\nh 2\nswap 0 2" },
|
| 219 |
+
trotter:{ n:"4", initial:"0011", lam:"0.0",
|
| 220 |
+
dsl:"# Trotter: H = -J ZZ + lam X, J=1, t=0.4, 8 steps\n" +
|
| 221 |
+
Array(8).fill(
|
| 222 |
+
"rzz -0.1 0 1\nrzz -0.1 1 2\nrzz -0.1 2 3\nrzz -0.1 3 0"
|
| 223 |
+
).join("\n") },
|
| 224 |
+
adversarial: { n:"4", initial:"0000",
|
| 225 |
+
dsl:"rzz 0.5 0 1\nx 2\nrzz 0.5 2 3" }
|
| 226 |
+
};
|
| 227 |
+
document.querySelectorAll("[data-preset]").forEach(b => b.onclick = () => {
|
| 228 |
+
const p = PRESETS[b.dataset.preset];
|
| 229 |
+
document.getElementById("n").value = p.n;
|
| 230 |
+
document.getElementById("initial").value = p.initial;
|
| 231 |
+
document.getElementById("dsl").value = p.dsl;
|
| 232 |
+
if (p.lam !== undefined) document.getElementById("plam").value = p.lam;
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
document.getElementById("run").onclick = () => {
|
| 236 |
+
let req;
|
| 237 |
+
try {
|
| 238 |
+
const n = parseInt(document.getElementById("n").value, 10);
|
| 239 |
+
req = {
|
| 240 |
+
n, initial: document.getElementById("initial").value.trim(),
|
| 241 |
+
ops: parseDSL(document.getElementById("dsl").value),
|
| 242 |
+
shots: parseInt(document.getElementById("shots").value, 10) || 0,
|
| 243 |
+
seed: document.getElementById("seed").value.trim() === "" ? null
|
| 244 |
+
: parseInt(document.getElementById("seed").value, 10),
|
| 245 |
+
cpsc: document.getElementById("cpsc").checked,
|
| 246 |
+
magnetization: document.getElementById("pmag").checked,
|
| 247 |
+
j: parseFloat(document.getElementById("pj").value) || 0,
|
| 248 |
+
lam: parseFloat(document.getElementById("plam").value) || 0
|
| 249 |
+
};
|
| 250 |
+
} catch (e) { alert("Script error: " + e.message); return; }
|
| 251 |
+
let out;
|
| 252 |
+
try { out = mercuryRun(req); }
|
| 253 |
+
catch (e) { alert("Simulator error: " + e.message); return; }
|
| 254 |
+
if (out.error) { alert("Simulator error: " + out.error); return; }
|
| 255 |
+
render(out);
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
function render(out) {
|
| 259 |
+
document.getElementById("diagram").textContent = out.diagram;
|
| 260 |
+
// histogram: prefer counts, fall back to probs
|
| 261 |
+
const data = Object.keys(out.counts).length ? out.counts :
|
| 262 |
+
Object.fromEntries(out.probs.map(([b, p]) => [b, p]));
|
| 263 |
+
drawHist(data);
|
| 264 |
+
const svb = document.getElementById("svBody"); svb.innerHTML = "";
|
| 265 |
+
for (const [b, re, im] of out.statevector) {
|
| 266 |
+
const p = re*re + im*im;
|
| 267 |
+
svb.innerHTML += `<tr><td>|${b}⟩</td><td>${re.toFixed(4)}</td><td>${im.toFixed(4)}</td><td>${p.toFixed(4)}</td></tr>`;
|
| 268 |
+
}
|
| 269 |
+
const prb = document.getElementById("probBody"); prb.innerHTML = "";
|
| 270 |
+
for (const [b, p] of out.probs) prb.innerHTML += `<tr><td>|${b}⟩</td><td>${p.toFixed(4)}</td></tr>`;
|
| 271 |
+
const c = document.getElementById("cpscOut");
|
| 272 |
+
if (out.cpsc) {
|
| 273 |
+
const v = out.cpsc;
|
| 274 |
+
let html = v.admitted
|
| 275 |
+
? `<div class="verdict-ok">✓ ADMITTED — every gate preserves the symmetry sector</div>`
|
| 276 |
+
: `<div class="verdict-bad">✗ REJECTED at compile time</div>`;
|
| 277 |
+
for (const r of v.rejections) html += `<div class="rej">· ${r.gate}: ${r.reason}</div>`;
|
| 278 |
+
html += `<div class="kv"><b>parity leakage</b><span>${v.leakage.toExponential(2)}</span></div>`;
|
| 279 |
+
html += `<div class="kv"><b>⟨H⟩ (J,λ as set)</b><span>${v.energy.toFixed(6)}</span></div>`;
|
| 280 |
+
html += `<div class="kv"><b>norm</b><span>${out.norm.toFixed(6)}</span></div>`;
|
| 281 |
+
c.innerHTML = html;
|
| 282 |
+
} else c.innerHTML = '<span class="muted">CPSC check disabled.</span>';
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function drawHist(data) {
|
| 286 |
+
const cv = document.getElementById("hist"), ctx = cv.getContext("2d");
|
| 287 |
+
const W = cv.width, H = cv.height;
|
| 288 |
+
ctx.clearRect(0, 0, W, H);
|
| 289 |
+
const keys = Object.keys(data).sort();
|
| 290 |
+
if (!keys.length) return;
|
| 291 |
+
const max = Math.max(...Object.values(data));
|
| 292 |
+
const bw = W / keys.length;
|
| 293 |
+
ctx.fillStyle = "#5cc8ff"; ctx.strokeStyle = "#1e2a3a";
|
| 294 |
+
ctx.font = "11px monospace"; ctx.textAlign = "center";
|
| 295 |
+
keys.forEach((k, i) => {
|
| 296 |
+
const h = (data[k] / max) * (H - 40);
|
| 297 |
+
const x = i * bw;
|
| 298 |
+
ctx.fillRect(x + 3, H - 25 - h, bw - 6, h);
|
| 299 |
+
ctx.fillStyle = "#7d8ea8";
|
| 300 |
+
ctx.fillText(k, x + bw / 2, H - 10);
|
| 301 |
+
ctx.fillText(String(Math.round(data[k] * 100) / 100), x + bw / 2, H - 28 - h);
|
| 302 |
+
ctx.fillStyle = "#5cc8ff";
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
</script>
|
| 306 |
+
</body>
|
| 307 |
+
</html>
|
frontend/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
-->
|
| 5 |
+
|
| 6 |
+
# Mercury — Ruby quantum circuit frontend for CPSC-QFT
|
| 7 |
+
|
| 8 |
+
A dependency-free Ruby quantum circuit library and full statevector
|
| 9 |
+
simulator, with a browser console (Ruby/WASM) for building circuits,
|
| 10 |
+
running them, and checking CPSC conservation laws at compile time.
|
| 11 |
+
|
| 12 |
+
## Layout
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
frontend/
|
| 16 |
+
lib/mercury.rb entry point (require this)
|
| 17 |
+
lib/mercury/version.rb
|
| 18 |
+
lib/mercury/gates.rb gate matrices: H X Y Z S T Rx Ry Rz U,
|
| 19 |
+
CNOT CZ CP SWAP RZZ XX, Toffoli
|
| 20 |
+
lib/mercury/circuit.rb circuit DSL + Unicode diagram
|
| 21 |
+
lib/mercury/simulator.rb full statevector simulator
|
| 22 |
+
lib/mercury/mini_json.rb dependency-free JSON (WASM-safe)
|
| 23 |
+
lib/mercury/cpsc.rb CPSC bridge: symmetry analyzer, Trotter
|
| 24 |
+
scattering evolution, leakage, <H>
|
| 25 |
+
test/test_mercury.rb 25 tests, 556 assertions (minitest, stdlib only)
|
| 26 |
+
web/index.html console template (= qsim.html after build)
|
| 27 |
+
web/app.rb WASM bridge (handle_request)
|
| 28 |
+
web/build.sh builds mercury_bundle.rb + qsim.html
|
| 29 |
+
web/qsim.html the console (generated)
|
| 30 |
+
web/mercury_bundle.rb concatenated Ruby (generated, fetched at boot)
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## Quickstart (Ruby)
|
| 34 |
+
|
| 35 |
+
```ruby
|
| 36 |
+
require_relative "lib/mercury"
|
| 37 |
+
|
| 38 |
+
# Bell pair
|
| 39 |
+
c = Mercury::Circuit.new(2) do |c|
|
| 40 |
+
c.h 0
|
| 41 |
+
c.cnot 0, 1
|
| 42 |
+
end
|
| 43 |
+
puts c.to_s
|
| 44 |
+
|
| 45 |
+
res = Mercury::Simulator.new.run(c, shots: 1000, seed: 42)
|
| 46 |
+
p res.counts # {"00"=>..., "11"=>...}
|
| 47 |
+
|
| 48 |
+
# CPSC conservation check (mirrors src/cpsc/analyzer.py)
|
| 49 |
+
p Mercury::CPSC.analyze(c)
|
| 50 |
+
# => {:admitted=>false, :rejections=>[{:gate=>"H[0]", ...}, ...]}
|
| 51 |
+
# H and CNOT do not preserve the Z2 parity sector — rejected, as designed.
|
| 52 |
+
|
| 53 |
+
# Trotterized scattering: H = -J ΣZ_i Z_{i+1} + λ ΣX_i (periodic)
|
| 54 |
+
trotter = Mercury::CPSC.trotter_circuit(4, j: 1.0, lam: 0.0, time: 0.4, steps: 8)
|
| 55 |
+
psi = Mercury::Simulator.new.run(trotter, initial: "0011").statevector
|
| 56 |
+
p Mercury::CPSC.leakage(psi, 4, Mercury::CPSC.parity_of(0b0011, 4)) # => 0.0
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Run the tests:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
cd frontend && ruby -Ilib test/test_mercury.rb
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Web console
|
| 66 |
+
|
| 67 |
+
Build, then serve the `web/` directory over HTTP (WASM requires it):
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
cd frontend/web && ./build.sh
|
| 71 |
+
ruby -run -e httpd -- . -p 8123
|
| 72 |
+
# open http://localhost:8123/qsim.html
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
The console boots Ruby 3.2 (ruby.wasm, CDN) and evaluates the verbatim
|
| 76 |
+
`mercury_bundle.rb`. Features:
|
| 77 |
+
|
| 78 |
+
- circuit script editor (one op per line) + gate palette + presets
|
| 79 |
+
(Bell, GHZ-3, QFT-3, Trotter scattering, adversarial X)
|
| 80 |
+
- Unicode circuit diagram, measurement histogram, statevector and
|
| 81 |
+
probability tables
|
| 82 |
+
- CPSC panel: compile-time admission verdict, parity leakage,
|
| 83 |
+
energy expectation — the analyzer rejects illegal gates before
|
| 84 |
+
simulation, never post-selects after
|
| 85 |
+
|
| 86 |
+
To publish on GitHub Pages, copy `qsim.html` and `mercury_bundle.rb`
|
| 87 |
+
side-by-side into `docs/`.
|
| 88 |
+
|
| 89 |
+
## Notes
|
| 90 |
+
|
| 91 |
+
- Little-endian: qubit `q` is bit `q` of the basis index, matching the
|
| 92 |
+
Python prototype's convention.
|
| 93 |
+
- At λ=0 the Trotter terms commute, so the Trotter circuit is exact:
|
| 94 |
+
verified bit-identical against `cpsc.channel.evolve_in_sector`
|
| 95 |
+
(max |Δ| = 0.0, n=4, t=0.4).
|
| 96 |
+
- At λ≠0 the transverse field genuinely mixes parity sectors; the
|
| 97 |
+
console reports the honest leakage instead of projecting it away.
|
| 98 |
+
- `Mercury::CPSC.prepare` was removed in favor of `initial:` basis
|
| 99 |
+
strings so state preparation never smuggles X gates past the analyzer.
|
| 100 |
+
|
| 101 |
+
## License
|
| 102 |
+
|
| 103 |
+
Dual strict copyleft: `LicenseRef-CPSC-ESCL-1.0` OR `AGPL-3.0-only`.
|
| 104 |
+
See the repository root `LICENSE`, `NOTICE`, `LICENSES/`.
|
frontend/lib/mercury.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
# Mercury: a dependency-free Ruby quantum circuit library and full
|
| 15 |
+
# statevector simulator, built as the circuit front end for CPSC-QFT.
|
| 16 |
+
require_relative "mercury/version"
|
| 17 |
+
require_relative "mercury/gates"
|
| 18 |
+
require_relative "mercury/circuit"
|
| 19 |
+
require_relative "mercury/simulator"
|
| 20 |
+
require_relative "mercury/mini_json"
|
| 21 |
+
require_relative "mercury/cpsc"
|
frontend/lib/mercury/circuit.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
module Mercury
|
| 15 |
+
# A quantum circuit: an ordered list of gate operations on n qubits.
|
| 16 |
+
#
|
| 17 |
+
# c = Mercury::Circuit.new(2) do |c|
|
| 18 |
+
# c.h 0
|
| 19 |
+
# c.cnot 0, 1
|
| 20 |
+
# c.measure_all
|
| 21 |
+
# end
|
| 22 |
+
#
|
| 23 |
+
# Multi-qubit gate qubit order follows the Gates convention: for
|
| 24 |
+
# cnot(control, target) the DSL takes (control, target) and stores
|
| 25 |
+
# [target, control] so matrix bit 0 is the target. Same for ccx:
|
| 26 |
+
# ccx(c1, c2, target) stores [target, c1, c2].
|
| 27 |
+
class Circuit
|
| 28 |
+
Op = Struct.new(:gate, :qubits, :params, :classical)
|
| 29 |
+
|
| 30 |
+
attr_reader :n, :ops
|
| 31 |
+
|
| 32 |
+
DSL_REORDER = { cnot: true, ccx: true }.freeze
|
| 33 |
+
|
| 34 |
+
def initialize(n)
|
| 35 |
+
raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1
|
| 36 |
+
@n = n
|
| 37 |
+
@ops = []
|
| 38 |
+
yield self if block_given?
|
| 39 |
+
end
|
| 40 |
+
|
| 41 |
+
def add(gate, *qubits, params: [])
|
| 42 |
+
gate = gate.to_sym
|
| 43 |
+
arity = Gates.arity(gate)
|
| 44 |
+
raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity
|
| 45 |
+
raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length
|
| 46 |
+
qubits.each do |q|
|
| 47 |
+
raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n
|
| 48 |
+
end
|
| 49 |
+
stored = DSL_REORDER[gate] ? qubits.reverse : qubits
|
| 50 |
+
@ops << Op.new(gate, stored, params, nil)
|
| 51 |
+
self
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
# Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry.
|
| 55 |
+
(Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname|
|
| 56 |
+
define_method(gname) do |*args|
|
| 57 |
+
need = Gates.param_count(gname)
|
| 58 |
+
arity = Gates.arity(gname)
|
| 59 |
+
raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity
|
| 60 |
+
params = args.first(need)
|
| 61 |
+
qubits = args.last(arity)
|
| 62 |
+
add(gname, *qubits, params: params)
|
| 63 |
+
end
|
| 64 |
+
end
|
| 65 |
+
|
| 66 |
+
def measure(qubit, clbit = nil)
|
| 67 |
+
raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n
|
| 68 |
+
@ops << Op.new(:measure, [qubit], [], clbit || qubit)
|
| 69 |
+
self
|
| 70 |
+
end
|
| 71 |
+
|
| 72 |
+
def measure_all
|
| 73 |
+
@n.times { |q| measure(q, q) }
|
| 74 |
+
self
|
| 75 |
+
end
|
| 76 |
+
|
| 77 |
+
def depth
|
| 78 |
+
@ops.length
|
| 79 |
+
end
|
| 80 |
+
|
| 81 |
+
# Minimal Unicode circuit diagram.
|
| 82 |
+
def to_s
|
| 83 |
+
cols = @ops.map { |op| column(op) }
|
| 84 |
+
rows = (0...@n).map do |q|
|
| 85 |
+
label = "q#{q}: "
|
| 86 |
+
body = cols.map { |col| col[q] }.join("")
|
| 87 |
+
label + body
|
| 88 |
+
end
|
| 89 |
+
rows.join("\n")
|
| 90 |
+
end
|
| 91 |
+
|
| 92 |
+
private
|
| 93 |
+
|
| 94 |
+
def column(op)
|
| 95 |
+
cells = Array.new(@n, "────────")
|
| 96 |
+
case op.gate
|
| 97 |
+
when :measure
|
| 98 |
+
cells[op.qubits[0]] = "───╫M───"
|
| 99 |
+
else
|
| 100 |
+
qs = op.qubits
|
| 101 |
+
tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase
|
| 102 |
+
if qs.length == 1
|
| 103 |
+
cells[qs[0]] = "───#{tag[0, 2]}───"
|
| 104 |
+
else
|
| 105 |
+
# multi-qubit: control dots on qs[1..], box on qs[0], rail between
|
| 106 |
+
lo, hi = qs.minmax
|
| 107 |
+
(lo..hi).each { |q| cells[q] = "───┼────" }
|
| 108 |
+
cells[qs[0]] = "───#{tag[0, 2]}───"
|
| 109 |
+
qs[1..].each { |q| cells[q] = "───●────" }
|
| 110 |
+
end
|
| 111 |
+
end
|
| 112 |
+
cells
|
| 113 |
+
end
|
| 114 |
+
end
|
| 115 |
+
end
|
frontend/lib/mercury/cpsc.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
# Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a
|
| 15 |
+
# Trotterized scattering evolution built as a Mercury circuit.
|
| 16 |
+
#
|
| 17 |
+
# Gate-kind semantics (identical to the Python original):
|
| 18 |
+
# - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z.
|
| 19 |
+
# - "parity_even": preserves prod Z (Z2 parity) but may change sum Z.
|
| 20 |
+
# - "illegal": odd X/Y support; changes Z-parity. Rejected.
|
| 21 |
+
module Mercury
|
| 22 |
+
module CPSC
|
| 23 |
+
# Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds.
|
| 24 |
+
KINDS = {
|
| 25 |
+
i: "magnetization", z: "magnetization", s: "magnetization",
|
| 26 |
+
t: "magnetization", sdg: "magnetization", tdg: "magnetization",
|
| 27 |
+
rz: "magnetization", cz: "magnetization", rzz: "magnetization",
|
| 28 |
+
cp: "magnetization",
|
| 29 |
+
xx: "parity_even",
|
| 30 |
+
x: "illegal", y: "illegal", h: "illegal",
|
| 31 |
+
rx: "illegal", ry: "illegal", u: "illegal",
|
| 32 |
+
cnot: "illegal", swap: "illegal", ccx: "illegal"
|
| 33 |
+
}.freeze
|
| 34 |
+
|
| 35 |
+
GateSpec = Struct.new(:name, :qubits, :kind)
|
| 36 |
+
Rejection = Struct.new(:spec, :reason)
|
| 37 |
+
|
| 38 |
+
class CompilationRejected < StandardError; end
|
| 39 |
+
|
| 40 |
+
class SymmetryAnalyzer
|
| 41 |
+
def initialize(n, conserve_magnetization: false)
|
| 42 |
+
raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1
|
| 43 |
+
@n = n
|
| 44 |
+
@conserve_magnetization = conserve_magnetization
|
| 45 |
+
end
|
| 46 |
+
|
| 47 |
+
def check(specs)
|
| 48 |
+
specs.flat_map do |spec|
|
| 49 |
+
reason = reject_reason(spec)
|
| 50 |
+
reason ? [Rejection.new(spec, reason)] : []
|
| 51 |
+
end
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
def admit(specs)
|
| 55 |
+
rejected = check(specs)
|
| 56 |
+
unless rejected.empty?
|
| 57 |
+
detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ")
|
| 58 |
+
raise CompilationRejected, detail
|
| 59 |
+
end
|
| 60 |
+
specs.dup
|
| 61 |
+
end
|
| 62 |
+
|
| 63 |
+
private
|
| 64 |
+
|
| 65 |
+
def reject_reason(spec)
|
| 66 |
+
spec.qubits.each do |q|
|
| 67 |
+
return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1)
|
| 68 |
+
end
|
| 69 |
+
case spec.kind
|
| 70 |
+
when "illegal"
|
| 71 |
+
"odd X/Y support changes Z-parity; forbidden sector"
|
| 72 |
+
when "parity_even", "magnetization"
|
| 73 |
+
nil
|
| 74 |
+
else
|
| 75 |
+
"unknown gate kind #{spec.kind}"
|
| 76 |
+
end
|
| 77 |
+
end
|
| 78 |
+
end
|
| 79 |
+
|
| 80 |
+
# Classify a Mercury::Circuit::Op into a GateSpec for the analyzer.
|
| 81 |
+
def self.classify(op)
|
| 82 |
+
kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" }
|
| 83 |
+
label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})"
|
| 84 |
+
GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind)
|
| 85 |
+
end
|
| 86 |
+
|
| 87 |
+
# Run the analyzer over a whole circuit. Returns
|
| 88 |
+
# { admitted: bool, rejections: [{gate, reason}] }.
|
| 89 |
+
def self.analyze(circuit, conserve_magnetization: false)
|
| 90 |
+
analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization)
|
| 91 |
+
specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) }
|
| 92 |
+
rejections = analyzer.check(specs)
|
| 93 |
+
{
|
| 94 |
+
admitted: rejections.empty?,
|
| 95 |
+
rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } }
|
| 96 |
+
}
|
| 97 |
+
end
|
| 98 |
+
|
| 99 |
+
# First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i
|
| 100 |
+
# (periodic), matching src/cpsc/hamiltonian.py. Per step dt:
|
| 101 |
+
# prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i).
|
| 102 |
+
def self.trotter_circuit(n, j:, lam:, time:, steps:)
|
| 103 |
+
raise ArgumentError, "steps must be >= 1" unless steps >= 1
|
| 104 |
+
dt = time.to_f / steps
|
| 105 |
+
Circuit.new(n) do |c|
|
| 106 |
+
steps.times do
|
| 107 |
+
(0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) }
|
| 108 |
+
(0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero?
|
| 109 |
+
end
|
| 110 |
+
end
|
| 111 |
+
end
|
| 112 |
+
|
| 113 |
+
def self.parity_of(basis, n)
|
| 114 |
+
basis.to_s(2).count("1") & 1
|
| 115 |
+
end
|
| 116 |
+
|
| 117 |
+
def self.magnetization_of(basis, n)
|
| 118 |
+
mag = 0
|
| 119 |
+
n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 }
|
| 120 |
+
mag
|
| 121 |
+
end
|
| 122 |
+
|
| 123 |
+
# Probability mass outside the input Z2-parity sector.
|
| 124 |
+
def self.leakage(amps, n, parity)
|
| 125 |
+
wrong = 0.0
|
| 126 |
+
amps.each_with_index do |a, b|
|
| 127 |
+
wrong += a.abs**2 if parity_of(b, n) != parity
|
| 128 |
+
end
|
| 129 |
+
wrong
|
| 130 |
+
end
|
| 131 |
+
|
| 132 |
+
# <psi|H|psi> for the transverse-field Ising H, computed directly.
|
| 133 |
+
def self.energy_expectation(amps, n, j:, lam:)
|
| 134 |
+
dim = 1 << n
|
| 135 |
+
e = 0.0
|
| 136 |
+
dim.times do |s|
|
| 137 |
+
p = amps[s].abs**2
|
| 138 |
+
next if p.zero?
|
| 139 |
+
zz = 0.0
|
| 140 |
+
n.times do |i|
|
| 141 |
+
zi = ((s >> i) & 1).zero? ? 1.0 : -1.0
|
| 142 |
+
zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0
|
| 143 |
+
zz += zi * zj
|
| 144 |
+
end
|
| 145 |
+
e += p * (-j * zz)
|
| 146 |
+
end
|
| 147 |
+
unless lam.zero?
|
| 148 |
+
x = 0.0
|
| 149 |
+
n.times do |i|
|
| 150 |
+
bit = 1 << i
|
| 151 |
+
dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real }
|
| 152 |
+
end
|
| 153 |
+
e += lam * x
|
| 154 |
+
end
|
| 155 |
+
e
|
| 156 |
+
end
|
| 157 |
+
|
| 158 |
+
end
|
| 159 |
+
end
|
frontend/lib/mercury/gates.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
# Gate matrices for Mercury. Little-endian convention: in a k-qubit gate
|
| 15 |
+
# applied to qubits [q0, q1, ...], matrix row/column index bit j (value
|
| 16 |
+
# 2**j) is the computational-basis state of qubit q_j. For a two-qubit
|
| 17 |
+
# gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0).
|
| 18 |
+
module Mercury
|
| 19 |
+
module Gates
|
| 20 |
+
ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze
|
| 21 |
+
TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze
|
| 22 |
+
THREE_QUBIT = %i[ccx].freeze
|
| 23 |
+
|
| 24 |
+
PARAM_COUNTS = {
|
| 25 |
+
i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0,
|
| 26 |
+
rx: 1, ry: 1, rz: 1, u: 3,
|
| 27 |
+
cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0,
|
| 28 |
+
ccx: 0
|
| 29 |
+
}.freeze
|
| 30 |
+
|
| 31 |
+
def self.arity(name)
|
| 32 |
+
return 1 if ONE_QUBIT.include?(name)
|
| 33 |
+
return 2 if TWO_QUBIT.include?(name)
|
| 34 |
+
return 3 if THREE_QUBIT.include?(name)
|
| 35 |
+
raise ArgumentError, "unknown gate #{name}"
|
| 36 |
+
end
|
| 37 |
+
|
| 38 |
+
def self.param_count(name)
|
| 39 |
+
PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" }
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
def self.matrix(name, params = [])
|
| 43 |
+
need = param_count(name)
|
| 44 |
+
raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need
|
| 45 |
+
|
| 46 |
+
case name
|
| 47 |
+
when :i then [[c(1), c(0)], [c(0), c(1)]]
|
| 48 |
+
when :x then [[c(0), c(1)], [c(1), c(0)]]
|
| 49 |
+
when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]]
|
| 50 |
+
when :z then [[c(1), c(0)], [c(0), c(-1)]]
|
| 51 |
+
when :h
|
| 52 |
+
s = Math.sqrt(0.5)
|
| 53 |
+
[[c(s), c(s)], [c(s), c(-s)]]
|
| 54 |
+
when :s then [[c(1), c(0)], [c(0), c(0, 1)]]
|
| 55 |
+
when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]]
|
| 56 |
+
when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]]
|
| 57 |
+
when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]]
|
| 58 |
+
when :rx
|
| 59 |
+
th = params[0] / 2.0
|
| 60 |
+
co = c(Math.cos(th)); si = c(0, -Math.sin(th))
|
| 61 |
+
[[co, si], [si, co]]
|
| 62 |
+
when :ry
|
| 63 |
+
th = params[0] / 2.0
|
| 64 |
+
[[c(Math.cos(th)), c(-Math.sin(th))],
|
| 65 |
+
[c(Math.sin(th)), c(Math.cos(th))]]
|
| 66 |
+
when :rz
|
| 67 |
+
th = params[0] / 2.0
|
| 68 |
+
[[phase(-th), c(0)], [c(0), phase(th)]]
|
| 69 |
+
when :u
|
| 70 |
+
th, ph, la = params
|
| 71 |
+
co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0))
|
| 72 |
+
[[co, c(-1) * phase(la) * si],
|
| 73 |
+
[phase(ph) * si, phase(ph + la) * co]]
|
| 74 |
+
when :cnot
|
| 75 |
+
# qubits [target, control]; flips target iff control == 1
|
| 76 |
+
[[c(1), c(0), c(0), c(0)],
|
| 77 |
+
[c(0), c(1), c(0), c(0)],
|
| 78 |
+
[c(0), c(0), c(0), c(1)],
|
| 79 |
+
[c(0), c(0), c(1), c(0)]]
|
| 80 |
+
when :cz
|
| 81 |
+
[[c(1), c(0), c(0), c(0)],
|
| 82 |
+
[c(0), c(1), c(0), c(0)],
|
| 83 |
+
[c(0), c(0), c(1), c(0)],
|
| 84 |
+
[c(0), c(0), c(0), c(-1)]]
|
| 85 |
+
when :cp
|
| 86 |
+
[[c(1), c(0), c(0), c(0)],
|
| 87 |
+
[c(0), c(1), c(0), c(0)],
|
| 88 |
+
[c(0), c(0), c(1), c(0)],
|
| 89 |
+
[c(0), c(0), c(0), phase(params[0])]]
|
| 90 |
+
when :swap
|
| 91 |
+
[[c(1), c(0), c(0), c(0)],
|
| 92 |
+
[c(0), c(0), c(1), c(0)],
|
| 93 |
+
[c(0), c(1), c(0), c(0)],
|
| 94 |
+
[c(0), c(0), c(0), c(1)]]
|
| 95 |
+
when :rzz
|
| 96 |
+
# exp(-i*theta/2 * ZxZ)
|
| 97 |
+
a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0)
|
| 98 |
+
[[a, c(0), c(0), c(0)],
|
| 99 |
+
[c(0), b, c(0), c(0)],
|
| 100 |
+
[c(0), c(0), b, c(0)],
|
| 101 |
+
[c(0), c(0), c(0), a]]
|
| 102 |
+
when :xx
|
| 103 |
+
# XxX: parity-even pair used by the CPSC analyzer
|
| 104 |
+
[[c(0), c(0), c(0), c(1)],
|
| 105 |
+
[c(0), c(0), c(1), c(0)],
|
| 106 |
+
[c(0), c(1), c(0), c(0)],
|
| 107 |
+
[c(1), c(0), c(0), c(0)]]
|
| 108 |
+
when :ccx
|
| 109 |
+
# qubits [target, control1, control2]; flips target iff both controls == 1
|
| 110 |
+
m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } }
|
| 111 |
+
m[6][6] = c(0); m[7][7] = c(0)
|
| 112 |
+
m[6][7] = c(1); m[7][6] = c(1)
|
| 113 |
+
m
|
| 114 |
+
end
|
| 115 |
+
end
|
| 116 |
+
|
| 117 |
+
def self.c(re, im = 0)
|
| 118 |
+
Complex(re, im)
|
| 119 |
+
end
|
| 120 |
+
|
| 121 |
+
def self.phase(angle)
|
| 122 |
+
Complex(Math.cos(angle), Math.sin(angle))
|
| 123 |
+
end
|
| 124 |
+
private_class_method :c, :phase
|
| 125 |
+
end
|
| 126 |
+
end
|
frontend/lib/mercury/mini_json.rb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
# Minimal JSON parser/generator covering exactly what the Mercury web
|
| 15 |
+
# bridge needs (objects, arrays, strings, numbers, booleans, null).
|
| 16 |
+
# Pure Ruby, no dependencies — safe inside Ruby WASM.
|
| 17 |
+
module Mercury
|
| 18 |
+
module MiniJSON
|
| 19 |
+
class ParseError < StandardError; end
|
| 20 |
+
|
| 21 |
+
def self.parse(str)
|
| 22 |
+
@src = str
|
| 23 |
+
@pos = 0
|
| 24 |
+
skip_ws
|
| 25 |
+
val = parse_value
|
| 26 |
+
skip_ws
|
| 27 |
+
raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length
|
| 28 |
+
val
|
| 29 |
+
end
|
| 30 |
+
|
| 31 |
+
def self.generate(obj)
|
| 32 |
+
case obj
|
| 33 |
+
when Hash
|
| 34 |
+
"{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}"
|
| 35 |
+
when Array
|
| 36 |
+
"[" + obj.map { |v| generate(v) }.join(",") + "]"
|
| 37 |
+
when String
|
| 38 |
+
'"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"'
|
| 39 |
+
when Integer, Float
|
| 40 |
+
raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite?
|
| 41 |
+
obj.to_s
|
| 42 |
+
when true then "true"
|
| 43 |
+
when false then "false"
|
| 44 |
+
when nil then "null"
|
| 45 |
+
else
|
| 46 |
+
raise ParseError, "cannot serialize #{obj.class}"
|
| 47 |
+
end
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
def self.skip_ws
|
| 51 |
+
@pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
def self.parse_value
|
| 55 |
+
case @src[@pos]
|
| 56 |
+
when '"' then parse_string
|
| 57 |
+
when "{" then parse_object
|
| 58 |
+
when "[" then parse_array
|
| 59 |
+
when "t" then literal("true", true)
|
| 60 |
+
when "f" then literal("false", false)
|
| 61 |
+
when "n" then literal("null", nil)
|
| 62 |
+
else parse_number
|
| 63 |
+
end
|
| 64 |
+
end
|
| 65 |
+
|
| 66 |
+
def self.literal(word, val)
|
| 67 |
+
raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word
|
| 68 |
+
@pos += word.length
|
| 69 |
+
val
|
| 70 |
+
end
|
| 71 |
+
|
| 72 |
+
def self.parse_string
|
| 73 |
+
@pos += 1 # opening quote
|
| 74 |
+
out = +""
|
| 75 |
+
while @pos < @src.length
|
| 76 |
+
ch = @src[@pos]
|
| 77 |
+
case ch
|
| 78 |
+
when '"'
|
| 79 |
+
@pos += 1
|
| 80 |
+
return out
|
| 81 |
+
when "\\"
|
| 82 |
+
@pos += 1
|
| 83 |
+
esc = @src[@pos]
|
| 84 |
+
out << case esc
|
| 85 |
+
when '"' then '"'
|
| 86 |
+
when "\\" then "\\"
|
| 87 |
+
when "/" then "/"
|
| 88 |
+
when "b" then "\b"
|
| 89 |
+
when "f" then "\f"
|
| 90 |
+
when "n" then "\n"
|
| 91 |
+
when "r" then "\r"
|
| 92 |
+
when "t" then "\t"
|
| 93 |
+
when "u"
|
| 94 |
+
hex = @src[@pos + 1, 4]
|
| 95 |
+
@pos += 4
|
| 96 |
+
[hex.to_i(16)].pack("U")
|
| 97 |
+
else raise ParseError, "bad escape at #{@pos}"
|
| 98 |
+
end
|
| 99 |
+
@pos += 1
|
| 100 |
+
else
|
| 101 |
+
out << ch
|
| 102 |
+
@pos += 1
|
| 103 |
+
end
|
| 104 |
+
end
|
| 105 |
+
raise ParseError, "unterminated string"
|
| 106 |
+
end
|
| 107 |
+
|
| 108 |
+
def self.parse_object
|
| 109 |
+
@pos += 1
|
| 110 |
+
obj = {}
|
| 111 |
+
skip_ws
|
| 112 |
+
if @src[@pos] == "}"
|
| 113 |
+
@pos += 1
|
| 114 |
+
return obj
|
| 115 |
+
end
|
| 116 |
+
loop do
|
| 117 |
+
skip_ws
|
| 118 |
+
raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"'
|
| 119 |
+
key = parse_string
|
| 120 |
+
skip_ws
|
| 121 |
+
raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":"
|
| 122 |
+
@pos += 1
|
| 123 |
+
skip_ws
|
| 124 |
+
obj[key] = parse_value
|
| 125 |
+
skip_ws
|
| 126 |
+
case @src[@pos]
|
| 127 |
+
when "," then @pos += 1
|
| 128 |
+
when "}" then @pos += 1; break
|
| 129 |
+
else raise ParseError, "expected , or } at #{@pos}"
|
| 130 |
+
end
|
| 131 |
+
end
|
| 132 |
+
obj
|
| 133 |
+
end
|
| 134 |
+
|
| 135 |
+
def self.parse_array
|
| 136 |
+
@pos += 1
|
| 137 |
+
arr = []
|
| 138 |
+
skip_ws
|
| 139 |
+
if @src[@pos] == "]"
|
| 140 |
+
@pos += 1
|
| 141 |
+
return arr
|
| 142 |
+
end
|
| 143 |
+
loop do
|
| 144 |
+
skip_ws
|
| 145 |
+
arr << parse_value
|
| 146 |
+
skip_ws
|
| 147 |
+
case @src[@pos]
|
| 148 |
+
when "," then @pos += 1
|
| 149 |
+
when "]" then @pos += 1; break
|
| 150 |
+
else raise ParseError, "expected , or ] at #{@pos}"
|
| 151 |
+
end
|
| 152 |
+
end
|
| 153 |
+
arr
|
| 154 |
+
end
|
| 155 |
+
|
| 156 |
+
def self.parse_number
|
| 157 |
+
m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)
|
| 158 |
+
raise ParseError, "bad number at #{@pos}" unless m
|
| 159 |
+
@pos += m[0].length
|
| 160 |
+
s = m[0]
|
| 161 |
+
(s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i
|
| 162 |
+
end
|
| 163 |
+
end
|
| 164 |
+
end
|
frontend/lib/mercury/simulator.rb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
module Mercury
|
| 15 |
+
# Full statevector simulator. Little-endian: qubit q is bit q of the
|
| 16 |
+
# basis index, matching the Gates matrix convention.
|
| 17 |
+
class Statevector
|
| 18 |
+
attr_reader :n, :amps
|
| 19 |
+
|
| 20 |
+
# initial: integer basis index, or MSB-first bitstring like "0011".
|
| 21 |
+
def initialize(n, initial: 0)
|
| 22 |
+
raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1
|
| 23 |
+
initial = Integer(initial, 2) if initial.is_a?(String)
|
| 24 |
+
raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1)
|
| 25 |
+
@n = n
|
| 26 |
+
@dim = 1 << n
|
| 27 |
+
@amps = Array.new(@dim, Complex(0, 0))
|
| 28 |
+
@amps[initial] = Complex(1, 0)
|
| 29 |
+
end
|
| 30 |
+
|
| 31 |
+
def dim
|
| 32 |
+
@dim
|
| 33 |
+
end
|
| 34 |
+
|
| 35 |
+
def norm
|
| 36 |
+
Math.sqrt(@amps.sum { |a| a.abs**2 })
|
| 37 |
+
end
|
| 38 |
+
|
| 39 |
+
def apply(gate, qubits, params = [])
|
| 40 |
+
matrix = Gates.matrix(gate, params)
|
| 41 |
+
case qubits.length
|
| 42 |
+
when 1 then apply_one(matrix, qubits[0])
|
| 43 |
+
when 2 then apply_two(matrix, qubits[0], qubits[1])
|
| 44 |
+
when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2])
|
| 45 |
+
else raise ArgumentError, "unsupported arity #{qubits.length}"
|
| 46 |
+
end
|
| 47 |
+
self
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
def probabilities
|
| 51 |
+
@amps.map { |a| a.abs**2 }
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
# Sample `shots` bitstrings (MSB-first strings) from the distribution.
|
| 55 |
+
def sample(shots, rng = Random.new)
|
| 56 |
+
probs = probabilities
|
| 57 |
+
cumulative = []
|
| 58 |
+
total = 0.0
|
| 59 |
+
probs.each { |p| total += p; cumulative << total }
|
| 60 |
+
Array.new(shots) do
|
| 61 |
+
r = rng.rand * total
|
| 62 |
+
idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1)
|
| 63 |
+
bitstring(idx)
|
| 64 |
+
end
|
| 65 |
+
end
|
| 66 |
+
|
| 67 |
+
# Projective Z measurement of one qubit; collapses the state in place.
|
| 68 |
+
def measure!(qubit, rng = Random.new)
|
| 69 |
+
raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1)
|
| 70 |
+
step = 1 << qubit
|
| 71 |
+
p0 = 0.0
|
| 72 |
+
@amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? }
|
| 73 |
+
outcome = rng.rand < p0 ? 0 : 1
|
| 74 |
+
renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0))
|
| 75 |
+
raise "measurement of zero-probability branch" if renorm.zero?
|
| 76 |
+
@amps.each_with_index do |a, s|
|
| 77 |
+
bit = (s & step).zero? ? 0 : 1
|
| 78 |
+
@amps[s] = bit == outcome ? a / renorm : Complex(0, 0)
|
| 79 |
+
end
|
| 80 |
+
outcome
|
| 81 |
+
end
|
| 82 |
+
|
| 83 |
+
def bitstring(index)
|
| 84 |
+
# MSB-first for display: qubit n-1 ... qubit 0
|
| 85 |
+
(0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join
|
| 86 |
+
end
|
| 87 |
+
|
| 88 |
+
private
|
| 89 |
+
|
| 90 |
+
def apply_one(m, q)
|
| 91 |
+
step = 1 << q
|
| 92 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 93 |
+
(0...@dim).each do |s|
|
| 94 |
+
next unless (s & step).zero?
|
| 95 |
+
i0 = s
|
| 96 |
+
i1 = s | step
|
| 97 |
+
a0 = @amps[i0]
|
| 98 |
+
a1 = @amps[i1]
|
| 99 |
+
nxt[i0] = m[0][0] * a0 + m[0][1] * a1
|
| 100 |
+
nxt[i1] = m[1][0] * a0 + m[1][1] * a1
|
| 101 |
+
end
|
| 102 |
+
@amps = nxt
|
| 103 |
+
end
|
| 104 |
+
|
| 105 |
+
def apply_two(m, q0, q1)
|
| 106 |
+
raise ArgumentError, "q0 == q1" if q0 == q1
|
| 107 |
+
s0 = 1 << q0
|
| 108 |
+
s1 = 1 << q1
|
| 109 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 110 |
+
(0...@dim).each do |s|
|
| 111 |
+
next unless (s & s0).zero? && (s & s1).zero?
|
| 112 |
+
idx = [s, s | s0, s | s1, s | s0 | s1]
|
| 113 |
+
old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])]
|
| 114 |
+
4.times do |r|
|
| 115 |
+
nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3]
|
| 116 |
+
end
|
| 117 |
+
end
|
| 118 |
+
@amps = nxt
|
| 119 |
+
end
|
| 120 |
+
|
| 121 |
+
def apply_three(m, q0, q1, q2)
|
| 122 |
+
raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3
|
| 123 |
+
s0 = 1 << q0
|
| 124 |
+
s1 = 1 << q1
|
| 125 |
+
s2 = 1 << q2
|
| 126 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 127 |
+
(0...@dim).each do |s|
|
| 128 |
+
next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero?
|
| 129 |
+
idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) }
|
| 130 |
+
old = idx.map { |i| @amps[i] }
|
| 131 |
+
8.times do |r|
|
| 132 |
+
acc = Complex(0, 0)
|
| 133 |
+
8.times { |cidx| acc += m[r][cidx] * old[cidx] }
|
| 134 |
+
nxt[idx[r]] = acc
|
| 135 |
+
end
|
| 136 |
+
end
|
| 137 |
+
@amps = nxt
|
| 138 |
+
end
|
| 139 |
+
end
|
| 140 |
+
|
| 141 |
+
# Runs a Circuit. :measure ops collapse immediately; `shots` samples the
|
| 142 |
+
# final distribution.
|
| 143 |
+
class Simulator
|
| 144 |
+
Result = Struct.new(:counts, :statevector, :probabilities, :measured)
|
| 145 |
+
|
| 146 |
+
def run(circuit, shots: 0, seed: nil, initial: 0)
|
| 147 |
+
rng = seed.nil? ? Random.new : Random.new(seed)
|
| 148 |
+
sv = Statevector.new(circuit.n, initial: initial)
|
| 149 |
+
measured = {}
|
| 150 |
+
circuit.ops.each do |op|
|
| 151 |
+
if op.gate == :measure
|
| 152 |
+
measured[op.classical] = sv.measure!(op.qubits[0], rng)
|
| 153 |
+
else
|
| 154 |
+
sv.apply(op.gate, op.qubits, op.params)
|
| 155 |
+
end
|
| 156 |
+
end
|
| 157 |
+
counts = {}
|
| 158 |
+
if shots.positive?
|
| 159 |
+
sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 }
|
| 160 |
+
end
|
| 161 |
+
Result.new(counts, sv.amps.dup, sv.probabilities, measured)
|
| 162 |
+
end
|
| 163 |
+
end
|
| 164 |
+
end
|
frontend/lib/mercury/version.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
module Mercury
|
| 15 |
+
VERSION = "0.3.0"
|
| 16 |
+
end
|
frontend/test/test_mercury.rb
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
require "minitest/autorun"
|
| 15 |
+
require_relative "../lib/mercury"
|
| 16 |
+
|
| 17 |
+
def assert_unitary(name, params = [])
|
| 18 |
+
m = Mercury::Gates.matrix(name, params)
|
| 19 |
+
d = m.length
|
| 20 |
+
d.times do |r|
|
| 21 |
+
d.times do |c|
|
| 22 |
+
acc = Complex(0, 0)
|
| 23 |
+
d.times { |k| acc += m[r][k] * m[c][k].conj }
|
| 24 |
+
expected = r == c ? Complex(1, 0) : Complex(0, 0)
|
| 25 |
+
assert_in_delta expected.real, acc.real, 1e-12, "#{name} unitary [#{r},#{c}] real"
|
| 26 |
+
assert_in_delta expected.imag, acc.imag, 1e-12, "#{name} unitary [#{r},#{c}] imag"
|
| 27 |
+
end
|
| 28 |
+
end
|
| 29 |
+
end
|
| 30 |
+
|
| 31 |
+
class TestGates < Minitest::Test
|
| 32 |
+
def test_all_gates_unitary
|
| 33 |
+
%i[i x y z h s t sdg tdg].each { |g| assert_unitary(g) }
|
| 34 |
+
%i[rx ry rz].each { |g| [0.0, 0.7, Math::PI, -2.3].each { |t| assert_unitary(g, [t]) } }
|
| 35 |
+
assert_unitary(:u, [0.7, 0.2, -0.5])
|
| 36 |
+
%i[cnot cz swap xx].each { |g| assert_unitary(g) }
|
| 37 |
+
assert_unitary(:cp, [1.1])
|
| 38 |
+
assert_unitary(:rzz, [0.9])
|
| 39 |
+
assert_unitary(:ccx)
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
def test_h_twice_is_identity
|
| 43 |
+
c = Mercury::Circuit.new(1) { |cc| cc.h(0); cc.h(0) }
|
| 44 |
+
res = Mercury::Simulator.new.run(c)
|
| 45 |
+
assert_in_delta 1.0, res.probabilities[0], 1e-12
|
| 46 |
+
end
|
| 47 |
+
|
| 48 |
+
def test_x_flips
|
| 49 |
+
c = Mercury::Circuit.new(1) { |cc| cc.x(0) }
|
| 50 |
+
res = Mercury::Simulator.new.run(c)
|
| 51 |
+
assert_in_delta 1.0, res.probabilities[1], 1e-12
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
def test_rx_pi_flips
|
| 55 |
+
c = Mercury::Circuit.new(1) { |cc| cc.rx(Math::PI, 0) }
|
| 56 |
+
res = Mercury::Simulator.new.run(c)
|
| 57 |
+
assert_in_delta 1.0, res.probabilities[1], 1e-12
|
| 58 |
+
end
|
| 59 |
+
|
| 60 |
+
def test_cnot_truth_table
|
| 61 |
+
# control=1, target=0 -> target flips; control as qubit 1, target qubit 0
|
| 62 |
+
c = Mercury::Circuit.new(2) { |cc| cc.x(1); cc.cnot(1, 0) }
|
| 63 |
+
res = Mercury::Simulator.new.run(c)
|
| 64 |
+
assert_in_delta 1.0, res.probabilities[0b11], 1e-12
|
| 65 |
+
# control=0 -> no flip
|
| 66 |
+
c2 = Mercury::Circuit.new(2) { |cc| cc.x(0); cc.cnot(1, 0) }
|
| 67 |
+
res2 = Mercury::Simulator.new.run(c2)
|
| 68 |
+
assert_in_delta 1.0, res2.probabilities[0b01], 1e-12
|
| 69 |
+
end
|
| 70 |
+
|
| 71 |
+
def test_toffoli
|
| 72 |
+
c = Mercury::Circuit.new(3) { |cc| cc.x(1); cc.x(2); cc.ccx(1, 2, 0) }
|
| 73 |
+
res = Mercury::Simulator.new.run(c)
|
| 74 |
+
assert_in_delta 1.0, res.probabilities[0b111], 1e-12
|
| 75 |
+
c2 = Mercury::Circuit.new(3) { |cc| cc.x(1); cc.ccx(1, 2, 0) }
|
| 76 |
+
res2 = Mercury::Simulator.new.run(c2)
|
| 77 |
+
assert_in_delta 1.0, res2.probabilities[0b010], 1e-12
|
| 78 |
+
end
|
| 79 |
+
end
|
| 80 |
+
|
| 81 |
+
class TestSimulator < Minitest::Test
|
| 82 |
+
def test_bell_state
|
| 83 |
+
c = Mercury::Circuit.new(2) { |cc| cc.h(0); cc.cnot(0, 1) }
|
| 84 |
+
res = Mercury::Simulator.new.run(c)
|
| 85 |
+
p = res.probabilities
|
| 86 |
+
assert_in_delta 0.5, p[0b00], 1e-12
|
| 87 |
+
assert_in_delta 0.5, p[0b11], 1e-12
|
| 88 |
+
assert_in_delta 0.0, p[0b01], 1e-12
|
| 89 |
+
assert_in_delta 0.0, p[0b10], 1e-12
|
| 90 |
+
end
|
| 91 |
+
|
| 92 |
+
def test_ghz3
|
| 93 |
+
c = Mercury::Circuit.new(3) { |cc| cc.h(0); cc.cnot(0, 1); cc.cnot(1, 2) }
|
| 94 |
+
res = Mercury::Simulator.new.run(c)
|
| 95 |
+
p = res.probabilities
|
| 96 |
+
assert_in_delta 0.5, p[0], 1e-12
|
| 97 |
+
assert_in_delta 0.5, p[0b111], 1e-12
|
| 98 |
+
end
|
| 99 |
+
|
| 100 |
+
def test_qft_zero_is_uniform
|
| 101 |
+
n = 3
|
| 102 |
+
c = Mercury::Circuit.new(n)
|
| 103 |
+
n.times do |j|
|
| 104 |
+
c.h(j)
|
| 105 |
+
((j + 1)...n).each { |k| c.cp(Math::PI / (1 << (k - j)), k, j) }
|
| 106 |
+
end
|
| 107 |
+
(0...(n / 2)).each { |j| c.swap(j, n - 1 - j) }
|
| 108 |
+
res = Mercury::Simulator.new.run(c)
|
| 109 |
+
res.probabilities.each { |p| assert_in_delta 1.0 / 8, p, 1e-12 }
|
| 110 |
+
end
|
| 111 |
+
|
| 112 |
+
def test_measurement_collapses_then_samples
|
| 113 |
+
# measure ops collapse first; sampling a collapsed state is deterministic
|
| 114 |
+
c = Mercury::Circuit.new(2) { |cc| cc.h(0); cc.cnot(0, 1); cc.measure_all }
|
| 115 |
+
res = Mercury::Simulator.new.run(c, shots: 2000, seed: 7)
|
| 116 |
+
assert_equal 2000, res.counts.values.sum
|
| 117 |
+
assert_equal 1, res.counts.keys.length, "collapsed state samples one outcome"
|
| 118 |
+
assert_equal 1, res.measured.values.uniq.length, "Bell pair bits must agree"
|
| 119 |
+
end
|
| 120 |
+
|
| 121 |
+
def test_sampling_statistics_no_measure_ops
|
| 122 |
+
c = Mercury::Circuit.new(2) { |cc| cc.h(0); cc.cnot(0, 1) }
|
| 123 |
+
res = Mercury::Simulator.new.run(c, shots: 2000, seed: 7)
|
| 124 |
+
assert_equal 2000, res.counts.values.sum
|
| 125 |
+
assert res.counts.keys.all? { |b| %w[00 11].include?(b) }, "only 00/11, got #{res.counts.keys}"
|
| 126 |
+
assert_in_delta 1000, res.counts["00"], 150
|
| 127 |
+
end
|
| 128 |
+
|
| 129 |
+
def test_norm_preserved_deep_circuit
|
| 130 |
+
rng = Random.new(3)
|
| 131 |
+
c = Mercury::Circuit.new(5)
|
| 132 |
+
60.times do
|
| 133 |
+
g = %i[h x y z s t rx ry rz].sample(random: rng)
|
| 134 |
+
q = rng.rand(5)
|
| 135 |
+
if %i[rx ry rz].include?(g)
|
| 136 |
+
c.add(g, q, params: [rng.rand * 2 * Math::PI])
|
| 137 |
+
else
|
| 138 |
+
c.add(g, q)
|
| 139 |
+
end
|
| 140 |
+
end
|
| 141 |
+
10.times do
|
| 142 |
+
a, b = rng.rand(5), rng.rand(5)
|
| 143 |
+
redo if a == b
|
| 144 |
+
c.cnot(a, b)
|
| 145 |
+
end
|
| 146 |
+
res = Mercury::Simulator.new.run(c)
|
| 147 |
+
assert_in_delta 1.0, res.probabilities.sum, 1e-9
|
| 148 |
+
end
|
| 149 |
+
|
| 150 |
+
def test_circuit_validation
|
| 151 |
+
assert_raises(ArgumentError) { Mercury::Circuit.new(2) { |c| c.h(2) } }
|
| 152 |
+
assert_raises(ArgumentError) { Mercury::Circuit.new(2) { |c| c.cnot(0, 0) } }
|
| 153 |
+
assert_raises(ArgumentError) { Mercury::Circuit.new(2).add(:nope, 0) }
|
| 154 |
+
end
|
| 155 |
+
|
| 156 |
+
def test_initial_bitstring
|
| 157 |
+
res = Mercury::Simulator.new.run(Mercury::Circuit.new(4), initial: "1010")
|
| 158 |
+
assert_in_delta 1.0, res.probabilities[0b1010], 1e-12
|
| 159 |
+
end
|
| 160 |
+
end
|
| 161 |
+
|
| 162 |
+
class TestCPSC < Minitest::Test
|
| 163 |
+
def test_forbidden_bit_flip_rejected
|
| 164 |
+
# mirrors tests/test_adversarial.py::test_forbidden_bit_flip_rejected_at_compile_time
|
| 165 |
+
c = Mercury::Circuit.new(4) { |cc| cc.rzz(0.5, 0, 1); cc.x(2); cc.rzz(0.5, 2, 3) }
|
| 166 |
+
verdict = Mercury::CPSC.analyze(c)
|
| 167 |
+
refute verdict[:admitted]
|
| 168 |
+
assert_equal 1, verdict[:rejections].length
|
| 169 |
+
assert_match(/X/, verdict[:rejections][0][:gate])
|
| 170 |
+
assert_match(/parity/i, verdict[:rejections][0][:reason])
|
| 171 |
+
end
|
| 172 |
+
|
| 173 |
+
def test_legal_circuit_admitted
|
| 174 |
+
c = Mercury::Circuit.new(4) { |cc| cc.rzz(0.5, 0, 1); cc.rzz(0.5, 2, 3); cc.xx(1, 2) }
|
| 175 |
+
verdict = Mercury::CPSC.analyze(c)
|
| 176 |
+
assert verdict[:admitted], "expected admission, got #{verdict[:rejections]}"
|
| 177 |
+
end
|
| 178 |
+
|
| 179 |
+
def test_qubit_out_of_range_rejected
|
| 180 |
+
analyzer = Mercury::CPSC::SymmetryAnalyzer.new(2)
|
| 181 |
+
spec = Mercury::CPSC::GateSpec.new("Z[5]", [5], "magnetization")
|
| 182 |
+
rej = analyzer.check([spec])
|
| 183 |
+
assert_equal 1, rej.length
|
| 184 |
+
assert_match(/outside lattice/, rej[0].reason)
|
| 185 |
+
end
|
| 186 |
+
|
| 187 |
+
def test_admit_raises_with_detail
|
| 188 |
+
analyzer = Mercury::CPSC::SymmetryAnalyzer.new(4)
|
| 189 |
+
bad = Mercury::CPSC::GateSpec.new("X[2]", [2], "illegal")
|
| 190 |
+
err = assert_raises(Mercury::CPSC::CompilationRejected) { analyzer.admit([bad]) }
|
| 191 |
+
assert_match(/X\[2\]/, err.message)
|
| 192 |
+
end
|
| 193 |
+
|
| 194 |
+
def test_trotter_lambda_zero_exact_sector
|
| 195 |
+
n = 4
|
| 196 |
+
init = 0b0011
|
| 197 |
+
trotter = Mercury::CPSC.trotter_circuit(n, j: 1.0, lam: 0.0, time: 0.4, steps: 8)
|
| 198 |
+
res = Mercury::Simulator.new.run(trotter, initial: init)
|
| 199 |
+
parity = Mercury::CPSC.parity_of(init, n)
|
| 200 |
+
assert_in_delta 0.0, Mercury::CPSC.leakage(res.statevector, n, parity), 1e-12
|
| 201 |
+
assert_in_delta 1.0, res.probabilities.sum, 1e-12
|
| 202 |
+
# magnetization conserved: only basis states with mag 0 carry weight
|
| 203 |
+
res.statevector.each_with_index do |a, b|
|
| 204 |
+
next if a.abs < 1e-12
|
| 205 |
+
assert_equal 0, Mercury::CPSC.magnetization_of(b, n), "basis #{b.to_s(2)} leaked magnetically"
|
| 206 |
+
end
|
| 207 |
+
end
|
| 208 |
+
|
| 209 |
+
def test_trotter_lambda_nonzero_honest_leakage
|
| 210 |
+
n = 3
|
| 211 |
+
init = 0b001
|
| 212 |
+
trotter = Mercury::CPSC.trotter_circuit(n, j: 1.0, lam: 0.5, time: 0.3, steps: 12)
|
| 213 |
+
res = Mercury::Simulator.new.run(trotter, initial: init)
|
| 214 |
+
assert_in_delta 1.0, res.probabilities.sum, 1e-12
|
| 215 |
+
leak = Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init, n))
|
| 216 |
+
assert leak > 0.0, "transverse field must mix parity sectors"
|
| 217 |
+
assert leak < 0.5, "leakage #{leak} implausibly large for small evolution"
|
| 218 |
+
end
|
| 219 |
+
|
| 220 |
+
def test_energy_conserved_lambda_zero
|
| 221 |
+
n = 4
|
| 222 |
+
init = 0b0011
|
| 223 |
+
e0 = Mercury::CPSC.energy_expectation(
|
| 224 |
+
Mercury::Simulator.new.run(Mercury::Circuit.new(n), initial: init).statevector,
|
| 225 |
+
n, j: 1.0, lam: 0.0
|
| 226 |
+
)
|
| 227 |
+
trotter = Mercury::CPSC.trotter_circuit(n, j: 1.0, lam: 0.0, time: 0.4, steps: 8)
|
| 228 |
+
e1 = Mercury::CPSC.energy_expectation(
|
| 229 |
+
Mercury::Simulator.new.run(trotter, initial: init).statevector,
|
| 230 |
+
n, j: 1.0, lam: 0.0
|
| 231 |
+
)
|
| 232 |
+
assert_in_delta e0, e1, 1e-9
|
| 233 |
+
end
|
| 234 |
+
|
| 235 |
+
def test_xx_pair_preserves_parity_simulation
|
| 236 |
+
c = Mercury::Circuit.new(3) { |cc| cc.xx(0, 2) }
|
| 237 |
+
res = Mercury::Simulator.new.run(c, initial: 0b001)
|
| 238 |
+
p0 = Mercury::CPSC.parity_of(0b001, 3)
|
| 239 |
+
assert_in_delta 0.0, Mercury::CPSC.leakage(res.statevector, 3, p0), 1e-12
|
| 240 |
+
assert_in_delta 1.0, res.probabilities[0b100], 1e-12
|
| 241 |
+
end
|
| 242 |
+
end
|
| 243 |
+
|
| 244 |
+
class TestMiniJSON < Minitest::Test
|
| 245 |
+
def test_round_trip
|
| 246 |
+
obj = { "n" => 2, "ops" => [["h", [0], []], ["cnot", [0, 1], [0.5]]],
|
| 247 |
+
"flag" => true, "nothing" => nil, "s" => "a\"b\\c" }
|
| 248 |
+
assert_equal obj, Mercury::MiniJSON.parse(Mercury::MiniJSON.generate(obj))
|
| 249 |
+
end
|
| 250 |
+
|
| 251 |
+
def test_numbers
|
| 252 |
+
assert_equal 42, Mercury::MiniJSON.parse("42")
|
| 253 |
+
assert_in_delta 3.14, Mercury::MiniJSON.parse("3.14"), 1e-12
|
| 254 |
+
assert_in_delta(-1.5e-3, Mercury::MiniJSON.parse("-1.5e-3"), 1e-15)
|
| 255 |
+
end
|
| 256 |
+
|
| 257 |
+
def test_bad_input_raises
|
| 258 |
+
assert_raises(Mercury::MiniJSON::ParseError) { Mercury::MiniJSON.parse("{bad}") }
|
| 259 |
+
assert_raises(Mercury::MiniJSON::ParseError) { Mercury::MiniJSON.parse("[1,2") }
|
| 260 |
+
end
|
| 261 |
+
end
|
frontend/web/app.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
# Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into
|
| 15 |
+
# mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript
|
| 16 |
+
# drives it via vm.eval("handle_request('<json>')").
|
| 17 |
+
#
|
| 18 |
+
# Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]],
|
| 19 |
+
# "shots":1024,"seed":42,"cpsc":true,"magnetization":false,
|
| 20 |
+
# "j":1.0,"lam":0.0}
|
| 21 |
+
# Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...},
|
| 22 |
+
# "statevector":[[bits,re,im],...],"norm":1.0,
|
| 23 |
+
# "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}}
|
| 24 |
+
|
| 25 |
+
def bitstring_of(index, n)
|
| 26 |
+
(0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join
|
| 27 |
+
end
|
| 28 |
+
|
| 29 |
+
def handle_request(json_str)
|
| 30 |
+
req = Mercury::MiniJSON.parse(json_str)
|
| 31 |
+
n = req["n"].to_i
|
| 32 |
+
raise "n must be 1..12" unless n.between?(1, 12)
|
| 33 |
+
|
| 34 |
+
c = Mercury::Circuit.new(n)
|
| 35 |
+
(req["ops"] || []).each do |op|
|
| 36 |
+
name = op[0].to_s.to_sym
|
| 37 |
+
qubits = op[1].map(&:to_i)
|
| 38 |
+
params = (op[2] || []).map(&:to_f)
|
| 39 |
+
c.add(name, *qubits, params: params)
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
initial = req.fetch("initial", "0" * n).to_s
|
| 43 |
+
raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/)
|
| 44 |
+
init_idx = Integer(initial, 2)
|
| 45 |
+
|
| 46 |
+
shots = req.fetch("shots", 0).to_i
|
| 47 |
+
seed = req["seed"]
|
| 48 |
+
|
| 49 |
+
res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx)
|
| 50 |
+
|
| 51 |
+
probs = []
|
| 52 |
+
res.probabilities.each_with_index do |p, i|
|
| 53 |
+
probs << [bitstring_of(i, n), p] if p > 1e-9
|
| 54 |
+
end
|
| 55 |
+
probs.sort_by! { |_, p| -p }
|
| 56 |
+
|
| 57 |
+
sv = []
|
| 58 |
+
res.statevector.each_with_index do |a, i|
|
| 59 |
+
sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9
|
| 60 |
+
end
|
| 61 |
+
sv.sort_by! { |_, re, im| -(re * re + im * im) }
|
| 62 |
+
|
| 63 |
+
out = {
|
| 64 |
+
"diagram" => c.to_s,
|
| 65 |
+
"probs" => probs.first(24),
|
| 66 |
+
"counts" => res.counts,
|
| 67 |
+
"statevector" => sv.first(24),
|
| 68 |
+
"norm" => Math.sqrt(res.probabilities.sum)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
if req["cpsc"]
|
| 72 |
+
verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"])
|
| 73 |
+
out["cpsc"] = {
|
| 74 |
+
"admitted" => verdict[:admitted],
|
| 75 |
+
"rejections" => verdict[:rejections],
|
| 76 |
+
"leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)),
|
| 77 |
+
"energy" => Mercury::CPSC.energy_expectation(
|
| 78 |
+
res.statevector, n,
|
| 79 |
+
j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f
|
| 80 |
+
)
|
| 81 |
+
}
|
| 82 |
+
end
|
| 83 |
+
|
| 84 |
+
Mercury::MiniJSON.generate(out)
|
| 85 |
+
rescue => e
|
| 86 |
+
Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" })
|
| 87 |
+
end
|
frontend/web/build.sh
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Build the Mercury web console:
|
| 3 |
+
# mercury_bundle.rb — concatenated Ruby library + WASM bridge (fetched by qsim.html at boot)
|
| 4 |
+
# qsim.html — the console; references CDN wasm assets (works in any modern browser)
|
| 5 |
+
# Drop both files side-by-side anywhere static (e.g. docs/ for GitHub Pages).
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
cd "$(dirname "$0")"
|
| 8 |
+
|
| 9 |
+
{
|
| 10 |
+
for f in ../lib/mercury/version.rb \
|
| 11 |
+
../lib/mercury/gates.rb \
|
| 12 |
+
../lib/mercury/circuit.rb \
|
| 13 |
+
../lib/mercury/simulator.rb \
|
| 14 |
+
../lib/mercury/mini_json.rb \
|
| 15 |
+
../lib/mercury/cpsc.rb \
|
| 16 |
+
app.rb; do
|
| 17 |
+
echo "# ---- $f ----"
|
| 18 |
+
cat "$f"
|
| 19 |
+
echo ""
|
| 20 |
+
done
|
| 21 |
+
} > mercury_bundle.rb
|
| 22 |
+
|
| 23 |
+
ruby -c mercury_bundle.rb
|
| 24 |
+
echo "wrote mercury_bundle.rb ($(wc -c < mercury_bundle.rb) bytes), qsim.html is index.html"
|
| 25 |
+
cp index.html qsim.html
|
| 26 |
+
echo "wrote qsim.html ($(wc -c < qsim.html) bytes)"
|
| 27 |
+
mkdir -p ../../docs
|
| 28 |
+
cp qsim.html ../../docs/qsim.html
|
| 29 |
+
cp mercury_bundle.rb ../../docs/mercury_bundle.rb
|
| 30 |
+
python3 -c 'from pathlib import Path; q=Path("../../docs/qsim.html"); q.write_text(q.read_text().replace("href=\"./index.html\"", "href=\"./\""))'
|
| 31 |
+
echo "published docs/qsim.html and docs/mercury_bundle.rb"
|
frontend/web/index.html
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<!--
|
| 3 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 4 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 5 |
+
-->
|
| 6 |
+
<html lang="en">
|
| 7 |
+
<head>
|
| 8 |
+
<meta charset="utf-8">
|
| 9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 10 |
+
<title>Mercury — Quantum Circuit Console · CPSC-QFT</title>
|
| 11 |
+
<style>
|
| 12 |
+
:root { --bg:#0a0e14; --panel:#111722; --line:#1e2a3a; --txt:#d6e2f0; --dim:#7d8ea8;
|
| 13 |
+
--acc:#5cc8ff; --ok:#5df08a; --bad:#ff6b6b; --warn:#ffd479; }
|
| 14 |
+
* { box-sizing:border-box; }
|
| 15 |
+
body { margin:0; background:var(--bg); color:var(--txt);
|
| 16 |
+
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px; }
|
| 17 |
+
header { padding:14px 20px; border-bottom:1px solid var(--line); display:flex;
|
| 18 |
+
align-items:baseline; gap:14px; flex-wrap:wrap; }
|
| 19 |
+
header h1 { margin:0; font-size:17px; letter-spacing:2px; color:var(--acc); }
|
| 20 |
+
header span { color:var(--dim); font-size:12px; }
|
| 21 |
+
#status { margin-left:auto; font-size:12px; color:var(--warn); }
|
| 22 |
+
#status.ready { color:var(--ok); }
|
| 23 |
+
main { display:grid; grid-template-columns:minmax(340px,430px) 1fr; gap:14px; padding:14px 20px; }
|
| 24 |
+
@media (max-width:900px){ main{ grid-template-columns:1fr; } }
|
| 25 |
+
.card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:12px; margin-bottom:14px; }
|
| 26 |
+
.card h2 { margin:0 0 10px; font-size:12px; letter-spacing:1.5px; color:var(--dim); text-transform:uppercase; }
|
| 27 |
+
label { display:block; color:var(--dim); font-size:11px; margin:8px 0 3px; }
|
| 28 |
+
input[type=text], input[type=number], textarea, select {
|
| 29 |
+
width:100%; background:#0a0e14; color:var(--txt); border:1px solid var(--line);
|
| 30 |
+
border-radius:5px; padding:7px 9px; font:inherit; font-size:12.5px; }
|
| 31 |
+
textarea { min-height:150px; resize:vertical; line-height:1.5; }
|
| 32 |
+
.row { display:flex; gap:8px; } .row > div { flex:1; }
|
| 33 |
+
button { background:#16202f; color:var(--txt); border:1px solid var(--line); border-radius:5px;
|
| 34 |
+
padding:7px 10px; font:inherit; font-size:12px; cursor:pointer; }
|
| 35 |
+
button:hover { border-color:var(--acc); color:var(--acc); }
|
| 36 |
+
button.primary { background:#12324a; border-color:var(--acc); color:var(--acc); font-weight:bold; }
|
| 37 |
+
button.primary:hover { background:#17405e; }
|
| 38 |
+
.palette { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
|
| 39 |
+
.palette button { padding:5px 9px; }
|
| 40 |
+
.presets { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
| 41 |
+
pre.diagram { background:#0a0e14; border:1px solid var(--line); border-radius:5px;
|
| 42 |
+
padding:10px; overflow-x:auto; font-size:12.5px; line-height:1.7; }
|
| 43 |
+
table { width:100%; border-collapse:collapse; font-size:12px; }
|
| 44 |
+
th, td { text-align:right; padding:4px 8px; border-bottom:1px solid var(--line); }
|
| 45 |
+
th { color:var(--dim); font-weight:normal; } td:first-child, th:first-child { text-align:left; }
|
| 46 |
+
.verdict-ok { color:var(--ok); font-weight:bold; } .verdict-bad { color:var(--bad); font-weight:bold; }
|
| 47 |
+
.rej { color:var(--bad); } .muted { color:var(--dim); }
|
| 48 |
+
canvas { width:100%; height:220px; background:#0a0e14; border:1px solid var(--line); border-radius:5px; }
|
| 49 |
+
.kv { display:flex; justify-content:space-between; padding:3px 0; }
|
| 50 |
+
.kv b { font-weight:normal; color:var(--dim); }
|
| 51 |
+
footer { padding:10px 20px 20px; color:var(--dim); font-size:11px; }
|
| 52 |
+
a { color:var(--acc); }
|
| 53 |
+
</style>
|
| 54 |
+
</head>
|
| 55 |
+
<body>
|
| 56 |
+
<header>
|
| 57 |
+
<h1>MERCURY</h1>
|
| 58 |
+
<a href="./index.html" style="color:#7d8ea8;text-decoration:none">site</a>
|
| 59 |
+
<span>quantum circuit console · CPSC-QFT frontend · Ruby/WASM statevector simulator</span>
|
| 60 |
+
<span id="status">● loading Ruby…</span>
|
| 61 |
+
</header>
|
| 62 |
+
<main>
|
| 63 |
+
<div>
|
| 64 |
+
<div class="card">
|
| 65 |
+
<h2>Circuit</h2>
|
| 66 |
+
<div class="row">
|
| 67 |
+
<div><label>Qubits (1–12)</label><input type="number" id="n" value="2" min="1" max="12"></div>
|
| 68 |
+
<div><label>Initial state (bitstring)</label><input type="text" id="initial" value="00"></div>
|
| 69 |
+
</div>
|
| 70 |
+
<label>Script — one op per line: <span class="muted">h 0 · rx 1.57 1 · cnot 0 1 · cp 0.79 1 0 · rzz 0.5 0 1 · xx 0 1 · measure 0</span></label>
|
| 71 |
+
<textarea id="dsl">h 0
|
| 72 |
+
cnot 0 1</textarea>
|
| 73 |
+
<label>Gate palette (appends)</label>
|
| 74 |
+
<div class="palette" id="palette"></div>
|
| 75 |
+
<label>Presets</label>
|
| 76 |
+
<div class="presets">
|
| 77 |
+
<button data-preset="bell">Bell pair</button>
|
| 78 |
+
<button data-preset="ghz">GHZ-3</button>
|
| 79 |
+
<button data-preset="qft">QFT-3</button>
|
| 80 |
+
<button data-preset="trotter">Trotter scattering</button>
|
| 81 |
+
<button data-preset="adversarial">Adversarial X</button>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="card">
|
| 85 |
+
<h2>Run</h2>
|
| 86 |
+
<div class="row">
|
| 87 |
+
<div><label>Shots (0 = statevector only)</label><input type="number" id="shots" value="1024" min="0" max="100000"></div>
|
| 88 |
+
<div><label>Seed (blank = random)</label><input type="text" id="seed" value="42"></div>
|
| 89 |
+
</div>
|
| 90 |
+
<label style="margin-top:10px"><input type="checkbox" id="cpsc" checked style="width:auto"> CPSC conservation check</label>
|
| 91 |
+
<div class="row">
|
| 92 |
+
<div><label>J (coupling)</label><input type="text" id="pj" value="1.0"></div>
|
| 93 |
+
<div><label>λ (transverse field)</label><input type="text" id="plam" value="0.0"></div>
|
| 94 |
+
<div><label style="margin-top:22px"><input type="checkbox" id="pmag" style="width:auto"> conserve ΣZ</label></div>
|
| 95 |
+
</div>
|
| 96 |
+
<div style="margin-top:10px"><button class="primary" id="run" style="width:100%; padding:10px">▶ Run circuit</button></div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="card">
|
| 99 |
+
<h2>Conservation (CPSC)</h2>
|
| 100 |
+
<div id="cpscOut"><span class="muted">Run a circuit to check symmetry-sector admission.</span></div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div>
|
| 104 |
+
<div class="card">
|
| 105 |
+
<h2>Circuit diagram</h2>
|
| 106 |
+
<pre class="diagram" id="diagram"><span class="muted">—</span></pre>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="card">
|
| 109 |
+
<h2>Measurement histogram</h2>
|
| 110 |
+
<canvas id="hist" width="800" height="220"></canvas>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="card">
|
| 113 |
+
<h2>Statevector (top amplitudes)</h2>
|
| 114 |
+
<table><thead><tr><th>basis</th><th>Re</th><th>Im</th><th>P</th></tr></thead>
|
| 115 |
+
<tbody id="svBody"><tr><td colspan="4" class="muted">—</td></tr></tbody></table>
|
| 116 |
+
</div>
|
| 117 |
+
<div class="card">
|
| 118 |
+
<h2>Probabilities</h2>
|
| 119 |
+
<table><thead><tr><th>basis</th><th>P</th></tr></thead>
|
| 120 |
+
<tbody id="probBody"><tr><td colspan="2" class="muted">—</td></tr></tbody></table>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</main>
|
| 124 |
+
<footer>
|
| 125 |
+
Mercury is a dependency-free Ruby statevector simulator (up to 12 qubits in-browser). The CPSC panel mirrors
|
| 126 |
+
<span class="muted">src/cpsc/analyzer.py</span>: gates are admitted only if they preserve the Z<sub>2</sub> parity sector —
|
| 127 |
+
a compile-time rejection rule, not post-selection. Trotter preset evolves the transverse-field Ising H under test.
|
| 128 |
+
</footer>
|
| 129 |
+
|
| 130 |
+
<script src="https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.3.0/dist/browser.umd.js"></script>
|
| 131 |
+
<script>
|
| 132 |
+
/* Mercury WASM boot — same pattern as the Intentsim console. */
|
| 133 |
+
const W = { vm: null, ready: false };
|
| 134 |
+
const RUBY_WASM_URL = "https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.3.0/dist/ruby+stdlib.wasm";
|
| 135 |
+
function setBoot(t, ok) {
|
| 136 |
+
const el = document.getElementById("status");
|
| 137 |
+
el.textContent = t;
|
| 138 |
+
el.classList.toggle("ready", !!ok);
|
| 139 |
+
document.getElementById("run").disabled = !ok;
|
| 140 |
+
}
|
| 141 |
+
function rbEscape(s) { return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); }
|
| 142 |
+
function mercuryRun(req) {
|
| 143 |
+
const code = `handle_request('${rbEscape(JSON.stringify(req))}')`;
|
| 144 |
+
return JSON.parse(W.vm.eval(code).toString());
|
| 145 |
+
}
|
| 146 |
+
async function bootRuby() {
|
| 147 |
+
try {
|
| 148 |
+
if (!window["ruby-wasm-wasi"]) throw new Error("ruby-wasm-wasi bundle missing");
|
| 149 |
+
setBoot("● loading ruby 3.2 webassembly (≈32 mb)…");
|
| 150 |
+
const { DefaultRubyVM } = window["ruby-wasm-wasi"];
|
| 151 |
+
const response = await fetch(RUBY_WASM_URL);
|
| 152 |
+
if (!response.ok) throw new Error("wasm fetch failed: " + response.status);
|
| 153 |
+
setBoot("● compiling webassembly…");
|
| 154 |
+
const module = await WebAssembly.compile(await response.arrayBuffer());
|
| 155 |
+
setBoot("● starting ruby vm…");
|
| 156 |
+
const { vm } = await DefaultRubyVM(module);
|
| 157 |
+
setBoot("● evaluating mercury bundle…");
|
| 158 |
+
const src = await (await fetch("mercury_bundle.rb")).text();
|
| 159 |
+
if (!src.includes("module Mercury")) throw new Error("mercury bundle not found");
|
| 160 |
+
vm.eval(src);
|
| 161 |
+
W.vm = vm; W.ready = true;
|
| 162 |
+
setBoot("● ruby ready — live simulation", true);
|
| 163 |
+
} catch (e) {
|
| 164 |
+
setBoot("● wasm unreachable (" + (e.message || e) + ")");
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
const PARAM_N = {rx:1, ry:1, rz:1, cp:1, rzz:1, u:3};
|
| 169 |
+
const GATE_LIST = ["h","x","y","z","s","t","rx","ry","rz","cnot","cz","cp","swap","rzz","xx","ccx"];
|
| 170 |
+
|
| 171 |
+
function parseDSL(text) {
|
| 172 |
+
const ops = [];
|
| 173 |
+
for (const raw of text.split("\n")) {
|
| 174 |
+
const line = raw.trim();
|
| 175 |
+
if (!line || line.startsWith("#")) continue;
|
| 176 |
+
const t = line.split(/\s+/);
|
| 177 |
+
const gate = t[0].toLowerCase();
|
| 178 |
+
let idx = 1, params = [];
|
| 179 |
+
const pn = PARAM_N[gate] || 0;
|
| 180 |
+
for (let k = 0; k < pn; k++) {
|
| 181 |
+
if (idx >= t.length) throw new Error("missing param for " + gate);
|
| 182 |
+
const v = parseFloat(t[idx++]);
|
| 183 |
+
if (!isFinite(v)) throw new Error("bad param " + t[idx-1]);
|
| 184 |
+
params.push(v);
|
| 185 |
+
}
|
| 186 |
+
const qubits = [];
|
| 187 |
+
while (idx < t.length) {
|
| 188 |
+
const q = parseInt(t[idx++], 10);
|
| 189 |
+
if (!isFinite(q) || q < 0) throw new Error("bad qubit");
|
| 190 |
+
qubits.push(q);
|
| 191 |
+
}
|
| 192 |
+
ops.push([gate, qubits, params]);
|
| 193 |
+
}
|
| 194 |
+
return ops;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
document.getElementById("run").disabled = true;
|
| 198 |
+
document.addEventListener("DOMContentLoaded", bootRuby);
|
| 199 |
+
|
| 200 |
+
// gate palette
|
| 201 |
+
const pal = document.getElementById("palette");
|
| 202 |
+
for (const g of GATE_LIST) {
|
| 203 |
+
const b = document.createElement("button");
|
| 204 |
+
b.textContent = g.toUpperCase();
|
| 205 |
+
b.onclick = () => {
|
| 206 |
+
const dsl = document.getElementById("dsl");
|
| 207 |
+
const pn = PARAM_N[g] || 0;
|
| 208 |
+
dsl.value += (dsl.value.endsWith("\n") || dsl.value === "" ? "" : "\n") + g + " " + "0.5 ".repeat(pn) + "0\n";
|
| 209 |
+
dsl.focus();
|
| 210 |
+
};
|
| 211 |
+
pal.appendChild(b);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
const PRESETS = {
|
| 215 |
+
bell: { n:"2", initial:"00", dsl:"h 0\ncnot 0 1" },
|
| 216 |
+
ghz: { n:"3", initial:"000", dsl:"h 0\ncnot 0 1\ncnot 1 2" },
|
| 217 |
+
qft: { n:"3", initial:"000",
|
| 218 |
+
dsl:"h 0\ncp 1.5708 1 0\ncp 0.7854 2 0\nh 1\ncp 1.5708 2 1\nh 2\nswap 0 2" },
|
| 219 |
+
trotter:{ n:"4", initial:"0011", lam:"0.0",
|
| 220 |
+
dsl:"# Trotter: H = -J ZZ + lam X, J=1, t=0.4, 8 steps\n" +
|
| 221 |
+
Array(8).fill(
|
| 222 |
+
"rzz -0.1 0 1\nrzz -0.1 1 2\nrzz -0.1 2 3\nrzz -0.1 3 0"
|
| 223 |
+
).join("\n") },
|
| 224 |
+
adversarial: { n:"4", initial:"0000",
|
| 225 |
+
dsl:"rzz 0.5 0 1\nx 2\nrzz 0.5 2 3" }
|
| 226 |
+
};
|
| 227 |
+
document.querySelectorAll("[data-preset]").forEach(b => b.onclick = () => {
|
| 228 |
+
const p = PRESETS[b.dataset.preset];
|
| 229 |
+
document.getElementById("n").value = p.n;
|
| 230 |
+
document.getElementById("initial").value = p.initial;
|
| 231 |
+
document.getElementById("dsl").value = p.dsl;
|
| 232 |
+
if (p.lam !== undefined) document.getElementById("plam").value = p.lam;
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
document.getElementById("run").onclick = () => {
|
| 236 |
+
let req;
|
| 237 |
+
try {
|
| 238 |
+
const n = parseInt(document.getElementById("n").value, 10);
|
| 239 |
+
req = {
|
| 240 |
+
n, initial: document.getElementById("initial").value.trim(),
|
| 241 |
+
ops: parseDSL(document.getElementById("dsl").value),
|
| 242 |
+
shots: parseInt(document.getElementById("shots").value, 10) || 0,
|
| 243 |
+
seed: document.getElementById("seed").value.trim() === "" ? null
|
| 244 |
+
: parseInt(document.getElementById("seed").value, 10),
|
| 245 |
+
cpsc: document.getElementById("cpsc").checked,
|
| 246 |
+
magnetization: document.getElementById("pmag").checked,
|
| 247 |
+
j: parseFloat(document.getElementById("pj").value) || 0,
|
| 248 |
+
lam: parseFloat(document.getElementById("plam").value) || 0
|
| 249 |
+
};
|
| 250 |
+
} catch (e) { alert("Script error: " + e.message); return; }
|
| 251 |
+
let out;
|
| 252 |
+
try { out = mercuryRun(req); }
|
| 253 |
+
catch (e) { alert("Simulator error: " + e.message); return; }
|
| 254 |
+
if (out.error) { alert("Simulator error: " + out.error); return; }
|
| 255 |
+
render(out);
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
function render(out) {
|
| 259 |
+
document.getElementById("diagram").textContent = out.diagram;
|
| 260 |
+
// histogram: prefer counts, fall back to probs
|
| 261 |
+
const data = Object.keys(out.counts).length ? out.counts :
|
| 262 |
+
Object.fromEntries(out.probs.map(([b, p]) => [b, p]));
|
| 263 |
+
drawHist(data);
|
| 264 |
+
const svb = document.getElementById("svBody"); svb.innerHTML = "";
|
| 265 |
+
for (const [b, re, im] of out.statevector) {
|
| 266 |
+
const p = re*re + im*im;
|
| 267 |
+
svb.innerHTML += `<tr><td>|${b}⟩</td><td>${re.toFixed(4)}</td><td>${im.toFixed(4)}</td><td>${p.toFixed(4)}</td></tr>`;
|
| 268 |
+
}
|
| 269 |
+
const prb = document.getElementById("probBody"); prb.innerHTML = "";
|
| 270 |
+
for (const [b, p] of out.probs) prb.innerHTML += `<tr><td>|${b}⟩</td><td>${p.toFixed(4)}</td></tr>`;
|
| 271 |
+
const c = document.getElementById("cpscOut");
|
| 272 |
+
if (out.cpsc) {
|
| 273 |
+
const v = out.cpsc;
|
| 274 |
+
let html = v.admitted
|
| 275 |
+
? `<div class="verdict-ok">✓ ADMITTED — every gate preserves the symmetry sector</div>`
|
| 276 |
+
: `<div class="verdict-bad">✗ REJECTED at compile time</div>`;
|
| 277 |
+
for (const r of v.rejections) html += `<div class="rej">· ${r.gate}: ${r.reason}</div>`;
|
| 278 |
+
html += `<div class="kv"><b>parity leakage</b><span>${v.leakage.toExponential(2)}</span></div>`;
|
| 279 |
+
html += `<div class="kv"><b>⟨H⟩ (J,λ as set)</b><span>${v.energy.toFixed(6)}</span></div>`;
|
| 280 |
+
html += `<div class="kv"><b>norm</b><span>${out.norm.toFixed(6)}</span></div>`;
|
| 281 |
+
c.innerHTML = html;
|
| 282 |
+
} else c.innerHTML = '<span class="muted">CPSC check disabled.</span>';
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function drawHist(data) {
|
| 286 |
+
const cv = document.getElementById("hist"), ctx = cv.getContext("2d");
|
| 287 |
+
const W = cv.width, H = cv.height;
|
| 288 |
+
ctx.clearRect(0, 0, W, H);
|
| 289 |
+
const keys = Object.keys(data).sort();
|
| 290 |
+
if (!keys.length) return;
|
| 291 |
+
const max = Math.max(...Object.values(data));
|
| 292 |
+
const bw = W / keys.length;
|
| 293 |
+
ctx.fillStyle = "#5cc8ff"; ctx.strokeStyle = "#1e2a3a";
|
| 294 |
+
ctx.font = "11px monospace"; ctx.textAlign = "center";
|
| 295 |
+
keys.forEach((k, i) => {
|
| 296 |
+
const h = (data[k] / max) * (H - 40);
|
| 297 |
+
const x = i * bw;
|
| 298 |
+
ctx.fillRect(x + 3, H - 25 - h, bw - 6, h);
|
| 299 |
+
ctx.fillStyle = "#7d8ea8";
|
| 300 |
+
ctx.fillText(k, x + bw / 2, H - 10);
|
| 301 |
+
ctx.fillText(String(Math.round(data[k] * 100) / 100), x + bw / 2, H - 28 - h);
|
| 302 |
+
ctx.fillStyle = "#5cc8ff";
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
</script>
|
| 306 |
+
</body>
|
| 307 |
+
</html>
|
frontend/web/mercury_bundle.rb
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---- ../lib/mercury/version.rb ----
|
| 2 |
+
# *******************************************************************************
|
| 3 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 4 |
+
#
|
| 5 |
+
# This program and the accompanying materials are made available under the
|
| 6 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 7 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 8 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 9 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 10 |
+
# classpath exception and no permissive relicensing.
|
| 11 |
+
#
|
| 12 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 13 |
+
# *******************************************************************************
|
| 14 |
+
|
| 15 |
+
module Mercury
|
| 16 |
+
VERSION = "0.1.0"
|
| 17 |
+
end
|
| 18 |
+
|
| 19 |
+
# ---- ../lib/mercury/gates.rb ----
|
| 20 |
+
# *******************************************************************************
|
| 21 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 22 |
+
#
|
| 23 |
+
# This program and the accompanying materials are made available under the
|
| 24 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 25 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 26 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 27 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 28 |
+
# classpath exception and no permissive relicensing.
|
| 29 |
+
#
|
| 30 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 31 |
+
# *******************************************************************************
|
| 32 |
+
|
| 33 |
+
# Gate matrices for Mercury. Little-endian convention: in a k-qubit gate
|
| 34 |
+
# applied to qubits [q0, q1, ...], matrix row/column index bit j (value
|
| 35 |
+
# 2**j) is the computational-basis state of qubit q_j. For a two-qubit
|
| 36 |
+
# gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0).
|
| 37 |
+
module Mercury
|
| 38 |
+
module Gates
|
| 39 |
+
ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze
|
| 40 |
+
TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze
|
| 41 |
+
THREE_QUBIT = %i[ccx].freeze
|
| 42 |
+
|
| 43 |
+
PARAM_COUNTS = {
|
| 44 |
+
i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0,
|
| 45 |
+
rx: 1, ry: 1, rz: 1, u: 3,
|
| 46 |
+
cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0,
|
| 47 |
+
ccx: 0
|
| 48 |
+
}.freeze
|
| 49 |
+
|
| 50 |
+
def self.arity(name)
|
| 51 |
+
return 1 if ONE_QUBIT.include?(name)
|
| 52 |
+
return 2 if TWO_QUBIT.include?(name)
|
| 53 |
+
return 3 if THREE_QUBIT.include?(name)
|
| 54 |
+
raise ArgumentError, "unknown gate #{name}"
|
| 55 |
+
end
|
| 56 |
+
|
| 57 |
+
def self.param_count(name)
|
| 58 |
+
PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" }
|
| 59 |
+
end
|
| 60 |
+
|
| 61 |
+
def self.matrix(name, params = [])
|
| 62 |
+
need = param_count(name)
|
| 63 |
+
raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need
|
| 64 |
+
|
| 65 |
+
case name
|
| 66 |
+
when :i then [[c(1), c(0)], [c(0), c(1)]]
|
| 67 |
+
when :x then [[c(0), c(1)], [c(1), c(0)]]
|
| 68 |
+
when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]]
|
| 69 |
+
when :z then [[c(1), c(0)], [c(0), c(-1)]]
|
| 70 |
+
when :h
|
| 71 |
+
s = Math.sqrt(0.5)
|
| 72 |
+
[[c(s), c(s)], [c(s), c(-s)]]
|
| 73 |
+
when :s then [[c(1), c(0)], [c(0), c(0, 1)]]
|
| 74 |
+
when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]]
|
| 75 |
+
when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]]
|
| 76 |
+
when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]]
|
| 77 |
+
when :rx
|
| 78 |
+
th = params[0] / 2.0
|
| 79 |
+
co = c(Math.cos(th)); si = c(0, -Math.sin(th))
|
| 80 |
+
[[co, si], [si, co]]
|
| 81 |
+
when :ry
|
| 82 |
+
th = params[0] / 2.0
|
| 83 |
+
[[c(Math.cos(th)), c(-Math.sin(th))],
|
| 84 |
+
[c(Math.sin(th)), c(Math.cos(th))]]
|
| 85 |
+
when :rz
|
| 86 |
+
th = params[0] / 2.0
|
| 87 |
+
[[phase(-th), c(0)], [c(0), phase(th)]]
|
| 88 |
+
when :u
|
| 89 |
+
th, ph, la = params
|
| 90 |
+
co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0))
|
| 91 |
+
[[co, c(-1) * phase(la) * si],
|
| 92 |
+
[phase(ph) * si, phase(ph + la) * co]]
|
| 93 |
+
when :cnot
|
| 94 |
+
# qubits [target, control]; flips target iff control == 1
|
| 95 |
+
[[c(1), c(0), c(0), c(0)],
|
| 96 |
+
[c(0), c(1), c(0), c(0)],
|
| 97 |
+
[c(0), c(0), c(0), c(1)],
|
| 98 |
+
[c(0), c(0), c(1), c(0)]]
|
| 99 |
+
when :cz
|
| 100 |
+
[[c(1), c(0), c(0), c(0)],
|
| 101 |
+
[c(0), c(1), c(0), c(0)],
|
| 102 |
+
[c(0), c(0), c(1), c(0)],
|
| 103 |
+
[c(0), c(0), c(0), c(-1)]]
|
| 104 |
+
when :cp
|
| 105 |
+
[[c(1), c(0), c(0), c(0)],
|
| 106 |
+
[c(0), c(1), c(0), c(0)],
|
| 107 |
+
[c(0), c(0), c(1), c(0)],
|
| 108 |
+
[c(0), c(0), c(0), phase(params[0])]]
|
| 109 |
+
when :swap
|
| 110 |
+
[[c(1), c(0), c(0), c(0)],
|
| 111 |
+
[c(0), c(0), c(1), c(0)],
|
| 112 |
+
[c(0), c(1), c(0), c(0)],
|
| 113 |
+
[c(0), c(0), c(0), c(1)]]
|
| 114 |
+
when :rzz
|
| 115 |
+
# exp(-i*theta/2 * ZxZ)
|
| 116 |
+
a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0)
|
| 117 |
+
[[a, c(0), c(0), c(0)],
|
| 118 |
+
[c(0), b, c(0), c(0)],
|
| 119 |
+
[c(0), c(0), b, c(0)],
|
| 120 |
+
[c(0), c(0), c(0), a]]
|
| 121 |
+
when :xx
|
| 122 |
+
# XxX: parity-even pair used by the CPSC analyzer
|
| 123 |
+
[[c(0), c(0), c(0), c(1)],
|
| 124 |
+
[c(0), c(0), c(1), c(0)],
|
| 125 |
+
[c(0), c(1), c(0), c(0)],
|
| 126 |
+
[c(1), c(0), c(0), c(0)]]
|
| 127 |
+
when :ccx
|
| 128 |
+
# qubits [target, control1, control2]; flips target iff both controls == 1
|
| 129 |
+
m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } }
|
| 130 |
+
m[6][6] = c(0); m[7][7] = c(0)
|
| 131 |
+
m[6][7] = c(1); m[7][6] = c(1)
|
| 132 |
+
m
|
| 133 |
+
end
|
| 134 |
+
end
|
| 135 |
+
|
| 136 |
+
def self.c(re, im = 0)
|
| 137 |
+
Complex(re, im)
|
| 138 |
+
end
|
| 139 |
+
|
| 140 |
+
def self.phase(angle)
|
| 141 |
+
Complex(Math.cos(angle), Math.sin(angle))
|
| 142 |
+
end
|
| 143 |
+
private_class_method :c, :phase
|
| 144 |
+
end
|
| 145 |
+
end
|
| 146 |
+
|
| 147 |
+
# ---- ../lib/mercury/circuit.rb ----
|
| 148 |
+
# *******************************************************************************
|
| 149 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 150 |
+
#
|
| 151 |
+
# This program and the accompanying materials are made available under the
|
| 152 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 153 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 154 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 155 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 156 |
+
# classpath exception and no permissive relicensing.
|
| 157 |
+
#
|
| 158 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 159 |
+
# *******************************************************************************
|
| 160 |
+
|
| 161 |
+
module Mercury
|
| 162 |
+
# A quantum circuit: an ordered list of gate operations on n qubits.
|
| 163 |
+
#
|
| 164 |
+
# c = Mercury::Circuit.new(2) do |c|
|
| 165 |
+
# c.h 0
|
| 166 |
+
# c.cnot 0, 1
|
| 167 |
+
# c.measure_all
|
| 168 |
+
# end
|
| 169 |
+
#
|
| 170 |
+
# Multi-qubit gate qubit order follows the Gates convention: for
|
| 171 |
+
# cnot(control, target) the DSL takes (control, target) and stores
|
| 172 |
+
# [target, control] so matrix bit 0 is the target. Same for ccx:
|
| 173 |
+
# ccx(c1, c2, target) stores [target, c1, c2].
|
| 174 |
+
class Circuit
|
| 175 |
+
Op = Struct.new(:gate, :qubits, :params, :classical)
|
| 176 |
+
|
| 177 |
+
attr_reader :n, :ops
|
| 178 |
+
|
| 179 |
+
DSL_REORDER = { cnot: true, ccx: true }.freeze
|
| 180 |
+
|
| 181 |
+
def initialize(n)
|
| 182 |
+
raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1
|
| 183 |
+
@n = n
|
| 184 |
+
@ops = []
|
| 185 |
+
yield self if block_given?
|
| 186 |
+
end
|
| 187 |
+
|
| 188 |
+
def add(gate, *qubits, params: [])
|
| 189 |
+
gate = gate.to_sym
|
| 190 |
+
arity = Gates.arity(gate)
|
| 191 |
+
raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity
|
| 192 |
+
raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length
|
| 193 |
+
qubits.each do |q|
|
| 194 |
+
raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n
|
| 195 |
+
end
|
| 196 |
+
stored = DSL_REORDER[gate] ? qubits.reverse : qubits
|
| 197 |
+
@ops << Op.new(gate, stored, params, nil)
|
| 198 |
+
self
|
| 199 |
+
end
|
| 200 |
+
|
| 201 |
+
# Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry.
|
| 202 |
+
(Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname|
|
| 203 |
+
define_method(gname) do |*args|
|
| 204 |
+
need = Gates.param_count(gname)
|
| 205 |
+
arity = Gates.arity(gname)
|
| 206 |
+
raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity
|
| 207 |
+
params = args.first(need)
|
| 208 |
+
qubits = args.last(arity)
|
| 209 |
+
add(gname, *qubits, params: params)
|
| 210 |
+
end
|
| 211 |
+
end
|
| 212 |
+
|
| 213 |
+
def measure(qubit, clbit = nil)
|
| 214 |
+
raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n
|
| 215 |
+
@ops << Op.new(:measure, [qubit], [], clbit || qubit)
|
| 216 |
+
self
|
| 217 |
+
end
|
| 218 |
+
|
| 219 |
+
def measure_all
|
| 220 |
+
@n.times { |q| measure(q, q) }
|
| 221 |
+
self
|
| 222 |
+
end
|
| 223 |
+
|
| 224 |
+
def depth
|
| 225 |
+
@ops.length
|
| 226 |
+
end
|
| 227 |
+
|
| 228 |
+
# Minimal Unicode circuit diagram.
|
| 229 |
+
def to_s
|
| 230 |
+
cols = @ops.map { |op| column(op) }
|
| 231 |
+
rows = (0...@n).map do |q|
|
| 232 |
+
label = "q#{q}: "
|
| 233 |
+
body = cols.map { |col| col[q] }.join("")
|
| 234 |
+
label + body
|
| 235 |
+
end
|
| 236 |
+
rows.join("\n")
|
| 237 |
+
end
|
| 238 |
+
|
| 239 |
+
private
|
| 240 |
+
|
| 241 |
+
def column(op)
|
| 242 |
+
cells = Array.new(@n, "────────")
|
| 243 |
+
case op.gate
|
| 244 |
+
when :measure
|
| 245 |
+
cells[op.qubits[0]] = "───╫M───"
|
| 246 |
+
else
|
| 247 |
+
qs = op.qubits
|
| 248 |
+
tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase
|
| 249 |
+
if qs.length == 1
|
| 250 |
+
cells[qs[0]] = "───#{tag[0, 2]}───"
|
| 251 |
+
else
|
| 252 |
+
# multi-qubit: control dots on qs[1..], box on qs[0], rail between
|
| 253 |
+
lo, hi = qs.minmax
|
| 254 |
+
(lo..hi).each { |q| cells[q] = "───┼────" }
|
| 255 |
+
cells[qs[0]] = "───#{tag[0, 2]}───"
|
| 256 |
+
qs[1..].each { |q| cells[q] = "───●────" }
|
| 257 |
+
end
|
| 258 |
+
end
|
| 259 |
+
cells
|
| 260 |
+
end
|
| 261 |
+
end
|
| 262 |
+
end
|
| 263 |
+
|
| 264 |
+
# ---- ../lib/mercury/simulator.rb ----
|
| 265 |
+
# *******************************************************************************
|
| 266 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 267 |
+
#
|
| 268 |
+
# This program and the accompanying materials are made available under the
|
| 269 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 270 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 271 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 272 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 273 |
+
# classpath exception and no permissive relicensing.
|
| 274 |
+
#
|
| 275 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 276 |
+
# *******************************************************************************
|
| 277 |
+
|
| 278 |
+
module Mercury
|
| 279 |
+
# Full statevector simulator. Little-endian: qubit q is bit q of the
|
| 280 |
+
# basis index, matching the Gates matrix convention.
|
| 281 |
+
class Statevector
|
| 282 |
+
attr_reader :n, :amps
|
| 283 |
+
|
| 284 |
+
# initial: integer basis index, or MSB-first bitstring like "0011".
|
| 285 |
+
def initialize(n, initial: 0)
|
| 286 |
+
raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1
|
| 287 |
+
initial = Integer(initial, 2) if initial.is_a?(String)
|
| 288 |
+
raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1)
|
| 289 |
+
@n = n
|
| 290 |
+
@dim = 1 << n
|
| 291 |
+
@amps = Array.new(@dim, Complex(0, 0))
|
| 292 |
+
@amps[initial] = Complex(1, 0)
|
| 293 |
+
end
|
| 294 |
+
|
| 295 |
+
def dim
|
| 296 |
+
@dim
|
| 297 |
+
end
|
| 298 |
+
|
| 299 |
+
def norm
|
| 300 |
+
Math.sqrt(@amps.sum { |a| a.abs**2 })
|
| 301 |
+
end
|
| 302 |
+
|
| 303 |
+
def apply(gate, qubits, params = [])
|
| 304 |
+
matrix = Gates.matrix(gate, params)
|
| 305 |
+
case qubits.length
|
| 306 |
+
when 1 then apply_one(matrix, qubits[0])
|
| 307 |
+
when 2 then apply_two(matrix, qubits[0], qubits[1])
|
| 308 |
+
when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2])
|
| 309 |
+
else raise ArgumentError, "unsupported arity #{qubits.length}"
|
| 310 |
+
end
|
| 311 |
+
self
|
| 312 |
+
end
|
| 313 |
+
|
| 314 |
+
def probabilities
|
| 315 |
+
@amps.map { |a| a.abs**2 }
|
| 316 |
+
end
|
| 317 |
+
|
| 318 |
+
# Sample `shots` bitstrings (MSB-first strings) from the distribution.
|
| 319 |
+
def sample(shots, rng = Random.new)
|
| 320 |
+
probs = probabilities
|
| 321 |
+
cumulative = []
|
| 322 |
+
total = 0.0
|
| 323 |
+
probs.each { |p| total += p; cumulative << total }
|
| 324 |
+
Array.new(shots) do
|
| 325 |
+
r = rng.rand * total
|
| 326 |
+
idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1)
|
| 327 |
+
bitstring(idx)
|
| 328 |
+
end
|
| 329 |
+
end
|
| 330 |
+
|
| 331 |
+
# Projective Z measurement of one qubit; collapses the state in place.
|
| 332 |
+
def measure!(qubit, rng = Random.new)
|
| 333 |
+
raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1)
|
| 334 |
+
step = 1 << qubit
|
| 335 |
+
p0 = 0.0
|
| 336 |
+
@amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? }
|
| 337 |
+
outcome = rng.rand < p0 ? 0 : 1
|
| 338 |
+
renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0))
|
| 339 |
+
raise "measurement of zero-probability branch" if renorm.zero?
|
| 340 |
+
@amps.each_with_index do |a, s|
|
| 341 |
+
bit = (s & step).zero? ? 0 : 1
|
| 342 |
+
@amps[s] = bit == outcome ? a / renorm : Complex(0, 0)
|
| 343 |
+
end
|
| 344 |
+
outcome
|
| 345 |
+
end
|
| 346 |
+
|
| 347 |
+
def bitstring(index)
|
| 348 |
+
# MSB-first for display: qubit n-1 ... qubit 0
|
| 349 |
+
(0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join
|
| 350 |
+
end
|
| 351 |
+
|
| 352 |
+
private
|
| 353 |
+
|
| 354 |
+
def apply_one(m, q)
|
| 355 |
+
step = 1 << q
|
| 356 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 357 |
+
(0...@dim).each do |s|
|
| 358 |
+
next unless (s & step).zero?
|
| 359 |
+
i0 = s
|
| 360 |
+
i1 = s | step
|
| 361 |
+
a0 = @amps[i0]
|
| 362 |
+
a1 = @amps[i1]
|
| 363 |
+
nxt[i0] = m[0][0] * a0 + m[0][1] * a1
|
| 364 |
+
nxt[i1] = m[1][0] * a0 + m[1][1] * a1
|
| 365 |
+
end
|
| 366 |
+
@amps = nxt
|
| 367 |
+
end
|
| 368 |
+
|
| 369 |
+
def apply_two(m, q0, q1)
|
| 370 |
+
raise ArgumentError, "q0 == q1" if q0 == q1
|
| 371 |
+
s0 = 1 << q0
|
| 372 |
+
s1 = 1 << q1
|
| 373 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 374 |
+
(0...@dim).each do |s|
|
| 375 |
+
next unless (s & s0).zero? && (s & s1).zero?
|
| 376 |
+
idx = [s, s | s0, s | s1, s | s0 | s1]
|
| 377 |
+
old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])]
|
| 378 |
+
4.times do |r|
|
| 379 |
+
nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3]
|
| 380 |
+
end
|
| 381 |
+
end
|
| 382 |
+
@amps = nxt
|
| 383 |
+
end
|
| 384 |
+
|
| 385 |
+
def apply_three(m, q0, q1, q2)
|
| 386 |
+
raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3
|
| 387 |
+
s0 = 1 << q0
|
| 388 |
+
s1 = 1 << q1
|
| 389 |
+
s2 = 1 << q2
|
| 390 |
+
nxt = Array.new(@dim, Complex(0, 0))
|
| 391 |
+
(0...@dim).each do |s|
|
| 392 |
+
next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero?
|
| 393 |
+
idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) }
|
| 394 |
+
old = idx.map { |i| @amps[i] }
|
| 395 |
+
8.times do |r|
|
| 396 |
+
acc = Complex(0, 0)
|
| 397 |
+
8.times { |cidx| acc += m[r][cidx] * old[cidx] }
|
| 398 |
+
nxt[idx[r]] = acc
|
| 399 |
+
end
|
| 400 |
+
end
|
| 401 |
+
@amps = nxt
|
| 402 |
+
end
|
| 403 |
+
end
|
| 404 |
+
|
| 405 |
+
# Runs a Circuit. :measure ops collapse immediately; `shots` samples the
|
| 406 |
+
# final distribution.
|
| 407 |
+
class Simulator
|
| 408 |
+
Result = Struct.new(:counts, :statevector, :probabilities, :measured)
|
| 409 |
+
|
| 410 |
+
def run(circuit, shots: 0, seed: nil, initial: 0)
|
| 411 |
+
rng = seed.nil? ? Random.new : Random.new(seed)
|
| 412 |
+
sv = Statevector.new(circuit.n, initial: initial)
|
| 413 |
+
measured = {}
|
| 414 |
+
circuit.ops.each do |op|
|
| 415 |
+
if op.gate == :measure
|
| 416 |
+
measured[op.classical] = sv.measure!(op.qubits[0], rng)
|
| 417 |
+
else
|
| 418 |
+
sv.apply(op.gate, op.qubits, op.params)
|
| 419 |
+
end
|
| 420 |
+
end
|
| 421 |
+
counts = {}
|
| 422 |
+
if shots.positive?
|
| 423 |
+
sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 }
|
| 424 |
+
end
|
| 425 |
+
Result.new(counts, sv.amps.dup, sv.probabilities, measured)
|
| 426 |
+
end
|
| 427 |
+
end
|
| 428 |
+
end
|
| 429 |
+
|
| 430 |
+
# ---- ../lib/mercury/mini_json.rb ----
|
| 431 |
+
# *******************************************************************************
|
| 432 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 433 |
+
#
|
| 434 |
+
# This program and the accompanying materials are made available under the
|
| 435 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 436 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 437 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 438 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 439 |
+
# classpath exception and no permissive relicensing.
|
| 440 |
+
#
|
| 441 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 442 |
+
# *******************************************************************************
|
| 443 |
+
|
| 444 |
+
# Minimal JSON parser/generator covering exactly what the Mercury web
|
| 445 |
+
# bridge needs (objects, arrays, strings, numbers, booleans, null).
|
| 446 |
+
# Pure Ruby, no dependencies — safe inside Ruby WASM.
|
| 447 |
+
module Mercury
|
| 448 |
+
module MiniJSON
|
| 449 |
+
class ParseError < StandardError; end
|
| 450 |
+
|
| 451 |
+
def self.parse(str)
|
| 452 |
+
@src = str
|
| 453 |
+
@pos = 0
|
| 454 |
+
skip_ws
|
| 455 |
+
val = parse_value
|
| 456 |
+
skip_ws
|
| 457 |
+
raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length
|
| 458 |
+
val
|
| 459 |
+
end
|
| 460 |
+
|
| 461 |
+
def self.generate(obj)
|
| 462 |
+
case obj
|
| 463 |
+
when Hash
|
| 464 |
+
"{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}"
|
| 465 |
+
when Array
|
| 466 |
+
"[" + obj.map { |v| generate(v) }.join(",") + "]"
|
| 467 |
+
when String
|
| 468 |
+
'"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"'
|
| 469 |
+
when Integer, Float
|
| 470 |
+
raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite?
|
| 471 |
+
obj.to_s
|
| 472 |
+
when true then "true"
|
| 473 |
+
when false then "false"
|
| 474 |
+
when nil then "null"
|
| 475 |
+
else
|
| 476 |
+
raise ParseError, "cannot serialize #{obj.class}"
|
| 477 |
+
end
|
| 478 |
+
end
|
| 479 |
+
|
| 480 |
+
def self.skip_ws
|
| 481 |
+
@pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/
|
| 482 |
+
end
|
| 483 |
+
|
| 484 |
+
def self.parse_value
|
| 485 |
+
case @src[@pos]
|
| 486 |
+
when '"' then parse_string
|
| 487 |
+
when "{" then parse_object
|
| 488 |
+
when "[" then parse_array
|
| 489 |
+
when "t" then literal("true", true)
|
| 490 |
+
when "f" then literal("false", false)
|
| 491 |
+
when "n" then literal("null", nil)
|
| 492 |
+
else parse_number
|
| 493 |
+
end
|
| 494 |
+
end
|
| 495 |
+
|
| 496 |
+
def self.literal(word, val)
|
| 497 |
+
raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word
|
| 498 |
+
@pos += word.length
|
| 499 |
+
val
|
| 500 |
+
end
|
| 501 |
+
|
| 502 |
+
def self.parse_string
|
| 503 |
+
@pos += 1 # opening quote
|
| 504 |
+
out = +""
|
| 505 |
+
while @pos < @src.length
|
| 506 |
+
ch = @src[@pos]
|
| 507 |
+
case ch
|
| 508 |
+
when '"'
|
| 509 |
+
@pos += 1
|
| 510 |
+
return out
|
| 511 |
+
when "\\"
|
| 512 |
+
@pos += 1
|
| 513 |
+
esc = @src[@pos]
|
| 514 |
+
out << case esc
|
| 515 |
+
when '"' then '"'
|
| 516 |
+
when "\\" then "\\"
|
| 517 |
+
when "/" then "/"
|
| 518 |
+
when "b" then "\b"
|
| 519 |
+
when "f" then "\f"
|
| 520 |
+
when "n" then "\n"
|
| 521 |
+
when "r" then "\r"
|
| 522 |
+
when "t" then "\t"
|
| 523 |
+
when "u"
|
| 524 |
+
hex = @src[@pos + 1, 4]
|
| 525 |
+
@pos += 4
|
| 526 |
+
[hex.to_i(16)].pack("U")
|
| 527 |
+
else raise ParseError, "bad escape at #{@pos}"
|
| 528 |
+
end
|
| 529 |
+
@pos += 1
|
| 530 |
+
else
|
| 531 |
+
out << ch
|
| 532 |
+
@pos += 1
|
| 533 |
+
end
|
| 534 |
+
end
|
| 535 |
+
raise ParseError, "unterminated string"
|
| 536 |
+
end
|
| 537 |
+
|
| 538 |
+
def self.parse_object
|
| 539 |
+
@pos += 1
|
| 540 |
+
obj = {}
|
| 541 |
+
skip_ws
|
| 542 |
+
if @src[@pos] == "}"
|
| 543 |
+
@pos += 1
|
| 544 |
+
return obj
|
| 545 |
+
end
|
| 546 |
+
loop do
|
| 547 |
+
skip_ws
|
| 548 |
+
raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"'
|
| 549 |
+
key = parse_string
|
| 550 |
+
skip_ws
|
| 551 |
+
raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":"
|
| 552 |
+
@pos += 1
|
| 553 |
+
skip_ws
|
| 554 |
+
obj[key] = parse_value
|
| 555 |
+
skip_ws
|
| 556 |
+
case @src[@pos]
|
| 557 |
+
when "," then @pos += 1
|
| 558 |
+
when "}" then @pos += 1; break
|
| 559 |
+
else raise ParseError, "expected , or } at #{@pos}"
|
| 560 |
+
end
|
| 561 |
+
end
|
| 562 |
+
obj
|
| 563 |
+
end
|
| 564 |
+
|
| 565 |
+
def self.parse_array
|
| 566 |
+
@pos += 1
|
| 567 |
+
arr = []
|
| 568 |
+
skip_ws
|
| 569 |
+
if @src[@pos] == "]"
|
| 570 |
+
@pos += 1
|
| 571 |
+
return arr
|
| 572 |
+
end
|
| 573 |
+
loop do
|
| 574 |
+
skip_ws
|
| 575 |
+
arr << parse_value
|
| 576 |
+
skip_ws
|
| 577 |
+
case @src[@pos]
|
| 578 |
+
when "," then @pos += 1
|
| 579 |
+
when "]" then @pos += 1; break
|
| 580 |
+
else raise ParseError, "expected , or ] at #{@pos}"
|
| 581 |
+
end
|
| 582 |
+
end
|
| 583 |
+
arr
|
| 584 |
+
end
|
| 585 |
+
|
| 586 |
+
def self.parse_number
|
| 587 |
+
m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)
|
| 588 |
+
raise ParseError, "bad number at #{@pos}" unless m
|
| 589 |
+
@pos += m[0].length
|
| 590 |
+
s = m[0]
|
| 591 |
+
(s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i
|
| 592 |
+
end
|
| 593 |
+
end
|
| 594 |
+
end
|
| 595 |
+
|
| 596 |
+
# ---- ../lib/mercury/cpsc.rb ----
|
| 597 |
+
# *******************************************************************************
|
| 598 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 599 |
+
#
|
| 600 |
+
# This program and the accompanying materials are made available under the
|
| 601 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 602 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 603 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 604 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 605 |
+
# classpath exception and no permissive relicensing.
|
| 606 |
+
#
|
| 607 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 608 |
+
# *******************************************************************************
|
| 609 |
+
|
| 610 |
+
# Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a
|
| 611 |
+
# Trotterized scattering evolution built as a Mercury circuit.
|
| 612 |
+
#
|
| 613 |
+
# Gate-kind semantics (identical to the Python original):
|
| 614 |
+
# - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z.
|
| 615 |
+
# - "parity_even": preserves prod Z (Z2 parity) but may change sum Z.
|
| 616 |
+
# - "illegal": odd X/Y support; changes Z-parity. Rejected.
|
| 617 |
+
module Mercury
|
| 618 |
+
module CPSC
|
| 619 |
+
# Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds.
|
| 620 |
+
KINDS = {
|
| 621 |
+
i: "magnetization", z: "magnetization", s: "magnetization",
|
| 622 |
+
t: "magnetization", sdg: "magnetization", tdg: "magnetization",
|
| 623 |
+
rz: "magnetization", cz: "magnetization", rzz: "magnetization",
|
| 624 |
+
cp: "magnetization",
|
| 625 |
+
xx: "parity_even",
|
| 626 |
+
x: "illegal", y: "illegal", h: "illegal",
|
| 627 |
+
rx: "illegal", ry: "illegal", u: "illegal",
|
| 628 |
+
cnot: "illegal", swap: "illegal", ccx: "illegal"
|
| 629 |
+
}.freeze
|
| 630 |
+
|
| 631 |
+
GateSpec = Struct.new(:name, :qubits, :kind)
|
| 632 |
+
Rejection = Struct.new(:spec, :reason)
|
| 633 |
+
|
| 634 |
+
class CompilationRejected < StandardError; end
|
| 635 |
+
|
| 636 |
+
class SymmetryAnalyzer
|
| 637 |
+
def initialize(n, conserve_magnetization: false)
|
| 638 |
+
raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1
|
| 639 |
+
@n = n
|
| 640 |
+
@conserve_magnetization = conserve_magnetization
|
| 641 |
+
end
|
| 642 |
+
|
| 643 |
+
def check(specs)
|
| 644 |
+
specs.flat_map do |spec|
|
| 645 |
+
reason = reject_reason(spec)
|
| 646 |
+
reason ? [Rejection.new(spec, reason)] : []
|
| 647 |
+
end
|
| 648 |
+
end
|
| 649 |
+
|
| 650 |
+
def admit(specs)
|
| 651 |
+
rejected = check(specs)
|
| 652 |
+
unless rejected.empty?
|
| 653 |
+
detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ")
|
| 654 |
+
raise CompilationRejected, detail
|
| 655 |
+
end
|
| 656 |
+
specs.dup
|
| 657 |
+
end
|
| 658 |
+
|
| 659 |
+
private
|
| 660 |
+
|
| 661 |
+
def reject_reason(spec)
|
| 662 |
+
spec.qubits.each do |q|
|
| 663 |
+
return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1)
|
| 664 |
+
end
|
| 665 |
+
case spec.kind
|
| 666 |
+
when "illegal"
|
| 667 |
+
"odd X/Y support changes Z-parity; forbidden sector"
|
| 668 |
+
when "parity_even", "magnetization"
|
| 669 |
+
nil
|
| 670 |
+
else
|
| 671 |
+
"unknown gate kind #{spec.kind}"
|
| 672 |
+
end
|
| 673 |
+
end
|
| 674 |
+
end
|
| 675 |
+
|
| 676 |
+
# Classify a Mercury::Circuit::Op into a GateSpec for the analyzer.
|
| 677 |
+
def self.classify(op)
|
| 678 |
+
kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" }
|
| 679 |
+
label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})"
|
| 680 |
+
GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind)
|
| 681 |
+
end
|
| 682 |
+
|
| 683 |
+
# Run the analyzer over a whole circuit. Returns
|
| 684 |
+
# { admitted: bool, rejections: [{gate, reason}] }.
|
| 685 |
+
def self.analyze(circuit, conserve_magnetization: false)
|
| 686 |
+
analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization)
|
| 687 |
+
specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) }
|
| 688 |
+
rejections = analyzer.check(specs)
|
| 689 |
+
{
|
| 690 |
+
admitted: rejections.empty?,
|
| 691 |
+
rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } }
|
| 692 |
+
}
|
| 693 |
+
end
|
| 694 |
+
|
| 695 |
+
# First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i
|
| 696 |
+
# (periodic), matching src/cpsc/hamiltonian.py. Per step dt:
|
| 697 |
+
# prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i).
|
| 698 |
+
def self.trotter_circuit(n, j:, lam:, time:, steps:)
|
| 699 |
+
raise ArgumentError, "steps must be >= 1" unless steps >= 1
|
| 700 |
+
dt = time.to_f / steps
|
| 701 |
+
Circuit.new(n) do |c|
|
| 702 |
+
steps.times do
|
| 703 |
+
(0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) }
|
| 704 |
+
(0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero?
|
| 705 |
+
end
|
| 706 |
+
end
|
| 707 |
+
end
|
| 708 |
+
|
| 709 |
+
def self.parity_of(basis, n)
|
| 710 |
+
basis.to_s(2).count("1") & 1
|
| 711 |
+
end
|
| 712 |
+
|
| 713 |
+
def self.magnetization_of(basis, n)
|
| 714 |
+
mag = 0
|
| 715 |
+
n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 }
|
| 716 |
+
mag
|
| 717 |
+
end
|
| 718 |
+
|
| 719 |
+
# Probability mass outside the input Z2-parity sector.
|
| 720 |
+
def self.leakage(amps, n, parity)
|
| 721 |
+
wrong = 0.0
|
| 722 |
+
amps.each_with_index do |a, b|
|
| 723 |
+
wrong += a.abs**2 if parity_of(b, n) != parity
|
| 724 |
+
end
|
| 725 |
+
wrong
|
| 726 |
+
end
|
| 727 |
+
|
| 728 |
+
# <psi|H|psi> for the transverse-field Ising H, computed directly.
|
| 729 |
+
def self.energy_expectation(amps, n, j:, lam:)
|
| 730 |
+
dim = 1 << n
|
| 731 |
+
e = 0.0
|
| 732 |
+
dim.times do |s|
|
| 733 |
+
p = amps[s].abs**2
|
| 734 |
+
next if p.zero?
|
| 735 |
+
zz = 0.0
|
| 736 |
+
n.times do |i|
|
| 737 |
+
zi = ((s >> i) & 1).zero? ? 1.0 : -1.0
|
| 738 |
+
zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0
|
| 739 |
+
zz += zi * zj
|
| 740 |
+
end
|
| 741 |
+
e += p * (-j * zz)
|
| 742 |
+
end
|
| 743 |
+
unless lam.zero?
|
| 744 |
+
x = 0.0
|
| 745 |
+
n.times do |i|
|
| 746 |
+
bit = 1 << i
|
| 747 |
+
dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real }
|
| 748 |
+
end
|
| 749 |
+
e += lam * x
|
| 750 |
+
end
|
| 751 |
+
e
|
| 752 |
+
end
|
| 753 |
+
|
| 754 |
+
end
|
| 755 |
+
end
|
| 756 |
+
|
| 757 |
+
# ---- app.rb ----
|
| 758 |
+
# *******************************************************************************
|
| 759 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 760 |
+
#
|
| 761 |
+
# This program and the accompanying materials are made available under the
|
| 762 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 763 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 764 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 765 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 766 |
+
# classpath exception and no permissive relicensing.
|
| 767 |
+
#
|
| 768 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 769 |
+
# *******************************************************************************
|
| 770 |
+
|
| 771 |
+
# Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into
|
| 772 |
+
# mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript
|
| 773 |
+
# drives it via vm.eval("handle_request('<json>')").
|
| 774 |
+
#
|
| 775 |
+
# Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]],
|
| 776 |
+
# "shots":1024,"seed":42,"cpsc":true,"magnetization":false,
|
| 777 |
+
# "j":1.0,"lam":0.0}
|
| 778 |
+
# Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...},
|
| 779 |
+
# "statevector":[[bits,re,im],...],"norm":1.0,
|
| 780 |
+
# "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}}
|
| 781 |
+
|
| 782 |
+
def bitstring_of(index, n)
|
| 783 |
+
(0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join
|
| 784 |
+
end
|
| 785 |
+
|
| 786 |
+
def handle_request(json_str)
|
| 787 |
+
req = Mercury::MiniJSON.parse(json_str)
|
| 788 |
+
n = req["n"].to_i
|
| 789 |
+
raise "n must be 1..12" unless n.between?(1, 12)
|
| 790 |
+
|
| 791 |
+
c = Mercury::Circuit.new(n)
|
| 792 |
+
(req["ops"] || []).each do |op|
|
| 793 |
+
name = op[0].to_s.to_sym
|
| 794 |
+
qubits = op[1].map(&:to_i)
|
| 795 |
+
params = (op[2] || []).map(&:to_f)
|
| 796 |
+
c.add(name, *qubits, params: params)
|
| 797 |
+
end
|
| 798 |
+
|
| 799 |
+
initial = req.fetch("initial", "0" * n).to_s
|
| 800 |
+
raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/)
|
| 801 |
+
init_idx = Integer(initial, 2)
|
| 802 |
+
|
| 803 |
+
shots = req.fetch("shots", 0).to_i
|
| 804 |
+
seed = req["seed"]
|
| 805 |
+
|
| 806 |
+
res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx)
|
| 807 |
+
|
| 808 |
+
probs = []
|
| 809 |
+
res.probabilities.each_with_index do |p, i|
|
| 810 |
+
probs << [bitstring_of(i, n), p] if p > 1e-9
|
| 811 |
+
end
|
| 812 |
+
probs.sort_by! { |_, p| -p }
|
| 813 |
+
|
| 814 |
+
sv = []
|
| 815 |
+
res.statevector.each_with_index do |a, i|
|
| 816 |
+
sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9
|
| 817 |
+
end
|
| 818 |
+
sv.sort_by! { |_, re, im| -(re * re + im * im) }
|
| 819 |
+
|
| 820 |
+
out = {
|
| 821 |
+
"diagram" => c.to_s,
|
| 822 |
+
"probs" => probs.first(24),
|
| 823 |
+
"counts" => res.counts,
|
| 824 |
+
"statevector" => sv.first(24),
|
| 825 |
+
"norm" => Math.sqrt(res.probabilities.sum)
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
if req["cpsc"]
|
| 829 |
+
verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"])
|
| 830 |
+
out["cpsc"] = {
|
| 831 |
+
"admitted" => verdict[:admitted],
|
| 832 |
+
"rejections" => verdict[:rejections],
|
| 833 |
+
"leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)),
|
| 834 |
+
"energy" => Mercury::CPSC.energy_expectation(
|
| 835 |
+
res.statevector, n,
|
| 836 |
+
j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f
|
| 837 |
+
)
|
| 838 |
+
}
|
| 839 |
+
end
|
| 840 |
+
|
| 841 |
+
Mercury::MiniJSON.generate(out)
|
| 842 |
+
rescue => e
|
| 843 |
+
Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" })
|
| 844 |
+
end
|
| 845 |
+
|
frontend/web/qsim.html
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<!--
|
| 3 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 4 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 5 |
+
-->
|
| 6 |
+
<html lang="en">
|
| 7 |
+
<head>
|
| 8 |
+
<meta charset="utf-8">
|
| 9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 10 |
+
<title>Mercury — Quantum Circuit Console · CPSC-QFT</title>
|
| 11 |
+
<style>
|
| 12 |
+
:root { --bg:#0a0e14; --panel:#111722; --line:#1e2a3a; --txt:#d6e2f0; --dim:#7d8ea8;
|
| 13 |
+
--acc:#5cc8ff; --ok:#5df08a; --bad:#ff6b6b; --warn:#ffd479; }
|
| 14 |
+
* { box-sizing:border-box; }
|
| 15 |
+
body { margin:0; background:var(--bg); color:var(--txt);
|
| 16 |
+
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px; }
|
| 17 |
+
header { padding:14px 20px; border-bottom:1px solid var(--line); display:flex;
|
| 18 |
+
align-items:baseline; gap:14px; flex-wrap:wrap; }
|
| 19 |
+
header h1 { margin:0; font-size:17px; letter-spacing:2px; color:var(--acc); }
|
| 20 |
+
header span { color:var(--dim); font-size:12px; }
|
| 21 |
+
#status { margin-left:auto; font-size:12px; color:var(--warn); }
|
| 22 |
+
#status.ready { color:var(--ok); }
|
| 23 |
+
main { display:grid; grid-template-columns:minmax(340px,430px) 1fr; gap:14px; padding:14px 20px; }
|
| 24 |
+
@media (max-width:900px){ main{ grid-template-columns:1fr; } }
|
| 25 |
+
.card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:12px; margin-bottom:14px; }
|
| 26 |
+
.card h2 { margin:0 0 10px; font-size:12px; letter-spacing:1.5px; color:var(--dim); text-transform:uppercase; }
|
| 27 |
+
label { display:block; color:var(--dim); font-size:11px; margin:8px 0 3px; }
|
| 28 |
+
input[type=text], input[type=number], textarea, select {
|
| 29 |
+
width:100%; background:#0a0e14; color:var(--txt); border:1px solid var(--line);
|
| 30 |
+
border-radius:5px; padding:7px 9px; font:inherit; font-size:12.5px; }
|
| 31 |
+
textarea { min-height:150px; resize:vertical; line-height:1.5; }
|
| 32 |
+
.row { display:flex; gap:8px; } .row > div { flex:1; }
|
| 33 |
+
button { background:#16202f; color:var(--txt); border:1px solid var(--line); border-radius:5px;
|
| 34 |
+
padding:7px 10px; font:inherit; font-size:12px; cursor:pointer; }
|
| 35 |
+
button:hover { border-color:var(--acc); color:var(--acc); }
|
| 36 |
+
button.primary { background:#12324a; border-color:var(--acc); color:var(--acc); font-weight:bold; }
|
| 37 |
+
button.primary:hover { background:#17405e; }
|
| 38 |
+
.palette { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
|
| 39 |
+
.palette button { padding:5px 9px; }
|
| 40 |
+
.presets { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
| 41 |
+
pre.diagram { background:#0a0e14; border:1px solid var(--line); border-radius:5px;
|
| 42 |
+
padding:10px; overflow-x:auto; font-size:12.5px; line-height:1.7; }
|
| 43 |
+
table { width:100%; border-collapse:collapse; font-size:12px; }
|
| 44 |
+
th, td { text-align:right; padding:4px 8px; border-bottom:1px solid var(--line); }
|
| 45 |
+
th { color:var(--dim); font-weight:normal; } td:first-child, th:first-child { text-align:left; }
|
| 46 |
+
.verdict-ok { color:var(--ok); font-weight:bold; } .verdict-bad { color:var(--bad); font-weight:bold; }
|
| 47 |
+
.rej { color:var(--bad); } .muted { color:var(--dim); }
|
| 48 |
+
canvas { width:100%; height:220px; background:#0a0e14; border:1px solid var(--line); border-radius:5px; }
|
| 49 |
+
.kv { display:flex; justify-content:space-between; padding:3px 0; }
|
| 50 |
+
.kv b { font-weight:normal; color:var(--dim); }
|
| 51 |
+
footer { padding:10px 20px 20px; color:var(--dim); font-size:11px; }
|
| 52 |
+
a { color:var(--acc); }
|
| 53 |
+
</style>
|
| 54 |
+
</head>
|
| 55 |
+
<body>
|
| 56 |
+
<header>
|
| 57 |
+
<h1>MERCURY</h1>
|
| 58 |
+
<a href="./index.html" style="color:#7d8ea8;text-decoration:none">site</a>
|
| 59 |
+
<span>quantum circuit console · CPSC-QFT frontend · Ruby/WASM statevector simulator</span>
|
| 60 |
+
<span id="status">● loading Ruby…</span>
|
| 61 |
+
</header>
|
| 62 |
+
<main>
|
| 63 |
+
<div>
|
| 64 |
+
<div class="card">
|
| 65 |
+
<h2>Circuit</h2>
|
| 66 |
+
<div class="row">
|
| 67 |
+
<div><label>Qubits (1–12)</label><input type="number" id="n" value="2" min="1" max="12"></div>
|
| 68 |
+
<div><label>Initial state (bitstring)</label><input type="text" id="initial" value="00"></div>
|
| 69 |
+
</div>
|
| 70 |
+
<label>Script — one op per line: <span class="muted">h 0 · rx 1.57 1 · cnot 0 1 · cp 0.79 1 0 · rzz 0.5 0 1 · xx 0 1 · measure 0</span></label>
|
| 71 |
+
<textarea id="dsl">h 0
|
| 72 |
+
cnot 0 1</textarea>
|
| 73 |
+
<label>Gate palette (appends)</label>
|
| 74 |
+
<div class="palette" id="palette"></div>
|
| 75 |
+
<label>Presets</label>
|
| 76 |
+
<div class="presets">
|
| 77 |
+
<button data-preset="bell">Bell pair</button>
|
| 78 |
+
<button data-preset="ghz">GHZ-3</button>
|
| 79 |
+
<button data-preset="qft">QFT-3</button>
|
| 80 |
+
<button data-preset="trotter">Trotter scattering</button>
|
| 81 |
+
<button data-preset="adversarial">Adversarial X</button>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="card">
|
| 85 |
+
<h2>Run</h2>
|
| 86 |
+
<div class="row">
|
| 87 |
+
<div><label>Shots (0 = statevector only)</label><input type="number" id="shots" value="1024" min="0" max="100000"></div>
|
| 88 |
+
<div><label>Seed (blank = random)</label><input type="text" id="seed" value="42"></div>
|
| 89 |
+
</div>
|
| 90 |
+
<label style="margin-top:10px"><input type="checkbox" id="cpsc" checked style="width:auto"> CPSC conservation check</label>
|
| 91 |
+
<div class="row">
|
| 92 |
+
<div><label>J (coupling)</label><input type="text" id="pj" value="1.0"></div>
|
| 93 |
+
<div><label>λ (transverse field)</label><input type="text" id="plam" value="0.0"></div>
|
| 94 |
+
<div><label style="margin-top:22px"><input type="checkbox" id="pmag" style="width:auto"> conserve ΣZ</label></div>
|
| 95 |
+
</div>
|
| 96 |
+
<div style="margin-top:10px"><button class="primary" id="run" style="width:100%; padding:10px">▶ Run circuit</button></div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="card">
|
| 99 |
+
<h2>Conservation (CPSC)</h2>
|
| 100 |
+
<div id="cpscOut"><span class="muted">Run a circuit to check symmetry-sector admission.</span></div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div>
|
| 104 |
+
<div class="card">
|
| 105 |
+
<h2>Circuit diagram</h2>
|
| 106 |
+
<pre class="diagram" id="diagram"><span class="muted">—</span></pre>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="card">
|
| 109 |
+
<h2>Measurement histogram</h2>
|
| 110 |
+
<canvas id="hist" width="800" height="220"></canvas>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="card">
|
| 113 |
+
<h2>Statevector (top amplitudes)</h2>
|
| 114 |
+
<table><thead><tr><th>basis</th><th>Re</th><th>Im</th><th>P</th></tr></thead>
|
| 115 |
+
<tbody id="svBody"><tr><td colspan="4" class="muted">—</td></tr></tbody></table>
|
| 116 |
+
</div>
|
| 117 |
+
<div class="card">
|
| 118 |
+
<h2>Probabilities</h2>
|
| 119 |
+
<table><thead><tr><th>basis</th><th>P</th></tr></thead>
|
| 120 |
+
<tbody id="probBody"><tr><td colspan="2" class="muted">—</td></tr></tbody></table>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</main>
|
| 124 |
+
<footer>
|
| 125 |
+
Mercury is a dependency-free Ruby statevector simulator (up to 12 qubits in-browser). The CPSC panel mirrors
|
| 126 |
+
<span class="muted">src/cpsc/analyzer.py</span>: gates are admitted only if they preserve the Z<sub>2</sub> parity sector —
|
| 127 |
+
a compile-time rejection rule, not post-selection. Trotter preset evolves the transverse-field Ising H under test.
|
| 128 |
+
</footer>
|
| 129 |
+
|
| 130 |
+
<script src="https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.3.0/dist/browser.umd.js"></script>
|
| 131 |
+
<script>
|
| 132 |
+
/* Mercury WASM boot — same pattern as the Intentsim console. */
|
| 133 |
+
const W = { vm: null, ready: false };
|
| 134 |
+
const RUBY_WASM_URL = "https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.3.0/dist/ruby+stdlib.wasm";
|
| 135 |
+
function setBoot(t, ok) {
|
| 136 |
+
const el = document.getElementById("status");
|
| 137 |
+
el.textContent = t;
|
| 138 |
+
el.classList.toggle("ready", !!ok);
|
| 139 |
+
document.getElementById("run").disabled = !ok;
|
| 140 |
+
}
|
| 141 |
+
function rbEscape(s) { return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); }
|
| 142 |
+
function mercuryRun(req) {
|
| 143 |
+
const code = `handle_request('${rbEscape(JSON.stringify(req))}')`;
|
| 144 |
+
return JSON.parse(W.vm.eval(code).toString());
|
| 145 |
+
}
|
| 146 |
+
async function bootRuby() {
|
| 147 |
+
try {
|
| 148 |
+
if (!window["ruby-wasm-wasi"]) throw new Error("ruby-wasm-wasi bundle missing");
|
| 149 |
+
setBoot("● loading ruby 3.2 webassembly (≈32 mb)…");
|
| 150 |
+
const { DefaultRubyVM } = window["ruby-wasm-wasi"];
|
| 151 |
+
const response = await fetch(RUBY_WASM_URL);
|
| 152 |
+
if (!response.ok) throw new Error("wasm fetch failed: " + response.status);
|
| 153 |
+
setBoot("● compiling webassembly…");
|
| 154 |
+
const module = await WebAssembly.compile(await response.arrayBuffer());
|
| 155 |
+
setBoot("● starting ruby vm…");
|
| 156 |
+
const { vm } = await DefaultRubyVM(module);
|
| 157 |
+
setBoot("● evaluating mercury bundle…");
|
| 158 |
+
const src = await (await fetch("mercury_bundle.rb")).text();
|
| 159 |
+
if (!src.includes("module Mercury")) throw new Error("mercury bundle not found");
|
| 160 |
+
vm.eval(src);
|
| 161 |
+
W.vm = vm; W.ready = true;
|
| 162 |
+
setBoot("● ruby ready — live simulation", true);
|
| 163 |
+
} catch (e) {
|
| 164 |
+
setBoot("● wasm unreachable (" + (e.message || e) + ")");
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
const PARAM_N = {rx:1, ry:1, rz:1, cp:1, rzz:1, u:3};
|
| 169 |
+
const GATE_LIST = ["h","x","y","z","s","t","rx","ry","rz","cnot","cz","cp","swap","rzz","xx","ccx"];
|
| 170 |
+
|
| 171 |
+
function parseDSL(text) {
|
| 172 |
+
const ops = [];
|
| 173 |
+
for (const raw of text.split("\n")) {
|
| 174 |
+
const line = raw.trim();
|
| 175 |
+
if (!line || line.startsWith("#")) continue;
|
| 176 |
+
const t = line.split(/\s+/);
|
| 177 |
+
const gate = t[0].toLowerCase();
|
| 178 |
+
let idx = 1, params = [];
|
| 179 |
+
const pn = PARAM_N[gate] || 0;
|
| 180 |
+
for (let k = 0; k < pn; k++) {
|
| 181 |
+
if (idx >= t.length) throw new Error("missing param for " + gate);
|
| 182 |
+
const v = parseFloat(t[idx++]);
|
| 183 |
+
if (!isFinite(v)) throw new Error("bad param " + t[idx-1]);
|
| 184 |
+
params.push(v);
|
| 185 |
+
}
|
| 186 |
+
const qubits = [];
|
| 187 |
+
while (idx < t.length) {
|
| 188 |
+
const q = parseInt(t[idx++], 10);
|
| 189 |
+
if (!isFinite(q) || q < 0) throw new Error("bad qubit");
|
| 190 |
+
qubits.push(q);
|
| 191 |
+
}
|
| 192 |
+
ops.push([gate, qubits, params]);
|
| 193 |
+
}
|
| 194 |
+
return ops;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
document.getElementById("run").disabled = true;
|
| 198 |
+
document.addEventListener("DOMContentLoaded", bootRuby);
|
| 199 |
+
|
| 200 |
+
// gate palette
|
| 201 |
+
const pal = document.getElementById("palette");
|
| 202 |
+
for (const g of GATE_LIST) {
|
| 203 |
+
const b = document.createElement("button");
|
| 204 |
+
b.textContent = g.toUpperCase();
|
| 205 |
+
b.onclick = () => {
|
| 206 |
+
const dsl = document.getElementById("dsl");
|
| 207 |
+
const pn = PARAM_N[g] || 0;
|
| 208 |
+
dsl.value += (dsl.value.endsWith("\n") || dsl.value === "" ? "" : "\n") + g + " " + "0.5 ".repeat(pn) + "0\n";
|
| 209 |
+
dsl.focus();
|
| 210 |
+
};
|
| 211 |
+
pal.appendChild(b);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
const PRESETS = {
|
| 215 |
+
bell: { n:"2", initial:"00", dsl:"h 0\ncnot 0 1" },
|
| 216 |
+
ghz: { n:"3", initial:"000", dsl:"h 0\ncnot 0 1\ncnot 1 2" },
|
| 217 |
+
qft: { n:"3", initial:"000",
|
| 218 |
+
dsl:"h 0\ncp 1.5708 1 0\ncp 0.7854 2 0\nh 1\ncp 1.5708 2 1\nh 2\nswap 0 2" },
|
| 219 |
+
trotter:{ n:"4", initial:"0011", lam:"0.0",
|
| 220 |
+
dsl:"# Trotter: H = -J ZZ + lam X, J=1, t=0.4, 8 steps\n" +
|
| 221 |
+
Array(8).fill(
|
| 222 |
+
"rzz -0.1 0 1\nrzz -0.1 1 2\nrzz -0.1 2 3\nrzz -0.1 3 0"
|
| 223 |
+
).join("\n") },
|
| 224 |
+
adversarial: { n:"4", initial:"0000",
|
| 225 |
+
dsl:"rzz 0.5 0 1\nx 2\nrzz 0.5 2 3" }
|
| 226 |
+
};
|
| 227 |
+
document.querySelectorAll("[data-preset]").forEach(b => b.onclick = () => {
|
| 228 |
+
const p = PRESETS[b.dataset.preset];
|
| 229 |
+
document.getElementById("n").value = p.n;
|
| 230 |
+
document.getElementById("initial").value = p.initial;
|
| 231 |
+
document.getElementById("dsl").value = p.dsl;
|
| 232 |
+
if (p.lam !== undefined) document.getElementById("plam").value = p.lam;
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
document.getElementById("run").onclick = () => {
|
| 236 |
+
let req;
|
| 237 |
+
try {
|
| 238 |
+
const n = parseInt(document.getElementById("n").value, 10);
|
| 239 |
+
req = {
|
| 240 |
+
n, initial: document.getElementById("initial").value.trim(),
|
| 241 |
+
ops: parseDSL(document.getElementById("dsl").value),
|
| 242 |
+
shots: parseInt(document.getElementById("shots").value, 10) || 0,
|
| 243 |
+
seed: document.getElementById("seed").value.trim() === "" ? null
|
| 244 |
+
: parseInt(document.getElementById("seed").value, 10),
|
| 245 |
+
cpsc: document.getElementById("cpsc").checked,
|
| 246 |
+
magnetization: document.getElementById("pmag").checked,
|
| 247 |
+
j: parseFloat(document.getElementById("pj").value) || 0,
|
| 248 |
+
lam: parseFloat(document.getElementById("plam").value) || 0
|
| 249 |
+
};
|
| 250 |
+
} catch (e) { alert("Script error: " + e.message); return; }
|
| 251 |
+
let out;
|
| 252 |
+
try { out = mercuryRun(req); }
|
| 253 |
+
catch (e) { alert("Simulator error: " + e.message); return; }
|
| 254 |
+
if (out.error) { alert("Simulator error: " + out.error); return; }
|
| 255 |
+
render(out);
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
function render(out) {
|
| 259 |
+
document.getElementById("diagram").textContent = out.diagram;
|
| 260 |
+
// histogram: prefer counts, fall back to probs
|
| 261 |
+
const data = Object.keys(out.counts).length ? out.counts :
|
| 262 |
+
Object.fromEntries(out.probs.map(([b, p]) => [b, p]));
|
| 263 |
+
drawHist(data);
|
| 264 |
+
const svb = document.getElementById("svBody"); svb.innerHTML = "";
|
| 265 |
+
for (const [b, re, im] of out.statevector) {
|
| 266 |
+
const p = re*re + im*im;
|
| 267 |
+
svb.innerHTML += `<tr><td>|${b}⟩</td><td>${re.toFixed(4)}</td><td>${im.toFixed(4)}</td><td>${p.toFixed(4)}</td></tr>`;
|
| 268 |
+
}
|
| 269 |
+
const prb = document.getElementById("probBody"); prb.innerHTML = "";
|
| 270 |
+
for (const [b, p] of out.probs) prb.innerHTML += `<tr><td>|${b}⟩</td><td>${p.toFixed(4)}</td></tr>`;
|
| 271 |
+
const c = document.getElementById("cpscOut");
|
| 272 |
+
if (out.cpsc) {
|
| 273 |
+
const v = out.cpsc;
|
| 274 |
+
let html = v.admitted
|
| 275 |
+
? `<div class="verdict-ok">✓ ADMITTED — every gate preserves the symmetry sector</div>`
|
| 276 |
+
: `<div class="verdict-bad">✗ REJECTED at compile time</div>`;
|
| 277 |
+
for (const r of v.rejections) html += `<div class="rej">· ${r.gate}: ${r.reason}</div>`;
|
| 278 |
+
html += `<div class="kv"><b>parity leakage</b><span>${v.leakage.toExponential(2)}</span></div>`;
|
| 279 |
+
html += `<div class="kv"><b>⟨H⟩ (J,λ as set)</b><span>${v.energy.toFixed(6)}</span></div>`;
|
| 280 |
+
html += `<div class="kv"><b>norm</b><span>${out.norm.toFixed(6)}</span></div>`;
|
| 281 |
+
c.innerHTML = html;
|
| 282 |
+
} else c.innerHTML = '<span class="muted">CPSC check disabled.</span>';
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function drawHist(data) {
|
| 286 |
+
const cv = document.getElementById("hist"), ctx = cv.getContext("2d");
|
| 287 |
+
const W = cv.width, H = cv.height;
|
| 288 |
+
ctx.clearRect(0, 0, W, H);
|
| 289 |
+
const keys = Object.keys(data).sort();
|
| 290 |
+
if (!keys.length) return;
|
| 291 |
+
const max = Math.max(...Object.values(data));
|
| 292 |
+
const bw = W / keys.length;
|
| 293 |
+
ctx.fillStyle = "#5cc8ff"; ctx.strokeStyle = "#1e2a3a";
|
| 294 |
+
ctx.font = "11px monospace"; ctx.textAlign = "center";
|
| 295 |
+
keys.forEach((k, i) => {
|
| 296 |
+
const h = (data[k] / max) * (H - 40);
|
| 297 |
+
const x = i * bw;
|
| 298 |
+
ctx.fillRect(x + 3, H - 25 - h, bw - 6, h);
|
| 299 |
+
ctx.fillStyle = "#7d8ea8";
|
| 300 |
+
ctx.fillText(k, x + bw / 2, H - 10);
|
| 301 |
+
ctx.fillText(String(Math.round(data[k] * 100) / 100), x + bw / 2, H - 28 - h);
|
| 302 |
+
ctx.fillStyle = "#5cc8ff";
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
</script>
|
| 306 |
+
</body>
|
| 307 |
+
</html>
|
lean/CPSC.lean
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/-
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
-/
|
| 5 |
+
|
| 6 |
+
import CPSC.Certificate
|
| 7 |
+
import CPSC.Axioms
|
| 8 |
+
import CPSC.Lemmas
|
lean/CPSC/Axioms.lean
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import CPSC.Certificate
|
| 2 |
+
|
| 3 |
+
/-
|
| 4 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 5 |
+
|
| 6 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 7 |
+
-/
|
| 8 |
+
|
| 9 |
+
/-!
|
| 10 |
+
Operator axioms.
|
| 11 |
+
|
| 12 |
+
These are not proved. They are the commutation facts the compiler is allowed
|
| 13 |
+
to use. A later matrix development has to discharge them; until then every
|
| 14 |
+
lemma in `CPSC.Lemmas` is conditional on this file.
|
| 15 |
+
-/
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
namespace CPSC
|
| 19 |
+
|
| 20 |
+
axiom Unitary : Type
|
| 21 |
+
|
| 22 |
+
axiom idU : Unitary
|
| 23 |
+
|
| 24 |
+
axiom mul : Unitary → Unitary → Unitary
|
| 25 |
+
|
| 26 |
+
axiom gateOp : GateKind → Unitary
|
| 27 |
+
|
| 28 |
+
/-- Commutation with magnetization, the λ = 0 invariant. -/
|
| 29 |
+
axiom commMag : Unitary → Prop
|
| 30 |
+
|
| 31 |
+
/-- Commutation with ∏ X, the λ ≠ 0 invariant. -/
|
| 32 |
+
axiom commSpin : Unitary → Prop
|
| 33 |
+
|
| 34 |
+
axiom id_comm_mag : commMag idU
|
| 35 |
+
|
| 36 |
+
axiom id_comm_spin : commSpin idU
|
| 37 |
+
|
| 38 |
+
axiom mul_comm_mag (U V : Unitary) :
|
| 39 |
+
commMag U → commMag V → commMag (mul U V)
|
| 40 |
+
|
| 41 |
+
axiom mul_comm_spin (U V : Unitary) :
|
| 42 |
+
commSpin U → commSpin V → commSpin (mul U V)
|
| 43 |
+
|
| 44 |
+
axiom mul_assoc (U V W : Unitary) :
|
| 45 |
+
mul (mul U V) W = mul U (mul V W)
|
| 46 |
+
|
| 47 |
+
axiom mul_id_left (U : Unitary) : mul idU U = U
|
| 48 |
+
|
| 49 |
+
axiom mul_id_right (U : Unitary) : mul U idU = U
|
| 50 |
+
|
| 51 |
+
axiom zz_mag (i j : Nat) : commMag (gateOp (.zz i j))
|
| 52 |
+
|
| 53 |
+
axiom zz_spin (i j : Nat) : commSpin (gateOp (.zz i j))
|
| 54 |
+
|
| 55 |
+
axiom z_mag (i : Nat) : commMag (gateOp (.loneZ i))
|
| 56 |
+
|
| 57 |
+
axiom pair_spin (i j : Nat) : commSpin (gateOp (.parityEvenPair i j))
|
| 58 |
+
|
| 59 |
+
axiom flip_spin (i : Nat) : commSpin (gateOp (.transverseFlip i))
|
| 60 |
+
|
| 61 |
+
axiom flip_not_mag (i : Nat) : ¬ commMag (gateOp (.transverseFlip i))
|
| 62 |
+
|
| 63 |
+
axiom pair_not_mag (i j : Nat) : ¬ commMag (gateOp (.parityEvenPair i j))
|
| 64 |
+
|
| 65 |
+
axiom z_not_spin (i : Nat) : ¬ commSpin (gateOp (.loneZ i))
|
| 66 |
+
|
| 67 |
+
/-- Bridge: the Boolean analyzer is sound for magnetization. -/
|
| 68 |
+
axiom mag_of_bool (g : GateKind) :
|
| 69 |
+
preservesMagnetization g = true → commMag (gateOp g)
|
| 70 |
+
|
| 71 |
+
/-- Bridge: the Boolean analyzer is sound for the spin flip. -/
|
| 72 |
+
axiom spin_of_bool (g : GateKind) :
|
| 73 |
+
preservesSpinFlip g = true → commSpin (gateOp g)
|
| 74 |
+
|
| 75 |
+
/-- Completeness: exact evolution generated by a commuting Hamiltonian stays in the sector. -/
|
| 76 |
+
axiom exact_evolution_in_manifold
|
| 77 |
+
(mode : Mode) (H : Unitary) :
|
| 78 |
+
(mode = .ising → commMag H) →
|
| 79 |
+
(mode = .spinFlip → commSpin H) →
|
| 80 |
+
(mode = .ising → commMag H) ∧ (mode = .spinFlip → commSpin H)
|
| 81 |
+
|
| 82 |
+
/-- A rejected generator is not repaired by right-multiplication. -/
|
| 83 |
+
axiom reject_not_absorbed_mag (g : GateKind) (V : Unitary) :
|
| 84 |
+
¬ commMag (gateOp g) → ¬ commMag (mul (gateOp g) V)
|
| 85 |
+
|
| 86 |
+
axiom reject_not_absorbed_spin (g : GateKind) (V : Unitary) :
|
| 87 |
+
¬ commSpin (gateOp g) → ¬ commSpin (mul (gateOp g) V)
|
| 88 |
+
|
| 89 |
+
end CPSC
|
lean/CPSC/Certificate.lean
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/-
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
|
| 4 |
+
This program and the accompanying materials are made available under the
|
| 5 |
+
terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, or, at your
|
| 6 |
+
option, the GNU Affero General Public License, Version 3 only. Both options
|
| 7 |
+
are strict copyleft. There is no classpath exception and no permissive
|
| 8 |
+
relicensing.
|
| 9 |
+
|
| 10 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 11 |
+
-/
|
| 12 |
+
|
| 13 |
+
/-!
|
| 14 |
+
Boolean certificate layer.
|
| 15 |
+
|
| 16 |
+
These lemmas are proved. They do not mention operators. The operator axioms
|
| 17 |
+
live in `CPSC.Axioms`, and the bridge lemmas in `CPSC.Lemmas`.
|
| 18 |
+
-/
|
| 19 |
+
|
| 20 |
+
namespace CPSC
|
| 21 |
+
|
| 22 |
+
inductive GateKind where
|
| 23 |
+
| zz : Nat → Nat → GateKind
|
| 24 |
+
| parityEvenPair : Nat → Nat → GateKind
|
| 25 |
+
| transverseFlip : Nat → GateKind
|
| 26 |
+
| loneZ : Nat → GateKind
|
| 27 |
+
deriving DecidableEq, Repr
|
| 28 |
+
|
| 29 |
+
inductive Mode where
|
| 30 |
+
| ising
|
| 31 |
+
| spinFlip
|
| 32 |
+
deriving DecidableEq, Repr
|
| 33 |
+
|
| 34 |
+
/-- λ = 0. A single transverse flip changes magnetization. -/
|
| 35 |
+
def preservesMagnetization : GateKind → Bool
|
| 36 |
+
| .zz _ _ => true
|
| 37 |
+
| .parityEvenPair _ _ => false
|
| 38 |
+
| .transverseFlip _ => false
|
| 39 |
+
| .loneZ _ => true
|
| 40 |
+
|
| 41 |
+
/-- λ ≠ 0. The TFIM symmetry is ∏ X, which a lone Z anticommutes with. -/
|
| 42 |
+
def preservesSpinFlip : GateKind → Bool
|
| 43 |
+
| .zz _ _ => true
|
| 44 |
+
| .parityEvenPair _ _ => true
|
| 45 |
+
| .transverseFlip _ => true
|
| 46 |
+
| .loneZ _ => false
|
| 47 |
+
|
| 48 |
+
def preserves : Mode → GateKind → Bool
|
| 49 |
+
| .ising, g => preservesMagnetization g
|
| 50 |
+
| .spinFlip, g => preservesSpinFlip g
|
| 51 |
+
|
| 52 |
+
def allPreserve (p : GateKind → Bool) : List GateKind → Bool
|
| 53 |
+
| [] => true
|
| 54 |
+
| g :: rest => p g && allPreserve p rest
|
| 55 |
+
|
| 56 |
+
def admitted (mode : Mode) (gs : List GateKind) : Bool :=
|
| 57 |
+
allPreserve (preserves mode) gs
|
| 58 |
+
|
| 59 |
+
theorem nil_preserves (p : GateKind → Bool) : allPreserve p [] = true := rfl
|
| 60 |
+
|
| 61 |
+
theorem bool_and_parts {a b : Bool} (h : (a && b) = true) : a = true ∧ b = true := by
|
| 62 |
+
cases a <;> cases b <;> simp_all
|
| 63 |
+
|
| 64 |
+
theorem cons_preserves
|
| 65 |
+
(p : GateKind → Bool) (g : GateKind) (rest : List GateKind)
|
| 66 |
+
(hg : p g = true) (hr : allPreserve p rest = true) :
|
| 67 |
+
allPreserve p (g :: rest) = true := by
|
| 68 |
+
simp [allPreserve, hg, hr]
|
| 69 |
+
|
| 70 |
+
theorem allPreserve_cons_iff
|
| 71 |
+
(p : GateKind → Bool) (g : GateKind) (rest : List GateKind) :
|
| 72 |
+
allPreserve p (g :: rest) = true ↔ p g = true ∧ allPreserve p rest = true := by
|
| 73 |
+
constructor
|
| 74 |
+
· intro h
|
| 75 |
+
exact bool_and_parts h
|
| 76 |
+
· intro h
|
| 77 |
+
exact cons_preserves p g rest h.1 h.2
|
| 78 |
+
|
| 79 |
+
theorem allPreserve_append
|
| 80 |
+
(p : GateKind → Bool) (xs ys : List GateKind) :
|
| 81 |
+
allPreserve p (xs ++ ys) = (allPreserve p xs && allPreserve p ys) := by
|
| 82 |
+
induction xs with
|
| 83 |
+
| nil => simp [allPreserve]
|
| 84 |
+
| cons g rest ih =>
|
| 85 |
+
simp [allPreserve, List.cons_append, ih, Bool.and_assoc]
|
| 86 |
+
|
| 87 |
+
theorem allPreserve_of_append
|
| 88 |
+
(p : GateKind → Bool) {xs ys : List GateKind}
|
| 89 |
+
(h : allPreserve p (xs ++ ys) = true) :
|
| 90 |
+
allPreserve p xs = true ∧ allPreserve p ys = true :=
|
| 91 |
+
bool_and_parts (by simpa [allPreserve_append] using h)
|
| 92 |
+
|
| 93 |
+
theorem transverse_breaks_ising (i : Nat) :
|
| 94 |
+
preservesMagnetization (.transverseFlip i) = false := rfl
|
| 95 |
+
|
| 96 |
+
theorem pair_breaks_ising (i j : Nat) :
|
| 97 |
+
preservesMagnetization (.parityEvenPair i j) = false := rfl
|
| 98 |
+
|
| 99 |
+
theorem loneZ_breaks_spin_flip (i : Nat) :
|
| 100 |
+
preservesSpinFlip (.loneZ i) = false := rfl
|
| 101 |
+
|
| 102 |
+
theorem zz_preserves_both (i j : Nat) :
|
| 103 |
+
preservesMagnetization (.zz i j) = true ∧ preservesSpinFlip (.zz i j) = true := by
|
| 104 |
+
constructor <;> rfl
|
| 105 |
+
|
| 106 |
+
theorem flip_preserves_spin (i : Nat) :
|
| 107 |
+
preservesSpinFlip (.transverseFlip i) = true := rfl
|
| 108 |
+
|
| 109 |
+
theorem z_preserves_ising (i : Nat) :
|
| 110 |
+
preservesMagnetization (.loneZ i) = true := rfl
|
| 111 |
+
|
| 112 |
+
theorem mode_switch_flip (i : Nat) :
|
| 113 |
+
preserves .ising (.transverseFlip i) = false ∧
|
| 114 |
+
preserves .spinFlip (.transverseFlip i) = true := by
|
| 115 |
+
constructor <;> rfl
|
| 116 |
+
|
| 117 |
+
theorem mode_switch_z (i : Nat) :
|
| 118 |
+
preserves .ising (.loneZ i) = true ∧
|
| 119 |
+
preserves .spinFlip (.loneZ i) = false := by
|
| 120 |
+
constructor <;> rfl
|
| 121 |
+
|
| 122 |
+
def isingRing4 : List GateKind :=
|
| 123 |
+
[.zz 0 1, .zz 1 2, .zz 2 3, .zz 3 0]
|
| 124 |
+
|
| 125 |
+
theorem isingRing4_admitted : allPreserve preservesMagnetization isingRing4 = true := rfl
|
| 126 |
+
|
| 127 |
+
def spinFlip4 : List GateKind :=
|
| 128 |
+
isingRing4 ++ [.transverseFlip 0, .transverseFlip 1, .transverseFlip 2, .transverseFlip 3]
|
| 129 |
+
|
| 130 |
+
theorem spinFlip4_admitted : allPreserve preservesSpinFlip spinFlip4 = true := rfl
|
| 131 |
+
|
| 132 |
+
theorem adversarial_ising_rejected :
|
| 133 |
+
allPreserve preservesMagnetization (isingRing4 ++ [.transverseFlip 2]) = false := rfl
|
| 134 |
+
|
| 135 |
+
theorem adversarial_spin_flip_rejected :
|
| 136 |
+
allPreserve preservesSpinFlip (spinFlip4 ++ [.loneZ 1]) = false := rfl
|
| 137 |
+
|
| 138 |
+
theorem rejected_cons
|
| 139 |
+
(p : GateKind → Bool) (g : GateKind) (rest : List GateKind)
|
| 140 |
+
(hg : p g = false) :
|
| 141 |
+
allPreserve p (g :: rest) = false := by
|
| 142 |
+
simp [allPreserve, hg]
|
| 143 |
+
|
| 144 |
+
theorem adversarial_head_rejected (i : Nat) (rest : List GateKind) :
|
| 145 |
+
admitted .ising (.transverseFlip i :: rest) = false :=
|
| 146 |
+
rejected_cons _ _ _ (transverse_breaks_ising i)
|
| 147 |
+
|
| 148 |
+
/-- Orbit closure is a predicate on supports. A ring of length n is the full orbit of one bond. -/
|
| 149 |
+
def bondOrbit (n : Nat) : List GateKind :=
|
| 150 |
+
List.range n |>.map fun i => .zz i ((i + 1) % n)
|
| 151 |
+
|
| 152 |
+
theorem bondOrbit_four_agrees : bondOrbit 4 = isingRing4 := by
|
| 153 |
+
rfl
|
| 154 |
+
|
| 155 |
+
end CPSC
|
lean/CPSC/Emitted.lean
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/-
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
-/
|
| 5 |
+
|
| 6 |
+
import CPSC.Certificate
|
| 7 |
+
|
| 8 |
+
namespace CPSC.Emitted
|
| 9 |
+
|
| 10 |
+
-- emitted by cpsc.certificate; do not edit by hand
|
| 11 |
+
def emittedHash : String := "e92f82910385ae3c46acc5479d57f4c6357cbbf9bf77d668e469a64b281db8e5"
|
| 12 |
+
def emittedGates : List CPSC.GateKind := [.zz 0 1, .zz 1 2, .zz 2 3, .zz 3 0]
|
| 13 |
+
theorem emitted_admitted : CPSC.allPreserve CPSC.preservesMagnetization emittedGates = true := by
|
| 14 |
+
rfl
|
| 15 |
+
|
| 16 |
+
end CPSC.Emitted
|
lean/CPSC/Lemmas.lean
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import CPSC.Axioms
|
| 2 |
+
|
| 3 |
+
/-
|
| 4 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 5 |
+
|
| 6 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 7 |
+
-/
|
| 8 |
+
|
| 9 |
+
/-!
|
| 10 |
+
Lemmas derived from the operator axioms.
|
| 11 |
+
|
| 12 |
+
Nothing in this file introduces a new axiom. Each result is either a Boolean
|
| 13 |
+
fact from `CPSC.Certificate` or a consequence of `CPSC.Axioms`.
|
| 14 |
+
-/
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
namespace CPSC
|
| 18 |
+
|
| 19 |
+
noncomputable def circuitOp : List GateKind → Unitary
|
| 20 |
+
| [] => idU
|
| 21 |
+
| g :: rest => mul (gateOp g) (circuitOp rest)
|
| 22 |
+
|
| 23 |
+
theorem circuitOp_nil : circuitOp [] = idU := rfl
|
| 24 |
+
|
| 25 |
+
theorem circuitOp_cons (g : GateKind) (rest : List GateKind) :
|
| 26 |
+
circuitOp (g :: rest) = mul (gateOp g) (circuitOp rest) := rfl
|
| 27 |
+
|
| 28 |
+
theorem circuitOp_append (xs ys : List GateKind) :
|
| 29 |
+
circuitOp (xs ++ ys) = mul (circuitOp xs) (circuitOp ys) := by
|
| 30 |
+
induction xs with
|
| 31 |
+
| nil =>
|
| 32 |
+
rw [List.nil_append, circuitOp_nil, mul_id_left]
|
| 33 |
+
| cons g rest ih =>
|
| 34 |
+
rw [List.cons_append, circuitOp_cons, circuitOp_cons, ih, mul_assoc]
|
| 35 |
+
|
| 36 |
+
theorem circuit_comm_mag
|
| 37 |
+
(gs : List GateKind) (h : allPreserve preservesMagnetization gs = true) :
|
| 38 |
+
commMag (circuitOp gs) := by
|
| 39 |
+
induction gs with
|
| 40 |
+
| nil =>
|
| 41 |
+
exact id_comm_mag
|
| 42 |
+
| cons g rest ih =>
|
| 43 |
+
have parts := bool_and_parts h
|
| 44 |
+
exact mul_comm_mag _ _ (mag_of_bool g parts.1) (ih parts.2)
|
| 45 |
+
|
| 46 |
+
theorem circuit_comm_spin
|
| 47 |
+
(gs : List GateKind) (h : allPreserve preservesSpinFlip gs = true) :
|
| 48 |
+
commSpin (circuitOp gs) := by
|
| 49 |
+
induction gs with
|
| 50 |
+
| nil =>
|
| 51 |
+
exact id_comm_spin
|
| 52 |
+
| cons g rest ih =>
|
| 53 |
+
have parts := bool_and_parts h
|
| 54 |
+
exact mul_comm_spin _ _ (spin_of_bool g parts.1) (ih parts.2)
|
| 55 |
+
|
| 56 |
+
theorem admitted_commutes
|
| 57 |
+
(mode : Mode) (gs : List GateKind) (h : admitted mode gs = true) :
|
| 58 |
+
(mode = .ising → commMag (circuitOp gs)) ∧
|
| 59 |
+
(mode = .spinFlip → commSpin (circuitOp gs)) := by
|
| 60 |
+
cases mode with
|
| 61 |
+
| ising =>
|
| 62 |
+
refine And.intro ?_ ?_
|
| 63 |
+
· intro _
|
| 64 |
+
exact circuit_comm_mag gs h
|
| 65 |
+
· intro contra
|
| 66 |
+
nomatch contra
|
| 67 |
+
| spinFlip =>
|
| 68 |
+
refine And.intro ?_ ?_
|
| 69 |
+
· intro contra
|
| 70 |
+
nomatch contra
|
| 71 |
+
· intro _
|
| 72 |
+
exact circuit_comm_spin gs h
|
| 73 |
+
|
| 74 |
+
theorem ising_ring_commutes : commMag (circuitOp isingRing4) :=
|
| 75 |
+
circuit_comm_mag isingRing4 isingRing4_admitted
|
| 76 |
+
|
| 77 |
+
theorem spin_flip_ring_commutes : commSpin (circuitOp spinFlip4) :=
|
| 78 |
+
circuit_comm_spin spinFlip4 spinFlip4_admitted
|
| 79 |
+
|
| 80 |
+
theorem transverse_circuit_not_ising (i : Nat) (rest : List GateKind) :
|
| 81 |
+
¬ commMag (circuitOp (.transverseFlip i :: rest)) :=
|
| 82 |
+
reject_not_absorbed_mag _ _ (flip_not_mag i)
|
| 83 |
+
|
| 84 |
+
theorem loneZ_circuit_not_spin (i : Nat) (rest : List GateKind) :
|
| 85 |
+
¬ commSpin (circuitOp (.loneZ i :: rest)) :=
|
| 86 |
+
reject_not_absorbed_spin _ _ (z_not_spin i)
|
| 87 |
+
|
| 88 |
+
theorem concatenated_ising
|
| 89 |
+
(xs ys : List GateKind)
|
| 90 |
+
(hx : allPreserve preservesMagnetization xs = true)
|
| 91 |
+
(hy : allPreserve preservesMagnetization ys = true) :
|
| 92 |
+
commMag (circuitOp (xs ++ ys)) := by
|
| 93 |
+
have hjoin : allPreserve preservesMagnetization (xs ++ ys) = true := by
|
| 94 |
+
rw [allPreserve_append, hx, hy]
|
| 95 |
+
rfl
|
| 96 |
+
exact circuit_comm_mag (xs ++ ys) hjoin
|
| 97 |
+
|
| 98 |
+
theorem completeness_of_admitted
|
| 99 |
+
(mode : Mode) (gs : List GateKind) (h : admitted mode gs = true) :
|
| 100 |
+
(mode = .ising → commMag (circuitOp gs)) ∧
|
| 101 |
+
(mode = .spinFlip → commSpin (circuitOp gs)) :=
|
| 102 |
+
admitted_commutes mode gs h
|
| 103 |
+
|
| 104 |
+
/-- The open matrix obligation, named so a later development can replace the axioms. -/
|
| 105 |
+
def OperatorCommutation : Prop :=
|
| 106 |
+
∀ g : GateKind, preservesMagnetization g = true → commMag (gateOp g)
|
| 107 |
+
|
| 108 |
+
theorem operator_commutation_from_bridge : OperatorCommutation :=
|
| 109 |
+
mag_of_bool
|
| 110 |
+
|
| 111 |
+
end CPSC
|
lean/lake-manifest.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"version": "1.1.0",
|
| 2 |
+
"packagesDir": ".lake/packages",
|
| 3 |
+
"packages": [],
|
| 4 |
+
"name": "CPSC",
|
| 5 |
+
"lakeDir": ".lake"}
|
lean/lakefile.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, or, at your
|
| 6 |
+
# option, the GNU Affero General Public License, Version 3 only.
|
| 7 |
+
#
|
| 8 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 9 |
+
# *******************************************************************************
|
| 10 |
+
|
| 11 |
+
name = "CPSC"
|
| 12 |
+
version = "0.1.0"
|
| 13 |
+
defaultTargets = ["CPSC"]
|
| 14 |
+
|
| 15 |
+
[[lean_lib]]
|
| 16 |
+
name = "CPSC"
|
lean/lean-toolchain
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
leanprover/lean4:v4.22.0
|
paper/ABSTRACT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
-->
|
| 5 |
+
|
| 6 |
+
# Abstract
|
| 7 |
+
|
| 8 |
+
We implement a compilation architecture for lattice scattering in which the symmetries of the target Hamiltonian are constraints on synthesis rather than filters on shots. The working model is the periodic transverse-field Ising chain, the qubit shadow of 1+1D φ⁴, not continuum φ⁴. Two facts fix the compiler. The quartic written as ∑ᵢ Xᵢ⁴ is an identity. Z-parity is a symmetry only at λ = 0; for λ ≠ 0 the symmetry is the spin flip ∏ᵢ Xᵢ.
|
| 9 |
+
|
| 10 |
+
The repository supplies a symmetry analyzer, a translation-orbit check, a first-order Trotter product of admitted gates, a Q# emission of those factors, and a certificate whose SHA-256 digest is bound to the gate list. On a 4-site chain the Ising schedule has Z-parity leakage 0, and the spin-flip schedule has ∏ X leakage 0 within roundoff. Inserting a forbidden gate fails compilation. A Boolean Lean skeleton discharges admission for the concrete rings; operator commutation on (ℂ²)⊗N remains an open obligation.
|
paper/PAPER.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 4 |
+
-->
|
| 5 |
+
|
| 6 |
+
# Conservation-preserving compilation for a 1+1D lattice shadow of φ⁴
|
| 7 |
+
|
| 8 |
+
Ahmad Ali Parr
|
| 9 |
+
7 October 2026
|
| 10 |
+
|
| 11 |
+
## Abstract
|
| 12 |
+
|
| 13 |
+
See [ABSTRACT.md](ABSTRACT.md). This note is the working paper for `cpsc-qft`. It describes what the code does. It does not claim a machine-checked proof of operator commutation, and it does not claim a continuum scattering amplitude.
|
| 14 |
+
|
| 15 |
+
## 1. Introduction
|
| 16 |
+
|
| 17 |
+
NISQ scattering experiments often evolve a Trotter circuit and discard shots whose measured quantum numbers disagree with the input. That is a diagnostic. It does not make the illegal amplitude unavailable to the device, and it does not tell a later hardware rewrite to refuse a gate.
|
| 18 |
+
|
| 19 |
+
The Conservation-Preserving Scattering Compiler (CPSC) inverts the order. A schedule is emitted only if every factor preserves the active symmetry and the support is closed under lattice translation. The emitted artifact carries a digest of that schedule. Changing a gate changes the digest, so an edited circuit no longer matches the certificate.
|
| 20 |
+
|
| 21 |
+
The target is not continuum φ⁴. It is the periodic transverse-field Ising model on N qubits,
|
| 22 |
+
|
| 23 |
+
$$
|
| 24 |
+
H = -J \sum_{i=0}^{N-1} Z_i Z_{i+1} + \lambda \sum_{i=0}^{N-1} X_i .
|
| 25 |
+
$$
|
| 26 |
+
|
| 27 |
+
The draft quartic (g/24) ∑ᵢ Xᵢ⁴ equals (g/24) N I, because X² = I. It shifts the zero of energy and opens no channel. The implementation records the shift and drops it.
|
| 28 |
+
|
| 29 |
+
## 2. Which operator is actually conserved
|
| 30 |
+
|
| 31 |
+
The draft listed Π_Q = ∑ᵢ Zᵢ as a symmetry of the interacting theory. That is false once λ ≠ 0.
|
| 32 |
+
|
| 33 |
+
A single X anticommutes with ∏ Z and changes the magnetization by 2. The transverse field is a sum of such terms, so neither magnetization nor Z-parity commutes with H at λ ≠ 0. The symmetry that does commute with both the bonds and the field is the global spin flip
|
| 34 |
+
|
| 35 |
+
$$
|
| 36 |
+
\Pi_X = \prod_{i=0}^{N-1} X_i .
|
| 37 |
+
$$
|
| 38 |
+
|
| 39 |
+
ZZ contributes two minus signs when conjugated by Π_X, so it commutes. Each X commutes with Π_X. A lone Z anticommutes with Π_X.
|
| 40 |
+
|
| 41 |
+
Translation is a symmetry because the couplings are uniform and the lattice is periodic. Energy is not a separate compilation constraint: a time-independent H already satisfies [H, H] = 0. Filtering on ⟨H⟩ after a Trotter step measures Trotter error, not a conservation-law violation.
|
| 42 |
+
|
| 43 |
+
The compiler therefore has two modes.
|
| 44 |
+
|
| 45 |
+
| Mode | When | Admitted | Rejected |
|
| 46 |
+
| --- | --- | --- | --- |
|
| 47 |
+
| Ising | λ = 0 | ZZ ring, lone Z | single X |
|
| 48 |
+
| Spin-flip | λ ≠ 0 | ZZ ring, full X orbit | lone Z |
|
| 49 |
+
|
| 50 |
+
A schedule must also be a union of cyclic images. One bond is not a translation-invariant interaction. The ring is.
|
| 51 |
+
|
| 52 |
+
## 3. Compiler
|
| 53 |
+
|
| 54 |
+
`compile_scattering` is the PCSS path.
|
| 55 |
+
|
| 56 |
+
1. Choose the mode from λ.
|
| 57 |
+
2. Admit gates or raise `CompilationRejected`.
|
| 58 |
+
3. Reject a support that is not closed under the cyclic shift.
|
| 59 |
+
4. Build a first-order Trotter product of the admitted local terms. ZZ gates apply exp(i J dt Zₐ Z_b). X gates apply exp(−i λ dt X). A lone Z, if admitted, is a spectator and does not evolve the state.
|
| 60 |
+
5. Bind a certificate to the admitted list, the mode, and the Trotter parameters.
|
| 61 |
+
|
| 62 |
+
In Ising mode the input is a computational basis state. In spin-flip mode the input is the even cat (|b⟩ + |flip b⟩)/√2, which is a +1 eigenstate of Π_X. A raw basis state is not an eigenstate of Π_X, so it is the wrong sector label.
|
| 63 |
+
|
| 64 |
+
There is no post-selection step. Leakage is measured only as a check that the emitted product stayed in the sector.
|
| 65 |
+
|
| 66 |
+
## 4. Certificate
|
| 67 |
+
|
| 68 |
+
The certificate is the SHA-256 of the canonical JSON payload: gate names, supports, kinds, mode, N, λ, J, time, and step count. The same payload is rendered as a Lean fragment
|
| 69 |
+
|
| 70 |
+
```lean
|
| 71 |
+
def emittedHash : String := "<digest>"
|
| 72 |
+
def emittedGates : List GateKind := [.zz 0 1, .zz 1 2, .zz 2 3, .zz 3 0]
|
| 73 |
+
theorem emitted_admitted : allPreserve preservesMagnetization emittedGates = true := rfl
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
`lean/CPSC/Certificate.lean` defines the predicates and discharges them by reduction for the 4-site ring and for the two adversarial extensions. `rfl` fails if a forbidden constructor is inserted. `lean/CPSC/Emitted.lean` is a sample binding produced by the compiler; its digest is `e92f82910385ae3c46acc5479d57f4c6357cbbf9bf77d668e469a64b281db8e5`.
|
| 77 |
+
|
| 78 |
+
This is a Boolean skeleton. It does not yet prove that the corresponding unitary commutes with Π_X as an operator. That obligation is stated and left open.
|
| 79 |
+
|
| 80 |
+
## 5. Q# emission
|
| 81 |
+
|
| 82 |
+
`qsharp/ConservationPreservingScattering.qs` is the hardware image of the admitted factors, not a second analyzer.
|
| 83 |
+
|
| 84 |
+
- `ApplyZZRing` is the bond orbit.
|
| 85 |
+
- `ApplyTransverseField` is the full X orbit, used only by the spin-flip schedule.
|
| 86 |
+
- `IsingEvolution` and `SpinFlipEvolution` are the Trotter loops.
|
| 87 |
+
- `RequireAdmitted` fails the operation if the host passes a schedule the analyzer refused.
|
| 88 |
+
- `MeasureZParity` is a diagnostic. It is not the compiler.
|
| 89 |
+
|
| 90 |
+
## 6. Checks
|
| 91 |
+
|
| 92 |
+
All figures below are from the statevector prototype on N = 4, J = 1, eight Trotter steps, t = 0.4. The suite is `pytest`: 14 passed.
|
| 93 |
+
|
| 94 |
+
- Ising ring, seed `0b0011`, λ = 0. Z-parity leakage `0`. The certificate matches the ring and does not match the ring with `X[0]` substituted.
|
| 95 |
+
- Ising ring plus `X[2]` is rejected at compile time: odd X support changes Z-parity.
|
| 96 |
+
- A single bond is rejected: the translation orbit is open.
|
| 97 |
+
- Spin-flip schedule, seed `0b0001`, λ = 0.5. ∏ X leakage `0` after clamping roundoff at `3.6×10⁻¹⁵`.
|
| 98 |
+
- The same schedule plus `Z[1]` is rejected: a lone Z anticommutes with ∏ X.
|
| 99 |
+
- The dropped quartic at g = 1 is `N g / 24 = 0.1667` on the identity.
|
| 100 |
+
|
| 101 |
+
These checks do not include device noise. Ideal leakage zero does not survive decoherence. The certificate does not claim otherwise.
|
| 102 |
+
|
| 103 |
+
## 7. What is not proved
|
| 104 |
+
|
| 105 |
+
Operator commutation, a noise bound, a truncated-oscillator φ⁴ encoding, and a Lean checker wired into CI are open. Completeness holds inside each mode by construction of H: the Ising limit conserves magnetization, and the transverse-field model conserves Π_X. Projecting onto magnetization at λ ≠ 0 is refused, because that projection would drop part of the true evolution.
|
| 106 |
+
|
| 107 |
+
The construction collides with symmetry-sector simulators and with verified circuit compilers. The piece that is specific here is the channel rule for this lattice shadow, the mode switch at λ = 0, and the digest bound to the admitted schedule.
|
| 108 |
+
|
| 109 |
+
## 8. Related work
|
| 110 |
+
|
| 111 |
+
Block-diagonalization by a conserved charge is standard. Symmetry verification and post-selection are the method this compiler refuses to substitute for synthesis. Subspace-preserving evolution in existing quantum toolkits already builds a sector unitary once the symmetry is declared. Machine-checked circuit semantics exist in other stacks and do not know this Hamiltonian. Formalization of free bosonic quantum field theory in Lean (Douglas, Hoback, Mei, Nissim, 2026) is a different target: continuum Euclidean axioms, not a lattice scattering schedule.
|
| 112 |
+
|
| 113 |
+
## License
|
| 114 |
+
|
| 115 |
+
Dual strict copyleft: `LicenseRef-CPSC-ESCL-1.0` or `AGPL-3.0-only`.
|
pyproject.toml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, or, at your
|
| 6 |
+
# option, the GNU Affero General Public License, Version 3 only.
|
| 7 |
+
#
|
| 8 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 9 |
+
# *******************************************************************************
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "cpsc-qft"
|
| 13 |
+
version = "0.3.0"
|
| 14 |
+
description = "Conservation-Preserving Scattering Compiler research scaffold"
|
| 15 |
+
readme = "README.md"
|
| 16 |
+
requires-python = ">=3.10"
|
| 17 |
+
license = { text = "LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only" }
|
| 18 |
+
authors = [{ name = "Ahmad Ali Parr" }]
|
| 19 |
+
dependencies = ["numpy>=1.26"]
|
| 20 |
+
|
| 21 |
+
[project.optional-dependencies]
|
| 22 |
+
dev = ["pytest>=8.0"]
|
| 23 |
+
|
| 24 |
+
[project.scripts]
|
| 25 |
+
cpsc-demo = "cpsc.demo:main"
|
| 26 |
+
|
| 27 |
+
[build-system]
|
| 28 |
+
requires = ["setuptools>=68"]
|
| 29 |
+
build-backend = "setuptools.build_meta"
|
| 30 |
+
|
| 31 |
+
[tool.setuptools.packages.find]
|
| 32 |
+
where = ["src"]
|
| 33 |
+
|
| 34 |
+
[tool.pytest.ini_options]
|
| 35 |
+
testpaths = ["tests"]
|
qsharp/Axioms.qs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// *******************************************************************************
|
| 2 |
+
// Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
//
|
| 4 |
+
// SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 5 |
+
// *******************************************************************************
|
| 6 |
+
|
| 7 |
+
namespace ConservationPreservingScattering {
|
| 8 |
+
open Microsoft.Quantum.Intrinsic;
|
| 9 |
+
|
| 10 |
+
// Operator axioms, as operations the compiler is allowed to emit.
|
| 11 |
+
// Lean names are in comments. Q# has no proof kernel; admission is the Bool gate.
|
| 12 |
+
|
| 13 |
+
newtype GateKind = (
|
| 14 |
+
Tag : Int,
|
| 15 |
+
Q0 : Int,
|
| 16 |
+
Q1 : Int
|
| 17 |
+
);
|
| 18 |
+
|
| 19 |
+
function ZZ(q0 : Int, q1 : Int) : GateKind {
|
| 20 |
+
return GateKind(0, q0, q1);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function ParityEvenPair(q0 : Int, q1 : Int) : GateKind {
|
| 24 |
+
return GateKind(1, q0, q1);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function TransverseFlip(q : Int) : GateKind {
|
| 28 |
+
return GateKind(2, q, -1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function LoneZ(q : Int) : GateKind {
|
| 32 |
+
return GateKind(3, q, -1);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// preservesMagnetization
|
| 36 |
+
function Axiom_PreservesMagnetization(g : GateKind) : Bool {
|
| 37 |
+
let tag = g::Tag;
|
| 38 |
+
return tag == 0 or tag == 3;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// preservesSpinFlip
|
| 42 |
+
function Axiom_PreservesSpinFlip(g : GateKind) : Bool {
|
| 43 |
+
let tag = g::Tag;
|
| 44 |
+
return tag == 0 or tag == 1 or tag == 2;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Mode 0 is Ising, mode 1 is spin-flip. Mirrors `preserves`.
|
| 48 |
+
function Axiom_Preserves(mode : Int, g : GateKind) : Bool {
|
| 49 |
+
return mode == 0 ? Axiom_PreservesMagnetization(g) | Axiom_PreservesSpinFlip(g);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// zz_mag / zz_spin: a bond is legal in both modes.
|
| 53 |
+
operation Axiom_ApplyZZ(left : Qubit, right : Qubit, angle : Double) : Unit is Adj + Ctl {
|
| 54 |
+
Rzz(2.0 * angle, left, right);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// flip_spin: a transverse flip is legal only after the spin-flip axiom accepts it.
|
| 58 |
+
operation Axiom_ApplyFlip(target : Qubit, angle : Double) : Unit is Adj + Ctl {
|
| 59 |
+
Rx(2.0 * angle, target);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// z_mag: a lone Z is legal only in Ising mode.
|
| 63 |
+
operation Axiom_ApplyZ(target : Qubit, angle : Double) : Unit is Adj + Ctl {
|
| 64 |
+
Rz(2.0 * angle, target);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// pair_spin: two flips, the parity-even pair.
|
| 68 |
+
operation Axiom_ApplyPair(left : Qubit, right : Qubit, angle : Double) : Unit is Adj + Ctl {
|
| 69 |
+
Rx(2.0 * angle, left);
|
| 70 |
+
Rx(2.0 * angle, right);
|
| 71 |
+
}
|
| 72 |
+
}
|
qsharp/ConservationPreservingScattering.qs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// *******************************************************************************
|
| 2 |
+
// Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
//
|
| 4 |
+
// This program and the accompanying materials are made available under the
|
| 5 |
+
// terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
// available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
// GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
// LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
// classpath exception and no permissive relicensing.
|
| 10 |
+
//
|
| 11 |
+
// SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
// *******************************************************************************
|
| 13 |
+
|
| 14 |
+
namespace ConservationPreservingScattering {
|
| 15 |
+
open Microsoft.Quantum.Intrinsic;
|
| 16 |
+
open Microsoft.Quantum.Canon;
|
| 17 |
+
open Microsoft.Quantum.Arrays;
|
| 18 |
+
open Microsoft.Quantum.Measurement;
|
| 19 |
+
open Microsoft.Quantum.Convert;
|
| 20 |
+
|
| 21 |
+
/// Ising bond orbit. Calls the ZZ axiom on every cyclic image.
|
| 22 |
+
operation ApplyZZRing(register : Qubit[], angle : Double) : Unit is Adj + Ctl {
|
| 23 |
+
let n = Length(register);
|
| 24 |
+
for i in 0 .. n - 1 {
|
| 25 |
+
Axiom_ApplyZZ(register[i], register[(i + 1) % n], angle);
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/// Full transverse orbit. Legal only as a spin-flip schedule, not as one site.
|
| 30 |
+
operation ApplyTransverseField(register : Qubit[], angle : Double) : Unit is Adj + Ctl {
|
| 31 |
+
for q in register {
|
| 32 |
+
Axiom_ApplyFlip(q, angle);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
operation RequireAdmitted(admitted : Bool, reason : String) : Unit {
|
| 37 |
+
if not admitted {
|
| 38 |
+
fail $"CPSC rejected the schedule: {reason}";
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/// λ = 0. Bonds only. Magnetization is the active invariant.
|
| 43 |
+
operation IsingEvolution(register : Qubit[], steps : Int, dt : Double, j : Double) : Unit is Adj + Ctl {
|
| 44 |
+
let schedule = Lemma_BondOrbit(Length(register));
|
| 45 |
+
RequireAdmitted(Lemma_AllAdmitted(0, schedule), "ising ring");
|
| 46 |
+
for _ in 1 .. steps {
|
| 47 |
+
Lemma_Interpret(register, 0, schedule, j * dt);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/// λ ≠ 0. Bond orbit plus transverse orbit. ∏ X is the active invariant.
|
| 52 |
+
operation SpinFlipEvolution(
|
| 53 |
+
register : Qubit[],
|
| 54 |
+
steps : Int,
|
| 55 |
+
dt : Double,
|
| 56 |
+
j : Double,
|
| 57 |
+
lam : Double
|
| 58 |
+
) : Unit is Adj + Ctl {
|
| 59 |
+
let n = Length(register);
|
| 60 |
+
let bonds = Lemma_BondOrbit(n);
|
| 61 |
+
let field = Lemma_TransverseOrbit(n);
|
| 62 |
+
RequireAdmitted(Lemma_AllAdmitted(1, bonds + field), "spin-flip orbit");
|
| 63 |
+
for _ in 1 .. steps {
|
| 64 |
+
Lemma_Interpret(register, 1, bonds, j * dt);
|
| 65 |
+
Lemma_Interpret(register, 1, field, lam * dt);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/// Refused schedule. Present so a host can see the lemma fail closed.
|
| 70 |
+
operation RejectedIsingFlip(register : Qubit[], site : Int, angle : Double) : Unit is Adj + Ctl {
|
| 71 |
+
let bad = [TransverseFlip(site)];
|
| 72 |
+
RequireAdmitted(Lemma_AllAdmitted(0, bad), "single X breaks Ising magnetization");
|
| 73 |
+
Lemma_Interpret(register, 0, bad, angle);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
operation RejectedSpinFlipZ(register : Qubit[], site : Int, angle : Double) : Unit is Adj + Ctl {
|
| 77 |
+
let bad = [LoneZ(site)];
|
| 78 |
+
RequireAdmitted(Lemma_AllAdmitted(1, bad), "lone Z breaks prod X");
|
| 79 |
+
Lemma_Interpret(register, 1, bad, angle);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/// Diagnostic, not a compiler filter.
|
| 83 |
+
operation MeasureZParity(register : Qubit[]) : Int {
|
| 84 |
+
mutable parity = 0;
|
| 85 |
+
for q in register {
|
| 86 |
+
if M(q) == One {
|
| 87 |
+
set parity = (parity + 1) % 2;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
return parity;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/// Diagnostic for the spin-flip sector: measure every qubit in X and return the parity.
|
| 94 |
+
operation MeasureXParity(register : Qubit[]) : Int {
|
| 95 |
+
mutable parity = 0;
|
| 96 |
+
for q in register {
|
| 97 |
+
H(q);
|
| 98 |
+
if M(q) == One {
|
| 99 |
+
set parity = (parity + 1) % 2;
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
return parity;
|
| 103 |
+
}
|
| 104 |
+
}
|
qsharp/Lemmas.qs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// *******************************************************************************
|
| 2 |
+
// Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
//
|
| 4 |
+
// SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 5 |
+
// *******************************************************************************
|
| 6 |
+
|
| 7 |
+
namespace ConservationPreservingScattering {
|
| 8 |
+
open Microsoft.Quantum.Intrinsic;
|
| 9 |
+
open Microsoft.Quantum.Arrays;
|
| 10 |
+
open Microsoft.Quantum.Canon;
|
| 11 |
+
|
| 12 |
+
// Lemmas as total functions and interpreting operations.
|
| 13 |
+
// Each one is the Q# image of a theorem in lean/CPSC/Lemmas.lean.
|
| 14 |
+
|
| 15 |
+
function Lemma_NilAdmitted() : Bool {
|
| 16 |
+
return true;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function Lemma_ConsAdmitted(mode : Int, head : GateKind, tailOk : Bool) : Bool {
|
| 20 |
+
return Axiom_Preserves(mode, head) and tailOk;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function Lemma_AllAdmitted(mode : Int, schedule : GateKind[]) : Bool {
|
| 24 |
+
mutable ok = true;
|
| 25 |
+
for g in schedule {
|
| 26 |
+
if not Axiom_Preserves(mode, g) {
|
| 27 |
+
set ok = false;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
return ok;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function Lemma_AppendAdmitted(left : Bool, right : Bool) : Bool {
|
| 34 |
+
return left and right;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function Lemma_TransverseBreaksIsing(q : Int) : Bool {
|
| 38 |
+
return not Axiom_PreservesMagnetization(TransverseFlip(q));
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function Lemma_LoneZBreaksSpinFlip(q : Int) : Bool {
|
| 42 |
+
return not Axiom_PreservesSpinFlip(LoneZ(q));
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
operation Lemma_ApplyAdmitted(
|
| 46 |
+
register : Qubit[],
|
| 47 |
+
g : GateKind,
|
| 48 |
+
angle : Double
|
| 49 |
+
) : Unit is Adj + Ctl {
|
| 50 |
+
let n = Length(register);
|
| 51 |
+
let q0 = g::Q0;
|
| 52 |
+
let q1 = g::Q1;
|
| 53 |
+
if q0 < 0 or q0 >= n or (q1 >= n) {
|
| 54 |
+
fail "CPSC lemma: qubit outside the register";
|
| 55 |
+
}
|
| 56 |
+
if g::Tag == 0 {
|
| 57 |
+
if q1 < 0 {
|
| 58 |
+
fail "CPSC lemma: ZZ is missing its second qubit";
|
| 59 |
+
}
|
| 60 |
+
Axiom_ApplyZZ(register[q0], register[q1], angle);
|
| 61 |
+
} elif g::Tag == 1 {
|
| 62 |
+
if q1 < 0 {
|
| 63 |
+
fail "CPSC lemma: pair is missing its second qubit";
|
| 64 |
+
}
|
| 65 |
+
Axiom_ApplyPair(register[q0], register[q1], angle);
|
| 66 |
+
} elif g::Tag == 2 {
|
| 67 |
+
Axiom_ApplyFlip(register[q0], angle);
|
| 68 |
+
} elif g::Tag == 3 {
|
| 69 |
+
Axiom_ApplyZ(register[q0], angle);
|
| 70 |
+
} else {
|
| 71 |
+
fail "CPSC lemma: unknown gate tag";
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// circuit_comm_*: interpret only an admitted schedule. A rejected head fails closed.
|
| 76 |
+
operation Lemma_Interpret(
|
| 77 |
+
register : Qubit[],
|
| 78 |
+
mode : Int,
|
| 79 |
+
schedule : GateKind[],
|
| 80 |
+
angle : Double
|
| 81 |
+
) : Unit is Adj + Ctl {
|
| 82 |
+
if not Lemma_AllAdmitted(mode, schedule) {
|
| 83 |
+
fail "CPSC rejected the schedule: a factor breaks the active invariant";
|
| 84 |
+
}
|
| 85 |
+
for g in schedule {
|
| 86 |
+
Lemma_ApplyAdmitted(register, g, angle);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function Lemma_BondOrbit(n : Int) : GateKind[] {
|
| 91 |
+
mutable gates = [];
|
| 92 |
+
for i in 0 .. n - 1 {
|
| 93 |
+
set gates += [ZZ(i, (i + 1) % n)];
|
| 94 |
+
}
|
| 95 |
+
return gates;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function Lemma_TransverseOrbit(n : Int) : GateKind[] {
|
| 99 |
+
mutable gates = [];
|
| 100 |
+
for i in 0 .. n - 1 {
|
| 101 |
+
set gates += [TransverseFlip(i)];
|
| 102 |
+
}
|
| 103 |
+
return gates;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
operation Lemma_IsingCircuit(register : Qubit[], angle : Double) : Unit is Adj + Ctl {
|
| 107 |
+
let schedule = Lemma_BondOrbit(Length(register));
|
| 108 |
+
Lemma_Interpret(register, 0, schedule, angle);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
operation Lemma_SpinFlipCircuit(register : Qubit[], angle : Double) : Unit is Adj + Ctl {
|
| 112 |
+
let n = Length(register);
|
| 113 |
+
let schedule = Lemma_BondOrbit(n) + Lemma_TransverseOrbit(n);
|
| 114 |
+
Lemma_Interpret(register, 1, schedule, angle);
|
| 115 |
+
}
|
| 116 |
+
}
|
src/cpsc/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
"""Conservation-Preserving Scattering Compiler, research prototype."""
|
| 15 |
+
|
| 16 |
+
from cpsc.analyzer import Gate, Rejection, SymmetryAnalyzer, SymmetryMode
|
| 17 |
+
from cpsc.certificate import Certificate
|
| 18 |
+
from cpsc.channel import evolve_in_sector, sector_basis
|
| 19 |
+
from cpsc.compiler import compile_scattering
|
| 20 |
+
|
| 21 |
+
__all__ = [
|
| 22 |
+
"Certificate",
|
| 23 |
+
"Gate",
|
| 24 |
+
"Rejection",
|
| 25 |
+
"SymmetryAnalyzer",
|
| 26 |
+
"SymmetryMode",
|
| 27 |
+
"compile_scattering",
|
| 28 |
+
"evolve_in_sector",
|
| 29 |
+
"sector_basis",
|
| 30 |
+
]
|
| 31 |
+
__version__ = "0.3.0"
|
src/cpsc/analyzer.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************
|
| 13 |
+
|
| 14 |
+
"""Compilation-time symmetry analyzer.
|
| 15 |
+
|
| 16 |
+
A gate is admitted only if it preserves the active invariant. This is a
|
| 17 |
+
rejection rule, not a post-selection filter.
|
| 18 |
+
|
| 19 |
+
Ising mode (lambda = 0) conserves magnetization and Z-parity. A single X is
|
| 20 |
+
forbidden. Spin-flip mode (lambda != 0) conserves prod X, the actual symmetry
|
| 21 |
+
of the transverse-field Ising model. A single Z is forbidden; a single X is not.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
from dataclasses import dataclass
|
| 27 |
+
from enum import Enum
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class SymmetryMode(str, Enum):
|
| 31 |
+
ISING = "ising"
|
| 32 |
+
SPIN_FLIP = "spin_flip"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass(frozen=True)
|
| 36 |
+
class Gate:
|
| 37 |
+
name: str
|
| 38 |
+
qubits: tuple[int, ...]
|
| 39 |
+
kind: str
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass(frozen=True)
|
| 43 |
+
class Rejection:
|
| 44 |
+
gate: Gate
|
| 45 |
+
reason: str
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class CompilationRejected(Exception):
|
| 49 |
+
"""Raised when the analyzer refuses a circuit at compile time."""
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class SymmetryAnalyzer:
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
n: int,
|
| 56 |
+
*,
|
| 57 |
+
conserve_magnetization: bool = False,
|
| 58 |
+
mode: SymmetryMode | str = SymmetryMode.ISING,
|
| 59 |
+
) -> None:
|
| 60 |
+
if n < 1:
|
| 61 |
+
raise ValueError("n must be positive")
|
| 62 |
+
self.n = n
|
| 63 |
+
self.mode = SymmetryMode(mode)
|
| 64 |
+
self.conserve_magnetization = conserve_magnetization or self.mode is SymmetryMode.ISING
|
| 65 |
+
|
| 66 |
+
def check(self, gates: list[Gate]) -> list[Rejection]:
|
| 67 |
+
rejected: list[Rejection] = []
|
| 68 |
+
for gate in gates:
|
| 69 |
+
reason = self._reject_reason(gate)
|
| 70 |
+
if reason is not None:
|
| 71 |
+
rejected.append(Rejection(gate, reason))
|
| 72 |
+
return rejected
|
| 73 |
+
|
| 74 |
+
def admit(self, gates: list[Gate]) -> list[Gate]:
|
| 75 |
+
rejected = self.check(gates)
|
| 76 |
+
if rejected:
|
| 77 |
+
detail = "; ".join(f"{r.gate.name}: {r.reason}" for r in rejected)
|
| 78 |
+
raise CompilationRejected(detail)
|
| 79 |
+
return list(gates)
|
| 80 |
+
|
| 81 |
+
def _reject_reason(self, gate: Gate) -> str | None:
|
| 82 |
+
for q in gate.qubits:
|
| 83 |
+
if not 0 <= q < self.n:
|
| 84 |
+
return f"qubit {q} outside lattice of size {self.n}"
|
| 85 |
+
if self.mode is SymmetryMode.ISING:
|
| 86 |
+
if gate.kind in {"illegal", "transverse"}:
|
| 87 |
+
return "odd X/Y support changes Z-parity; forbidden sector"
|
| 88 |
+
if gate.kind in {"parity_even", "magnetization", "phase"}:
|
| 89 |
+
return None
|
| 90 |
+
else:
|
| 91 |
+
if gate.kind == "phase":
|
| 92 |
+
return "lone Z anticommutes with prod X; forbidden spin-flip sector"
|
| 93 |
+
if gate.kind in {"illegal", "transverse", "parity_even", "magnetization"}:
|
| 94 |
+
return None
|
| 95 |
+
return f"unknown gate kind {gate.kind}"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def x(q: int) -> Gate:
|
| 99 |
+
return Gate(f"X[{q}]", (q,), "illegal")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def z(q: int) -> Gate:
|
| 103 |
+
return Gate(f"Z[{q}]", (q,), "phase")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def zz(q: int, r: int) -> Gate:
|
| 107 |
+
return Gate(f"ZZ[{q},{r}]", (q, r), "magnetization")
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def rx_pair(q: int, r: int) -> Gate:
|
| 111 |
+
"""Two transverse flips. Parity-even under prod Z, magnetization-changing."""
|
| 112 |
+
return Gate(f"RX[{q}]*RX[{r}]", (q, r), "parity_even")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def ising_ring(n: int) -> list[Gate]:
|
| 116 |
+
return [zz(i, (i + 1) % n) for i in range(n)]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def transverse_field(n: int) -> list[Gate]:
|
| 120 |
+
return [x(i) for i in range(n)]
|
src/cpsc/certificate.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# *******************************************************************************
|
| 2 |
+
# Copyright (c) 2026 Ahmad Ali Parr and others.
|
| 3 |
+
#
|
| 4 |
+
# This program and the accompanying materials are made available under the
|
| 5 |
+
# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is
|
| 6 |
+
# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the
|
| 7 |
+
# GNU Affero General Public License, Version 3 only, which is available in
|
| 8 |
+
# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no
|
| 9 |
+
# classpath exception and no permissive relicensing.
|
| 10 |
+
#
|
| 11 |
+
# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only
|
| 12 |
+
# *******************************************************************************"""Certificate binding. The digest is the witness; a gate edit breaks it."""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import hashlib
|
| 17 |
+
import json
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
|
| 20 |
+
from cpsc.analyzer import Gate
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass(frozen=True)
|
| 24 |
+
class Certificate:
|
| 25 |
+
digest: str
|
| 26 |
+
mode: str
|
| 27 |
+
n: int
|
| 28 |
+
lean: str
|
| 29 |
+
payload: str
|
| 30 |
+
lam: float
|
| 31 |
+
j: float
|
| 32 |
+
time: float
|
| 33 |
+
steps: int
|
| 34 |
+
|
| 35 |
+
def matches(self, gates: list[Gate]) -> bool:
|
| 36 |
+
return self.digest == digest_for(gates, mode=self.mode, n=self.n, lam=self.lam, j=self.j, time=self.time, steps=self.steps)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def digest_for(gates: list[Gate], *, mode: str, n: int, lam: float, j: float, time: float, steps: int) -> str:
|
| 40 |
+
payload = json.dumps(
|
| 41 |
+
{
|
| 42 |
+
"gates": [[g.name, list(g.qubits), g.kind] for g in gates],
|
| 43 |
+
"mode": mode,
|
| 44 |
+
"n": n,
|
| 45 |
+
"lambda": lam,
|
| 46 |
+
"j": j,
|
| 47 |
+
"time": time,
|
| 48 |
+
"steps": steps,
|
| 49 |
+
},
|
| 50 |
+
separators=(",", ":"),
|
| 51 |
+
)
|
| 52 |
+
return hashlib.sha256(payload.encode()).hexdigest()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _lean_kind(gate: Gate) -> str:
|
| 56 |
+
if gate.kind == "magnetization":
|
| 57 |
+
a, b = gate.qubits
|
| 58 |
+
return f".zz {a} {b}"
|
| 59 |
+
if gate.kind == "phase":
|
| 60 |
+
return f".loneZ {gate.qubits[0]}"
|
| 61 |
+
if gate.kind in {"illegal", "transverse"}:
|
| 62 |
+
return f".transverseFlip {gate.qubits[0]}"
|
| 63 |
+
if gate.kind == "parity_even":
|
| 64 |
+
a, b = gate.qubits
|
| 65 |
+
return f".parityEvenPair {a} {b}"
|
| 66 |
+
raise ValueError(gate.kind)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def bind_certificate(
|
| 70 |
+
gates: list[Gate],
|
| 71 |
+
*,
|
| 72 |
+
mode: str,
|
| 73 |
+
n: int,
|
| 74 |
+
lam: float,
|
| 75 |
+
j: float,
|
| 76 |
+
time: float,
|
| 77 |
+
steps: int,
|
| 78 |
+
) -> Certificate:
|
| 79 |
+
payload = json.dumps(
|
| 80 |
+
{
|
| 81 |
+
"gates": [[g.name, list(g.qubits), g.kind] for g in gates],
|
| 82 |
+
"mode": mode,
|
| 83 |
+
"n": n,
|
| 84 |
+
"lambda": lam,
|
| 85 |
+
"j": j,
|
| 86 |
+
"time": time,
|
| 87 |
+
"steps": steps,
|
| 88 |
+
},
|
| 89 |
+
separators=(",", ":"),
|
| 90 |
+
)
|
| 91 |
+
digest = digest_for(gates, mode=mode, n=n, lam=lam, j=j, time=time, steps=steps)
|
| 92 |
+
kinds = ", ".join(_lean_kind(g) for g in gates)
|
| 93 |
+
predicate = "preservesMagnetization" if mode == "ising" else "preservesSpinFlip"
|
| 94 |
+
lean = (
|
| 95 |
+
"-- emitted by cpsc.certificate; do not edit by hand\n"
|
| 96 |
+
f"def emittedHash : String := \"{digest}\"\n"
|
| 97 |
+
f"def emittedGates : List GateKind := [{kinds}]\n"
|
| 98 |
+
f"theorem emitted_admitted : allPreserve {predicate} emittedGates = true := by\n"
|
| 99 |
+
" rfl\n"
|
| 100 |
+
)
|
| 101 |
+
return Certificate(
|
| 102 |
+
digest=digest, mode=mode, n=n, lean=lean, payload=payload,
|
| 103 |
+
lam=lam, j=j, time=time, steps=steps,
|
| 104 |
+
)
|