SNAPKITTYWEST commited on
Commit
eef9104
·
verified ·
1 Parent(s): 3f93aea

October 2026 main drop: mirror from GitHub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.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

  • SHA256: fbdcd1226610c697e603a7473200531b8ccec72d3ea1a78154bc01e572f7997d
  • Pointer size: 131 Bytes
  • Size of remote file: 143 kB
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
+ )