diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..0aa69fd7e8baaff747d38be9cb54611229ec297c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +docs/mercury-circuit-demo.gif filter=lfs diff=lfs merge=lfs -text +docs/mercury-circuit-demo.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..066bfd66719feeba8bec0482645230135088d181 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +*.egg-info/ +dist/ +build/ +.lake/ +lean/CPSC/.lake/ +*.olean +.DS_Store + +lean/.lake/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..d0664042ff91a87b2af3da20fb7179f28952c7b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ + + +# Changelog + +## 0.3.0 — 2026-10-07 + +Production release of the research prototype. + +- Mercury Ruby frontend: circuit DSL, statevector simulator, CPSC bridge, WASM console. +- GitHub Pages site in `docs/`, console at `qsim.html`. +- Lean Boolean lemmas proved; operator commutation held as axioms; `lake build` passes. +- Q# axiom/lemma interpreter fails closed on a rejected factor. +- Dual strict copyleft: `LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only`. + +Known limit: the browser analyzer mirrors Z-parity admission. The Python compiler is the path that switches to spin-flip mode at λ ≠ 0. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000000000000000000000000000000000..99cedffeef4e835d61d209fd3f19431d75a3351c --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,21 @@ +cff-version: 1.2.0 +message: "If you use this scaffold, cite it as software, not as a discharged formalization." +title: "CPSC: Conservation-Preserving Scattering Compiler" +abstract: > + Research scaffold for compilation-time enforcement of lattice momentum and + charge in 1+1D qubit-lattice scattering, with a Lean 4 certificate interface + and an executable symmetry analyzer. Proof obligations are open. +authors: + - family-names: Parr + given-names: Ahmad Ali +version: 0.1.0 +date-released: 2026-10-07 +license: AGPL-3.0-only +repository-code: "https://github.com/AHMADALIPARR/cpsc-qft" +keywords: + - quantum-field-theory + - formal-verification + - lean4 + - scattering + - symmetry +license-url: "https://github.com/AHMADALIPARR/cpsc-qft/blob/main/LICENSE" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..5dd20e73561a6ec149dfc11dca276fe0cf2ec145 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +CPSC Eclipse Strict Copyleft License, Version 1.0 +or, at your option, the GNU Affero General Public License, Version 3 only. + +Copyright (c) 2026 Ahmad Ali Parr + +This repository is dual-licensed under: + + (A) the CPSC Eclipse Strict Copyleft License, Version 1.0 + (LicenseRef-CPSC-ESCL-1.0), in LICENSES/CPSC-ESCL-1.0.txt + + (B) the GNU Affero General Public License, Version 3 only + (AGPL-3.0-only), in LICENSES/AGPL-3.0.txt + +You may use the work under either license. Both are strict copyleft. +Neither option is the Eclipse Public License, the Apache License, or a +permissive grant. There is no classpath exception. A recipient may not +relicense the work, or a modified version of it, under MIT, Apache-2.0, +BSD, EPL-2.0, or any other non-strict-copyleft license. + +File headers are part of the license notice. Keep them. + +This custom instrument is not issued by the Eclipse Foundation and grants +no right to Eclipse trademarks. diff --git a/LICENSES/AGPL-3.0.txt b/LICENSES/AGPL-3.0.txt new file mode 100644 index 0000000000000000000000000000000000000000..be3f7b28e564e7dd05eaf59d64adba1a4065ac0e --- /dev/null +++ b/LICENSES/AGPL-3.0.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSES/CPSC-ESCL-1.0.txt b/LICENSES/CPSC-ESCL-1.0.txt new file mode 100644 index 0000000000000000000000000000000000000000..cc8411774631025224cb9e91394f1f6913b27164 --- /dev/null +++ b/LICENSES/CPSC-ESCL-1.0.txt @@ -0,0 +1,149 @@ +CPSC Eclipse Strict Copyleft License +Version 1.0, 7 October 2026 + +Copyright (c) 2026 Ahmad Ali Parr + +This license is a custom instrument for the Conservation-Preserving +Scattering Compiler. It uses the Eclipse project notice style (copyright +header, SPDX identifier, accompanying-materials clause). It is not the +Eclipse Public License 1.0 or 2.0, it is not issued by the Eclipse +Foundation, and it grants no license to any Eclipse trademark. + +The copyleft in this instrument is strict. It is taken from the dual +offer of the GNU Affero General Public License, Version 3 only +("AGPL-3.0-only"), including that license's network-use source obligation. +Where this text and the AGPL both apply by the recipient's choice, the +chosen text governs. Where a downstream distributor offers the work +onward, both options must remain available. + +0. Definitions. + +"This License" means version 1.0 of the CPSC Eclipse Strict Copyleft +License. + +"The Program" means any copyrightable work licensed under this License. + +"You" means the licensee. + +"Modify" means copy from or adapt all or part of the Program in a fashion +requiring copyright permission, other than making an exact copy. + +"Covered work" means the unmodified Program or a work based on it, +including a translation, a hardware mapping, a gate-list rewrite, a +certificate binding, and a library linked so as to form a larger program. + +"Corresponding Source" means the preferred form for modifying the work, +including scripts required to build and run it, and excluding only system +libraries and unmodified general-purpose tools. + +"Convey" means any propagation that enables other parties to make or +receive copies. Remote interaction by users with a modified covered work +is treated as conveying for the source obligation in section 5. + +1. Grant. + +You receive a worldwide, royalty-free, non-exclusive copyright license to +run, study, modify, and convey the Program, and a patent license under +the contributors' essential patent claims, only if you comply with this +License. These grants are irrevocable for the term of copyright if you +remain in compliance. + +2. Dual offer. + +The Program is licensed under this License or, at your option, under the +GNU Affero General Public License, Version 3 only. The AGPL text is +included as LICENSES/AGPL-3.0.txt and may not be modified. Choosing the +AGPL does not extinguish the obligation, when you convey a modified +version, to keep the dual offer available to your recipients. + +3. Strict copyleft. + +You may convey a modified covered work only under this same dual offer, +as a whole, to anyone who receives a copy. You may not: + + (a) relicense the work, or any part of it, under a permissive license + or a weak-copyleft license, including MIT, BSD, Apache-2.0, + EPL-1.0, EPL-2.0, LGPL, or MPL; + + (b) add a classpath exception, linking exception, or other term that + lets a proprietary work incorporate the Program without itself + becoming a covered work; + + (c) impose a further restriction that would be forbidden by AGPL-3.0 + section 10; + + (d) remove or alter the copyright header, the SPDX identifier + "LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only", or the statement that + both options are strict copyleft. + +A compilation on a storage medium with independent works is an aggregate +only if the compilation does not limit users' rights beyond what this +License allows. Inclusion in an aggregate does not extend this License +to independent works. + +4. Source with binaries. + +If you convey a covered work in object form, you must also convey the +Corresponding Source under this dual offer, on a durable medium, by a +written offer valid for at least three years, or by equivalent network +access at no further charge, in the manner of AGPL-3.0 section 6. + +5. Network use. + +If you modify a covered work and users interact with it remotely through +a computer network, you must offer those users Corresponding Source of +the modified work under this dual offer, at no charge, by a prominent +network offer. This is the AGPL-3.0 section 13 obligation, and it applies +whether the recipient elects this License or the AGPL. + +6. Headers. + +Every source file of a covered work must carry a notice substantially +in the Eclipse header form: + + Copyright (c) and others. + Made available under the CPSC Eclipse Strict Copyleft License, + Version 1.0, or, at your option, the GNU AGPL Version 3 only. + SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only + +Files that cannot carry an inline comment must be listed, with the same +SPDX expression, in REUSE.toml. + +7. Notices and marks. + +Keep intact all notices of this License, of the AGPL, and of the absence +of warranty. Do not use the names Eclipse, Eclipse Foundation, or CPSC +to endorse a derivative without written permission. No trademark is +granted. + +8. Termination. + +If you violate this License, your rights end. They reinstate if you cure +the violation within 30 days of notice, or permanently if the holder +fails to notify you within 60 days of ceasing the violation, following +the pattern of AGPL-3.0 section 8. Downstream recipients who received +copies while you were in compliance keep their licenses. + +9. No surrender of others' freedom. + +If conditions are imposed on you that contradict this License, you may +not convey the work at all. + +10. Disclaimer of warranty. + +THE PROGRAM IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING FITNESS FOR A PARTICULAR PURPOSE AND +NON-INFRINGEMENT. THE ENTIRE RISK AS TO QUALITY AND PERFORMANCE IS WITH +YOU. + +11. Limitation of liability. + +IN NO EVENT WILL A COPYRIGHT HOLDER OR CONTRIBUTOR BE LIABLE FOR DAMAGES +ARISING FROM THE PROGRAM, UNDER ANY THEORY, UNLESS REQUIRED BY LAW OR +AGREED IN WRITING. + +12. Versioning. + +Only Ahmad Ali Parr may publish a new version of this License. A later +version applies to the Program only if a notice says so. The dual AGPL +option remains AGPL-3.0-only unless a later notice says otherwise. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..98d39b94608a248302fc0dc266effa306686f3b7 --- /dev/null +++ b/NOTICE @@ -0,0 +1,19 @@ +CPSC — Conservation-Preserving Scattering Compiler +Copyright (c) 2026 Ahmad Ali Parr and others. + +This program and the accompanying materials are made available under the +terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +GNU Affero General Public License, Version 3 only, which is available in +LICENSES/AGPL-3.0.txt. + +SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only + +This is not an Eclipse Foundation project. The notice form follows the +Eclipse accompanying-materials convention. The copyleft is the strict +network copyleft of the dual AGPL-3.0-only offer. No classpath exception +is granted. + +Third-party: LICENSES/AGPL-3.0.txt is the GNU Affero General Public +License, Version 3, Copyright (C) 2007 Free Software Foundation, Inc. +Verbatim copies of that license text are permitted; changing it is not. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b158bde1a27ee85e8a5c5cc6e416bfed1aa8ab72 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +--- +license: other +license_name: cpsc-escl-1.0-or-agpl-3.0-only +license_link: https://huggingface.co/Snapkitty/cpsc-qft/blob/main/LICENSE +tags: +- snapkitty +- october-2026-drop +- ruby +- python +- lean4 +- qsharp +- quantum-field-theory +- lattice +- compiler +- quantum-computing +--- + +> 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**. + diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000000000000000000000000000000000000..7b2db899e73b1f91b3755c5d1a243bae232bf314 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,13 @@ +version = 1 + +[[annotations]] +path = ["LICENSE", "NOTICE", "LICENSES/CPSC-ESCL-1.0.txt", "lean/lean-toolchain", ".gitignore"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Ahmad Ali Parr" +SPDX-License-Identifier = "LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only" + +[[annotations]] +path = ["LICENSES/AGPL-3.0.txt"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2007 Free Software Foundation, Inc." +SPDX-License-Identifier = "FSFAP" diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docs/CHANNEL_PRUNING.md b/docs/CHANNEL_PRUNING.md new file mode 100644 index 0000000000000000000000000000000000000000..4a93fa441e018e0e43650a4cdd93ddd17c37ac3a --- /dev/null +++ b/docs/CHANNEL_PRUNING.md @@ -0,0 +1,34 @@ + + +# Channel pruning for a 1+1 → 1+2 process + +The draft asked for an inelastic $1+1 \to 1+2$ collision whose output lies strictly in the input energy manifold. + +On this lattice that sentence has to be weakened. + +## What the process is + +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. + +Operationally the channel check is: + +- Input: a two-domain-wall state, or a computational bitstring with a declared parity and (if $\lambda = 0$) magnetization. +- 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}}$. +- Forbidden output: the opposite parity sector. A single spin flip is the generator of that forbidden move. + +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. + +## Pruning rule implemented + +`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. + +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$. + +## Adversarial gate + +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}$. diff --git a/docs/OPEN_PROBLEMS.md b/docs/OPEN_PROBLEMS.md new file mode 100644 index 0000000000000000000000000000000000000000..2236f92d8ab9df20a18bb9d432abb55f2f02c4e0 --- /dev/null +++ b/docs/OPEN_PROBLEMS.md @@ -0,0 +1,35 @@ + + +# Open problems + +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. + +## Discharged in the prototype + +1. Boolean admission. `lean/CPSC/Certificate.lean` reduces the 4-site ring and both adversarial extensions by `rfl`. +2. Digest binding. `cpsc.certificate` hashes the schedule. Editing a gate makes `Certificate.matches` false. +3. Mode switch. λ = 0 rejects a single X. λ ≠ 0 rejects a lone Z. Translation orbits must be closed. + +## Obligations not discharged + +1. Operator commutation. The Boolean predicate is not yet proved equal to `[U, Π] = 0` on `(ℂ²)⊗N`. +2. Leakage under hardware noise. Ideal leakage on N = 4 is zero; device leakage is unmodeled. +3. Momentum as an eigenspace, rather than orbit closure of the gate support. +4. A non-trivial φ⁴ encoding. `X^4 = I` on qubits. The next model is a truncated oscillator per site. +5. A Lean binary in CI. The emitted theorem is generated; `lake build` has not been run here. + +## Collisions + +- Post-selection on symmetry bits (symmetry verification, symmetry-protected mitigation) is the method this repo refuses to call a compiler. +- Subspace-preserving compilation and Pauli twirling / sector projection in Qiskit and PennyLane already build the block-diagonal evolution for a declared symmetry. +- Verified circuit compilers (VOQC, SQIR, CertiQ-style stacks) already attach machine-checked semantics to gate lists. They do not know about $\phi^4$. +- SPT phases are a property of a ground state, not of a synthesizer. The analogy in the draft is motivational only. + +## What would make the novelty claim true + +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. diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md new file mode 100644 index 0000000000000000000000000000000000000000..c85f29b43bdf82b09a7175a0b3bfc597ff62ee49 --- /dev/null +++ b/docs/SPECIFICATION.md @@ -0,0 +1,76 @@ + + +# Specification + +## 1. Objects + +A QFT specification is a tuple $\mathcal{S} = (\mathcal{H}, H, \mathcal{G})$. + +- $\mathcal{H} = (\mathbb{C}^2)^{\otimes N}$, computational basis ordered little-endian. +- $H$ is the periodic transverse-field Ising Hamiltonian in Section 2. +- $\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$. + +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$. + +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. + +## 2. Hamiltonian + +$$ +H = -J \sum_{i=0}^{N-1} Z_i Z_{i+1} + \lambda \sum_{i=0}^{N-1} X_i ++ \frac{g}{24} \sum_{i=0}^{N-1} X_i^4 +$$ + +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. + +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$. + +## 3. Symmetry-constrained manifold + +$$ +\mathcal{M}_{\mathrm{sym}} + = \{ U \in U(2^N) \mid [U, P_Z] = 0,\ [U, T] = 0 \} +$$ + +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. + +The group average + +$$ +\mathcal{P}(O) = \frac{1}{N} \sum_{k=0}^{N-1} T^{-k} O T^{k} +$$ + +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. + +Charge projection for $\mathbb{Z}_2$ is the average $(O + P_Z O P_Z)/2$. + +## 4. Channel-space form + +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 + +$$ +U_{\mathrm{QFT}} = \bigoplus_\lambda U_\lambda +$$ + +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. + +## 5. Algorithm (PCSS) + +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. +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). +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. +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. +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. + +## 6. Proof obligations + +See `lean/CPSC/Certificate.lean` and `docs/OPEN_PROBLEMS.md`. + +- Commutation: each emitted gate $G$ satisfies $[G, \Pi_\chi] = 0$. +- Leakage: the ideal compiled unitary has zero amplitude between distinct $\lambda$ sectors. +- Completeness: $e^{-iHt} \in \mathcal{M}_{\mathrm{sym}}$ whenever $[H, \Pi_\chi] = 0$. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000000000000000000000000000000000..b26e526fc348da24c93d09843825216a1c62bdde --- /dev/null +++ b/docs/index.html @@ -0,0 +1,66 @@ + + + + + + +CPSC-QFT — Conservation-Preserving Scattering Compiler + + + + +
+

CPSC-QFT

+

Conservation-preserving scattering compiler for a 1+1D lattice shadow of φ⁴. Version 0.3.0. Dual strict copyleft.

+
+
+
+
+

Mercury console

+

Ruby 3.2 in the browser. Build a circuit, run the statevector, and see the compile-time symmetry verdict.

+

Open the console

+
+
+

Paper

+

What is proved, what is measured, and which operator is actually conserved.

+

paper/PAPER.md

+
+
+

Certificate

+

Boolean lemmas are proved. Operator commutation is an explicit axiom set. lake build passes on Lean 4.22.0.

+

Lemmas.lean

+
+
+

Source

+

Python compiler, Q# emission, Mercury library. Tests live next to the code.

+

AHMADALIPARR/cpsc-qft

+
+
+
+

Release check

+

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 frontend/lib/mercury/cpsc.rb; the Python compiler is the mode-accurate path. WASM is loaded from jsDelivr, so the console needs network.

+
+
+
+ LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only. Not the Eclipse Public License. +
+ + diff --git a/docs/mercury-circuit-demo.gif b/docs/mercury-circuit-demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..b3810acb8a097eeb7c6ba15d6a2cab238883d5ba --- /dev/null +++ b/docs/mercury-circuit-demo.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbdcd1226610c697e603a7473200531b8ccec72d3ea1a78154bc01e572f7997d +size 142655 diff --git a/docs/mercury-circuit-demo.mp4 b/docs/mercury-circuit-demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..3df68466738f89f7fad0177be9509cc71ae6cf26 --- /dev/null +++ b/docs/mercury-circuit-demo.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0292673a14f213e7702309efbb01e7aa9323e3d32d416495f223edaf62661f1d +size 258045 diff --git a/docs/mercury_bundle.rb b/docs/mercury_bundle.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c0c115efda21b023325c27698174fe261ca7f18 --- /dev/null +++ b/docs/mercury_bundle.rb @@ -0,0 +1,845 @@ +# ---- ../lib/mercury/version.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + VERSION = "0.1.0" +end + +# ---- ../lib/mercury/gates.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Gate matrices for Mercury. Little-endian convention: in a k-qubit gate +# applied to qubits [q0, q1, ...], matrix row/column index bit j (value +# 2**j) is the computational-basis state of qubit q_j. For a two-qubit +# gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0). +module Mercury + module Gates + ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze + TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze + THREE_QUBIT = %i[ccx].freeze + + PARAM_COUNTS = { + i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0, + rx: 1, ry: 1, rz: 1, u: 3, + cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0, + ccx: 0 + }.freeze + + def self.arity(name) + return 1 if ONE_QUBIT.include?(name) + return 2 if TWO_QUBIT.include?(name) + return 3 if THREE_QUBIT.include?(name) + raise ArgumentError, "unknown gate #{name}" + end + + def self.param_count(name) + PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" } + end + + def self.matrix(name, params = []) + need = param_count(name) + raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need + + case name + when :i then [[c(1), c(0)], [c(0), c(1)]] + when :x then [[c(0), c(1)], [c(1), c(0)]] + when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]] + when :z then [[c(1), c(0)], [c(0), c(-1)]] + when :h + s = Math.sqrt(0.5) + [[c(s), c(s)], [c(s), c(-s)]] + when :s then [[c(1), c(0)], [c(0), c(0, 1)]] + when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]] + when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]] + when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]] + when :rx + th = params[0] / 2.0 + co = c(Math.cos(th)); si = c(0, -Math.sin(th)) + [[co, si], [si, co]] + when :ry + th = params[0] / 2.0 + [[c(Math.cos(th)), c(-Math.sin(th))], + [c(Math.sin(th)), c(Math.cos(th))]] + when :rz + th = params[0] / 2.0 + [[phase(-th), c(0)], [c(0), phase(th)]] + when :u + th, ph, la = params + co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0)) + [[co, c(-1) * phase(la) * si], + [phase(ph) * si, phase(ph + la) * co]] + when :cnot + # qubits [target, control]; flips target iff control == 1 + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(0), c(1)], + [c(0), c(0), c(1), c(0)]] + when :cz + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(0), c(0), c(-1)]] + when :cp + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(0), c(0), phase(params[0])]] + when :swap + [[c(1), c(0), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(0), c(1)]] + when :rzz + # exp(-i*theta/2 * ZxZ) + a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0) + [[a, c(0), c(0), c(0)], + [c(0), b, c(0), c(0)], + [c(0), c(0), b, c(0)], + [c(0), c(0), c(0), a]] + when :xx + # XxX: parity-even pair used by the CPSC analyzer + [[c(0), c(0), c(0), c(1)], + [c(0), c(0), c(1), c(0)], + [c(0), c(1), c(0), c(0)], + [c(1), c(0), c(0), c(0)]] + when :ccx + # qubits [target, control1, control2]; flips target iff both controls == 1 + m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } } + m[6][6] = c(0); m[7][7] = c(0) + m[6][7] = c(1); m[7][6] = c(1) + m + end + end + + def self.c(re, im = 0) + Complex(re, im) + end + + def self.phase(angle) + Complex(Math.cos(angle), Math.sin(angle)) + end + private_class_method :c, :phase + end +end + +# ---- ../lib/mercury/circuit.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + # A quantum circuit: an ordered list of gate operations on n qubits. + # + # c = Mercury::Circuit.new(2) do |c| + # c.h 0 + # c.cnot 0, 1 + # c.measure_all + # end + # + # Multi-qubit gate qubit order follows the Gates convention: for + # cnot(control, target) the DSL takes (control, target) and stores + # [target, control] so matrix bit 0 is the target. Same for ccx: + # ccx(c1, c2, target) stores [target, c1, c2]. + class Circuit + Op = Struct.new(:gate, :qubits, :params, :classical) + + attr_reader :n, :ops + + DSL_REORDER = { cnot: true, ccx: true }.freeze + + def initialize(n) + raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 + @n = n + @ops = [] + yield self if block_given? + end + + def add(gate, *qubits, params: []) + gate = gate.to_sym + arity = Gates.arity(gate) + raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity + raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length + qubits.each do |q| + raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n + end + stored = DSL_REORDER[gate] ? qubits.reverse : qubits + @ops << Op.new(gate, stored, params, nil) + self + end + + # Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry. + (Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname| + define_method(gname) do |*args| + need = Gates.param_count(gname) + arity = Gates.arity(gname) + raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity + params = args.first(need) + qubits = args.last(arity) + add(gname, *qubits, params: params) + end + end + + def measure(qubit, clbit = nil) + raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n + @ops << Op.new(:measure, [qubit], [], clbit || qubit) + self + end + + def measure_all + @n.times { |q| measure(q, q) } + self + end + + def depth + @ops.length + end + + # Minimal Unicode circuit diagram. + def to_s + cols = @ops.map { |op| column(op) } + rows = (0...@n).map do |q| + label = "q#{q}: " + body = cols.map { |col| col[q] }.join("") + label + body + end + rows.join("\n") + end + + private + + def column(op) + cells = Array.new(@n, "────────") + case op.gate + when :measure + cells[op.qubits[0]] = "───╫M───" + else + qs = op.qubits + tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase + if qs.length == 1 + cells[qs[0]] = "───#{tag[0, 2]}───" + else + # multi-qubit: control dots on qs[1..], box on qs[0], rail between + lo, hi = qs.minmax + (lo..hi).each { |q| cells[q] = "───┼────" } + cells[qs[0]] = "───#{tag[0, 2]}───" + qs[1..].each { |q| cells[q] = "───●────" } + end + end + cells + end + end +end + +# ---- ../lib/mercury/simulator.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + # Full statevector simulator. Little-endian: qubit q is bit q of the + # basis index, matching the Gates matrix convention. + class Statevector + attr_reader :n, :amps + + # initial: integer basis index, or MSB-first bitstring like "0011". + def initialize(n, initial: 0) + raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 + initial = Integer(initial, 2) if initial.is_a?(String) + raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1) + @n = n + @dim = 1 << n + @amps = Array.new(@dim, Complex(0, 0)) + @amps[initial] = Complex(1, 0) + end + + def dim + @dim + end + + def norm + Math.sqrt(@amps.sum { |a| a.abs**2 }) + end + + def apply(gate, qubits, params = []) + matrix = Gates.matrix(gate, params) + case qubits.length + when 1 then apply_one(matrix, qubits[0]) + when 2 then apply_two(matrix, qubits[0], qubits[1]) + when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2]) + else raise ArgumentError, "unsupported arity #{qubits.length}" + end + self + end + + def probabilities + @amps.map { |a| a.abs**2 } + end + + # Sample `shots` bitstrings (MSB-first strings) from the distribution. + def sample(shots, rng = Random.new) + probs = probabilities + cumulative = [] + total = 0.0 + probs.each { |p| total += p; cumulative << total } + Array.new(shots) do + r = rng.rand * total + idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1) + bitstring(idx) + end + end + + # Projective Z measurement of one qubit; collapses the state in place. + def measure!(qubit, rng = Random.new) + raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1) + step = 1 << qubit + p0 = 0.0 + @amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? } + outcome = rng.rand < p0 ? 0 : 1 + renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0)) + raise "measurement of zero-probability branch" if renorm.zero? + @amps.each_with_index do |a, s| + bit = (s & step).zero? ? 0 : 1 + @amps[s] = bit == outcome ? a / renorm : Complex(0, 0) + end + outcome + end + + def bitstring(index) + # MSB-first for display: qubit n-1 ... qubit 0 + (0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join + end + + private + + def apply_one(m, q) + step = 1 << q + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & step).zero? + i0 = s + i1 = s | step + a0 = @amps[i0] + a1 = @amps[i1] + nxt[i0] = m[0][0] * a0 + m[0][1] * a1 + nxt[i1] = m[1][0] * a0 + m[1][1] * a1 + end + @amps = nxt + end + + def apply_two(m, q0, q1) + raise ArgumentError, "q0 == q1" if q0 == q1 + s0 = 1 << q0 + s1 = 1 << q1 + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & s0).zero? && (s & s1).zero? + idx = [s, s | s0, s | s1, s | s0 | s1] + old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])] + 4.times do |r| + nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3] + end + end + @amps = nxt + end + + def apply_three(m, q0, q1, q2) + raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3 + s0 = 1 << q0 + s1 = 1 << q1 + s2 = 1 << q2 + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero? + idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) } + old = idx.map { |i| @amps[i] } + 8.times do |r| + acc = Complex(0, 0) + 8.times { |cidx| acc += m[r][cidx] * old[cidx] } + nxt[idx[r]] = acc + end + end + @amps = nxt + end + end + + # Runs a Circuit. :measure ops collapse immediately; `shots` samples the + # final distribution. + class Simulator + Result = Struct.new(:counts, :statevector, :probabilities, :measured) + + def run(circuit, shots: 0, seed: nil, initial: 0) + rng = seed.nil? ? Random.new : Random.new(seed) + sv = Statevector.new(circuit.n, initial: initial) + measured = {} + circuit.ops.each do |op| + if op.gate == :measure + measured[op.classical] = sv.measure!(op.qubits[0], rng) + else + sv.apply(op.gate, op.qubits, op.params) + end + end + counts = {} + if shots.positive? + sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 } + end + Result.new(counts, sv.amps.dup, sv.probabilities, measured) + end + end +end + +# ---- ../lib/mercury/mini_json.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Minimal JSON parser/generator covering exactly what the Mercury web +# bridge needs (objects, arrays, strings, numbers, booleans, null). +# Pure Ruby, no dependencies — safe inside Ruby WASM. +module Mercury + module MiniJSON + class ParseError < StandardError; end + + def self.parse(str) + @src = str + @pos = 0 + skip_ws + val = parse_value + skip_ws + raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length + val + end + + def self.generate(obj) + case obj + when Hash + "{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}" + when Array + "[" + obj.map { |v| generate(v) }.join(",") + "]" + when String + '"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"' + when Integer, Float + raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite? + obj.to_s + when true then "true" + when false then "false" + when nil then "null" + else + raise ParseError, "cannot serialize #{obj.class}" + end + end + + def self.skip_ws + @pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/ + end + + def self.parse_value + case @src[@pos] + when '"' then parse_string + when "{" then parse_object + when "[" then parse_array + when "t" then literal("true", true) + when "f" then literal("false", false) + when "n" then literal("null", nil) + else parse_number + end + end + + def self.literal(word, val) + raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word + @pos += word.length + val + end + + def self.parse_string + @pos += 1 # opening quote + out = +"" + while @pos < @src.length + ch = @src[@pos] + case ch + when '"' + @pos += 1 + return out + when "\\" + @pos += 1 + esc = @src[@pos] + out << case esc + when '"' then '"' + when "\\" then "\\" + when "/" then "/" + when "b" then "\b" + when "f" then "\f" + when "n" then "\n" + when "r" then "\r" + when "t" then "\t" + when "u" + hex = @src[@pos + 1, 4] + @pos += 4 + [hex.to_i(16)].pack("U") + else raise ParseError, "bad escape at #{@pos}" + end + @pos += 1 + else + out << ch + @pos += 1 + end + end + raise ParseError, "unterminated string" + end + + def self.parse_object + @pos += 1 + obj = {} + skip_ws + if @src[@pos] == "}" + @pos += 1 + return obj + end + loop do + skip_ws + raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"' + key = parse_string + skip_ws + raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":" + @pos += 1 + skip_ws + obj[key] = parse_value + skip_ws + case @src[@pos] + when "," then @pos += 1 + when "}" then @pos += 1; break + else raise ParseError, "expected , or } at #{@pos}" + end + end + obj + end + + def self.parse_array + @pos += 1 + arr = [] + skip_ws + if @src[@pos] == "]" + @pos += 1 + return arr + end + loop do + skip_ws + arr << parse_value + skip_ws + case @src[@pos] + when "," then @pos += 1 + when "]" then @pos += 1; break + else raise ParseError, "expected , or ] at #{@pos}" + end + end + arr + end + + def self.parse_number + m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/) + raise ParseError, "bad number at #{@pos}" unless m + @pos += m[0].length + s = m[0] + (s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i + end + end +end + +# ---- ../lib/mercury/cpsc.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a +# Trotterized scattering evolution built as a Mercury circuit. +# +# Gate-kind semantics (identical to the Python original): +# - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z. +# - "parity_even": preserves prod Z (Z2 parity) but may change sum Z. +# - "illegal": odd X/Y support; changes Z-parity. Rejected. +module Mercury + module CPSC + # Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds. + KINDS = { + i: "magnetization", z: "magnetization", s: "magnetization", + t: "magnetization", sdg: "magnetization", tdg: "magnetization", + rz: "magnetization", cz: "magnetization", rzz: "magnetization", + cp: "magnetization", + xx: "parity_even", + x: "illegal", y: "illegal", h: "illegal", + rx: "illegal", ry: "illegal", u: "illegal", + cnot: "illegal", swap: "illegal", ccx: "illegal" + }.freeze + + GateSpec = Struct.new(:name, :qubits, :kind) + Rejection = Struct.new(:spec, :reason) + + class CompilationRejected < StandardError; end + + class SymmetryAnalyzer + def initialize(n, conserve_magnetization: false) + raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1 + @n = n + @conserve_magnetization = conserve_magnetization + end + + def check(specs) + specs.flat_map do |spec| + reason = reject_reason(spec) + reason ? [Rejection.new(spec, reason)] : [] + end + end + + def admit(specs) + rejected = check(specs) + unless rejected.empty? + detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ") + raise CompilationRejected, detail + end + specs.dup + end + + private + + def reject_reason(spec) + spec.qubits.each do |q| + return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1) + end + case spec.kind + when "illegal" + "odd X/Y support changes Z-parity; forbidden sector" + when "parity_even", "magnetization" + nil + else + "unknown gate kind #{spec.kind}" + end + end + end + + # Classify a Mercury::Circuit::Op into a GateSpec for the analyzer. + def self.classify(op) + kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" } + label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})" + GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind) + end + + # Run the analyzer over a whole circuit. Returns + # { admitted: bool, rejections: [{gate, reason}] }. + def self.analyze(circuit, conserve_magnetization: false) + analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization) + specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) } + rejections = analyzer.check(specs) + { + admitted: rejections.empty?, + rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } } + } + end + + # First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i + # (periodic), matching src/cpsc/hamiltonian.py. Per step dt: + # prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i). + def self.trotter_circuit(n, j:, lam:, time:, steps:) + raise ArgumentError, "steps must be >= 1" unless steps >= 1 + dt = time.to_f / steps + Circuit.new(n) do |c| + steps.times do + (0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) } + (0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero? + end + end + end + + def self.parity_of(basis, n) + basis.to_s(2).count("1") & 1 + end + + def self.magnetization_of(basis, n) + mag = 0 + n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 } + mag + end + + # Probability mass outside the input Z2-parity sector. + def self.leakage(amps, n, parity) + wrong = 0.0 + amps.each_with_index do |a, b| + wrong += a.abs**2 if parity_of(b, n) != parity + end + wrong + end + + # for the transverse-field Ising H, computed directly. + def self.energy_expectation(amps, n, j:, lam:) + dim = 1 << n + e = 0.0 + dim.times do |s| + p = amps[s].abs**2 + next if p.zero? + zz = 0.0 + n.times do |i| + zi = ((s >> i) & 1).zero? ? 1.0 : -1.0 + zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0 + zz += zi * zj + end + e += p * (-j * zz) + end + unless lam.zero? + x = 0.0 + n.times do |i| + bit = 1 << i + dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real } + end + e += lam * x + end + e + end + + end +end + +# ---- app.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into +# mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript +# drives it via vm.eval("handle_request('')"). +# +# Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]], +# "shots":1024,"seed":42,"cpsc":true,"magnetization":false, +# "j":1.0,"lam":0.0} +# Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...}, +# "statevector":[[bits,re,im],...],"norm":1.0, +# "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}} + +def bitstring_of(index, n) + (0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join +end + +def handle_request(json_str) + req = Mercury::MiniJSON.parse(json_str) + n = req["n"].to_i + raise "n must be 1..12" unless n.between?(1, 12) + + c = Mercury::Circuit.new(n) + (req["ops"] || []).each do |op| + name = op[0].to_s.to_sym + qubits = op[1].map(&:to_i) + params = (op[2] || []).map(&:to_f) + c.add(name, *qubits, params: params) + end + + initial = req.fetch("initial", "0" * n).to_s + raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/) + init_idx = Integer(initial, 2) + + shots = req.fetch("shots", 0).to_i + seed = req["seed"] + + res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx) + + probs = [] + res.probabilities.each_with_index do |p, i| + probs << [bitstring_of(i, n), p] if p > 1e-9 + end + probs.sort_by! { |_, p| -p } + + sv = [] + res.statevector.each_with_index do |a, i| + sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9 + end + sv.sort_by! { |_, re, im| -(re * re + im * im) } + + out = { + "diagram" => c.to_s, + "probs" => probs.first(24), + "counts" => res.counts, + "statevector" => sv.first(24), + "norm" => Math.sqrt(res.probabilities.sum) + } + + if req["cpsc"] + verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"]) + out["cpsc"] = { + "admitted" => verdict[:admitted], + "rejections" => verdict[:rejections], + "leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)), + "energy" => Mercury::CPSC.energy_expectation( + res.statevector, n, + j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f + ) + } + end + + Mercury::MiniJSON.generate(out) +rescue => e + Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" }) +end + diff --git a/docs/qsim.html b/docs/qsim.html new file mode 100644 index 0000000000000000000000000000000000000000..6384392ed2c829be585929aa349436baf4e6ae3f --- /dev/null +++ b/docs/qsim.html @@ -0,0 +1,307 @@ + + + + + + +Mercury — Quantum Circuit Console · CPSC-QFT + + + +
+

MERCURY

+ site + quantum circuit console · CPSC-QFT frontend · Ruby/WASM statevector simulator + ● loading Ruby… +
+
+
+
+

Circuit

+
+
+
+
+ + + +
+ +
+ + + + + +
+
+
+

Run

+
+
+
+
+ +
+
+
+
+
+
+
+
+

Conservation (CPSC)

+
Run a circuit to check symmetry-sector admission.
+
+
+
+
+

Circuit diagram

+
—
+
+
+

Measurement histogram

+ +
+
+

Statevector (top amplitudes)

+ +
basisReImP
—
+
+
+

Probabilities

+ +
basisP
—
+
+
+
+
+ Mercury is a dependency-free Ruby statevector simulator (up to 12 qubits in-browser). The CPSC panel mirrors + src/cpsc/analyzer.py: gates are admitted only if they preserve the Z2 parity sector — + a compile-time rejection rule, not post-selection. Trotter preset evolves the transverse-field Ising H under test. +
+ + + + + diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f709ce9f6755da94601a95c7a47e0ba1e9ffb270 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,104 @@ + + +# Mercury — Ruby quantum circuit frontend for CPSC-QFT + +A dependency-free Ruby quantum circuit library and full statevector +simulator, with a browser console (Ruby/WASM) for building circuits, +running them, and checking CPSC conservation laws at compile time. + +## Layout + +``` +frontend/ + lib/mercury.rb entry point (require this) + lib/mercury/version.rb + lib/mercury/gates.rb gate matrices: H X Y Z S T Rx Ry Rz U, + CNOT CZ CP SWAP RZZ XX, Toffoli + lib/mercury/circuit.rb circuit DSL + Unicode diagram + lib/mercury/simulator.rb full statevector simulator + lib/mercury/mini_json.rb dependency-free JSON (WASM-safe) + lib/mercury/cpsc.rb CPSC bridge: symmetry analyzer, Trotter + scattering evolution, leakage, + test/test_mercury.rb 25 tests, 556 assertions (minitest, stdlib only) + web/index.html console template (= qsim.html after build) + web/app.rb WASM bridge (handle_request) + web/build.sh builds mercury_bundle.rb + qsim.html + web/qsim.html the console (generated) + web/mercury_bundle.rb concatenated Ruby (generated, fetched at boot) +``` + +## Quickstart (Ruby) + +```ruby +require_relative "lib/mercury" + +# Bell pair +c = Mercury::Circuit.new(2) do |c| + c.h 0 + c.cnot 0, 1 +end +puts c.to_s + +res = Mercury::Simulator.new.run(c, shots: 1000, seed: 42) +p res.counts # {"00"=>..., "11"=>...} + +# CPSC conservation check (mirrors src/cpsc/analyzer.py) +p Mercury::CPSC.analyze(c) +# => {:admitted=>false, :rejections=>[{:gate=>"H[0]", ...}, ...]} +# H and CNOT do not preserve the Z2 parity sector — rejected, as designed. + +# Trotterized scattering: H = -J ΣZ_i Z_{i+1} + λ ΣX_i (periodic) +trotter = Mercury::CPSC.trotter_circuit(4, j: 1.0, lam: 0.0, time: 0.4, steps: 8) +psi = Mercury::Simulator.new.run(trotter, initial: "0011").statevector +p Mercury::CPSC.leakage(psi, 4, Mercury::CPSC.parity_of(0b0011, 4)) # => 0.0 +``` + +Run the tests: + +```bash +cd frontend && ruby -Ilib test/test_mercury.rb +``` + +## Web console + +Build, then serve the `web/` directory over HTTP (WASM requires it): + +```bash +cd frontend/web && ./build.sh +ruby -run -e httpd -- . -p 8123 +# open http://localhost:8123/qsim.html +``` + +The console boots Ruby 3.2 (ruby.wasm, CDN) and evaluates the verbatim +`mercury_bundle.rb`. Features: + +- circuit script editor (one op per line) + gate palette + presets + (Bell, GHZ-3, QFT-3, Trotter scattering, adversarial X) +- Unicode circuit diagram, measurement histogram, statevector and + probability tables +- CPSC panel: compile-time admission verdict, parity leakage, + energy expectation — the analyzer rejects illegal gates before + simulation, never post-selects after + +To publish on GitHub Pages, copy `qsim.html` and `mercury_bundle.rb` +side-by-side into `docs/`. + +## Notes + +- Little-endian: qubit `q` is bit `q` of the basis index, matching the + Python prototype's convention. +- At λ=0 the Trotter terms commute, so the Trotter circuit is exact: + verified bit-identical against `cpsc.channel.evolve_in_sector` + (max |Δ| = 0.0, n=4, t=0.4). +- At λ≠0 the transverse field genuinely mixes parity sectors; the + console reports the honest leakage instead of projecting it away. +- `Mercury::CPSC.prepare` was removed in favor of `initial:` basis + strings so state preparation never smuggles X gates past the analyzer. + +## License + +Dual strict copyleft: `LicenseRef-CPSC-ESCL-1.0` OR `AGPL-3.0-only`. +See the repository root `LICENSE`, `NOTICE`, `LICENSES/`. diff --git a/frontend/lib/mercury.rb b/frontend/lib/mercury.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3cb69342e13d31d57af98001c657ae79570a375 --- /dev/null +++ b/frontend/lib/mercury.rb @@ -0,0 +1,21 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Mercury: a dependency-free Ruby quantum circuit library and full +# statevector simulator, built as the circuit front end for CPSC-QFT. +require_relative "mercury/version" +require_relative "mercury/gates" +require_relative "mercury/circuit" +require_relative "mercury/simulator" +require_relative "mercury/mini_json" +require_relative "mercury/cpsc" diff --git a/frontend/lib/mercury/circuit.rb b/frontend/lib/mercury/circuit.rb new file mode 100644 index 0000000000000000000000000000000000000000..b216531dc44c1712999289827d32d84f3efd7792 --- /dev/null +++ b/frontend/lib/mercury/circuit.rb @@ -0,0 +1,115 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + # A quantum circuit: an ordered list of gate operations on n qubits. + # + # c = Mercury::Circuit.new(2) do |c| + # c.h 0 + # c.cnot 0, 1 + # c.measure_all + # end + # + # Multi-qubit gate qubit order follows the Gates convention: for + # cnot(control, target) the DSL takes (control, target) and stores + # [target, control] so matrix bit 0 is the target. Same for ccx: + # ccx(c1, c2, target) stores [target, c1, c2]. + class Circuit + Op = Struct.new(:gate, :qubits, :params, :classical) + + attr_reader :n, :ops + + DSL_REORDER = { cnot: true, ccx: true }.freeze + + def initialize(n) + raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 + @n = n + @ops = [] + yield self if block_given? + end + + def add(gate, *qubits, params: []) + gate = gate.to_sym + arity = Gates.arity(gate) + raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity + raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length + qubits.each do |q| + raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n + end + stored = DSL_REORDER[gate] ? qubits.reverse : qubits + @ops << Op.new(gate, stored, params, nil) + self + end + + # Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry. + (Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname| + define_method(gname) do |*args| + need = Gates.param_count(gname) + arity = Gates.arity(gname) + raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity + params = args.first(need) + qubits = args.last(arity) + add(gname, *qubits, params: params) + end + end + + def measure(qubit, clbit = nil) + raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n + @ops << Op.new(:measure, [qubit], [], clbit || qubit) + self + end + + def measure_all + @n.times { |q| measure(q, q) } + self + end + + def depth + @ops.length + end + + # Minimal Unicode circuit diagram. + def to_s + cols = @ops.map { |op| column(op) } + rows = (0...@n).map do |q| + label = "q#{q}: " + body = cols.map { |col| col[q] }.join("") + label + body + end + rows.join("\n") + end + + private + + def column(op) + cells = Array.new(@n, "────────") + case op.gate + when :measure + cells[op.qubits[0]] = "───╫M───" + else + qs = op.qubits + tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase + if qs.length == 1 + cells[qs[0]] = "───#{tag[0, 2]}───" + else + # multi-qubit: control dots on qs[1..], box on qs[0], rail between + lo, hi = qs.minmax + (lo..hi).each { |q| cells[q] = "───┼────" } + cells[qs[0]] = "───#{tag[0, 2]}───" + qs[1..].each { |q| cells[q] = "───●────" } + end + end + cells + end + end +end diff --git a/frontend/lib/mercury/cpsc.rb b/frontend/lib/mercury/cpsc.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ccdc1b200e9bb99c8b59eff0f033064e61fd64c --- /dev/null +++ b/frontend/lib/mercury/cpsc.rb @@ -0,0 +1,159 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a +# Trotterized scattering evolution built as a Mercury circuit. +# +# Gate-kind semantics (identical to the Python original): +# - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z. +# - "parity_even": preserves prod Z (Z2 parity) but may change sum Z. +# - "illegal": odd X/Y support; changes Z-parity. Rejected. +module Mercury + module CPSC + # Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds. + KINDS = { + i: "magnetization", z: "magnetization", s: "magnetization", + t: "magnetization", sdg: "magnetization", tdg: "magnetization", + rz: "magnetization", cz: "magnetization", rzz: "magnetization", + cp: "magnetization", + xx: "parity_even", + x: "illegal", y: "illegal", h: "illegal", + rx: "illegal", ry: "illegal", u: "illegal", + cnot: "illegal", swap: "illegal", ccx: "illegal" + }.freeze + + GateSpec = Struct.new(:name, :qubits, :kind) + Rejection = Struct.new(:spec, :reason) + + class CompilationRejected < StandardError; end + + class SymmetryAnalyzer + def initialize(n, conserve_magnetization: false) + raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1 + @n = n + @conserve_magnetization = conserve_magnetization + end + + def check(specs) + specs.flat_map do |spec| + reason = reject_reason(spec) + reason ? [Rejection.new(spec, reason)] : [] + end + end + + def admit(specs) + rejected = check(specs) + unless rejected.empty? + detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ") + raise CompilationRejected, detail + end + specs.dup + end + + private + + def reject_reason(spec) + spec.qubits.each do |q| + return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1) + end + case spec.kind + when "illegal" + "odd X/Y support changes Z-parity; forbidden sector" + when "parity_even", "magnetization" + nil + else + "unknown gate kind #{spec.kind}" + end + end + end + + # Classify a Mercury::Circuit::Op into a GateSpec for the analyzer. + def self.classify(op) + kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" } + label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})" + GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind) + end + + # Run the analyzer over a whole circuit. Returns + # { admitted: bool, rejections: [{gate, reason}] }. + def self.analyze(circuit, conserve_magnetization: false) + analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization) + specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) } + rejections = analyzer.check(specs) + { + admitted: rejections.empty?, + rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } } + } + end + + # First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i + # (periodic), matching src/cpsc/hamiltonian.py. Per step dt: + # prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i). + def self.trotter_circuit(n, j:, lam:, time:, steps:) + raise ArgumentError, "steps must be >= 1" unless steps >= 1 + dt = time.to_f / steps + Circuit.new(n) do |c| + steps.times do + (0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) } + (0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero? + end + end + end + + def self.parity_of(basis, n) + basis.to_s(2).count("1") & 1 + end + + def self.magnetization_of(basis, n) + mag = 0 + n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 } + mag + end + + # Probability mass outside the input Z2-parity sector. + def self.leakage(amps, n, parity) + wrong = 0.0 + amps.each_with_index do |a, b| + wrong += a.abs**2 if parity_of(b, n) != parity + end + wrong + end + + # for the transverse-field Ising H, computed directly. + def self.energy_expectation(amps, n, j:, lam:) + dim = 1 << n + e = 0.0 + dim.times do |s| + p = amps[s].abs**2 + next if p.zero? + zz = 0.0 + n.times do |i| + zi = ((s >> i) & 1).zero? ? 1.0 : -1.0 + zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0 + zz += zi * zj + end + e += p * (-j * zz) + end + unless lam.zero? + x = 0.0 + n.times do |i| + bit = 1 << i + dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real } + end + e += lam * x + end + e + end + + end +end diff --git a/frontend/lib/mercury/gates.rb b/frontend/lib/mercury/gates.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c44ff16607d5ab63cb3d49feaa94e0107854f40 --- /dev/null +++ b/frontend/lib/mercury/gates.rb @@ -0,0 +1,126 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Gate matrices for Mercury. Little-endian convention: in a k-qubit gate +# applied to qubits [q0, q1, ...], matrix row/column index bit j (value +# 2**j) is the computational-basis state of qubit q_j. For a two-qubit +# gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0). +module Mercury + module Gates + ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze + TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze + THREE_QUBIT = %i[ccx].freeze + + PARAM_COUNTS = { + i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0, + rx: 1, ry: 1, rz: 1, u: 3, + cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0, + ccx: 0 + }.freeze + + def self.arity(name) + return 1 if ONE_QUBIT.include?(name) + return 2 if TWO_QUBIT.include?(name) + return 3 if THREE_QUBIT.include?(name) + raise ArgumentError, "unknown gate #{name}" + end + + def self.param_count(name) + PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" } + end + + def self.matrix(name, params = []) + need = param_count(name) + raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need + + case name + when :i then [[c(1), c(0)], [c(0), c(1)]] + when :x then [[c(0), c(1)], [c(1), c(0)]] + when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]] + when :z then [[c(1), c(0)], [c(0), c(-1)]] + when :h + s = Math.sqrt(0.5) + [[c(s), c(s)], [c(s), c(-s)]] + when :s then [[c(1), c(0)], [c(0), c(0, 1)]] + when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]] + when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]] + when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]] + when :rx + th = params[0] / 2.0 + co = c(Math.cos(th)); si = c(0, -Math.sin(th)) + [[co, si], [si, co]] + when :ry + th = params[0] / 2.0 + [[c(Math.cos(th)), c(-Math.sin(th))], + [c(Math.sin(th)), c(Math.cos(th))]] + when :rz + th = params[0] / 2.0 + [[phase(-th), c(0)], [c(0), phase(th)]] + when :u + th, ph, la = params + co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0)) + [[co, c(-1) * phase(la) * si], + [phase(ph) * si, phase(ph + la) * co]] + when :cnot + # qubits [target, control]; flips target iff control == 1 + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(0), c(1)], + [c(0), c(0), c(1), c(0)]] + when :cz + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(0), c(0), c(-1)]] + when :cp + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(0), c(0), phase(params[0])]] + when :swap + [[c(1), c(0), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(0), c(1)]] + when :rzz + # exp(-i*theta/2 * ZxZ) + a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0) + [[a, c(0), c(0), c(0)], + [c(0), b, c(0), c(0)], + [c(0), c(0), b, c(0)], + [c(0), c(0), c(0), a]] + when :xx + # XxX: parity-even pair used by the CPSC analyzer + [[c(0), c(0), c(0), c(1)], + [c(0), c(0), c(1), c(0)], + [c(0), c(1), c(0), c(0)], + [c(1), c(0), c(0), c(0)]] + when :ccx + # qubits [target, control1, control2]; flips target iff both controls == 1 + m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } } + m[6][6] = c(0); m[7][7] = c(0) + m[6][7] = c(1); m[7][6] = c(1) + m + end + end + + def self.c(re, im = 0) + Complex(re, im) + end + + def self.phase(angle) + Complex(Math.cos(angle), Math.sin(angle)) + end + private_class_method :c, :phase + end +end diff --git a/frontend/lib/mercury/mini_json.rb b/frontend/lib/mercury/mini_json.rb new file mode 100644 index 0000000000000000000000000000000000000000..55a50d79899e0bf257ab31c84eff0b74de047503 --- /dev/null +++ b/frontend/lib/mercury/mini_json.rb @@ -0,0 +1,164 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Minimal JSON parser/generator covering exactly what the Mercury web +# bridge needs (objects, arrays, strings, numbers, booleans, null). +# Pure Ruby, no dependencies — safe inside Ruby WASM. +module Mercury + module MiniJSON + class ParseError < StandardError; end + + def self.parse(str) + @src = str + @pos = 0 + skip_ws + val = parse_value + skip_ws + raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length + val + end + + def self.generate(obj) + case obj + when Hash + "{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}" + when Array + "[" + obj.map { |v| generate(v) }.join(",") + "]" + when String + '"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"' + when Integer, Float + raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite? + obj.to_s + when true then "true" + when false then "false" + when nil then "null" + else + raise ParseError, "cannot serialize #{obj.class}" + end + end + + def self.skip_ws + @pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/ + end + + def self.parse_value + case @src[@pos] + when '"' then parse_string + when "{" then parse_object + when "[" then parse_array + when "t" then literal("true", true) + when "f" then literal("false", false) + when "n" then literal("null", nil) + else parse_number + end + end + + def self.literal(word, val) + raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word + @pos += word.length + val + end + + def self.parse_string + @pos += 1 # opening quote + out = +"" + while @pos < @src.length + ch = @src[@pos] + case ch + when '"' + @pos += 1 + return out + when "\\" + @pos += 1 + esc = @src[@pos] + out << case esc + when '"' then '"' + when "\\" then "\\" + when "/" then "/" + when "b" then "\b" + when "f" then "\f" + when "n" then "\n" + when "r" then "\r" + when "t" then "\t" + when "u" + hex = @src[@pos + 1, 4] + @pos += 4 + [hex.to_i(16)].pack("U") + else raise ParseError, "bad escape at #{@pos}" + end + @pos += 1 + else + out << ch + @pos += 1 + end + end + raise ParseError, "unterminated string" + end + + def self.parse_object + @pos += 1 + obj = {} + skip_ws + if @src[@pos] == "}" + @pos += 1 + return obj + end + loop do + skip_ws + raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"' + key = parse_string + skip_ws + raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":" + @pos += 1 + skip_ws + obj[key] = parse_value + skip_ws + case @src[@pos] + when "," then @pos += 1 + when "}" then @pos += 1; break + else raise ParseError, "expected , or } at #{@pos}" + end + end + obj + end + + def self.parse_array + @pos += 1 + arr = [] + skip_ws + if @src[@pos] == "]" + @pos += 1 + return arr + end + loop do + skip_ws + arr << parse_value + skip_ws + case @src[@pos] + when "," then @pos += 1 + when "]" then @pos += 1; break + else raise ParseError, "expected , or ] at #{@pos}" + end + end + arr + end + + def self.parse_number + m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/) + raise ParseError, "bad number at #{@pos}" unless m + @pos += m[0].length + s = m[0] + (s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i + end + end +end diff --git a/frontend/lib/mercury/simulator.rb b/frontend/lib/mercury/simulator.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c93470f074aff5f8a44ffb84e00f747d0eeb371 --- /dev/null +++ b/frontend/lib/mercury/simulator.rb @@ -0,0 +1,164 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + # Full statevector simulator. Little-endian: qubit q is bit q of the + # basis index, matching the Gates matrix convention. + class Statevector + attr_reader :n, :amps + + # initial: integer basis index, or MSB-first bitstring like "0011". + def initialize(n, initial: 0) + raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 + initial = Integer(initial, 2) if initial.is_a?(String) + raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1) + @n = n + @dim = 1 << n + @amps = Array.new(@dim, Complex(0, 0)) + @amps[initial] = Complex(1, 0) + end + + def dim + @dim + end + + def norm + Math.sqrt(@amps.sum { |a| a.abs**2 }) + end + + def apply(gate, qubits, params = []) + matrix = Gates.matrix(gate, params) + case qubits.length + when 1 then apply_one(matrix, qubits[0]) + when 2 then apply_two(matrix, qubits[0], qubits[1]) + when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2]) + else raise ArgumentError, "unsupported arity #{qubits.length}" + end + self + end + + def probabilities + @amps.map { |a| a.abs**2 } + end + + # Sample `shots` bitstrings (MSB-first strings) from the distribution. + def sample(shots, rng = Random.new) + probs = probabilities + cumulative = [] + total = 0.0 + probs.each { |p| total += p; cumulative << total } + Array.new(shots) do + r = rng.rand * total + idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1) + bitstring(idx) + end + end + + # Projective Z measurement of one qubit; collapses the state in place. + def measure!(qubit, rng = Random.new) + raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1) + step = 1 << qubit + p0 = 0.0 + @amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? } + outcome = rng.rand < p0 ? 0 : 1 + renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0)) + raise "measurement of zero-probability branch" if renorm.zero? + @amps.each_with_index do |a, s| + bit = (s & step).zero? ? 0 : 1 + @amps[s] = bit == outcome ? a / renorm : Complex(0, 0) + end + outcome + end + + def bitstring(index) + # MSB-first for display: qubit n-1 ... qubit 0 + (0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join + end + + private + + def apply_one(m, q) + step = 1 << q + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & step).zero? + i0 = s + i1 = s | step + a0 = @amps[i0] + a1 = @amps[i1] + nxt[i0] = m[0][0] * a0 + m[0][1] * a1 + nxt[i1] = m[1][0] * a0 + m[1][1] * a1 + end + @amps = nxt + end + + def apply_two(m, q0, q1) + raise ArgumentError, "q0 == q1" if q0 == q1 + s0 = 1 << q0 + s1 = 1 << q1 + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & s0).zero? && (s & s1).zero? + idx = [s, s | s0, s | s1, s | s0 | s1] + old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])] + 4.times do |r| + nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3] + end + end + @amps = nxt + end + + def apply_three(m, q0, q1, q2) + raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3 + s0 = 1 << q0 + s1 = 1 << q1 + s2 = 1 << q2 + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero? + idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) } + old = idx.map { |i| @amps[i] } + 8.times do |r| + acc = Complex(0, 0) + 8.times { |cidx| acc += m[r][cidx] * old[cidx] } + nxt[idx[r]] = acc + end + end + @amps = nxt + end + end + + # Runs a Circuit. :measure ops collapse immediately; `shots` samples the + # final distribution. + class Simulator + Result = Struct.new(:counts, :statevector, :probabilities, :measured) + + def run(circuit, shots: 0, seed: nil, initial: 0) + rng = seed.nil? ? Random.new : Random.new(seed) + sv = Statevector.new(circuit.n, initial: initial) + measured = {} + circuit.ops.each do |op| + if op.gate == :measure + measured[op.classical] = sv.measure!(op.qubits[0], rng) + else + sv.apply(op.gate, op.qubits, op.params) + end + end + counts = {} + if shots.positive? + sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 } + end + Result.new(counts, sv.amps.dup, sv.probabilities, measured) + end + end +end diff --git a/frontend/lib/mercury/version.rb b/frontend/lib/mercury/version.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d715df57630b56dc574b5800b060c288bc6379d --- /dev/null +++ b/frontend/lib/mercury/version.rb @@ -0,0 +1,16 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + VERSION = "0.3.0" +end diff --git a/frontend/test/test_mercury.rb b/frontend/test/test_mercury.rb new file mode 100644 index 0000000000000000000000000000000000000000..41240691c227fe6dffefc4b769696f34bd29b000 --- /dev/null +++ b/frontend/test/test_mercury.rb @@ -0,0 +1,261 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +require "minitest/autorun" +require_relative "../lib/mercury" + +def assert_unitary(name, params = []) + m = Mercury::Gates.matrix(name, params) + d = m.length + d.times do |r| + d.times do |c| + acc = Complex(0, 0) + d.times { |k| acc += m[r][k] * m[c][k].conj } + expected = r == c ? Complex(1, 0) : Complex(0, 0) + assert_in_delta expected.real, acc.real, 1e-12, "#{name} unitary [#{r},#{c}] real" + assert_in_delta expected.imag, acc.imag, 1e-12, "#{name} unitary [#{r},#{c}] imag" + end + end +end + +class TestGates < Minitest::Test + def test_all_gates_unitary + %i[i x y z h s t sdg tdg].each { |g| assert_unitary(g) } + %i[rx ry rz].each { |g| [0.0, 0.7, Math::PI, -2.3].each { |t| assert_unitary(g, [t]) } } + assert_unitary(:u, [0.7, 0.2, -0.5]) + %i[cnot cz swap xx].each { |g| assert_unitary(g) } + assert_unitary(:cp, [1.1]) + assert_unitary(:rzz, [0.9]) + assert_unitary(:ccx) + end + + def test_h_twice_is_identity + c = Mercury::Circuit.new(1) { |cc| cc.h(0); cc.h(0) } + res = Mercury::Simulator.new.run(c) + assert_in_delta 1.0, res.probabilities[0], 1e-12 + end + + def test_x_flips + c = Mercury::Circuit.new(1) { |cc| cc.x(0) } + res = Mercury::Simulator.new.run(c) + assert_in_delta 1.0, res.probabilities[1], 1e-12 + end + + def test_rx_pi_flips + c = Mercury::Circuit.new(1) { |cc| cc.rx(Math::PI, 0) } + res = Mercury::Simulator.new.run(c) + assert_in_delta 1.0, res.probabilities[1], 1e-12 + end + + def test_cnot_truth_table + # control=1, target=0 -> target flips; control as qubit 1, target qubit 0 + c = Mercury::Circuit.new(2) { |cc| cc.x(1); cc.cnot(1, 0) } + res = Mercury::Simulator.new.run(c) + assert_in_delta 1.0, res.probabilities[0b11], 1e-12 + # control=0 -> no flip + c2 = Mercury::Circuit.new(2) { |cc| cc.x(0); cc.cnot(1, 0) } + res2 = Mercury::Simulator.new.run(c2) + assert_in_delta 1.0, res2.probabilities[0b01], 1e-12 + end + + def test_toffoli + c = Mercury::Circuit.new(3) { |cc| cc.x(1); cc.x(2); cc.ccx(1, 2, 0) } + res = Mercury::Simulator.new.run(c) + assert_in_delta 1.0, res.probabilities[0b111], 1e-12 + c2 = Mercury::Circuit.new(3) { |cc| cc.x(1); cc.ccx(1, 2, 0) } + res2 = Mercury::Simulator.new.run(c2) + assert_in_delta 1.0, res2.probabilities[0b010], 1e-12 + end +end + +class TestSimulator < Minitest::Test + def test_bell_state + c = Mercury::Circuit.new(2) { |cc| cc.h(0); cc.cnot(0, 1) } + res = Mercury::Simulator.new.run(c) + p = res.probabilities + assert_in_delta 0.5, p[0b00], 1e-12 + assert_in_delta 0.5, p[0b11], 1e-12 + assert_in_delta 0.0, p[0b01], 1e-12 + assert_in_delta 0.0, p[0b10], 1e-12 + end + + def test_ghz3 + c = Mercury::Circuit.new(3) { |cc| cc.h(0); cc.cnot(0, 1); cc.cnot(1, 2) } + res = Mercury::Simulator.new.run(c) + p = res.probabilities + assert_in_delta 0.5, p[0], 1e-12 + assert_in_delta 0.5, p[0b111], 1e-12 + end + + def test_qft_zero_is_uniform + n = 3 + c = Mercury::Circuit.new(n) + n.times do |j| + c.h(j) + ((j + 1)...n).each { |k| c.cp(Math::PI / (1 << (k - j)), k, j) } + end + (0...(n / 2)).each { |j| c.swap(j, n - 1 - j) } + res = Mercury::Simulator.new.run(c) + res.probabilities.each { |p| assert_in_delta 1.0 / 8, p, 1e-12 } + end + + def test_measurement_collapses_then_samples + # measure ops collapse first; sampling a collapsed state is deterministic + c = Mercury::Circuit.new(2) { |cc| cc.h(0); cc.cnot(0, 1); cc.measure_all } + res = Mercury::Simulator.new.run(c, shots: 2000, seed: 7) + assert_equal 2000, res.counts.values.sum + assert_equal 1, res.counts.keys.length, "collapsed state samples one outcome" + assert_equal 1, res.measured.values.uniq.length, "Bell pair bits must agree" + end + + def test_sampling_statistics_no_measure_ops + c = Mercury::Circuit.new(2) { |cc| cc.h(0); cc.cnot(0, 1) } + res = Mercury::Simulator.new.run(c, shots: 2000, seed: 7) + assert_equal 2000, res.counts.values.sum + assert res.counts.keys.all? { |b| %w[00 11].include?(b) }, "only 00/11, got #{res.counts.keys}" + assert_in_delta 1000, res.counts["00"], 150 + end + + def test_norm_preserved_deep_circuit + rng = Random.new(3) + c = Mercury::Circuit.new(5) + 60.times do + g = %i[h x y z s t rx ry rz].sample(random: rng) + q = rng.rand(5) + if %i[rx ry rz].include?(g) + c.add(g, q, params: [rng.rand * 2 * Math::PI]) + else + c.add(g, q) + end + end + 10.times do + a, b = rng.rand(5), rng.rand(5) + redo if a == b + c.cnot(a, b) + end + res = Mercury::Simulator.new.run(c) + assert_in_delta 1.0, res.probabilities.sum, 1e-9 + end + + def test_circuit_validation + assert_raises(ArgumentError) { Mercury::Circuit.new(2) { |c| c.h(2) } } + assert_raises(ArgumentError) { Mercury::Circuit.new(2) { |c| c.cnot(0, 0) } } + assert_raises(ArgumentError) { Mercury::Circuit.new(2).add(:nope, 0) } + end + + def test_initial_bitstring + res = Mercury::Simulator.new.run(Mercury::Circuit.new(4), initial: "1010") + assert_in_delta 1.0, res.probabilities[0b1010], 1e-12 + end +end + +class TestCPSC < Minitest::Test + def test_forbidden_bit_flip_rejected + # mirrors tests/test_adversarial.py::test_forbidden_bit_flip_rejected_at_compile_time + c = Mercury::Circuit.new(4) { |cc| cc.rzz(0.5, 0, 1); cc.x(2); cc.rzz(0.5, 2, 3) } + verdict = Mercury::CPSC.analyze(c) + refute verdict[:admitted] + assert_equal 1, verdict[:rejections].length + assert_match(/X/, verdict[:rejections][0][:gate]) + assert_match(/parity/i, verdict[:rejections][0][:reason]) + end + + def test_legal_circuit_admitted + c = Mercury::Circuit.new(4) { |cc| cc.rzz(0.5, 0, 1); cc.rzz(0.5, 2, 3); cc.xx(1, 2) } + verdict = Mercury::CPSC.analyze(c) + assert verdict[:admitted], "expected admission, got #{verdict[:rejections]}" + end + + def test_qubit_out_of_range_rejected + analyzer = Mercury::CPSC::SymmetryAnalyzer.new(2) + spec = Mercury::CPSC::GateSpec.new("Z[5]", [5], "magnetization") + rej = analyzer.check([spec]) + assert_equal 1, rej.length + assert_match(/outside lattice/, rej[0].reason) + end + + def test_admit_raises_with_detail + analyzer = Mercury::CPSC::SymmetryAnalyzer.new(4) + bad = Mercury::CPSC::GateSpec.new("X[2]", [2], "illegal") + err = assert_raises(Mercury::CPSC::CompilationRejected) { analyzer.admit([bad]) } + assert_match(/X\[2\]/, err.message) + end + + def test_trotter_lambda_zero_exact_sector + n = 4 + init = 0b0011 + trotter = Mercury::CPSC.trotter_circuit(n, j: 1.0, lam: 0.0, time: 0.4, steps: 8) + res = Mercury::Simulator.new.run(trotter, initial: init) + parity = Mercury::CPSC.parity_of(init, n) + assert_in_delta 0.0, Mercury::CPSC.leakage(res.statevector, n, parity), 1e-12 + assert_in_delta 1.0, res.probabilities.sum, 1e-12 + # magnetization conserved: only basis states with mag 0 carry weight + res.statevector.each_with_index do |a, b| + next if a.abs < 1e-12 + assert_equal 0, Mercury::CPSC.magnetization_of(b, n), "basis #{b.to_s(2)} leaked magnetically" + end + end + + def test_trotter_lambda_nonzero_honest_leakage + n = 3 + init = 0b001 + trotter = Mercury::CPSC.trotter_circuit(n, j: 1.0, lam: 0.5, time: 0.3, steps: 12) + res = Mercury::Simulator.new.run(trotter, initial: init) + assert_in_delta 1.0, res.probabilities.sum, 1e-12 + leak = Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init, n)) + assert leak > 0.0, "transverse field must mix parity sectors" + assert leak < 0.5, "leakage #{leak} implausibly large for small evolution" + end + + def test_energy_conserved_lambda_zero + n = 4 + init = 0b0011 + e0 = Mercury::CPSC.energy_expectation( + Mercury::Simulator.new.run(Mercury::Circuit.new(n), initial: init).statevector, + n, j: 1.0, lam: 0.0 + ) + trotter = Mercury::CPSC.trotter_circuit(n, j: 1.0, lam: 0.0, time: 0.4, steps: 8) + e1 = Mercury::CPSC.energy_expectation( + Mercury::Simulator.new.run(trotter, initial: init).statevector, + n, j: 1.0, lam: 0.0 + ) + assert_in_delta e0, e1, 1e-9 + end + + def test_xx_pair_preserves_parity_simulation + c = Mercury::Circuit.new(3) { |cc| cc.xx(0, 2) } + res = Mercury::Simulator.new.run(c, initial: 0b001) + p0 = Mercury::CPSC.parity_of(0b001, 3) + assert_in_delta 0.0, Mercury::CPSC.leakage(res.statevector, 3, p0), 1e-12 + assert_in_delta 1.0, res.probabilities[0b100], 1e-12 + end +end + +class TestMiniJSON < Minitest::Test + def test_round_trip + obj = { "n" => 2, "ops" => [["h", [0], []], ["cnot", [0, 1], [0.5]]], + "flag" => true, "nothing" => nil, "s" => "a\"b\\c" } + assert_equal obj, Mercury::MiniJSON.parse(Mercury::MiniJSON.generate(obj)) + end + + def test_numbers + assert_equal 42, Mercury::MiniJSON.parse("42") + assert_in_delta 3.14, Mercury::MiniJSON.parse("3.14"), 1e-12 + assert_in_delta(-1.5e-3, Mercury::MiniJSON.parse("-1.5e-3"), 1e-15) + end + + def test_bad_input_raises + assert_raises(Mercury::MiniJSON::ParseError) { Mercury::MiniJSON.parse("{bad}") } + assert_raises(Mercury::MiniJSON::ParseError) { Mercury::MiniJSON.parse("[1,2") } + end +end diff --git a/frontend/web/app.rb b/frontend/web/app.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebed1d081701fccb830fd4df2cb690e21209cf98 --- /dev/null +++ b/frontend/web/app.rb @@ -0,0 +1,87 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into +# mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript +# drives it via vm.eval("handle_request('')"). +# +# Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]], +# "shots":1024,"seed":42,"cpsc":true,"magnetization":false, +# "j":1.0,"lam":0.0} +# Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...}, +# "statevector":[[bits,re,im],...],"norm":1.0, +# "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}} + +def bitstring_of(index, n) + (0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join +end + +def handle_request(json_str) + req = Mercury::MiniJSON.parse(json_str) + n = req["n"].to_i + raise "n must be 1..12" unless n.between?(1, 12) + + c = Mercury::Circuit.new(n) + (req["ops"] || []).each do |op| + name = op[0].to_s.to_sym + qubits = op[1].map(&:to_i) + params = (op[2] || []).map(&:to_f) + c.add(name, *qubits, params: params) + end + + initial = req.fetch("initial", "0" * n).to_s + raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/) + init_idx = Integer(initial, 2) + + shots = req.fetch("shots", 0).to_i + seed = req["seed"] + + res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx) + + probs = [] + res.probabilities.each_with_index do |p, i| + probs << [bitstring_of(i, n), p] if p > 1e-9 + end + probs.sort_by! { |_, p| -p } + + sv = [] + res.statevector.each_with_index do |a, i| + sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9 + end + sv.sort_by! { |_, re, im| -(re * re + im * im) } + + out = { + "diagram" => c.to_s, + "probs" => probs.first(24), + "counts" => res.counts, + "statevector" => sv.first(24), + "norm" => Math.sqrt(res.probabilities.sum) + } + + if req["cpsc"] + verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"]) + out["cpsc"] = { + "admitted" => verdict[:admitted], + "rejections" => verdict[:rejections], + "leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)), + "energy" => Mercury::CPSC.energy_expectation( + res.statevector, n, + j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f + ) + } + end + + Mercury::MiniJSON.generate(out) +rescue => e + Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" }) +end diff --git a/frontend/web/build.sh b/frontend/web/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..ae17f8bb6e1b169c382758755608400df429cf18 --- /dev/null +++ b/frontend/web/build.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Build the Mercury web console: +# mercury_bundle.rb — concatenated Ruby library + WASM bridge (fetched by qsim.html at boot) +# qsim.html — the console; references CDN wasm assets (works in any modern browser) +# Drop both files side-by-side anywhere static (e.g. docs/ for GitHub Pages). +set -euo pipefail +cd "$(dirname "$0")" + +{ + for f in ../lib/mercury/version.rb \ + ../lib/mercury/gates.rb \ + ../lib/mercury/circuit.rb \ + ../lib/mercury/simulator.rb \ + ../lib/mercury/mini_json.rb \ + ../lib/mercury/cpsc.rb \ + app.rb; do + echo "# ---- $f ----" + cat "$f" + echo "" + done +} > mercury_bundle.rb + +ruby -c mercury_bundle.rb +echo "wrote mercury_bundle.rb ($(wc -c < mercury_bundle.rb) bytes), qsim.html is index.html" +cp index.html qsim.html +echo "wrote qsim.html ($(wc -c < qsim.html) bytes)" +mkdir -p ../../docs +cp qsim.html ../../docs/qsim.html +cp mercury_bundle.rb ../../docs/mercury_bundle.rb +python3 -c 'from pathlib import Path; q=Path("../../docs/qsim.html"); q.write_text(q.read_text().replace("href=\"./index.html\"", "href=\"./\""))' +echo "published docs/qsim.html and docs/mercury_bundle.rb" diff --git a/frontend/web/index.html b/frontend/web/index.html new file mode 100644 index 0000000000000000000000000000000000000000..13e8611c828d851fdd908a06ee8eadf2ef5283d6 --- /dev/null +++ b/frontend/web/index.html @@ -0,0 +1,307 @@ + + + + + + +Mercury — Quantum Circuit Console · CPSC-QFT + + + +
+

MERCURY

+ site + quantum circuit console · CPSC-QFT frontend · Ruby/WASM statevector simulator + ● loading Ruby… +
+
+
+
+

Circuit

+
+
+
+
+ + + +
+ +
+ + + + + +
+
+
+

Run

+
+
+
+
+ +
+
+
+
+
+
+
+
+

Conservation (CPSC)

+
Run a circuit to check symmetry-sector admission.
+
+
+
+
+

Circuit diagram

+
—
+
+
+

Measurement histogram

+ +
+
+

Statevector (top amplitudes)

+ +
basisReImP
—
+
+
+

Probabilities

+ +
basisP
—
+
+
+
+
+ Mercury is a dependency-free Ruby statevector simulator (up to 12 qubits in-browser). The CPSC panel mirrors + src/cpsc/analyzer.py: gates are admitted only if they preserve the Z2 parity sector — + a compile-time rejection rule, not post-selection. Trotter preset evolves the transverse-field Ising H under test. +
+ + + + + diff --git a/frontend/web/mercury_bundle.rb b/frontend/web/mercury_bundle.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c0c115efda21b023325c27698174fe261ca7f18 --- /dev/null +++ b/frontend/web/mercury_bundle.rb @@ -0,0 +1,845 @@ +# ---- ../lib/mercury/version.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + VERSION = "0.1.0" +end + +# ---- ../lib/mercury/gates.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Gate matrices for Mercury. Little-endian convention: in a k-qubit gate +# applied to qubits [q0, q1, ...], matrix row/column index bit j (value +# 2**j) is the computational-basis state of qubit q_j. For a two-qubit +# gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0). +module Mercury + module Gates + ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze + TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze + THREE_QUBIT = %i[ccx].freeze + + PARAM_COUNTS = { + i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0, + rx: 1, ry: 1, rz: 1, u: 3, + cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0, + ccx: 0 + }.freeze + + def self.arity(name) + return 1 if ONE_QUBIT.include?(name) + return 2 if TWO_QUBIT.include?(name) + return 3 if THREE_QUBIT.include?(name) + raise ArgumentError, "unknown gate #{name}" + end + + def self.param_count(name) + PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" } + end + + def self.matrix(name, params = []) + need = param_count(name) + raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need + + case name + when :i then [[c(1), c(0)], [c(0), c(1)]] + when :x then [[c(0), c(1)], [c(1), c(0)]] + when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]] + when :z then [[c(1), c(0)], [c(0), c(-1)]] + when :h + s = Math.sqrt(0.5) + [[c(s), c(s)], [c(s), c(-s)]] + when :s then [[c(1), c(0)], [c(0), c(0, 1)]] + when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]] + when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]] + when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]] + when :rx + th = params[0] / 2.0 + co = c(Math.cos(th)); si = c(0, -Math.sin(th)) + [[co, si], [si, co]] + when :ry + th = params[0] / 2.0 + [[c(Math.cos(th)), c(-Math.sin(th))], + [c(Math.sin(th)), c(Math.cos(th))]] + when :rz + th = params[0] / 2.0 + [[phase(-th), c(0)], [c(0), phase(th)]] + when :u + th, ph, la = params + co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0)) + [[co, c(-1) * phase(la) * si], + [phase(ph) * si, phase(ph + la) * co]] + when :cnot + # qubits [target, control]; flips target iff control == 1 + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(0), c(1)], + [c(0), c(0), c(1), c(0)]] + when :cz + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(0), c(0), c(-1)]] + when :cp + [[c(1), c(0), c(0), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(0), c(0), phase(params[0])]] + when :swap + [[c(1), c(0), c(0), c(0)], + [c(0), c(0), c(1), c(0)], + [c(0), c(1), c(0), c(0)], + [c(0), c(0), c(0), c(1)]] + when :rzz + # exp(-i*theta/2 * ZxZ) + a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0) + [[a, c(0), c(0), c(0)], + [c(0), b, c(0), c(0)], + [c(0), c(0), b, c(0)], + [c(0), c(0), c(0), a]] + when :xx + # XxX: parity-even pair used by the CPSC analyzer + [[c(0), c(0), c(0), c(1)], + [c(0), c(0), c(1), c(0)], + [c(0), c(1), c(0), c(0)], + [c(1), c(0), c(0), c(0)]] + when :ccx + # qubits [target, control1, control2]; flips target iff both controls == 1 + m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } } + m[6][6] = c(0); m[7][7] = c(0) + m[6][7] = c(1); m[7][6] = c(1) + m + end + end + + def self.c(re, im = 0) + Complex(re, im) + end + + def self.phase(angle) + Complex(Math.cos(angle), Math.sin(angle)) + end + private_class_method :c, :phase + end +end + +# ---- ../lib/mercury/circuit.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + # A quantum circuit: an ordered list of gate operations on n qubits. + # + # c = Mercury::Circuit.new(2) do |c| + # c.h 0 + # c.cnot 0, 1 + # c.measure_all + # end + # + # Multi-qubit gate qubit order follows the Gates convention: for + # cnot(control, target) the DSL takes (control, target) and stores + # [target, control] so matrix bit 0 is the target. Same for ccx: + # ccx(c1, c2, target) stores [target, c1, c2]. + class Circuit + Op = Struct.new(:gate, :qubits, :params, :classical) + + attr_reader :n, :ops + + DSL_REORDER = { cnot: true, ccx: true }.freeze + + def initialize(n) + raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 + @n = n + @ops = [] + yield self if block_given? + end + + def add(gate, *qubits, params: []) + gate = gate.to_sym + arity = Gates.arity(gate) + raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity + raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length + qubits.each do |q| + raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n + end + stored = DSL_REORDER[gate] ? qubits.reverse : qubits + @ops << Op.new(gate, stored, params, nil) + self + end + + # Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry. + (Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname| + define_method(gname) do |*args| + need = Gates.param_count(gname) + arity = Gates.arity(gname) + raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity + params = args.first(need) + qubits = args.last(arity) + add(gname, *qubits, params: params) + end + end + + def measure(qubit, clbit = nil) + raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n + @ops << Op.new(:measure, [qubit], [], clbit || qubit) + self + end + + def measure_all + @n.times { |q| measure(q, q) } + self + end + + def depth + @ops.length + end + + # Minimal Unicode circuit diagram. + def to_s + cols = @ops.map { |op| column(op) } + rows = (0...@n).map do |q| + label = "q#{q}: " + body = cols.map { |col| col[q] }.join("") + label + body + end + rows.join("\n") + end + + private + + def column(op) + cells = Array.new(@n, "────────") + case op.gate + when :measure + cells[op.qubits[0]] = "───╫M───" + else + qs = op.qubits + tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase + if qs.length == 1 + cells[qs[0]] = "───#{tag[0, 2]}───" + else + # multi-qubit: control dots on qs[1..], box on qs[0], rail between + lo, hi = qs.minmax + (lo..hi).each { |q| cells[q] = "───┼────" } + cells[qs[0]] = "───#{tag[0, 2]}───" + qs[1..].each { |q| cells[q] = "───●────" } + end + end + cells + end + end +end + +# ---- ../lib/mercury/simulator.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +module Mercury + # Full statevector simulator. Little-endian: qubit q is bit q of the + # basis index, matching the Gates matrix convention. + class Statevector + attr_reader :n, :amps + + # initial: integer basis index, or MSB-first bitstring like "0011". + def initialize(n, initial: 0) + raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 + initial = Integer(initial, 2) if initial.is_a?(String) + raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1) + @n = n + @dim = 1 << n + @amps = Array.new(@dim, Complex(0, 0)) + @amps[initial] = Complex(1, 0) + end + + def dim + @dim + end + + def norm + Math.sqrt(@amps.sum { |a| a.abs**2 }) + end + + def apply(gate, qubits, params = []) + matrix = Gates.matrix(gate, params) + case qubits.length + when 1 then apply_one(matrix, qubits[0]) + when 2 then apply_two(matrix, qubits[0], qubits[1]) + when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2]) + else raise ArgumentError, "unsupported arity #{qubits.length}" + end + self + end + + def probabilities + @amps.map { |a| a.abs**2 } + end + + # Sample `shots` bitstrings (MSB-first strings) from the distribution. + def sample(shots, rng = Random.new) + probs = probabilities + cumulative = [] + total = 0.0 + probs.each { |p| total += p; cumulative << total } + Array.new(shots) do + r = rng.rand * total + idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1) + bitstring(idx) + end + end + + # Projective Z measurement of one qubit; collapses the state in place. + def measure!(qubit, rng = Random.new) + raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1) + step = 1 << qubit + p0 = 0.0 + @amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? } + outcome = rng.rand < p0 ? 0 : 1 + renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0)) + raise "measurement of zero-probability branch" if renorm.zero? + @amps.each_with_index do |a, s| + bit = (s & step).zero? ? 0 : 1 + @amps[s] = bit == outcome ? a / renorm : Complex(0, 0) + end + outcome + end + + def bitstring(index) + # MSB-first for display: qubit n-1 ... qubit 0 + (0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join + end + + private + + def apply_one(m, q) + step = 1 << q + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & step).zero? + i0 = s + i1 = s | step + a0 = @amps[i0] + a1 = @amps[i1] + nxt[i0] = m[0][0] * a0 + m[0][1] * a1 + nxt[i1] = m[1][0] * a0 + m[1][1] * a1 + end + @amps = nxt + end + + def apply_two(m, q0, q1) + raise ArgumentError, "q0 == q1" if q0 == q1 + s0 = 1 << q0 + s1 = 1 << q1 + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & s0).zero? && (s & s1).zero? + idx = [s, s | s0, s | s1, s | s0 | s1] + old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])] + 4.times do |r| + nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3] + end + end + @amps = nxt + end + + def apply_three(m, q0, q1, q2) + raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3 + s0 = 1 << q0 + s1 = 1 << q1 + s2 = 1 << q2 + nxt = Array.new(@dim, Complex(0, 0)) + (0...@dim).each do |s| + next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero? + idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) } + old = idx.map { |i| @amps[i] } + 8.times do |r| + acc = Complex(0, 0) + 8.times { |cidx| acc += m[r][cidx] * old[cidx] } + nxt[idx[r]] = acc + end + end + @amps = nxt + end + end + + # Runs a Circuit. :measure ops collapse immediately; `shots` samples the + # final distribution. + class Simulator + Result = Struct.new(:counts, :statevector, :probabilities, :measured) + + def run(circuit, shots: 0, seed: nil, initial: 0) + rng = seed.nil? ? Random.new : Random.new(seed) + sv = Statevector.new(circuit.n, initial: initial) + measured = {} + circuit.ops.each do |op| + if op.gate == :measure + measured[op.classical] = sv.measure!(op.qubits[0], rng) + else + sv.apply(op.gate, op.qubits, op.params) + end + end + counts = {} + if shots.positive? + sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 } + end + Result.new(counts, sv.amps.dup, sv.probabilities, measured) + end + end +end + +# ---- ../lib/mercury/mini_json.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Minimal JSON parser/generator covering exactly what the Mercury web +# bridge needs (objects, arrays, strings, numbers, booleans, null). +# Pure Ruby, no dependencies — safe inside Ruby WASM. +module Mercury + module MiniJSON + class ParseError < StandardError; end + + def self.parse(str) + @src = str + @pos = 0 + skip_ws + val = parse_value + skip_ws + raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length + val + end + + def self.generate(obj) + case obj + when Hash + "{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}" + when Array + "[" + obj.map { |v| generate(v) }.join(",") + "]" + when String + '"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"' + when Integer, Float + raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite? + obj.to_s + when true then "true" + when false then "false" + when nil then "null" + else + raise ParseError, "cannot serialize #{obj.class}" + end + end + + def self.skip_ws + @pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/ + end + + def self.parse_value + case @src[@pos] + when '"' then parse_string + when "{" then parse_object + when "[" then parse_array + when "t" then literal("true", true) + when "f" then literal("false", false) + when "n" then literal("null", nil) + else parse_number + end + end + + def self.literal(word, val) + raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word + @pos += word.length + val + end + + def self.parse_string + @pos += 1 # opening quote + out = +"" + while @pos < @src.length + ch = @src[@pos] + case ch + when '"' + @pos += 1 + return out + when "\\" + @pos += 1 + esc = @src[@pos] + out << case esc + when '"' then '"' + when "\\" then "\\" + when "/" then "/" + when "b" then "\b" + when "f" then "\f" + when "n" then "\n" + when "r" then "\r" + when "t" then "\t" + when "u" + hex = @src[@pos + 1, 4] + @pos += 4 + [hex.to_i(16)].pack("U") + else raise ParseError, "bad escape at #{@pos}" + end + @pos += 1 + else + out << ch + @pos += 1 + end + end + raise ParseError, "unterminated string" + end + + def self.parse_object + @pos += 1 + obj = {} + skip_ws + if @src[@pos] == "}" + @pos += 1 + return obj + end + loop do + skip_ws + raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"' + key = parse_string + skip_ws + raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":" + @pos += 1 + skip_ws + obj[key] = parse_value + skip_ws + case @src[@pos] + when "," then @pos += 1 + when "}" then @pos += 1; break + else raise ParseError, "expected , or } at #{@pos}" + end + end + obj + end + + def self.parse_array + @pos += 1 + arr = [] + skip_ws + if @src[@pos] == "]" + @pos += 1 + return arr + end + loop do + skip_ws + arr << parse_value + skip_ws + case @src[@pos] + when "," then @pos += 1 + when "]" then @pos += 1; break + else raise ParseError, "expected , or ] at #{@pos}" + end + end + arr + end + + def self.parse_number + m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/) + raise ParseError, "bad number at #{@pos}" unless m + @pos += m[0].length + s = m[0] + (s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i + end + end +end + +# ---- ../lib/mercury/cpsc.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a +# Trotterized scattering evolution built as a Mercury circuit. +# +# Gate-kind semantics (identical to the Python original): +# - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z. +# - "parity_even": preserves prod Z (Z2 parity) but may change sum Z. +# - "illegal": odd X/Y support; changes Z-parity. Rejected. +module Mercury + module CPSC + # Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds. + KINDS = { + i: "magnetization", z: "magnetization", s: "magnetization", + t: "magnetization", sdg: "magnetization", tdg: "magnetization", + rz: "magnetization", cz: "magnetization", rzz: "magnetization", + cp: "magnetization", + xx: "parity_even", + x: "illegal", y: "illegal", h: "illegal", + rx: "illegal", ry: "illegal", u: "illegal", + cnot: "illegal", swap: "illegal", ccx: "illegal" + }.freeze + + GateSpec = Struct.new(:name, :qubits, :kind) + Rejection = Struct.new(:spec, :reason) + + class CompilationRejected < StandardError; end + + class SymmetryAnalyzer + def initialize(n, conserve_magnetization: false) + raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1 + @n = n + @conserve_magnetization = conserve_magnetization + end + + def check(specs) + specs.flat_map do |spec| + reason = reject_reason(spec) + reason ? [Rejection.new(spec, reason)] : [] + end + end + + def admit(specs) + rejected = check(specs) + unless rejected.empty? + detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ") + raise CompilationRejected, detail + end + specs.dup + end + + private + + def reject_reason(spec) + spec.qubits.each do |q| + return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1) + end + case spec.kind + when "illegal" + "odd X/Y support changes Z-parity; forbidden sector" + when "parity_even", "magnetization" + nil + else + "unknown gate kind #{spec.kind}" + end + end + end + + # Classify a Mercury::Circuit::Op into a GateSpec for the analyzer. + def self.classify(op) + kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" } + label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})" + GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind) + end + + # Run the analyzer over a whole circuit. Returns + # { admitted: bool, rejections: [{gate, reason}] }. + def self.analyze(circuit, conserve_magnetization: false) + analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization) + specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) } + rejections = analyzer.check(specs) + { + admitted: rejections.empty?, + rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } } + } + end + + # First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i + # (periodic), matching src/cpsc/hamiltonian.py. Per step dt: + # prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i). + def self.trotter_circuit(n, j:, lam:, time:, steps:) + raise ArgumentError, "steps must be >= 1" unless steps >= 1 + dt = time.to_f / steps + Circuit.new(n) do |c| + steps.times do + (0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) } + (0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero? + end + end + end + + def self.parity_of(basis, n) + basis.to_s(2).count("1") & 1 + end + + def self.magnetization_of(basis, n) + mag = 0 + n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 } + mag + end + + # Probability mass outside the input Z2-parity sector. + def self.leakage(amps, n, parity) + wrong = 0.0 + amps.each_with_index do |a, b| + wrong += a.abs**2 if parity_of(b, n) != parity + end + wrong + end + + # for the transverse-field Ising H, computed directly. + def self.energy_expectation(amps, n, j:, lam:) + dim = 1 << n + e = 0.0 + dim.times do |s| + p = amps[s].abs**2 + next if p.zero? + zz = 0.0 + n.times do |i| + zi = ((s >> i) & 1).zero? ? 1.0 : -1.0 + zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0 + zz += zi * zj + end + e += p * (-j * zz) + end + unless lam.zero? + x = 0.0 + n.times do |i| + bit = 1 << i + dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real } + end + e += lam * x + end + e + end + + end +end + +# ---- app.rb ---- +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +# Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into +# mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript +# drives it via vm.eval("handle_request('')"). +# +# Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]], +# "shots":1024,"seed":42,"cpsc":true,"magnetization":false, +# "j":1.0,"lam":0.0} +# Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...}, +# "statevector":[[bits,re,im],...],"norm":1.0, +# "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}} + +def bitstring_of(index, n) + (0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join +end + +def handle_request(json_str) + req = Mercury::MiniJSON.parse(json_str) + n = req["n"].to_i + raise "n must be 1..12" unless n.between?(1, 12) + + c = Mercury::Circuit.new(n) + (req["ops"] || []).each do |op| + name = op[0].to_s.to_sym + qubits = op[1].map(&:to_i) + params = (op[2] || []).map(&:to_f) + c.add(name, *qubits, params: params) + end + + initial = req.fetch("initial", "0" * n).to_s + raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/) + init_idx = Integer(initial, 2) + + shots = req.fetch("shots", 0).to_i + seed = req["seed"] + + res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx) + + probs = [] + res.probabilities.each_with_index do |p, i| + probs << [bitstring_of(i, n), p] if p > 1e-9 + end + probs.sort_by! { |_, p| -p } + + sv = [] + res.statevector.each_with_index do |a, i| + sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9 + end + sv.sort_by! { |_, re, im| -(re * re + im * im) } + + out = { + "diagram" => c.to_s, + "probs" => probs.first(24), + "counts" => res.counts, + "statevector" => sv.first(24), + "norm" => Math.sqrt(res.probabilities.sum) + } + + if req["cpsc"] + verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"]) + out["cpsc"] = { + "admitted" => verdict[:admitted], + "rejections" => verdict[:rejections], + "leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)), + "energy" => Mercury::CPSC.energy_expectation( + res.statevector, n, + j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f + ) + } + end + + Mercury::MiniJSON.generate(out) +rescue => e + Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" }) +end + diff --git a/frontend/web/qsim.html b/frontend/web/qsim.html new file mode 100644 index 0000000000000000000000000000000000000000..13e8611c828d851fdd908a06ee8eadf2ef5283d6 --- /dev/null +++ b/frontend/web/qsim.html @@ -0,0 +1,307 @@ + + + + + + +Mercury — Quantum Circuit Console · CPSC-QFT + + + +
+

MERCURY

+ site + quantum circuit console · CPSC-QFT frontend · Ruby/WASM statevector simulator + ● loading Ruby… +
+
+
+
+

Circuit

+
+
+
+
+ + + +
+ +
+ + + + + +
+
+
+

Run

+
+
+
+
+ +
+
+
+
+
+
+
+
+

Conservation (CPSC)

+
Run a circuit to check symmetry-sector admission.
+
+
+
+
+

Circuit diagram

+
—
+
+
+

Measurement histogram

+ +
+
+

Statevector (top amplitudes)

+ +
basisReImP
—
+
+
+

Probabilities

+ +
basisP
—
+
+
+
+
+ Mercury is a dependency-free Ruby statevector simulator (up to 12 qubits in-browser). The CPSC panel mirrors + src/cpsc/analyzer.py: gates are admitted only if they preserve the Z2 parity sector — + a compile-time rejection rule, not post-selection. Trotter preset evolves the transverse-field Ising H under test. +
+ + + + + diff --git a/lean/CPSC.lean b/lean/CPSC.lean new file mode 100644 index 0000000000000000000000000000000000000000..1fe703786b8d487d49cc8860fc7b02e0dc32b906 --- /dev/null +++ b/lean/CPSC.lean @@ -0,0 +1,8 @@ +/- +Copyright (c) 2026 Ahmad Ali Parr and others. +SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +-/ + +import CPSC.Certificate +import CPSC.Axioms +import CPSC.Lemmas diff --git a/lean/CPSC/Axioms.lean b/lean/CPSC/Axioms.lean new file mode 100644 index 0000000000000000000000000000000000000000..d7ef1283e6dee141e1696008192ced30f84cb994 --- /dev/null +++ b/lean/CPSC/Axioms.lean @@ -0,0 +1,89 @@ +import CPSC.Certificate + +/- +Copyright (c) 2026 Ahmad Ali Parr and others. + +SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +-/ + +/-! +Operator axioms. + +These are not proved. They are the commutation facts the compiler is allowed +to use. A later matrix development has to discharge them; until then every +lemma in `CPSC.Lemmas` is conditional on this file. +-/ + + +namespace CPSC + +axiom Unitary : Type + +axiom idU : Unitary + +axiom mul : Unitary → Unitary → Unitary + +axiom gateOp : GateKind → Unitary + +/-- Commutation with magnetization, the λ = 0 invariant. -/ +axiom commMag : Unitary → Prop + +/-- Commutation with ∏ X, the λ ≠ 0 invariant. -/ +axiom commSpin : Unitary → Prop + +axiom id_comm_mag : commMag idU + +axiom id_comm_spin : commSpin idU + +axiom mul_comm_mag (U V : Unitary) : + commMag U → commMag V → commMag (mul U V) + +axiom mul_comm_spin (U V : Unitary) : + commSpin U → commSpin V → commSpin (mul U V) + +axiom mul_assoc (U V W : Unitary) : + mul (mul U V) W = mul U (mul V W) + +axiom mul_id_left (U : Unitary) : mul idU U = U + +axiom mul_id_right (U : Unitary) : mul U idU = U + +axiom zz_mag (i j : Nat) : commMag (gateOp (.zz i j)) + +axiom zz_spin (i j : Nat) : commSpin (gateOp (.zz i j)) + +axiom z_mag (i : Nat) : commMag (gateOp (.loneZ i)) + +axiom pair_spin (i j : Nat) : commSpin (gateOp (.parityEvenPair i j)) + +axiom flip_spin (i : Nat) : commSpin (gateOp (.transverseFlip i)) + +axiom flip_not_mag (i : Nat) : ¬ commMag (gateOp (.transverseFlip i)) + +axiom pair_not_mag (i j : Nat) : ¬ commMag (gateOp (.parityEvenPair i j)) + +axiom z_not_spin (i : Nat) : ¬ commSpin (gateOp (.loneZ i)) + +/-- Bridge: the Boolean analyzer is sound for magnetization. -/ +axiom mag_of_bool (g : GateKind) : + preservesMagnetization g = true → commMag (gateOp g) + +/-- Bridge: the Boolean analyzer is sound for the spin flip. -/ +axiom spin_of_bool (g : GateKind) : + preservesSpinFlip g = true → commSpin (gateOp g) + +/-- Completeness: exact evolution generated by a commuting Hamiltonian stays in the sector. -/ +axiom exact_evolution_in_manifold + (mode : Mode) (H : Unitary) : + (mode = .ising → commMag H) → + (mode = .spinFlip → commSpin H) → + (mode = .ising → commMag H) ∧ (mode = .spinFlip → commSpin H) + +/-- A rejected generator is not repaired by right-multiplication. -/ +axiom reject_not_absorbed_mag (g : GateKind) (V : Unitary) : + ¬ commMag (gateOp g) → ¬ commMag (mul (gateOp g) V) + +axiom reject_not_absorbed_spin (g : GateKind) (V : Unitary) : + ¬ commSpin (gateOp g) → ¬ commSpin (mul (gateOp g) V) + +end CPSC diff --git a/lean/CPSC/Certificate.lean b/lean/CPSC/Certificate.lean new file mode 100644 index 0000000000000000000000000000000000000000..c5d822caa375eb39d2bab18c3237300c85c45d1f --- /dev/null +++ b/lean/CPSC/Certificate.lean @@ -0,0 +1,155 @@ +/- +Copyright (c) 2026 Ahmad Ali Parr and others. + +This program and the accompanying materials are made available under the +terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, or, at your +option, the GNU Affero General Public License, Version 3 only. Both options +are strict copyleft. There is no classpath exception and no permissive +relicensing. + +SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +-/ + +/-! +Boolean certificate layer. + +These lemmas are proved. They do not mention operators. The operator axioms +live in `CPSC.Axioms`, and the bridge lemmas in `CPSC.Lemmas`. +-/ + +namespace CPSC + +inductive GateKind where + | zz : Nat → Nat → GateKind + | parityEvenPair : Nat → Nat → GateKind + | transverseFlip : Nat → GateKind + | loneZ : Nat → GateKind + deriving DecidableEq, Repr + +inductive Mode where + | ising + | spinFlip + deriving DecidableEq, Repr + +/-- λ = 0. A single transverse flip changes magnetization. -/ +def preservesMagnetization : GateKind → Bool + | .zz _ _ => true + | .parityEvenPair _ _ => false + | .transverseFlip _ => false + | .loneZ _ => true + +/-- λ ≠ 0. The TFIM symmetry is ∏ X, which a lone Z anticommutes with. -/ +def preservesSpinFlip : GateKind → Bool + | .zz _ _ => true + | .parityEvenPair _ _ => true + | .transverseFlip _ => true + | .loneZ _ => false + +def preserves : Mode → GateKind → Bool + | .ising, g => preservesMagnetization g + | .spinFlip, g => preservesSpinFlip g + +def allPreserve (p : GateKind → Bool) : List GateKind → Bool + | [] => true + | g :: rest => p g && allPreserve p rest + +def admitted (mode : Mode) (gs : List GateKind) : Bool := + allPreserve (preserves mode) gs + +theorem nil_preserves (p : GateKind → Bool) : allPreserve p [] = true := rfl + +theorem bool_and_parts {a b : Bool} (h : (a && b) = true) : a = true ∧ b = true := by + cases a <;> cases b <;> simp_all + +theorem cons_preserves + (p : GateKind → Bool) (g : GateKind) (rest : List GateKind) + (hg : p g = true) (hr : allPreserve p rest = true) : + allPreserve p (g :: rest) = true := by + simp [allPreserve, hg, hr] + +theorem allPreserve_cons_iff + (p : GateKind → Bool) (g : GateKind) (rest : List GateKind) : + allPreserve p (g :: rest) = true ↔ p g = true ∧ allPreserve p rest = true := by + constructor + · intro h + exact bool_and_parts h + · intro h + exact cons_preserves p g rest h.1 h.2 + +theorem allPreserve_append + (p : GateKind → Bool) (xs ys : List GateKind) : + allPreserve p (xs ++ ys) = (allPreserve p xs && allPreserve p ys) := by + induction xs with + | nil => simp [allPreserve] + | cons g rest ih => + simp [allPreserve, List.cons_append, ih, Bool.and_assoc] + +theorem allPreserve_of_append + (p : GateKind → Bool) {xs ys : List GateKind} + (h : allPreserve p (xs ++ ys) = true) : + allPreserve p xs = true ∧ allPreserve p ys = true := + bool_and_parts (by simpa [allPreserve_append] using h) + +theorem transverse_breaks_ising (i : Nat) : + preservesMagnetization (.transverseFlip i) = false := rfl + +theorem pair_breaks_ising (i j : Nat) : + preservesMagnetization (.parityEvenPair i j) = false := rfl + +theorem loneZ_breaks_spin_flip (i : Nat) : + preservesSpinFlip (.loneZ i) = false := rfl + +theorem zz_preserves_both (i j : Nat) : + preservesMagnetization (.zz i j) = true ∧ preservesSpinFlip (.zz i j) = true := by + constructor <;> rfl + +theorem flip_preserves_spin (i : Nat) : + preservesSpinFlip (.transverseFlip i) = true := rfl + +theorem z_preserves_ising (i : Nat) : + preservesMagnetization (.loneZ i) = true := rfl + +theorem mode_switch_flip (i : Nat) : + preserves .ising (.transverseFlip i) = false ∧ + preserves .spinFlip (.transverseFlip i) = true := by + constructor <;> rfl + +theorem mode_switch_z (i : Nat) : + preserves .ising (.loneZ i) = true ∧ + preserves .spinFlip (.loneZ i) = false := by + constructor <;> rfl + +def isingRing4 : List GateKind := + [.zz 0 1, .zz 1 2, .zz 2 3, .zz 3 0] + +theorem isingRing4_admitted : allPreserve preservesMagnetization isingRing4 = true := rfl + +def spinFlip4 : List GateKind := + isingRing4 ++ [.transverseFlip 0, .transverseFlip 1, .transverseFlip 2, .transverseFlip 3] + +theorem spinFlip4_admitted : allPreserve preservesSpinFlip spinFlip4 = true := rfl + +theorem adversarial_ising_rejected : + allPreserve preservesMagnetization (isingRing4 ++ [.transverseFlip 2]) = false := rfl + +theorem adversarial_spin_flip_rejected : + allPreserve preservesSpinFlip (spinFlip4 ++ [.loneZ 1]) = false := rfl + +theorem rejected_cons + (p : GateKind → Bool) (g : GateKind) (rest : List GateKind) + (hg : p g = false) : + allPreserve p (g :: rest) = false := by + simp [allPreserve, hg] + +theorem adversarial_head_rejected (i : Nat) (rest : List GateKind) : + admitted .ising (.transverseFlip i :: rest) = false := + rejected_cons _ _ _ (transverse_breaks_ising i) + +/-- Orbit closure is a predicate on supports. A ring of length n is the full orbit of one bond. -/ +def bondOrbit (n : Nat) : List GateKind := + List.range n |>.map fun i => .zz i ((i + 1) % n) + +theorem bondOrbit_four_agrees : bondOrbit 4 = isingRing4 := by + rfl + +end CPSC diff --git a/lean/CPSC/Emitted.lean b/lean/CPSC/Emitted.lean new file mode 100644 index 0000000000000000000000000000000000000000..a42945690243299aeedc72f068d3869908bd883f --- /dev/null +++ b/lean/CPSC/Emitted.lean @@ -0,0 +1,16 @@ +/- +Copyright (c) 2026 Ahmad Ali Parr and others. +SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +-/ + +import CPSC.Certificate + +namespace CPSC.Emitted + +-- emitted by cpsc.certificate; do not edit by hand +def emittedHash : String := "e92f82910385ae3c46acc5479d57f4c6357cbbf9bf77d668e469a64b281db8e5" +def emittedGates : List CPSC.GateKind := [.zz 0 1, .zz 1 2, .zz 2 3, .zz 3 0] +theorem emitted_admitted : CPSC.allPreserve CPSC.preservesMagnetization emittedGates = true := by + rfl + +end CPSC.Emitted diff --git a/lean/CPSC/Lemmas.lean b/lean/CPSC/Lemmas.lean new file mode 100644 index 0000000000000000000000000000000000000000..274fc7dc5f7371aeb114398ed60963272a12435e --- /dev/null +++ b/lean/CPSC/Lemmas.lean @@ -0,0 +1,111 @@ +import CPSC.Axioms + +/- +Copyright (c) 2026 Ahmad Ali Parr and others. + +SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +-/ + +/-! +Lemmas derived from the operator axioms. + +Nothing in this file introduces a new axiom. Each result is either a Boolean +fact from `CPSC.Certificate` or a consequence of `CPSC.Axioms`. +-/ + + +namespace CPSC + +noncomputable def circuitOp : List GateKind → Unitary + | [] => idU + | g :: rest => mul (gateOp g) (circuitOp rest) + +theorem circuitOp_nil : circuitOp [] = idU := rfl + +theorem circuitOp_cons (g : GateKind) (rest : List GateKind) : + circuitOp (g :: rest) = mul (gateOp g) (circuitOp rest) := rfl + +theorem circuitOp_append (xs ys : List GateKind) : + circuitOp (xs ++ ys) = mul (circuitOp xs) (circuitOp ys) := by + induction xs with + | nil => + rw [List.nil_append, circuitOp_nil, mul_id_left] + | cons g rest ih => + rw [List.cons_append, circuitOp_cons, circuitOp_cons, ih, mul_assoc] + +theorem circuit_comm_mag + (gs : List GateKind) (h : allPreserve preservesMagnetization gs = true) : + commMag (circuitOp gs) := by + induction gs with + | nil => + exact id_comm_mag + | cons g rest ih => + have parts := bool_and_parts h + exact mul_comm_mag _ _ (mag_of_bool g parts.1) (ih parts.2) + +theorem circuit_comm_spin + (gs : List GateKind) (h : allPreserve preservesSpinFlip gs = true) : + commSpin (circuitOp gs) := by + induction gs with + | nil => + exact id_comm_spin + | cons g rest ih => + have parts := bool_and_parts h + exact mul_comm_spin _ _ (spin_of_bool g parts.1) (ih parts.2) + +theorem admitted_commutes + (mode : Mode) (gs : List GateKind) (h : admitted mode gs = true) : + (mode = .ising → commMag (circuitOp gs)) ∧ + (mode = .spinFlip → commSpin (circuitOp gs)) := by + cases mode with + | ising => + refine And.intro ?_ ?_ + · intro _ + exact circuit_comm_mag gs h + · intro contra + nomatch contra + | spinFlip => + refine And.intro ?_ ?_ + · intro contra + nomatch contra + · intro _ + exact circuit_comm_spin gs h + +theorem ising_ring_commutes : commMag (circuitOp isingRing4) := + circuit_comm_mag isingRing4 isingRing4_admitted + +theorem spin_flip_ring_commutes : commSpin (circuitOp spinFlip4) := + circuit_comm_spin spinFlip4 spinFlip4_admitted + +theorem transverse_circuit_not_ising (i : Nat) (rest : List GateKind) : + ¬ commMag (circuitOp (.transverseFlip i :: rest)) := + reject_not_absorbed_mag _ _ (flip_not_mag i) + +theorem loneZ_circuit_not_spin (i : Nat) (rest : List GateKind) : + ¬ commSpin (circuitOp (.loneZ i :: rest)) := + reject_not_absorbed_spin _ _ (z_not_spin i) + +theorem concatenated_ising + (xs ys : List GateKind) + (hx : allPreserve preservesMagnetization xs = true) + (hy : allPreserve preservesMagnetization ys = true) : + commMag (circuitOp (xs ++ ys)) := by + have hjoin : allPreserve preservesMagnetization (xs ++ ys) = true := by + rw [allPreserve_append, hx, hy] + rfl + exact circuit_comm_mag (xs ++ ys) hjoin + +theorem completeness_of_admitted + (mode : Mode) (gs : List GateKind) (h : admitted mode gs = true) : + (mode = .ising → commMag (circuitOp gs)) ∧ + (mode = .spinFlip → commSpin (circuitOp gs)) := + admitted_commutes mode gs h + +/-- The open matrix obligation, named so a later development can replace the axioms. -/ +def OperatorCommutation : Prop := + ∀ g : GateKind, preservesMagnetization g = true → commMag (gateOp g) + +theorem operator_commutation_from_bridge : OperatorCommutation := + mag_of_bool + +end CPSC diff --git a/lean/lake-manifest.json b/lean/lake-manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..e48b0772606b3f42306687382cf176b23560323d --- /dev/null +++ b/lean/lake-manifest.json @@ -0,0 +1,5 @@ +{"version": "1.1.0", + "packagesDir": ".lake/packages", + "packages": [], + "name": "CPSC", + "lakeDir": ".lake"} diff --git a/lean/lakefile.toml b/lean/lakefile.toml new file mode 100644 index 0000000000000000000000000000000000000000..78084b978f6e518cf3f8160b4d44009b86242118 --- /dev/null +++ b/lean/lakefile.toml @@ -0,0 +1,16 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, or, at your +# option, the GNU Affero General Public License, Version 3 only. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +name = "CPSC" +version = "0.1.0" +defaultTargets = ["CPSC"] + +[[lean_lib]] +name = "CPSC" diff --git a/lean/lean-toolchain b/lean/lean-toolchain new file mode 100644 index 0000000000000000000000000000000000000000..6ac6d4c4ce9f2e1f08f2ca97b60a31cc39de23ef --- /dev/null +++ b/lean/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.22.0 diff --git a/paper/ABSTRACT.md b/paper/ABSTRACT.md new file mode 100644 index 0000000000000000000000000000000000000000..e6e0914850b8bea35dad02d0d7ac1e790ac92ba4 --- /dev/null +++ b/paper/ABSTRACT.md @@ -0,0 +1,10 @@ + + +# Abstract + +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ᵢ. + +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. diff --git a/paper/PAPER.md b/paper/PAPER.md new file mode 100644 index 0000000000000000000000000000000000000000..7f389d4a8d4249d162029716a4192f721707e616 --- /dev/null +++ b/paper/PAPER.md @@ -0,0 +1,115 @@ + + +# Conservation-preserving compilation for a 1+1D lattice shadow of φ⁴ + +Ahmad Ali Parr +7 October 2026 + +## Abstract + +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. + +## 1. Introduction + +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. + +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. + +The target is not continuum φ⁴. It is the periodic transverse-field Ising model on N qubits, + +$$ +H = -J \sum_{i=0}^{N-1} Z_i Z_{i+1} + \lambda \sum_{i=0}^{N-1} X_i . +$$ + +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. + +## 2. Which operator is actually conserved + +The draft listed Π_Q = ∑ᵢ Zᵢ as a symmetry of the interacting theory. That is false once λ ≠ 0. + +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 + +$$ +\Pi_X = \prod_{i=0}^{N-1} X_i . +$$ + +ZZ contributes two minus signs when conjugated by Π_X, so it commutes. Each X commutes with Π_X. A lone Z anticommutes with Π_X. + +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. + +The compiler therefore has two modes. + +| Mode | When | Admitted | Rejected | +| --- | --- | --- | --- | +| Ising | λ = 0 | ZZ ring, lone Z | single X | +| Spin-flip | λ ≠ 0 | ZZ ring, full X orbit | lone Z | + +A schedule must also be a union of cyclic images. One bond is not a translation-invariant interaction. The ring is. + +## 3. Compiler + +`compile_scattering` is the PCSS path. + +1. Choose the mode from λ. +2. Admit gates or raise `CompilationRejected`. +3. Reject a support that is not closed under the cyclic shift. +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. +5. Bind a certificate to the admitted list, the mode, and the Trotter parameters. + +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. + +There is no post-selection step. Leakage is measured only as a check that the emitted product stayed in the sector. + +## 4. Certificate + +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 + +```lean +def emittedHash : String := "" +def emittedGates : List GateKind := [.zz 0 1, .zz 1 2, .zz 2 3, .zz 3 0] +theorem emitted_admitted : allPreserve preservesMagnetization emittedGates = true := rfl +``` + +`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`. + +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. + +## 5. Q# emission + +`qsharp/ConservationPreservingScattering.qs` is the hardware image of the admitted factors, not a second analyzer. + +- `ApplyZZRing` is the bond orbit. +- `ApplyTransverseField` is the full X orbit, used only by the spin-flip schedule. +- `IsingEvolution` and `SpinFlipEvolution` are the Trotter loops. +- `RequireAdmitted` fails the operation if the host passes a schedule the analyzer refused. +- `MeasureZParity` is a diagnostic. It is not the compiler. + +## 6. Checks + +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. + +- Ising ring, seed `0b0011`, λ = 0. Z-parity leakage `0`. The certificate matches the ring and does not match the ring with `X[0]` substituted. +- Ising ring plus `X[2]` is rejected at compile time: odd X support changes Z-parity. +- A single bond is rejected: the translation orbit is open. +- Spin-flip schedule, seed `0b0001`, λ = 0.5. ∏ X leakage `0` after clamping roundoff at `3.6×10⁻¹⁵`. +- The same schedule plus `Z[1]` is rejected: a lone Z anticommutes with ∏ X. +- The dropped quartic at g = 1 is `N g / 24 = 0.1667` on the identity. + +These checks do not include device noise. Ideal leakage zero does not survive decoherence. The certificate does not claim otherwise. + +## 7. What is not proved + +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. + +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. + +## 8. Related work + +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. + +## License + +Dual strict copyleft: `LicenseRef-CPSC-ESCL-1.0` or `AGPL-3.0-only`. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..7da8c43923115ead46166e7662bbf018d6b18391 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, or, at your +# option, the GNU Affero General Public License, Version 3 only. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +[project] +name = "cpsc-qft" +version = "0.3.0" +description = "Conservation-Preserving Scattering Compiler research scaffold" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only" } +authors = [{ name = "Ahmad Ali Parr" }] +dependencies = ["numpy>=1.26"] + +[project.optional-dependencies] +dev = ["pytest>=8.0"] + +[project.scripts] +cpsc-demo = "cpsc.demo:main" + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/qsharp/Axioms.qs b/qsharp/Axioms.qs new file mode 100644 index 0000000000000000000000000000000000000000..254b513fd993f3734afb33b5b8af5833b0c42fb1 --- /dev/null +++ b/qsharp/Axioms.qs @@ -0,0 +1,72 @@ +// ******************************************************************************* +// Copyright (c) 2026 Ahmad Ali Parr and others. +// +// SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +// ******************************************************************************* + +namespace ConservationPreservingScattering { + open Microsoft.Quantum.Intrinsic; + + // Operator axioms, as operations the compiler is allowed to emit. + // Lean names are in comments. Q# has no proof kernel; admission is the Bool gate. + + newtype GateKind = ( + Tag : Int, + Q0 : Int, + Q1 : Int + ); + + function ZZ(q0 : Int, q1 : Int) : GateKind { + return GateKind(0, q0, q1); + } + + function ParityEvenPair(q0 : Int, q1 : Int) : GateKind { + return GateKind(1, q0, q1); + } + + function TransverseFlip(q : Int) : GateKind { + return GateKind(2, q, -1); + } + + function LoneZ(q : Int) : GateKind { + return GateKind(3, q, -1); + } + + // preservesMagnetization + function Axiom_PreservesMagnetization(g : GateKind) : Bool { + let tag = g::Tag; + return tag == 0 or tag == 3; + } + + // preservesSpinFlip + function Axiom_PreservesSpinFlip(g : GateKind) : Bool { + let tag = g::Tag; + return tag == 0 or tag == 1 or tag == 2; + } + + // Mode 0 is Ising, mode 1 is spin-flip. Mirrors `preserves`. + function Axiom_Preserves(mode : Int, g : GateKind) : Bool { + return mode == 0 ? Axiom_PreservesMagnetization(g) | Axiom_PreservesSpinFlip(g); + } + + // zz_mag / zz_spin: a bond is legal in both modes. + operation Axiom_ApplyZZ(left : Qubit, right : Qubit, angle : Double) : Unit is Adj + Ctl { + Rzz(2.0 * angle, left, right); + } + + // flip_spin: a transverse flip is legal only after the spin-flip axiom accepts it. + operation Axiom_ApplyFlip(target : Qubit, angle : Double) : Unit is Adj + Ctl { + Rx(2.0 * angle, target); + } + + // z_mag: a lone Z is legal only in Ising mode. + operation Axiom_ApplyZ(target : Qubit, angle : Double) : Unit is Adj + Ctl { + Rz(2.0 * angle, target); + } + + // pair_spin: two flips, the parity-even pair. + operation Axiom_ApplyPair(left : Qubit, right : Qubit, angle : Double) : Unit is Adj + Ctl { + Rx(2.0 * angle, left); + Rx(2.0 * angle, right); + } +} diff --git a/qsharp/ConservationPreservingScattering.qs b/qsharp/ConservationPreservingScattering.qs new file mode 100644 index 0000000000000000000000000000000000000000..b68e2876262b863ca7827c59707f3ed5af0f38ce --- /dev/null +++ b/qsharp/ConservationPreservingScattering.qs @@ -0,0 +1,104 @@ +// ******************************************************************************* +// Copyright (c) 2026 Ahmad Ali Parr and others. +// +// This program and the accompanying materials are made available under the +// terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +// available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +// GNU Affero General Public License, Version 3 only, which is available in +// LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +// classpath exception and no permissive relicensing. +// +// SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +// ******************************************************************************* + +namespace ConservationPreservingScattering { + open Microsoft.Quantum.Intrinsic; + open Microsoft.Quantum.Canon; + open Microsoft.Quantum.Arrays; + open Microsoft.Quantum.Measurement; + open Microsoft.Quantum.Convert; + + /// Ising bond orbit. Calls the ZZ axiom on every cyclic image. + operation ApplyZZRing(register : Qubit[], angle : Double) : Unit is Adj + Ctl { + let n = Length(register); + for i in 0 .. n - 1 { + Axiom_ApplyZZ(register[i], register[(i + 1) % n], angle); + } + } + + /// Full transverse orbit. Legal only as a spin-flip schedule, not as one site. + operation ApplyTransverseField(register : Qubit[], angle : Double) : Unit is Adj + Ctl { + for q in register { + Axiom_ApplyFlip(q, angle); + } + } + + operation RequireAdmitted(admitted : Bool, reason : String) : Unit { + if not admitted { + fail $"CPSC rejected the schedule: {reason}"; + } + } + + /// λ = 0. Bonds only. Magnetization is the active invariant. + operation IsingEvolution(register : Qubit[], steps : Int, dt : Double, j : Double) : Unit is Adj + Ctl { + let schedule = Lemma_BondOrbit(Length(register)); + RequireAdmitted(Lemma_AllAdmitted(0, schedule), "ising ring"); + for _ in 1 .. steps { + Lemma_Interpret(register, 0, schedule, j * dt); + } + } + + /// λ ≠ 0. Bond orbit plus transverse orbit. ∏ X is the active invariant. + operation SpinFlipEvolution( + register : Qubit[], + steps : Int, + dt : Double, + j : Double, + lam : Double + ) : Unit is Adj + Ctl { + let n = Length(register); + let bonds = Lemma_BondOrbit(n); + let field = Lemma_TransverseOrbit(n); + RequireAdmitted(Lemma_AllAdmitted(1, bonds + field), "spin-flip orbit"); + for _ in 1 .. steps { + Lemma_Interpret(register, 1, bonds, j * dt); + Lemma_Interpret(register, 1, field, lam * dt); + } + } + + /// Refused schedule. Present so a host can see the lemma fail closed. + operation RejectedIsingFlip(register : Qubit[], site : Int, angle : Double) : Unit is Adj + Ctl { + let bad = [TransverseFlip(site)]; + RequireAdmitted(Lemma_AllAdmitted(0, bad), "single X breaks Ising magnetization"); + Lemma_Interpret(register, 0, bad, angle); + } + + operation RejectedSpinFlipZ(register : Qubit[], site : Int, angle : Double) : Unit is Adj + Ctl { + let bad = [LoneZ(site)]; + RequireAdmitted(Lemma_AllAdmitted(1, bad), "lone Z breaks prod X"); + Lemma_Interpret(register, 1, bad, angle); + } + + /// Diagnostic, not a compiler filter. + operation MeasureZParity(register : Qubit[]) : Int { + mutable parity = 0; + for q in register { + if M(q) == One { + set parity = (parity + 1) % 2; + } + } + return parity; + } + + /// Diagnostic for the spin-flip sector: measure every qubit in X and return the parity. + operation MeasureXParity(register : Qubit[]) : Int { + mutable parity = 0; + for q in register { + H(q); + if M(q) == One { + set parity = (parity + 1) % 2; + } + } + return parity; + } +} diff --git a/qsharp/Lemmas.qs b/qsharp/Lemmas.qs new file mode 100644 index 0000000000000000000000000000000000000000..5632c3cecd9791313297d8b4d60fe8f529f6470a --- /dev/null +++ b/qsharp/Lemmas.qs @@ -0,0 +1,116 @@ +// ******************************************************************************* +// Copyright (c) 2026 Ahmad Ali Parr and others. +// +// SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +// ******************************************************************************* + +namespace ConservationPreservingScattering { + open Microsoft.Quantum.Intrinsic; + open Microsoft.Quantum.Arrays; + open Microsoft.Quantum.Canon; + + // Lemmas as total functions and interpreting operations. + // Each one is the Q# image of a theorem in lean/CPSC/Lemmas.lean. + + function Lemma_NilAdmitted() : Bool { + return true; + } + + function Lemma_ConsAdmitted(mode : Int, head : GateKind, tailOk : Bool) : Bool { + return Axiom_Preserves(mode, head) and tailOk; + } + + function Lemma_AllAdmitted(mode : Int, schedule : GateKind[]) : Bool { + mutable ok = true; + for g in schedule { + if not Axiom_Preserves(mode, g) { + set ok = false; + } + } + return ok; + } + + function Lemma_AppendAdmitted(left : Bool, right : Bool) : Bool { + return left and right; + } + + function Lemma_TransverseBreaksIsing(q : Int) : Bool { + return not Axiom_PreservesMagnetization(TransverseFlip(q)); + } + + function Lemma_LoneZBreaksSpinFlip(q : Int) : Bool { + return not Axiom_PreservesSpinFlip(LoneZ(q)); + } + + operation Lemma_ApplyAdmitted( + register : Qubit[], + g : GateKind, + angle : Double + ) : Unit is Adj + Ctl { + let n = Length(register); + let q0 = g::Q0; + let q1 = g::Q1; + if q0 < 0 or q0 >= n or (q1 >= n) { + fail "CPSC lemma: qubit outside the register"; + } + if g::Tag == 0 { + if q1 < 0 { + fail "CPSC lemma: ZZ is missing its second qubit"; + } + Axiom_ApplyZZ(register[q0], register[q1], angle); + } elif g::Tag == 1 { + if q1 < 0 { + fail "CPSC lemma: pair is missing its second qubit"; + } + Axiom_ApplyPair(register[q0], register[q1], angle); + } elif g::Tag == 2 { + Axiom_ApplyFlip(register[q0], angle); + } elif g::Tag == 3 { + Axiom_ApplyZ(register[q0], angle); + } else { + fail "CPSC lemma: unknown gate tag"; + } + } + + // circuit_comm_*: interpret only an admitted schedule. A rejected head fails closed. + operation Lemma_Interpret( + register : Qubit[], + mode : Int, + schedule : GateKind[], + angle : Double + ) : Unit is Adj + Ctl { + if not Lemma_AllAdmitted(mode, schedule) { + fail "CPSC rejected the schedule: a factor breaks the active invariant"; + } + for g in schedule { + Lemma_ApplyAdmitted(register, g, angle); + } + } + + function Lemma_BondOrbit(n : Int) : GateKind[] { + mutable gates = []; + for i in 0 .. n - 1 { + set gates += [ZZ(i, (i + 1) % n)]; + } + return gates; + } + + function Lemma_TransverseOrbit(n : Int) : GateKind[] { + mutable gates = []; + for i in 0 .. n - 1 { + set gates += [TransverseFlip(i)]; + } + return gates; + } + + operation Lemma_IsingCircuit(register : Qubit[], angle : Double) : Unit is Adj + Ctl { + let schedule = Lemma_BondOrbit(Length(register)); + Lemma_Interpret(register, 0, schedule, angle); + } + + operation Lemma_SpinFlipCircuit(register : Qubit[], angle : Double) : Unit is Adj + Ctl { + let n = Length(register); + let schedule = Lemma_BondOrbit(n) + Lemma_TransverseOrbit(n); + Lemma_Interpret(register, 1, schedule, angle); + } +} diff --git a/src/cpsc/__init__.py b/src/cpsc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8e2af1454c10bdf6f16f727cdce6e8edfe39b4a9 --- /dev/null +++ b/src/cpsc/__init__.py @@ -0,0 +1,31 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +"""Conservation-Preserving Scattering Compiler, research prototype.""" + +from cpsc.analyzer import Gate, Rejection, SymmetryAnalyzer, SymmetryMode +from cpsc.certificate import Certificate +from cpsc.channel import evolve_in_sector, sector_basis +from cpsc.compiler import compile_scattering + +__all__ = [ + "Certificate", + "Gate", + "Rejection", + "SymmetryAnalyzer", + "SymmetryMode", + "compile_scattering", + "evolve_in_sector", + "sector_basis", +] +__version__ = "0.3.0" diff --git a/src/cpsc/analyzer.py b/src/cpsc/analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..ef3bd60ad30c9471467bc26e190e00aa1543e42b --- /dev/null +++ b/src/cpsc/analyzer.py @@ -0,0 +1,120 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +"""Compilation-time symmetry analyzer. + +A gate is admitted only if it preserves the active invariant. This is a +rejection rule, not a post-selection filter. + +Ising mode (lambda = 0) conserves magnetization and Z-parity. A single X is +forbidden. Spin-flip mode (lambda != 0) conserves prod X, the actual symmetry +of the transverse-field Ising model. A single Z is forbidden; a single X is not. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +class SymmetryMode(str, Enum): + ISING = "ising" + SPIN_FLIP = "spin_flip" + + +@dataclass(frozen=True) +class Gate: + name: str + qubits: tuple[int, ...] + kind: str + + +@dataclass(frozen=True) +class Rejection: + gate: Gate + reason: str + + +class CompilationRejected(Exception): + """Raised when the analyzer refuses a circuit at compile time.""" + + +class SymmetryAnalyzer: + def __init__( + self, + n: int, + *, + conserve_magnetization: bool = False, + mode: SymmetryMode | str = SymmetryMode.ISING, + ) -> None: + if n < 1: + raise ValueError("n must be positive") + self.n = n + self.mode = SymmetryMode(mode) + self.conserve_magnetization = conserve_magnetization or self.mode is SymmetryMode.ISING + + def check(self, gates: list[Gate]) -> list[Rejection]: + rejected: list[Rejection] = [] + for gate in gates: + reason = self._reject_reason(gate) + if reason is not None: + rejected.append(Rejection(gate, reason)) + return rejected + + def admit(self, gates: list[Gate]) -> list[Gate]: + rejected = self.check(gates) + if rejected: + detail = "; ".join(f"{r.gate.name}: {r.reason}" for r in rejected) + raise CompilationRejected(detail) + return list(gates) + + def _reject_reason(self, gate: Gate) -> str | None: + for q in gate.qubits: + if not 0 <= q < self.n: + return f"qubit {q} outside lattice of size {self.n}" + if self.mode is SymmetryMode.ISING: + if gate.kind in {"illegal", "transverse"}: + return "odd X/Y support changes Z-parity; forbidden sector" + if gate.kind in {"parity_even", "magnetization", "phase"}: + return None + else: + if gate.kind == "phase": + return "lone Z anticommutes with prod X; forbidden spin-flip sector" + if gate.kind in {"illegal", "transverse", "parity_even", "magnetization"}: + return None + return f"unknown gate kind {gate.kind}" + + +def x(q: int) -> Gate: + return Gate(f"X[{q}]", (q,), "illegal") + + +def z(q: int) -> Gate: + return Gate(f"Z[{q}]", (q,), "phase") + + +def zz(q: int, r: int) -> Gate: + return Gate(f"ZZ[{q},{r}]", (q, r), "magnetization") + + +def rx_pair(q: int, r: int) -> Gate: + """Two transverse flips. Parity-even under prod Z, magnetization-changing.""" + return Gate(f"RX[{q}]*RX[{r}]", (q, r), "parity_even") + + +def ising_ring(n: int) -> list[Gate]: + return [zz(i, (i + 1) % n) for i in range(n)] + + +def transverse_field(n: int) -> list[Gate]: + return [x(i) for i in range(n)] diff --git a/src/cpsc/certificate.py b/src/cpsc/certificate.py new file mode 100644 index 0000000000000000000000000000000000000000..f58bd0173929375f6de1040daa7d30ed7f622cfc --- /dev/null +++ b/src/cpsc/certificate.py @@ -0,0 +1,104 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# *******************************************************************************"""Certificate binding. The digest is the witness; a gate edit breaks it.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass + +from cpsc.analyzer import Gate + + +@dataclass(frozen=True) +class Certificate: + digest: str + mode: str + n: int + lean: str + payload: str + lam: float + j: float + time: float + steps: int + + def matches(self, gates: list[Gate]) -> bool: + return self.digest == digest_for(gates, mode=self.mode, n=self.n, lam=self.lam, j=self.j, time=self.time, steps=self.steps) + + +def digest_for(gates: list[Gate], *, mode: str, n: int, lam: float, j: float, time: float, steps: int) -> str: + payload = json.dumps( + { + "gates": [[g.name, list(g.qubits), g.kind] for g in gates], + "mode": mode, + "n": n, + "lambda": lam, + "j": j, + "time": time, + "steps": steps, + }, + separators=(",", ":"), + ) + return hashlib.sha256(payload.encode()).hexdigest() + + +def _lean_kind(gate: Gate) -> str: + if gate.kind == "magnetization": + a, b = gate.qubits + return f".zz {a} {b}" + if gate.kind == "phase": + return f".loneZ {gate.qubits[0]}" + if gate.kind in {"illegal", "transverse"}: + return f".transverseFlip {gate.qubits[0]}" + if gate.kind == "parity_even": + a, b = gate.qubits + return f".parityEvenPair {a} {b}" + raise ValueError(gate.kind) + + +def bind_certificate( + gates: list[Gate], + *, + mode: str, + n: int, + lam: float, + j: float, + time: float, + steps: int, +) -> Certificate: + payload = json.dumps( + { + "gates": [[g.name, list(g.qubits), g.kind] for g in gates], + "mode": mode, + "n": n, + "lambda": lam, + "j": j, + "time": time, + "steps": steps, + }, + separators=(",", ":"), + ) + digest = digest_for(gates, mode=mode, n=n, lam=lam, j=j, time=time, steps=steps) + kinds = ", ".join(_lean_kind(g) for g in gates) + predicate = "preservesMagnetization" if mode == "ising" else "preservesSpinFlip" + lean = ( + "-- emitted by cpsc.certificate; do not edit by hand\n" + f"def emittedHash : String := \"{digest}\"\n" + f"def emittedGates : List GateKind := [{kinds}]\n" + f"theorem emitted_admitted : allPreserve {predicate} emittedGates = true := by\n" + " rfl\n" + ) + return Certificate( + digest=digest, mode=mode, n=n, lean=lean, payload=payload, + lam=lam, j=j, time=time, steps=steps, + ) diff --git a/src/cpsc/channel.py b/src/cpsc/channel.py new file mode 100644 index 0000000000000000000000000000000000000000..97a3db1d4e2fd1fb12bd612c6adc13a39d9a15a7 --- /dev/null +++ b/src/cpsc/channel.py @@ -0,0 +1,172 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +"""Channel-space evolution: exponentiate H only inside the allowed sector.""" + +from __future__ import annotations + +import numpy as np + +from cpsc.hamiltonian import magnetization_of, parity_of, transverse_ising + + +def sector_basis( + n: int, + *, + parity: int, + magnetization: int | None = None, +) -> list[int]: + basis: list[int] = [] + for b in range(1 << n): + if parity_of(b, n) != parity: + continue + if magnetization is not None and magnetization_of(b, n) != magnetization: + continue + basis.append(b) + return basis + + +def evolve_in_sector( + n: int, + initial_basis: int, + *, + time: float, + j: float = 1.0, + lam: float = 0.0, + conserve_magnetization: bool = False, +) -> np.ndarray: + """Return the full-space statevector of the sector-restricted evolution. + + Z-parity completeness holds only at lambda = 0. For lambda != 0 the + symmetry is prod X. Magnetization pruning is refused unless lambda == 0. + """ + if not 0 <= initial_basis < (1 << n): + raise ValueError("initial basis out of range") + parity = parity_of(initial_basis, n) + mag = magnetization_of(initial_basis, n) if conserve_magnetization else None + if conserve_magnetization and lam != 0.0: + raise ValueError( + "magnetization is not a symmetry of the transverse field; " + "refusing a completeness-breaking projection" + ) + allowed = sector_basis(n, parity=parity, magnetization=mag) + h = transverse_ising(n, j=j, lam=lam) + index = {b: i for i, b in enumerate(allowed)} + block = np.zeros((len(allowed), len(allowed)), dtype=np.complex128) + for a in allowed: + for b in allowed: + block[index[a], index[b]] = h[a, b] + evals, evecs = np.linalg.eigh(block) + phase = np.exp(-1j * evals * time) + u = (evecs * phase) @ evecs.conj().T + psi0 = np.zeros(len(allowed), dtype=np.complex128) + psi0[index[initial_basis]] = 1.0 + evolved = u @ psi0 + full = np.zeros(1 << n, dtype=np.complex128) + for b, amp in zip(allowed, evolved): + full[b] = amp + return full + + +def leakage(state: np.ndarray, n: int, parity: int) -> float: + """Probability mass on the wrong Z-parity sector.""" + wrong = 0.0 + for b, amp in enumerate(state): + if parity_of(b, n) != parity: + wrong += float(np.abs(amp) ** 2) + return wrong + + +def apply_prod_x(state: np.ndarray, n: int) -> np.ndarray: + out = np.zeros_like(state) + mask = (1 << n) - 1 + for basis, amp in enumerate(state): + out[basis ^ mask] += amp + return out + + +def even_cat(n: int, basis: int) -> np.ndarray: + """Normalized +1 eigenstate of prod X built from a computational seed.""" + state = np.zeros(1 << n, dtype=np.complex128) + state[basis] = 1.0 / np.sqrt(2.0) + state[basis ^ ((1 << n) - 1)] += 1.0 / np.sqrt(2.0) + return state + + +def z_parity_leakage(state: np.ndarray, n: int, parity: int) -> float: + return leakage(state, n, parity) + + +def x_parity_leakage(state: np.ndarray, n: int, seed_basis: int) -> float: + """Mass outside the even prod-X sector.""" + del seed_basis + flipped = apply_prod_x(state, n) + projected = 0.5 * (state + flipped) + return float(max(0.0, 1.0 - np.vdot(projected, projected).real)) + + +def _apply_rx(state: np.ndarray, n: int, qubit: int, angle: float) -> np.ndarray: + cos = np.cos(angle) + sin = np.sin(angle) + out = np.zeros_like(state) + bit = 1 << qubit + for basis, amp in enumerate(state): + if amp == 0: + continue + out[basis] += cos * amp + out[basis ^ bit] += -1j * sin * amp + return out + + +def _apply_rzz(state: np.ndarray, n: int, q1: int, q2: int, angle: float) -> np.ndarray: + out = state.copy() + for basis in range(state.shape[0]): + z1 = 1.0 if ((basis >> q1) & 1) == 0 else -1.0 + z2 = 1.0 if ((basis >> q2) & 1) == 0 else -1.0 + out[basis] *= np.exp(1j * angle * z1 * z2) + return out + + +def evolve_trotter( + n: int, + initial_basis: int, + gates, + *, + time: float, + steps: int, + j: float, + lam: float, + conserve_magnetization: bool, +) -> np.ndarray: + """First-order Trotter product of the admitted local terms.""" + if conserve_magnetization: + state = np.zeros(1 << n, dtype=np.complex128) + state[initial_basis] = 1.0 + else: + state = even_cat(n, initial_basis) + dt = time / steps + for _ in range(steps): + for gate in gates: + if gate.kind == "magnetization": + a, b = gate.qubits + state = _apply_rzz(state, n, a, b, j * dt) + elif gate.kind in {"illegal", "transverse"}: + state = _apply_rx(state, n, gate.qubits[0], lam * dt) + elif gate.kind == "parity_even": + for q in gate.qubits: + state = _apply_rx(state, n, q, lam * dt) + elif gate.kind == "phase": + continue + else: + raise ValueError(f"no evolution rule for {gate.kind}") + return state diff --git a/src/cpsc/compiler.py b/src/cpsc/compiler.py new file mode 100644 index 0000000000000000000000000000000000000000..9cc8e4fb464899b13486934c1e6708d2f9e72466 --- /dev/null +++ b/src/cpsc/compiler.py @@ -0,0 +1,93 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +"""Proof-carrying scattering synthesis. + +The compiler admits a gate list, checks the translation orbit, exponentiates +only the admitted local terms, and binds a certificate to the admitted list. +""" + +from __future__ import annotations + +import numpy as np + +from cpsc.analyzer import CompilationRejected, Gate, SymmetryAnalyzer, SymmetryMode +from cpsc.certificate import Certificate, bind_certificate +from cpsc.channel import evolve_trotter, x_parity_leakage, z_parity_leakage +from cpsc.hamiltonian import magnetization_of, parity_of + + +def translation_orbit_closed(gates: list[Gate], n: int) -> str | None: + """A translation-invariant schedule must include every cyclic image. + + ZZ[i, i+1] is closed only if every bond appears. A lone transverse flip + is closed only if every site appears with the same kind. Global gates + with empty support are already invariant. + """ + names = {(g.kind, g.qubits) for g in gates} + for kind, qubits in list(names): + if not qubits: + continue + for shift in range(n): + image = tuple((q + shift) % n for q in qubits) + if (kind, image) not in names and (kind, image[::-1]) not in names: + return ( + f"{kind}{qubits} is not closed under translation; " + f"missing image {image}" + ) + return None + + +def compile_scattering( + n: int, + initial_basis: int, + gates: list[Gate], + *, + time: float, + steps: int = 8, + j: float = 1.0, + lam: float = 0.0, + require_translation: bool = True, +) -> dict: + if steps < 1: + raise ValueError("steps must be positive") + mode = SymmetryMode.ISING if lam == 0.0 else SymmetryMode.SPIN_FLIP + analyzer = SymmetryAnalyzer(n, mode=mode) + admitted = analyzer.admit(gates) + if require_translation: + reason = translation_orbit_closed(admitted, n) + if reason is not None: + raise CompilationRejected(reason) + conserve_mag = mode is SymmetryMode.ISING + state = evolve_trotter( + n, + initial_basis, + admitted, + time=time, + steps=steps, + j=j, + lam=lam, + conserve_magnetization=conserve_mag, + ) + certificate = bind_certificate(admitted, mode=mode.value, n=n, lam=lam, j=j, time=time, steps=steps) + z_leak = z_parity_leakage(state, n, parity_of(initial_basis, n)) + x_leak = x_parity_leakage(state, n, initial_basis) + return { + "mode": mode.value, + "admitted": admitted, + "state": state, + "certificate": certificate, + "z_parity_leakage": z_leak, + "x_parity_leakage": x_leak, + "magnetization": magnetization_of(initial_basis, n) if conserve_mag else None, + } diff --git a/src/cpsc/demo.py b/src/cpsc/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..f13b0897ed9ac5b0365a043c165326a867d70023 --- /dev/null +++ b/src/cpsc/demo.py @@ -0,0 +1,43 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +"""Command-line demonstration of both symmetry modes.""" + +from __future__ import annotations + +from cpsc.analyzer import CompilationRejected, ising_ring, transverse_field, x, z +from cpsc.compiler import compile_scattering +from cpsc.hamiltonian import x4_identity_shift + + +def main() -> None: + n = 4 + ising = compile_scattering(n, 0b0011, ising_ring(n), time=0.4, steps=8, lam=0.0) + print("ising mode", ising["mode"], "z-leakage", f"{ising['z_parity_leakage']:.3e}") + print("certificate", ising["certificate"].digest[:16]) + try: + compile_scattering(n, 0b0011, ising_ring(n) + [x(2)], time=0.2, lam=0.0) + except CompilationRejected as exc: + print("ising rejected", exc) + gates = ising_ring(n) + transverse_field(n) + flip = compile_scattering(n, 0b0001, gates, time=0.4, steps=8, lam=0.5) + print("spin-flip x-leakage", f"{flip['x_parity_leakage']:.3e}") + try: + compile_scattering(n, 0b0001, gates + [z(1)], time=0.2, lam=0.5) + except CompilationRejected as exc: + print("spin-flip rejected", exc) + print("dropped X^4 shift", x4_identity_shift(n, 1.0)) + + +if __name__ == "__main__": + main() diff --git a/src/cpsc/hamiltonian.py b/src/cpsc/hamiltonian.py new file mode 100644 index 0000000000000000000000000000000000000000..8600dfe927da4886ca2b31e61511c50cbebc7c8a --- /dev/null +++ b/src/cpsc/hamiltonian.py @@ -0,0 +1,60 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +"""Transverse-field Ising stand-in for the 1+1D lattice specification. + +The draft quartic (g/4!) sum_i X_i^4 is identically (g/24) N I, because X^2 = I. +It is reported and then omitted from the matrix. +""" + +from __future__ import annotations + +import numpy as np + + +def x4_identity_shift(n: int, g: float) -> float: + """Constant added by sum_i X_i^4 / 24. Does not change eigenvectors.""" + return (g / 24.0) * n + + +def transverse_ising(n: int, j: float = 1.0, lam: float = 0.5) -> np.ndarray: + """Periodic H = -J sum Z_i Z_{i+1} + lam sum X_i, shape (2^n, 2^n).""" + if n < 2 or n > 10: + raise ValueError("n must be in 2..10 for the dense prototype") + dim = 1 << n + h = np.zeros((dim, dim), dtype=np.complex128) + for i in range(n): + # X_i on qubit i (little-endian bit i). + for basis in range(dim): + flipped = basis ^ (1 << i) + h[flipped, basis] += lam + # Z_i Z_{i+1} + nxt = (i + 1) % n + for basis in range(dim): + zi = 1.0 if ((basis >> i) & 1) == 0 else -1.0 + zj = 1.0 if ((basis >> nxt) & 1) == 0 else -1.0 + h[basis, basis] += -j * zi * zj + return h + + +def parity_of(basis: int, n: int) -> int: + """Z-parity as the popcount mod 2. P_Z |b> = (-1)^{popcount} |b>.""" + return (basis.bit_count() if hasattr(basis, "bit_count") else bin(basis).count("1")) & 1 + + +def magnetization_of(basis: int, n: int) -> int: + """Sum of Z eigenvalues. |0> contributes +1, |1> contributes -1.""" + mag = 0 + for i in range(n): + mag += 1 if ((basis >> i) & 1) == 0 else -1 + return mag diff --git a/tests/test_adversarial.py b/tests/test_adversarial.py new file mode 100644 index 0000000000000000000000000000000000000000..fcdcb83e437afc1433ab652932a9069ea649c274 --- /dev/null +++ b/tests/test_adversarial.py @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +import pytest + +from cpsc.analyzer import CompilationRejected, SymmetryAnalyzer, rx_pair, x, zz + + +def test_forbidden_bit_flip_rejected_at_compile_time(): + analyzer = SymmetryAnalyzer(4) + rejected = analyzer.check([zz(0, 1), x(2), zz(2, 3)]) + assert len(rejected) == 1 + assert rejected[0].gate.name == "X[2]" + assert "parity" in rejected[0].reason + + +def test_legal_circuit_admitted(): + analyzer = SymmetryAnalyzer(4, conserve_magnetization=True) + gates = analyzer.admit([zz(0, 1), zz(2, 3), rx_pair(1, 2)]) + assert len(gates) == 3 + + +def test_admit_raises_on_adversarial_edit(): + analyzer = SymmetryAnalyzer(6) + with pytest.raises(CompilationRejected): + analyzer.admit([zz(0, 1), x(5)]) diff --git a/tests/test_certificate.py b/tests/test_certificate.py new file mode 100644 index 0000000000000000000000000000000000000000..a7b6c465577d4ea8fde181d336710b1c8eb03f03 --- /dev/null +++ b/tests/test_certificate.py @@ -0,0 +1,27 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +from cpsc.analyzer import ising_ring, x +from cpsc.compiler import compile_scattering + + +def test_manual_edit_breaks_certificate(): + gates = ising_ring(4) + compiled = compile_scattering(4, 0b0011, gates, time=0.3, steps=4, lam=0.0) + cert = compiled["certificate"] + assert cert.matches(gates) + edited = gates[:-1] + [x(0)] + assert not cert.matches(edited) + assert "emittedHash" in cert.lean + assert cert.digest in cert.lean + assert "native_decide" in cert.lean or "rfl" in cert.lean diff --git a/tests/test_compiler.py b/tests/test_compiler.py new file mode 100644 index 0000000000000000000000000000000000000000..b7126ca6dd019135579d38f3dc2f05f0b1cb32ec --- /dev/null +++ b/tests/test_compiler.py @@ -0,0 +1,52 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +import numpy as np +import pytest + +from cpsc.analyzer import CompilationRejected, SymmetryAnalyzer, SymmetryMode, ising_ring, transverse_field, x, z, zz +from cpsc.compiler import compile_scattering + + +def test_ising_ring_compiles_and_stays_in_sector(): + n = 4 + compiled = compile_scattering(n, 0b0011, ising_ring(n), time=0.6, steps=6, lam=0.0) + assert compiled["mode"] == "ising" + assert compiled["z_parity_leakage"] < 1e-10 + assert abs(np.vdot(compiled["state"], compiled["state"]) - 1) < 1e-10 + assert compiled["certificate"].matches(ising_ring(n)) + + +def test_adversarial_x_rejected_in_ising_mode(): + with pytest.raises(CompilationRejected): + compile_scattering(4, 0b0011, ising_ring(4) + [x(1)], time=0.2, lam=0.0) + + +def test_spin_flip_rejects_lone_z_and_keeps_x_sector(): + n = 4 + gates = ising_ring(n) + transverse_field(n) + compiled = compile_scattering(n, 0b0001, gates, time=0.4, steps=10, lam=0.7) + assert compiled["mode"] == "spin_flip" + assert compiled["x_parity_leakage"] < 1e-9 + with pytest.raises(CompilationRejected): + compile_scattering(n, 0b0001, gates + [z(2)], time=0.2, lam=0.7) + + +def test_open_translation_orbit_rejected(): + with pytest.raises(CompilationRejected): + compile_scattering(4, 0b0011, [zz(0, 1)], time=0.2, lam=0.0) + + +def test_spin_flip_analyzer_admits_transverse_field(): + admitted = SymmetryAnalyzer(3, mode=SymmetryMode.SPIN_FLIP).admit(transverse_field(3)) + assert len(admitted) == 3 diff --git a/tests/test_energy_shift.py b/tests/test_energy_shift.py new file mode 100644 index 0000000000000000000000000000000000000000..db1d0ea2bfa3eb225f0d28f1e928688cbce4c4a5 --- /dev/null +++ b/tests/test_energy_shift.py @@ -0,0 +1,53 @@ +# ******************************************************************************* +# Copyright (c) 2026 Ahmad Ali Parr and others. +# +# This program and the accompanying materials are made available under the +# terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is +# available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the +# GNU Affero General Public License, Version 3 only, which is available in +# LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no +# classpath exception and no permissive relicensing. +# +# SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only +# ******************************************************************************* + +import numpy as np +import pytest + +from cpsc.channel import evolve_in_sector, leakage +from cpsc.hamiltonian import magnetization_of, parity_of, transverse_ising, x4_identity_shift + + +def test_x4_is_proportional_to_identity(): + # The draft quartic does not open a new scattering channel. + assert x4_identity_shift(5, 1.2) == pytest.approx(5 * 1.2 / 24.0) + + +def test_sector_evolution_has_zero_parity_leakage(): + n = 4 + initial = 0b0101 + psi = evolve_in_sector(n, initial, time=0.7, j=1.0, lam=0.4) + assert leakage(psi, n, parity_of(initial, n)) < 1e-10 + assert abs(np.vdot(psi, psi) - 1) < 1e-10 + + +def test_magnetization_projection_refused_when_field_is_on(): + with pytest.raises(ValueError): + evolve_in_sector(3, 0b001, time=0.2, lam=0.3, conserve_magnetization=True) + + +def test_ising_limit_stays_in_weight_sector(): + n = 4 + initial = 0b0011 + psi = evolve_in_sector( + n, initial, time=0.5, j=1.0, lam=0.0, conserve_magnetization=True + ) + weight = magnetization_of(initial, n) + for basis, amp in enumerate(psi): + if abs(amp) > 1e-10: + assert magnetization_of(basis, n) == weight + + +def test_hamiltonian_is_hermitian(): + h = transverse_ising(3, j=0.8, lam=0.3) + assert np.allclose(h, h.conj().T)