├── .gitignore ├── LICENSE ├── README.md ├── monodromy ├── __init__.py ├── attic │ ├── deflate.py │ ├── inflate.py │ ├── lrs_decompose.py │ ├── ordered.py │ ├── sample.py │ └── unordered.py ├── backend │ ├── __init__.py │ ├── backend_abc.py │ └── lrs.py ├── coordinates.py ├── coverage.py ├── elimination.py ├── exceptions.py ├── haar.py ├── io │ ├── __init__.py │ ├── base.py │ └── lrcalc.py ├── polynomials.py ├── polytopes.py ├── render.py ├── static │ ├── __init__.py │ ├── examples.py │ ├── interference.py │ ├── matrices.py │ └── qlr_table.py ├── utilities.py └── volume.py ├── pyproject.toml ├── requirements-test.txt ├── requirements.txt ├── scripts ├── approx_gateset.py ├── demo.py ├── gateset.py ├── nuop.py ├── proof.py ├── qft.py ├── single_circuit.py └── xx_sequence.py ├── setup.py ├── test ├── test_coordinates.py ├── test_coverage.py ├── test_elimination.py ├── test_haar.py ├── test_polynomials.py ├── test_polytopes.py └── test_volume.py └── usage.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | .idea 4 | .DS_Store 5 | .python-version 6 | *.pyc 7 | .coverage 8 | .hypothesis 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `monodromy` 2 | 3 | Computations in the monodromy polytope for quantum gate sets 4 | 5 | ## Overview 6 | 7 | [Fixed-Depth Two-Qubit Circuits and the Monodromy Polytope](https://arxiv.org/abs/1904.10541) described a technique for determining whether a given two-qubit unitary can be written as a circuit with a prescribed sequence of two-qubit interactions, interleaved with arbitrary single-qubit unitaries. 8 | This python package is a computationally effective implementation of that technique. 9 | 10 | ## Installation 11 | 12 | 1. This package comes with a `requirements.txt` file. 13 | Begin by installing those requirements, using `pip -r requirements.txt`. 14 | 2. Install [`lrs`](http://cgm.cs.mcgill.ca/~avis/C/lrs.html). 15 | Typically, this means downloading the source, building it, and placing the generated executable somewhere in the search path for your python process. 16 | **NOTE:** We require version version ≥0.7.1b of `lrsgmp` (renamed to `lrs`). 17 | 3. *Optionally,* install [`lrcalc`](https://sites.math.rutgers.edu/~asbuch/lrcalc/). Typically, this means downloading the source, building it, then building the Cython bindings, and installing the resulting package. *This is not necessary: `lrcalc` is needed only for the curious user who wants to regenerate the contents of `qlr_table.py`.* 18 | 19 | ## Configuration 20 | 21 | If you're unable to put `lrs` in your search path, you can specify its location via the environment variable `LRS_PATH`. 22 | 23 | ## Usage 24 | 25 | Give it a whirl with `scripts/demo.py`. 26 | 27 | ## Notes 28 | 29 | We've designed the package around polytopes with rational coordinates. 30 | In practice this suits the use cases of quantum computer scientists fine, but it is easy to imagine use cases outside of this. 31 | `lrs` makes strong assumptions about the kind of arithmetic used, so if one were to want to process irrational polytopes, one would be obligated to move away from `lrs`. 32 | 33 | `lrs` is licensed under GPLv2, which makes it insuitable for inclusion in some projects. 34 | We may someday want to remove (or make optional) this dependency from this project. 35 | To this end, we have set up a `backend` which describes a contained and minimal set of calls we make of any computational library. 36 | -------------------------------------------------------------------------------- /monodromy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/__init__.py 3 | 4 | Top-level imports for `monodromy`. 5 | """ 6 | 7 | import monodromy.coverage 8 | import monodromy.polytopes 9 | 10 | from monodromy.backend.lrs import LRSBackend 11 | import monodromy.backend 12 | 13 | monodromy.backend.backend = LRSBackend() 14 | -------------------------------------------------------------------------------- /monodromy/attic/deflate.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/io/deflate.py 3 | 4 | Routines for deflating a coverage set for export. 5 | """ 6 | 7 | from dataclasses import asdict 8 | from typing import Dict, List 9 | 10 | from ..coordinates import monodromy_alcove_c2 11 | from ..coverage import CircuitPolytope, deduce_qlr_consequences 12 | from ..xx_decompose.precalculate import calculate_unordered_scipy_coverage_set 13 | from ..static import everything_polytope, identity_polytope 14 | 15 | 16 | def generate_deflated_coverage_data( 17 | operations: List[CircuitPolytope], 18 | chatty=True 19 | ) -> Dict: 20 | """ 21 | Generates the deflated data tables used to prime the `MonodromyZXDecomposer` 22 | compilation pass. Returns a dictionary of relevant tables. 23 | """ 24 | # Generate data for all possible combinations (with multiplicity) of 25 | # operations, stopping only when adding a gate does not improve coverage. 26 | buckets = [1] + [0] * (len(operations) - 1) 27 | coverage_set = { 28 | (0,) * len(operations): CircuitPolytope( 29 | cost=0., 30 | operations=[], 31 | convex_subpolytopes=identity_polytope.convex_subpolytopes 32 | ) 33 | } 34 | 35 | while True: 36 | if chatty: 37 | print("Working on " + 38 | ', '.join(str(b) + ' ' + o.operations[0] 39 | for b, o in zip(buckets, operations)) + 40 | ".") 41 | # find an antecedent CircuitPolytope (and edge) from coverage_set 42 | first_nonzero_index = next((i for i, j in enumerate(buckets) if j != 0), 43 | None) 44 | decremented_tuple = tuple(j if i != first_nonzero_index else j - 1 45 | for i, j in enumerate(buckets)) 46 | input_polytope = coverage_set[decremented_tuple] 47 | operation_polytope = operations[first_nonzero_index] 48 | 49 | # calculate CircuitPolytope for this bucket value 50 | output_polytope = deduce_qlr_consequences( 51 | target="c", 52 | a_polytope=input_polytope, 53 | b_polytope=operation_polytope, 54 | c_polytope=everything_polytope, 55 | ) 56 | output_polytope = CircuitPolytope( 57 | operations=sum([count * operation.operations 58 | for count, operation in zip(buckets, operations)], 59 | []), 60 | cost=0., 61 | convex_subpolytopes=output_polytope.convex_subpolytopes 62 | ) 63 | 64 | # stash this into coverage_set 65 | coverage_set[tuple(buckets)] = output_polytope 66 | 67 | # increment the bucket counters 68 | if not output_polytope.contains(monodromy_alcove_c2): 69 | buckets[0] += 1 70 | else: 71 | # if it has perfect coverage, roll over: 72 | # zero out the first nonzero bucket, increment the one after it 73 | buckets[first_nonzero_index] = 0 74 | if first_nonzero_index + 1 == len(buckets): 75 | # if rolling over overflows, break 76 | break 77 | else: 78 | buckets[first_nonzero_index + 1] += 1 79 | 80 | # also perform the scipy precalculation 81 | scipy_coverage_set = calculate_unordered_scipy_coverage_set( 82 | list(coverage_set.values()), operations, chatty=chatty 83 | ) 84 | 85 | return { 86 | "serialized_coverage_set": { 87 | k: asdict(v) 88 | for k, v in coverage_set.items() 89 | }, 90 | "serialized_scipy_coverage_set": [ 91 | asdict(x) for x in scipy_coverage_set 92 | ], 93 | } 94 | -------------------------------------------------------------------------------- /monodromy/attic/inflate.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/io/inflate.py 3 | 4 | Routines for re-inflating a previously exported coverage set. 5 | """ 6 | 7 | from collections import Counter 8 | from typing import Dict, List, Tuple 9 | 10 | from ..xx_decompose.circuits import OperationPolytope 11 | from .base import CircuitPolytopeData 12 | from ..coordinates import monodromy_alcove_c2 13 | 14 | 15 | def inflate_scipy_data(deflated_data): 16 | """ 17 | Re-inflates serialized coverage set data. 18 | """ 19 | 20 | coverage_set = { 21 | k: CircuitPolytopeData.inflate(v) 22 | for k, v in deflated_data["coverage_set"].items() 23 | } 24 | precomputed_backsolutions = [ 25 | CircuitPolytopeData.inflate(d) 26 | for d in deflated_data["precomputed_backsolutions"] 27 | ] 28 | 29 | return { 30 | "coverage_set": coverage_set, 31 | "precomputed_backsolutions": precomputed_backsolutions, 32 | } 33 | 34 | 35 | def filter_scipy_data( 36 | operations: List[OperationPolytope], 37 | *, 38 | coverage_set: Dict[Tuple, CircuitPolytopeData] = None, 39 | precomputed_backsolutions: List[CircuitPolytopeData] = None, 40 | chatty=True, 41 | ): 42 | """ 43 | Attaches costs to the tables to be supplied to `MonodromyZXDecomposer`. 44 | """ 45 | # reinflate the polytopes, simultaneously calculating their current cost 46 | inflated_polytopes = [ 47 | CircuitPolytopeData( 48 | cost=sum([x * y.cost for x, y in zip(k, operations)]), 49 | convex_subpolytopes=v.convex_subpolytopes, 50 | operations=v.operations, 51 | ) for k, v in coverage_set.items() 52 | ] 53 | 54 | # retain only the low-cost polytopes, discarding everything after a 55 | # universal template has been found. 56 | cost_trimmed_polytopes = [] 57 | for polytope in sorted(inflated_polytopes, key=lambda x: x.cost): 58 | if chatty: 59 | print(f"Keeping {'.'.join(polytope.operations)}: {polytope.cost}") 60 | cost_trimmed_polytopes.append(polytope) 61 | if (set([tuple(x) for x in 62 | polytope.convex_subpolytopes[0].inequalities]) == 63 | set([tuple(x) for x in 64 | monodromy_alcove_c2.convex_subpolytopes[0].inequalities])): 65 | break 66 | 67 | if chatty: 68 | print(f"Kept {len(cost_trimmed_polytopes)} regions.") 69 | 70 | # then, discard those polytopes which perfectly overlap previously seen 71 | # polytopes. this is computationally expensive to do exactly, so we use an 72 | # approximate check instead. 73 | seen_polytopes = [] 74 | coverage_trimmed_polytopes = [] 75 | for polytope in cost_trimmed_polytopes: 76 | if chatty: 77 | print(f"Reconsidering {'.'.join(polytope.operations)}... ", end="") 78 | these_polytopes = [ 79 | (set([tuple(x) for x in cp.inequalities]), 80 | set([tuple(y) for y in cp.equalities])) 81 | for cp in polytope.convex_subpolytopes 82 | ] 83 | if all(p in seen_polytopes for p in these_polytopes): 84 | if chatty: 85 | print("skipping.") 86 | continue 87 | if chatty: 88 | print("keeping.") 89 | seen_polytopes += these_polytopes 90 | coverage_trimmed_polytopes.append(polytope) 91 | 92 | if chatty: 93 | print(f"Kept {len(coverage_trimmed_polytopes)} regions.") 94 | 95 | # finally, re-inflate the relevant subset of the scipy precomputation. 96 | reinflated_scipy = [] 97 | coverage_trimmed_signatures = [Counter(x.operations) 98 | for x in coverage_trimmed_polytopes] 99 | reinflated_scipy = [ 100 | x for x in precomputed_backsolutions 101 | if Counter(x.operations) in coverage_trimmed_signatures 102 | ] 103 | 104 | return { 105 | "operations": operations, 106 | "coverage_set": coverage_trimmed_polytopes, 107 | "precomputed_backsolutions": reinflated_scipy, 108 | } 109 | -------------------------------------------------------------------------------- /monodromy/attic/lrs_decompose.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | 5 | from dataclasses import dataclass 6 | from typing import List 7 | 8 | 9 | @dataclass 10 | class OperationPolytopeData(CircuitPolytopeData): 11 | """ 12 | A polytope which describes a single gate, together with a precomputed 13 | QISKit circuit expressing its canonical form in native operations. 14 | 15 | For example, the native operation sqrtCX on a device would be encoded as an 16 | OperationPolytope with the same canonical coordinates as 1/2 XX, and with a 17 | `canonical_circuit` slot containing 18 | 19 | H 1 ; sqrtCX ; H 1 20 | 21 | which expresses 1/2 XX in terms of this native multiqubit interaction. 22 | """ 23 | canonical_circuit: qiskit.QuantumCircuit 24 | 25 | 26 | @dataclass 27 | class OperationPolytope(OperationPolytopeData, CircuitPolytope): 28 | """ 29 | See OperationPolytopeData. 30 | """ 31 | pass 32 | 33 | 34 | def decomposition_hop( 35 | coverage_set: List[CircuitPolytope], 36 | operations: List[OperationPolytope], 37 | container: Polytope, 38 | target_polytope: Polytope 39 | ): 40 | """ 41 | Using a fixed `coverage_set` and `operations`, takes a `target_polytope` 42 | describing some canonical gates to be modeled within `container`, then finds 43 | a lower-cost member of the coverage set and a preimage for the target within 44 | it. 45 | 46 | Returns a tuple: ( 47 | preimage canonical point, 48 | operation name, 49 | target canonical point, 50 | coverage polytope to which the preimage belongs 51 | ) 52 | """ 53 | ancestor_polytope, operation_polytope = None, None 54 | 55 | # otherwise, find the ancestor and edge for this polytope. 56 | for polytope in operations: 57 | if polytope.operations[0] == container.operations[-1]: 58 | operation_polytope = polytope 59 | break 60 | for polytope in coverage_set: 61 | if polytope.operations == container.operations[:-1]: 62 | ancestor_polytope = polytope 63 | break 64 | 65 | if ancestor_polytope is None or operation_polytope is None: 66 | raise ValueError("Unable to find ancestor / operation polytope.") 67 | 68 | # calculate the intersection of qlr + (ancestor, operation, target), 69 | # then project to the first tuple. 70 | # NOTE: the extra condition is to force compatibility with 71 | # `decompose_xxyy_into_xxyy_xx`, but it isn't necessary in general. 72 | # in fact, it's also not sufficient: we may have to retry this 73 | # this decomposition step if that routine fails later on. 74 | backsolution_polytope = intersect_and_project( 75 | target="a", 76 | a_polytope=ancestor_polytope, 77 | b_polytope=operation_polytope, 78 | c_polytope=target_polytope, 79 | extra_polytope=Polytope(convex_subpolytopes=[ 80 | # equate first source and first target coordinates 81 | ConvexPolytope(inequalities=[ 82 | [0, 1, 1, 0, 0, 0, 0, -1, -1, 0], 83 | [0, -1, -1, 0, 0, 0, 0, 1, 1, 0], 84 | ]), 85 | # equate first source and second target coordinates 86 | ConvexPolytope(inequalities=[ 87 | [0, 1, 1, 0, 0, 0, 0, -1, 0, -1], 88 | [0, -1, -1, 0, 0, 0, 0, 1, 0, 1], 89 | ]), 90 | # equate first source and third target coordinates 91 | ConvexPolytope(inequalities=[ 92 | [0, 1, 1, 0, 0, 0, 0, 0, -1, -1], 93 | [0, -1, -1, 0, 0, 0, 0, 0, 1, 1], 94 | ]), 95 | # equate second source and second target coordinates 96 | ConvexPolytope(inequalities=[ 97 | [0, 1, 0, 1, 0, 0, 0, -1, 0, -1], 98 | [0, -1, 0, -1, 0, 0, 0, 1, 0, 1], 99 | ]), 100 | # equate second source and third target coordinates 101 | ConvexPolytope(inequalities=[ 102 | [0, 1, 0, 1, 0, 0, 0, 0, -1, -1], 103 | [0, -1, 0, -1, 0, 0, 0, 0, 1, 1], 104 | ]), 105 | # equate third source and third target coordinates 106 | ConvexPolytope(inequalities=[ 107 | [0, 0, 1, 1, 0, 0, 0, 0, -1, -1], 108 | [0, 0, -1, -1, 0, 0, 0, 0, 1, 1], 109 | ]), 110 | ]) 111 | ) 112 | 113 | # pick any nonzero point in the backsolution polytope, 114 | # then recurse on that point and the ancestor polytope 115 | all_vertices = [] 116 | for convex_polytope in backsolution_polytope.convex_subpolytopes: 117 | all_vertices += convex_polytope.vertices 118 | if 0 != len(all_vertices): 119 | return ( 120 | # TODO: THIS IS A STOPGAP MEASURE!!! 121 | sample(all_vertices, 1)[0], 122 | operation_polytope.operations[0], 123 | target_polytope.convex_subpolytopes[0].vertices[0], 124 | ancestor_polytope 125 | ) 126 | else: 127 | raise ValueError("Empty backsolution polytope.") 128 | 129 | 130 | def decomposition_hops( 131 | coverage_set: List[CircuitPolytope], 132 | operations: List[OperationPolytope], 133 | target_polytope: Polytope 134 | ): 135 | """ 136 | Fixing a `coverage_set` and a set of `operations`, finds a minimal 137 | decomposition for a canonical interaction in `target_polytope` into a 138 | sequence of operations drawn from `operations`, together with specific 139 | intermediate canonical points linked by them. 140 | 141 | Returns a list of tuples of shape (source vertex, operation, target vertex), 142 | so that each target vertex is accessible from its source vertex by 143 | application of the operation, each target vertex matches its next source 144 | vertex, the original source vertex corresponds to the identity, and the 145 | last target lies in `target_polytope`. 146 | """ 147 | decomposition = [] 148 | 149 | working_polytope = cheapest_container(coverage_set, target_polytope) 150 | 151 | if working_polytope is None: 152 | raise ValueError(f"{target_polytope} not contained in coverage set.") 153 | 154 | # if this polytope corresponds to the empty operation, we're done. 155 | while 0 != len(working_polytope.operations): 156 | source_vertex, operation, target_vertex, working_polytope = \ 157 | decomposition_hop( 158 | coverage_set, operations, working_polytope, target_polytope 159 | ) 160 | 161 | # a/k/a decomposition.push 162 | decomposition.insert(0, (source_vertex, operation, target_vertex)) 163 | target_polytope = exactly(*source_vertex) 164 | 165 | return decomposition 166 | -------------------------------------------------------------------------------- /monodromy/attic/ordered.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/decompose/ordered.py 3 | 4 | 5 | """ 6 | 7 | def calculate_scipy_coverage_set( 8 | coverage_set: List[CircuitPolytope], 9 | operations: List[OperationPolytope], 10 | chatty=False 11 | ) -> List[CircuitPolytope]: 12 | """ 13 | Precalculates a set of backsolution polytopes associated to `covering_set` 14 | and `operations`. 15 | 16 | Used as efficient input to `scipy_decomposition_hops` below. 17 | """ 18 | coordinates = { 19 | "a": [0, 1, 2, 3], 20 | "b": [0, 4, 5, 6], 21 | "c": [0, 7, 8, 9], 22 | } 23 | 24 | inflated_operation_polytope = prereduce_operation_polytopes( 25 | operations=operations, 26 | target_coordinate="a", 27 | background_polytope=Polytope(convex_subpolytopes=[ 28 | # equate first source and first target coordinates 29 | ConvexPolytope(inequalities=[ 30 | [0, 1, 1, 0, 0, 0, 0, -1, -1, 0], 31 | [0, -1, -1, 0, 0, 0, 0, 1, 1, 0], 32 | ]), 33 | # equate first source and second target coordinates 34 | ConvexPolytope(inequalities=[ 35 | [0, 1, 1, 0, 0, 0, 0, -1, 0, -1], 36 | [0, -1, -1, 0, 0, 0, 0, 1, 0, 1], 37 | ]), 38 | # equate first source and third target coordinates 39 | ConvexPolytope(inequalities=[ 40 | [0, 1, 1, 0, 0, 0, 0, 0, -1, -1], 41 | [0, -1, -1, 0, 0, 0, 0, 0, 1, 1], 42 | ]), 43 | # equate second source and second target coordinates 44 | ConvexPolytope(inequalities=[ 45 | [0, 1, 0, 1, 0, 0, 0, -1, 0, -1], 46 | [0, -1, 0, -1, 0, 0, 0, 1, 0, 1], 47 | ]), 48 | # equate second source and third target coordinates 49 | ConvexPolytope(inequalities=[ 50 | [0, 1, 0, 1, 0, 0, 0, 0, -1, -1], 51 | [0, -1, 0, -1, 0, 0, 0, 0, 1, 1], 52 | ]), 53 | # equate third source and third target coordinates 54 | ConvexPolytope(inequalities=[ 55 | [0, 0, 1, 1, 0, 0, 0, 0, -1, -1], 56 | [0, 0, -1, -1, 0, 0, 0, 0, 1, 1], 57 | ])]), 58 | chatty=chatty, 59 | ) 60 | 61 | scipy_coverage_set = [] 62 | 63 | if chatty: 64 | print("Working on scipy precalculation.") 65 | for operation_polytope in coverage_set: 66 | if 0 == len(operation_polytope.operations): 67 | continue 68 | 69 | if chatty: 70 | print(f"Working on {'.'.join(operation_polytope.operations)}...") 71 | 72 | ancestor_polytope = next( 73 | (polytope for polytope in coverage_set 74 | if polytope.operations == operation_polytope.operations[:-1]), 75 | exactly(0, 0, 0)) 76 | 77 | backsolution_polytope = inflated_operation_polytope[ 78 | operation_polytope.operations[-1] 79 | ] 80 | 81 | # also impose whatever constraints we were given besides 82 | backsolution_polytope = backsolution_polytope.intersect( 83 | cylinderize( 84 | ancestor_polytope, 85 | coordinates["a"], 86 | parent_dimension=7 87 | ) 88 | ) 89 | backsolution_polytope = backsolution_polytope.reduce() 90 | 91 | scipy_coverage_set.append(CircuitPolytope( 92 | convex_subpolytopes=backsolution_polytope.convex_subpolytopes, 93 | cost=operation_polytope.cost, 94 | operations=operation_polytope.operations, 95 | )) 96 | 97 | return scipy_coverage_set 98 | 99 | 100 | def scipy_decomposition_hops( 101 | coverage_set: List[CircuitPolytope], 102 | scipy_coverage_set: List[CircuitPolytope], 103 | target_polytope: PolytopeData 104 | ): 105 | """ 106 | Fixing a `coverage_set` and a `scipy_coverage_set`, finds a minimal 107 | decomposition for a canonical interaction in `target_polytope` into a 108 | sequence of operations linking the polytopes in the coverage sets, together 109 | with specific intermediate canonical points linked by them. 110 | 111 | Returns a list of tuples of shape (source vertex, operation, target vertex), 112 | so that each target vertex is accessible from its source vertex by 113 | application of the operation, each target vertex matches its next source 114 | vertex, the original source vertex corresponds to the identity, and the 115 | last target lies in `target_polytope`. 116 | 117 | NOTE: `scipy_coverage_set` is extracted from `coverage_set` using 118 | `calculate_scipy_coverage_set` above. 119 | """ 120 | decomposition = [] # retval 121 | working_polytope = None 122 | 123 | # NOTE: if `target_polytope` were an actual point, could use .has_element 124 | best_cost = float("inf") 125 | for polytope in coverage_set: 126 | if polytope.cost < best_cost: 127 | for convex_subpolytope in \ 128 | polytope.intersect(target_polytope).convex_subpolytopes: 129 | solution = scipy_get_random_vertex(convex_subpolytope) 130 | 131 | if solution.success: 132 | working_polytope = polytope 133 | best_cost = polytope.cost 134 | break 135 | 136 | if working_polytope is None: 137 | raise ValueError(f"{target_polytope} not contained in coverage set.") 138 | 139 | working_operations = working_polytope.operations 140 | 141 | # if this polytope corresponds to the empty operation, we're done. 142 | while 0 < len(working_operations): 143 | backsolution_polytope = None 144 | solution = None 145 | 146 | for polytope in scipy_coverage_set: 147 | if polytope.operations == working_operations: 148 | backsolution_polytope = polytope 149 | break 150 | if backsolution_polytope is None: 151 | raise NoBacksolution() 152 | 153 | # impose the target constraints, which sit on "b" 154 | # (really on "c", but "b" has already been projected off) 155 | intersected_polytope = PolytopeData(convex_subpolytopes=[]) 156 | for cp in backsolution_polytope.convex_subpolytopes: 157 | intersected_polytope.convex_subpolytopes.append( 158 | ConvexPolytopeData( 159 | inequalities=[ 160 | *cp.inequalities, 161 | *[[ineq[0], 0, 0, 0, ineq[1], ineq[2], ineq[3]] 162 | for ineq in 163 | target_polytope.convex_subpolytopes[0].inequalities] 164 | ], 165 | equalities=cp.equalities, 166 | ) 167 | ) 168 | backsolution_polytope = intersected_polytope 169 | 170 | # walk over the backsolution polytopes, try to find one that's solvable 171 | shuffle(backsolution_polytope.convex_subpolytopes) 172 | for convex_subpolytope in backsolution_polytope.convex_subpolytopes: 173 | solution = scipy_get_random_vertex(convex_subpolytope) 174 | if solution.success: 175 | break 176 | 177 | if solution is None or not solution.success: 178 | raise NoBacksolution() 179 | 180 | # a/k/a decomposition.push 181 | decomposition.insert( 182 | 0, 183 | (solution.x[:3], working_operations[-1], solution.x[-3:]) 184 | ) 185 | # NOTE: using `exactly` here causes an infinite loop. 186 | target_polytope = nearly(*solution.x[:3]) 187 | working_operations = working_operations[:-1] 188 | 189 | return decomposition 190 | -------------------------------------------------------------------------------- /monodromy/attic/sample.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/attic/sample.py 3 | 4 | 5 | """ 6 | 7 | 8 | # TODO: In rare cases this generates an empty range, and I'm not sure why. 9 | # TODO: This doesn't sample uniformly; it treats the pushed-forward uniform 10 | # distribution along a projection as uniform, which is false. 11 | def random_alcove_coordinate(denominator=100): 12 | first_numerator = randint(0, denominator // 2) 13 | second_numerator = randint( 14 | max(-first_numerator // 3, -(denominator - 2 * first_numerator) // 2), 15 | min(first_numerator, (3 * denominator - 6 * first_numerator) // 2) 16 | ) 17 | third_numerator = randint( 18 | max(-(first_numerator + second_numerator) // 2 + 1, 19 | -(denominator - 2 * first_numerator) // 2), 20 | min(second_numerator + 1, 21 | denominator - 2 * first_numerator - second_numerator) 22 | ) 23 | 24 | return ( 25 | Fraction(first_numerator, denominator), 26 | Fraction(second_numerator, denominator), 27 | Fraction(third_numerator, denominator), 28 | ) 29 | 30 | 31 | def sample_irreducible_circuit(coverage_set, operations, target_gate_polytope): 32 | """ 33 | Produces a randomly generated circuit of the prescribed type which cannot 34 | be rewritten into a circuit of lower cost. 35 | """ 36 | 37 | operation_gates = { 38 | operation.operations[0]: 39 | qiskit.extensions.UnitaryGate( 40 | canonical_matrix(*alcove_to_positive_canonical_coordinate( 41 | *operation.convex_subpolytopes[0].vertices[0])), 42 | label=f"CAN({operation.operations[0]})" 43 | ) 44 | for operation in operations 45 | } 46 | 47 | q = qiskit.QuantumRegister(2, 'q') 48 | 49 | while True: 50 | qc = qiskit.QuantumCircuit(q) 51 | 52 | qc.u3(qubit=0, 53 | theta=np.random.uniform(2 * np.pi), 54 | phi=np.random.uniform(2 * np.pi), 55 | lam=np.random.uniform(2 * np.pi)) 56 | qc.u3(qubit=1, 57 | theta=np.random.uniform(2 * np.pi), 58 | phi=np.random.uniform(2 * np.pi), 59 | lam=np.random.uniform(2 * np.pi)) 60 | for operation in target_gate_polytope.operations: 61 | qc.append(operation_gates[operation], q) 62 | qc.u3(qubit=0, 63 | theta=np.random.uniform(2 * np.pi), 64 | phi=np.random.uniform(2 * np.pi), 65 | lam=np.random.uniform(2 * np.pi)) 66 | qc.u3(qubit=1, 67 | theta=np.random.uniform(2 * np.pi), 68 | phi=np.random.uniform(2 * np.pi), 69 | lam=np.random.uniform(2 * np.pi)) 70 | 71 | point = unitary_to_alcove_coordinate( 72 | qiskit.quantum_info.Operator(qc).data 73 | ) 74 | computed_depth = cheapest_container( 75 | coverage_set, exactly(*point[:-1]) 76 | ).cost 77 | 78 | if computed_depth >= target_gate_polytope.cost: 79 | break 80 | 81 | return qc 82 | -------------------------------------------------------------------------------- /monodromy/attic/unordered.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/xx_decompose/precalculate.py 3 | 4 | Precalculates the "backsolution" polytopes used for constructing right-angled 5 | paths. 6 | 7 | NOTE: This code is _not_ for export into QISKit. 8 | """ 9 | 10 | from collections import Counter 11 | from typing import List 12 | 13 | from ..coverage import prereduce_operation_polytopes 14 | from ..elimination import cylinderize 15 | from ..polytopes import ConvexPolytope, Polytope 16 | 17 | from .circuits import CircuitPolytope 18 | 19 | 20 | def calculate_unordered_scipy_coverage_set( 21 | coverage_set: List[CircuitPolytope], 22 | operations: List[CircuitPolytope], 23 | chatty=False 24 | ) -> List[CircuitPolytope]: 25 | """ 26 | The terms in `coverage_set` are related by equations of the form 27 | 28 | P_(a1 ... a(n-1)) O_(an) = P_(a1 ... an), 29 | 30 | where O_an is a choice of some element in `operations`. As part of 31 | `build_coverage_set`, we calculate a "frontier" of P_(a1 ... an) in order to 32 | exhaust the Weyl alcove, but we discard the relationship described above. 33 | In order to produce circuits, it's useful to have this relationship 34 | available, so we re-compute it here so that it's available for input to 35 | `scipy_unordered_decomposition_hops`. 36 | 37 | We make a major further (conjectural) assumption: that the coverage polytope 38 | corresponding to a fixed sequence of X-interactions is invariant under 39 | permutation of the interaction strengths. This has two effects: 40 | + We can trim the total amount of computation performed. 41 | + For another, the "right-angled" assumption is only valid if we allow the 42 | path builder to choose which gate it will cleave from a circuit, rather 43 | than requiring it to pick the "last" gate in some specified permutation. 44 | By scanning over permutations during decomposition, we avoid the pitfall. 45 | """ 46 | coordinates = { 47 | "a": [0, 1, 2, 3], 48 | "b": [0, 4, 5, 6], 49 | "c": [0, 7, 8, 9], 50 | } 51 | 52 | inflated_operation_polytope = prereduce_operation_polytopes( 53 | operations=operations, 54 | target_coordinate="a", 55 | background_polytope=Polytope(convex_subpolytopes=[ 56 | # equate first source and first target coordinates 57 | ConvexPolytope(inequalities=[ 58 | [0, 1, 1, 0, 0, 0, 0, -1, -1, 0], 59 | [0, -1, -1, 0, 0, 0, 0, 1, 1, 0], 60 | ]), 61 | # equate first source and second target coordinates 62 | ConvexPolytope(inequalities=[ 63 | [0, 1, 1, 0, 0, 0, 0, -1, 0, -1], 64 | [0, -1, -1, 0, 0, 0, 0, 1, 0, 1], 65 | ]), 66 | # equate first source and third target coordinates 67 | ConvexPolytope(inequalities=[ 68 | [0, 1, 1, 0, 0, 0, 0, 0, -1, -1], 69 | [0, -1, -1, 0, 0, 0, 0, 0, 1, 1], 70 | ]), 71 | # equate second source and second target coordinates 72 | ConvexPolytope(inequalities=[ 73 | [0, 1, 0, 1, 0, 0, 0, -1, 0, -1], 74 | [0, -1, 0, -1, 0, 0, 0, 1, 0, 1], 75 | ]), 76 | # equate second source and third target coordinates 77 | ConvexPolytope(inequalities=[ 78 | [0, 1, 0, 1, 0, 0, 0, 0, -1, -1], 79 | [0, -1, 0, -1, 0, 0, 0, 0, 1, 1], 80 | ]), 81 | # equate third source and third target coordinates 82 | ConvexPolytope(inequalities=[ 83 | [0, 0, 1, 1, 0, 0, 0, 0, -1, -1], 84 | [0, 0, -1, -1, 0, 0, 0, 0, 1, 1], 85 | ])]), 86 | chatty=chatty, 87 | ) 88 | 89 | scipy_coverage_set = [] 90 | 91 | if chatty: 92 | print("Working on scipy precalculation.") 93 | 94 | # we're looking to walk "up" from descendants to ancestors. 95 | for descendant_polytope in coverage_set: 96 | if 0 == len(descendant_polytope.operations): 97 | continue 98 | 99 | if chatty: 100 | print(f"Precalculating for {'.'.join(descendant_polytope.operations)}...") 101 | 102 | for operation_polytope in operations: 103 | operation = operation_polytope.operations[0] 104 | # if we don't use this operation, we can't backtrack along it. 105 | if operation not in descendant_polytope.operations: 106 | continue 107 | 108 | # otherwise, locate an up-to-reordering ancestor. 109 | ancestor_polytope = next( 110 | (polytope for polytope in coverage_set 111 | if Counter(descendant_polytope.operations) == 112 | Counter([operation] + polytope.operations)), 113 | None 114 | ) 115 | if ancestor_polytope is None: 116 | if chatty: 117 | print(f"{'.'.join(descendant_polytope.operations)} has no " 118 | f"ancestor along {operation}.") 119 | print("Available coverage set entries:") 120 | for x in coverage_set: 121 | print(f"{'.'.join(x.operations)}") 122 | raise ValueError(f"{'.'.join(descendant_polytope.operations)} " 123 | f"has no ancestor along {operation}.") 124 | 125 | if chatty: 126 | print(f" ... backtracking along {operation} to " 127 | f"{'.'.join(ancestor_polytope.operations)}...") 128 | 129 | # also impose whatever constraints we were given besides 130 | backsolution_polytope = inflated_operation_polytope[operation] \ 131 | .intersect(cylinderize( 132 | ancestor_polytope, 133 | coordinates["a"], 134 | parent_dimension=7 135 | )) \ 136 | .reduce() 137 | 138 | scipy_coverage_set.append(CircuitPolytope( 139 | convex_subpolytopes=backsolution_polytope.convex_subpolytopes, 140 | cost=descendant_polytope.cost, 141 | operations=ancestor_polytope.operations + [operation], 142 | )) 143 | 144 | return scipy_coverage_set 145 | 146 | 147 | 148 | # 149 | # The code below was for export into QISKit, but no longer! 150 | # 151 | 152 | 153 | def single_unordered_decomposition_hop( 154 | target, working_operations, scipy_coverage_set 155 | ): 156 | """ 157 | Produces a single inverse step in a right-angled path. The target of the 158 | step is `target`, expressed in monodromy coordinates, and which belongs to 159 | to the circuit type consisting of XX-type operations enumerated in 160 | `working_operations`. The step is taken along one such operation in 161 | `working_operations`, and the source of the step belongs 162 | 163 | Returns a dictionary keyed on "hop", "ancestor", and "operations_remaining", 164 | which respectively are: a triple (source, operation, target) describing the 165 | single step; the source coordinate of the step; and the remaining set of 166 | operations yet to be stripped off. 167 | 168 | NOTE: Operates with the assumption that gates within the circuit 169 | decomposition may be freely permuted. 170 | """ 171 | backsolution_polytope = PolytopeData(convex_subpolytopes=[]) 172 | for ancestor in scipy_coverage_set: 173 | # check that this is actually an ancestor 174 | if Counter(ancestor.operations) != Counter(working_operations): 175 | continue 176 | 177 | # impose the target constraints, which sit on "b" 178 | # (really on "c", but "b" has already been projected off) 179 | backsolution_polytope.convex_subpolytopes += [ 180 | ConvexPolytopeData( 181 | inequalities=[ 182 | [ineq[0] + sum(x * y for x, y in zip(ineq[4:], target)), 183 | ineq[1], ineq[2], ineq[3]] 184 | for ineq in cp.inequalities 185 | ], 186 | equalities=[ 187 | [eq[0] + sum(x * y for x, y in zip(eq[4:], target)), 188 | eq[1], eq[2], eq[3]] 189 | for eq in cp.equalities 190 | ], 191 | ) 192 | for cp in ancestor.convex_subpolytopes 193 | ] 194 | 195 | # walk over the convex backsolution subpolytopes, try to find one 196 | # that's solvable 197 | try: 198 | solution = manual_get_random_vertex(backsolution_polytope) 199 | 200 | return { 201 | "hop": (solution, ancestor.operations[-1], target), 202 | "ancestor": solution, 203 | "operations_remaining": ancestor.operations[:-1] 204 | } 205 | except NoFeasibleSolutions: 206 | pass 207 | 208 | raise NoBacksolution() 209 | -------------------------------------------------------------------------------- /monodromy/backend/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/backend/__init__.py 3 | 4 | Broadcast imports for the backend. 5 | """ 6 | 7 | from ..exceptions import NoFeasibleSolutions 8 | 9 | backend = None 10 | """ 11 | Global repository for the active backend. 12 | """ 13 | -------------------------------------------------------------------------------- /monodromy/backend/backend_abc.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/backend/backend_abc.py 3 | 4 | A generic backend specification for polytope computations. 5 | """ 6 | 7 | from abc import ABC, abstractmethod 8 | 9 | 10 | class Backend(ABC): 11 | """ 12 | Generic backend interface for polytope procedures. 13 | """ 14 | 15 | @staticmethod 16 | @abstractmethod 17 | def volume(convex_polytope): # ConvexPolytope -> PolytopeVolume 18 | """ 19 | Calculates the Eucliean volume of the ConvexPolytope. 20 | 21 | Signals `NoFeasibleSolutions` if the ConvexPolytope has no solutions. 22 | """ 23 | pass 24 | 25 | @staticmethod 26 | @abstractmethod 27 | def reduce(convex_polytope): # ConvexPolytope -> ConvexPolytope 28 | """ 29 | Calculates a minimum set of inequalities specifying the ConvexPolytope. 30 | 31 | Signals `NoFeasibleSolutions` if the ConvexPolytope has no solutions. 32 | """ 33 | pass 34 | 35 | @staticmethod 36 | @abstractmethod 37 | def vertices(convex_polytope): # ConvexPolytope -> List[List[Fraction]] 38 | """ 39 | Calculates the vertices of the ConvexPolytope. 40 | 41 | Signals `NoFeasibleSolutions` if the ConvexPolytope has no solutions. 42 | """ 43 | pass 44 | 45 | @staticmethod 46 | @abstractmethod 47 | def triangulation(convex_polytope): # ConvexPolytope -> List[List[int]] 48 | """ 49 | Calculates a triangulation of the input ConvexPolytope. Returns a list 50 | of simplices, each specified as a list of its vertices, in turn each 51 | specified as the index into .vertices at which it appears. 52 | """ 53 | pass 54 | 55 | @staticmethod 56 | @abstractmethod 57 | def convex_hull(vertices): # List[List[Fraction]] -> ConvexPolytope 58 | """ 59 | Produces a minimal ConvexPolytope from a set of vertices. 60 | """ 61 | pass 62 | -------------------------------------------------------------------------------- /monodromy/backend/lrs.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/backend/lrs.py 3 | 4 | Communication interface for `lrs`, a package for convex hull problems. 5 | 6 | More information about `lrs`: http://cgm.cs.mcgill.ca/~avis/C/lrs.html 7 | """ 8 | 9 | 10 | from copy import copy 11 | from fractions import Fraction 12 | from functools import reduce 13 | import math # for gcd 14 | from operator import itemgetter 15 | from os import getenv 16 | from subprocess import Popen, PIPE 17 | from typing import List 18 | 19 | from .backend_abc import Backend 20 | from ..exceptions import NoFeasibleSolutions 21 | from ..polytopes import ConvexPolytope, PolytopeVolume 22 | from ..utilities import lcm 23 | 24 | 25 | LRS_ENV = "LRS_PATH" 26 | """Environment variable used to override the path to the `lrs` executable.""" 27 | 28 | 29 | LRS_PATH = getenv(LRS_ENV, "lrs") 30 | """Default path to the user's `lrs` executable.""" 31 | 32 | 33 | def check_for_lrs(): 34 | """ 35 | Checks whether `lrs` is findable and executable. 36 | """ 37 | try: 38 | proc = Popen([LRS_PATH], stdin=PIPE, stdout=PIPE, stderr=PIPE) 39 | proc.communicate(b"") 40 | return True 41 | except Exception: # FileNotFoundError, for instance 42 | return False 43 | 44 | 45 | class LRSBackend(Backend): 46 | def __init__(self): 47 | super().__init__() 48 | assert check_for_lrs(), "LRS not present." 49 | 50 | @staticmethod 51 | def volume(convex_polytope: ConvexPolytope) -> PolytopeVolume: 52 | if 0 == len(convex_polytope.vertices): 53 | raise NoFeasibleSolutions() 54 | 55 | vertices = [[Fraction(1, 1), *x] for x in convex_polytope.vertices] 56 | vertex_payload = encode_vertices(vertices) 57 | inequality_response = single_lrs_pass(vertex_payload) 58 | inequality_dictionary = decode_inequalities(inequality_response) 59 | return PolytopeVolume( 60 | volume=inequality_dictionary["volume"], 61 | dimension=inequality_dictionary["dimension"], 62 | ) 63 | 64 | # NOTE: This method uses the `redund` option, which is a recent addition to 65 | # `lrs` and may be buggy. The (commented out) variant of this method 66 | # below performs the same computation without `redund`, but is slower. 67 | # @staticmethod 68 | # def reduce(convex_polytope: ConvexPolytope) -> ConvexPolytope: 69 | # clone = copy(convex_polytope) 70 | # 71 | # inequalities = convex_polytope.inequalities 72 | # equalities = convex_polytope.equalities 73 | # inequality_payload = encode_inequalities( 74 | # inequalities, equalities, 75 | # options=["redund 0 0"] # lrs ≥ 7.1 76 | # ) 77 | # inequality_response = single_lrs_pass(inequality_payload) 78 | # inequality_dictionary = decode_inequalities(inequality_response) 79 | # 80 | # clone.inequalities = inequality_dictionary["inequalities"] 81 | # clone.equalities = inequality_dictionary["equalities"] 82 | # 83 | # return clone 84 | 85 | # NOTE: This method does not use the `redund` option, which is a recent 86 | # addition to `lrs` and may be buggy. The variant of this method 87 | # above performs the same computation with `redund`, so is faster. 88 | @staticmethod 89 | def reduce(convex_polytope: ConvexPolytope) -> ConvexPolytope: 90 | clone = copy(convex_polytope) 91 | 92 | inequalities = convex_polytope.inequalities 93 | equalities = convex_polytope.equalities 94 | inequality_payload = encode_inequalities( 95 | inequalities, equalities, 96 | ) 97 | vertex_response = single_lrs_pass(inequality_payload) 98 | vertices = decode_vertices(vertex_response) 99 | vertex_payload = encode_vertices(vertices) 100 | inequality_response = single_lrs_pass(vertex_payload) 101 | inequality_dictionary = decode_inequalities(inequality_response) 102 | 103 | clone.inequalities = inequality_dictionary["inequalities"] 104 | clone.equalities = inequality_dictionary["equalities"] 105 | 106 | return clone 107 | 108 | @staticmethod 109 | def vertices(convex_polytope: ConvexPolytope) -> List[List[Fraction]]: 110 | inequalities = convex_polytope.inequalities 111 | equalities = convex_polytope.equalities 112 | inequality_payload = encode_inequalities(inequalities, equalities) 113 | vertex_response = single_lrs_pass(inequality_payload) 114 | vertices = decode_vertices(vertex_response) 115 | if any([v[0] == 0 for v in vertices]): 116 | raise ValueError("Polytope is not bounded.") 117 | 118 | return [v[1:] for v in vertices] 119 | 120 | @staticmethod 121 | def triangulation(convex_polytope: ConvexPolytope) -> List[List]: 122 | if 0 == len(convex_polytope.vertices): 123 | raise NoFeasibleSolutions() 124 | 125 | vertex_payload = encode_vertices([(Fraction(1, 1), *v) 126 | for v in convex_polytope.vertices], 127 | options=["triangulation"]) 128 | response = single_lrs_pass(vertex_payload) 129 | simplices = decode_simplices(response) 130 | return simplices["simplices"] 131 | 132 | @staticmethod 133 | def convex_hull(vertices: List[List[Fraction]]) -> ConvexPolytope: 134 | payload = encode_vertices([(1, *x) for x in vertices]) 135 | response = single_lrs_pass(payload) 136 | inequalities, equalities = itemgetter("inequalities", "equalities")( 137 | decode_inequalities(response) 138 | ) 139 | return ConvexPolytope( 140 | inequalities=inequalities, 141 | equalities=equalities, 142 | ) 143 | 144 | 145 | def single_lrs_pass(payload: bytes, chatty=False) -> bytes: 146 | """Generic wrapper for lrs.""" 147 | if chatty: 148 | print("=== LRS CALL ===") 149 | print("Payload:") 150 | print(payload.decode()) 151 | proc = Popen([LRS_PATH], stdin=PIPE, stdout=PIPE, stderr=PIPE) 152 | stdout, stderr = proc.communicate(payload) 153 | # TODO: You could do something with stderr, but beware: if lrs restarts with 154 | # a different arithmetic type, it puts some chatter on that stream. 155 | if chatty: 156 | print("Response:") 157 | print(stdout.decode()) 158 | return stdout 159 | 160 | 161 | def encode_inequalities(inequalities, equalities=None, name="name", 162 | options=None) -> bytes: 163 | """Format `inequalities` for consumption by lrs.""" 164 | equalities = equalities if equalities is not None else [] 165 | options = options if options is not None else [] 166 | output = "" 167 | output += name + "\n" 168 | output += "H-representation\n" 169 | # if 0 < len(equalities): 170 | # output += f"linearity {len(equalities)} " \ 171 | # f"{' '.join(range(1, 1 + len(equalities)))}\n" 172 | output += "begin\n" 173 | output += (f"{len(inequalities) + 2*len(equalities)}" 174 | f" {len((inequalities + equalities)[0])}" 175 | " rational\n") 176 | for row in inequalities + equalities + [[-x for x in eq] for eq in equalities]: 177 | row_gcd = abs(reduce(math.gcd, row)) 178 | row_gcd = row_gcd if row_gcd != 0 else 1 179 | output += " ".join([str(x // row_gcd) for x in row]) + "\n" 180 | output += "end\n" 181 | for option in options: 182 | output += f"{option}\n" 183 | 184 | return output.encode() 185 | 186 | 187 | def decode_inequalities(lrs_output: bytes): 188 | """Parse lrs output (an `H-representation`) into python data.""" 189 | volume = None 190 | rows = [] 191 | name = None 192 | equality_indices = [] 193 | invocation_signature = None 194 | break_at_end = False 195 | for line in lrs_output.decode('utf-8').splitlines(): 196 | # initialize 197 | if line.startswith('*lrs') and line != invocation_signature: 198 | name = None 199 | invocation_signature = line 200 | # stash the Volume output 201 | if line.startswith('*Volume='): 202 | if volume is None: 203 | volume = Fraction(line[8:]) 204 | continue 205 | # ignore comments 206 | if line.startswith('*') or line == '': 207 | continue 208 | # first non-comment line is our name 209 | if name is None: 210 | name = line 211 | continue 212 | # ignore begin / end, assume they're in the right place 213 | if line.startswith('end'): 214 | if break_at_end: 215 | break 216 | else: 217 | continue 218 | if line.startswith('begin'): 219 | rows = [] 220 | continue 221 | # skip the table size, if it's present 222 | if 'rational' in line: 223 | continue 224 | # skip echoed option 225 | if line.startswith('redund'): 226 | break_at_end = True 227 | continue 228 | # check that we're looking at the right kind of representation 229 | if line == 'H-representation': 230 | continue 231 | if line == 'V-representation': 232 | raise ValueError("Inequality table decoder got a vertex table as input") 233 | # if we produced a degenerate polytope, note the indices 234 | if line.startswith('linearity'): 235 | equality_indices = [int(x) for x in line[9:].split()[1:]] 236 | continue 237 | 238 | new_row = [Fraction(x) for x in line.split()] 239 | row_lcm = abs(lcm(*[x.denominator for x in new_row])) 240 | rows.append([int(x * row_lcm) for x in new_row]) 241 | 242 | if 0 == len(rows): 243 | if break_at_end: 244 | raise NoFeasibleSolutions() 245 | else: 246 | print(lrs_output.decode('utf-8')) 247 | raise TypeError("Something bad happened in `lrs`.") 248 | 249 | return dict( 250 | inequalities=[row for index, row in enumerate(rows) 251 | if 1 + index not in equality_indices], 252 | equalities=[row for index, row in enumerate(rows) 253 | if 1 + index in equality_indices], 254 | volume=volume, 255 | dimension=len(rows[0]) - 1 - len(equality_indices), 256 | ) 257 | 258 | 259 | def decode_simplices(lrs_output: bytes): 260 | """Parse lrs output from a tetrahedral run into python data.""" 261 | simplices = [] 262 | for line in lrs_output.decode('utf-8').splitlines(): 263 | # initialize 264 | if line.startswith('*lrs'): 265 | simplices = [] 266 | if line.startswith('end'): 267 | break 268 | if line.startswith('F#'): 269 | tokens = line.split() 270 | position = tokens.index("vertices/rays") 271 | indices = [] 272 | while True: 273 | position += 1 274 | try: 275 | indices.append(int(tokens[position]) - 1) 276 | except ValueError: 277 | break 278 | simplices.append(indices) 279 | 280 | return dict( 281 | simplices=simplices 282 | ) 283 | 284 | 285 | def encode_vertices(vertices, name="name", options=None) -> bytes: 286 | """Format `vertices` for consumption by lrs.""" 287 | options = [] if options is None else options 288 | output = "" 289 | output += name + "\n" 290 | output += "V-representation\n" 291 | output += "begin\n" 292 | output += f"{len(vertices)} {len(vertices[0])} rational\n" 293 | for vertex in vertices: 294 | output += " ".join([str(x) for x in vertex]) + "\n" 295 | output += "end\n" 296 | output += "volume\n" 297 | for option in options: 298 | output += f"{option}\n" 299 | 300 | return output.encode() 301 | 302 | 303 | def decode_vertices(lrs_output: bytes): 304 | """Parse lrs output (a `V-representation`) into python data.""" 305 | invocation_signature = None 306 | name = None 307 | vertices = [] 308 | for line in lrs_output.decode('utf-8').splitlines(): 309 | if line.startswith('*lrs') and line != invocation_signature: 310 | name = None 311 | invocation_signature = line 312 | # ignore comments 313 | if line.startswith('*') or line == '': 314 | continue 315 | # first non-comment line is our name 316 | if name is None: 317 | name = line 318 | continue 319 | # ignore begin / end, assume they're in the right place 320 | if line.startswith('end'): 321 | continue 322 | if line.startswith('begin'): 323 | vertices = [] 324 | continue 325 | if line == 'V-representation': 326 | continue 327 | if line == 'H-representation': 328 | raise ValueError("Vertex table decoder got an inequality table as input") 329 | if line.startswith('linearity'): 330 | continue 331 | if line.startswith("No feasible solution"): 332 | raise NoFeasibleSolutions() 333 | vertices.append([Fraction(x) for x in line.split()]) 334 | 335 | if 0 == len(vertices): 336 | print(lrs_output.decode('utf-8')) 337 | raise TypeError("Something bad happened in `lrs`.") 338 | 339 | return vertices 340 | -------------------------------------------------------------------------------- /monodromy/coverage.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/coverage.py 3 | 4 | Routines for converting a family of native gates to a minimal set of minimum- 5 | cost circuits whose union covers the entire monodromy polytope. 6 | """ 7 | 8 | from dataclasses import dataclass 9 | from fractions import Fraction 10 | import heapq 11 | from typing import Dict, List, Optional 12 | 13 | from .coordinates import monodromy_alcove, monodromy_alcove_c2, rho_reflect 14 | from .io.base import CircuitPolytopeData 15 | from .elimination import cylinderize, project 16 | from .polytopes import Polytope, trim_polytope_set 17 | from .static.examples import everything_polytope, identity_polytope 18 | from .static.qlr_table import qlr_polytope 19 | 20 | 21 | @dataclass 22 | class CircuitPolytope(Polytope, CircuitPolytopeData): 23 | """ 24 | A polytope describing the alcove coverage of a particular circuit type. 25 | """ 26 | 27 | def __gt__(self, other): 28 | return (self.cost > other.cost) or \ 29 | (self.cost == other.cost and self.volume > other.volume) 30 | 31 | def __ge__(self, other): 32 | return (self.cost > other.cost) or \ 33 | (self.cost == other.cost and self.volume >= other.volume) 34 | 35 | def __lt__(self, other): 36 | return (self.cost < other.cost) or \ 37 | (self.cost == other.cost and self.volume < other.volume) 38 | 39 | def __le__(self, other): 40 | return (self.cost < other.cost) or \ 41 | (self.cost == other.cost and self.volume <= other.volume) 42 | 43 | 44 | def deduce_qlr_consequences( 45 | target: str, 46 | a_polytope: Polytope, 47 | b_polytope: Polytope, 48 | c_polytope: Polytope, 49 | extra_polytope: Optional[Polytope] = None, 50 | ) -> Polytope: 51 | """ 52 | Produces the consequences for `target` for a family of a-, b-, and 53 | c-inequalities. `target` can take on the values 'a', 'b', or 'c'. 54 | """ 55 | 56 | coordinates = { 57 | "a": [0, 1, 2, 3], 58 | "b": [0, 4, 5, 6], 59 | "c": [0, 7, 8, 9], 60 | } 61 | assert target in coordinates.keys() 62 | 63 | if extra_polytope is None: 64 | extra_polytope = everything_polytope 65 | 66 | p = extra_polytope.intersect(qlr_polytope) 67 | p = p.union(rho_reflect(p, coordinates[target])) 68 | # impose the "large" alcove constraints 69 | for value in coordinates.values(): 70 | p = p.intersect(cylinderize(monodromy_alcove, value)) 71 | 72 | # also impose whatever constraints we were given besides 73 | p = p.intersect(cylinderize(a_polytope, coordinates["a"])) 74 | p = p.intersect(cylinderize(b_polytope, coordinates["b"])) 75 | p = p.intersect(cylinderize(c_polytope, coordinates["c"])) 76 | 77 | # restrict to the A_{C_2} part of the target coordinate 78 | p = p.intersect(cylinderize(monodromy_alcove_c2, coordinates[target])) 79 | 80 | # lastly, project away the non-target parts 81 | p = p.reduce() 82 | for index in range(9, 0, -1): 83 | if index in coordinates[target]: 84 | continue 85 | p = project(p, index) 86 | p = p.reduce() 87 | 88 | return p 89 | 90 | 91 | def prereduce_operation_polytopes( 92 | operations: List[CircuitPolytope], 93 | target_coordinate: str = "c", 94 | background_polytope: Optional[Polytope] = None, 95 | chatty: bool = False, 96 | ) -> Dict[str, CircuitPolytope]: 97 | """ 98 | Specializes the "b"-coordinates of the monodromy polytope to a particular 99 | operation, then projects them away. 100 | """ 101 | 102 | coordinates = { 103 | "a": [0, 1, 2, 3], 104 | "b": [0, 4, 5, 6], 105 | "c": [0, 7, 8, 9], 106 | } 107 | prereduced_operation_polytopes = {} 108 | 109 | for operation in operations: 110 | if chatty: 111 | print(f"Prereducing QLR relations for {'.'.join(operation.operations)}") 112 | p = background_polytope if background_polytope is not None \ 113 | else everything_polytope 114 | p = p.intersect(qlr_polytope) 115 | p = p.union(rho_reflect(p, coordinates[target_coordinate])) 116 | for value in coordinates.values(): 117 | p = p.intersect(cylinderize(monodromy_alcove, value)) 118 | p = p.intersect(cylinderize(operation, coordinates["b"])) 119 | p = p.intersect(cylinderize(monodromy_alcove_c2, coordinates[target_coordinate])) 120 | 121 | # project away the operation part 122 | p = p.reduce() 123 | for index in [6, 5, 4]: 124 | p = project(p, index) 125 | p = p.reduce() 126 | prereduced_operation_polytopes[operation.operations[-1]] = p 127 | 128 | return prereduced_operation_polytopes 129 | 130 | 131 | def build_coverage_set( 132 | operations: List[CircuitPolytope], 133 | chatty: bool = False, 134 | ) -> List[CircuitPolytope]: 135 | """ 136 | Given a set of `operations`, thought of as members of a native gate set, 137 | this emits a list of circuit shapes built as sequences of those operations 138 | which is: 139 | 140 | + Exhaustive: Every two-qubit unitary is covered by one of the circuit 141 | designs in the list. 142 | + Irredundant: No circuit design is completely contained within other 143 | designs in the list which are of equal or lower cost. 144 | 145 | If `chatty` is toggled, emits progress messages. 146 | """ 147 | 148 | # assert that operations.operation are unique 149 | if len(set([operation.operations[-1] for operation in operations])) != len(operations): 150 | raise ValueError("Operations must be unique.") 151 | 152 | # start by generating precalculated operation polytopes 153 | prereduced_operation_polytopes = prereduce_operation_polytopes( 154 | operations 155 | ) 156 | 157 | # a collection of polytopes explored so far, and their union 158 | total_polytope = CircuitPolytope( 159 | convex_subpolytopes=identity_polytope.convex_subpolytopes, 160 | operations=[], 161 | cost=0., 162 | ) 163 | necessary_polytopes = [total_polytope] 164 | 165 | # a priority queue of sequences to be explored 166 | to_be_explored = [] 167 | for operation in operations: 168 | heapq.heappush(to_be_explored, operation) 169 | 170 | # a set of polytopes waiting to be reduced, all of equal cost 171 | to_be_reduced = [] 172 | waiting_cost = 0 173 | 174 | # main loop: dequeue the next cheapest gate combination to explore 175 | while 0 < len(to_be_explored): 176 | next_polytope = heapq.heappop(to_be_explored) 177 | 178 | # if this cost is bigger than the old cost, flush 179 | if next_polytope.cost > waiting_cost: 180 | to_be_reduced = trim_polytope_set( 181 | to_be_reduced, fixed_polytopes=[total_polytope] 182 | ) 183 | necessary_polytopes += to_be_reduced 184 | for new_polytope in to_be_reduced: 185 | total_polytope = total_polytope.union(new_polytope).reduce() 186 | to_be_reduced = [] 187 | waiting_cost = next_polytope.cost 188 | 189 | if chatty: 190 | print(f"Considering {'·'.join(next_polytope.operations)};\t", 191 | end="") 192 | 193 | # find the ancestral polytope 194 | tail_polytope = next((p for p in necessary_polytopes 195 | if p.operations == next_polytope.operations[:-1]), 196 | None) 197 | # if there's no ancestor, skip 198 | if tail_polytope is None: 199 | if chatty: 200 | print("no ancestor, skipping.") 201 | continue 202 | 203 | # take the head's polytopes, adjoin the new gate (& its rotation), 204 | # calculate new polytopes, and add those polytopes to the working set 205 | # TODO: This used to be part of a call to `deduce_qlr_consequences`, which 206 | # we split up for efficiency. See GH #13. 207 | new_polytope = prereduced_operation_polytopes[ 208 | next_polytope.operations[-1] 209 | ] 210 | new_polytope = new_polytope.intersect(cylinderize( 211 | tail_polytope, [0, 1, 2, 3], parent_dimension=7 212 | )) 213 | new_polytope = new_polytope.reduce() 214 | for index in [3, 2, 1]: 215 | new_polytope = project(new_polytope, index).reduce() 216 | # specialize it from a Polytope to a CircuitPolytope 217 | new_polytope = CircuitPolytope( 218 | operations=next_polytope.operations, 219 | cost=next_polytope.cost, 220 | convex_subpolytopes=new_polytope.convex_subpolytopes 221 | ) 222 | 223 | to_be_reduced.append(new_polytope) 224 | 225 | if chatty: 226 | print(f"Cost {next_polytope.cost} ", end="") 227 | volume = new_polytope.volume 228 | if volume.dimension == 3: 229 | volume = volume.volume / monodromy_alcove_c2.volume.volume 230 | print(f"and Euclidean volume {float(100 * volume):6.2f}%") 231 | else: 232 | print(f"and Euclidean volume {0:6.2f}%") 233 | 234 | # if this polytope is NOT of maximum volume, 235 | if monodromy_alcove_c2.volume > new_polytope.volume: 236 | # add this polytope + the continuations to the priority queue 237 | for operation in operations: 238 | heapq.heappush(to_be_explored, CircuitPolytope( 239 | operations=next_polytope.operations + operation.operations, 240 | cost=next_polytope.cost + operation.cost, 241 | convex_subpolytopes=operation.convex_subpolytopes, 242 | )) 243 | else: 244 | # the cheapest option that gets us to 100% is good enough. 245 | break 246 | 247 | # one last flush 248 | necessary_polytopes += trim_polytope_set( 249 | to_be_reduced, fixed_polytopes=[total_polytope] 250 | ) 251 | for new_polytope in to_be_reduced: 252 | total_polytope = total_polytope.union(new_polytope).reduce() 253 | 254 | return necessary_polytopes 255 | 256 | 257 | def print_coverage_set(necessary_polytopes): 258 | print("Percent volume of A_C2\t | Cost\t | Sequence name") 259 | for gate in necessary_polytopes: 260 | vol = gate.volume 261 | if vol.dimension == 3: 262 | vol = vol.volume / monodromy_alcove_c2.volume.volume 263 | else: 264 | vol = Fraction(0) 265 | print(f"{float(100 * vol):6.2f}% = " 266 | f"{str(vol.numerator): >4}/{str(vol.denominator): <4} " 267 | f"\t | {float(gate.cost):4.2f}" 268 | f"\t | {'.'.join(gate.operations)}") 269 | -------------------------------------------------------------------------------- /monodromy/elimination.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/elimination.py 3 | 4 | Implements coordinate-wise inclusion and projections of inequality families. 5 | """ 6 | 7 | from typing import List 8 | 9 | from .polytopes import Polytope, ConvexPolytope 10 | 11 | 12 | def cylinderize( 13 | polytope: Polytope, 14 | coordinate_map: List[int], 15 | parent_dimension: int = 10): 16 | """ 17 | Consumes a `polytope` and a list of n coordinates on Q^m, and emits a 18 | polytope cylinderized along the complement of those Q^m coordinates. 19 | """ 20 | 21 | cylinderized_polytope = Polytope(convex_subpolytopes=[]) 22 | for convex_subpolytope in polytope.convex_subpolytopes: 23 | cylinderized_subpolytope = ConvexPolytope( 24 | inequalities=[], 25 | name=convex_subpolytope.name 26 | ) 27 | for inequality in convex_subpolytope.inequalities: 28 | new_row = [0] * parent_dimension 29 | for source_value, target_index in zip(inequality, coordinate_map): 30 | new_row[target_index] += source_value 31 | cylinderized_subpolytope.inequalities.append(new_row) 32 | for equality in convex_subpolytope.equalities: 33 | new_row = [0] * parent_dimension 34 | for source_value, target_index in zip(equality, coordinate_map): 35 | new_row[target_index] += source_value 36 | cylinderized_subpolytope.equalities.append(new_row) 37 | cylinderized_polytope.convex_subpolytopes.append( 38 | cylinderized_subpolytope 39 | ) 40 | 41 | return cylinderized_polytope 42 | 43 | 44 | def project(polytope, index): 45 | """ 46 | Returns the projection of `polytope` away from coordinate `index`. 47 | 48 | Implements the (naive) Fourier-Motzkin elimination algorithm; see 49 | 50 | https://en.wikipedia.org/wiki/Fourier%E2%80%93Motzkin_elimination 51 | 52 | for more details. 53 | 54 | NOTE: Some pairs of inequalities of the result may belong to equalities. 55 | To collect these equalities, call reduce. 56 | NOTE: lrs supposedly supports this, but they note that others have reported 57 | bugs and so suggest that users don't engage with it. 58 | """ 59 | 60 | projected_polytope = Polytope(convex_subpolytopes=[]) 61 | 62 | for convex_subpolytope in polytope.convex_subpolytopes: 63 | # F-M collects inequalities into three buckets: 64 | # those with the `index` summand zero, positive, and negative. 65 | zero_equalities = [] 66 | zero_inequalities = [] 67 | positive_inequalities = [] 68 | negative_inequalities = [] 69 | for inequality in convex_subpolytope.inequalities: 70 | if 0 == inequality[index]: 71 | zero_inequalities.append(inequality) 72 | elif 0 < inequality[index]: 73 | positive_inequalities.append(inequality) 74 | elif 0 > inequality[index]: 75 | negative_inequalities.append(inequality) 76 | else: 77 | raise TypeError(f"Switch failure on {inequality[index]}") 78 | for equality in convex_subpolytope.equalities: 79 | if 0 == equality[index]: 80 | zero_equalities.append(equality) 81 | elif 0 < equality[index]: 82 | positive_inequalities.append(equality) 83 | negative_inequalities.append([-x for x in equality]) 84 | elif 0 > equality[index]: 85 | negative_inequalities.append(equality) 86 | positive_inequalities.append([-x for x in equality]) 87 | else: 88 | raise TypeError(f"Switch failure on {equality[index]}") 89 | 90 | joined_inequalities = [] 91 | for positive_inequality in positive_inequalities: 92 | for negative_inequality in negative_inequalities: 93 | # `positive_inequality` can be written as `1 * xj >= pos_rest`, 94 | # `negative_inequality` can be written as `neg_rest >= 1 * xj`. 95 | # Each pair contributes an inequality `neg_rest >= pos_rest`, 96 | # or `neg_rest - pos_rest >= 0` . 97 | 98 | pos_scalar = positive_inequality[index] 99 | neg_scalar = negative_inequality[index] 100 | joined_inequality = [p * -neg_scalar + n * pos_scalar 101 | for (p, n) in zip(positive_inequality, negative_inequality)] 102 | joined_inequality = joined_inequality[:index] + joined_inequality[1+index:] 103 | 104 | joined_inequalities.append(joined_inequality) 105 | 106 | # For the remainder, we just ignore the unwanted coordinate. 107 | zero_inequalities = [z[:index] + z[1+index:] for z in zero_inequalities] 108 | zero_equalities = [z[:index] + z[1+index:] for z in zero_equalities] 109 | 110 | projected_polytope.convex_subpolytopes.append( 111 | ConvexPolytope(inequalities=zero_inequalities + joined_inequalities, 112 | equalities=zero_equalities, 113 | name=convex_subpolytope.name) 114 | ) 115 | 116 | return projected_polytope 117 | -------------------------------------------------------------------------------- /monodromy/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/exceptions.py 3 | 4 | Exception classes used throughout the project. 5 | """ 6 | 7 | 8 | class NoBacksolution(Exception): 9 | """ 10 | Signaled when the circuit backsolver can't find a suitable preimage point. 11 | 12 | Conjectured to be probabilistically meaningless: should be fine to re-run 13 | the call after catching this error. 14 | """ 15 | pass 16 | 17 | 18 | class NoFeasibleSolutions(Exception): 19 | """Emitted when reducing a convex polytope with no solutions.""" 20 | pass 21 | -------------------------------------------------------------------------------- /monodromy/haar.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/haar.py 3 | 4 | Routines for calculating the Haar volume of polytopes expressed in positive 5 | canonical coordinates. 6 | """ 7 | 8 | from math import sqrt 9 | from typing import List 10 | 11 | import numpy as np 12 | 13 | from .coordinates import monodromy_to_positive_canonical_polytope 14 | from .io.base import CircuitPolytopeData 15 | from .polynomials import Polynomial, TrigPolynomial 16 | from .polytopes import alternating_sum, make_convex_polytope 17 | from .static.examples import empty_polytope 18 | from .utilities import epsilon 19 | 20 | 21 | # duck typing means poor dispatching... 22 | def _haar_volume_tetrahedron(tetrahedron, integrand=None): 23 | """ 24 | Integrates the PU(4) Haar form over a 3D tetrahedron expressed in positive 25 | canonical coordinates, normalized so that CX lies at (pi/2, 0, 0). (NOTE: 26 | This differs from almost everywhere else in this codebase by a factor of 2!) 27 | 28 | See Watts, O'Connor, and Vala's _Metric Structure of the Space of Two-Qubit 29 | Gates, Perfect Entanglers and Quantum Control_, Equation (27), which we've 30 | rewritten one step further to remove all products. 31 | 32 | Takes an optional `integrand`, a polynomial expressed in (c1, c2, c3), to 33 | integrate against the Haar measure. 34 | """ 35 | 36 | tetrahedron = list([list(x) for x in tetrahedron]) 37 | 38 | if len(tetrahedron) != 4 or any([len(x) != 3 for x in tetrahedron]): 39 | return 0 40 | 41 | ((x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3)) = tetrahedron 42 | 43 | determinant = np.linalg.det(np.array([ 44 | [x1 - x0, y1 - y0, z1 - z0], 45 | [x2 - x0, y2 - y0, z2 - z0], 46 | [x3 - x0, y3 - y0, z3 - z0], 47 | ])) 48 | 49 | c1 = Polynomial.from_linear_list([x0, x1 - x0, x2 - x0, x3 - x0]) 50 | c2 = Polynomial.from_linear_list([y0, y1 - y0, y2 - y0, y3 - y0]) 51 | c3 = Polynomial.from_linear_list([z0, z1 - z0, z2 - z0, z3 - z0]) 52 | 53 | # transform integrand into tetrahedral coordinates 54 | if integrand is None: 55 | transformed_integrand = Polynomial.from_linear_list([1]) 56 | else: 57 | transformed_integrand = Polynomial() 58 | for power_tuple, coefficient in integrand.coefficient_table.items(): 59 | summand = Polynomial.from_linear_list([1]) 60 | summand = summand * coefficient 61 | for _ in [] if len(power_tuple) < 1 else range(power_tuple[0]): 62 | summand = summand * c1 63 | for _ in [] if len(power_tuple) < 2 else range(power_tuple[1]): 64 | summand = summand * c2 65 | for _ in [] if len(power_tuple) < 3 else range(power_tuple[2]): 66 | summand = summand * c3 67 | transformed_integrand = transformed_integrand + summand 68 | 69 | haar_form = [] 70 | for left, right in [(c1, c2), (c2, c3), (c3, c1)]: 71 | haar_form += [ 72 | TrigPolynomial( 73 | trig_fn="cos", 74 | coefficients=transformed_integrand, 75 | arguments=(left * 2 - right * 4) 76 | ), 77 | TrigPolynomial( 78 | trig_fn="cos", 79 | coefficients=transformed_integrand, 80 | arguments=(left * 2 + right * 4) 81 | ), 82 | TrigPolynomial( 83 | trig_fn="cos", 84 | coefficients=transformed_integrand * -1, 85 | arguments=(left * 4 - right * 2) 86 | ), 87 | TrigPolynomial( 88 | trig_fn="cos", 89 | coefficients=transformed_integrand * -1, 90 | arguments=(left * 4 + right * 2) 91 | ), 92 | ] 93 | 94 | haar_form = sum([term.integrate(2, Polynomial.from_linear_list([0]), 95 | Polynomial.from_linear_list([1, -1, -1])) 96 | for term in haar_form], []) 97 | haar_form = sum([term.integrate(1, Polynomial.from_linear_list([0]), 98 | Polynomial.from_linear_list([1, -1])) 99 | for term in haar_form], []) 100 | haar_form = sum([term.integrate(0, Polynomial.from_linear_list([0]), 101 | Polynomial.from_linear_list([1])) 102 | for term in haar_form], []) 103 | 104 | return abs(determinant) / (2 / 3 * np.pi) * \ 105 | sum(term.to_number() for term in haar_form) 106 | 107 | 108 | def _haar_volume_convex_polytope(convex_polytope, integrand=None): 109 | """ 110 | Integrates the PU(4) Haar form, expressed in positive canonical coordinates, 111 | over a 3D convex polytope. 112 | 113 | Takes an optional `integrand`, a polynomial expressed in (c1, c2, c3), to 114 | integrate against the Haar measure. 115 | """ 116 | 117 | vertices = [[np.pi * x for x in vertex] 118 | for vertex in convex_polytope.vertices] 119 | mapped_tetrahedra = list([list(vertices[index] for index in tetrahedron) 120 | for tetrahedron in convex_polytope.triangulation]) 121 | return sum([_haar_volume_tetrahedron(tetrahedron, integrand=integrand) 122 | for tetrahedron in mapped_tetrahedra]) 123 | 124 | 125 | def haar_volume(polytope, integrand=None): 126 | """ 127 | Integrates the PU(4) Haar form, expressed in positive canonical coordinates, 128 | over a 3D polytope. 129 | 130 | Takes an optional `integrand`, a polynomial expressed in (c1, c2, c3), to 131 | integrate against the Haar measure. 132 | """ 133 | def volume_fn(convex_polytope): 134 | return _haar_volume_convex_polytope(convex_polytope, integrand=integrand) 135 | 136 | return alternating_sum(polytope, volume_fn) 137 | 138 | 139 | def distance_polynomial_integrals( 140 | coverage_set: List[CircuitPolytopeData], 141 | max_degree=0, 142 | chatty=False 143 | ): 144 | """ 145 | Computes the integrals of dist^n dHaar over the "fresh" part of each member 146 | of `coverage_set` for exponent n in the range [0, max_degree]. Returns a 147 | dictionary mapping operations tuples from the `coverage_set` to a list of 148 | calculated integration values. 149 | """ 150 | # the nth moment is given by integrating the nth power of 151 | # min { l_1(-, (0, 0, 0)), l_1(-, (pi, 0, 0)) }, 152 | # which we split into the two integrals ("positive" and "negative") 153 | # depending on which min argument actually represents the minimum. 154 | positive_halfspace = make_convex_polytope( 155 | [[1, -2, 0, 0]], name="positive halfspace" 156 | ) 157 | negative_halfspace = make_convex_polytope( 158 | [[-1, 2, 0, 0]], name="negative halfspace" 159 | ) 160 | 161 | positive_polytopes_so_far = empty_polytope 162 | negative_polytopes_so_far = empty_polytope 163 | 164 | polynomial_averages = dict() 165 | 166 | if chatty: 167 | for degree in range(1 + max_degree): 168 | print(f" deg {degree}\t | ", end="") 169 | print("Sequence name") 170 | 171 | for polytope in coverage_set: 172 | polytope = monodromy_to_positive_canonical_polytope(polytope) 173 | positive_polytope = polytope.intersect(positive_halfspace).reduce() 174 | negative_polytope = polytope.intersect(negative_halfspace).reduce() 175 | positive_complementary_polytope = positive_polytopes_so_far \ 176 | .intersect(positive_polytope).reduce() 177 | negative_complementary_polytope = negative_polytopes_so_far \ 178 | .intersect(negative_polytope).reduce() 179 | 180 | # could reuse these, but probably as cheap to recreate them 181 | positive_polynomial_form = Polynomial.from_linear_list([1]) 182 | negative_polynomial_form = Polynomial.from_linear_list([1]) 183 | polynomial_averages[tuple(polytope.operations)] = [] 184 | for degree in range(1 + max_degree): 185 | integral = ( 186 | haar_volume(positive_polytope, positive_polynomial_form) 187 | + haar_volume(negative_polytope, negative_polynomial_form) 188 | - haar_volume(positive_complementary_polytope, positive_polynomial_form) 189 | - haar_volume(negative_complementary_polytope, negative_polynomial_form) 190 | ) 191 | polynomial_averages[tuple(polytope.operations)].append(integral) 192 | 193 | if chatty: 194 | print(f"{integral:5.5f}\t | ", end="") 195 | 196 | # update the polynomial forms 197 | positive_polynomial_form = positive_polynomial_form * \ 198 | Polynomial.from_linear_list([0, 1, 1, 1]) 199 | negative_polynomial_form = negative_polynomial_form * \ 200 | Polynomial.from_linear_list([np.pi, -1, 1, 1]) 201 | 202 | if chatty: 203 | print(f"{'.'.join(polytope.operations)}") 204 | 205 | positive_polytopes_so_far = positive_polytopes_so_far.union(positive_polytope).reduce() 206 | negative_polytopes_so_far = negative_polytopes_so_far.union(negative_polytope).reduce() 207 | 208 | return polynomial_averages 209 | 210 | 211 | def expected_cost(coverage_set, chatty=False): 212 | """ 213 | Calculates the expected cost, using the Haar measure, of a `coverage_set` 214 | expressed in monodromy coordinates. 215 | """ 216 | 217 | integrals = distance_polynomial_integrals(coverage_set, chatty=chatty) 218 | expected_cost = 0 219 | 220 | for polytope in coverage_set: 221 | expected_cost += polytope.cost * integrals[tuple(polytope.operations)][0] 222 | 223 | return expected_cost 224 | 225 | 226 | def cost_statistics(coverage_set, offset, scale_factor, chatty=False): 227 | """ 228 | Calculates a variety of summary statistics involving the expected cost, as 229 | in the Haar measure, of a `coverage_set` expressed in monodromy coordinates. 230 | 231 | Assumes an affine-linear cost model for operation infidelity in interaction 232 | strength (cf. `optimize.py`): 233 | 234 | cost = (# interactions) * offset + (interaction total) * scale_factor . 235 | """ 236 | 237 | polynomial_integrals = distance_polynomial_integrals( 238 | coverage_set, max_degree=2, chatty=chatty, 239 | ) 240 | 241 | average_cost = 0 242 | square_sigma_cost = 0 243 | square_sigma_overshot = 0 244 | 245 | for polytope in coverage_set: 246 | # these are the integrals of dist^0, dist^1, and dist^2 over the 247 | # subregion of p which is cost-minimal. 248 | d0, d1, d2 = polynomial_integrals[tuple(polytope.operations)] 249 | 250 | # if this region is negligible, neglect it. 251 | if abs(d0) < epsilon: 252 | continue 253 | 254 | average_cost += d0 * polytope.cost 255 | 256 | square_sigma_cost += d0 * polytope.cost ** 2 257 | 258 | # sigma_overshot**2 = int (overshot - average overshot)**2 dHaar. expand 259 | # overshot as a difference, then expand that in powers of the distance 260 | # functional. 261 | square_sigma_overshot += d0 * (polytope.cost - 3 * offset) ** 2 262 | square_sigma_overshot += d1 * (-2 * (polytope.cost - 3 * offset) * scale_factor * 2 / np.pi) 263 | square_sigma_overshot += d2 * (scale_factor * 2 / np.pi) ** 2 264 | 265 | # postprocessing 266 | average_overshot = average_cost - (3 * offset + 3 / 2 * scale_factor) 267 | 268 | square_sigma_cost = square_sigma_cost - average_cost ** 2 269 | square_sigma_overshot = square_sigma_overshot - average_overshot ** 2 270 | if 0 > square_sigma_cost > -1e-10: 271 | square_sigma_cost = 0 272 | if 0 > square_sigma_overshot > -1e-10: 273 | square_sigma_overshot = 0 274 | 275 | return { 276 | "average_cost": average_cost, 277 | "average_overshot": average_overshot, 278 | "sigma_cost": sqrt(square_sigma_cost), 279 | "sigma_overshot": sqrt(square_sigma_overshot), 280 | } 281 | 282 | 283 | # Here is a `sympy` version that I wish I could use instead of all of 284 | # `polynomials.py`. 285 | # 286 | # def _haar_volume_tetrahedron(tetrahedron): 287 | # """ 288 | # Integrates the PU(4) Haar form over a 3D tetrahedron. 289 | # """ 290 | # import sympy 291 | # 292 | # ((x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3)) = tetrahedron 293 | # x, y, z = sympy.symbols("x y z") 294 | # 295 | # c1 = x0 + x * x1 + y * x2 + z * x3 296 | # c2 = y0 + x * y1 + y * y2 + z * y3 297 | # c3 = z0 + x * z1 + y * z2 + z * z3 298 | # 299 | # expr = sum( 300 | # [sympy.cos(2 * left - 4 * right) + sympy.cos(2 * left + 4 * right) 301 | # for left, right in [(c1, c2), (c2, c3), (c3, c1)]]) 302 | # expr -= sum( 303 | # [sympy.cos(4 * left - 2 * right) + sympy.cos(4 * left + 2 * right) 304 | # for left, right in [(c1, c2), (c2, c3), (c3, c1)]]) 305 | # expr = sympy.integrate(expr, (z, 0, 1 - x - y)) 306 | # expr = sympy.integrate(expr, (y, 0, 1 - x)) 307 | # expr = sympy.integrate(expr, (x, 0, 1)) 308 | # return expr.evalf() 309 | -------------------------------------------------------------------------------- /monodromy/io/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ConvexPolytopeData, PolytopeData, CircuitPolytopeData 2 | -------------------------------------------------------------------------------- /monodromy/io/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/io/base.py 3 | 4 | Bare dataclasses which house polytope information. 5 | """ 6 | 7 | from dataclasses import dataclass, field 8 | from typing import List 9 | 10 | 11 | anonymous_convex_polytope_counter = 0 12 | 13 | 14 | def generate_anonymous_cp_name(): 15 | global anonymous_convex_polytope_counter 16 | anonymous_convex_polytope_counter += 1 17 | return f"anonymous_convex_polytope_{anonymous_convex_polytope_counter}" 18 | 19 | 20 | @dataclass 21 | class ConvexPolytopeData: 22 | """ 23 | The raw data underlying a ConvexPolytope. Describes a single convex 24 | polytope, specified by families of `inequalities` and `equalities`, each 25 | entry of which respectively corresponds to 26 | 27 | inequalities[j][0] + sum_i inequalities[j][i] * xi >= 0 28 | 29 | and 30 | 31 | equalities[j][0] + sum_i equalities[j][i] * xi == 0. 32 | """ 33 | 34 | inequalities: List[List[int]] 35 | equalities: List[List[int]] = field(default_factory=list) 36 | name: str = field(default_factory=generate_anonymous_cp_name) 37 | 38 | @classmethod 39 | def inflate(cls, data): 40 | """ 41 | Converts the `data` produced by `dataclasses.asdict` to a live object. 42 | """ 43 | 44 | return cls(**data) 45 | 46 | 47 | @dataclass 48 | class PolytopeData: 49 | """ 50 | The raw data of a union of convex polytopes. 51 | """ 52 | 53 | convex_subpolytopes: List[ConvexPolytopeData] 54 | 55 | @classmethod 56 | def inflate(cls, data): 57 | """ 58 | Converts the `data` produced by `dataclasses.asdict` to a live object. 59 | """ 60 | 61 | data = { 62 | **data, 63 | # overrides 64 | "convex_subpolytopes": [ 65 | ConvexPolytopeData.inflate(x) if isinstance(x, dict) else x 66 | for x in data["convex_subpolytopes"] 67 | ] 68 | } 69 | 70 | return cls(**data) 71 | 72 | 73 | @dataclass 74 | class CircuitPolytopeData(PolytopeData): 75 | """ 76 | A polytope describing the alcove coverage of a particular circuit type. 77 | """ 78 | cost: float 79 | operations: List[str] 80 | -------------------------------------------------------------------------------- /monodromy/io/lrcalc.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/io/lrcalc.py 3 | 4 | Extracts quantum Littlewood-Richardson coefficients from the package `lrcalc`. 5 | 6 | This package is cumbersome to install, so we provide a prebaked copy of this 7 | table in `qlr_table.py`. 8 | """ 9 | 10 | from copy import copy 11 | 12 | import lrcalc 13 | 14 | 15 | def qlr(r, k, a, b): 16 | """ 17 | Computes the quantum Littlewood-Richardson coefficients N_{ab}^{c, d} in the 18 | small quantum cohomology ring of the Grassmannian Gr(r, k). For supplied 19 | a and b, this computes the set of c and d for which N = 1. 20 | 21 | Returns a dictionary of the form {c: d} over values where N_ab^{c, d} = 1. 22 | """ 23 | return { 24 | tuple(list(c) + [0]*(r - len(c))): 25 | (sum(a) + sum(b) - sum(c)) // (r + k) 26 | for c, value in lrcalc.mult_quantum(a, b, r, k).items() if value == 1 27 | } 28 | 29 | 30 | def displacements(r, k, skip_to=None): 31 | """ 32 | Iterates over the ordered sequence of partitions of total size `r + k` into 33 | `r` parts, presented as displacements from the terminal partiiton. 34 | 35 | If `skip_to` is supplied, start enumeration from this element. 36 | """ 37 | def normalize(p, r, k): 38 | """ 39 | Roll the odometer `p` over until it becomes a legal (`r`, `k`) 40 | displacement. 41 | """ 42 | if p[0] > k: 43 | return None 44 | for index, (item, next_item) in enumerate(zip(p, p[1:])): 45 | if next_item > item: 46 | p[index+1] = 0 47 | p[index] += 1 48 | return normalize(p, r, k) 49 | return p 50 | 51 | ok = skip_to is None 52 | p = [0 for j in range(0, r)] 53 | while p is not None: 54 | if p == skip_to: 55 | ok = True 56 | if ok: 57 | yield copy(p) 58 | p[-1] += 1 59 | p = normalize(p, r, k) 60 | 61 | 62 | def regenerate_qlr_table(): 63 | """ 64 | Uses `lrcalc` to rebuild the table stored in `qlr_table.py`. 65 | """ 66 | qlr_table = [] # [[r, k, [*a], [*b], [*c], d], ...] 67 | for r in range(1, 4): 68 | k = 4 - r 69 | # r bounds the length; k bounds the contents 70 | for a in displacements(r, k): 71 | for b in displacements(r, k, skip_to=a): 72 | for c, d in qlr(r, k, a, b).items(): 73 | qlr_table.append([r, k, a, b, list(c), d]) 74 | return qlr_table 75 | -------------------------------------------------------------------------------- /monodromy/polynomials.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/polynomials.py 3 | 4 | Calculus with symbolic (trigonometric) polynomials. 5 | 6 | NOTE: My strong preference would have been to use something like sympy, but I 7 | couldn't make it performant. :( See the bottom of haar.py for the kind 8 | of thing I need to be able to do. 9 | """ 10 | 11 | from collections import defaultdict 12 | from copy import copy 13 | from dataclasses import dataclass, field 14 | from itertools import zip_longest 15 | import math 16 | from numbers import Real 17 | from typing import Dict, Tuple 18 | 19 | from .utilities import epsilon 20 | 21 | 22 | def canonicalize_power_tuple(power_tuple): 23 | """ 24 | Entries in `Polynomial` are stored as (exponent tuple): coefficient, where 25 | the exponent tuple is required not to have any trailing zeroes. This takes 26 | a tuple and rewrites it into that form. 27 | """ 28 | while len(power_tuple) > 0 and power_tuple[-1] == 0: 29 | power_tuple = power_tuple[:-1] 30 | return power_tuple 31 | 32 | 33 | def get_from_power_tuple(power_tuple, index): 34 | """ 35 | Extracts an exponent from an exponent tuple that may have been canonicalized 36 | to be shorter than the expected length, by removing extra zeroes. 37 | """ 38 | if index >= len(power_tuple): 39 | return 0 40 | else: 41 | return power_tuple[index] 42 | 43 | 44 | @dataclass 45 | class Polynomial: 46 | """ 47 | Models a (multivariate) polynomial with fractional coefficients. 48 | """ 49 | 50 | coefficient_table: Dict[Tuple, Real] = field( 51 | default_factory=lambda: defaultdict(lambda: 0) 52 | ) 53 | 54 | @classmethod 55 | def from_coefficient_list(cls, coefficient_list): 56 | """ 57 | Converts a list [c0, c1, c2, ..., cn] to the polynomial 58 | 59 | c0 + c1 x0 + c2 x0^2 + ... + cn x0^n. 60 | """ 61 | polynomial = Polynomial() 62 | for power, value in enumerate(coefficient_list): 63 | power_tuple = canonicalize_power_tuple((power,)) 64 | polynomial.coefficient_table[power_tuple] = value 65 | return polynomial 66 | 67 | @classmethod 68 | def from_linear_list(cls, linear_list): 69 | """ 70 | Converts a list [k, d0, d1, ..., dn] to the polynomial 71 | 72 | k + d0 x0 + d1 x1 + ... + dn xn. 73 | """ 74 | polynomial = Polynomial() 75 | head, *linear_list = linear_list 76 | polynomial.coefficient_table[()] = head 77 | for place, value in enumerate(linear_list): 78 | key = ((0,) * place) + (1,) 79 | polynomial.coefficient_table[key] = value 80 | return polynomial 81 | 82 | def __add__(self, right): 83 | if not isinstance(right, Polynomial): 84 | right = Polynomial.from_linear_list([right]) 85 | 86 | polynomial = Polynomial(coefficient_table=copy(self.coefficient_table)) 87 | for k, v in right.coefficient_table.items(): 88 | polynomial.coefficient_table[k] += v 89 | 90 | return polynomial 91 | 92 | def __sub__(self, right): 93 | return self + (right * -1) 94 | 95 | def __mul__(self, right): 96 | if not isinstance(right, Polynomial): 97 | right = Polynomial.from_linear_list([right]) 98 | 99 | polynomial = Polynomial() 100 | for k, v in self.coefficient_table.items(): 101 | for kp, vp in right.coefficient_table.items(): 102 | kpp = tuple(x + y for x, y in zip_longest(k, kp, fillvalue=0)) 103 | polynomial.coefficient_table[kpp] += v * vp 104 | 105 | return polynomial 106 | 107 | def __str__(self): 108 | output = "" 109 | for k, v in self.coefficient_table.items(): 110 | if v == 0: 111 | continue 112 | output += str(v) 113 | if any([power != 0 for power in k]): 114 | output += f" * x^{k}" 115 | output += " + " 116 | 117 | return output[:-3] 118 | 119 | def __eq__(self, other): 120 | for k, v in self.coefficient_table.items(): 121 | if v != other.coefficient_table[k]: 122 | return False 123 | for k, v in other.coefficient_table.items(): 124 | if v != self.coefficient_table[k]: 125 | return False 126 | return True 127 | 128 | def evaluate(self, variable, value): 129 | """ 130 | Replaces the variable indexed at `variable` by the Polynomial `value`. 131 | """ 132 | value_powers = [1, value] 133 | 134 | evaluated_polynomial = Polynomial() 135 | 136 | for k, v in self.coefficient_table.items(): 137 | power = get_from_power_tuple(k, variable) 138 | 139 | while power >= len(value_powers): # extend value_powers as needed 140 | value_powers.append(value_powers[-1] * value) 141 | 142 | monomial = Polynomial() 143 | suppressed_key = canonicalize_power_tuple( 144 | k[:variable] + (0,) + k[variable + 1:] 145 | ) 146 | monomial.coefficient_table[suppressed_key] = v 147 | evaluated_polynomial += monomial * value_powers[power] 148 | 149 | return evaluated_polynomial 150 | 151 | def indefinite_integral(self, variable): 152 | """ 153 | Produces the indefinite integral against the variable indexed at 154 | `variable`. The result has vanishing constant term. 155 | """ 156 | integrated_polynomial = Polynomial() 157 | 158 | for k, v in self.coefficient_table.items(): 159 | if variable < len(k): 160 | shifted_key = k[:variable] + (1 + k[variable],) + k[1 + variable:] 161 | else: 162 | shifted_key = k + (0,) * (variable - len(k)) + (1,) 163 | integrated_polynomial.coefficient_table[shifted_key] = \ 164 | v / (shifted_key[variable]) 165 | 166 | return integrated_polynomial 167 | 168 | def definite_integral(self, var, lower, upper): 169 | """ 170 | Produces the definite integral against the variable indexed at 171 | `variable`, as integrated from `lower` to `upper`, which may themselves 172 | be `Polynomial` expressions. 173 | """ 174 | integrated_polynomial = self.indefinite_integral(var) 175 | 176 | return integrated_polynomial.evaluate(var, upper) - \ 177 | integrated_polynomial.evaluate(var, lower) 178 | 179 | def derivative(self, var): 180 | """ 181 | Produces the derivative against the variable indexed at `variable`. 182 | """ 183 | differentiated_polynomial = Polynomial() 184 | 185 | for k, v in self.coefficient_table.items(): 186 | if get_from_power_tuple(k, var) == 0: 187 | continue 188 | 189 | shifted_key = (*k[:var], -1 + k[var], *k[1 + var:]) 190 | shifted_key = canonicalize_power_tuple(shifted_key) 191 | differentiated_polynomial.coefficient_table[shifted_key] = k[var] * v 192 | 193 | return differentiated_polynomial 194 | 195 | def to_number(self): 196 | """ 197 | Extracts from a constant polynomial its literal constant value. 198 | """ 199 | for k, v in self.coefficient_table.items(): 200 | if k != () and abs(v) > epsilon: 201 | raise ValueError("Cannot convert a nonconstant to a number.") 202 | return self.coefficient_table[()] 203 | 204 | 205 | @dataclass 206 | class TrigPolynomial: 207 | """ 208 | Models a term of the form (multivar. poly.) * trig_fn(linear poly.), 209 | as arising in the expression for the Haar measure pushed forward to the 210 | positive canonical alcove. 211 | 212 | NOTE: This could be extended to handle multiplication of terms, since 213 | product-to-sum formulas preserve linearity in the argument, but not 214 | higher-degree arguments, since integrating those requires Fresnel 215 | integrals. 216 | """ 217 | 218 | coefficients: Polynomial 219 | arguments: Polynomial 220 | trig_fn: str = "cos" # in ["sin", "cos"] 221 | 222 | def __mul__(self, other): 223 | return TrigPolynomial( 224 | trig_fn=self.trig_fn, 225 | arguments=self.arguments, 226 | coefficients=(self.coefficients * other), 227 | ) 228 | 229 | def __str__(self): 230 | return f"[{self.coefficients}]{self.trig_fn}({self.arguments})" 231 | 232 | def evaluate(self, variable, value): 233 | """ 234 | Replaces the variable indexed at `variable` by the (linear) Polynomial 235 | `value`. 236 | """ 237 | return TrigPolynomial( 238 | trig_fn=self.trig_fn, 239 | arguments=self.arguments.evaluate(variable, value), 240 | coefficients=self.coefficients.evaluate(variable, value), 241 | ) 242 | 243 | def integrate(self, variable, lower, upper): # -> List[TrigPolynomial] 244 | """ 245 | Produces the definite integral against the variable indexed at 246 | `variable`, as integrated from `lower` to `upper`, which may themselves 247 | be (linear) `Polynomial` expressions. 248 | """ 249 | linear_coefficient = sum( 250 | [v if 1 == get_from_power_tuple(k, variable) else 0 251 | for k, v in self.arguments.coefficient_table.items()]) 252 | args_constant_p = abs(linear_coefficient) < epsilon 253 | coeffs_constant_p = all( 254 | [0 == get_from_power_tuple(k, variable) or abs(v) < epsilon 255 | for k, v in self.coefficients.coefficient_table.items()]) 256 | 257 | # base case 1: a bare polynomial 258 | if args_constant_p: 259 | integrated_coefficients = self.coefficients.definite_integral( 260 | variable, lower, upper 261 | ) 262 | return [TrigPolynomial( 263 | trig_fn=self.trig_fn, 264 | arguments=self.arguments, # no need to integrate this constant 265 | coefficients=integrated_coefficients 266 | )] 267 | 268 | if self.trig_fn == "sin": 269 | linear_coefficient *= -1 270 | trig_integral = TrigPolynomial( 271 | trig_fn="sin" if self.trig_fn == "cos" else "cos", 272 | arguments=self.arguments, 273 | coefficients=self.coefficients * (1 / linear_coefficient), 274 | ) 275 | 276 | # base case 2: a bare trig function 277 | if coeffs_constant_p: 278 | return [ 279 | trig_integral.evaluate(variable, upper), 280 | trig_integral.evaluate(variable, lower) * -1 281 | ] 282 | 283 | # recursive case: integrate by parts 284 | return [ 285 | trig_integral.evaluate(variable, upper), 286 | trig_integral.evaluate(variable, lower) * -1, 287 | *(TrigPolynomial( 288 | coefficients=trig_integral.coefficients.derivative(variable) \ 289 | * -1, 290 | trig_fn=trig_integral.trig_fn, 291 | arguments=trig_integral.arguments, 292 | ).integrate(variable, lower, upper)) 293 | ] 294 | 295 | def to_number(self): 296 | """ 297 | Extracts from a constant trig polynomial its literal constant value. 298 | """ 299 | if self.trig_fn == "sin": 300 | return self.coefficients.to_number() * math.sin( 301 | self.arguments.to_number()) 302 | elif self.trig_fn == "cos": 303 | return self.coefficients.to_number() * math.cos( 304 | self.arguments.to_number()) 305 | else: 306 | raise ValueError("Only sin and cos are supported trig functions.") 307 | -------------------------------------------------------------------------------- /monodromy/polytopes.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/polytopes.py 3 | 4 | Basic data structures for manipulating (non/convex) polytopes. 5 | """ 6 | 7 | from copy import copy 8 | from dataclasses import dataclass 9 | from fractions import Fraction 10 | from typing import List, Optional 11 | 12 | import monodromy.backend 13 | from monodromy.exceptions import NoFeasibleSolutions 14 | from monodromy.io.base import ConvexPolytopeData, PolytopeData, \ 15 | generate_anonymous_cp_name 16 | from monodromy.volume import alternating_sum 17 | from monodromy.utilities import clear_memoization, epsilon, memoized_property 18 | 19 | 20 | @dataclass(order=True) 21 | class PolytopeVolume: 22 | """ 23 | Represents the volume of a (possibly not top-dimensional) polytope. 24 | """ 25 | dimension: int 26 | volume: Fraction 27 | 28 | def __add__(self, other): 29 | if self.dimension > other.dimension: 30 | return self 31 | elif self.dimension < other.dimension: 32 | return other 33 | else: 34 | return PolytopeVolume( 35 | volume=self.volume + other.volume, 36 | dimension=self.dimension, 37 | ) 38 | 39 | def __sub__(self, other): 40 | if self.dimension > other.dimension: 41 | return self 42 | elif self.dimension == other.dimension: 43 | return PolytopeVolume( 44 | dimension=self.dimension, 45 | volume=self.volume - other.volume, 46 | ) 47 | else: 48 | raise ValueError(f"Illegal to subtract high dim'l volume " 49 | f"from low dim'l source.") 50 | 51 | 52 | @dataclass 53 | class ConvexPolytope(ConvexPolytopeData): 54 | """ 55 | Houses a single convex polytope, together with methods for manipulation. 56 | 57 | NOTE: This object is meant to be read-only after instantiation. 58 | """ 59 | 60 | @memoized_property 61 | def volume(self) -> PolytopeVolume: 62 | """ 63 | (Top-dimensional) Euclidean volume of this convex body. 64 | """ 65 | try: 66 | return monodromy.backend.backend.volume(self) 67 | except NoFeasibleSolutions: 68 | return PolytopeVolume(dimension=0, volume=Fraction(0)) 69 | 70 | @memoized_property 71 | def vertices(self) -> List[List[Fraction]]: 72 | """ 73 | Set of extremal vertices of this convex body. 74 | """ 75 | try: 76 | return monodromy.backend.backend.vertices(self) 77 | except NoFeasibleSolutions: 78 | return [] 79 | 80 | @memoized_property 81 | def triangulation(self) -> List[List[int]]: 82 | """ 83 | Non-overlapping simplices which constitute this polytope, specified as 84 | tuples of indices into .vertices . 85 | """ 86 | if 0 == len(self.vertices): 87 | return [] 88 | return monodromy.backend.backend.triangulation(self) 89 | 90 | @classmethod 91 | def convex_hull(cls, vertices): 92 | """ 93 | Produces the minimal ConvexPolytope containing the list of `vertices`. 94 | """ 95 | return monodromy.backend.backend.convex_hull(vertices) 96 | 97 | def reduce(self): # -> ConvexPolytope 98 | """ 99 | Produces an equivalent convex body with irredundant inequalities. 100 | 101 | Raises NoFeasibleSolutions if the reduced polytope is empty. 102 | """ 103 | return monodromy.backend.backend.reduce(self) 104 | 105 | def __str__(self) -> str: 106 | output = f"# {self.name}: \n" 107 | for inequality in self.inequalities: 108 | output += f"{str(inequality[0]): >5}" 109 | for index, item in enumerate(inequality[1:]): 110 | output += f" + {str(item): >5} x{1+index}" 111 | output += " >= 0\n" 112 | 113 | for equality in self.equalities: 114 | output += f"{str(equality[0]): >5}" 115 | for index, item in enumerate(equality[1:]): 116 | output += f" + {str(item): >5} x{1+index}" 117 | output += " == 0\n" 118 | 119 | return output 120 | 121 | def intersect(self, other): # ConvexPolytope, ConvexPolytope -> ConvexPolytope 122 | """ 123 | Returns A cap B. 124 | """ 125 | return ConvexPolytope( 126 | inequalities=self.inequalities + other.inequalities, 127 | equalities=self.equalities + other.equalities, 128 | name=f"{self.name} ∩ {other.name}" 129 | ) 130 | 131 | def has_element(self, point) -> bool: 132 | """ 133 | Returns True when `point` belongs to `self`. 134 | """ 135 | return (all([-epsilon <= inequality[0] + 136 | sum(x * y for x, y in 137 | zip(point, inequality[1:])) 138 | for inequality in self.inequalities]) and 139 | all([abs(equality[0] + sum(x * y for x, y in 140 | zip(point, equality[1:]))) 141 | <= epsilon 142 | for equality in self.equalities])) 143 | 144 | def contains(self, other) -> bool: 145 | """ 146 | Returns True when this convex body is contained in the right-hand one. 147 | """ 148 | # NOTE: Alternatively, you could check volumes, as below. Also 149 | # alternatively, you could use .reduce() and check that the facet 150 | # definitions are the same (up to rescaling?). I think this is 151 | # the most efficient version, since it doesn't enumerate vertices? 152 | cap_vertices = other.intersect(self).vertices 153 | return all([v in cap_vertices for v in other.vertices]) 154 | 155 | 156 | @dataclass 157 | class Polytope(PolytopeData): 158 | """ 159 | A manipulable union of convex polytopes. 160 | 161 | NOTE: This object is meant to be read-only after instantiation. 162 | """ 163 | 164 | @classmethod 165 | def inflate(cls, data): 166 | """ 167 | Converts the `data` produced by `dataclasses.asdict` to a live object. 168 | """ 169 | 170 | data = { 171 | **data, 172 | "convex_subpolytopes": [ 173 | ConvexPolytope.inflate(x) if isinstance(x, dict) else x 174 | for x in data["convex_subpolytopes"] 175 | ], 176 | } 177 | 178 | return super().inflate(data) 179 | 180 | @memoized_property 181 | def volume(self) -> PolytopeVolume: 182 | """ 183 | Computes the Euclidean volume of this polytope. 184 | """ 185 | 186 | volumes = [cp.volume for cp in self.convex_subpolytopes] 187 | top_dimension = 0 if len(volumes) == 0 \ 188 | else max([volume.dimension for volume in volumes]) 189 | 190 | def unwrapped_volume(convex_polytope): 191 | if convex_polytope.volume.dimension == top_dimension: 192 | return convex_polytope.volume.volume 193 | else: 194 | return 0 195 | 196 | volume = alternating_sum( 197 | polytope=Polytope(convex_subpolytopes=[ 198 | cp for cp in self.convex_subpolytopes 199 | if cp.volume.dimension == top_dimension 200 | ]), 201 | volume_fn=unwrapped_volume, 202 | ) 203 | 204 | return PolytopeVolume(dimension=top_dimension, volume=volume) 205 | 206 | @memoized_property 207 | def vertices(self): 208 | """ 209 | Returns the vertices of the convex subpolytopes. 210 | """ 211 | return [convex_subpolytope.vertices 212 | for convex_subpolytope in self.convex_subpolytopes] 213 | 214 | def reduce(self): 215 | """ 216 | Removes redundant inequality sets from a Polytope. 217 | """ 218 | 219 | independent_polytopes = [] 220 | for convex_subpolytope in self.convex_subpolytopes: 221 | try: 222 | independent_polytopes.append(Polytope( 223 | convex_subpolytopes=[convex_subpolytope.reduce()] 224 | )) 225 | except NoFeasibleSolutions: 226 | pass 227 | 228 | independent_polytopes = trim_polytope_set(independent_polytopes) 229 | 230 | clone = copy(self) 231 | clone.convex_subpolytopes = [ 232 | independent_polytope.convex_subpolytopes[0] 233 | for independent_polytope in independent_polytopes 234 | ] 235 | 236 | return clone 237 | 238 | def union(self, other): 239 | """ 240 | Returns A cup B. 241 | """ 242 | clone = copy(self) 243 | clone.convex_subpolytopes = (self.convex_subpolytopes + 244 | other.convex_subpolytopes) 245 | clear_memoization(clone) 246 | return clone 247 | 248 | def intersect(self, other): 249 | """ 250 | Returns A cap B. 251 | """ 252 | # distribute the intersection over the union 253 | convex_subpolytopes = [] 254 | for left_subpolytope in self.convex_subpolytopes: 255 | for right_subpolytope in other.convex_subpolytopes: 256 | convex_subpolytopes.append(left_subpolytope.intersect( 257 | right_subpolytope 258 | )) 259 | 260 | clone = copy(self) 261 | clone.convex_subpolytopes = convex_subpolytopes 262 | clear_memoization(clone) 263 | return clone 264 | 265 | def __str__(self): 266 | output = "[\n" 267 | for index, item in enumerate(self.convex_subpolytopes): 268 | output += str(item) 269 | if 1 + index < len(self.convex_subpolytopes): 270 | output += "," 271 | output += "\n" 272 | output += "]" 273 | 274 | return output 275 | 276 | def has_element(self, point) -> bool: 277 | """ 278 | Returns T when point belongs to this Polytope. 279 | """ 280 | return any([cp.has_element(point) for cp in self.convex_subpolytopes]) 281 | 282 | def contains(self, other) -> bool: 283 | """ 284 | Returns True when the other polytope is contained in this one. 285 | """ 286 | # for n self.convex_subpolytopes and m other.convex_subpolytopes, 287 | # computing these volumes takes worst-case 2^m + 2^(nm) calls to lrs. 288 | # however, a necessary-but-insufficient condition for containment is 289 | # a containment of vertex sets, which takes only m + nm calls to lrs. 290 | # we check that first and short-circuit if it fails. 291 | 292 | for other_subvertices in other.vertices: 293 | for other_vertex in other_subvertices: 294 | if not self.has_element(other_vertex): 295 | return False 296 | 297 | # now do the expensive version that also handles sufficiency 298 | intersection = other.intersect(self) 299 | little_volume = other.volume 300 | cap_volume = intersection.volume 301 | return cap_volume == little_volume 302 | 303 | 304 | def trim_polytope_set( 305 | trimmable_polytopes: List[Polytope], 306 | fixed_polytopes: Optional[List[Polytope]] = None 307 | ) -> List[Polytope]: 308 | """ 309 | Reduce a family of `Polytope`s by removing those which are in the union of 310 | the rest. 311 | 312 | For flexibility, we break the input into two parts: a set of ConvexPolytopes 313 | which we're trying to trim, as well as a set of ConvexPolytopes which 314 | contribute to the notion of redundancy but which we don't attempt to reduce. 315 | 316 | Returns an irredundant subsequence from trimmable_polytopes. 317 | """ 318 | 319 | # NOTE: This is an expensive call, because testing for (convex) polytope 320 | # containment in a (nonconvex!!) polytope is tricky business. 321 | # There is absolutely room for improvement in performance here. 322 | 323 | fixed_polytope = Polytope(convex_subpolytopes=[]) 324 | if fixed_polytopes is not None: 325 | for polytope in fixed_polytopes: 326 | fixed_polytope = fixed_polytope.union(polytope) 327 | 328 | # sort them by volume, then traverse in ascending order 329 | trimmable_polytopes = sorted( 330 | trimmable_polytopes, 331 | key=lambda x: x.volume, 332 | reverse=True, 333 | ) 334 | for index in range(len(trimmable_polytopes) - 1, -1, -1): 335 | # pick a polytope, test whether it's contained in the others 336 | this_polytope = trimmable_polytopes[index] 337 | 338 | other_polytope = fixed_polytope 339 | for subindex, polytope in enumerate(trimmable_polytopes): 340 | if subindex == index: 341 | continue 342 | other_polytope = other_polytope.union(polytope) 343 | 344 | if other_polytope.contains(this_polytope): 345 | del trimmable_polytopes[index] 346 | 347 | return trimmable_polytopes 348 | 349 | 350 | def make_convex_polytope( 351 | inequalities: List[List[int]], 352 | equalities: Optional[List[List[int]]] = None, 353 | name: Optional[str] = None, 354 | ) -> Polytope: 355 | """ 356 | Convenience method for forming a Polytope with one component. 357 | """ 358 | equalities = equalities if equalities is not None else [] 359 | name = name if name is not None else generate_anonymous_cp_name() 360 | 361 | return Polytope(convex_subpolytopes=[ 362 | ConvexPolytope(inequalities=inequalities, 363 | equalities=equalities, 364 | name=name) 365 | ]) 366 | -------------------------------------------------------------------------------- /monodromy/render.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/render.py 3 | 4 | Utilities for rendering polytopes. 5 | """ 6 | 7 | from typing import List 8 | 9 | from monodromy.coverage import CircuitPolytope 10 | 11 | 12 | def polytopes_to_mathematica(necessary_polytopes: List[CircuitPolytope]): 13 | output = "" 14 | output += "polytopeData = {" 15 | for n, gate_polytope in enumerate(necessary_polytopes): 16 | output += "{" 17 | output += f"{float(gate_polytope.cost)}, " 18 | for i, polytope in enumerate(gate_polytope.convex_subpolytopes): 19 | output += "{" 20 | vertices = polytope.vertices 21 | for j, vertex in enumerate(vertices): 22 | output += "{" 23 | output += f"{vertex[0]}, {vertex[1]}, {vertex[2]}" 24 | if 1 + j != len(vertices): 25 | output += "}, " 26 | else: 27 | output += "}" 28 | if 1 + i != len(gate_polytope.convex_subpolytopes): 29 | output += "}, " 30 | else: 31 | output += "}" 32 | if 1 + n != len(necessary_polytopes): 33 | output += "}, " 34 | else: 35 | output += "}" 36 | output += "};" 37 | 38 | output += """ 39 | corners = {{0, 0, 0}, {1/4, 1/4, 1/4}, {1/4, 1/4, -(1/4)}, {3/8, 3/ 40 | 8, -(1/8)}, {3/8, -(1/8), -(1/8)}, {1/2, 0, 0}}; 41 | names = {{0, 0, 0} -> "I", {1/4, 1/4, -1/4} -> "CZ", {1/2, 0, 0} -> 42 | "ISWAP", {1/4, 1/4, 1/4} -> "SWAP", {3/8, 3/8, -1/8} -> Sqrt[ 43 | SWAP], {3/8, -1/8, -1/8} -> Sqrt[SWAP]'}; 44 | 45 | (* tune this loop bound to skip degenerate solids *) 46 | skipTo := 6 47 | (* tune these scalars to get a picture w/o Z-fighting *) 48 | OffsetCoords[coord_, n_] := ((1 + 0.5^n) (coord - {0.25, 0.1, -0.1})) 49 | 50 | Module[{directives = {}, n, depth, vertices, maxdepth}, 51 | maxdepth = Max[First /@ polytopeData]; 52 | For[n = skipTo, n <= Length[polytopeData], n++, 53 | (* inject new color settings *) 54 | depth = polytopeData[[n, 1]]; 55 | vertices = Rest[polytopeData[[n]]]; 56 | directives = Join[directives, { 57 | Hue[(depth - skipTo)/maxdepth], 58 | Opacity[1.0 - (depth - skipTo)/maxdepth] 59 | }]; 60 | (* add new polyhedra *) 61 | directives = Join[directives, 62 | With[{mesh = ConvexHullMesh[OffsetCoords[#, n] & /@ #]}, 63 | GraphicsComplex[ 64 | MeshCoordinates[mesh], {EdgeForm[], MeshCells[mesh, 2]}]] & /@ 65 | vertices]; 66 | ]; 67 | directives]; 68 | Show[Graphics3D[Lighting -> "Neutral", Boxed -> False], 69 | Graphics3D@(Join @@ 70 | Table[{Sphere[OffsetCoords[corner, skipTo], 0.02], 71 | Text[corner /. names, 72 | OffsetCoords[corner, 5 skipTo] + 73 | 0.05*If[Norm[corner] == 0, 0, corner/Norm[corner]]]}, {corner, 74 | corners}]), 75 | Graphics3D[%, Boxed -> False, ViewPoint -> {0, 1, 1}, 76 | Lighting -> {{"Ambient", White}}]] 77 | """ 78 | 79 | return output 80 | -------------------------------------------------------------------------------- /monodromy/static/__init__.py: -------------------------------------------------------------------------------- 1 | from .examples import everything_polytope, exactly, identity_polytope 2 | from .matrices import canonical_matrix 3 | from .qlr_table import qlr_polytope 4 | -------------------------------------------------------------------------------- /monodromy/static/examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/static/examples.py 3 | 4 | A variety of "standard" polytopes and gates. 5 | """ 6 | 7 | from ..polytopes import ConvexPolytope, make_convex_polytope, Polytope 8 | 9 | 10 | def exactly(*coordinates, name=None) -> Polytope: 11 | """ 12 | Produce a family of inequalities that forces equality with `coordinates`. 13 | """ 14 | table = [] 15 | for index, coordinate in enumerate(coordinates): 16 | row = [0] * (1 + len(coordinates)) 17 | row[0] = coordinate.numerator 18 | row[1 + index] = -coordinate.denominator 19 | table.append(row) 20 | return make_convex_polytope([], equalities=table, name=name) 21 | 22 | 23 | everything_polytope = Polytope(convex_subpolytopes=[ 24 | ConvexPolytope(inequalities=[], name="True") 25 | ]) 26 | """ 27 | The basic boolean "True" polytope: all points belong. 28 | 29 | NOTE: This polytope is dimensionless. 30 | """ 31 | 32 | 33 | empty_polytope = Polytope(convex_subpolytopes=[]) 34 | """ 35 | The basic boolean "False" polytope: no points belong. 36 | 37 | NOTE: This polytope is dimensionless. 38 | """ 39 | 40 | 41 | identity_polytope = Polytope(convex_subpolytopes=[ 42 | ConvexPolytope(inequalities=[], name="origin", equalities=[ 43 | [0, 1, 0, 0], 44 | [0, 0, 1, 0], 45 | [0, 0, 0, 1], 46 | ]) 47 | ]) 48 | """ 49 | A polytope containing only the canonical coordinate of the identity gate, i.e., 50 | the origin in 3-space. 51 | """ 52 | 53 | 54 | # # some parametric gates of interest 55 | # CPHASE_polytope = make_convex_polytope([ 56 | # [0, 1, -1, 0,], # x1 == x2 57 | # [0, -1, 1, 0,], 58 | # [0, 0, 1, 1,], # x2 == -x3 59 | # [0, 0, -1, -1,], 60 | # *monodromy_alcove_c2.convex_subpolytopes[0].inequalities, 61 | # ]) 62 | # XY_polytope = make_convex_polytope([ 63 | # [0, 0, 1, 0], # x2 == 0 64 | # [0, 0, -1, 0], 65 | # [0, 0, 0, 1], # x3 == 0 66 | # [0, 0, 0, -1], 67 | # *monodromy_alcove_c2.convex_subpolytopes[0].inequalities, 68 | # ]) 69 | # 70 | # 71 | # # some other gates of interest 72 | # sqrtCX_polytope = exactly(Fraction(1, 8), Fraction(1, 8), Fraction(-1, 8)) 73 | # thirdCX_polytope = exactly(Fraction(1, 12), Fraction(1, 12), Fraction(-1, 12)) 74 | -------------------------------------------------------------------------------- /monodromy/static/matrices.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/static/matrices.py 3 | 4 | Matrix representations of various standard gates. 5 | """ 6 | 7 | import numpy as np 8 | 9 | 10 | def canonical_matrix(a=0, b=0, c=0): 11 | """The canonical operator exp(-i(a XX + b YY + c ZZ)).""" 12 | cplus, cminus = np.cos(a + b), np.cos(a - b) 13 | splus, sminus = np.sin(a + b), np.sin(a - b) 14 | eic = np.exp(1j * c) 15 | 16 | return np.array([ 17 | [ cminus * eic, 0, 0, -1j * sminus * eic, ], 18 | [ 0, cplus / eic, -1j * splus / eic, 0, ], 19 | [ 0, -1j * splus / eic, cplus / eic, 0, ], 20 | [-1j * sminus * eic, 0, 0, cminus * eic, ], 21 | ]) 22 | 23 | 24 | def rx_matrix(theta=0): 25 | """The rotation operator exp(-i theta X).""" 26 | return np.array([ 27 | [ np.cos(theta), -1j * np.sin(theta), ], 28 | [-1j * np.sin(theta), np.cos(theta), ], 29 | ]) 30 | 31 | 32 | def ry_matrix(theta=0): 33 | """The rotation operator exp(-i theta Y).""" 34 | return np.array([ 35 | [np.cos(theta), -np.sin(theta), ], 36 | [np.sin(theta), np.cos(theta), ], 37 | ]) 38 | 39 | 40 | def rz_matrix(theta=0): 41 | """The rotation operator exp(-i theta Z).""" 42 | return np.array([ 43 | [np.exp(1j * -theta), 0, ], 44 | [ 0, np.exp(1j * theta), ], 45 | ]) 46 | -------------------------------------------------------------------------------- /monodromy/static/qlr_table.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/static/qlr_table.py 3 | 4 | Precomputed descriptions of the monodromy polytope for SU(4) (and PU(4)). 5 | """ 6 | 7 | from ..polytopes import make_convex_polytope 8 | 9 | 10 | # r k a b c d 11 | qlr_table = [[1, 3, [0], [0], [0], 0], 12 | [1, 3, [0], [1], [1], 0], 13 | [1, 3, [0], [2], [2], 0], 14 | [1, 3, [0], [3], [3], 0], 15 | [1, 3, [1], [1], [2], 0], 16 | [1, 3, [1], [2], [3], 0], 17 | [1, 3, [1], [3], [0], 1], 18 | [1, 3, [2], [2], [0], 1], 19 | [1, 3, [2], [3], [1], 1], 20 | [1, 3, [3], [3], [2], 1], 21 | # r k a b c d 22 | [2, 2, [0, 0], [0, 0], [0, 0], 0], 23 | [2, 2, [0, 0], [1, 0], [1, 0], 0], 24 | [2, 2, [0, 0], [1, 1], [1, 1], 0], 25 | [2, 2, [0, 0], [2, 0], [2, 0], 0], 26 | [2, 2, [0, 0], [2, 1], [2, 1], 0], 27 | [2, 2, [0, 0], [2, 2], [2, 2], 0], 28 | [2, 2, [1, 0], [1, 0], [1, 1], 0], 29 | [2, 2, [1, 0], [1, 0], [2, 0], 0], 30 | [2, 2, [1, 0], [1, 1], [2, 1], 0], 31 | [2, 2, [1, 0], [2, 0], [2, 1], 0], 32 | [2, 2, [1, 0], [2, 1], [2, 2], 0], 33 | [2, 2, [1, 0], [2, 1], [0, 0], 1], 34 | [2, 2, [1, 0], [2, 2], [1, 0], 1], 35 | [2, 2, [1, 1], [1, 1], [2, 2], 0], 36 | [2, 2, [1, 1], [2, 0], [0, 0], 1], 37 | [2, 2, [1, 1], [2, 1], [1, 0], 1], 38 | [2, 2, [1, 1], [2, 2], [2, 0], 1], 39 | [2, 2, [2, 0], [2, 0], [2, 2], 0], 40 | [2, 2, [2, 0], [2, 1], [1, 0], 1], 41 | [2, 2, [2, 0], [2, 2], [1, 1], 1], 42 | [2, 2, [2, 1], [2, 1], [2, 0], 1], 43 | [2, 2, [2, 1], [2, 1], [1, 1], 1], 44 | [2, 2, [2, 1], [2, 2], [2, 1], 1], 45 | [2, 2, [2, 2], [2, 2], [0, 0], 2], 46 | # r k a b c d 47 | [3, 1, [0, 0, 0], [0, 0, 0], [0, 0, 0], 0], 48 | [3, 1, [0, 0, 0], [1, 0, 0], [1, 0, 0], 0], 49 | [3, 1, [0, 0, 0], [1, 1, 0], [1, 1, 0], 0], 50 | [3, 1, [0, 0, 0], [1, 1, 1], [1, 1, 1], 0], 51 | [3, 1, [1, 0, 0], [1, 0, 0], [1, 1, 0], 0], 52 | [3, 1, [1, 0, 0], [1, 1, 0], [1, 1, 1], 0], 53 | [3, 1, [1, 0, 0], [1, 1, 1], [0, 0, 0], 1], 54 | [3, 1, [1, 1, 0], [1, 1, 0], [0, 0, 0], 1], 55 | [3, 1, [1, 1, 0], [1, 1, 1], [1, 0, 0], 1], 56 | [3, 1, [1, 1, 1], [1, 1, 1], [1, 1, 0], 1]] 57 | """ 58 | Precomputed table of quantum Littlewood-Richardson coefficients for the small 59 | quantum cohomology ring of k-planes in C^4, 0 < k < 4. Each entry is of the 60 | form [r, k, [*a], [*b], [*c], d], corresponding to the relation 61 | 62 | N_{ab}^{c, d} = 1 or = q^d. 63 | 64 | NOTE: We include only entries with a <= b in the traversal ordering used by 65 | `monodromy.io.lrcalc.displacements`. 66 | 67 | NOTE: This table can be regenerated using 68 | `monodromy.io.lrcalc.regenerate_qlr_table` . 69 | """ 70 | 71 | 72 | def ineq_from_qlr(r, k, a, b, c, d): 73 | """ 74 | Generates a monodromy polytope inequality from the position of a nonzero 75 | quantum Littlewood-Richardson coefficient for su_4. 76 | 77 | See (*) in Theorem 23 of /1904.10541 . 78 | 79 | NOTE: `r` is ignored, since `4 = r + k` makes it redundant with `k`. 80 | """ 81 | 82 | # $$d - \sum_{i=1}^r \alpha_{k+i-a_i} 83 | # - \sum_{i=1}^r \beta_{k+i-b_i} 84 | # + \sum_{i=1}^r \delta_{k+i-c_i} \ge 0$$ 85 | 86 | new_row = [d, 87 | 0, 0, 0, 0, # alpha's 88 | 0, 0, 0, 0, # beta's 89 | 0, 0, 0, 0, ] # gamma's 90 | for i, ai in enumerate(a): 91 | index = k + (i + 1) - ai # subscript in the Biswas inequality 92 | offset = 0 # last entry before alpha 93 | new_row[index + offset] -= 1 # poke the value in 94 | for i, bi in enumerate(b): 95 | index = k + (i + 1) - bi # subscript in the Biswas inequality 96 | offset = 4 # last entry before beta 97 | new_row[index + offset] -= 1 # poke the value in 98 | for i, ci in enumerate(c): 99 | index = k + (i + 1) - ci # subscript in the Biswas inequality 100 | offset = 8 # last entry before gamma 101 | new_row[index + offset] += 1 # poke the value in 102 | 103 | # now remember that a4 = -a1-a2-a3 and so on 104 | new_row = [new_row[0], 105 | *[x - new_row[4] for x in new_row[1:4]], 106 | *[x - new_row[8] for x in new_row[5:8]], 107 | *[x - new_row[12] for x in new_row[9:12]] 108 | ] 109 | 110 | return new_row 111 | 112 | 113 | def generate_qlr_inequalities(): 114 | """ 115 | Regenerates the set of monodromy polytope inequalities from the stored table 116 | `qlr_table` of quantum Littlewood-Richardson coefficients. 117 | """ 118 | qlr_inequalities = [] 119 | for r, k, a, b, c, d in qlr_table: 120 | qlr_inequalities.append(ineq_from_qlr(r, k, a, b, c, d)) 121 | if a != b: 122 | qlr_inequalities.append(ineq_from_qlr(r, k, b, a, c, d)) 123 | 124 | return qlr_inequalities 125 | 126 | 127 | qlr_polytope = make_convex_polytope( 128 | generate_qlr_inequalities(), 129 | name="QLR relations" 130 | ) 131 | """ 132 | This houses the monodromy polytope, the main static input of the whole calc'n. 133 | This polytope does _not_ also contain the alcove constraints. 134 | """ 135 | -------------------------------------------------------------------------------- /monodromy/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/utilities.py 3 | 4 | Depository for generic python utility snippets. 5 | """ 6 | 7 | 8 | from fractions import Fraction 9 | from functools import wraps 10 | from typing import List 11 | import warnings 12 | 13 | import numpy as np 14 | 15 | 16 | epsilon = 1e-6 # Fraction(1, 1_000_000) 17 | 18 | 19 | memoized_attr_bucket = '_memoized_attrs' 20 | 21 | 22 | def memoized_property(fget): 23 | attr_name = f'_{fget.__name__}' 24 | 25 | @wraps(fget) 26 | def fget_memoized(self): 27 | if not hasattr(self, attr_name): 28 | setattr(self, attr_name, fget(self)) 29 | if hasattr(self, memoized_attr_bucket): 30 | getattr(self, memoized_attr_bucket).append(attr_name) 31 | else: 32 | setattr(self, memoized_attr_bucket, [attr_name]) 33 | return getattr(self, attr_name) 34 | 35 | return property(fget_memoized) 36 | 37 | 38 | def clear_memoization(obj): 39 | if hasattr(obj, memoized_attr_bucket): 40 | for field in getattr(obj, memoized_attr_bucket): 41 | delattr(obj, field) 42 | delattr(obj, memoized_attr_bucket) 43 | return obj 44 | 45 | 46 | # https://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation 47 | def bit_iteration(length, weight): 48 | """Iterate over bitpatterns of size `length` with `weight` bits flagged.""" 49 | if weight == 0: 50 | yield 0 51 | return 52 | 53 | pattern = 2 ** weight - 1 54 | while pattern < 2 ** length: 55 | yield pattern 56 | t = (pattern | (pattern - 1)) + 1 57 | pattern = t | ((((t & -t) // (pattern & -pattern)) >> 1) - 1) 58 | 59 | 60 | def bitcount(bits): 61 | return bin(bits).count('1') 62 | 63 | 64 | def bitscatter(bits, mask): 65 | """ 66 | Scatters the contents of bitvector `bits` onto the raised bits in `mask`. 67 | """ 68 | value = 0 69 | mask_walker = enumerate(reversed(bin(mask)[2:])) 70 | for bit_index, mask_index in enumerate([x for x, y in mask_walker if y == '1']): 71 | value |= (bits & (1 << bit_index)) << (mask_index - bit_index) 72 | return value 73 | 74 | 75 | def fractionify(table) -> List[List[Fraction]]: 76 | """ 77 | Convenience routine for not writing Fraction() a whole bunch. 78 | 79 | NOTE: This can be poorly behaved if your rationals don't have exact floating 80 | point representations! 81 | """ 82 | return [[Fraction(i) for i in j] for j in table] 83 | 84 | 85 | def lcm(*numbers): 86 | import math 87 | 88 | assert 1 <= len(numbers) 89 | ret = numbers[0] 90 | for number in numbers[1:]: 91 | ret = ret * number // math.gcd(ret, number) 92 | return ret 93 | 94 | 95 | def nearp(x, y, modulus=np.pi/2, epsilon=epsilon): 96 | """ 97 | Checks whether two points are near each other, accounting for float jitter 98 | and wraparound. 99 | """ 100 | return abs(np.mod(abs(x - y), modulus)) < epsilon or \ 101 | abs(np.mod(abs(x - y), modulus) - modulus) < epsilon 102 | 103 | 104 | def l1_distance(x, y): 105 | """ 106 | Computes the l_1 / Manhattan distance between two coordinates. 107 | """ 108 | return sum([abs(xx - yy) for xx, yy in zip(x, y)]) 109 | 110 | 111 | # TODO: THIS IS A STOPGAP!!! 112 | def safe_arccos(numerator, denominator): 113 | """ 114 | Computes arccos(n/d) with different (better?) numerical stability. 115 | """ 116 | threshold = 0.005 117 | 118 | if abs(numerator) > abs(denominator) and \ 119 | abs(numerator - denominator) < threshold: 120 | return 0.0 121 | elif abs(numerator) > abs(denominator) and \ 122 | abs(numerator + denominator) < threshold: 123 | return np.pi 124 | else: 125 | with warnings.catch_warnings(): 126 | warnings.filterwarnings("ignore", category=RuntimeWarning) 127 | return np.arccos(numerator / denominator) 128 | -------------------------------------------------------------------------------- /monodromy/volume.py: -------------------------------------------------------------------------------- 1 | """ 2 | monodromy/volume.py 3 | 4 | Helper routines for efficiently calculating the volume of a `Polytope`, 5 | presented as a union of `ConvexPolytope`s. 6 | """ 7 | 8 | from monodromy.utilities import bitcount, bit_iteration, bitscatter 9 | 10 | 11 | def bitmask_iterator(mask, determined_bitmask, total_bitcount, negative_bitmasks): 12 | """ 13 | Yields bitstrings of length `total_bitcount` which match `mask` 14 | on the raised bits in `determined_bitmask` and which _do not_ wholly 15 | match any of the masks in `negative_bitmasks`. 16 | """ 17 | undetermined_bitmask = determined_bitmask ^ ((1 << total_bitcount) - 1) 18 | if 0 == len(negative_bitmasks): 19 | remaining_bitcount = total_bitcount - bitcount(determined_bitmask) 20 | for j in range(1 << remaining_bitcount): 21 | # paint possible remainders into the undetermined mask 22 | yield mask | bitscatter(j, undetermined_bitmask) 23 | else: 24 | negative_bitmask, rest = negative_bitmasks[0], negative_bitmasks[1:] 25 | # ensure that it's possible to find any non-matches 26 | if ((negative_bitmask & determined_bitmask == negative_bitmask) and 27 | (negative_bitmask & mask == negative_bitmask)): 28 | return 29 | 30 | # if we're wholly determined, just recurse 31 | if negative_bitmask & determined_bitmask == negative_bitmask: 32 | yield from bitmask_iterator( 33 | mask, 34 | determined_bitmask | negative_bitmask, 35 | total_bitcount, 36 | rest 37 | ) 38 | return 39 | 40 | # otherwise, fill in the undetermined bits in negative_bitmask other 41 | # than the value 11...1 . 42 | undetermined_bitmask &= negative_bitmask 43 | for j in range((1 << bitcount(undetermined_bitmask))): 44 | augmented_mask = mask | bitscatter(j, undetermined_bitmask) 45 | if augmented_mask & negative_bitmask == negative_bitmask: 46 | continue 47 | yield from bitmask_iterator( 48 | augmented_mask, 49 | determined_bitmask | negative_bitmask, 50 | total_bitcount, 51 | rest 52 | ) 53 | 54 | 55 | def alternating_sum(polytope, volume_fn): 56 | """ 57 | Efficiently computes the inclusion-exclusion alternating sum for the volume 58 | of a `Polytope`, as computed by `volume_fn` on its convex intersections. 59 | 60 | `volume_fn` is required to be: 61 | 62 | + real-valued, 63 | + strictly monotonic: if A ≤ B, then vol(A) ≤ vol(B), 64 | with equality only if A = B, 65 | + weakly additive: vol(A u B) ≤ vol(A) + vol(B). 66 | """ 67 | # This method is quite complex. The basic idea is to use inclusion-exclusion 68 | # to calculate the volume of `polytope` according to `volume_fn`, but our 69 | # Polytopes tend to come in highly degenerate families, which we exploit to 70 | # lower the naively exponential complexity of this procedure. (Compare 71 | # `naive_alternating_sum` at the bottom of this file.) The two basic 72 | # mechanisms are: 73 | # 74 | # + If vol(A) = 0, then vol(AB) = 0 for any B, hence the entire lattice 75 | # under A can be discarded. 76 | # + If vol(A) = vol(AB), then vol(AC) = vol(ABC) for any C. These occur in 77 | # canceling pairs, hence the entire lattice under A can be discarded. 78 | # 79 | # The complexity comes from applying multiple relations of the second sort: 80 | # one half of a canceling pair in a later application might have been 81 | # consumed as part of a canceling pair in an earlier application. We're 82 | # encouraged to delay dealing with this: the sooner we can finish a large 83 | # exponential walk, the better off we are, so we collect these relations 84 | # until we have finished a full intersection depth with no new volumes. 85 | # See below for a comment describing the correction to the double-counting. 86 | 87 | total_volume = 0 88 | volume_fn_calls = 0 89 | 90 | vanishing_masks = [] # mask 91 | alternating_masks = [] # (mask, toggle) 92 | 93 | previous_volumes = {} # mask -> volume 94 | 95 | # compute the "single-count" sum 96 | for d in range(len(polytope.convex_subpolytopes)): 97 | volumes = {} 98 | did_work = False 99 | for bitstring in bit_iteration(length=len(polytope.convex_subpolytopes), 100 | weight=1 + d): 101 | # if this is guaranteed to be zero, skip it 102 | if any([mask & bitstring == mask for mask in vanishing_masks]): 103 | continue 104 | 105 | # if this belongs to an alternating skip, skip it 106 | if any([mask & bitstring == mask 107 | for mask, toggle in alternating_masks]): 108 | continue 109 | 110 | # if this is inheritable from the previous stage, inherit it 111 | for mask, toggle in alternating_masks: 112 | previous_volume = previous_volumes.get(bitstring ^ toggle, None) 113 | if ((mask & bitstring == mask) and (bitstring & toggle != 0) 114 | and previous_volume is not None): 115 | volumes[bitstring] = previous_volume 116 | break 117 | 118 | # if that failed, calculate from scratch 119 | if volumes.get(bitstring, None) is None: 120 | intersection = None 121 | for index, convex_subpolytope in enumerate(polytope.convex_subpolytopes): 122 | if 0 != (bitstring & (1 << index)): 123 | intersection = convex_subpolytope if intersection is None \ 124 | else intersection.intersect(convex_subpolytope) 125 | volumes[bitstring] = volume_fn(intersection) 126 | volume_fn_calls += 1 127 | 128 | # if this has vanishing volume, add it to the skip set; all done. 129 | if volumes[bitstring] == 0: 130 | vanishing_masks.append(bitstring) 131 | volumes[bitstring] = None 132 | continue 133 | 134 | # try to pair this volume with parents 135 | for parent_index in range(len(polytope.convex_subpolytopes)): 136 | parent_toggle = 1 << parent_index 137 | parent_bitstring = bitstring ^ parent_toggle 138 | parent_volume = previous_volumes.get(parent_bitstring, None) 139 | # ensure that we have this parent 140 | if 0 == bitstring & parent_toggle: 141 | continue 142 | # ensure that our volumes agree 143 | if volumes[bitstring] != parent_volume: 144 | continue 145 | # if we're noticing a coincidence now, it's the first time it's 146 | # happened, since otherwise we would have been caught by the 147 | # skip clause at the start of the middle loop. 148 | alternating_masks.append((parent_bitstring, parent_toggle)) 149 | if 1 == d % 2: 150 | total_volume = total_volume - volumes[bitstring] 151 | else: 152 | total_volume = total_volume + volumes[bitstring] 153 | break 154 | 155 | for bitstring in bit_iteration(length=len(polytope.convex_subpolytopes), 156 | weight=1+d): 157 | volume = volumes.get(bitstring, None) 158 | if volume is None: 159 | continue 160 | if any([mask & bitstring == mask 161 | for mask, toggle in alternating_masks]): 162 | continue 163 | 164 | did_work = True 165 | if 1 == d % 2: 166 | total_volume = total_volume - volume 167 | else: 168 | total_volume = total_volume + volume 169 | 170 | if not did_work: 171 | break 172 | 173 | # rotate the records 174 | previous_volumes = volumes 175 | 176 | # Now we account for the multiply-canceled terms, using the existing sorting 177 | # of `alternating_masks`. Double-counting occurs only when a pair 178 | # (mask, toggle) matches an untoggled bitstring and a preceding mask matches 179 | # its toggle-on form. We can search for such bitstrings according to the 180 | # _earliest_ preceding mask that matches its toggled form, ensuring that we 181 | # only correct each previously-erroneously-included string once. 182 | 183 | for j, (mask, toggle) in enumerate(alternating_masks): 184 | for k, (kth_mask, _) in enumerate(alternating_masks[:j]): 185 | if 0 == toggle & kth_mask: 186 | continue 187 | for bitstring in bitmask_iterator( 188 | # jth mask matches bitstring, kth mask matches only after toggle 189 | mask | (kth_mask ^ toggle), mask | kth_mask, 190 | # expect masks of this size 191 | len(polytope.convex_subpolytopes), 192 | # masks in [0, k) don't match and don't include the toggle 193 | [earlier_mask & -(toggle + 1) for 194 | earlier_mask, _ in alternating_masks[:k]] + 195 | # masks in (k, j) don't match, regardless of toggle 196 | [earlier_mask for earlier_mask, _ in alternating_masks[1+k:j]] + 197 | vanishing_masks 198 | ): 199 | intersection = None 200 | for index, convex_subpolytope in enumerate( 201 | polytope.convex_subpolytopes): 202 | if 0 != (bitstring & (1 << index)): 203 | intersection = convex_subpolytope if intersection is None \ 204 | else intersection.intersect(convex_subpolytope) 205 | 206 | volume_fn_calls += 1 207 | if 1 == bitcount(bitstring) % 2: 208 | total_volume += volume_fn(intersection) 209 | else: 210 | total_volume -= volume_fn(intersection) 211 | 212 | return total_volume 213 | 214 | 215 | # PEDAGOGICAL VALUE ONLY 216 | def naive_alternating_sum(polytope): 217 | """ 218 | Inefficiently computes the Euclidean volume of a `Polytope` using the 219 | inclusion-exclusion alternating. 220 | """ 221 | total_volume = 0 222 | 223 | for d in range(len(polytope.convex_subpolytopes)): 224 | volumes = {} 225 | for bitstring in bit_iteration(length=len(polytope.convex_subpolytopes), 226 | weight=1 + d): 227 | if volumes.get(bitstring, None) is None: 228 | intersection = None 229 | for index, convex_subpolytope in enumerate(polytope.convex_subpolytopes): 230 | if 0 != (bitstring & (1 << index)): 231 | intersection = convex_subpolytope if intersection is None \ 232 | else intersection.intersect(convex_subpolytope) 233 | volumes[bitstring] = intersection.volume 234 | 235 | if 1 == d % 2: 236 | total_volume = total_volume - volumes[bitstring] 237 | else: 238 | total_volume = total_volume + volumes[bitstring] 239 | 240 | return total_volume 241 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | scipy 3 | qiskit 4 | qiskit-terra 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | 3 | # SEE README: 4 | # lrs 5 | # lrcalc 6 | -------------------------------------------------------------------------------- /scripts/approx_gateset.py: -------------------------------------------------------------------------------- 1 | """ 2 | scripts/approx_gateset.py 3 | 4 | Example script showing how to optimize a(n XX-based) gateset for performance 5 | against a user-defined cost metric. This version of the script performs Monte 6 | Carlo sampling of unitaries from the Haar distribution, which lets us analyze 7 | non-exact decomposition techniques, like approximation and mirroring. 8 | 9 | NOTE: The optimization loop requires `pybobyqa`, a derivative-free optimizer. 10 | 11 | NOTE: `rescaled_objective` always includes a full XX. 12 | """ 13 | 14 | from itertools import count 15 | import math 16 | from time import perf_counter 17 | import warnings 18 | 19 | import numpy as np 20 | import pybobyqa 21 | from scipy.stats import unitary_group 22 | 23 | import qiskit 24 | from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer 25 | 26 | gateset_dimension = 2 # how many gates to include beyond a full CX 27 | filename = "approx_gateset_landscape_2d_mirror.dat" # .dat file with expected cost info 28 | approximate = True 29 | mirror = True 30 | 31 | print(f"Gateset dim: {gateset_dimension}, " 32 | f"approximate: {approximate}, " 33 | f"mirror: {mirror}") 34 | 35 | # 36 | # ERROR MODEL 37 | # 38 | # We assume that the infidelity cost of a native 2Q interaction is affinely 39 | # related to the interaction strength. The following two values track the 40 | # offset and the slope of this affine-linear function. 41 | # 42 | # first summand: 2Q invocation cost; second summand: cost of local gates 43 | offset = 909 / (10000 * 100) + 1 / 1000 44 | # note: Isaac reports this value in percent per degree 45 | scale_factor = (64 * 90) / (10000 * 100) 46 | 47 | 48 | def operation_cost( 49 | strength, 50 | scale_factor: float = scale_factor, 51 | offset: float = offset, 52 | ): 53 | return strength * scale_factor + offset 54 | 55 | 56 | # useful for reproducibility 57 | np.random.seed(0) 58 | 59 | # tuples of descending strengths in [0, 1] 60 | # -> {"average_cost", "average_overshot", "sigma_cost", "sigma_overshot"} 61 | cost_table = {} 62 | 63 | 64 | def extract_cost(circuit, basis_infidelity): 65 | execution_infidelity = 0 66 | for g, q, _ in circuit.data: 67 | if len(q) < 2: 68 | continue 69 | if isinstance(g, qiskit.circuit.library.CXGate): 70 | execution_infidelity += basis_infidelity[np.pi / 2] 71 | elif isinstance(g, qiskit.circuit.library.RZXGate): 72 | execution_infidelity += basis_infidelity[g.params[0]] 73 | else: 74 | warnings.warn(f"Unknown 2Q gate: {g}") 75 | 76 | return execution_infidelity 77 | 78 | 79 | def single_circuit_infidelity(decomposer, u, basis_infidelity, approximate=True): 80 | basis_fidelity = {k: 1 - v for k, v in basis_infidelity.items()} 81 | circuit = decomposer(u, basis_fidelity=basis_fidelity, 82 | approximate=approximate) 83 | execution_infidelity = extract_cost(circuit, basis_infidelity) 84 | model_matrix = qiskit.quantum_info.operators.Operator(circuit).data 85 | model_infidelity = 1 - 1 / 20 * ( 86 | 4 + abs(np.trace(np.conj(np.transpose(u)) @ model_matrix)) ** 2) 87 | return model_infidelity + execution_infidelity 88 | 89 | 90 | def single_sample_infidelity(decomposer, basis_infidelity, 91 | mirror=mirror, approximate=approximate): 92 | u = unitary_group.rvs(4) 93 | u = u / (np.linalg.det(u) ** (1 / 4)) 94 | infidelity = single_circuit_infidelity(decomposer, u, basis_infidelity, approximate=approximate) 95 | 96 | if mirror: 97 | v = u @ qiskit.circuit.library.SwapGate().to_matrix() 98 | mirror_infidelity = single_circuit_infidelity(decomposer, v, 99 | basis_infidelity, 100 | approximate=approximate) 101 | infidelity = min(infidelity, mirror_infidelity) 102 | 103 | return infidelity 104 | 105 | 106 | def objective(ratios, attempts=10_000): 107 | global cost_table 108 | 109 | decomposer = XXDecomposer(euler_basis="PSX") 110 | basis_infidelity = { 111 | np.pi / 2 * ratio: operation_cost(ratio) for ratio in ratios 112 | } 113 | 114 | timer = perf_counter() 115 | infidelity = 0 116 | for _ in range(attempts): 117 | infidelity += single_sample_infidelity(decomposer, basis_infidelity) 118 | infidelity /= attempts 119 | print(f"Analyzing {ratios} -> {infidelity} " 120 | f"took {perf_counter() - timer:.5f} seconds.") 121 | 122 | cost_table[tuple(ratios)] = {"average_cost": infidelity} 123 | 124 | return infidelity 125 | 126 | 127 | def rescaled_objective(ratios): 128 | """ 129 | `objective` with its domain rescaled for easier use by `pybobyqa`: the 130 | sequence 131 | 132 | [a1, a2, ..., an] 133 | 134 | is forwarded to `objective` as 135 | 136 | [b1, b2, ..., bn] = [a1, a1 * a2, ..., a1 * a2 * ... an], 137 | 138 | so that 0 <= a1, a2, ..., an <= 1 implies b1 >= b2 >= ... >= bn. 139 | """ 140 | triangular_strengths = [] 141 | for ratio in [1, *ratios]: # automatically include a full CX 142 | if 0 < len(triangular_strengths): 143 | previous_strength = triangular_strengths[-1] 144 | else: 145 | previous_strength = 1 146 | triangular_strengths.append(previous_strength * ratio) 147 | 148 | return objective(triangular_strengths) 149 | 150 | 151 | def print_cost_table(): 152 | """ 153 | Utility function for printing the expected costs calculated so far. 154 | """ 155 | global filename, gateset_dimension 156 | 157 | keys = ["average_cost"] 158 | 159 | print("Dumping cost table...") 160 | with open(filename, "w") as fh: 161 | fh.write(' '.join([f'strength{n}' for n in range(1 + gateset_dimension)]) 162 | + " " + ' '.join(keys) + '\n') 163 | for k, v in cost_table.items(): 164 | fh.write(' '.join(str(float(entry)) for entry in k) + ' ' + 165 | ' '.join(str(v[key]) for key in keys) + '\n') 166 | print("Dumped.") 167 | 168 | 169 | ################################################################################ 170 | 171 | # make the best use of time by first using `pybobyqa` to calculate an optimal 172 | # gateset. 173 | x0 = np.array([1/2] * gateset_dimension) 174 | solution = pybobyqa.solve( 175 | rescaled_objective, x0, 176 | bounds=([0] * gateset_dimension, [1] * gateset_dimension), 177 | objfun_has_noise=False, 178 | print_progress=True, 179 | rhoend=1e-4 180 | ) 181 | 182 | print("Optimizer solution:") 183 | print(solution) 184 | 185 | print(cost_table) 186 | 187 | 188 | ################################################################################ 189 | 190 | print("Now we enter an infinite loop to flesh out the gateset landscape and " 191 | "turn it into a nice plot overall. Use KeyboardInterrupt to quit " 192 | "whenever you're satisfied.") 193 | 194 | 195 | def iterate_over_total( 196 | total, 197 | bucket_count, 198 | fn, 199 | partial_fill=None 200 | ): 201 | partial_fill = partial_fill if partial_fill is not None else [] 202 | if bucket_count == len(partial_fill): 203 | return fn(partial_fill) 204 | 205 | if bucket_count == 1 + len(partial_fill): 206 | if total - sum(partial_fill) >= 2: 207 | return iterate_over_total( 208 | total, 209 | bucket_count, 210 | fn, 211 | [*partial_fill, total - sum(partial_fill)] 212 | ) 213 | else: 214 | return 215 | 216 | for denominator in range(1, total - sum(partial_fill)): 217 | iterate_over_total( 218 | total, 219 | bucket_count, 220 | fn, 221 | [*partial_fill, denominator] 222 | ) 223 | 224 | 225 | def iterate_over_numerators( 226 | denominators, 227 | fn, 228 | partial_fill=None 229 | ): 230 | partial_fill = partial_fill if partial_fill is not None else [] 231 | if 0 == len(denominators): 232 | return fn(partial_fill) 233 | for j in range(1, denominators[0]): 234 | if 0 < len(partial_fill) and j / denominators[0] >= partial_fill[-1]: 235 | continue 236 | if math.gcd(j, denominators[0]) != 1: 237 | continue 238 | iterate_over_numerators( 239 | denominators[1:], 240 | fn, 241 | partial_fill=[*partial_fill, j / denominators[0]] 242 | ) 243 | 244 | 245 | # this loop enumerates rational tuples whose denominators grow maximally slowly, 246 | # which do not repeat, and which are sorted descending. 247 | # 248 | # it also includes the full CX in the call to `objective`, to match the behavior 249 | # of the optimization step above. 250 | for total in count(1): 251 | iterate_over_total( 252 | total, 253 | gateset_dimension, 254 | lambda denominators: [ 255 | iterate_over_numerators( 256 | denominators, 257 | lambda ratios: objective([1, ] + ratios) 258 | ), 259 | print_cost_table() 260 | ] 261 | ) 262 | -------------------------------------------------------------------------------- /scripts/demo.py: -------------------------------------------------------------------------------- 1 | import qiskit.quantum_info 2 | from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer 3 | 4 | import numpy as np 5 | from scipy.stats import unitary_group 6 | 7 | from monodromy.coverage import * 8 | from monodromy.static.examples import * 9 | from monodromy.haar import expected_cost 10 | import monodromy.render 11 | 12 | 13 | def default_zx_operation_cost( 14 | strength: Fraction, 15 | # note: Isaac reports this value in percent per degree 16 | scale_factor: float = (64 * 90) / (10000 * 100), 17 | # first component: 2Q invocation cost; second component: local cost 18 | offset: float = 909 / (10000 * 100) + 1 / 1000, 19 | ): 20 | """ 21 | A sample fidelity cost model, extracted from experiment, for ZX operations. 22 | """ 23 | return strength * scale_factor + offset 24 | 25 | 26 | def get_zx_operations(strengths: Dict[Fraction, float]) \ 27 | -> List[CircuitPolytope]: 28 | """ 29 | Converts a dictionary mapping fractional CX `strengths` to fidelities to the 30 | corresponding list of `OperationPolytope`s. 31 | """ 32 | operations = [] 33 | 34 | for strength, fidelity in strengths.items(): 35 | operations.append(CircuitPolytope( 36 | operations=[f"rzx(pi/2 * {strength})"], 37 | cost=fidelity, 38 | convex_subpolytopes=exactly( 39 | strength / 4, strength / 4, -strength / 4, 40 | ).convex_subpolytopes, 41 | )) 42 | 43 | return operations 44 | 45 | 46 | operations = get_zx_operations({ 47 | frac: default_zx_operation_cost(frac) 48 | for frac in [Fraction(1), Fraction(1, 2), Fraction(1, 3)] 49 | }) 50 | 51 | # build the set of covering polytopes 52 | print("==== Working to build a set of covering polytopes ====") 53 | coverage_set = build_coverage_set(operations, chatty=True) 54 | 55 | # print it out for user inspection 56 | print("==== Done. Here's what we found: ====") 57 | print_coverage_set(coverage_set) 58 | 59 | print("==== Haar volumes ====") 60 | print(f"Haar-expectation cost: {expected_cost(coverage_set, chatty=True)}") 61 | 62 | # flex the rendering code 63 | print("==== Render these in Mathematica: =====") 64 | print(monodromy.render.polytopes_to_mathematica(coverage_set)) 65 | 66 | # perform a gate decomposition 67 | print("==== Compiling a single Haar-random gate into CX, CX/2, CX/3 ====") 68 | # generate a random special unitary 69 | u = unitary_group.rvs(4) 70 | u /= np.linalg.det(u) ** (1 / 4) 71 | 72 | # decompose into CX, CX/2, and CX/3 73 | monodromy_decomposer = XXDecomposer(euler_basis="PSX") 74 | circuit = monodromy_decomposer(u, approximate=False) 75 | 76 | with np.printoptions(precision=4, suppress=True): 77 | print(u) 78 | print(qiskit.quantum_info.Operator(circuit).data) 79 | print(f"=== {(abs(u - qiskit.quantum_info.Operator(circuit).data) < 1e-1).all()} ===") 80 | print(circuit) 81 | -------------------------------------------------------------------------------- /scripts/gateset.py: -------------------------------------------------------------------------------- 1 | """ 2 | scripts/gateset.py 3 | 4 | Example script showing how to optimize a(n XX-based) gateset for performance 5 | against a user-defined cost metric. 6 | 7 | NOTE: The optimization loop requires `pybobyqa`, a derivative-free optimizer. 8 | 9 | NOTE: We don't make use of tools which are further specialized for XX-based 10 | gate-sets. By modifying the `exactly` in `get_operations`, a user could 11 | optimize over any shape of gateset they please. 12 | 13 | NOTE: `rescaled_objective` always includes a full XX. 14 | """ 15 | 16 | from itertools import count 17 | import math 18 | from time import perf_counter 19 | 20 | import numpy as np 21 | import pybobyqa 22 | 23 | from monodromy.coverage import * 24 | from monodromy.static.examples import * 25 | from monodromy.haar import cost_statistics # , expected_cost 26 | 27 | gateset_dimension = 1 # how many gates to include beyond a full CX 28 | filename = "gateset_landscape_1d.dat" # .dat file with expected cost info 29 | 30 | # 31 | # ERROR MODEL 32 | # 33 | # We assume that the infidelity cost of a native 2Q interaction is affinely 34 | # related to the interaction strength. The following two values track the 35 | # offset and the slope of this affine-linear function. 36 | # 37 | # first summand: 2Q invocation cost; second summand: cost of local gates 38 | offset = 909 / (10000 * 100) + 1 / 1000 39 | # note: Isaac reports this value in percent per degree 40 | scale_factor = (64 * 90) / (10000 * 100) 41 | 42 | 43 | def operation_cost( 44 | strength: Fraction, 45 | scale_factor: float = scale_factor, 46 | offset: float = offset, 47 | ): 48 | return strength * scale_factor + offset 49 | 50 | 51 | def get_operations(*strengths): 52 | """ 53 | Builds a family of XX-type operations, where each strength in `strengths` is 54 | specified as a fraction of the "full strength" XX-type operation, CX. 55 | """ 56 | return [ 57 | CircuitPolytope( 58 | convex_subpolytopes=exactly( 59 | Fraction( 1, 4) * strength, 60 | Fraction( 1, 4) * strength, 61 | Fraction(-1, 4) * strength 62 | ).convex_subpolytopes, 63 | cost=operation_cost(strength), 64 | operations=[f"{str(strength)} XX"], 65 | ) for strength in set(strengths) 66 | ] 67 | 68 | 69 | # useful for reproducibility 70 | np.random.seed(0) 71 | 72 | # tuples of descending strengths in [0, 1] 73 | # -> {"average_cost", "average_overshot", "sigma_cost", "sigma_overshot"} 74 | cost_table = {} 75 | 76 | 77 | def objective(ratios): 78 | """ 79 | Function to be optimized: consumes a family of interaction strengths, then 80 | computes the expected cost of compiling a Haar-randomly chosen 2Q operator. 81 | """ 82 | global offset, scale_factor 83 | 84 | timer_coverage = perf_counter() 85 | operations = get_operations(*ratios) 86 | strengths_string = ', '.join([str(s) + " XX" for s in ratios]) 87 | print(f"Working on " + strengths_string) 88 | coverage_set = build_coverage_set(operations, chatty=True) 89 | timer_coverage = perf_counter() - timer_coverage 90 | timer_haar = perf_counter() 91 | cost_table[tuple(ratios)] = cost_statistics( 92 | coverage_set, offset=offset, scale_factor=scale_factor, chatty=True 93 | ) 94 | cost = cost_table[tuple(ratios)]["average_cost"] 95 | timer_haar = perf_counter() - timer_haar 96 | print( 97 | f"{strengths_string} took {timer_coverage:.3f}s + {timer_haar:.3f}s = " 98 | f"{timer_coverage + timer_haar:.3f}s") 99 | return cost 100 | 101 | 102 | def rescaled_objective(ratios, max_denominator=100): 103 | """ 104 | `objective` with its domain rescaled for easier use by `pybobyqa`: the 105 | sequence 106 | 107 | [a1, a2, ..., an] 108 | 109 | is forwarded to `objective` as 110 | 111 | [b1, b2, ..., bn] = [a1, a1 * a2, ..., a1 * a2 * ... an], 112 | 113 | so that 0 <= a1, a2, ..., an <= 1 implies b1 >= b2 >= ... >= bn. 114 | """ 115 | triangular_strengths = [] 116 | for ratio in [1, *ratios]: # automatically include a full CX 117 | if 0 < len(triangular_strengths): 118 | previous_strength = triangular_strengths[-1] 119 | else: 120 | previous_strength = Fraction(1) 121 | triangular_strengths.append( 122 | previous_strength * Fraction(ratio) 123 | .limit_denominator(max_denominator) 124 | ) 125 | 126 | return objective(triangular_strengths) 127 | 128 | 129 | def print_cost_table(): 130 | """ 131 | Utility function for printing the expected costs calculated so far. 132 | """ 133 | global filename, gateset_dimension 134 | 135 | keys = ["average_cost", "average_overshot", "sigma_cost", "sigma_overshot"] 136 | 137 | print("Dumping cost table...") 138 | with open(filename, "w") as fh: 139 | fh.write(' '.join([f'strength{n}' for n in range(1 + gateset_dimension)]) 140 | + " " + ' '.join(keys) + '\n') 141 | for k, v in cost_table.items(): 142 | fh.write(' '.join(str(float(entry)) for entry in k) + ' ' + 143 | ' '.join(str(v[key]) for key in keys) + '\n') 144 | print("Dumped.") 145 | 146 | 147 | ################################################################################ 148 | 149 | # make the best use of time by first using `pybobyqa` to calculate an optimal 150 | # gateset. 151 | x0 = np.array([Fraction(1, 2)] * gateset_dimension) 152 | solution = pybobyqa.solve( 153 | rescaled_objective, x0, 154 | bounds=([0] * gateset_dimension, [1] * gateset_dimension), 155 | objfun_has_noise=False, 156 | print_progress=True, 157 | rhoend=1e-4 158 | ) 159 | 160 | print("Optimizer solution:") 161 | print(solution) 162 | 163 | print(cost_table) 164 | 165 | 166 | ################################################################################ 167 | 168 | print("Now we enter an infinite loop to flesh out the gateset landscape and " 169 | "turn it into a nice plot overall. Use KeyboardInterrupt to quit " 170 | "whenever you're satisfied.") 171 | 172 | 173 | def iterate_over_total( 174 | total, 175 | bucket_count, 176 | fn, 177 | partial_fill=None 178 | ): 179 | partial_fill = partial_fill if partial_fill is not None else [] 180 | if bucket_count == len(partial_fill): 181 | return fn(partial_fill) 182 | 183 | if bucket_count == 1 + len(partial_fill): 184 | if total - sum(partial_fill) >= 2: 185 | return iterate_over_total( 186 | total, 187 | bucket_count, 188 | fn, 189 | [*partial_fill, total - sum(partial_fill)] 190 | ) 191 | else: 192 | return 193 | 194 | for denominator in range(1, total - sum(partial_fill)): 195 | iterate_over_total( 196 | total, 197 | bucket_count, 198 | fn, 199 | [*partial_fill, denominator] 200 | ) 201 | 202 | 203 | def iterate_over_numerators( 204 | denominators, 205 | fn, 206 | partial_fill=None 207 | ): 208 | partial_fill = partial_fill if partial_fill is not None else [] 209 | if 0 == len(denominators): 210 | return fn(partial_fill) 211 | for j in range(1, denominators[0]): 212 | if 0 < len(partial_fill) and j / denominators[0] >= partial_fill[-1]: 213 | continue 214 | if math.gcd(j, denominators[0]) != 1: 215 | continue 216 | iterate_over_numerators( 217 | denominators[1:], 218 | fn, 219 | partial_fill=[*partial_fill, Fraction(j, denominators[0])] 220 | ) 221 | 222 | 223 | # this loop enumerates rational tuples whose denominators grow maximally slowly, 224 | # which do not repeat, and which are sorted descending. 225 | # 226 | # it also includes the full CX in the call to `objective`, to match the behavior 227 | # of the optimization step above. 228 | for total in count(1): 229 | iterate_over_total( 230 | total, 231 | gateset_dimension, 232 | lambda denominators: [ 233 | iterate_over_numerators( 234 | denominators, 235 | lambda ratios: objective([1, ] + ratios) 236 | ), 237 | print_cost_table() 238 | ] 239 | ) 240 | -------------------------------------------------------------------------------- /scripts/nuop.py: -------------------------------------------------------------------------------- 1 | """ 2 | scripts/nuop.py 3 | 4 | This script compares the performance of our synthesis techniques with Lao et 5 | al.'s `NuOp` package: 6 | 7 | https://github.com/prakashmurali/NuOp . 8 | 9 | Unhappily, NuOp is not presently formulated as a package, so it's not so easy 10 | to include. Additionally, it has some artificial constraints written into 11 | constants with function-local definitions — for instance, it won't synthesize 12 | circuits of depth greater than 4 — which we want to override in our comparison. 13 | Accordingly, we've copy/pasted the entire body of their package below. Search 14 | for "# COMPARISON" to hop to where the script actually begins. 15 | """ 16 | 17 | import qiskit 18 | 19 | from time import perf_counter 20 | 21 | import numpy as np 22 | from scipy.optimize import minimize 23 | from scipy.stats import unitary_group 24 | 25 | from qiskit.circuit.library.standard_gates import RZZGate 26 | from qiskit.extensions.unitary import UnitaryGate 27 | from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer 28 | 29 | from monodromy.utilities import epsilon 30 | 31 | 32 | results_filename = "nuop_stats.dat" 33 | gate_strength = np.pi / 4 34 | 35 | 36 | # 37 | # nuop/gates_numpy.py 38 | # 39 | 40 | 41 | def cphase_gate(theta): 42 | return np.matrix([ 43 | [ 44 | 1, 0, 0, 0 45 | ], 46 | [ 47 | 0, 1, 0, 0 48 | ], 49 | [ 50 | 0, 0, 1, 0 51 | ], 52 | [ 53 | 0, 0, 0, np.cos(theta) + 1j * np.sin(theta) 54 | ]]) 55 | 56 | 57 | def cnot_gate(): 58 | return np.matrix([ 59 | [ 60 | 1, 0, 0, 0 61 | ], 62 | [ 63 | 0, 1, 0, 0 64 | ], 65 | [ 66 | 0, 0, 0, 1 67 | ], 68 | [ 69 | 0, 0, 1, 0 70 | ]]) 71 | 72 | 73 | def fsim_gate(theta, phi): 74 | return np.matrix([ 75 | [ 76 | 1, 0, 0, 0 77 | ], 78 | [ 79 | 0, 80 | np.cos(theta), 81 | -1j * np.sin(theta), 82 | 0 83 | ], 84 | [ 85 | 0, 86 | -1j * np.sin(theta), 87 | np.cos(theta), 88 | 0 89 | ], 90 | [ 91 | 0, 0, 0, np.cos(phi) - 1j * np.sin(phi) 92 | ]]) 93 | 94 | 95 | def xy_gate(theta): 96 | return np.matrix([ 97 | [ 98 | 1, 0, 0, 0 99 | ], 100 | [ 101 | 0, 102 | np.cos(theta / 2), 103 | 1j * np.sin(theta / 2), 104 | 0 105 | ], 106 | [ 107 | 0, 108 | 1j * np.sin(theta / 2), 109 | np.cos(theta / 2), 110 | 0 111 | ], 112 | [ 113 | 0, 0, 0, 1 114 | ] 115 | ]) 116 | 117 | 118 | def cz_gate(): 119 | return np.matrix([[1, 0, 0, 0], 120 | [0, 1, 0, 0], 121 | [0, 0, 1, 0], 122 | [0, 0, 0, -1]]) 123 | 124 | 125 | def rzz_unitary(theta): 126 | return np.array([[np.exp(-1j * theta / 2), 0, 0, 0], 127 | [0, np.exp(1j * theta / 2), 0, 0], 128 | [0, 0, np.exp(1j * theta / 2), 0], 129 | [0, 0, 0, np.exp(-1j * theta / 2)]], dtype=complex) 130 | 131 | 132 | def get_gate_unitary_qiskit(gate_op): 133 | # Let's assume all the default unitary matrices in qiskit, which has 134 | # different endianness from our convention, so we will need to reverse the 135 | # qubit order when we apply our decomposition pass. 136 | if isinstance(gate_op, UnitaryGate): 137 | return gate_op.to_matrix() 138 | elif isinstance(gate_op, RZZGate): 139 | return rzz_unitary(gate_op.params[0]) 140 | else: 141 | return gate_op.to_matrix() 142 | 143 | 144 | # 145 | # nuop/parallel_two_qubit_gate_decomposition.py 146 | # 147 | 148 | 149 | class GateTemplate: 150 | """ 151 | Creates a unitary matrix using a specified two-qubit gate, number of layers 152 | and single-qubit rotation parameters 153 | """ 154 | 155 | def __init__(self, two_qubit_gate, two_qubit_gate_params): 156 | """ 157 | two_qubit_gate: a function that returns the numpy matrix for the desired 158 | gate 159 | two_qubit_gate_params: inputs to the function e.g., for fsim gates, 160 | params has fixed theta, phi values 161 | """ 162 | self.two_qubit_gate = two_qubit_gate 163 | self.two_qubit_gate_params = two_qubit_gate_params 164 | 165 | def u3_gate(self, theta, phi, lam): 166 | return np.matrix([ 167 | [ 168 | np.cos(theta / 2), 169 | -np.exp(1j * lam) * np.sin(theta / 2) 170 | ], 171 | [ 172 | np.exp(1j * phi) * np.sin(theta / 2), 173 | np.exp(1j * (phi + lam)) * np.cos(theta / 2) 174 | ]]) 175 | 176 | def multiply_all(self, matrices): 177 | product = np.eye(4) 178 | for i in range(len(matrices)): 179 | product = np.matmul(matrices[i], product) 180 | return product 181 | 182 | def u3_layer(self, x): 183 | u3_1 = self.u3_gate(*x[0:3]) 184 | u3_2 = self.u3_gate(*x[3:6]) 185 | t1 = np.kron(u3_1, u3_2) 186 | return t1 187 | 188 | def n_layer_unitary(self, n_layers, params): 189 | """ 190 | n_layers: number of layers desired in the template 191 | params: list of 1Q gate rotation parameters, specified by the optimizer 192 | """ 193 | gate_list = [] 194 | idx = 0 195 | gate_list.append(self.u3_layer(params[idx:idx + 6])) 196 | idx += 6 197 | for i in range(n_layers): 198 | if len(self.two_qubit_gate_params): 199 | gate_list.append( 200 | self.two_qubit_gate(*self.two_qubit_gate_params)) 201 | else: 202 | gate_list.append(self.two_qubit_gate()) 203 | gate_list.append(self.u3_layer(params[idx:idx + 6])) 204 | idx += 6 205 | return self.multiply_all(gate_list) 206 | 207 | def get_num_params(self, n_layers): 208 | return 6 * (n_layers + 1) 209 | 210 | 211 | class TwoQubitGateSynthesizer: 212 | """ 213 | Synthesises a gate implementation for a target unitary, using a specified 214 | gate template 215 | """ 216 | 217 | def __init__(self, target_unitary, gate_template_obj): 218 | self.target_unitary = target_unitary 219 | self.gate_template_obj = gate_template_obj 220 | 221 | def unitary_distance_function(self, A, B): 222 | # return (1 - np.abs(np.sum(np.multiply(B,np.conj(np.transpose(A))))) / 4) 223 | # return (1 - (np.abs(np.sum(np.multiply(B,np.conj(A)))))**2+4 / 20) # quantum volume paper 224 | return (1 - np.abs(np.sum(np.multiply(B, np.conj(A)))) / 4) 225 | 226 | def make_cost_function(self, n_layers): 227 | target_unitary = self.target_unitary 228 | 229 | def cost_function(x): 230 | A = self.gate_template_obj.n_layer_unitary(n_layers, x) 231 | B = target_unitary 232 | return self.unitary_distance_function(A, B) 233 | 234 | return cost_function 235 | 236 | def get_num_params(self, n_layers): 237 | return self.gate_template_obj.get_num_params(n_layers) 238 | 239 | def rand_initialize(self, n_layers): 240 | params = self.get_num_params(n_layers) 241 | return [np.pi * 2 * np.random.random() for i in range(params)] 242 | 243 | def solve_instance(self, n_layers, trials): 244 | self.cost_function = self.make_cost_function(n_layers) 245 | results = [] 246 | best_idx = 0 247 | best_val = float('inf') 248 | for i in range(trials): 249 | init = self.rand_initialize(n_layers) 250 | res = minimize(self.cost_function, init, method='BFGS', 251 | options={'maxiter': 1000 * 30}) 252 | results.append(res) 253 | if best_val > res.fun: 254 | best_val = res.fun 255 | best_idx = i 256 | return results[best_idx] 257 | 258 | def optimal_decomposition(self, tol=1e-3, fidelity_2q_gate=1.0, 259 | fidelity_1q_gate=[1.0, 1.0]): 260 | max_num_layers = 10 261 | cutoff_with_tol = True 262 | results = [] 263 | best_idx = 0 264 | best_fidelity = 0 265 | 266 | for i in range(max_num_layers): 267 | if cutoff_with_tol and best_fidelity > 1.0 - tol: 268 | break 269 | 270 | # Solve an instance with i+1 layers, doing 1 random trial 271 | res = self.solve_instance(n_layers=i + 1, trials=1) 272 | results.append(res) 273 | 274 | # Evaluate the fidelity after adding one layer 275 | hw_fidelity = ((fidelity_1q_gate[0] * fidelity_1q_gate[1]) ** (2 + i)) * \ 276 | (fidelity_2q_gate ** (i + 1)) 277 | unitary_fidelity = 1.0 - res.fun 278 | current_fidelity = hw_fidelity * unitary_fidelity 279 | 280 | # Update if the best_fidelity so far has been 0 (initial case) 281 | if best_fidelity == 0: 282 | best_idx = i 283 | best_fidelity = current_fidelity 284 | 285 | # Update if the current value is smaller than the previous minimum 286 | if current_fidelity - best_fidelity > tol * 0.1: 287 | best_idx = i 288 | best_fidelity = current_fidelity 289 | 290 | return best_idx + 1, results[best_idx], best_fidelity 291 | 292 | 293 | # 294 | # COMPARISON 295 | # 296 | 297 | monodromy_decomposer = XXDecomposer(euler_basis="PSX") 298 | gate_template = GateTemplate(qiskit.circuit.library.RZXGate, [gate_strength]) 299 | 300 | with open(results_filename, "w") as fh: 301 | print("monodromy_depth monodromy_time nuop_depth nuop_time") 302 | fh.write("monodromy_depth monodromy_time nuop_depth nuop_time\n") 303 | 304 | for _ in range(1000): 305 | # generate a random special unitary 306 | u = unitary_group.rvs(4) 307 | u /= np.linalg.det(u) ** (1/4) 308 | 309 | # find the best exact and approximate points 310 | monodromy_time = perf_counter() 311 | circuit = monodromy_decomposer( 312 | u, approximate=False, basis_fidelity={gate_strength: 1.0} 313 | ) 314 | monodromy_time = perf_counter() - monodromy_time 315 | monodromy_depth = sum([isinstance(datum[0], qiskit.circuit.library.RZXGate) 316 | for datum in circuit.data]) 317 | 318 | nuop_time = perf_counter() 319 | nuop_depth, _, nuop_fidelity = TwoQubitGateSynthesizer( 320 | u, gate_template 321 | ).optimal_decomposition( 322 | tol=epsilon, fidelity_2q_gate=1.0, fidelity_1q_gate=[1.0, 1.0] 323 | ) 324 | nuop_time = perf_counter() - nuop_time 325 | 326 | with open(results_filename, "a") as fh: 327 | fh.write(f"{monodromy_depth} {monodromy_time} " 328 | f"{nuop_depth} {nuop_time}\n") 329 | print(monodromy_time, monodromy_depth, nuop_time, nuop_depth) 330 | -------------------------------------------------------------------------------- /scripts/proof.py: -------------------------------------------------------------------------------- 1 | from monodromy.static.interference import check_main_xx_theorem, regenerate_xx_solution_polytopes 2 | 3 | print("Checking main global theorem.") 4 | check_main_xx_theorem() 5 | print("Done.") 6 | 7 | print("Checking main local theorem.") 8 | regenerate_xx_solution_polytopes() 9 | print("Done.") -------------------------------------------------------------------------------- /scripts/qft.py: -------------------------------------------------------------------------------- 1 | """ 2 | scripts/qft.py 3 | 4 | Count the XX interactions in synthesized QFT circuits of various sizes. 5 | 6 | NOTE: Runs indefinitely. 7 | """ 8 | 9 | import qiskit 10 | import numpy as np 11 | from collections import defaultdict 12 | from itertools import count 13 | 14 | for qubit_count in count(2): 15 | qc = qiskit.circuit.library.QFT(qubit_count) 16 | cx_counts = defaultdict(lambda: 0) 17 | for gate, _, _ in qiskit.transpile( 18 | qc, basis_gates=['u3', 'cx'], translation_method='synthesis' 19 | ).data: 20 | if isinstance(gate, qiskit.circuit.library.CXGate): 21 | cx_counts[np.pi] += 1 22 | elif isinstance(gate, qiskit.circuit.library.RZXGate): 23 | cx_counts[gate.params[0]] += 1 24 | 25 | rzx_counts = defaultdict(lambda: 0) 26 | for gate, _, _ in qiskit.transpile( 27 | qc, basis_gates=['u3', 'rzx'], translation_method='synthesis' 28 | ).data: 29 | if isinstance(gate, qiskit.circuit.library.CXGate): 30 | rzx_counts[np.pi] += 1 31 | elif isinstance(gate, qiskit.circuit.library.RZXGate): 32 | rzx_counts[gate.params[0]] += 1 33 | 34 | print(f"At qubit count {qubit_count}:") 35 | print(f"CX counts: {cx_counts}") 36 | print(f"CX, CX/2, CX/3 counts: {rzx_counts}") 37 | -------------------------------------------------------------------------------- /scripts/single_circuit.py: -------------------------------------------------------------------------------- 1 | """ 2 | scripts/single_circuit.py 3 | 4 | Calculate the polytope of canonical coordinates accessible to a fixed circuit 5 | template of the form 6 | 7 | local0 * gates[0] * local1 * ... * localn * gates[n] * local(n+1). 8 | """ 9 | 10 | from qiskit.circuit.library import RZXGate 11 | import numpy as np 12 | 13 | gates = [RZXGate(np.pi/3), RZXGate(np.pi/3), RZXGate(np.pi/3), RZXGate(np.pi/3)] 14 | 15 | 16 | from fractions import Fraction 17 | from monodromy.coordinates import monodromy_to_positive_canonical_polytope, \ 18 | positive_canonical_alcove_c2, unitary_to_monodromy_coordinate 19 | from monodromy.coverage import deduce_qlr_consequences 20 | from monodromy.static.examples import exactly, identity_polytope, \ 21 | everything_polytope 22 | 23 | 24 | circuit_polytope = identity_polytope 25 | 26 | 27 | for gate in gates: 28 | b_polytope = exactly( 29 | *(Fraction(x).limit_denominator(10_000) 30 | for x in unitary_to_monodromy_coordinate(gate.to_matrix())[:-1]) 31 | ) 32 | circuit_polytope = deduce_qlr_consequences( 33 | target="c", 34 | a_polytope=circuit_polytope, 35 | b_polytope=b_polytope, 36 | c_polytope=everything_polytope 37 | ) 38 | 39 | print(monodromy_to_positive_canonical_polytope(circuit_polytope)) 40 | print(f"{monodromy_to_positive_canonical_polytope(circuit_polytope).volume} vs " 41 | f"{positive_canonical_alcove_c2.volume}") 42 | -------------------------------------------------------------------------------- /scripts/xx_sequence.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import monodromy 4 | 5 | from monodromy.coordinates import monodromy_alcove, monodromy_alcove_c2, monodromy_to_positive_canonical_polytope, rho_reflect 6 | from monodromy.elimination import cylinderize, project 7 | from monodromy.polytopes import ConvexPolytope, Polytope 8 | from monodromy.static import qlr_polytope 9 | 10 | from itertools import count 11 | 12 | biswas_relations = (qlr_polytope 13 | # enlarge to the pu_4 version of the QLR relations 14 | .union(rho_reflect(qlr_polytope, [0, 7, 8, 9])) 15 | # constrain in- and out-coordinates to the appropriate alcove 16 | .intersect(cylinderize(monodromy_alcove, [0, 1, 2, 3], 10)) 17 | .intersect(cylinderize(monodromy_alcove_c2, [0, 7, 8, 9], 10)) 18 | ) 19 | 20 | # constrain interaction coordinates to be of XX-type 21 | biswas_relations = biswas_relations.intersect(Polytope(convex_subpolytopes=[ 22 | ConvexPolytope( 23 | inequalities=[[1, 0, 0, 0, -4, 0, 0, 0, 0, 0]], 24 | equalities=[ 25 | [0, 0, 0, 0, 1, -1, 0, 0, 0, 0], # x1 == x2 26 | [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], # x2 == -x3 27 | ] 28 | ) 29 | ])) 30 | 31 | # switch to canonical coordinates 32 | biswas_relations = monodromy_to_positive_canonical_polytope( 33 | biswas_relations, coordinates=[0, 1, 2, 3]) 34 | biswas_relations = monodromy_to_positive_canonical_polytope( 35 | biswas_relations, coordinates=[0, 4, 5, 6]) 36 | biswas_relations = monodromy_to_positive_canonical_polytope( 37 | biswas_relations, coordinates=[0, 7, 8, 9]) 38 | 39 | # reduce the biswas relations to have following coordinates: 40 | # k a1 a2 a3 beta b1 b2 b3 41 | biswas_relations = biswas_relations.reduce() 42 | biswas_relations = project(biswas_relations, 6).reduce() 43 | biswas_relations = project(biswas_relations, 5).reduce() 44 | 45 | xx_polytope = monodromy.static.examples.identity_polytope 46 | for n in count(1): 47 | print(f"Working on an XX interaction sequence of length {n}...") 48 | 49 | # inflate xx_polytope from [*a_coords, *interaction_coords] to [*a_coords, *b_coords, *interaction_coords, beta] 50 | xx_polytope = cylinderize( 51 | xx_polytope, 52 | coordinate_map=[0, 1, 2, 3] + list(range(7, 7 + (n - 1))), 53 | parent_dimension=1 + 3 + 3 + n, 54 | ).intersect(cylinderize( 55 | biswas_relations, 56 | coordinate_map=[0, 1, 2, 3, -1, 4, 5, 6], 57 | parent_dimension=1 + 3 + 3 + n, 58 | )) 59 | 60 | # project away the old a-coordinates 61 | start_time = perf_counter() 62 | print("Working on the reduction 1/3...", end="") 63 | xx_polytope = project(xx_polytope, 3).reduce() 64 | print(f" done. Took {perf_counter() - start_time} seconds.") 65 | 66 | start_time = perf_counter() 67 | print("Working on the reduction 2/3...", end="") 68 | xx_polytope = project(xx_polytope, 2).reduce() 69 | print(f" done. Took {perf_counter() - start_time} seconds.") 70 | 71 | start_time = perf_counter() 72 | print("Working on the reduction 3/3...", end="") 73 | xx_polytope = project(xx_polytope, 1).reduce() 74 | print(f" done. Took {perf_counter() - start_time} seconds.") 75 | 76 | # now the old c-coordinates are sitting where the a-coordinates were! 77 | print("The first three coordinates are the canonical coordinates CAN(x1, x2, x3).") 78 | print("The remaining coordinates x4, ..., xk are the XX interaction strengths.") 79 | print(xx_polytope) 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # I'd have preferred a setup.cfg, but `pip -e` rejects it. 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="monodromy", # Replace with your own username 10 | version="0.0.1", 11 | author="Eric Peterson", 12 | author_email="Eric.Peterson@ibm.com", 13 | description="Computations in the monodromy polytope for quantum gate sets", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.ibm.com/IBM-Q-Software/monodromy", 17 | project_urls={ 18 | "Bug Tracker": "https://github.ibm.com/IBM-Q-Software/monodromy/issues", 19 | }, 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | # "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | packages=setuptools.find_packages(where="src"), 26 | python_requires=">=3.6", 27 | ) 28 | -------------------------------------------------------------------------------- /test/test_coordinates.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_coordinates.py 3 | 4 | Tests for monodromy/coordinates.py . 5 | """ 6 | 7 | from fractions import Fraction 8 | import unittest 9 | 10 | import ddt 11 | import qiskit 12 | 13 | from monodromy.coordinates import * 14 | from monodromy.static.examples import exactly 15 | 16 | epsilon = 0.001 17 | 18 | 19 | @ddt.ddt 20 | class TestMonodromyCoordinates(unittest.TestCase): 21 | """Check various coordinate routines.""" 22 | 23 | def assertApproximate(self, a, b): 24 | self.assertTrue(np.all(np.abs(np.array(a) - np.array(b)) < epsilon), 25 | msg=f"a: {a}\nb: {b}\n") 26 | 27 | @ddt.data((qiskit.circuit.library.SwapGate().to_matrix(), 28 | (1/4, 1/4, 1/4, -3/4)), 29 | (qiskit.circuit.library.CXGate().to_matrix(), 30 | (1/4, 1/4, -1/4, -1/4)),) 31 | @ddt.unpack 32 | def test_unitary_to_monodromy_coordinate(self, matrix, target): 33 | """Check the monodromy coordinates of some operators.""" 34 | self.assertApproximate( 35 | np.array(target), 36 | np.array(unitary_to_monodromy_coordinate(matrix)) 37 | ) 38 | 39 | @ddt.data( 40 | ((1/4, 1/4, -1/4), (np.pi / 4, 0, 0)), # CZ 41 | ((1/2, 0, 0), (np.pi/4, np.pi/4, 0)) # ISWAP 42 | ) 43 | @ddt.unpack 44 | def test_monodromy_to_positive_canonical_coordinate( 45 | self, in_coord, out_coord 46 | ): 47 | """Check the conversion from monodromy to positive canonical coords.""" 48 | self.assertApproximate( 49 | out_coord, 50 | monodromy_to_positive_canonical_coordinate(*in_coord) 51 | ) 52 | 53 | @ddt.data( 54 | ((np.pi / 4, 0, 0), (1 / 4, 1 / 4, -1 / 4)), # CZ 55 | ((np.pi / 4, np.pi / 4, 0), (1 / 2, 0, 0)) # ISWAP 56 | ) 57 | @ddt.unpack 58 | def test_positive_canonical_to_monodromy_coordinate( 59 | self, in_coord, out_coord 60 | ): 61 | """Check the conversion from positive canonical to monodromy coords.""" 62 | self.assertApproximate( 63 | out_coord, 64 | positive_canonical_to_monodromy_coordinate(*in_coord) 65 | ) 66 | 67 | @ddt.data( 68 | (monodromy_alcove_c2, positive_canonical_alcove_c2), 69 | (monodromy_alcove, positive_canonical_alcove), 70 | (exactly(Fraction(1, 4), Fraction(1, 4), -Fraction(1, 4)), 71 | exactly(Fraction(1, 2), Fraction(0), Fraction(0))), # CZ 72 | ) 73 | @ddt.unpack 74 | def test_monodromy_to_positive_canonical_polytope(self, input, expected): 75 | """Check the conversion of _polytopes_ from monodromy to positive canonical coords.""" 76 | result = monodromy_to_positive_canonical_polytope(input) 77 | self.assertTrue(expected.contains(result) and 78 | result.contains(expected)) 79 | -------------------------------------------------------------------------------- /test/test_coverage.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_coverage.py 3 | 4 | Tests for monodromy/coverage.py . 5 | """ 6 | 7 | import ddt 8 | import unittest 9 | 10 | from monodromy.polytopes import ConvexPolytope, PolytopeVolume 11 | from monodromy.coverage import * 12 | from monodromy.static.examples import exactly, identity_polytope 13 | 14 | 15 | @ddt.ddt 16 | class TestMonodromyCoverage(unittest.TestCase): 17 | """Check various coverage set routines.""" 18 | 19 | def test_cx_coverage(self): 20 | """Test Section III of quant-ph/0308045 .""" 21 | cx_polytope = CircuitPolytope( 22 | convex_subpolytopes=exactly( 23 | Fraction(1, 4), Fraction(1, 4), Fraction(-1, 4) 24 | ).convex_subpolytopes, 25 | cost=1, 26 | operations=["CX"] 27 | ) 28 | coverage_set = build_coverage_set([cx_polytope]) 29 | self.assertEqual( 30 | {(), ("CX",), ("CX", "CX"), ("CX", "CX", "CX")}, 31 | {tuple(item.operations) for item in coverage_set} 32 | ) 33 | cx0_polytope = next(p for p in coverage_set if p.operations == []) 34 | cx1_polytope = next(p for p in coverage_set if p.operations == ["CX"]) 35 | cx2_polytope = next(p for p in coverage_set if p.operations == ["CX", "CX"]) 36 | cx3_polytope = next(p for p in coverage_set if p.operations == ["CX", "CX", "CX"]) 37 | self.assertTrue(cx3_polytope.contains(cx2_polytope)) 38 | self.assertFalse(cx2_polytope.contains(cx3_polytope)) 39 | self.assertTrue(cx2_polytope.contains(cx1_polytope)) 40 | self.assertFalse(cx1_polytope.contains(cx2_polytope)) 41 | self.assertFalse(cx1_polytope.contains(cx0_polytope)) 42 | self.assertFalse(cx0_polytope.contains(cx1_polytope)) 43 | self.assertTrue(identity_polytope.contains(cx0_polytope)) 44 | self.assertTrue(cx3_polytope.contains(monodromy_alcove_c2)) 45 | self.assertTrue(cx_polytope.contains(cx1_polytope)) 46 | 47 | expected_cx2 = Polytope(convex_subpolytopes=[ 48 | ConvexPolytope.convex_hull( 49 | [[Fraction(0), Fraction(0), Fraction(0)], 50 | [Fraction(1, 2), Fraction(1, 2), -Fraction(1, 2)], 51 | [Fraction(1, 2), Fraction(0), Fraction(0)]] 52 | ) 53 | ]) 54 | self.assertTrue(expected_cx2.contains(cx2_polytope)) 55 | self.assertTrue(cx2_polytope.contains(expected_cx2)) 56 | 57 | def test_sqrtcx_coverage(self): 58 | """Test Example 49 of Peterson-Crooks-Smith.""" 59 | sqrtcx_polytope = CircuitPolytope( 60 | convex_subpolytopes=exactly( 61 | Fraction(1, 8), Fraction(1, 8), Fraction(-1, 8) 62 | ).convex_subpolytopes, 63 | cost=1/2 + 1e-10, 64 | operations=["sqrtCX"] 65 | ) 66 | coverage_set = build_coverage_set([sqrtcx_polytope]) 67 | 68 | self.assertEqual( 69 | {tuple(x.operations) for x in coverage_set}, 70 | {tuple("sqrtCX" for _ in range(0, j)) for j in range(0, 6 + 1)} 71 | ) 72 | 73 | expected_volumes = { 74 | ('sqrtCX',) * 0: PolytopeVolume(dimension=0, volume=Fraction(1, 1)), 75 | ('sqrtCX',) * 1: PolytopeVolume(dimension=0, volume=Fraction(2, 1)), 76 | ('sqrtCX',) * 2: PolytopeVolume(dimension=2, volume=Fraction(1, 16)), 77 | ('sqrtCX',) * 3: PolytopeVolume(dimension=3, volume=Fraction(1, 96)), 78 | ('sqrtCX',) * 4: PolytopeVolume(dimension=3, volume=Fraction(5, 288)), 79 | ('sqrtCX',) * 5: PolytopeVolume(dimension=3, volume=Fraction(47, 2304)), 80 | ('sqrtCX',) * 6: PolytopeVolume(dimension=3, volume=Fraction(1, 48)) 81 | } 82 | 83 | for x in coverage_set: 84 | self.assertEqual(x.volume, expected_volumes[tuple(x.operations)]) 85 | 86 | def test_cx_iswap_cphase_comparison(self): 87 | """Test Lemma 46 of Peterson-Crooks-Smith.""" 88 | cx_polytope = CircuitPolytope( 89 | convex_subpolytopes=exactly( 90 | Fraction(1, 4), Fraction(1, 4), Fraction(-1, 4) 91 | ).convex_subpolytopes, 92 | cost=1, 93 | operations=["CX"] 94 | ) 95 | iswap_polytope = CircuitPolytope( 96 | convex_subpolytopes=exactly( 97 | Fraction(1, 2), Fraction(0), Fraction(0), 98 | ).convex_subpolytopes, 99 | cost=1, 100 | operations=["ISWAP"] 101 | ) 102 | cphase_polytope = CircuitPolytope( 103 | convex_subpolytopes=[ConvexPolytope( 104 | inequalities=[[0, 1, 0, 0], [1, -2, 0, 0]], 105 | equalities=[[0, 1, -1, 0], [0, 1, 0, 1]] 106 | )], 107 | cost=1, 108 | operations=["CPHASE"], 109 | ) 110 | cx_coverage_set = build_coverage_set([cx_polytope]) 111 | iswap_coverage_set = build_coverage_set([iswap_polytope]) 112 | cphase_coverage_set = build_coverage_set([cphase_polytope]) 113 | 114 | # compare depth 2 polytopes 115 | cx2_polytope = next(p for p in cx_coverage_set if 2 == len(p.operations)) 116 | iswap2_polytope = next(p for p in iswap_coverage_set if 2 == len(p.operations)) 117 | cphase2_polytope = next(p for p in cphase_coverage_set if 2 == len(p.operations)) 118 | self.assertTrue(cx2_polytope.contains(iswap2_polytope)) 119 | self.assertTrue(iswap2_polytope.contains(cphase2_polytope)) 120 | self.assertTrue(cphase2_polytope.contains(cx2_polytope)) 121 | 122 | # compare depth 3 polytopes 123 | cx3_polytope = next(p for p in cx_coverage_set if 3 == len(p.operations)) 124 | iswap3_polytope = next(p for p in iswap_coverage_set if 3 == len(p.operations)) 125 | cphase3_polytope = next(p for p in cphase_coverage_set if 3 == len(p.operations)) 126 | self.assertTrue(cx3_polytope.contains(iswap3_polytope)) 127 | self.assertTrue(iswap3_polytope.contains(cphase3_polytope)) 128 | self.assertTrue(cphase3_polytope.contains(cx3_polytope)) 129 | -------------------------------------------------------------------------------- /test/test_elimination.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_elimination.py 3 | 4 | Tests for monodromy/elimination.py . 5 | """ 6 | 7 | import qiskit 8 | 9 | import ddt 10 | import unittest 11 | 12 | from monodromy.coordinates import monodromy_alcove_c2_pcs 13 | from monodromy.elimination import * 14 | from monodromy.polytopes import make_convex_polytope 15 | 16 | 17 | @ddt.ddt 18 | class TestMonodromyElimination(unittest.TestCase): 19 | """Check various elimination routines.""" 20 | 21 | def test_cube_from_cylinders(self): 22 | """Build a cube out of cylinderized intervals.""" 23 | 24 | interval = make_convex_polytope([ 25 | [0, 1], [1, -1] 26 | ]) 27 | 28 | cube = cylinderize(interval, [0, 1], 4) \ 29 | .intersect(cylinderize(interval, [0, 2], 4)) \ 30 | .intersect(cylinderize(interval, [0, 3], 4)) 31 | 32 | expected = make_convex_polytope([ 33 | [0, 1, 0, 0], [1, -1, 0, 0], 34 | [0, 0, 1, 0], [1, 0, -1, 0], 35 | [0, 0, 0, 1], [1, 0, 0, -1], 36 | ]) 37 | 38 | self.assertTrue(cube.contains(expected)) 39 | self.assertTrue(expected.contains(cube)) 40 | 41 | def test_project_cylinderize_inverses(self): 42 | """Test that projection after cylinderization is a NOP.""" 43 | original = monodromy_alcove_c2_pcs 44 | cylinderized = cylinderize(original, [0, 1, 2, 3, 4], 5) 45 | projected = project(cylinderized, 4) 46 | self.assertTrue(projected.contains(original)) 47 | self.assertTrue(original.contains(projected)) 48 | -------------------------------------------------------------------------------- /test/test_haar.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_haar.py 3 | 4 | Tests for monodromy/haar.py . 5 | """ 6 | 7 | import ddt 8 | import unittest 9 | 10 | from fractions import Fraction 11 | 12 | from monodromy.coordinates import positive_canonical_alcove_c2 13 | from monodromy.coverage import build_coverage_set, CircuitPolytope 14 | from monodromy.haar import * 15 | from monodromy.static.examples import exactly 16 | 17 | epsilon = 0.001 18 | 19 | 20 | @ddt.ddt 21 | class TestMonodromyHaar(unittest.TestCase): 22 | """Check various Haar volume routines.""" 23 | 24 | def test_alcove_haar_volume(self): 25 | """Check that the alcove has unit Haar volume.""" 26 | self.assertEqual(1.0, haar_volume(positive_canonical_alcove_c2)) 27 | 28 | def test_expected_interaction_strength(self): 29 | """Check that the Haar-random expected interaction strength is 3/2.""" 30 | left_expected_interaction_strength = haar_volume( 31 | positive_canonical_alcove_c2.intersect(make_convex_polytope([ 32 | [1, -2, 0, 0] 33 | ])), 34 | Polynomial.from_linear_list([0, 1, 1, 1]) 35 | ) 36 | right_expected_interaction_strength = haar_volume( 37 | positive_canonical_alcove_c2.intersect(make_convex_polytope([ 38 | [-1, 2, 0, 0] 39 | ])), 40 | Polynomial.from_linear_list([np.pi, -1, 1, 1]) 41 | ) 42 | 43 | self.assertAlmostEqual( 44 | left_expected_interaction_strength, 45 | right_expected_interaction_strength, 46 | delta=epsilon 47 | ) 48 | 49 | self.assertAlmostEqual( 50 | left_expected_interaction_strength + right_expected_interaction_strength, 51 | (np.pi / 2) * 3 / 2 52 | ) 53 | 54 | def test_cost_statistics(self): 55 | """ 56 | Check that Haar-random cost statistics are computed correctly in a known 57 | example. 58 | """ 59 | cx_polytope = CircuitPolytope( 60 | convex_subpolytopes=exactly( 61 | Fraction(1, 4), Fraction(1, 4), Fraction(-1, 4) 62 | ).convex_subpolytopes, 63 | cost=1, 64 | operations=["CX"] 65 | ) 66 | coverage_set = build_coverage_set([cx_polytope]) 67 | statistics = cost_statistics(coverage_set, 0, 1) 68 | expected = { 69 | "average_cost": 3.0, 70 | "average_overshot": 1.5, 71 | "sigma_cost": 0.0, 72 | "sigma_overshot": 0.28527, 73 | } 74 | for k in statistics.keys(): 75 | self.assertAlmostEqual( 76 | statistics[k], expected[k], delta=epsilon 77 | ) 78 | -------------------------------------------------------------------------------- /test/test_polynomials.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_polynomials.py 3 | 4 | Tests for monodromy/polynomials.py . 5 | """ 6 | 7 | import ddt 8 | import unittest 9 | 10 | import numpy as np 11 | 12 | from monodromy.polynomials import * 13 | 14 | epsilon = 0.001 15 | 16 | 17 | @ddt.ddt 18 | class TestMonodromyPolynomials(unittest.TestCase): 19 | """Check various polynomial routines.""" 20 | 21 | @ddt.data((Polynomial.from_coefficient_list([1, 2]), 22 | Polynomial.from_coefficient_list([3, 4, 5]), 23 | Polynomial.from_coefficient_list([4, 6, 5])), 24 | (Polynomial.from_linear_list([1, 1, 0, 2]), 25 | Polynomial.from_linear_list([3, 0, 2, 1]), 26 | Polynomial.from_linear_list([4, 1, 2, 3]))) 27 | @ddt.unpack 28 | def test_sums(self, left_addend, right_addend, result): 29 | """Check that polynomials add.""" 30 | self.assertEqual(left_addend + right_addend, result) 31 | 32 | @ddt.data((Polynomial.from_coefficient_list([1, 2]), 33 | Polynomial.from_coefficient_list([3, 4, 5]), 34 | Polynomial.from_coefficient_list([-2, -2, -5])), 35 | (Polynomial.from_linear_list([1, 1, 0, 2]), 36 | Polynomial.from_linear_list([3, 0, 2, 1]), 37 | Polynomial.from_linear_list([-2, 1, -2, 1]))) 38 | @ddt.unpack 39 | def test_differences(self, minuend, subtrahend, result): 40 | """Check that polynomials subtract.""" 41 | self.assertEqual(minuend - subtrahend, result) 42 | 43 | @ddt.data((Polynomial.from_coefficient_list([1, 1]), 44 | Polynomial.from_coefficient_list([1, 1]), 45 | Polynomial.from_coefficient_list([1, 2, 1])), 46 | (Polynomial.from_coefficient_list([1, 2, 1]), 47 | Polynomial.from_coefficient_list([1, 2, 1]), 48 | Polynomial.from_coefficient_list([1, 4, 6, 4, 1])),) 49 | @ddt.unpack 50 | def test_products(self, left_factor, right_factor, result): 51 | """Check that polynomials multiply.""" 52 | self.assertEqual(left_factor * right_factor, result) 53 | 54 | @ddt.data((Polynomial.from_coefficient_list([1, 2, 1]), 55 | 0, 1, 56 | Polynomial.from_coefficient_list([4])), 57 | (Polynomial.from_coefficient_list([1, 2, 1]), 58 | 0, 0, 59 | Polynomial.from_coefficient_list([1])), 60 | (Polynomial.from_coefficient_list([1, 2, 1]), 61 | 0, -1, 62 | Polynomial.from_coefficient_list([0])), 63 | (Polynomial.from_coefficient_list([1, 2, 1]), 64 | 1, 1, 65 | Polynomial.from_coefficient_list([1, 2, 1])), 66 | (Polynomial.from_linear_list([1, 2, 5, 13]), 67 | 0, 1, 68 | Polynomial.from_linear_list([3, 0, 5, 13])), 69 | (Polynomial.from_linear_list([1, 2, 5, 13]), 70 | 2, 2, 71 | Polynomial.from_linear_list([27, 2, 5])), 72 | (Polynomial.from_linear_list([1, 2, 5, 13]), 73 | 4, 1, 74 | Polynomial.from_linear_list([1, 2, 5, 13])), 75 | ) 76 | @ddt.unpack 77 | def test_evaluations(self, polynomial, variable, value, result): 78 | """Check that polynomials evaluate.""" 79 | self.assertEqual(polynomial.evaluate(variable, value), result) 80 | 81 | @ddt.data((Polynomial.from_coefficient_list([1, 1, -1]), 0, 82 | Polynomial.from_coefficient_list([0, 1, 1/2, -1/3])),) 83 | @ddt.unpack 84 | def test_indefinite_integrals(self, integrand, variable, result): 85 | """Check that polynomials have primitives.""" 86 | self.assertEqual(integrand.indefinite_integral(variable), result) 87 | 88 | @ddt.data((Polynomial.from_coefficient_list([1, 2, -3]), 89 | 0, -1, 1, 90 | Polynomial.from_coefficient_list([0])), ) 91 | @ddt.unpack 92 | def test_definite_integrals(self, integrand, variable, lower, upper, result): 93 | """Check that polynomials have areas.""" 94 | self.assertEqual(integrand.definite_integral(variable, lower, upper), result) 95 | 96 | @ddt.data((Polynomial.from_coefficient_list([1, 2, -3]), 0, 97 | Polynomial.from_coefficient_list([2, -6])), 98 | (Polynomial.from_linear_list([1, 2, 3, 4]), 0, 99 | Polynomial.from_linear_list([2]))) 100 | @ddt.unpack 101 | def test_derivatives(self, integrand, variable, result): 102 | """Check that polynomials have derivatives.""" 103 | self.assertEqual(integrand.derivative(variable), result) 104 | 105 | @ddt.data((Polynomial.from_coefficient_list([1, 2, 3]), 0), 106 | (Polynomial.from_linear_list([1, 2, 3]), 1), 107 | (Polynomial.from_coefficient_list([1, 2, 3]), 2), 108 | (Polynomial.from_linear_list([1, 2, 3]), 5)) 109 | @ddt.unpack 110 | def test_ftc(self, polynomial, variable): 111 | self.assertEqual( 112 | polynomial, 113 | polynomial.indefinite_integral(variable) 114 | .derivative(variable) 115 | ) 116 | 117 | 118 | @ddt.ddt 119 | class TestMonodromyTrigPolynomials(unittest.TestCase): 120 | """Check various trig polynomial routines.""" 121 | 122 | @ddt.data((Polynomial.from_coefficient_list([1, 2, 3]), 123 | "sin", Polynomial.from_coefficient_list([-np.pi, np.pi]), 124 | 0, 0, 1, 125 | (12 - 7 * np.pi ** 2) / (np.pi ** 3)),) 126 | @ddt.unpack 127 | def test_definite_integrals( 128 | self, coefficients, trig_fn, arguments, 129 | variable, lower, upper, result 130 | ): 131 | """Test that trig polynomials can be integrated.""" 132 | polynomial = TrigPolynomial( 133 | coefficients=coefficients, 134 | arguments=arguments, 135 | trig_fn=trig_fn, 136 | ) 137 | primitives = polynomial.integrate(variable, lower, upper) 138 | self.assertTrue(abs( 139 | result - 140 | sum([primitive.to_number() for primitive in primitives]) 141 | ) < epsilon) 142 | -------------------------------------------------------------------------------- /test/test_polytopes.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_polytopes.py 3 | 4 | Tests for monodromy/polytopes.py . 5 | """ 6 | 7 | import ddt 8 | import unittest 9 | 10 | from monodromy.polytopes import * 11 | 12 | epsilon = 0.001 13 | 14 | 15 | @ddt.ddt 16 | class TestMonodromyConvexPolytopes(unittest.TestCase): 17 | """Check various convex polytope routines.""" 18 | 19 | cube = ConvexPolytope( 20 | inequalities=[ 21 | [1, -1, 0, 0], 22 | [1, 1, 0, 0], 23 | [1, 0, -1, 0], 24 | [1, 0, 1, 0], 25 | [1, 0, 0, -1], 26 | [1, 0, 0, 1], 27 | ], 28 | ) 29 | 30 | def test_volume(self): 31 | self.assertEqual(PolytopeVolume(3, Fraction(8)), 32 | self.cube.volume) 33 | 34 | def test_vertices(self): 35 | self.assertEqual( 36 | set([tuple(v) for v in self.cube.vertices]), 37 | {(-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (-1, 1, 1), 38 | ( 1, -1, -1), ( 1, -1, 1), ( 1, 1, -1), ( 1, 1, 1)}) 39 | 40 | def test_triangulation(self): 41 | tetrahedralized_cube = [] 42 | for tetrahedron_indices in self.cube.triangulation: 43 | tetrahedron_vertices = [self.cube.vertices[i] for i in tetrahedron_indices] 44 | tetrahedron = ConvexPolytope.convex_hull(tetrahedron_vertices) 45 | tetrahedralized_cube.append(tetrahedron) 46 | self.assertTrue(self.cube.contains(tetrahedron)) 47 | 48 | total_volume = sum((x.volume for x in tetrahedralized_cube), 49 | PolytopeVolume(3, Fraction(0))) 50 | self.assertEqual(total_volume, PolytopeVolume(3, Fraction(8))) 51 | 52 | def test_reduce(self): 53 | overspecified_cube = ConvexPolytope( 54 | inequalities=self.cube.inequalities + [ 55 | [5, 1, 0, 0], [5, 0, 1, 0], [5, 0, 0, 1], 56 | ] 57 | ) 58 | reduced_cube = overspecified_cube.reduce() 59 | self.assertTrue(reduced_cube.contains(self.cube)) 60 | self.assertTrue(self.cube.contains(reduced_cube)) 61 | self.assertTrue(all([x in self.cube.inequalities 62 | for x in reduced_cube.inequalities])) 63 | 64 | def test_intersect(self): 65 | shifted_cube = ConvexPolytope(inequalities=[ 66 | [0, 1, 0, 0], 67 | [2, -1, 0, 0], 68 | [0, 0, 1, 0], 69 | [2, 0, -1, 0], 70 | [0, 0, 0, 1], 71 | [2, 0, 0, -1], 72 | ]) 73 | intersected_cubes = self.cube.intersect(shifted_cube).reduce() 74 | clipped_cube = ConvexPolytope(inequalities=[ 75 | [0, 1, 0, 0], 76 | [1, -1, 0, 0], 77 | [0, 0, 1, 0], 78 | [1, 0, -1, 0], 79 | [0, 0, 0, 1], 80 | [1, 0, 0, -1], 81 | ]) 82 | self.assertTrue(clipped_cube.contains(intersected_cubes)) 83 | self.assertTrue(intersected_cubes.contains(clipped_cube)) 84 | 85 | def test_empty_intersection(self): 86 | unsatisfiable_polytope = self.cube.intersect(ConvexPolytope( 87 | inequalities=[[-9, 1, 0, 0]]) 88 | ) 89 | with self.assertRaises(NoFeasibleSolutions): 90 | unsatisfiable_polytope.reduce() 91 | 92 | def test_has_element(self): 93 | self.assertTrue(self.cube.has_element([0, 0, 0])) 94 | self.assertFalse(self.cube.has_element([10, -10, 20])) 95 | 96 | 97 | @ddt.ddt 98 | class TestMonodromyPolytopes(unittest.TestCase): 99 | """Check various non-convex polytope routines.""" 100 | 101 | overlapping_cubes = make_convex_polytope([ 102 | [1, -1, 0, 0], # cube [-1, 1]^(x 3) 103 | [1, 1, 0, 0], 104 | [1, 0, -1, 0], 105 | [1, 0, 1, 0], 106 | [1, 0, 0, -1], 107 | [1, 0, 0, 1], 108 | ]).union(make_convex_polytope([ 109 | [0, 1, 0, 0], # cube [0, 2]^(x 3) 110 | [2, -1, 0, 0], 111 | [0, 0, 1, 0], 112 | [2, 0, -1, 0], 113 | [0, 0, 0, 1], 114 | [2, 0, 0, -1], 115 | ])) 116 | 117 | def test_volume(self): 118 | self.assertEqual(PolytopeVolume(3, Fraction(15)), 119 | self.overlapping_cubes.volume) 120 | 121 | def test_reduce_eliminates_convex_components(self): 122 | redundant_polytope = self.overlapping_cubes.union(make_convex_polytope([ 123 | [0, 1, 0, 0], # cube [0, 1]^(x 3) 124 | [1, -1, 0, 0], 125 | [0, 0, 1, 0], 126 | [1, 0, -1, 0], 127 | [0, 0, 0, 1], 128 | [1, 0, 0, -1], 129 | ])) 130 | result = redundant_polytope.reduce() 131 | self.assertTrue(self.overlapping_cubes.contains(result)) 132 | self.assertTrue(result.contains(self.overlapping_cubes)) 133 | self.assertTrue(len(result.convex_subpolytopes) < 134 | len(redundant_polytope.convex_subpolytopes)) 135 | 136 | def test_union_PIE(self): 137 | self.assertEqual( 138 | self.overlapping_cubes.volume, ( 139 | self.overlapping_cubes.convex_subpolytopes[0].volume + 140 | self.overlapping_cubes.convex_subpolytopes[1].volume - 141 | self.overlapping_cubes.convex_subpolytopes[0].intersect( 142 | self.overlapping_cubes.convex_subpolytopes[1] 143 | ).volume 144 | ) 145 | ) 146 | self.assertTrue(self.overlapping_cubes.contains( 147 | make_convex_polytope( 148 | self.overlapping_cubes.convex_subpolytopes[0].inequalities 149 | ) 150 | )) 151 | self.assertTrue(self.overlapping_cubes.contains( 152 | make_convex_polytope( 153 | self.overlapping_cubes.convex_subpolytopes[1].inequalities 154 | ) 155 | )) 156 | 157 | def test_self_intersection(self): 158 | self.assertEqual( 159 | self.overlapping_cubes.volume, 160 | self.overlapping_cubes.intersect( 161 | self.overlapping_cubes 162 | ).volume 163 | ) 164 | -------------------------------------------------------------------------------- /test/test_volume.py: -------------------------------------------------------------------------------- 1 | """ 2 | test/test_volume.py 3 | 4 | Tests for monodromy/volume.py . 5 | """ 6 | 7 | import qiskit 8 | 9 | import ddt 10 | import unittest 11 | 12 | from monodromy.polytopes import make_convex_polytope 13 | from monodromy.volume import * 14 | 15 | epsilon = 0.001 16 | 17 | 18 | @ddt.ddt 19 | class TestMonodromyVolume(unittest.TestCase): 20 | """Check various volume routines.""" 21 | 22 | def volume_fn(self, dim): 23 | counter = 0 24 | 25 | def volume_fn(convex_polytope): 26 | nonlocal counter 27 | counter += 1 28 | if convex_polytope.volume.dimension == dim: 29 | return convex_polytope.volume.volume 30 | elif convex_polytope.volume.dimension < dim: 31 | return 0 32 | else: 33 | raise ValueError("Unexpectedly large volume.") 34 | 35 | def get_counter(): 36 | nonlocal counter 37 | return counter 38 | 39 | return volume_fn, get_counter 40 | 41 | def test_null_efficiency(self): 42 | """Test that empty polytopes have skipped children""" 43 | polytope = make_convex_polytope([ 44 | [0, 1], [1, -1], 45 | ]).union(make_convex_polytope([ 46 | [0, 1], [-1, -1], 47 | ])) 48 | 49 | volume_fn, get_counter = self.volume_fn(1) 50 | alternating_sum(polytope, volume_fn) 51 | naive_count = 2 ** len(polytope.convex_subpolytopes) - 1 52 | self.assertLess(get_counter(), naive_count) 53 | 54 | def test_duplication_efficiency(self): 55 | """Test that equal polytopes have skipped children.""" 56 | polytope = make_convex_polytope([ 57 | [0, 1], [1, -1], # [0, 1] 58 | ]).union(make_convex_polytope([ 59 | [0, 1], [2, -1], # [0, 2] 60 | ])).union(make_convex_polytope([ 61 | [-1, 1], [1, -1] # [-1, 1] 62 | ])) 63 | 64 | volume_fn, get_counter = self.volume_fn(1) 65 | alternating_sum(polytope, volume_fn) 66 | naive_count = 2 ** len(polytope.convex_subpolytopes) - 1 67 | self.assertLess(get_counter(), naive_count) 68 | --------------------------------------------------------------------------------