├── .gitignore ├── .travis.yml ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── examples ├── 1of5.qmasm ├── README.md ├── and4.qmasm ├── circsat.qmasm ├── comparator.qmasm ├── feature-test.qmasm ├── gates.qmasm ├── maze6x6.qmasm ├── qmasm-maze.go └── sort4.qmasm ├── extras ├── README.md ├── qmasm-gen-all-to-all ├── qmasm-gen-chimera ├── qmasm-gen-current ├── qmasm-mode.el └── qmasm.ssh ├── scripts └── qb2qmasm ├── setup.cfg ├── setup.py └── src └── qmasm ├── __init__.py ├── __main__.py ├── assertions.py ├── cmdline.py ├── output.py ├── parse.py ├── problem.py ├── solutions.py ├── solve.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | examples/qmasm-maze 4 | src/qmasm.egg-info/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "3.8" 7 | 8 | before_install: 9 | - pip install --upgrade pip 10 | - pip install --upgrade numpy 11 | 12 | install: 13 | - python setup.py install 14 | 15 | script: 16 | - cd examples 17 | - function run_and_check () { expected="$1"; shift; actual=$("$@" | md5sum | cut -d' ' -f1); if [ "$expected" = "$actual" ] ; then true; else echo "Expected $expected but saw $actual" 1>&2; false; fi; } 18 | - run_and_check 08399934cd8092743d5a69f4ec637701 qmasm --format=qmasm --solver=neal feature-test.qmasm 19 | - run_and_check 38d9318cd4f4d34d1aafadcfca145362 qmasm --solver=tabu 1of5.qmasm 20 | - run_and_check 38d9318cd4f4d34d1aafadcfca145362 qmasm -O2 -v -v --solver=greedy 1of5.qmasm 21 | - run_and_check ac41b2e24401324f610ae0d7a74eb424 qmasm --solver=neal --format=ocean --pin="x10 := true" circsat.qmasm 22 | - run_and_check 5296b7ddbf24c9a2f7785acb782ea2c8 qmasm --solver=exact --run 1of5.qmasm 23 | - test "$(qmasm --solver=exact --run -v 1of5.qmasm 2>&1 | wc -l)" -eq 122 24 | - qmasm --solver=exact --run -v -v and4.qmasm 2>&1 | grep -q '16 excluding duplicate variable assignments' 25 | - test "$(qmasm --samples=1234 -v --solver=neal --run --pin="x10 := true" circsat.qmasm | awk '$2 == "True" && $1 ~ /^x(1|2|10)$/ {good++; next} $2 == "False" && $1 == "x3" {good++; next} $2 ~ /^(True|False)$/ {bad++} END {print good+0, bad+0}')" = "4 0" 26 | - test "$(qmasm --solver=tabu --run -v -v --pin="x10 := true" circsat.qmasm | grep -c PASS:)" -eq 22 27 | - test "$(qmasm --solver=qbsolv,neal --run -v -v --pin="x10 := true" circsat.qmasm | grep -c PASS:)" -eq 22 28 | - qmasm --solver=tabu --run --values=ints --pin="in[4:1] := 1001" sort4.qmasm | grep -q -E 'out.*11000.*24' 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2016, Triad National Security, LLC 2 | All rights reserved. 3 | 4 | This software was produced under U.S. Government contract 89233218CNA000001 for Los Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC for the U.S. Department of Energy/National Nuclear Security Administration. All rights in the program are reserved by Triad National Security, LLC, and the U.S. Department of Energy/National Nuclear Security Administration. The Government is granted for itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license in this material to reproduce, prepare derivative works, distribute copies to the public, perform publicly and display publicly, and to permit others to do so. NEITHER THE GOVERNMENT NOR TRIAD NATIONAL SECURITY, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is modified to produce derivative works, such modified software should be clearly marked, so as not to confuse it with the version available from LANL. 5 | 6 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | * Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.md 3 | recursive-include extras * 4 | recursive-include examples * 5 | exclude examples/qmasm-maze 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QMASM: A Quantum Macro Assembler 2 | ================================ 3 | 4 | [![Build Status](https://travis-ci.org/lanl/qmasm.svg?branch=master)](https://travis-ci.org/lanl/qmasm) 5 | [![PyPI version](https://badge.fury.io/py/qmasm.svg)](https://badge.fury.io/py/qmasm) 6 | 7 | Description 8 | ----------- 9 | 10 | QMASM fills a gap in the software ecosystem for [D-Wave's adiabatic quantum computers](http://www.dwavesys.com/) by shielding the programmer from having to know system-specific hardware details while still enabling programs to be expressed at a fairly low level of abstraction. It is therefore analogous to a conventional macro assembler and can be used in much the same way: as a target either for programmers who want a great deal of control over the hardware or for compilers that implement higher-level languages. 11 | 12 | N.B. This tool used to be called "QASM" but was renamed to avoid confusion with [MIT's QASM](http://www.media.mit.edu/quanta/quanta-web/projects/qasm-tools/), which is used to describe quantum circuits (a different model of quantum computation from what the D-Wave uses) and the [IBM Quantum Experience](http://www.research.ibm.com/quantum/)'s [QASM (now OpenQASM) language](https://github.com/QISKit/openqasm), also used for describing quantum circuits. 13 | 14 | Installation 15 | ------------ 16 | 17 | QMASM is written in Python. The latest release can be downloaded and installed from [PyPI](https://pypi.org/project/qmasm/) via 18 | ```bash 19 | pip install qmasm 20 | ``` 21 | 22 | Alternatively, QMASM can be installed manually from GitHub using the standard [Setuptools](https://setuptools.readthedocs.io/) installation mechanisms. For example, use 23 | ```bash 24 | python setup.py install 25 | ``` 26 | to install QMASM in the default location and 27 | ```bash 28 | python setup.py install --prefix=/my/install/directory 29 | ``` 30 | to install elsewhere. 31 | 32 | Documentation 33 | ------------- 34 | 35 | Documentation for QMASM can be found on the [QMASM wiki](https://github.com/lanl/qmasm/wiki). 36 | 37 | QMASM (then known as QASM) is discussed in the following publication: 38 | 39 | > Scott Pakin. "A Quantum Macro Assembler". In _Proceedings of the 20th Annual IEEE High Performance Extreme Computing Conference_ (HPEC 2016), Waltham, Massachusetts, USA, 13–15 September 2016. DOI: [10.1109/HPEC.2016.7761637](http://dx.doi.org/10.1109/HPEC.2016.7761637). 40 | 41 | 42 | License 43 | ------- 44 | 45 | QMASM is provided under a BSD-ish license with a "modifications must be indicated" clause. See [the LICENSE file](http://github.com/lanl/qmasm/blob/master/LICENSE.md) for the full text. 46 | 47 | This package is part of the Hybrid Quantum-Classical Computing suite, known internally as LA-CC-16-032. 48 | 49 | Author 50 | ------ 51 | 52 | Scott Pakin, 53 | -------------------------------------------------------------------------------- /examples/1of5.qmasm: -------------------------------------------------------------------------------- 1 | ################################################# 2 | # QMASM example: one "on" bit out of five total # 3 | # By Scott Pakin # 4 | ################################################# 5 | 6 | # Point weights 7 | # A-E are the variables we care about. 8 | # $a1-$a3 are ancillary variables. 9 | A -2 10 | B 1 11 | C -2 12 | D -1 13 | E 1 14 | $a1 0 15 | $a2 4 16 | $a3 3 17 | 18 | # Coupler strengths 19 | A B 2 20 | A C 2 21 | A D 2 22 | A E 1 23 | A $a1 1 24 | A $a2 -4 25 | A $a3 -4 26 | B C 2 27 | B D 2 28 | B E 1 29 | B $a1 1 30 | B $a2 -4 31 | B $a3 -1 32 | C D 2 33 | C E 3 34 | C $a1 -1 35 | C $a2 -4 36 | C $a3 -4 37 | D E 4 38 | D $a1 -2 39 | D $a2 -4 40 | D $a3 -3 41 | E $a1 -4 42 | E $a2 0 43 | E $a3 -4 44 | $a1 $a2 -4 45 | $a1 $a3 3 46 | $a2 $a3 0 47 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | QMASM examples 2 | ============= 3 | 4 | This directory contains examples of QMASM code. 5 | 6 | Feature test 7 | ------------ 8 | 9 | * Main file: [`feature-test.qmasm`](feature-test.qmasm) 10 | 11 | * Command line: `qmasm --format=qmasm feature-test.qmasm` 12 | 13 | This program does nothing useful. Rather, it provides a showcase of [all of QMASM's language constructs](//github.com/lanl/qmasm/wiki/File-format). Read the [`feature-test.qmasm` source code](feature-test.qmasm) to see examples of the various mechanisms that a QMASM program can use. 14 | 15 | One of five 16 | ----------- 17 | 18 | * Main file: [`1of5.qmasm`](1of5.qmasm) 19 | 20 | * Command line: `qmasm --run 1of5.qmasm` 21 | 22 | This is a trivial demonstration of QMASM. The program defines variables *A*, *B*, *C*, *D*, and *E* and outputs all (Boolean) values of those in which exactly one variable is *true*: 23 | ``` 24 | Solution #1 (energy = -21.12): 25 | 26 | Name(s) Spin Boolean 27 | ------- ---- ------- 28 | A -1 False 29 | B -1 False 30 | C -1 False 31 | D -1 False 32 | E +1 True 33 | 34 | Solution #2 (energy = -21.12): 35 | 36 | Name(s) Spin Boolean 37 | ------- ---- ------- 38 | A -1 False 39 | B -1 False 40 | C -1 False 41 | D +1 True 42 | E -1 False 43 | 44 | Solution #3 (energy = -21.12): 45 | 46 | Name(s) Spin Boolean 47 | ------- ---- ------- 48 | A -1 False 49 | B -1 False 50 | C +1 True 51 | D -1 False 52 | E -1 False 53 | 54 | Solution #4 (energy = -21.12): 55 | 56 | Name(s) Spin Boolean 57 | ------- ---- ------- 58 | A -1 False 59 | B +1 True 60 | C -1 False 61 | D -1 False 62 | E -1 False 63 | 64 | Solution #5 (energy = -21.12): 65 | 66 | Name(s) Spin Boolean 67 | ------- ---- ------- 68 | A +1 True 69 | B -1 False 70 | C -1 False 71 | D -1 False 72 | E -1 False 73 | ``` 74 | 75 | Try experimenting with the `--pin` option. If a variable is pinned to *true*, QMASM will output the single solution that honors that constraint. If a variable is pinned to *false*, QMASM will output the four solutions. If *two* variables are pinned to *true*, a situation `1of5.qmasm` does not include in its ground state, QMASM may return no solutions or it may return one or more *incorrect* solutions—whatever exhibits the lowest total energy and doesn't break any chains or pins. 76 | 77 | Macro chaining 78 | -------------- 79 | 80 | * Main file: [`and4.qmasm`](and4.qmasm) 81 | 82 | * Command line: `qmasm --run and4.qmasm` 83 | 84 | This is a demonstration of QMASM's multi-instantiation form of `!use_macro`. The program defines a 2-input AND gate (*Y* = *A* AND *B*) called `and_chain` that chains its output (`Y`) to the next macro instance's `A` input. The program then defines a macro called `big_and` that instantiates `and_chain` three times, as `$and1`, `$and2`, and `$and3`, then names its overall inputs (`A`, `B`, `C`, and `D`) and output (`Y`) in terms of its constituent macros' inputs and outputs. The result is a 4-input AND gate. 85 | 86 | ``` 87 | Solution #1 (energy = -21.25, tally = 236): 88 | 89 | Name(s) Spin Boolean 90 | ------------ ---- -------- 91 | big_and.A -1 False 92 | big_and.B -1 False 93 | big_and.C -1 False 94 | big_and.D -1 False 95 | big_and.Y -1 False 96 | 97 | Solution #2 (energy = -21.25, tally = 138): 98 | 99 | Name(s) Spin Boolean 100 | ------------ ---- -------- 101 | big_and.A -1 False 102 | big_and.B -1 False 103 | big_and.C -1 False 104 | big_and.D +1 True 105 | big_and.Y -1 False 106 | 107 | Solution #3 (energy = -21.25, tally = 131): 108 | 109 | Name(s) Spin Boolean 110 | ------------ ---- -------- 111 | big_and.A -1 False 112 | big_and.B -1 False 113 | big_and.C +1 True 114 | big_and.D -1 False 115 | big_and.Y -1 False 116 | ``` 117 |

118 | 119 | ``` 120 | Solution #15 (energy = -21.25, tally = 7): 121 | 122 | Name(s) Spin Boolean 123 | ------------ ---- -------- 124 | big_and.A +1 True 125 | big_and.B +1 True 126 | big_and.C +1 True 127 | big_and.D -1 False 128 | big_and.Y -1 False 129 | 130 | Solution #16 (energy = -21.25, tally = 3): 131 | 132 | Name(s) Spin Boolean 133 | ------------ ---- -------- 134 | big_and.A +1 True 135 | big_and.B +1 True 136 | big_and.C +1 True 137 | big_and.D +1 True 138 | big_and.Y +1 True 139 | ``` 140 | 141 | Circuit satisfiability 142 | ---------------------- 143 | 144 | * Main file: [`circsat.qmasm`](circsat.qmasm) 145 | 146 | * Helper file: [`gates.qmasm`](gates.qmasm) 147 | 148 | * Command line: `qmasm --run --pin="x10 := true" circsat.qmasm` 149 | 150 | Given a Boolean expression with *n* inputs and one output, determine the sets of inputs that make the output *true*. This is a classic NP-complete problem. `circsat.qmasm` represents a particular 3-input Boolean expression borrowed from the [*Introduction to Algorithms*](https://mitpress.mit.edu/books/introduction-algorithms) textbook's discussion of NP-completeness. 151 | 152 | `gates.qmasm` defines macros for various Boolean operators (NOT, 2-input OR, 3-input AND), which are then used by the top-level program, `circsat.qmasm`. `circsat.qmasm` maps inputs *x1*, *x2*, and *x3* to output *x10*. By default, the program returns all sets of inputs and the corresponding output. Pinning *x10* to *true* returns only the solutions to the circuit-satisfiability problem. In this case, there is only one solution: 153 | ``` 154 | Solution #1 (energy = -80.00): 155 | 156 | Name(s) Spin Boolean 157 | ------- ---- ------- 158 | x1 +1 True 159 | x10 +1 True 160 | x2 +1 True 161 | x3 -1 False 162 | ``` 163 | 164 | Sorting 165 | ------- 166 | 167 | * Main file: [`sort4.qmasm`](sort4.qmasm) 168 | 169 | * Helper file: [`comparator.qmasm`](comparator.qmasm) 170 | 171 | * Command line: `qmasm --run --pin="in[1:4] := 0110" sort4.qmasm` 172 | 173 | Sort a list of four 1-bit numbers. `sort4.qmasm` implements a 4-element sorting network from [*Sorting and Searching*](http://www.informit.com/store/art-of-computer-programming-volume-3-sorting-and-searching-9780201896855). Specify values for inputs *in[1]*, *in[2]*, *in[3]*, and *in[4]*, and the program will sort these into *out[1]*, *out[2]*, *out[3]*, and *out[4]*: 174 | ``` 175 | Solution #1 (energy = -122.00, tally = 3): 176 | 177 | Name(s) Spin Boolean 178 | --------- ---- -------- 179 | in[1] -1 False 180 | in[2] +1 True 181 | in[3] +1 True 182 | in[4] -1 False 183 | out[1] -1 False 184 | out[2] -1 False 185 | out[3] -1 False 186 | out[4] +1 True 187 | ``` 188 | 189 | Oops, that didn't work. The D-Wave incorrectly sorted `0 1 1 0` into `0 0 0 1`, due to hardware artifacts such as limited precision or cross-qubit interference. This is a case where optimization postprocessing is useful for transforming nearly correct solutions into truly correct solutions: 190 | ``` 191 | $ qmasm --postproc=opt --run --pin="in[1:4] := 0110" sort4.qmasm 192 | Solution #1 (energy = -93.50, tally = 1000): 193 | 194 | Name(s) Spin Boolean 195 | --------- ---- -------- 196 | in[1] -1 False 197 | in[2] +1 True 198 | in[3] +1 True 199 | in[4] -1 False 200 | out[1] -1 False 201 | out[2] -1 False 202 | out[3] +1 True 203 | out[4] +1 True 204 | ``` 205 | Because there is no clear distinction between inputs and outputs, one can also specify the outputs and receive a list of inputs that would sort to those outputs. For example, `qmasm --run --pin="out[1:4] := 0011" sort4.qmasm` produces the following six solutions: 206 | ``` 207 | Solution #1 (energy = -92.50, tally = 63): 208 | 209 | Name(s) Spin Boolean 210 | --------- ---- -------- 211 | in[1] +1 True 212 | in[2] -1 False 213 | in[3] -1 False 214 | in[4] +1 True 215 | out[1] -1 False 216 | out[2] -1 False 217 | out[3] +1 True 218 | out[4] +1 True 219 | 220 | Solution #2 (energy = -92.50, tally = 34): 221 | 222 | Name(s) Spin Boolean 223 | --------- ---- -------- 224 | in[1] -1 False 225 | in[2] -1 False 226 | in[3] +1 True 227 | in[4] +1 True 228 | out[1] -1 False 229 | out[2] -1 False 230 | out[3] +1 True 231 | out[4] +1 True 232 | 233 | Solution #3 (energy = -92.50, tally = 128): 234 | 235 | Name(s) Spin Boolean 236 | --------- ---- -------- 237 | in[1] +1 True 238 | in[2] -1 False 239 | in[3] +1 True 240 | in[4] -1 False 241 | out[1] -1 False 242 | out[2] -1 False 243 | out[3] +1 True 244 | out[4] +1 True 245 | 246 | Solution #4 (energy = -92.50, tally = 16): 247 | 248 | Name(s) Spin Boolean 249 | --------- ---- -------- 250 | in[1] -1 False 251 | in[2] +1 True 252 | in[3] -1 False 253 | in[4] +1 True 254 | out[1] -1 False 255 | out[2] -1 False 256 | out[3] +1 True 257 | out[4] +1 True 258 | 259 | Solution #5 (energy = -92.50, tally = 56): 260 | 261 | Name(s) Spin Boolean 262 | --------- ---- -------- 263 | in[1] +1 True 264 | in[2] +1 True 265 | in[3] -1 False 266 | in[4] -1 False 267 | out[1] -1 False 268 | out[2] -1 False 269 | out[3] +1 True 270 | out[4] +1 True 271 | 272 | Solution #6 (energy = -92.50, tally = 25): 273 | 274 | Name(s) Spin Boolean 275 | --------- ---- -------- 276 | in[1] -1 False 277 | in[2] +1 True 278 | in[3] +1 True 279 | in[4] -1 False 280 | out[1] -1 False 281 | out[2] -1 False 282 | out[3] +1 True 283 | out[4] +1 True 284 | ``` 285 | That is, it permuted {0, 0, 1, 1} into {1, 0, 0, 1}, {0, 0, 1, 1}, {1, 0, 1, 0}, {0, 1, 0, 1}, {1, 1, 0, 0}, and {0, 1, 1, 0}. 286 | 287 | One can even specify combinations of inputs and outputs. As an exercise, see what solutions `qmasm --run --pin="in[1:3] out[2] := 0110" sort4.qmasm` leads to. 288 | 289 | Shortest path through a maze 290 | ---------------------------- 291 | 292 | * Main file: [`maze6x6.qmasm`](maze6x6.qmasm) 293 | 294 | * Command line: `qmasm --postproc=opt --run maze6x6.qmasm | egrep 'Solution|True'` 295 | 296 | Find the shortest path through a 6×6 maze. 297 | ``` 298 | A B C D E F 299 | +--+--+--+--+--+ + 300 | 1 | | | 301 | + + +--+ + + + 302 | 2 | | | | | | 303 | +--+--+ + + +--+ 304 | 3 | | | | 305 | + +--+--+ + + + 306 | 4 | | | | | | 307 | + +--+ +--+ +--+ 308 | 5 | | 309 | + + + +--+ +--+ 310 | 6 | | | | | 311 | +--+ +--+--+--+--+ 312 | ``` 313 | The cleverness in the implementation is that each room of the maze (macro `room`) constrains the shortest path to traversing either zero or two of the four compass directions. The former case implies that the shortest path does not pass through that room. The latter case implies that the shortest path enters and exits the room exactly once. All other options require more energy. Pinning the ingress and egress to *true* (done within `maze6x6.qmasm` itself) causes the minimal-energy solution to represent the shortest path between the corresponding two rooms. 314 | 315 | The program takes a long time to embed in the Chimera graph so be patient. (`maze6x6.qmasm` can therefore serve as a good test for new embedding algorithms.) When it runs, it finds a single valid solution: 316 | ``` 317 | Solution #1 (energy = -664.00, tally = 1000): 318 | B5.E +1 True 319 | B5.S +1 True 320 | B6.N +1 True 321 | B6.S +1 True 322 | C5.E +1 True 323 | C5.W +1 True 324 | D5.E +1 True 325 | D5.W +1 True 326 | E1.E +1 True 327 | E1.S +1 True 328 | E2.N +1 True 329 | E2.S +1 True 330 | E3.N +1 True 331 | E3.S +1 True 332 | E4.N +1 True 333 | E4.S +1 True 334 | E5.N +1 True 335 | E5.W +1 True 336 | F1.N +1 True 337 | F1.W +1 True 338 | ``` 339 | This can be validated by beginning at the ingress at `F1.N`. The only exit from F1 other than the ingress is `F1.W`, which takes us to `E1`. Because we came from `E1.E` the only other exit is `E1.S`, which takes us to `E2`. The process continues until we reach the egress at `B6.S`. 340 | 341 | If you want to experiment with other mazes, [`qmasm-maze.go`](qmasm-maze.go) is the source code for a maze-generating program written in [Go](https://golang.org/). Once you've installed a Go compiler, build `qmasm-maze` with 342 | ```bash 343 | go get github.com/spakin/disjoint 344 | go build qmasm-maze.go 345 | ``` 346 | In case you're unfamiliar with Go, the first line of the above installs a dependency, and the second line compiles the program. You'll need to set your `GOPATH` environment variable so `go` knows where to install to (e.g., `export GOPATH=$HOME/go`). 347 | 348 | Once compiled, `qmasm-maze` accepts a pair of dimensions (the number of rooms wide and tall) to use for the maze and outputs QMASM code: 349 | ```bash 350 | ./qmasm-maze gen 6 6 > my-maze.qmasm 351 | ``` 352 | 353 | `qmasm-maze` can also validate solutions produced by running the code. For example, the solution shown above can be seen to be correct: 354 | ``` 355 | $ qmasm --postproc=opt --run maze6x6.qmasm | ./qmasm-maze validate 356 | Solution 1: F1 -- E1 -- E2 -- E3 -- E4 -- E5 -- D5 -- C5 -- B5 -- B6 357 | ``` 358 | Without optimization postprocessing, QMASM frequently returns invalid paths through the maze: 359 | ``` 360 | $ ./qmasm-maze gen 6 6 | qmasm --run 2>&1 | ./qmasm-maze validate 361 | Solution 1: [Room F1 contains 1 exit(s) but should have 0 or 2] 362 | Solution 2: [Room E1 contains 1 exit(s) but should have 0 or 2] 363 | ``` 364 | -------------------------------------------------------------------------------- /examples/and4.qmasm: -------------------------------------------------------------------------------- 1 | ################################################# 2 | # QMASM example: Chain 2-input AND gates # 3 | # into a 4-input AND gate # 4 | # # 5 | # By Scott Pakin # 6 | ################################################# 7 | 8 | !begin_macro and_chain 9 | A -0.5 10 | B -0.5 11 | Y 1 12 | 13 | A B 0.5 14 | A Y -1 15 | B Y -1 16 | Y !next.A -1 # Connect our output to the subsequent A input 17 | !end_macro and_chain 18 | 19 | !begin_macro big_and 20 | !use_macro and_chain $and1 $and2 $and3 21 | A = $and1.A 22 | B = $and1.B 23 | C = $and2.B 24 | D = $and3.B 25 | Y = $and3.Y 26 | !end_macro big_and 27 | 28 | !use_macro big_and big_and 29 | -------------------------------------------------------------------------------- /examples/circsat.qmasm: -------------------------------------------------------------------------------- 1 | # Solve a circuit-satisfiability problem. 2 | 3 | !include 4 | 5 | !use_macro not1 not_x4 6 | not_x4.$A = x3 7 | not_x4.$Y = $x4 8 | 9 | !use_macro or2 or_x5 10 | or_x5.$A = x1 11 | or_x5.$B = x2 12 | or_x5.$Y = $x5 13 | 14 | !use_macro not1 not_x6 15 | not_x6.$A = $x4 16 | not_x6.$Y = $x6 17 | 18 | !use_macro and3 and_x7 19 | and_x7.$A = x1 20 | and_x7.$B = x2 21 | and_x7.$C = $x4 22 | and_x7.$Y = $x7 23 | 24 | !use_macro or2 or_x8 25 | or_x8.$A = $x5 26 | or_x8.$B = $x6 27 | or_x8.$Y = $x8 28 | 29 | !use_macro or2 or_x9 30 | or_x9.$A = $x6 31 | or_x9.$B = $x7 32 | or_x9.$Y = $x9 33 | 34 | !use_macro and3 and_x10 35 | and_x10.$A = $x8 36 | and_x10.$B = $x9 37 | and_x10.$C = $x7 38 | and_x10.$Y = x10 39 | -------------------------------------------------------------------------------- /examples/comparator.qmasm: -------------------------------------------------------------------------------- 1 | #################################### 2 | # Comparator for a sorting network # 3 | # By Scott Pakin # 4 | #################################### 5 | 6 | # Semantics: 7 | # 8 | # IF $a < $b THEN 9 | # $min = $a 10 | # $max = $b 11 | # ELSE 12 | # $min = $b 13 | # $max = $a 14 | # ENDIF 15 | 16 | !begin_macro comparator 17 | $a 0 18 | $b 0 19 | $min 1 20 | $max -1 21 | 22 | $a $b 1 23 | $a $min -1 24 | $a $max -0.5 25 | $b $min -1 26 | $b $max -0.5 27 | $min $max -0.5 28 | !end_macro comparator 29 | -------------------------------------------------------------------------------- /examples/feature-test.qmasm: -------------------------------------------------------------------------------- 1 | ################################################# 2 | # This program does nothing useful. It merely # 3 | # serves as a showcase of QMASM's features. # 4 | # # 5 | # By Scott Pakin # 6 | ################################################# 7 | 8 | # Weights 9 | abc 0.5 10 | def 0.5 # Comments are allowed on the same line as other statements. 11 | 12 | # Strengths 13 | abc def 0.5 14 | 15 | # Assertions 16 | !assert abc = 0 || def = 0 17 | 18 | # Chains 19 | abc = ghi 20 | 21 | # Anti-chains 22 | def /= jkl 23 | 24 | # Let-binding to a symbol 25 | !let yes := True 26 | 27 | # Pins 28 | jkl := yes 29 | 30 | # Equivalences 31 | jkl <-> mno 32 | 33 | # Includes 34 | !include "comparator" 35 | 36 | # Assertions 37 | !assert 1 + 2 + 3 = 6 || 4 + 5 * 6 = 4 + (5 * 6) && 4 + 5 * 6 < (4 + 5) * 6 38 | !assert mno + 8 /= 3**(def + 1) 39 | !assert 56^1 = (1<<4 | 1<<1 | 1<= 25%7 41 | 42 | # Macro definitions 43 | !begin_macro XOR 44 | !assert Y = A^B 45 | A 0.5 46 | B -0.5 47 | Y -0.5 48 | $a1 1 49 | 50 | A B -0.5 51 | A Y -0.5 52 | A $a1 1 53 | B Y 0.5 54 | B $a1 -1 55 | Y $a1 -1 56 | !end_macro XOR 57 | 58 | # Macro uses 59 | !use_macro XOR excl_or 60 | excl_or.A := true 61 | excl_or.B := FaLsE # Note: case-insensitive 62 | 63 | # Let-binding to an expression 64 | !let idx1 := (6 + 1)*2*(4 - 1) 65 | !let idx2 := idx1/3 66 | alfa[idx1] = alfa[idx2] 67 | 68 | # Conditionals 69 | !if idx1 < idx2 70 | alfa[idx1] = excl_or.Y 71 | !else 72 | alfa[idx2] = excl_or.Y 73 | !end_if 74 | 75 | # Iteration over integer values 76 | !for idx := 0, 2, ..., 15 77 | alfa[idx] = zulu 78 | !end_for 79 | 80 | # Iteration over symbols 81 | !for icao := alfa, bravo, charlie, delta, echo 82 | icao[42] zulu -3.25 83 | !end_for 84 | 85 | # Changing the BQM type. 86 | !bqm_type qubo 87 | !begin_macro OR 88 | A 1 89 | B 1 90 | Y 1 91 | A B 1 92 | A Y -2 93 | B Y -2 94 | !end_macro OR 95 | !bqm_type ising 96 | !use_macro OR qubo_or 97 | !assert qubo_or.Y = qubo_or.A | qubo_or.B 98 | 99 | # Chain of macros 100 | !begin_macro XOR_chain 101 | !use_macro XOR xor 102 | A = xor.A 103 | B = xor.B 104 | Y = xor.Y 105 | Y = !next.A 106 | !end_macro XOR_chain 107 | !use_macro XOR_chain first second third 108 | # (In this chain, first.Y = second.A and second.Y = third.A.) 109 | 110 | # Renaming symbols 111 | big -0.75 112 | small 0.75 113 | big small 0.50 114 | big -> bigger 115 | small -> smaller 116 | six[2:0] := 110 117 | !assert six[0] /= 1 118 | six[2] = six[1] 119 | six[1] /= six[0] 120 | six[2:0] -> six[0:2] # Convert from 110 to 011. 121 | !assert six[2:0] = 3 122 | -------------------------------------------------------------------------------- /examples/gates.qmasm: -------------------------------------------------------------------------------- 1 | # 3-input AND gate (Y = A and B and C) 2 | !begin_macro and3 3 | $A -0.2727 4 | $B 0.0000 5 | $C -0.2727 6 | $Y 0.3636 7 | $a1 0.3636 8 | 9 | $A $B 0.0000 10 | $A $C 0.0909 11 | $A $Y -0.1818 12 | $A $a1 -0.1818 13 | $B $C 0.0000 14 | $B $Y -0.3636 15 | $B $a1 0.3636 16 | $C $Y -0.1818 17 | $C $a1 -0.1818 18 | $Y $a1 -0.1818 19 | !end_macro and3 20 | 21 | # 2-input OR gate (Y = A or B) 22 | !begin_macro or2 23 | $A 0.3333 24 | $B 0.3333 25 | $Y -0.6667 26 | 27 | $A $B 0.3333 28 | $A $Y -0.6667 29 | $B $Y -0.6667 30 | !end_macro or2 31 | 32 | # 1-input NOT gate (Y = not A) 33 | !begin_macro not1 34 | $A -0.5 35 | $Y -0.5 36 | 37 | $A $Y 1.0 38 | !end_macro not1 39 | -------------------------------------------------------------------------------- /examples/maze6x6.qmasm: -------------------------------------------------------------------------------- 1 | ######################################### 2 | # Find the shortest path through a maze # 3 | # By Scott Pakin # 4 | ######################################### 5 | 6 | # This is a generated file. 7 | # Command line: ./qmasm-maze gen 6 6 8 | 9 | # Maze to solve: 10 | # 11 | # A B C D E F 12 | # +--+--+--+--+--+ + 13 | # 1 | | | 14 | # + + +--+ + + + 15 | # 2 | | | | | | 16 | # +--+--+ + + +--+ 17 | # 3 | | | | 18 | # + +--+--+ + + + 19 | # 4 | | | | | | 20 | # + +--+ +--+ +--+ 21 | # 5 | | 22 | # + + + +--+ +--+ 23 | # 6 | | | | | 24 | # +--+ +--+--+--+--+ 25 | 26 | # Truth table for a room: 27 | # 28 | # 0 0 0 0 29 | # 0 0 1 1 30 | # 0 1 0 1 31 | # 0 1 1 0 32 | # 1 0 0 1 33 | # 1 0 1 0 34 | # 1 1 0 0 35 | 36 | # Define a macro for a room that has the preceding truth table as 37 | # the degenerate ground state of the corresponding Hamiltonian. 38 | !begin_macro room 39 | N 0.50 40 | E 0.50 41 | S 0.50 42 | W 0.50 43 | $a1 1.00 44 | 45 | N E 0.25 46 | N S 0.25 47 | N W 0.25 48 | N $a1 0.50 49 | E S 0.25 50 | E W 0.25 51 | E $a1 0.50 52 | S W 0.25 53 | S $a1 0.50 54 | W $a1 0.50 55 | !end_macro room 56 | 57 | # Define some helpful aliases. 58 | !let egress := TRUE 59 | !let wall := FALSE 60 | 61 | # Output in turn each room of the maze. 62 | 63 | !use_macro room A1 64 | A1.N := wall 65 | A1.W := wall 66 | 67 | !use_macro room B1 68 | B1.N := wall 69 | B1.W = A1.E 70 | 71 | !use_macro room C1 72 | C1.N := wall 73 | C1.S := wall 74 | C1.W = B1.E 75 | 76 | !use_macro room D1 77 | D1.N := wall 78 | D1.E := wall 79 | D1.W = C1.E 80 | 81 | !use_macro room E1 82 | E1.N := wall 83 | E1.W := wall 84 | 85 | !use_macro room F1 86 | F1.N := egress 87 | F1.E := wall 88 | F1.W = E1.E 89 | 90 | !use_macro room A2 91 | A2.E := wall 92 | A2.S := wall 93 | A2.W := wall 94 | A2.N = A1.S 95 | 96 | !use_macro room B2 97 | B2.S := wall 98 | B2.W := wall 99 | B2.N = B1.S 100 | 101 | !use_macro room C2 102 | C2.N := wall 103 | C2.E := wall 104 | C2.W = B2.E 105 | 106 | !use_macro room D2 107 | D2.E := wall 108 | D2.W := wall 109 | D2.N = D1.S 110 | 111 | !use_macro room E2 112 | E2.E := wall 113 | E2.W := wall 114 | E2.N = E1.S 115 | 116 | !use_macro room F2 117 | F2.E := wall 118 | F2.S := wall 119 | F2.W := wall 120 | F2.N = F1.S 121 | 122 | !use_macro room A3 123 | A3.N := wall 124 | A3.E := wall 125 | A3.W := wall 126 | 127 | !use_macro room B3 128 | B3.N := wall 129 | B3.S := wall 130 | B3.W := wall 131 | 132 | !use_macro room C3 133 | C3.E := wall 134 | C3.S := wall 135 | C3.N = C2.S 136 | C3.W = B3.E 137 | 138 | !use_macro room D3 139 | D3.W := wall 140 | D3.N = D2.S 141 | 142 | !use_macro room E3 143 | E3.N = E2.S 144 | E3.W = D3.E 145 | 146 | !use_macro room F3 147 | F3.N := wall 148 | F3.E := wall 149 | F3.W = E3.E 150 | 151 | !use_macro room A4 152 | A4.E := wall 153 | A4.W := wall 154 | A4.N = A3.S 155 | 156 | !use_macro room B4 157 | B4.N := wall 158 | B4.S := wall 159 | B4.W := wall 160 | 161 | !use_macro room C4 162 | C4.N := wall 163 | C4.E := wall 164 | C4.W = B4.E 165 | 166 | !use_macro room D4 167 | D4.E := wall 168 | D4.S := wall 169 | D4.W := wall 170 | D4.N = D3.S 171 | 172 | !use_macro room E4 173 | E4.E := wall 174 | E4.W := wall 175 | E4.N = E3.S 176 | 177 | !use_macro room F4 178 | F4.E := wall 179 | F4.S := wall 180 | F4.W := wall 181 | F4.N = F3.S 182 | 183 | !use_macro room A5 184 | A5.W := wall 185 | A5.N = A4.S 186 | 187 | !use_macro room B5 188 | B5.N := wall 189 | B5.W = A5.E 190 | 191 | !use_macro room C5 192 | C5.N = C4.S 193 | C5.W = B5.E 194 | 195 | !use_macro room D5 196 | D5.N := wall 197 | D5.S := wall 198 | D5.W = C5.E 199 | 200 | !use_macro room E5 201 | E5.N = E4.S 202 | E5.W = D5.E 203 | 204 | !use_macro room F5 205 | F5.N := wall 206 | F5.E := wall 207 | F5.S := wall 208 | F5.W = E5.E 209 | 210 | !use_macro room A6 211 | A6.E := wall 212 | A6.S := wall 213 | A6.W := wall 214 | A6.N = A5.S 215 | 216 | !use_macro room B6 217 | B6.S := egress 218 | B6.E := wall 219 | B6.W := wall 220 | B6.N = B5.S 221 | 222 | !use_macro room C6 223 | C6.E := wall 224 | C6.S := wall 225 | C6.W := wall 226 | C6.N = C5.S 227 | 228 | !use_macro room D6 229 | D6.N := wall 230 | D6.S := wall 231 | D6.W := wall 232 | 233 | !use_macro room E6 234 | E6.S := wall 235 | E6.N = E5.S 236 | E6.W = D6.E 237 | 238 | !use_macro room F6 239 | F6.N := wall 240 | F6.E := wall 241 | F6.S := wall 242 | F6.W = E6.E 243 | -------------------------------------------------------------------------------- /examples/qmasm-maze.go: -------------------------------------------------------------------------------- 1 | /* 2 | qmasm-maze generates mazes for solution by QMASM and validates solutions 3 | returned from QMASM. 4 | 5 | Author: Scott Pakin, pakin@lanl.gov 6 | */ 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "log" 13 | "math/rand" 14 | "os" 15 | "path" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "bufio" 21 | "errors" 22 | "github.com/spakin/disjoint" 23 | ) 24 | 25 | // notify is used to write status messages to the user. 26 | var notify *log.Logger 27 | 28 | // A Room is identified by its walls and by the other rooms it can reach. 29 | type Room struct { 30 | N bool // North side of room is a wall 31 | S bool // South side of room is a wall 32 | E bool // East side of room is a wall 33 | W bool // West side of room is a wall 34 | Reaches *disjoint.Element // Element in a set of reachable rooms 35 | } 36 | 37 | // A Maze is a 2-D array of Rooms. 38 | type Maze [][]Room 39 | 40 | // RandomMaze creates a maze of given dimensions. 41 | func RandomMaze(w, h int) Maze { 42 | // Allocate and initialize the maze to all walls present. 43 | maze := make([][]Room, h) 44 | for y := range maze { 45 | maze[y] = make([]Room, w) 46 | for x := range maze[y] { 47 | // Start with all walls present and no other rooms reachable. 48 | maze[y][x].N = true 49 | maze[y][x].S = true 50 | maze[y][x].E = true 51 | maze[y][x].W = true 52 | maze[y][x].Reaches = disjoint.NewElement() 53 | } 54 | } 55 | 56 | // Repeatedly remove walls until a single connected component remains. 57 | for cc := w * h; cc > 1; { 58 | // Because of symmetry, we need only connect to the right or 59 | // down rather than in all four directions. 60 | x0 := rand.Intn(w) 61 | y0 := rand.Intn(h) 62 | x1 := x0 63 | y1 := y0 64 | dir := rand.Intn(2) 65 | if dir == 0 && x0 < w-1 { 66 | x1++ // Go right. 67 | } else if dir == 1 && y0 < h-1 { 68 | y1++ // Go down. 69 | } else { 70 | continue // Can't go in the desired direction 71 | } 72 | if maze[y0][x0].Reaches.Find() == maze[y1][x1].Reaches.Find() { 73 | continue // Already connected 74 | } 75 | 76 | // Tear down the wall. 77 | if dir == 0 { 78 | // Right/left 79 | maze[y0][x0].E = false 80 | maze[y1][x1].W = false 81 | } else { 82 | // Down/up 83 | maze[y0][x0].S = false 84 | maze[y1][x1].N = false 85 | } 86 | disjoint.Union(maze[y0][x0].Reaches, maze[y1][x1].Reaches) 87 | cc-- 88 | } 89 | 90 | // Punch one hole on the top and one hole on the bottom. 91 | maze[0][rand.Intn(w)].N = false 92 | maze[h-1][rand.Intn(w)].S = false 93 | 94 | // Return the generated maze. 95 | return maze 96 | } 97 | 98 | // ColName maps the numbers {0, 1, 2, ..., 675} to the strings {"A", "B", "C", 99 | // ..., "ZZ"}. 100 | func ColName(c int) string { 101 | switch { 102 | case c < 26: 103 | return fmt.Sprintf("%c", c+'A') 104 | 105 | case c < 26*26: 106 | return fmt.Sprintf("%c%c", c/26+'A'-1, c%26+'A') 107 | 108 | default: 109 | notify.Fatalf("Invalid column number %d", c) 110 | } 111 | return "" // Will never get here. 112 | } 113 | 114 | // RoomName maps 0-based row and column numbers to letter-number strings (e.g., 115 | // "J15"). 116 | func RoomName(r, c int) string { 117 | return fmt.Sprintf("%s%d", ColName(c), r+1) 118 | } 119 | 120 | // Write outputs a Maze with a given row prefix. We assume the maze contains 121 | // fewer than 677 columns and 1000 rows. 122 | func (m Maze) Write(w io.Writer, pfx string) { 123 | // Output a column header. 124 | fmt.Fprint(w, pfx+" ") 125 | for c := range m[0] { 126 | fmt.Fprintf(w, " %-2s", ColName(c)) 127 | } 128 | fmt.Fprint(w, "\n") 129 | 130 | // Output each row in turn. 131 | for r, row := range m { 132 | // Output the row's northern walls as one line of ASCII graphics. 133 | fmt.Fprint(w, pfx+" ") 134 | for _, cell := range row { 135 | if cell.N { 136 | fmt.Fprintf(w, "+--") 137 | } else { 138 | fmt.Fprintf(w, "+ ") 139 | } 140 | } 141 | fmt.Fprintln(w, "+") 142 | 143 | // Output the row's western walls as another line of ASCII 144 | // graphics. 145 | fmt.Fprintf(w, "%s%3d ", pfx, r+1) 146 | for _, cell := range row { 147 | if cell.W { 148 | fmt.Fprintf(w, "| ") 149 | } else { 150 | fmt.Fprintf(w, " ") 151 | } 152 | } 153 | 154 | // End the line with the single, easternmost wall. 155 | if row[len(row)-1].E { 156 | fmt.Fprintln(w, "|") 157 | } else { 158 | fmt.Fprintln(w, "") 159 | } 160 | } 161 | 162 | // Output the bottomost row's southern walls as a final line of ASCII 163 | // graphics. 164 | fmt.Fprint(w, pfx+" ") 165 | for _, cell := range m[len(m)-1] { 166 | if cell.S { 167 | fmt.Fprintf(w, "+--") 168 | } else { 169 | fmt.Fprintf(w, "+ ") 170 | } 171 | } 172 | fmt.Fprintln(w, "+") 173 | } 174 | 175 | // WriteHeader writes some boilerplate header to a file. 176 | func WriteHeader(w io.Writer, m Maze) { 177 | fmt.Fprintln(w, `######################################### 178 | # Find the shortest path through a maze # 179 | # By Scott Pakin # 180 | ######################################### 181 | 182 | # This is a generated file. 183 | # Command line:`, 184 | strings.Join(os.Args, " "), ` 185 | 186 | # Maze to solve: 187 | #`) 188 | m.Write(w, "# ") 189 | fmt.Fprintln(w, ` 190 | # Truth table for a room: 191 | # 192 | # 0 0 0 0 193 | # 0 0 1 1 194 | # 0 1 0 1 195 | # 0 1 1 0 196 | # 1 0 0 1 197 | # 1 0 1 0 198 | # 1 1 0 0 199 | 200 | # Define a macro for a room that has the preceding truth table as 201 | # the degenerate ground state of the corresponding Hamiltonian. 202 | !begin_macro room 203 | N 0.50 204 | E 0.50 205 | S 0.50 206 | W 0.50 207 | $a1 1.00 208 | 209 | N E 0.25 210 | N S 0.25 211 | N W 0.25 212 | N $a1 0.50 213 | E S 0.25 214 | E W 0.25 215 | E $a1 0.50 216 | S W 0.25 217 | S $a1 0.50 218 | W $a1 0.50 219 | !end_macro room 220 | 221 | # Define some helpful aliases. 222 | !let egress := TRUE 223 | !let wall := FALSE 224 | `) 225 | } 226 | 227 | // WriteRooms writes the rooms of a maze to a file. 228 | func WriteRooms(w io.Writer, m Maze) { 229 | fmt.Fprintln(w, "# Output in turn each room of the maze.") 230 | for r, row := range m { 231 | for c, cell := range row { 232 | fmt.Fprintln(w, "") 233 | rstr := RoomName(r, c) 234 | fmt.Fprintf(w, "!use_macro room %s\n", rstr) 235 | 236 | // Output egresses (which always face north or south in 237 | // the current implementation). 238 | if r == 0 && !cell.N { 239 | fmt.Fprintln(w, rstr+".N := egress") 240 | } 241 | if r == len(m)-1 && !cell.S { 242 | fmt.Fprintln(w, rstr+".S := egress") 243 | } 244 | 245 | // Output all walls. 246 | if cell.N { 247 | fmt.Fprintln(w, rstr+".N := wall") 248 | } 249 | if cell.E { 250 | fmt.Fprintln(w, rstr+".E := wall") 251 | } 252 | if cell.S { 253 | fmt.Fprintln(w, rstr+".S := wall") 254 | } 255 | if cell.W { 256 | fmt.Fprintln(w, rstr+".W := wall") 257 | } 258 | 259 | // Output northern and western paths. (The others are 260 | // symmetric.) 261 | if r > 0 && !cell.N { 262 | fmt.Fprintf(w, "%s.N = %s.S\n", rstr, RoomName(r-1, c)) 263 | } 264 | if c > 0 && !cell.W { 265 | fmt.Fprintf(w, "%s.W = %s.E\n", rstr, RoomName(r, c-1)) 266 | } 267 | } 268 | } 269 | } 270 | 271 | // GenerateMaze generates a maze of given dimensions. 272 | func GenerateMaze(out io.Writer, w, h int) { 273 | // Validate the dimensions provided. 274 | if w < 1 || h < 1 { 275 | notify.Fatal("Mazes must contain at least one row and one column") 276 | } 277 | if w >= 676 || h > 999 { 278 | notify.Fatal("Mazes must contain no more than 999 rows and 676 columns") 279 | } 280 | 281 | // Seed the random-number generator. 282 | seed := time.Now().UnixNano() * int64(os.Getpid()) 283 | rand.Seed(seed) 284 | 285 | // Generate a maze. 286 | maze := RandomMaze(w, h) 287 | 288 | // Output QMASM code. 289 | WriteHeader(out, maze) 290 | WriteRooms(out, maze) 291 | } 292 | 293 | // NewMaze allocates a new, empty Maze. 294 | func NewMaze() Maze { 295 | m := make([][]Room, 0, 10) 296 | for i := range m { 297 | m[i] = make([]Room, 0, 10) 298 | } 299 | return m 300 | } 301 | 302 | // Extend extends a maze by parsing a coordinate and a Boolean value. 303 | func (m Maze) Extend(s string, b bool) Maze { 304 | // Parse the string. Invalid input strings do not extend the maze. 305 | var i int 306 | col := 0 307 | for i = 0; s[i] >= 'A' && s[i] <= 'Z'; i++ { 308 | col = col*26 + int(s[i]-'A') 309 | } 310 | row := 0 311 | for ; s[i] >= '0' && s[i] <= '9'; i++ { 312 | row = row*10 + int(s[i]-'0') 313 | } 314 | row-- 315 | if row < 0 { 316 | return m // Invalid room specification 317 | } 318 | i++ // Skip the "." 319 | dir := s[i] 320 | switch dir { 321 | case 'N', 'E', 'S', 'W': 322 | default: 323 | return m // Invalid room specification 324 | } 325 | 326 | // Add rows and columns as necessary. We assume we'll typically be 327 | // adding rooms in order. 328 | for row >= len(m) { 329 | m = append(m, make([]Room, 1)) 330 | } 331 | for col >= len(m[row]) { 332 | m[row] = append(m[row], Room{}) 333 | } 334 | 335 | // Update the specified room. 336 | switch dir { 337 | case 'N': 338 | m[row][col].N = b 339 | case 'S': 340 | m[row][col].S = b 341 | case 'E': 342 | m[row][col].E = b 343 | case 'W': 344 | m[row][col].W = b 345 | default: 346 | panic("Invalid direction") // We should never get here. 347 | } 348 | return m 349 | } 350 | 351 | // ReadSolutions returns a list of maze solutions and their tallies as read 352 | // from a Reader. 353 | func ReadMazes(r *bufio.Reader) ([]Maze, []int) { 354 | mazes := make([]Maze, 0, 1) // List of mazes to return 355 | var m Maze // Current maze 356 | tallies := make([]int, 0, 1) // List of tallies to return 357 | var t int64 // Current tally 358 | haveSoln := false // true=saw at least one Solution; false=still in header text 359 | for { 360 | // Read a line from the file and split it into fields. 361 | ln, err := r.ReadString('\n') 362 | if err == io.EOF { 363 | break 364 | } 365 | if err != nil { 366 | notify.Fatal(err) 367 | } 368 | f := strings.Fields(ln) 369 | if len(f) == 0 { 370 | continue 371 | } 372 | 373 | // Process the current line. 374 | switch { 375 | case f[0] == "Solution": 376 | // Start a new maze when we see "Solution". 377 | haveSoln = true 378 | if m != nil { 379 | mazes = append(mazes, m) 380 | tallies = append(tallies, int(t)) 381 | } 382 | m = NewMaze() 383 | t, err = strconv.ParseInt(f[7][:len(f[7])-2], 10, 0) 384 | if err != nil { 385 | notify.Fatal(err) 386 | } 387 | 388 | case !haveSoln: 389 | // Don't get confused by header text. 390 | 391 | case len(f) == 2 && (f[1] == "True" || f[1] == "False"): 392 | // Append a room to the current maze when we see a 393 | // solution row. 394 | m = m.Extend(f[0], f[1] == "True") 395 | } 396 | } 397 | if m != nil { 398 | mazes = append(mazes, m) // Final maze 399 | tallies = append(tallies, int(t)) // Final tally 400 | } 401 | return mazes, tallies 402 | } 403 | 404 | // NextRoom maps a "from" direction (N, S, E, or W) and room 405 | // coordinates to a new "from" direction and room coordinates. 406 | func (m Maze) NextRoom(r, c int, dir string) (int, int, string) { 407 | room := m[r][c] 408 | switch { 409 | case room.N && dir != "N": 410 | return r - 1, c, "S" 411 | case room.E && dir != "E": 412 | return r, c + 1, "W" 413 | case room.S && dir != "S": 414 | return r + 1, c, "N" 415 | case room.W && dir != "W": 416 | return r, c - 1, "E" 417 | default: 418 | panic("Unexpectedly stuck in a room") // Should never get here. 419 | } 420 | } 421 | 422 | // PathString returns a path through a maze or an error. 423 | func (m Maze) PathString() (string, error) { 424 | // Ensure that each room has either zero or two exits. 425 | bool2int := map[bool]int{true: 1, false: 0} 426 | for r, row := range m { 427 | for c, cell := range row { 428 | n := bool2int[cell.N] + bool2int[cell.E] + bool2int[cell.S] + bool2int[cell.W] 429 | switch n { 430 | case 0: 431 | case 2: 432 | default: 433 | return "", fmt.Errorf("Room %s contains %d exit(s) but should have 0 or 2", RoomName(r, c), n) 434 | } 435 | } 436 | } 437 | 438 | // Find the ingress and egress columns. Complain if more than one of 439 | // each is found. 440 | cin := -1 441 | for c, cell := range m[0] { 442 | if cell.N { 443 | if cin == -1 { 444 | cin = c 445 | } else { 446 | return "", fmt.Errorf("More than one ingress exists on the top row (%s and %d)", RoomName(0, cin), RoomName(0, c)) 447 | } 448 | } 449 | } 450 | if cin == -1 { 451 | return "", errors.New("No ingress found in the top row") 452 | } 453 | nrows := len(m) 454 | cout := -1 455 | for c, cell := range m[nrows-1] { 456 | if cell.S { 457 | if cout == -1 { 458 | cout = c 459 | } else { 460 | return "", fmt.Errorf("More than one egress exists on the bottom row (%s and %d)", RoomName(nrows-1, cout), RoomName(nrows-1, c)) 461 | } 462 | } 463 | } 464 | if cout == -1 { 465 | return "", errors.New("No egress found in the bottom row") 466 | } 467 | 468 | // Complain if the maze is open on the left or right. 469 | for r, row := range m { 470 | if row[0].W { 471 | return "", fmt.Errorf("Maze unexpectedly exits to the left at %s", RoomName(r, 0)) 472 | } 473 | ncols := len(row) 474 | if row[ncols-1].E { 475 | return "", fmt.Errorf("Maze unexpectedly exits to the right at %s", RoomName(r, ncols-1)) 476 | } 477 | } 478 | 479 | // Starting from the ingress, make our way to the egress. 480 | path := make([]string, 0, nrows*nrows) 481 | for r, c, d := 0, cin, "N"; r != nrows-1 || c != cout; r, c, d = m.NextRoom(r, c, d) { 482 | path = append(path, RoomName(r, c)) 483 | } 484 | path = append(path, RoomName(nrows-1, cout)) 485 | return strings.Join(path, " -- "), nil 486 | } 487 | 488 | // ValidatePaths reports if a path through a maze looks valid. 489 | func ValidatePaths(r io.Reader) { 490 | // Ensure we can read line-by-line. 491 | rb, ok := r.(*bufio.Reader) 492 | if !ok { 493 | rb = bufio.NewReader(r) 494 | } 495 | 496 | // Read a list of mazes. 497 | mazes, tallies := ReadMazes(rb) 498 | 499 | // Process each maze in turn. 500 | for i, m := range mazes { 501 | fmt.Printf("Solution %d (tally: %d): ", i+1, tallies[i]) 502 | s, err := m.PathString() 503 | if err != nil { 504 | fmt.Printf("[%s]\n", err) 505 | } else { 506 | fmt.Println(s) 507 | } 508 | } 509 | } 510 | 511 | func main() { 512 | // Parse the command line. 513 | notify = log.New(os.Stderr, path.Base(os.Args[0])+": ", 0) 514 | switch { 515 | case len(os.Args) == 4 && os.Args[1] == "gen": 516 | // Mode 1: Generate a maze. 517 | w, err := strconv.Atoi(os.Args[2]) 518 | if err != nil { 519 | notify.Fatal(err) 520 | } 521 | h, err := strconv.Atoi(os.Args[3]) 522 | if err != nil { 523 | notify.Fatal(err) 524 | } 525 | GenerateMaze(os.Stdout, w, h) 526 | 527 | case len(os.Args) == 2 && os.Args[1] == "validate": 528 | // Mode 2: Validate QMASM maze output. 529 | ValidatePaths(os.Stdin) 530 | 531 | default: 532 | notify.Fatalf("Usage: %s gen | %s validate", os.Args[0], os.Args[0]) 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /examples/sort4.qmasm: -------------------------------------------------------------------------------- 1 | ################################### 2 | # 4-bit sorting network # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | !include 7 | 8 | !use_macro comparator c1 9 | c1.$a = in[2] 10 | c1.$b = in[3] 11 | $s1_2 = c1.$min 12 | $s1_3 = c1.$max 13 | 14 | !use_macro comparator c2 15 | c2.$a = in[1] 16 | c2.$b = $s1_2 17 | $s2_1 = c2.$min 18 | $s2_2 = c2.$max 19 | 20 | !use_macro comparator c3 21 | c3.$a = $s1_3 22 | c3.$b = in[4] 23 | $s3_3 = c3.$min 24 | $s3_4 = c3.$max 25 | 26 | !use_macro comparator c4 27 | c4.$a = $s2_2 28 | c4.$b = $s3_3 29 | $s4_2 = c4.$min 30 | $s4_3 = c4.$max 31 | 32 | !use_macro comparator c5 33 | c5.$a = $s2_1 34 | c5.$b = $s4_2 35 | out[1] = c5.$min 36 | out[2] = c5.$max 37 | 38 | !use_macro comparator c6 39 | c6.$a = $s4_3 40 | c6.$b = $s3_4 41 | out[3] = c6.$min 42 | out[4] = c6.$max 43 | -------------------------------------------------------------------------------- /extras/README.md: -------------------------------------------------------------------------------- 1 | QMASM extras 2 | ============ 3 | 4 | This directory contains additional tools that may be of use to QMASM programmers. 5 | 6 | QMASM stylesheets 7 | ----------------- 8 | 9 | [`qmasm-mode.el`](qmasm-mode.el) is an Emacs major mode for editing QMASM source code. It is currently a very simple mode that provides only syntax highlighting. Load the mode manually with `M-x load-library` then `M-x qmasm-mode` or automatically by including statements like the following in your `.emacs` file: 10 | ```Emacs Lisp 11 | (load-library "qmasm-mode") 12 | (setq auto-mode-alist (cons '("\\.qmasm$" . qmasm-mode) auto-mode-alist)) 13 | ``` 14 | 15 | [`qmasm.ssh`](qmasm.ssh) is a stylsheet for the [a2ps](https://www.gnu.org/software/a2ps/) "Any to PostScript" pretty printer. One can print QMASM source code with a command like the following: 16 | ```bash 17 | a2ps -1 --prologue=color -E/path/to/qmasm.ssh my-program.qmasm | lpr 18 | ``` 19 | See the a2ps documentation for instructions on how to install `qmasm.ssh` and use that stylesheet implicitly for all source files ending in `.qmasm`. (In short, put `qmasm.ssh` in the `a2ps/sheets` directory, and edit `a2ps/sheets/sheets.map` to associate the `.qmasm` file extension with the `qmasm.ssh` stylesheet.) 20 | 21 | Topology generation 22 | ------------------- 23 | 24 | QMASM's `--topology-file` option lets the user define a graph topology to target in place of the D-Wave hardware's actual topology. The format is a list of space-separated vertex pairs, one pair per line. Comments, which go from the first `#` character to the end of the line, can also be included in the file. 25 | 26 | The following scripts can be used to construct files that can be used as an argument to `--topology-file`: 27 | 28 | * [`qmasm-gen-chimera`](qmasm-gen-chimera) generates a complete Chimera graph of arbitrary size. It takes three arguments: the width of the Chimera graph in unit cells, the height of the Chimera graph in unit cells, and the number of vertices in each of a unit cell's two partitions. For example, a complete D-Wave 2000Q could be generated with `qmasm-gen-chimera 16 16 4`. 29 | 30 | * [`qmasm-gen-current`](qmasm-gen-current) outputs the current topology. It takes no arguments but expects the various `DW_INTERNAL__*` environment variables to be set properly. 31 | 32 | * [`qmasm-gen-all-to-all`](qmasm-gen-all-to-all) outputs a complete graph of a given number of vertices. The script takes one argument, which is the number of vertices. 33 | -------------------------------------------------------------------------------- /extras/qmasm-gen-all-to-all: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | #################################### 4 | # Generate a connectivity list for # 5 | # a complete graph of N nodes # 6 | # # 7 | # By Scott Pakin # 8 | #################################### 9 | 10 | import sys 11 | 12 | # Parse the command line. 13 | if len(sys.argv) != 2: 14 | sys.stderr.write("Usage: %s \n" % sys.argv[0]) 15 | sys.exit(1) 16 | nnodes = int(sys.argv[1]) # Number of vertices in the graph 17 | 18 | # Connect every node to every other node. 19 | for i in range(nnodes - 1): 20 | for j in range(i + 1, nnodes): 21 | print("%d %d" % (i, j)) 22 | -------------------------------------------------------------------------------- /extras/qmasm-gen-chimera: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | #################################### 4 | # Generate a connectivity list for # 5 | # an {M, N, L} Chimera graph # 6 | # # 7 | # By Scott Pakin # 8 | #################################### 9 | 10 | import sys 11 | 12 | # Parse the command line. 13 | if len(sys.argv) != 4: 14 | sys.stderr.write("Usage: %s \n" % sys.argv[0]) 15 | sys.exit(1) 16 | width = int(sys.argv[1]) # Width in unit cells 17 | height = int(sys.argv[2]) # Height in unit cells 18 | pnodes = int(sys.argv[3]) # Nodes in each of a unit cell's two partitions 19 | 20 | # Loop over all unit cells. 21 | for y in range(height): 22 | for x in range(width): 23 | # Connect all nodes in the left partition to all nodes in the 24 | # right partition. 25 | base = (y*width + x)*2*pnodes 26 | for r1 in range(pnodes): 27 | for r2 in range(pnodes, 2*pnodes): 28 | print("%d %d" % (base + r1, base + r2)) 29 | 30 | # Connect each node in the right partition to its peer in the 31 | # unit cell to the right. 32 | if x < width - 1: 33 | for r in range(pnodes, 2*pnodes): 34 | print("%d %d" % (base + r, base + r + 2*pnodes)) 35 | 36 | # Connect each node in the left partition to its peer in the 37 | # unit cell below. 38 | if y < height - 1: 39 | for r in range(0, pnodes): 40 | print("%d %d" % (base + r, base + r + 2*pnodes*width)) 41 | -------------------------------------------------------------------------------- /extras/qmasm-gen-current: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | #################################### 4 | # Generate a connectivity list for # 5 | # the current solver's topology # 6 | # # 7 | # By Scott Pakin # 8 | #################################### 9 | 10 | import argparse 11 | import sys 12 | from dwave.system import DWaveSampler 13 | 14 | def abend(msg): 15 | "Output an error message and abort the program." 16 | sys.stderr.write('%s: %s\n' % (sys.argv[0], msg)) 17 | sys.exit(1) 18 | 19 | # Parse the command line. 20 | cl_parser = argparse.ArgumentParser(description='Generate a connectivity list for a given hardware topology') 21 | cl_parser.add_argument('--profile', type=str, default=None, metavar='NAME', 22 | help='Profile name from dwave.conf to use') 23 | cl_parser.add_argument('--solver', type=str, default=None, metavar='NAME', 24 | help='Solver name from dwave.conf to use') 25 | cl_args = cl_parser.parse_args() 26 | 27 | # Acquire the current topology. 28 | try: 29 | sampler = DWaveSampler(profile=cl_args.profile, solver=cl_args.solver) 30 | except Exception as e: 31 | abend(str(e)) 32 | try: 33 | hw_adj = sampler.properties["couplers"] 34 | except KeyError: 35 | abend("Failed to query solver %s's topology" % sampler.solver.id) 36 | 37 | # Canonicalize and sort the edge list. 38 | edges = set() 39 | for u, v in hw_adj: 40 | if u == v: 41 | abend("Topology contains a self edge: (%d, %d)" % (u, v)) 42 | if u > v: 43 | u, v = v, u 44 | edges.add((u, v)) 45 | for u, v in sorted(edges): 46 | print("%d %d" % (u, v)) 47 | -------------------------------------------------------------------------------- /extras/qmasm-mode.el: -------------------------------------------------------------------------------- 1 | ;;; qmasm-mode.el --- Emacs mode for editing QMASM code 2 | 3 | ;; Author: Scott Pakin 4 | ;; Keywords: tools, languages 5 | 6 | (defconst qmasm-mode-syntax-table 7 | (let ((table (make-syntax-table))) 8 | ;; '"' is a string delimiter. 9 | (modify-syntax-entry ?\" "\"" table) 10 | 11 | ;; Comments go from "#" to the end of the line. 12 | (modify-syntax-entry ?\# "<" table) 13 | (modify-syntax-entry ?\n ">" table) 14 | table)) 15 | 16 | (defvar qmasm-highlights 17 | ;; Directives are treated as functions. 18 | '(("![a-z_.]+" . font-lock-function-name-face))) 19 | 20 | (define-derived-mode qmasm-mode prog-mode "QMASM" 21 | :syntax-table qmasm-mode-syntax-table 22 | (setq font-lock-defaults '(qmasm-highlights)) 23 | (font-lock-fontify-buffer)) 24 | -------------------------------------------------------------------------------- /extras/qmasm.ssh: -------------------------------------------------------------------------------- 1 | # a2ps style sheet for QMASM 2 | 3 | style "QMASM" is 4 | written by "Scott Pakin " 5 | version is 1.0 6 | requires a2ps 4.12a 7 | 8 | alphabets are 9 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&*+-./<=>?@_" 10 | 11 | sequences are 12 | "#" Comment 13 | end sequences 14 | 15 | keywords in Keyword_strong are 16 | !begin_macro, !end_macro, !use_macro, !include, !assert, 17 | !let, !if, !else, !end_if, !for, !end_for, 18 | !alias 19 | end keywords 20 | 21 | keywords in Keyword are 22 | "true", "false" 23 | end keywords 24 | 25 | optional operators are 26 | := \equiv, 27 | <-> \leftrightarrow 28 | end operators 29 | 30 | end style 31 | -------------------------------------------------------------------------------- /scripts/qb2qmasm: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ################################### 4 | # Convert Qubist to QMASM # 5 | # By Scott Pakin # 6 | ################################### 7 | 8 | import argparse 9 | import sys 10 | 11 | # Parse the command line. 12 | cl_parser = argparse.ArgumentParser(description="Convert Qubist input to QMASM input") 13 | cl_parser.add_argument("input", nargs="?", metavar="FILE", default="-", 14 | help="Qubist-format input file (default: standard input)") 15 | cl_parser.add_argument("-o", "--output", metavar="FILE", default="-", 16 | help="file to which to write QMASM code (default: stdandard output)") 17 | cl_parser.add_argument("-f", "--format", metavar="FORMAT", default="%d", 18 | help='printf-style format string for formatting qubit numbers (default: "%%d")') 19 | cl_parser.add_argument("-r", "--renumber-from", metavar="INT", type=int, 20 | help="starting number from which to renumber qubits") 21 | cl_args = cl_parser.parse_args() 22 | 23 | # Open the input file. 24 | if cl_args.input == "-": 25 | infile = sys.stdin 26 | else: 27 | try: 28 | infile = open(cl_args.input, "r") 29 | except IOError: 30 | sys.stderr.write("%s: Failed to open %s for input\n" % (sys.argv[0], cl_args.input)) 31 | sys.exit(1) 32 | 33 | # Open the output file. 34 | if cl_args.output == "-": 35 | outfile = sys.stdout 36 | else: 37 | try: 38 | outfile = open(cl_args.output, "w") 39 | except IOError: 40 | sys.stderr.write("%s: Failed to open %s for output\n" % cl_args.output) 41 | sys.exit(1) 42 | 43 | # Read the input file into memory, keeping track of all qubit numbers seen. 44 | qubist = [] 45 | qnums = set() 46 | for line in infile: 47 | fields = line.split() 48 | if len(fields) != 3: 49 | continue 50 | q1, q2, val = int(fields[0]), int(fields[1]), fields[2] 51 | qnums.add(q1) 52 | qnums.add(q2) 53 | qubist.append((q1, q2, val)) 54 | qnums = sorted(qnums) 55 | 56 | # Map old qubit numbers to new qubit numbers. 57 | if cl_args.renumber_from != None: 58 | newq = dict(list(zip(qnums, list(range(cl_args.renumber_from, cl_args.renumber_from + len(qnums)))))) 59 | else: 60 | newq = {q: q for q in qnums} 61 | 62 | # Convert each line in turn. 63 | for q1, q2, val in qubist: 64 | if q1 == q2: 65 | # Point weight 66 | fmt = cl_args.format + " %s\n" 67 | outfile.write(fmt % (newq[q1], val)) 68 | else: 69 | # Coupler strength 70 | fmt = cl_args.format + " " + cl_args.format + " %s\n" 71 | outfile.write(fmt % (newq[q1], newq[q2], val)) 72 | 73 | # Wrap up. 74 | if cl_args.input != "-": 75 | infile.close() 76 | if cl_args.output != "-": 77 | outfile.close() 78 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ################################### 4 | # Install QMASM # 5 | # By Scott Pakin # 6 | ################################### 7 | 8 | from setuptools import setup, find_packages 9 | 10 | long_description = '''QMASM fills a gap in the software ecosystem for [D-Wave's adiabatic quantum computers](http://www.dwavesys.com/) by shielding the programmer from having to know system-specific hardware details while still enabling programs to be expressed at a fairly low level of abstraction. It is therefore analogous to a conventional macro assembler and can be used in much the same way: as a target either for programmers who want a great deal of control over the hardware or for compilers that implement higher-level languages.''' 11 | 12 | setup(name = 'qmasm', 13 | version = '4.1', 14 | description = 'Quantum Macro Assembler', 15 | long_description = long_description, 16 | long_description_content_type = 'text/markdown', 17 | author = 'Scott Pakin', 18 | author_email = 'pakin@lanl.gov', 19 | classifiers = [ 20 | 'Topic :: Software Development :: Compilers', 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Environment :: Console', 23 | 'Programming Language :: Python :: 3', 24 | 'Intended Audience :: Developers'], 25 | url = 'https://github.com/lanl/qmasm', 26 | download_url = 'https://github.com/lanl/qmasm/archive/v4.1.tar.gz', 27 | license = 'BSD-ish', 28 | keywords = ['quantum', 'annealing', 'macro', 'assembler', 'd-wave'], 29 | entry_points = { 30 | 'console_scripts': ['qmasm = qmasm.__main__:main'] 31 | }, 32 | scripts = ['scripts/qb2qmasm', 33 | 'extras/qmasm-gen-all-to-all', 34 | 'extras/qmasm-gen-chimera', 35 | 'extras/qmasm-gen-current'], 36 | packages = find_packages('src'), 37 | package_dir = {'': 'src'}, 38 | python_requires = '>= 3.8', 39 | install_requires = ['dwave-ocean-sdk'] 40 | ) 41 | -------------------------------------------------------------------------------- /src/qmasm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanl/qmasm/2222dff00753e6c34c44be2dadf4844ec5caa504/src/qmasm/__init__.py -------------------------------------------------------------------------------- /src/qmasm/__main__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ################################### 4 | # Quantum Macro Assembler # 5 | # By Scott Pakin # 6 | ################################### 7 | 8 | import re 9 | import sys 10 | from qmasm.cmdline import ParseCommandLine 11 | from qmasm.output import OutputMixin 12 | from qmasm.parse import FileParser 13 | from qmasm.problem import Problem 14 | from qmasm.solve import Sampler 15 | from qmasm.utils import Utilities, SymbolMapping 16 | 17 | class QMASM(ParseCommandLine, Utilities, OutputMixin): 18 | "QMASM represents everything the program can do." 19 | 20 | def __init__(self): 21 | # List of Statement objects 22 | self.program = [] 23 | 24 | # Map between symbols and numbers. 25 | self.sym_map = SymbolMapping() 26 | 27 | # Multiple components of QMASM require a definition of an identifier. 28 | self.ident_re = re.compile(r'[^-+*/%&\|^~()<=>#,\s]+') 29 | 30 | # Define a scale factor for converting floats to ints for MiniZinc's 31 | # sake. 32 | self.minizinc_scale_factor = 10000.0 33 | 34 | def run(self): 35 | "Execute the entire QMASM processing sequence." 36 | 37 | # Parse the command line. 38 | cl_args = self.parse_command_line() 39 | self.report_command_line(cl_args) 40 | 41 | # Parse the original input file(s) into an internal representation. 42 | fparse = FileParser(self) 43 | fparse.process_files(cl_args.input) 44 | 45 | # Parse the variable pinnings specified on the command line. Append 46 | # these to the program. 47 | if cl_args.pin != None: 48 | for pin in cl_args.pin: 49 | self.program.extend(fparse.process_pin("[command line]", 1, pin)) 50 | 51 | # Walk the statements in the program, processing each in turn. 52 | logical = Problem(self) 53 | for stmt in self.program: 54 | stmt.update_qmi("", "", logical) 55 | 56 | # Define a strength for each user-specified chain and anti-chain, and 57 | # assign strengths to those chains. 58 | self.chain_strength = logical.assign_chain_strength(cl_args.chain_strength) 59 | if cl_args.verbose >= 1: 60 | sys.stderr.write("Chain strength: %7.4f\n\n" % self.chain_strength) 61 | 62 | # We now have enough information to produce an Ocean BinaryQuadraticModel. 63 | logical.generate_bqm() 64 | 65 | # Convert chains to aliases where possible. 66 | if cl_args.O >= 1: 67 | logical.convert_chains_to_aliases(cl_args.verbose) 68 | 69 | # Simplify the problem if possible. 70 | if cl_args.O >= 1: 71 | logical.simplify_problem(cl_args.verbose) 72 | 73 | # Establish a connection to a D-Wave or software sampler. 74 | sampler = Sampler(self, profile=cl_args.profile, solver=cl_args.solver) 75 | sampler.show_properties(cl_args.verbose) 76 | 77 | # Convert user-specified chains, anti-chains, and pins to assertions. 78 | logical.append_assertions_from_statements() 79 | 80 | # Determine if we're expected to write an output file. If --run was 81 | # specified, we write a file only if --output was also specified. 82 | write_output_file = not (cl_args.output == "" and cl_args.run) 83 | 84 | # Determine when to write an output file: either pre- or post-embedding. 85 | write_time = None 86 | if write_output_file: 87 | # Start with always_embed --> post, otherwise pre. 88 | write_time = "pre" 89 | if cl_args.always_embed: 90 | write_time = "post" 91 | 92 | # QMASM output is always pre-embedding. 93 | if cl_args.format == "qmasm" and write_time == "post": 94 | self.warn("Ignoring --always embed; incompatible with --format=qmasm") 95 | write_time = "pre" 96 | 97 | # Qubist output is always post-embedding. 98 | if cl_args.format == "qubist": 99 | write_time = "post" 100 | 101 | # Produce pre-embedding output files. 102 | anneal_sched = self.parse_anneal_sched_string(cl_args.schedule) 103 | sampler_args = {"anneal_schedule": anneal_sched, 104 | "annealing_time": cl_args.anneal_time, 105 | "num_reads": cl_args.samples, 106 | "num_spin_reversal_transforms": cl_args.spin_revs, 107 | "postprocess": cl_args.postproc, 108 | "chain_strength": -self.chain_strength} 109 | if write_output_file and write_time == "pre": 110 | self.write_output(logical, cl_args.output, cl_args.format, cl_args.qubo, sampler, sampler_args) 111 | if not cl_args.run: 112 | sys.exit(0) 113 | 114 | 115 | # Embed the problem on the physical topology. 116 | physical = sampler.embed_problem(logical, cl_args.topology_file, 117 | cl_args.pack_qubits, cl_args.physical, 118 | cl_args.verbose) 119 | 120 | # Map each logical qubit to one or more symbols. 121 | max_num = self.sym_map.max_number() 122 | num2syms = [[] for _ in range(max_num + 1)] 123 | all_num2syms = [[] for _ in range(max_num + 1)] 124 | max_sym_name_len = 7 125 | for s, n in self.sym_map.symbol_number_items(): 126 | all_num2syms[n].append(s) 127 | if cl_args.verbose >= 2 or "$" not in s: 128 | num2syms[n].append(s) 129 | max_sym_name_len = max(max_sym_name_len, len(repr(num2syms[n])) - 1) 130 | physical.num2syms = all_num2syms 131 | 132 | # Output the embedding. 133 | physical.output_embedding(cl_args.verbose, max_sym_name_len) 134 | 135 | # Abort if any variables failed to embed. 136 | if physical.embedding != {}: 137 | danglies = physical.dangling_variables() 138 | if len(danglies) > 0: 139 | self.abend("Disconnected variables encountered: %s" % str(sorted(danglies))) 140 | 141 | # Output some problem statistics. 142 | if cl_args.verbose > 0: 143 | physical.output_embedding_statistics() 144 | 145 | # Produce post-embedding output files. 146 | if write_output_file and write_time == "post": 147 | self.write_output(physical, cl_args.output, cl_args.format, cl_args.qubo, sampler, sampler_args) 148 | 149 | # If we weren't told to run anything we can exit now. 150 | if not cl_args.run: 151 | return 152 | 153 | # Solve the specified problem. 154 | if cl_args.verbose >= 1: 155 | sys.stderr.write("Submitting the problem to the %s solver.\n\n" % sampler.client_info["solver_name"]) 156 | composites = self.parse_composite_string(cl_args.composites) 157 | solutions = sampler.acquire_samples(cl_args.verbose, 158 | composites, 159 | physical, 160 | anneal_sched, 161 | cl_args.samples, 162 | cl_args.anneal_time, 163 | cl_args.spin_revs, 164 | cl_args.postproc) 165 | solutions.report_timing_information(cl_args.verbose) 166 | solutions.report_chain_break_information(cl_args.verbose) 167 | 168 | # Filter the solutions as directed by the user. 169 | filtered_solns = solutions.filter(cl_args.show, cl_args.verbose, cl_args.samples, cl_args.equality) 170 | 171 | # Output energy tallies. 172 | if cl_args.verbose >= 2: 173 | solutions.report_energy_tallies(filtered_solns, True) 174 | elif cl_args.verbose >= 1: 175 | solutions.report_energy_tallies(filtered_solns, False) 176 | 177 | # Output the solution to the standard output device. 178 | show_asserts = cl_args.verbose >= 2 or cl_args.show in ["best", "all"] 179 | filtered_solns.output_solutions(cl_args.values, cl_args.verbose, show_asserts) 180 | 181 | # Run the D-Wave Problem Inspector if instructed to do so. 182 | if cl_args.visualize: 183 | solutions.visualize() 184 | 185 | def main(): 186 | "Run QMASM." 187 | q = QMASM() 188 | q.run() 189 | 190 | if __name__ == "__main__": 191 | main() 192 | -------------------------------------------------------------------------------- /src/qmasm/assertions.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Parse an !assert directive # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | import qmasm 7 | import re 8 | import sys 9 | 10 | class AST(object): 11 | "Represent an abstract syntax tree." 12 | 13 | def __init__(self, qmasm_obj, type, value, kids=[]): 14 | self.qmasm = qmasm_obj 15 | self.type = type 16 | self.value = value 17 | self.kids = kids 18 | self.code = lambda isb: self.qmasm.abend("Internal error: Attempt to evaluate an AST without compiling it first") # Function that evaluates the AST given a mapping from identifiers to bits 19 | self._str = None # Memoized string representation 20 | self.pin_parser = qmasm.parse.PinParser() 21 | 22 | def _needs_parens(self): 23 | "Return True if an AST node should be parenthesized." 24 | return self.type == "factor" and self.kids[0].type == "conn" 25 | 26 | def _str_helper(self): 27 | "Do most of the work for the __str__ method." 28 | # Conditionally parenthesize all child strings. 29 | nkids = len(self.kids) 30 | kids_str = [str(k) for k in self.kids] 31 | for i in range(nkids): 32 | if self.kids[i]._needs_parens(): 33 | kids_str[i] = "(" + kids_str[i] + ")" 34 | 35 | # Return ourself as a string. 36 | if nkids == 0: 37 | return str(self.value) 38 | if nkids == 1: 39 | if self.type == "unary" and self.value != "id": 40 | return "%s%s" % (self.value, kids_str[0]) 41 | return kids_str[0] 42 | if nkids == 2: 43 | if self.value in ["*", "/", "%", "&", "<<", ">>", "**"]: 44 | return "%s%s%s" % (kids_str[0], self.value, kids_str[1]) 45 | else: 46 | return "%s %s %s" % (kids_str[0], self.value, kids_str[1]) 47 | if nkids == 3: 48 | if self.type == "if_expr": 49 | return "if %s then %s else %s endif" % (str(self.kids[0]), str(self.kids[1]), str(self.kids[2])) 50 | raise Exception("Internal error parsing (%s, %s)" % (repr(self.type), repr(self.value))) 51 | 52 | def __str__(self): 53 | if self._str == None: 54 | self._str = self._str_helper() 55 | return self._str 56 | 57 | def prefix_identifiers(self, prefix, next_prefix): 58 | "Prefix every identifier with a given string." 59 | if self.type == "ident": 60 | self.value = self.qmasm.apply_prefix(self.value, prefix, next_prefix) 61 | else: 62 | for k in self.kids: 63 | k.prefix_identifiers(prefix, next_prefix) 64 | 65 | def replace_ident(self, old_ident, new_ident): 66 | "Replace every occurrence of one identifer with another." 67 | if self.type == "ident": 68 | if self.value == old_ident: 69 | self.value = new_ident 70 | else: 71 | for k in self.kids: 72 | k.replace_ident(old_ident, new_ident) 73 | 74 | class EvaluationError(Exception): 75 | "Represent an exception thrown during AST evaluation." 76 | pass 77 | 78 | def _evaluate_ident(self, i2b): 79 | "Evaluate a variable, including array variables." 80 | val = 0 81 | for v in self.pin_parser.parse_lhs(self.value): 82 | try: 83 | bit = i2b[v] 84 | if bit == None: 85 | raise self.EvaluationError("Unused variable %s" % v) 86 | val = val*2 + bit 87 | except KeyError: 88 | raise self.EvaluationError("Undefined variable %s" % v) 89 | return val 90 | 91 | def _compile_unary(self, kvals): 92 | "Compile a unary expression." 93 | if self.value == "-": 94 | return lambda i2b: -kvals[0](i2b) 95 | elif self.value == "~": 96 | return lambda i2b: ~kvals[0](i2b) 97 | elif self.value == "!": 98 | return lambda i2b: int(kvals[0](i2b) == 0) 99 | elif self.value in ["+", "id"]: 100 | return lambda i2b: kvals[0](i2b) 101 | else: 102 | raise self.EvaluationError('Internal error compiling unary "%s"' % self.value) 103 | 104 | def _evaluate_power(self, base, exp): 105 | "Raise one integer to the power of another." 106 | if exp < 0: 107 | raise self.EvaluationError("Negative powers (%d) are not allowed" % exp) 108 | return base**exp 109 | 110 | def _compile_arith(self, kvals): 111 | "Compile an arithmetic expression." 112 | if self.value == "+": 113 | return lambda i2b: kvals[0](i2b) + kvals[1](i2b) 114 | elif self.value == "-": 115 | return lambda i2b: kvals[0](i2b) - kvals[1](i2b) 116 | elif self.value == "*": 117 | return lambda i2b: kvals[0](i2b) * kvals[1](i2b) 118 | elif self.value == "/": 119 | return lambda i2b: kvals[0](i2b) // kvals[1](i2b) 120 | elif self.value == "%": 121 | return lambda i2b: kvals[0](i2b) % kvals[1](i2b) 122 | elif self.value == "&": 123 | return lambda i2b: kvals[0](i2b) & kvals[1](i2b) 124 | elif self.value == "|": 125 | return lambda i2b: kvals[0](i2b) | kvals[1](i2b) 126 | elif self.value == "^": 127 | return lambda i2b: kvals[0](i2b) ^ kvals[1](i2b) 128 | elif self.value == "<<": 129 | return lambda i2b: kvals[0](i2b) << kvals[1](i2b) 130 | elif self.value == ">>": 131 | return lambda i2b: kvals[0](i2b) >> kvals[1](i2b) 132 | elif self.value == "**": 133 | return lambda i2b: self._evaluate_power(kvals[0](i2b), kvals[1](i2b)) 134 | else: 135 | raise self.EvaluationError("Internal error compiling arithmetic operator %s" % self.value) 136 | 137 | def _compile_rel(self, kvals): 138 | "Compile a relational expression." 139 | if self.value == "=": 140 | return lambda i2b: kvals[0](i2b) == kvals[1](i2b) 141 | elif self.value == "/=": 142 | return lambda i2b: kvals[0](i2b) != kvals[1](i2b) 143 | elif self.value == "<": 144 | return lambda i2b: kvals[0](i2b) < kvals[1](i2b) 145 | elif self.value == "<=": 146 | return lambda i2b: kvals[0](i2b) <= kvals[1](i2b) 147 | elif self.value == ">": 148 | return lambda i2b: kvals[0](i2b) > kvals[1](i2b) 149 | elif self.value == ">=": 150 | return lambda i2b: kvals[0](i2b) >= kvals[1](i2b) 151 | else: 152 | raise self.EvaluationError("Internal error compiling relational operator %s" % self.value) 153 | 154 | def _compile_conn(self, kvals): 155 | "Compile a logical connective." 156 | if self.value == "&&": 157 | return lambda i2b: kvals[0](i2b) and kvals[1](i2b) 158 | elif self.value == "||": 159 | return lambda i2b: kvals[0](i2b) or kvals[1](i2b) 160 | else: 161 | raise self.EvaluationError("Internal error compiling logical connective %s" % self.value) 162 | 163 | def _evaluate_if_expr(self, i2b, kvals): 164 | if kvals[0](i2b): 165 | return kvals[1](i2b) 166 | else: 167 | return kvals[2](i2b) 168 | 169 | def _compile_if_expr(self, kvals): 170 | "Compile an if...then...else expression." 171 | return lambda i2b: self._evaluate_if_expr(i2b, kvals) 172 | 173 | def _compile_node(self): 174 | 175 | """Compile the AST to a function that returns either True or False 176 | given a mapping from identifiers to bits.""" 177 | kvals = [k._compile_node() for k in self.kids] 178 | if self.type == "ident": 179 | # Variable 180 | return lambda i2b: self._evaluate_ident(i2b) 181 | elif self.type == "int": 182 | # Constant 183 | return lambda i2b: self.value 184 | elif self.type == "unary": 185 | # Unary expression 186 | return self._compile_unary(kvals) 187 | elif len(kvals) == 1: 188 | # All other single-child nodes return their child unmodified. 189 | return kvals[0] 190 | elif self.type in ["power", "term", "expr"]: 191 | return self._compile_arith(kvals) 192 | elif self.type == "rel": 193 | return self._compile_rel(kvals) 194 | elif self.type == "conn": 195 | return self._compile_conn(kvals) 196 | elif self.type == "if_expr": 197 | return self._compile_if_expr(kvals) 198 | else: 199 | raise self.EvaluationError("Internal error compiling AST node of type %s, value %s" % (repr(self.type), repr(self.value))) 200 | 201 | def compile(self): 202 | "Compile an AST for faster evaluation." 203 | self.code = self._compile_node() 204 | 205 | def evaluate(self, i2b): 206 | "Evaluate the AST to a value, given a mapping from identifiers to bits." 207 | try: 208 | return self.code(i2b) 209 | except self.EvaluationError as e: 210 | self.qmasm.abend("%s in assertion %s" % (e, self)) 211 | 212 | class AssertParser(object): 213 | int_re = re.compile(r'\d+') 214 | conn_re = re.compile(r'\|\||&&') 215 | rel_re = re.compile(r'/?=|[<>]=?') 216 | arith_re = re.compile(r'[-+/%&\|^~!]|>>|<<|\*\*?') 217 | keyword_re = re.compile(r'\b(if|then|else|endif)\b') 218 | 219 | def __init__(self, qmasm): 220 | self.qmasm = qmasm 221 | 222 | class ParseError(Exception): 223 | pass 224 | 225 | def lex(self, s): 226 | "Split a string into tokens (tuples of type and value)." 227 | tokens = [] 228 | s = s.lstrip() 229 | while len(s) > 0: 230 | # Match parentheses. 231 | if s[0] == "(": 232 | tokens.append(("lparen", "(")) 233 | s = s[1:].lstrip() 234 | continue 235 | if s[0] == ")": 236 | tokens.append(("rparen", ")")) 237 | s = s[1:].lstrip() 238 | continue 239 | 240 | # Match keywords. 241 | mo = self.keyword_re.match(s) 242 | if mo != None: 243 | match = mo.group(0) 244 | tokens.append((match, match)) 245 | s = s[len(match):].lstrip() 246 | continue 247 | 248 | # Match positive integers. 249 | mo = self.int_re.match(s) 250 | if mo != None: 251 | match = mo.group(0) 252 | tokens.append(("int", int(match))) 253 | s = s[len(match):].lstrip() 254 | continue 255 | 256 | # Match connectives. 257 | mo = self.conn_re.match(s) 258 | if mo != None: 259 | match = mo.group(0) 260 | tokens.append(("conn", match)) 261 | s = s[len(match):].lstrip() 262 | continue 263 | 264 | # Match "<<" and ">>" before we match "<" and ">". 265 | if len(s) >= 2 and (s[:2] == "<<" or s[:2] == ">>"): 266 | tokens.append(("arith", s[:2])) 267 | s = s[2:].lstrip() 268 | continue 269 | 270 | # Match relational operators. 271 | mo = self.rel_re.match(s) 272 | if mo != None: 273 | match = mo.group(0) 274 | tokens.append(("rel", match)) 275 | s = s[len(match):].lstrip() 276 | continue 277 | 278 | # Match "**" before we match "*". 279 | if len(s) >= 2 and s[:2] == "**": 280 | tokens.append(("power", s[:2])) 281 | s = s[2:].lstrip() 282 | continue 283 | 284 | # Match arithmetic operators. 285 | mo = self.arith_re.match(s) 286 | if mo != None: 287 | match = mo.group(0) 288 | tokens.append(("arith", match)) 289 | s = s[len(match):].lstrip() 290 | continue 291 | 292 | # Everything else is an identifier. 293 | mo = self.qmasm.ident_re.match(s) 294 | if mo != None: 295 | match = mo.group(0) 296 | tokens.append(("ident", match)) 297 | s = s[len(match):].lstrip() 298 | continue 299 | raise self.ParseError("Failed to parse %s" % s) 300 | tokens.append(("EOF", "EOF")) 301 | return tokens 302 | 303 | def advance(self): 304 | "Advance to the next symbol." 305 | self.tokidx += 1 306 | self.sym = self.tokens[self.tokidx] 307 | 308 | def accept(self, ty): 309 | """Advance to the next token if the current token matches a given 310 | token type and return True. Otherwise, return False.""" 311 | if self.sym[0] == ty: 312 | self.advance() 313 | return True 314 | return False 315 | 316 | def expect(self, ty): 317 | """Advance to the next token if the current token matches a given 318 | token. Otherwise, fail.""" 319 | if not self.accept(ty): 320 | raise self.ParseError("Expected %s but saw %s" % (ty, repr(self.sym[1]))) 321 | 322 | def generic_operator(self, return_type, child_method, sym_type, valid_ops): 323 | "Match one or more somethings to produce something else." 324 | # Produce a list of ASTs representing children. 325 | c = child_method() 326 | ops = [self.sym[1]] 327 | asts = [c] 328 | while self.sym[0] == sym_type and ops[-1] in valid_ops: 329 | self.advance() 330 | c = child_method() 331 | ops.append(self.sym[1]) 332 | asts.append(c) 333 | 334 | # Handle the trivial case of the identity operation. 335 | if len(asts) == 1: 336 | return AST(self.qmasm, return_type, None, asts) 337 | 338 | # Merge the ASTs in a left-associative fashion into a single AST. 339 | ops.pop() 340 | while len(asts) > 1: 341 | asts = [AST(self.qmasm, return_type, ops[0], [asts[0], asts[1]])] + asts[2:] 342 | ops.pop(0) 343 | return asts[0] 344 | 345 | def if_expr(self): 346 | "Return an if...then...else expression." 347 | self.expect("if") 348 | cond = self.conjunction() 349 | self.expect("then") 350 | then_expr = self.expression() 351 | self.expect("else") 352 | else_expr = self.expression() 353 | self.expect("endif") 354 | return AST(self.qmasm, "if_expr", None, [cond, then_expr, else_expr]) 355 | 356 | def factor(self): 357 | "Return a factor (variable, integer, or expression)." 358 | val = self.sym[1] 359 | if self.accept("ident"): 360 | child = AST(self.qmasm, "ident", val) 361 | elif self.accept("int"): 362 | child = AST(self.qmasm, "int", val) 363 | elif self.accept("lparen"): 364 | child = self.disjunction() 365 | self.expect("rparen") 366 | elif self.sym[0] == "arith": 367 | child = self.unary() 368 | elif self.sym[0] == "if": 369 | child = self.if_expr() 370 | elif val == "EOF": 371 | raise self.ParseError("Parse error at end of expression") 372 | else: 373 | raise self.ParseError('Parse error at "%s"' % val) 374 | return AST(self.qmasm, "factor", None, [child]) 375 | 376 | def power(self): 377 | "Return a factor or a factor raised to the power of a second factor." 378 | f1 = self.factor() 379 | op = self.sym[1] 380 | if self.sym[0] == "power" and op == "**": 381 | self.advance() 382 | f2 = self.power() 383 | return AST(self.qmasm, "power", op, [f1, f2]) 384 | return AST(self.qmasm, "power", None, [f1]) 385 | 386 | def unary(self): 387 | "Return a unary operator applied to a power." 388 | op = self.sym[1] 389 | if op in ["+", "-", "~", "!"]: 390 | self.advance() 391 | else: 392 | op = "id" 393 | return AST(self.qmasm, "unary", op, [self.power()]) 394 | 395 | def term(self): 396 | "Return a term (product of one or more unaries)." 397 | return self.generic_operator("term", self.unary, "arith", ["*", "/", "%", "&", "<<", ">>"]) 398 | 399 | def expression(self): 400 | "Return an expression (sum of one or more terms)." 401 | return self.generic_operator("expr", self.term, "arith", ["+", "-", "|", "^"]) 402 | 403 | def comparison(self): 404 | "Return a comparison of exactly two expressions." 405 | e1 = self.expression() 406 | op = self.sym[1] 407 | if self.sym[0] != "rel": 408 | return AST(self.qmasm, "rel", None, [e1]) 409 | self.advance() 410 | e2 = self.expression() 411 | return AST(self.qmasm, "rel", op, [e1, e2]) 412 | 413 | def conjunction(self): 414 | "Return a conjunction (logical AND of one or more comparisons)." 415 | return self.generic_operator("conn", self.comparison, "conn", ["&&"]) 416 | 417 | def disjunction(self): 418 | "Return a disjunction (logical OR of one or more conjunctions)." 419 | return self.generic_operator("conn", self.conjunction, "conn", ["||"]) 420 | 421 | def parse(self, s): 422 | "Parse a relational expression into an AST" 423 | self.tokens = self.lex(s) 424 | self.tokidx = -1 425 | self.advance() 426 | try: 427 | ast = self.disjunction() 428 | if self.sym[0] != "EOF": 429 | raise self.ParseError('Parse error at "%s"' % self.sym[1]) 430 | except self.ParseError as e: 431 | qmasm.abend('%s in "%s"' % (e, s)) 432 | return ast 433 | -------------------------------------------------------------------------------- /src/qmasm/cmdline.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Parse the QMASM command line # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | import argparse 7 | import re 8 | import shlex 9 | import string 10 | import sys 11 | 12 | class ParseCommandLine(object): 13 | def parse_command_line(self): 14 | "Parse the QMASM command line. Return an argparse.Namespace." 15 | 16 | # Define all of our command-line arguments. 17 | cl_parser = argparse.ArgumentParser(description="Assemble a symbolic Hamiltonian into a numeric one") 18 | cl_parser.add_argument("input", nargs="*", 19 | help="file(s) from which to read a symbolic Hamiltonian") 20 | cl_parser.add_argument("-v", "--verbose", action="count", default=0, 21 | help="increase output verbosity (can be specified repeatedly)") 22 | cl_parser.add_argument("--run", action="store_true", 23 | help="run the program on the current solver") 24 | cl_parser.add_argument("-o", "--output", metavar="FILE", default="", 25 | help="file to which to write weights and strengths (default: none)") 26 | cl_parser.add_argument("-O", type=int, nargs="?", const=1, default=0, 27 | metavar="LEVEL", 28 | help="optimize the layout; at -O1, remove unnecessary qubits") 29 | cl_parser.add_argument("--pin", action="append", 30 | help="pin a set of qubits to a set of true or false values") 31 | cl_parser.add_argument("--format", choices=["qubist", "ocean", "qbsolv", "qmasm", "numpy", "minizinc", "bqpjson"], default="qubist", 32 | help="output-file format") 33 | cl_parser.add_argument("--values", choices=["bools", "ints"], default="bools", 34 | help="output solution values as Booleans or integers (default: bools)") 35 | cl_parser.add_argument("--profile", type=str, default=None, metavar="NAME", 36 | help="Profile name from dwave.conf to use") 37 | cl_parser.add_argument("--solver", type=str, default=None, metavar="NAME", 38 | help='Solver name from dwave.conf to use or one of the special names "exact", "sim-anneal", "tabu", "greedy", "kerberos[,]", or "qbsolv[,]"') 39 | cl_parser.add_argument("--chain-strength", metavar="NEG_NUM", type=float, 40 | help="negative-valued chain strength (default: automatic)") 41 | cl_parser.add_argument("--pin-weight", metavar="NEG_NUM", type=float, 42 | help="negative-valued pin weight (default: automatic)") 43 | cl_parser.add_argument("--qubo", action="store_true", 44 | help="where supported, produce output files in QUBO rather than Ising format") 45 | cl_parser.add_argument("--samples", metavar="POS_INT", type=int, default=1000, 46 | help="number of samples to take (default: 1000)") 47 | cl_parser.add_argument("--anneal-time", metavar="POS_INT", type=int, default=None, 48 | help="annealing time in microseconds (default: automatic)") 49 | cl_parser.add_argument("--spin-revs", metavar="POS_INT", type=int, default=0, 50 | help="number of spin-reversal transforms to perform (default: 0)") 51 | cl_parser.add_argument("--topology-file", default=None, metavar="FILE", 52 | help="name of a file describing the topology (list of vertex pairs)") 53 | cl_parser.add_argument("--postproc", choices=["none", "sample", "opt"], 54 | default="none", 55 | help='type of postprocessing to perform (default: "none")') 56 | cl_parser.add_argument("--show", choices=["valid", "all", "best"], default="valid", 57 | help='show valid solutions, all solutions, or the best (even if invalid) solutions (default: "valid")') 58 | cl_parser.add_argument("--always-embed", action="store_true", 59 | help="when writing an output file, embed the problem in the physical topology even when not required (default: false)") 60 | cl_parser.add_argument("--composites", metavar="COMP1,COMP2,...", 61 | default="", 62 | help='wrap the solver within one or more composites (currently only "virtualgraph")') 63 | cl_parser.add_argument("--pack-qubits", metavar="POS_INT", type=int, 64 | help='attempt to pack the problem into an N-qubit "corner" of the physical topology during embedding') 65 | cl_parser.add_argument("--equality", metavar="FLOAT", type=float, default=0.0001, 66 | help="specify the maximum distance between energy levels to be considered equal") 67 | cl_parser.add_argument("--physical", action="store_true", 68 | help="map variables containing a number to the physical qubits represented by that number") 69 | cl_parser.add_argument("--schedule", metavar="T,S,...", type=str, 70 | help="specify an annealing schedule as alternating lists of times (microseconds) and annealing fractions (0.0 to 1.0)") 71 | cl_parser.add_argument("--visualize", action="store_true", 72 | help="run the D-Wave Problem Inspector on the result of the first submitted problem") 73 | 74 | # Parse the command line. 75 | cl_args = cl_parser.parse_args() 76 | 77 | # Perform a few sanity checks on the parameters. 78 | if cl_args.chain_strength != None and cl_args.chain_strength >= 0.0: 79 | self.warn("A non-negative chain strength (%.20g) was specified\n" % cl_args.chain_strength) 80 | if cl_args.pin_weight != None and cl_args.pin_weight >= 0.0: 81 | self.warn("A non-negative pin strength (%.20g) was specified\n" % cl_args.pin_weight) 82 | if cl_args.spin_revs > cl_args.samples: 83 | self.abend("The number of spin reversals is not allowed to exceed the number of samples") 84 | self.parse_composite_string(cl_args.composites) # Check for errors and discard the result. 85 | self.parse_anneal_sched_string(cl_args.schedule) # Check for errors and discard the result. 86 | return cl_args 87 | 88 | def parse_composite_string(self, cstr): 89 | "Split the composites string into a list. Abort on error." 90 | comps = [] 91 | if cstr == "": 92 | return comps 93 | for c in cstr.split(","): 94 | if c == "virtualgraph": 95 | comps.append("VirtualGraph") 96 | else: 97 | self.abend('Unrecognized composite "%s"' % c) 98 | return comps 99 | 100 | def parse_anneal_sched_string(self, astr): 101 | "Parse an annealing schedule into a list of (time, frac) tuples." 102 | if astr == None: 103 | return None 104 | num_re = re.compile(r'[-+Ee.\d]+') # All characters that can appear in a floating-point-number 105 | nums = num_re.findall(astr) 106 | if len(nums)%2 == 1: 107 | self.abend('Failed to parse "%s" as alternating times and annealing fractions' % astr) 108 | sched = [] 109 | for i in range(0, len(nums), 2): 110 | try: 111 | t = float(nums[i]) 112 | except ValueError: 113 | self.abend('Failed to parse "%s" as a floating-point number' % nums[i]) 114 | try: 115 | s = float(nums[i + 1]) 116 | except ValueError: 117 | self.abend('Failed to parse "%s" as a floating-point number' % nums[i + 1]) 118 | sched.append((t, s)) 119 | if len(sched) < 2: 120 | self.abend('Failed to parse "%s" into two or more (time, frac) pairs' % astr) 121 | return sched 122 | 123 | def get_command_line(self): 124 | "Return the command line as a string, properly quoted." 125 | return " ".join([shlex.quote(a) for a in sys.argv]) 126 | 127 | def report_command_line(self, cl_args): 128 | "For provenance and debugging purposes, report our command line parameters." 129 | # Output the command line as is. 130 | verbosity = cl_args.verbose 131 | if verbosity < 1: 132 | return 133 | sys.stderr.write("Command line provided:\n\n") 134 | sys.stderr.write(" %s\n\n" % self.get_command_line()) 135 | 136 | # At higher levels of verbosity, output every single option. 137 | if verbosity < 2: 138 | return 139 | sys.stderr.write("All QMASM parameters:\n\n") 140 | params = vars(cl_args) 141 | klen = max([len(a) for a in params.keys()]) 142 | klen = max(klen + 2, len("Option")) # +2 for "--" 143 | vlen = max([len(repr(a)) for a in params.values()]) 144 | klen = max(vlen, len("Value(s)")) 145 | sys.stderr.write(" %-*s %-*s\n" % (klen, "Option", vlen, "Value(s)")) 146 | sys.stderr.write(" %s %s\n" % ("-" * klen, "-" * vlen)) 147 | for k in sorted(params.keys()): 148 | kname = k.replace("_", "-") 149 | sys.stderr.write(" %-*s %-*s\n" % (klen, "--" + kname, vlen, repr(params[k]))) 150 | sys.stderr.write("\n") 151 | -------------------------------------------------------------------------------- /src/qmasm/output.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Output QUBOs in various formats # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | import datetime 7 | import json 8 | import numpy as np 9 | import random 10 | import shlex 11 | import sys 12 | 13 | class OutputMixin(object): 14 | "Provide functions for outputting problems and solutions." 15 | 16 | def open_output_file(self, oname, mode="w"): 17 | "Open a file or standard output." 18 | if oname == "": 19 | outfile = sys.stdout 20 | else: 21 | try: 22 | outfile = open(oname, mode) 23 | except IOError: 24 | self.abend('Failed to open %s for output' % oname) 25 | return outfile 26 | 27 | def output_qmasm(self, outfile): 28 | "Output weights and strengths as a flattened QMASM source file." 29 | for p in self.program: 30 | outfile.write("%s\n" % p.as_str()) 31 | 32 | def output_bqpjson(self, outfile, as_qubo, problem): 33 | "Output weights and strengths in bqpjson format, either Ising or QUBO." 34 | # Prepare the "easy" fields. 35 | bqp = {} 36 | bqp["version"] = "1.0.0" 37 | bqp["id"] = random.randint(2**20, 2**60) 38 | bqp["scale"] = 1.0 39 | bqp["offset"] = 0.0 40 | if as_qubo: 41 | bqp["variable_domain"] = "boolean" 42 | else: 43 | bqp["variable_domain"] = "spin" 44 | 45 | # Prepare the list of all variables. 46 | var_ids = set(problem.weights.keys()) 47 | for q1, q2 in problem.strengths.keys(): 48 | var_ids.add(q1) 49 | var_ids.add(q2) 50 | bqp["variable_ids"] = sorted(var_ids) 51 | 52 | # Prepare the linear terms. 53 | lin_terms = [] 54 | for q, wt in sorted(problem.weights.items()): 55 | lin_terms.append({ 56 | "id": q, 57 | "coeff": wt}) 58 | bqp["linear_terms"] = lin_terms 59 | 60 | # Prepare the quadratic terms. 61 | quad_terms = [] 62 | strengths = self.canonicalize_strengths(problem.strengths) 63 | for (q1, q2), wt in sorted(strengths.items()): 64 | quad_terms.append({ 65 | "id_tail": q1, 66 | "id_head": q2, 67 | "coeff": wt}) 68 | bqp["quadratic_terms"] = quad_terms 69 | 70 | # Prepare some metadata. 71 | metadata = {} 72 | if as_qubo: 73 | metadata["description"] = "QUBO problem compiled by QMASM (https://github.com/lanl/qmasm)" 74 | else: 75 | metadata["description"] = "Ising problem compiled by QMASM (https://github.com/lanl/qmasm)" 76 | metadata["command_line"] = self.get_command_line() 77 | metadata["generated"] = datetime.datetime.utcnow().isoformat() 78 | metadata["variable_names"] = {s: [n] 79 | for s, n in self.sym_map.symbol_number_items()} 80 | bqp["metadata"] = metadata 81 | 82 | # Output the problem in JSON format. 83 | outfile.write(json.dumps(bqp, indent=2, sort_keys=True) + "\n") 84 | 85 | def output_minizinc(self, outfile, as_qubo, problem, energy=None): 86 | "Output weights and strengths as a MiniZinc constraint problem." 87 | # Write some header information. 88 | outfile.write("""% Use MiniZinc to minimize a given Hamiltonian. 89 | % 90 | % Producer: QMASM (https://github.com/lanl/qmasm/) 91 | % Author: Scott Pakin (pakin@lanl.gov) 92 | """) 93 | outfile.write("%% Command line: %s\n\n" % " ".join([shlex.quote(a) for a in sys.argv])) 94 | 95 | # The model is easier to express as a QUBO so convert to that format. 96 | if as_qubo: 97 | qprob = problem 98 | else: 99 | qprob = problem.convert_to_qubo() 100 | 101 | # Map each qubit to one or more symbols. 102 | num2syms = {} 103 | for s, n in self.sym_map.symbol_number_items(): 104 | try: 105 | # Physical problem 106 | for pn in qprob.embedding[n]: 107 | try: 108 | num2syms[pn].append(s) 109 | except KeyError: 110 | num2syms[pn] = [s] 111 | except AttributeError: 112 | # Logical problem 113 | try: 114 | num2syms[n].append(s) 115 | except KeyError: 116 | num2syms[n] = [s] 117 | for n in num2syms.keys(): 118 | num2syms[n].sort(key=lambda s: ("$" in s, s)) 119 | 120 | # Find the character width of the longest list of symbol names. 121 | max_sym_name_len = max([len(repr(ss)) - 1 for ss in num2syms.values()] + [7]) 122 | 123 | # Output all QMASM variables as MiniZinc variables. 124 | qubits_used = set(qprob.weights.keys()) 125 | qubits_used.update([qs[0] for qs in qprob.strengths.keys()]) 126 | qubits_used.update([qs[1] for qs in qprob.strengths.keys()]) 127 | for q in sorted(qubits_used): 128 | outfile.write("var 0..1: q%d; %% %s\n" % (q, " ".join(num2syms[q]))) 129 | outfile.write("\n") 130 | 131 | # Define variables representing products of QMASM variables. Constrain 132 | # the product variables to be the products. 133 | outfile.write("% Define p_X_Y variables and constrain them to be the product of qX and qY.\n") 134 | for q0, q1 in sorted(qprob.strengths.keys()): 135 | pstr = "p_%d_%d" % (q0, q1) 136 | outfile.write("var 0..1: %s;\n" % pstr) 137 | outfile.write("constraint %s >= q%d + q%d - 1;\n" % (pstr, q0, q1)) 138 | outfile.write("constraint %s <= q%d;\n" % (pstr, q0)) 139 | outfile.write("constraint %s <= q%d;\n" % (pstr, q1)) 140 | outfile.write("\n") 141 | 142 | # Express energy as one, big Hamiltonian. 143 | scale_to_int = lambda f: int(round(10000.0*f)) 144 | outfile.write("var int: energy =\n") 145 | weight_terms = ["%8d * q%d" % (scale_to_int(wt), q) for q, wt in sorted(qprob.weights.items())] 146 | strength_terms = ["%8d * p_%d_%d" % (scale_to_int(s), qs[0], qs[1]) for qs, s in sorted(qprob.strengths.items())] 147 | all_terms = weight_terms + strength_terms 148 | outfile.write(" %s;\n" % " +\n ".join(all_terms)) 149 | 150 | # Because we can't both minimize and enumerate all solutions, we 151 | # normally do only the former with instructions for the user on how to 152 | # switch to the latter. However, if an energy was specified, comment 153 | # out the minimization step and uncomment the enumeration step. 154 | outfile.write("\n") 155 | outfile.write("% First pass: Compute the minimum energy.\n") 156 | if energy == None: 157 | outfile.write("solve minimize energy;\n") 158 | else: 159 | outfile.write("% solve minimize energy;\n") 160 | outfile.write(""" 161 | %% Second pass: Find all minimum-energy solutions. 162 | %% 163 | %% Once you've solved for minimum energy, comment out the "solve minimize 164 | %% energy" line, plug the minimal energy value into the following line, 165 | %% uncomment it and the "solve satisfy" line, and re-run MiniZinc, requesting 166 | %% all solutions this time. The catch is that you need to use the raw 167 | %% energy value so be sure to modify the output block to show(energy) 168 | %% instead of show(energy/%.10g + %.10g). 169 | """ % (self.minizinc_scale_factor, qprob.bqm.offset)) 170 | if energy == None: 171 | outfile.write("%constraint energy = -12345;\n") 172 | outfile.write("%solve satisfy;\n\n") 173 | else: 174 | outfile.write("constraint energy = %d;\n" % energy) 175 | outfile.write("solve satisfy;\n\n") 176 | 177 | # Output code to show the results symbolically. We output in the same 178 | # format as QMASM normally does. Unfortunately, I don't know how to get 179 | # MiniZinc to output the current solution number explicitly so I had to 180 | # hard-wire "Solution #1". 181 | outfile.write("output [\n") 182 | outfile.write(' "Solution #1 (energy = ", show(energy/%.10g + %.10g), ", tally = 1)\\n\\n",\n' % (self.minizinc_scale_factor, qprob.bqm.offset)) 183 | outfile.write(' " %-*s Spin Boolean\\n",\n' % (max_sym_name_len, "Name(s)")) 184 | outfile.write(' " %s ---- -------\\n",\n' % ("-" * max_sym_name_len)) 185 | outlist = [] 186 | for n, ss in num2syms.items(): 187 | if ss == []: 188 | continue 189 | syms = " ".join(ss) 190 | line = "" 191 | line += '" %-*s ", ' % (max_sym_name_len, syms) 192 | if as_qubo: 193 | line += 'show_int(4, q%d), ' % n 194 | else: 195 | line += 'show_int(4, 2*q%d - 1), ' % n 196 | line += '" ", if show(q%d) == "1" then "True" else "False" endif, ' % n 197 | line += '"\\n"' 198 | outlist.append(line) 199 | outlist.sort() 200 | outfile.write(" %s\n];\n" % ",\n ".join(outlist)) 201 | 202 | def output_qbsolv(self, outfile, problem): 203 | "Output weights and strengths in qbsolv format." 204 | # Determine the list of nonzero weights and strengths. 205 | qprob = problem.convert_to_qubo() 206 | output_weights, output_strengths = qprob.weights, qprob.strengths 207 | max_node = max(list(output_weights.keys()) + [max(qs) for qs in output_strengths.keys()]) 208 | num_nonzero_weights = len([q for q, wt in output_weights.items() if wt != 0.0]) 209 | num_nonzero_strengths = len([qs for qs, wt in output_strengths.items() if wt != 0.0]) 210 | 211 | # Assign dummy qubit numbers to qubits whose value is known a priori. 212 | try: 213 | n_known = len(qprob.known_values) 214 | except TypeError: 215 | n_known = 0 216 | try: 217 | extra_nodes = dict(zip(sorted(qprob.known_values.keys()), 218 | range(max_node + 1, max_node + 1 + n_known))) 219 | except AttributeError: 220 | extra_nodes = {} 221 | max_node += n_known 222 | num_nonzero_weights += n_known 223 | output_weights.update({num: qprob.known_values[sym]*qmasm.pin_weight 224 | for sym, num in extra_nodes.items()}) 225 | sym2num = dict(self.sym_map.symbol_number_items()) 226 | sym2num.update(extra_nodes) 227 | 228 | # Output a name-to-number map as header comments. 229 | key_width = 0 230 | val_width = 0 231 | items = [] 232 | for s, n in sym2num.items(): 233 | if len(s) > key_width: 234 | key_width = len(s) 235 | 236 | # Map logical to physical if possible. 237 | try: 238 | # Physical problem 239 | known_values = qprob.logical.merged_known_values() 240 | pin_map = {k: v for k, v in qprob.logical.pinned} 241 | except AttributeError: 242 | # Logical problem 243 | known_values = qprob.merged_known_values() 244 | pin_map = {k: v for k, v in qprob.pinned} 245 | try: 246 | nstr = " ".join([str(n) for n in sorted(qprob.embedding[n])]) 247 | except AttributeError: 248 | # Logical problem 249 | nstr = str(n) 250 | except KeyError: 251 | try: 252 | nstr = "[Pinned to %s]" % repr(pin_map[n]) 253 | except KeyError: 254 | try: 255 | nstr = "[Provably %s]" % known_values[n] 256 | except KeyError: 257 | try: 258 | same = qprob.logical.contractions[n] 259 | nstr = "[Same as %s]" % " ".join(self.sym_map.to_symbols(same)) 260 | except KeyError: 261 | nstr = "[Disconnected]" 262 | if len(nstr) > val_width: 263 | val_width = len(nstr) 264 | items.append((s, nstr)) 265 | items.sort() 266 | for s, nstr in items: 267 | outfile.write("c %-*s --> %-*s\n" % (key_width, s, val_width, nstr)) 268 | 269 | # Output all nonzero weights and strengths. 270 | outfile.write("p qubo 0 %d %d %d\n" % (max_node + 1, num_nonzero_weights, num_nonzero_strengths)) 271 | for q, wt in sorted(output_weights.items()): 272 | if wt != 0.0: 273 | outfile.write("%d %d %.10g\n" % (q, q, wt)) 274 | for qs, wt in sorted(output_strengths.items()): 275 | if wt != 0.0: 276 | outfile.write("%d %d %.10g\n" % (qs[0], qs[1], wt)) 277 | 278 | def output_qubist(self, outfile, as_qubo, problem, sampler): 279 | "Output weights and strengths in Qubist format, either Ising or QUBO." 280 | # Convert the problem to Ising, scale it for the hardware, then convert 281 | # to QUBO if requested. 282 | prob = problem.convert_to_ising() 283 | prob.autoscale_coefficients(sampler) 284 | if as_qubo: 285 | prob = prob.convert_to_qubo() 286 | output_weights = prob.weights 287 | output_strengths = prob.strengths 288 | 289 | # Format all weights and all strengths in Qubist format. 290 | data = [] 291 | for q, wt in sorted(output_weights.items()): 292 | if wt != 0.0: 293 | data.append("%d %d %.10g" % (q, q, wt)) 294 | for sp, str in sorted(output_strengths.items()): 295 | if str != 0.0: 296 | sp = sorted(sp) 297 | data.append("%d %d %.10g" % (sp[0], sp[1], str)) 298 | 299 | # Output the header and data in Qubist format. 300 | try: 301 | num_qubits = sampler.sampler.properties["num_qubits"] 302 | except KeyError: 303 | # If the solver lacks a fixed hardware representation we assert 304 | # that the number of hardware qubits is exactly the number of 305 | # qubits we require. 306 | num_qubits = len(output_weights) 307 | outfile.write("%d %d\n" % (num_qubits, len(data))) 308 | for d in data: 309 | outfile.write("%s\n" % d) 310 | 311 | def output_ocean(self, outfile, as_qubo, problem, sampler, sampler_args): 312 | "Output weights and strengths as an Ocean program, either Ising or QUBO." 313 | # Select each variable's alphabetically first symbol, favoring symbols 314 | # without dollar signs. 315 | all_nums = self.sym_map.all_numbers() 316 | num2sym = {} 317 | for n in all_nums: 318 | syms = list(self.sym_map.to_symbols(n)) 319 | syms.sort(key=lambda s: ("$" in s, s)) 320 | num2sym[n] = syms[0] 321 | 322 | # Output some Python boilerplate. 323 | outfile.write("#! /usr/bin/env python\n\n") 324 | outfile.write("import dimod\n") 325 | physical = getattr(problem, "logical", None) != None 326 | if physical: 327 | outfile.write("from dwave.system import DWaveSampler\n\n") 328 | else: 329 | outfile.write("from dwave.system import DWaveSampler, EmbeddingComposite\n\n") 330 | 331 | # Output code to set up the problem. 332 | if physical: 333 | linear = ", ".join(["%d: %.16g" % e for e in sorted(problem.bqm.linear.items())]) 334 | quadratic = ", ".join(["(%d, %d): %.20g" % (ns[0], ns[1], wt) for ns, wt in sorted(problem.bqm.quadratic.items())]) 335 | else: 336 | linear = ", ".join(sorted(["'%s': %.20g" % (num2sym[n], wt) for n, wt in problem.bqm.linear.items()])) 337 | quadratic = ", ".join(sorted(["('%s', '%s'): %.20g" % (num2sym[ns[0]], num2sym[ns[1]], wt) for ns, wt in problem.bqm.quadratic.items()])) 338 | outfile.write("linear = {%s}\n" % linear) 339 | outfile.write("quadratic = {%s}\n" % quadratic) 340 | if as_qubo: 341 | vtype = "BINARY" 342 | else: 343 | vtype = "SPIN" 344 | outfile.write("bqm = dimod.BinaryQuadraticModel(linear, quadratic, %.5g, dimod.%s)\n\n" % (problem.bqm.offset, vtype)) 345 | 346 | # Modify the sampler arguments as necessary. 347 | sampler_args = {k: v for k, v in sampler_args.items() if v != None} 348 | try: 349 | if sampler_args["anneal_schedule"] != None: 350 | del sampler_args["annealing_time"] 351 | except KeyError: 352 | pass 353 | try: 354 | pp = sampler_args["postprocess"] 355 | if pp == "sample": 356 | sampler_args["postprocess"] = "sampling" 357 | elif pp == "opt": 358 | sampler_args["postprocess"] = "optimization" 359 | else: 360 | del sampler_args["postprocess"] 361 | except KeyError: 362 | pass 363 | try: 364 | if sampler_args["num_spin_reversal_transforms"] == 0: 365 | del sampler_args["num_spin_reversal_transforms"] 366 | except: 367 | pass 368 | arg_str = ", ".join(["%s=%s" % (k, repr(v)) for k, v in sampler_args.items()]) 369 | 370 | # Output code to solve the problem. 371 | if physical: 372 | outfile.write("sampler = DWaveSampler()\n") 373 | else: 374 | outfile.write("sampler = EmbeddingComposite(DWaveSampler())\n") 375 | outfile.write("result = sampler.sample(bqm, %s)\n" % arg_str) 376 | 377 | # Output code to display the results QMASM-style. 378 | outfile.write(r''' 379 | data = result.data(fields=["sample", "energy", "num_occurrences"]) 380 | wd = max([8] + [len(v) for v in result.variables]) 381 | vnames = sorted(result.variables, key=lambda v: ("$" in v, v)) 382 | for i in range(len(result.samples())): 383 | if i > 0: 384 | print("") 385 | s = next(data) 386 | print("Solution #%d (energy = %.4g, tally = %d):\n" % (i + 1, s.energy, s.num_occurrences)) 387 | print(" %-*s Value" % (wd, "Variable")) 388 | print(" %s -----" % ("-"*wd)) 389 | for v in vnames: 390 | print(" %-*s %s" % (wd, v, s.sample[v] == 1)) 391 | ''') 392 | 393 | def output_npz(self, oname, problem): 394 | "Output a problem as a NumPy QUBO matrix." 395 | physical = getattr(problem, "logical", None) != None 396 | vars = sorted(problem.all_bqm_variables(force_recompute=True)) 397 | if physical: 398 | mat = problem.bqm.to_numpy_matrix(variable_order=vars).astype('d') 399 | np.savez_compressed(oname, qubo=mat, pvars=vars) 400 | else: 401 | sym_map = problem.qmasm.sym_map 402 | syms = [' '.join(sym_map.to_symbols(n)) for n in vars] 403 | mat = problem.bqm.to_numpy_matrix(variable_order=vars).astype('d') 404 | np.savez_compressed(oname, qubo=mat, lvars=vars, syms=syms) 405 | 406 | def write_output(self, problem, oname, oformat, as_qubo, sampler, sampler_args): 407 | "Write an output file in one of a variety of formats." 408 | 409 | # Open the output file. 410 | outfile = self.open_output_file(oname) 411 | 412 | # Output the weights and strengths in the specified format. 413 | if oformat == "qubist": 414 | self.output_qubist(outfile, as_qubo, problem, sampler) 415 | elif oformat == "ocean": 416 | self.output_ocean(outfile, as_qubo, problem, sampler, sampler_args) 417 | elif oformat == "qbsolv": 418 | self.output_qbsolv(outfile, problem) 419 | elif oformat == "qmasm": 420 | self.output_qmasm(outfile) 421 | elif oformat == "minizinc": 422 | self.output_minizinc(outfile, as_qubo, problem) 423 | elif oformat == "bqpjson": 424 | self.output_bqpjson(outfile, as_qubo, problem) 425 | elif oformat == "numpy": 426 | self.output_npz(oname, problem) 427 | else: 428 | self.abend('internal error outputting "%s"' % oformat) 429 | 430 | # Close the output file. 431 | if oname != "": 432 | outfile.close() 433 | -------------------------------------------------------------------------------- /src/qmasm/problem.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Define an Ising or QUBO problem # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | import copy 7 | import dimod 8 | import re 9 | import sys 10 | from collections import defaultdict 11 | from qmasm.assertions import AssertParser 12 | 13 | # Problem is currently just a thin veneer over dimod.BinaryQuadraticModel. If 14 | # it turns out we don't even need this veneer, we may replace it with direct 15 | # use of dimod.BinaryQuadraticModel. 16 | class Problem(object): 17 | "Represent an Ising problem." 18 | 19 | def __init__(self, qmasm): 20 | self.qmasm = qmasm # Pointer to the top-level QMASM class 21 | self.weights = defaultdict(lambda: 0.0) # Map from a spin to a point weight 22 | self.strengths = defaultdict(lambda: 0.0) # Map from a pair of spins to a coupler strength 23 | self.chains = set() # Subset of strengths keys that represents user-defined chains (always logical) 24 | self.antichains = set() # Subset of strengths keys that represents user-defined anti-chains (always logical) 25 | self.assertions = [] # List of assertions (as ASTs) to enforce 26 | self.pending_asserts = [] # List of {string, op, string} tuples pending conversion to assertions 27 | self.pinned = [] # Pairs of {unique number, Boolean} to pin 28 | self.known_values = {} # Map from a spin to -1 or +1 29 | self.bqm_vars = None # Set of all variables appearing in the BQM 30 | self.contractions = {} # Map from a spin to another spin it must be equal to 31 | self.num2syms = [] # Map from a logical variable number to all associated symbols 32 | 33 | def assign_chain_strength(self, ch_str): 34 | """Define a strength for each user-specified and automatically 35 | generated chain, and assign strengths to those chains (and negative 36 | strength to all anti-chains). Return the computed chain strength.""" 37 | chain_strength = ch_str 38 | if chain_strength == None: 39 | # Chain strength defaults to twice the maximum strength in the data. 40 | try: 41 | chain_strength = -2*max([abs(w) for w in self.strengths.values()]) 42 | except ValueError: 43 | # No strengths -- use weights instead. 44 | try: 45 | chain_strength = -2*max([abs(w) for w in self.weights.values()]) 46 | except ValueError: 47 | # No weights or strengths -- arbitrarily choose -1. 48 | chain_strength = -1.0 49 | for c in self.chains: 50 | self.strengths[c] += chain_strength 51 | for c in self.antichains: 52 | self.strengths[c] -= chain_strength 53 | return chain_strength 54 | 55 | def generate_bqm(self): 56 | "Generate a BinaryQuadraticModel version of the Problem." 57 | # Create an Ising-model BQM. 58 | bqm = dimod.BinaryQuadraticModel(self.weights, self.strengths, 0, dimod.SPIN) 59 | 60 | # Pin all variables the user asked to pin. 61 | bool2spin = {False: -1, True: +1} 62 | pins = {q: bool2spin[b] for q, b in self.pinned} 63 | for q in pins: 64 | # Ensure that every pinned variable exists. Otherwise, 65 | # fix_variables will throw a KeyError. 66 | bqm.add_variable(q, 0) 67 | bqm.fix_variables(pins) 68 | 69 | # Store the BQM. 70 | self.bqm = bqm 71 | 72 | def convert_to_qubo(self): 73 | "Return a copy of the problem with QUBO weights and strengths." 74 | qprob = copy.deepcopy(self) 75 | qprob.bqm.change_vartype(dimod.BINARY) 76 | qprob.weights = qprob.bqm.linear 77 | qprob.strengths = qprob.bqm.quadratic 78 | return qprob 79 | 80 | def convert_to_ising(self): 81 | "Return a copy of the problem with Ising weights and strengths." 82 | iprob = copy.deepcopy(self) 83 | iprob.bqm.change_vartype(dimod.SPIN) 84 | iprob.weights = iprob.bqm.linear 85 | iprob.strengths = iprob.bqm.quadratic 86 | return iprob 87 | 88 | def all_bqm_variables(self, force_recompute=False): 89 | "Return a set of all variables, referenced in linear and/or quadratic terms in the BQM." 90 | if self.bqm_vars != None and not force_recompute: 91 | return self.bqm_vars 92 | self.bqm_vars = set(self.bqm.linear) 93 | for u, v in self.bqm.quadratic: 94 | self.bqm_vars.add(u) 95 | self.bqm_vars.add(v) 96 | return self.bqm_vars 97 | 98 | def numeric_variables(self): 99 | """Return a mapping from a variable name to a list of physical qubits 100 | to which it should be mapped.""" 101 | var2phys = {} 102 | num_re = re.compile(r'\d+') 103 | for s, n in self.qmasm.sym_map.symbol_number_items(): 104 | nums = num_re.findall(s) 105 | if nums != []: 106 | var2phys[n] = [int(k) for k in nums] 107 | return var2phys 108 | 109 | def _edges_to_adj_list(self, edges): 110 | "Convert a list of edges to an adjacency list." 111 | adj = defaultdict(lambda: []) 112 | for u, v in edges: 113 | adj[u].append(v) 114 | adj[v].append(u) 115 | return adj 116 | 117 | def _traversal_from_root(self, adj, visited, root): 118 | """"Return a reversed traversal order of an adjacency list from a 119 | given root such that each right vertex is a leaf if all 120 | preceding right vertices are removed.""" 121 | order = [] 122 | new_visited = visited.copy() 123 | new_visited.add(root) 124 | for v in adj[root]: 125 | if v in new_visited: 126 | continue 127 | order.append((root, v)) 128 | ord, vis = self._traversal_from_root(adj, new_visited, v) 129 | order.extend(ord) 130 | new_visited.update(vis) 131 | return order, new_visited 132 | 133 | def traversal_order(self, edges): 134 | """"Return a reversed traversal order of a graph such that each right 135 | vertex is a leaf if all preceding right vertices are removed.""" 136 | adj = self._edges_to_adj_list(edges) 137 | order = [] 138 | nodes = set() 139 | for u, v in edges: 140 | nodes.add(u) 141 | nodes.add(v) 142 | visited = set() 143 | for u in nodes: 144 | if u in visited: 145 | continue 146 | ord, vis = self._traversal_from_root(adj, visited, u) 147 | order.extend(ord) 148 | visited.update(vis) 149 | order.reverse() 150 | return order 151 | 152 | def convert_chains_to_aliases(self, verbosity): 153 | "Replace user-defined chains with aliases." 154 | # Say what we're about to do 155 | if verbosity >= 2: 156 | sys.stderr.write("Replaced user-defined chains with aliases:\n\n") 157 | sys.stderr.write(" %6d logical qubits before optimization\n" % len(self.all_bqm_variables())) 158 | 159 | # At the time of this writing, a BinaryQuadraticModel elides variables 160 | # with a weight of zero. But then contract_variables complains that 161 | # the variable can't be found. Hence, we add back all zero-valued 162 | # variables just to keep contract_variables from failing. Similarly, 163 | # we ensure we have a strength defined (even if zero) for all chains to 164 | # prevent contract_variables from complaining. 165 | self.bqm.add_variables_from({q[0]: 0 for q in self.chains}) 166 | self.bqm.add_variables_from({q[1]: 0 for q in self.chains}) 167 | self.bqm.add_interactions_from({q: 0 for q in self.chains}) 168 | 169 | # Remove variables that are made equivalent to other variable via 170 | # user-defined chains. 171 | order = self.traversal_order(self.chains) 172 | for u, v in order: 173 | self.bqm.contract_variables(u, v) 174 | self.contractions[v] = u 175 | 176 | # Say what we just did. 177 | if verbosity >= 2: 178 | sys.stderr.write(" %6d logical qubits after optimization\n\n" % len(self.all_bqm_variables(force_recompute=True))) 179 | 180 | def simplify_problem(self, verbosity): 181 | "Find and remove variables with known outputs." 182 | # Say what we're going to do. 183 | if verbosity >= 2: 184 | sys.stderr.write("Simplified the problem:\n\n") 185 | sys.stderr.write(" %6d logical qubits before optimization\n" % len(self.all_bqm_variables())) 186 | 187 | # Simplify the BQM. 188 | self.known_values = dimod.roof_duality.fix_variables(self.bqm, True) 189 | self.bqm.fix_variables(self.known_values) 190 | 191 | # Say what we just did. 192 | if verbosity >= 2: 193 | num_left = len(self.all_bqm_variables(force_recompute=True)) 194 | sys.stderr.write(" %6d logical qubits after optimization\n\n" % num_left) 195 | if num_left == 0: 196 | sys.stderr.write(" Note: A complete solution can be found classically using roof duality.\n\n") 197 | 198 | def append_assertions_from_statements(self): 199 | "Convert user-specified chains, anti-chains, and pins to assertions." 200 | # Convert pending assertions to actual assertions. 201 | # TODO: Quote variables containing special characters. 202 | ap = AssertParser(self.qmasm) 203 | for s1, op, s2 in self.pending_asserts: 204 | ast = ap.parse(s1 + " " + op + " " + s2) 205 | ast.compile() 206 | self.assertions.append(ast) 207 | 208 | def merged_known_values(self): 209 | "Merge pinned, known_values, and chains into a s single dictionary." 210 | merged = {k: v for k, v in self.pinned} 211 | spin2bool = {-1: False, +1: True} 212 | for k, v in self.known_values.items(): 213 | merged[k] = spin2bool[v] 214 | remaining = copy.copy(self.chains) 215 | while len(remaining) > 0: 216 | still_remaining = set() 217 | for q0, q1 in remaining: 218 | if q0 in merged: 219 | merged[q1] = merged[q0] 220 | elif q1 in merged: 221 | merged[q0] = merged[q1] 222 | else: 223 | still_remaining.add((q0, q1)) 224 | if len(still_remaining) == len(remaining): 225 | break # No progress was made. 226 | remaining = still_remaining 227 | return merged 228 | 229 | def dangling_variables(self): 230 | "Return a set of variables that are neither embedded nor have a known value." 231 | dangling = set() 232 | known_values = self.logical.merged_known_values() 233 | for i in range(len(self.num2syms)): 234 | if self.num2syms[i] == []: 235 | continue 236 | if i not in self.embedding and i not in known_values and i not in self.contractions: 237 | dangling.update(self.num2syms[i]) 238 | return dangling 239 | 240 | def autoscale_coefficients(self, sampler): 241 | "Scale weights and strengths to match what the hardware allows." 242 | try: 243 | h_range = sampler.sampler.properties["h_range"] 244 | J_range = sampler.sampler.properties["j_range"] 245 | min_h = min([w for w in self.weights.values()]) 246 | max_h = max([w for w in self.weights.values()]) 247 | min_J = min([w for w in self.strengths.values()]) 248 | max_J = max([w for w in self.strengths.values()]) 249 | scale = min(abs(h_range[0]/min_h), abs(h_range[1]/max_h), 250 | abs(J_range[0]/min_J), abs(J_range[1]/max_J)) 251 | self.bqm.scale(scale) 252 | self.weights = self.bqm.linear 253 | self.strengths = self.bqm.quadratic 254 | except KeyError: 255 | pass # Probably a software solver. 256 | 257 | def _describe_logical_to_physical(self, reduce_contractions): 258 | "Map each logical qubit to zero or more physical qubits." 259 | # On the first pass, assign a descriptive tuple to each logical qubit 260 | # number. 261 | log2phys = [] # Map from a logical qubit to a descriptive tuple 262 | known_values = self.logical.merged_known_values() 263 | pin_map = {k: v for k, v in self.logical.pinned} 264 | for i in range(len(self.num2syms)): 265 | if self.num2syms[i] == []: 266 | log2phys.append(("unused",)) 267 | continue 268 | try: 269 | log2phys.append(("physical", sorted(self.embedding[i]))) 270 | except KeyError: 271 | try: 272 | log2phys.append(("pinned", pin_map[i])) 273 | except KeyError: 274 | try: 275 | log2phys.append(("known", known_values[i])) 276 | except KeyError: 277 | try: 278 | log2phys.append(("same_as", self.contractions[i])) 279 | except KeyError: 280 | if self.embedding == {}: 281 | log2phys.append(("physical", [i])) 282 | else: 283 | log2phys.append(("disconnected",)) 284 | 285 | # On the second, optional, pass, replace same_as tuples with whatever 286 | # they transitively point to. 287 | if reduce_contractions: 288 | for i in range(len(log2phys)): 289 | while log2phys[i][0] == "same_as": 290 | log2phys[i] = log2phys[log2phys[i][1]] 291 | return log2phys 292 | 293 | def _output_embedding_verbosely(self, max_sym_name_len, verbosity): 294 | "Verbosely output the mapping from logical to physical qubits." 295 | sys.stderr.write("Established a mapping from logical to physical qubits:\n\n") 296 | sys.stderr.write(" Logical %-*s Physical\n" % (max_sym_name_len, "Name(s)")) 297 | sys.stderr.write(" ------- %s -------------------------\n" % ("-" * max_sym_name_len)) 298 | log2phys_desc = self._describe_logical_to_physical(verbosity < 2) 299 | for i in range(len(log2phys_desc)): 300 | # Ignore unused logical qubits and "uninteresting" symbols (i.e., 301 | # those containing a "$"). 302 | desc = log2phys_desc[i] 303 | tag = desc[0] 304 | if tag == "unused": 305 | continue 306 | syms = self.num2syms[i] 307 | if verbosity < 2: 308 | syms = [s for s in syms if "$" not in s] 309 | if len(syms) == 0: 310 | continue 311 | name_list = " ".join(sorted(syms)) 312 | 313 | # Determine how to display the qubit based on the tag. 314 | if tag == "physical": 315 | phys_str = " ".join(["%4d" % e for e in desc[1]]) 316 | elif tag == "pinned": 317 | phys_str = "[Pinned to %s]" % repr(desc[1]) 318 | elif tag == "known": 319 | phys_str = "[Provably %s]" % repr(desc[1]) 320 | elif tag == "same_as": 321 | phys_str = "[Same as logical %d]" % desc[1] 322 | elif tag == "disconnected": 323 | phys_str = "[Disconnected]" 324 | else: 325 | self.qmasm.abend('Internal error: Unknown tag type "%s"' % tag) 326 | sys.stderr.write(" %7d %-*s %s\n" % (i, max_sym_name_len, name_list, phys_str)) 327 | sys.stderr.write("\n") 328 | 329 | def _output_embedding_tersely(self, max_sym_name_len): 330 | "Tersely output the mapping from logical to physical qubits." 331 | log2phys_comments = [] 332 | log2phys_desc = self._describe_logical_to_physical(True) 333 | for i in range(len(log2phys_desc)): 334 | # Ignore unused logical qubits and "uninteresting" symbols (i.e., 335 | # those containing a "$"). 336 | desc = log2phys_desc[i] 337 | tag = desc[0] 338 | if tag == "unused": 339 | continue 340 | syms = [s for s in self.num2syms[i] if "$" not in s] 341 | if len(syms) == 0: 342 | continue 343 | name_list = " ".join(sorted(syms)) 344 | 345 | # Determine how to display the qubit based on the tag. 346 | if tag == "physical": 347 | phys_str = " ".join(["%4d" % e for e in desc[1]]) 348 | elif tag == "pinned": 349 | phys_str = "[%s]" % repr(desc[1]) 350 | elif tag == "known": 351 | phys_str = "[%s]" % repr(desc[1]) 352 | elif tag == "same_as": 353 | same_str = " ".join(self.qmasm.sym_map.to_symbols(desc[1])) 354 | phys_str = "[Same as logical %s]" % same_str 355 | elif tag == "disconnected": 356 | phys_str = "[Disconnected]" 357 | else: 358 | self.qmasm.abend('Internal error: Unknown tag type "%s"' % tag) 359 | log2phys_comments.append("# %s --> %s" % (name_list, phys_str)) 360 | log2phys_comments.sort() 361 | sys.stderr.write("\n".join(log2phys_comments) + "\n") 362 | 363 | def output_embedding(self, verbosity, max_sym_name_len): 364 | "Output the mapping from logical to physical qubits." 365 | if verbosity > 0: 366 | self._output_embedding_verbosely(max_sym_name_len, verbosity) 367 | else: 368 | self._output_embedding_tersely(max_sym_name_len) 369 | 370 | def output_embedding_statistics(self): 371 | "Output various statistics about the embedding." 372 | # Tally the original set of variables. 373 | logical = self.logical 374 | all_vars = set(logical.weights) 375 | for q1, q2 in logical.strengths: 376 | all_vars.add(q1) 377 | all_vars.add(q2) 378 | 379 | # Tally the reduced set of variables. 380 | bqm = logical.bqm 381 | final_all_vars = set(bqm.linear.keys()) 382 | for q1, q2 in bqm.quadratic.keys(): 383 | final_all_vars.add(q1) 384 | final_all_vars.add(q2) 385 | 386 | # Determine how the original set of variables was reduced. 387 | log2phys_desc = self._describe_logical_to_physical(False) 388 | same_as = len([d for d in log2phys_desc if d[0] == 'same_as']) 389 | pinned = [d for d in log2phys_desc if d[0] == 'pinned'] 390 | pinned_true = len([p for p in pinned if p[1] == True]) 391 | pinned_false = len([p for p in pinned if p[1] == False]) 392 | 393 | # Output statistics. 394 | known_true = sum([v == 1 for v in logical.known_values.values()]) 395 | known_false = len(logical.known_values) - known_true 396 | sys.stderr.write("Computed the following statistics of the logical-to-physical mapping:\n\n") 397 | sys.stderr.write(" Type Metric Value\n") 398 | sys.stderr.write(" -------- ------------------ -----\n") 399 | sys.stderr.write(" Logical Original variables %5d\n" % len(all_vars)) 400 | sys.stderr.write(" Logical Provably true %5d\n" % known_true) 401 | sys.stderr.write(" Logical Provably false %5d\n" % known_false) 402 | sys.stderr.write(" Logical Pinned to true %5d\n" % pinned_true) 403 | sys.stderr.write(" Logical Pinned to false %5d\n" % pinned_false) 404 | sys.stderr.write(" Logical Aliased %5d\n" % same_as) 405 | sys.stderr.write(" Logical Original Strengths %5d\n" % len(logical.strengths)) 406 | sys.stderr.write(" Logical Equalities %5d\n" % len(logical.chains)) 407 | sys.stderr.write(" Logical Inequalities %5d\n" % len(logical.antichains)) 408 | sys.stderr.write(" Logical Final variables %5d\n" % len(final_all_vars)) 409 | sys.stderr.write(" Logical Final strengths %5d\n" % len(bqm.quadratic)) 410 | sys.stderr.write(" Physical Spins %5d\n" % len(self.weights)) 411 | sys.stderr.write(" Physical Couplers %5d\n" % len(self.strengths)) 412 | sys.stderr.write("\n") 413 | 414 | # Output some additional chain statistics. 415 | chain_lens = [len(c) for c in self.embedding.values()] 416 | max_chain_len = 0 417 | if chain_lens != []: 418 | max_chain_len = max(chain_lens) 419 | num_max_chains = len([l for l in chain_lens if l == max_chain_len]) 420 | sys.stderr.write(" Maximum chain length = %d (occurrences = %d)\n\n" % (max_chain_len, num_max_chains)) 421 | -------------------------------------------------------------------------------- /src/qmasm/solutions.py: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Store and filter solutions for QMASM # 3 | # By Scott Pakin # 4 | ######################################## 5 | 6 | import copy 7 | import math 8 | import numpy as np 9 | import re 10 | import sys 11 | from collections import defaultdict 12 | from dwave import inspector 13 | from dwave.embedding import unembed_sampleset, chain_breaks, chain_break_frequency 14 | from scipy.stats import median_absolute_deviation 15 | 16 | # Define a class to represent a single solution. 17 | class Solution: 18 | "Represent a near-minimal state of a spin system." 19 | 20 | def __init__(self, problem, sym2col, num2col, raw_ss, fixed_soln, tally, energy, all_vars): 21 | # Map named variables to spins. 22 | self.problem = problem 23 | self.sym2col = sym2col 24 | self.num2col = num2col 25 | self.raw_ss = raw_ss 26 | self.fixed_soln = fixed_soln 27 | self.tally = tally 28 | self.energy = energy 29 | self.id = 0 # Map from spins to an int 30 | self._checked_asserts = None # Memoized result of check_assertions 31 | 32 | # Determine which columns we care about. 33 | num2syms = self.problem.num2syms 34 | log2phys_desc = self.problem._describe_logical_to_physical(False) 35 | if all_vars: 36 | good_cols = set(self.sym2col.values()) 37 | else: 38 | good_cols = set() 39 | same_syms = self._same_symbols() 40 | for s, c in self.sym2col.items(): 41 | for other_s in same_syms[s]: 42 | if '$' not in other_s: 43 | good_cols.add(c) 44 | break 45 | 46 | # Compute an ID for the solution as a function of the "good" columns. 47 | for c in good_cols: 48 | self.id = self.id*2 + int((self.fixed_soln[c] + 1)//2) 49 | 50 | # Ensure every symbol has an associated value. 51 | self.sym2bool = self._all_symbol_values() 52 | 53 | def _same_symbols(self): 54 | "Map every symbol to a set of equivalent symbols." 55 | # Map each variable number to a set of symbols. 56 | all_num2syms = defaultdict(lambda: set()) 57 | for n, syms in enumerate(self.problem.num2syms): 58 | all_num2syms[n].update(syms) 59 | 60 | # Repeatedly merge sets where variables are equivalent. 61 | log2phys_desc = self.problem._describe_logical_to_physical(False) 62 | same_as = {n: info[1] for n, info in enumerate(log2phys_desc) if info[0] == 'same_as'} 63 | changed = True 64 | while changed: 65 | changed = False 66 | for n1, n2 in same_as.items(): 67 | s1 = all_num2syms[n1] 68 | s2 = all_num2syms[n2] 69 | ss = s1 | s2 70 | if len(s1) != len(ss) or len(s2) != len(ss): 71 | all_num2syms[n1] = ss 72 | all_num2syms[n2] = ss 73 | changed = True 74 | 75 | # Map each symbol to all equivalent symbols (including self). 76 | equiv_syms = {} 77 | for syms in all_num2syms.values(): 78 | for s in syms: 79 | equiv_syms[s] = syms 80 | return equiv_syms 81 | 82 | def _all_symbol_values(self): 83 | """Return a mapping from every symbol to a value, including symbols 84 | with no physical representation.""" 85 | # Assign values to all symbols that were eventually embedded. 86 | sym2bool = {} 87 | sym_map = self.problem.qmasm.sym_map 88 | all_sym2num = sym_map.symbol_number_items() 89 | for s, n in all_sym2num: 90 | try: 91 | c = self.num2col[n] 92 | sym2bool[s] = self.fixed_soln[c] == 1 93 | except KeyError: 94 | pass # We handle non-embedded symbols below. 95 | 96 | # Include values inferred from roof duality. 97 | for s, n in all_sym2num: 98 | try: 99 | spin = self.problem.logical.known_values[n] 100 | sym2bool[s] = spin == 1 101 | except KeyError: 102 | pass 103 | 104 | # Include variables pinned explicitly by the program or user. 105 | for num, bval in self.problem.logical.pinned: 106 | for s in sym_map.to_symbols(num): 107 | sym2bool[s] = bval 108 | 109 | # Include contracted variables (those chained to other another 110 | # variable's value). 111 | contracted = self.problem.traversal_order(self.problem.logical.chains) 112 | contracted.reverse() 113 | for num1, num2 in contracted: 114 | for s1 in sym_map.to_symbols(num1): 115 | for s2 in sym_map.to_symbols(num2): 116 | try: 117 | sym2bool[s2] = sym2bool[s1] 118 | except KeyError: 119 | pass 120 | return sym2bool 121 | 122 | def broken_chains(self): 123 | "Return True if the solution contains broken chains." 124 | unbroken = unembed_sampleset(self.raw_ss, self.problem.embedding, 125 | self.problem.logical.bqm, 126 | chain_break_method=chain_breaks.discard) 127 | return len(unbroken) == 0 128 | 129 | def broken_user_chains(self): 130 | "Return True if the solution contains broken user-specified chains or anti-chains." 131 | # Compare logical qubits for equal values. 132 | for lq1, lq2 in self.problem.logical.chains: 133 | try: 134 | idx1, idx2 = self.num2col[lq1], self.num2col[lq2] 135 | if self.fixed_soln[idx1] != self.fixed_soln[idx2]: 136 | return True 137 | except KeyError: 138 | pass # Elided logical qubit 139 | for lq1, lq2 in self.problem.logical.antichains: 140 | try: 141 | idx1, idx2 = self.num2col[lq1], self.num2col[lq2] 142 | if self.fixed_soln[idx1] == self.fixed_soln[idx2]: 143 | return True 144 | except KeyError: 145 | pass # Elided logical qubit 146 | return False 147 | 148 | def broken_pins(self): 149 | "Return True if the solution contains broken pins." 150 | bool2spin = [-1, +1] 151 | for pq, pin in self.problem.logical.pinned: 152 | try: 153 | idx = self.num2col[pq] 154 | if self.fixed_soln[idx] != bool2spin[pin]: 155 | return True 156 | except KeyError: 157 | pass # Elided logical qubit 158 | return False 159 | 160 | def check_assertions(self, stop_on_fail=False): 161 | "Return the result of applying each assertion." 162 | # Return the previous result, if any. 163 | if self._checked_asserts != None: 164 | return self._checked_asserts 165 | 166 | # Test each assertion in turn. 167 | results = [] 168 | sym2bit = {s: int(bval) for s, bval in self.sym2bool.items()} 169 | for a in self.problem.assertions: 170 | results.append((str(a), a.evaluate(sym2bit))) 171 | if stop_on_fail and not results[-1][1]: 172 | return results 173 | self._checked_asserts = results 174 | return self._checked_asserts 175 | 176 | def failed_assertions(self, stop_on_fail): 177 | "Return True if the solution contains failed assertions." 178 | return not all([a[1] for a in self.check_assertions(stop_on_fail)]) 179 | 180 | def output_asserts(self, verbosity): 181 | "Helper function for output_solution that outputs assertion results." 182 | # Do nothing if the program contains no assertions. Otherwise, output 183 | # some header text. 184 | if len(self.problem.assertions) == 0: 185 | return 186 | if verbosity >= 2: 187 | print(" Assertions made") 188 | print(" ---------------") 189 | else: 190 | print(" Assertions failed") 191 | print(" -----------------") 192 | 193 | # Output each assertion in turn. 194 | n_failed = 0 195 | for (astr, ok) in self.check_assertions(): 196 | if not ok: 197 | print(" FAIL: %s" % astr) 198 | n_failed += 1 199 | elif verbosity >= 2: 200 | print(" PASS: %s" % astr) 201 | if verbosity < 2 and n_failed == 0: 202 | print(" [none]") 203 | print("") 204 | 205 | def _numeric_solution(self): 206 | "Convert single- and multi-bit values to numbers." 207 | # Map each name to a number and to the number of bits required. 208 | idx_re = re.compile(r'^([^\[\]]+)\[(\d+)\]$') 209 | name2num = {} 210 | name2nbits = {} 211 | for nm, bval in self.sym2bool.items(): 212 | # Parse the name into a prefix and array index. 213 | match = idx_re.search(nm) 214 | if match == None: 215 | # No array index: Treat as a 1-bit number. 216 | name2num[nm] = int(bval) 217 | name2nbits[nm] = 1 218 | continue 219 | 220 | # Integrate the current spin into the overall number. 221 | array, idx = match.groups() 222 | b = int(bval) << int(idx) 223 | try: 224 | name2num[array] += b 225 | name2nbits[array] = max(name2nbits[array], int(idx) + 1) 226 | except KeyError: 227 | name2num[array] = b 228 | name2nbits[array] = int(idx) + 1 229 | 230 | # Merge the two maps. 231 | return {nm: (name2num[nm], name2nbits[nm]) for nm in name2num.keys()} 232 | 233 | def output_solution_bools(self, verbosity): 234 | "Output the solution as a list of Boolean variables." 235 | # Find the width of the longest symbol name. 236 | all_syms = self.sym2bool.keys() 237 | if verbosity < 2: 238 | all_syms = [s for s in all_syms if "$" not in s] 239 | all_syms = sorted(all_syms) 240 | max_sym_name_len = max(8, max([len(s) for s in all_syms])) 241 | 242 | # Output one symbol per line. 243 | print(" %-*s Value" % (max_sym_name_len, "Variable")) 244 | print(" %s -----" % ("-" * max_sym_name_len)) 245 | for s in all_syms: 246 | bval = self.sym2bool[s] 247 | print(" %-*s %s" % (max_sym_name_len, s, bval)) 248 | print("") 249 | 250 | def output_solution_ints(self, verbosity): 251 | "Output the solution as integer values." 252 | # Convert each value to a decimal and a binary string. Along the way, 253 | # find the width of the longest name and the largest number. 254 | name2info = self._numeric_solution() 255 | if verbosity < 2: 256 | name2info = {n: i for n, i in name2info.items() if "$" not in n} 257 | max_sym_name_len = max([len(s) for s in list(name2info.keys()) + ["Name"]]) 258 | max_decimal_len = 7 259 | max_binary_len = 6 260 | name2strs = {} 261 | for name, info in name2info.items(): 262 | bstr = ("{0:0" + str(info[1]) + "b}").format(info[0]) 263 | dstr = str(info[0]) 264 | max_binary_len = max(max_binary_len, len(bstr)) 265 | max_decimal_len = max(max_decimal_len, len(dstr)) 266 | name2strs[name] = (bstr, dstr) 267 | 268 | # Output one name per line. 269 | print(" %-*s %-*s Decimal" % (max_sym_name_len, "Name", max_binary_len, "Binary")) 270 | print(" %s %s %s" % ("-" * max_sym_name_len, "-" * max_binary_len, "-" * max_decimal_len)) 271 | for name, (bstr, dstr) in sorted(name2strs.items()): 272 | print(" %-*s %*s %*s" % (max_sym_name_len, name, max_binary_len, bstr, max_decimal_len, dstr)) 273 | print("") 274 | 275 | class Solutions(object): 276 | "Represent all near-minimal states of a spin system." 277 | 278 | def __init__(self, raw_results, answer, problem, all_vars): 279 | # Store our arguments. 280 | self.raw_results = raw_results 281 | self.answer = answer 282 | self.problem = problem 283 | 284 | # Unembed the solutions. Fix rather than discard invalid solutions. 285 | if self.problem.embedding == {}: 286 | fixed_answer = self.answer.copy() 287 | else: 288 | fixed_answer = unembed_sampleset(self.answer, self.problem.embedding, 289 | self.problem.logical.bqm, 290 | chain_break_method=chain_breaks.majority_vote) 291 | 292 | # Construct one Solution object per solution. 293 | self.solutions = [] 294 | energies = fixed_answer.record.energy 295 | tallies = fixed_answer.record.num_occurrences 296 | fixed_solns = fixed_answer.record.sample 297 | raw_solns = self.answer.record.sample 298 | sym2col, num2col = self._map_to_column(fixed_answer, 299 | self.problem.embedding, 300 | self.problem.qmasm.sym_map) 301 | for i in range(len(fixed_solns)): 302 | sset = self.answer.slice(i, i + 1) 303 | self.solutions.append(Solution(self.problem, sym2col, num2col, sset, 304 | fixed_solns[i], tallies[i], energies[i], 305 | all_vars)) 306 | 307 | # Store the frequency of chain breaks across the entire SampleSet. 308 | self.chain_breaks = chain_break_frequency(self.answer, self.problem.embedding) 309 | 310 | def _map_to_column(self, sset, embedding, sym_map): 311 | """Return a mapping from a symbol name and from a logical qubit number 312 | to a column number in a SampleSet.""" 313 | # Compute a mapping from logical qubit to column number. 314 | num2col = {} 315 | for i in range(len(sset.variables)): 316 | num2col[sset.variables[i]] = i 317 | 318 | # Compute a mapping from symbol to a column number. We need to go 319 | # symbol --> logical qubit --> column number. 320 | sym2col = {} 321 | for sym, num in sym_map.symbol_number_items(): 322 | try: 323 | sym2col[sym] = num2col[num] 324 | except KeyError: 325 | pass 326 | return sym2col, num2col 327 | 328 | def report_timing_information(self, verbosity): 329 | "Output solver timing information." 330 | if verbosity == 0: 331 | return 332 | timing_info = sorted(list(self.answer.info["timing"].items())) 333 | sys.stderr.write("Timing information:\n\n") 334 | sys.stderr.write(" %-30s %-10s\n" % ("Measurement", "Value (us)")) 335 | sys.stderr.write(" %s %s\n" % ("-" * 30, "-" * 10)) 336 | for timing_value in timing_info: 337 | sys.stderr.write(" %-30s %10d\n" % timing_value) 338 | sys.stderr.write("\n") 339 | 340 | def report_chain_break_information(self, verbosity): 341 | "Output information about common chain breaks." 342 | # Ensure we have something to report. 343 | if verbosity < 2: 344 | return 345 | sys.stderr.write("Chain-break frequencies:\n\n") 346 | total_breakage = sum(self.chain_breaks.values()) 347 | if total_breakage == 0.0: 348 | sys.stderr.write(" [No broken chains encountered]\n\n") 349 | return 350 | 351 | # Report only chains that have ever been broken. 352 | chain_breaks = [] 353 | max_name_len = 11 354 | sym_map = self.problem.qmasm.sym_map 355 | for n, f in self.chain_breaks.items(): 356 | ss = sym_map.to_symbols(n) 357 | sstr = " ".join(sorted(ss)) 358 | chain_breaks.append((sstr, f)) 359 | max_name_len = max(max_name_len, len(sstr)) 360 | chain_breaks.sort() 361 | sys.stderr.write(" %-*s Broken\n" % (max_name_len, "Variable(s)")) 362 | sys.stderr.write(" %s -------\n" % ("-" * max_name_len)) 363 | for sstr, f in chain_breaks: 364 | if f > 0.0: 365 | sys.stderr.write(" %-*s %6.2f%%\n" % (max_name_len, sstr, f*100.0)) 366 | sys.stderr.write("\n") 367 | 368 | def report_energy_tallies(self, filtered_solns, full_output): 369 | "Output information about the observed energy levels." 370 | # Acquire a tally of each energy level. 371 | energies_tallies = list(zip(self.answer.record.energy, self.answer.record.num_occurrences)) 372 | 373 | # If the caller requested full output, generate a complete energy 374 | # histogram. 375 | if full_output: 376 | # Merge energies based on a fixed number of digits after 377 | # the decimal point. 378 | raw_hist = {} 379 | for e, t in energies_tallies: 380 | estr = "%11.4f" % e 381 | try: 382 | raw_hist[estr] += t 383 | except KeyError: 384 | raw_hist[estr] = t 385 | 386 | # Output a histogram of energy values. 387 | sys.stderr.write("Histogram of raw energy values (merged to 4 digits after the decimal point):\n\n") 388 | sys.stderr.write(" Energy Tally\n") 389 | sys.stderr.write(" ----------- --------\n") 390 | for et in sorted(raw_hist.items(), reverse=True): 391 | sys.stderr.write(" %s %8d\n" % et) 392 | sys.stderr.write("\n") 393 | 394 | # Compute various statistics on the raw energies. 395 | raw_energies = np.array([v 396 | for lst in [[e]*t for e, t in energies_tallies] 397 | for v in lst]) 398 | raw_min = np.amin(raw_energies) 399 | raw_mean = np.mean(raw_energies) 400 | raw_median = np.median(raw_energies) 401 | raw_mad = median_absolute_deviation(raw_energies) 402 | raw_stddev = np.std(raw_energies) 403 | raw_max = np.amax(raw_energies) 404 | 405 | # Compute various statistics on the filtered energies. 406 | filtered_energies = np.array([v 407 | for lst in [[s.energy]*s.tally for s in filtered_solns.solutions] 408 | for v in lst]) 409 | if len(filtered_energies) == 0: 410 | filtered_min = math.nan 411 | filtered_mean = math.nan 412 | filtered_median = math.nan 413 | filtered_mad = math.nan 414 | filtered_stddev = math.nan 415 | filtered_max = math.nan 416 | else: 417 | filtered_min = np.amin(filtered_energies) 418 | filtered_mean = np.mean(filtered_energies) 419 | filtered_median = np.median(filtered_energies) 420 | filtered_mad = median_absolute_deviation(filtered_energies) 421 | filtered_stddev = np.std(filtered_energies) 422 | filtered_max = np.amax(filtered_energies) 423 | 424 | # Output energy statistics. 425 | sys.stderr.write("Energy statistics:\n\n") 426 | sys.stderr.write(" Statistic All solutions Filtered solutions\n") 427 | sys.stderr.write(" --------- ---------------------- -----------------------\n") 428 | sys.stderr.write(" Minimum %11.4f %11.4f\n" % (raw_min, filtered_min)) 429 | sys.stderr.write(" Median %11.4f +/- %-7.4f %11.4f +/- %-7.4f\n" % (raw_median, raw_mad, filtered_median, filtered_mad)) 430 | sys.stderr.write(" Mean %11.4f +/- %-7.4f %11.4f +/- %-7.4f\n" % (raw_mean, raw_stddev, filtered_mean, filtered_stddev)) 431 | sys.stderr.write(" Maximum %11.4f %11.4f\n" % (raw_max, filtered_max)) 432 | sys.stderr.write("\n") 433 | 434 | def discard_broken_chains(self): 435 | "Discard solutions with broken chains. Return the remaining solutions." 436 | if self.problem.embedding == {}: 437 | return self.solutions 438 | return [s for s in self.solutions if not s.broken_chains()] 439 | 440 | def discard_broken_user_chains(self): 441 | "Discard solutions with broken user-specified chains. Return the remaining solutions." 442 | return [s for s in self.solutions if not s.broken_user_chains()] 443 | 444 | def discard_broken_pins(self): 445 | "Discard solutions with broken pins. Return the remaining solutions." 446 | return [s for s in self.solutions if not s.broken_pins()] 447 | 448 | def discard_failed_assertions(self, stop_on_fail): 449 | "Discard solutions with failed assertions. Return the remaining solutions." 450 | return [s for s in self.solutions if not s.failed_assertions(stop_on_fail)] 451 | 452 | def discard_non_minimal(self, equality): 453 | '''Discard solutions with non-minimal energy. Return the remaining 454 | solutions. Use the argument as the minimum difference in floating-point 455 | values to be considered "equal".''' 456 | if len(self.solutions) == 0: 457 | return [] 458 | min_energy = min([s.energy for s in self.solutions]) 459 | return [s for s in self.solutions if abs(s.energy - min_energy) < equality] 460 | 461 | def merge_duplicates(self): 462 | """Merge duplicate solutions (same assignments to all non-ignored 463 | variables). Replace the merged solutions.""" 464 | id2soln = {} 465 | for s in copy.deepcopy(self.solutions): # Deep copy because of destructive update of tally 466 | try: 467 | id2soln[s.id].tally += s.tally 468 | except KeyError: 469 | id2soln[s.id] = s 470 | solutions = list(id2soln.values()) 471 | solutions.sort(key=lambda s: (s.energy, s.id)) 472 | return solutions 473 | 474 | def filter(self, show, verbose, nsamples, equality): 475 | '''Return solutions as filtered according to the "show" parameter. 476 | Output information about the filtering based on the "verbose" 477 | parameter. Perform floating-point comparisons using the "equality" 478 | parameter".''' 479 | # Prepare various views of the solutions. 480 | all_solns = copy.copy(self) 481 | valid_solns = copy.copy(self) 482 | best_solns = copy.copy(self) 483 | 484 | # Output the total number and number of unique solutions. 485 | if verbose >= 1: 486 | ndigits = len(str(nsamples)) 487 | sys.stderr.write("Number of solutions found (filtered cumulatively):\n\n") 488 | sys.stderr.write(" %*d total solutions\n" % (ndigits, nsamples)) 489 | sys.stderr.write(" %*d unique solutions\n" % (ndigits, len(all_solns.solutions))) 490 | 491 | # Filter out broken chains. 492 | if verbose >= 1 or show == "valid": 493 | valid_solns.solutions = valid_solns.discard_broken_chains() 494 | if verbose >= 1: 495 | sys.stderr.write(" %*d with no broken chains\n" % (ndigits, len(valid_solns.solutions))) 496 | if show == "best": 497 | filtered_best_solns = best_solns.discard_broken_chains() 498 | if len(filtered_best_solns) > 1: 499 | best_solns.solutions = filtered_best_solns 500 | 501 | # Filter out broken user-defined chains. 502 | if verbose >= 1 or show == "valid": 503 | valid_solns.solutions = valid_solns.discard_broken_user_chains() 504 | if verbose >= 1: 505 | sys.stderr.write(" %*d with no broken user-specified chains or anti-chains\n" % (ndigits, len(valid_solns.solutions))) 506 | if show == "best": 507 | filtered_best_solns = best_solns.discard_broken_user_chains() 508 | if len(filtered_best_solns) > 1: 509 | best_solns.solutions = filtered_best_solns 510 | 511 | # Filter out broken pins. 512 | if verbose >= 1 or show == "valid": 513 | valid_solns.solutions = valid_solns.discard_broken_pins() 514 | if verbose >= 1: 515 | sys.stderr.write(" %*d with no broken pins\n" % (ndigits, len(valid_solns.solutions))) 516 | if show == "best": 517 | filtered_best_solns = best_solns.discard_broken_pins() 518 | if len(filtered_best_solns) > 1: 519 | best_solns.solutions = filtered_best_solns 520 | 521 | # Filter out failed assertions. 522 | stop_on_fail = show == "valid" and verbose < 2 523 | if verbose >= 1 or show == "valid": 524 | valid_solns.solutions = valid_solns.discard_failed_assertions(stop_on_fail) 525 | if verbose >= 1: 526 | sys.stderr.write(" %*d with no failed assertions\n" % (ndigits, len(valid_solns.solutions))) 527 | if show == "best": 528 | filtered_best_solns = best_solns.discard_failed_assertions(stop_on_fail) 529 | if len(filtered_best_solns) > 1: 530 | best_solns.solutions = filtered_best_solns 531 | 532 | # Filter out solutions that are not at minimal energy. 533 | if verbose >= 1 or show == "valid": 534 | valid_solns.solutions = valid_solns.discard_non_minimal(equality) 535 | if verbose >= 1: 536 | sys.stderr.write(" %*d at minimal energy\n" % (ndigits, len(valid_solns.solutions))) 537 | if show == "best": 538 | filtered_best_solns = best_solns.discard_non_minimal(equality) 539 | if len(filtered_best_solns) > 1: 540 | best_solns.solutions = filtered_best_solns 541 | 542 | # Merge duplicate solutions. 543 | if verbose >= 1 or show == "valid": 544 | valid_solns.solutions = valid_solns.merge_duplicates() 545 | if verbose >= 1: 546 | sys.stderr.write(" %*d excluding duplicate variable assignments\n" % (ndigits, len(valid_solns.solutions))) 547 | sys.stderr.write("\n") 548 | if show == "best": 549 | filtered_best_solns = best_solns.merge_duplicates() 550 | if len(filtered_best_solns) > 1: 551 | best_solns.solutions = filtered_best_solns 552 | 553 | # Return the requested set of solutions. Sort these first by energy 554 | # then by ID. 555 | soln_key = lambda s: (s.energy, s.id) 556 | if show == "valid": 557 | valid_solns.solutions.sort(key=soln_key) 558 | return valid_solns 559 | elif show == "all": 560 | all_solns.solutions.sort(key=soln_key) 561 | return all_solns 562 | elif show == "best": 563 | best_solns.solutions.sort(key=soln_key) 564 | return best_solns 565 | else: 566 | raise Exception("Internal error processing --show=%s" % show) 567 | 568 | def output_solutions(self, style, verbosity, show_asserts): 569 | "Output a user-readable solution to the standard output device." 570 | # Output each solution in turn. 571 | if len(self.solutions) == 0: 572 | print("No valid solutions found.") 573 | return 574 | for snum in range(len(self.solutions)): 575 | soln = self.solutions[snum] 576 | print("Solution #%d (energy = %.4f, tally = %s):\n" % (snum + 1, soln.energy, soln.tally)) 577 | if style == "bools": 578 | soln.output_solution_bools(verbosity) 579 | elif style == "ints": 580 | soln.output_solution_ints(verbosity) 581 | else: 582 | raise Exception('Output style "%s" not recognized' % style) 583 | if show_asserts: 584 | soln.output_asserts(verbosity) 585 | 586 | def visualize(self): 587 | "Run the D-Wave Problem Inspector on the first raw result." 588 | try: 589 | inspector.show(self.raw_results[0]) 590 | except: 591 | self.problem.qmasm.abend('Failed to run the D-Wave Problem Inspector') 592 | -------------------------------------------------------------------------------- /src/qmasm/solve.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Solve a binary quadratic model # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | import copy 7 | import dimod 8 | import hashlib 9 | import json 10 | import marshal 11 | import minorminer 12 | import networkx as nx 13 | import os 14 | import random 15 | import re 16 | import sys 17 | import threading 18 | import time 19 | from collections import defaultdict 20 | from concurrent.futures import ThreadPoolExecutor 21 | from dimod import ExactSolver, SampleSet 22 | from dwave.cloud import Client, hybrid, qpu, sw 23 | from dwave.cloud.exceptions import SolverFailureError, SolverNotFoundError 24 | from dwave.embedding import embed_bqm 25 | from dwave.system import DWaveSampler, EmbeddingComposite, VirtualGraphComposite, LeapHybridSampler 26 | from dwave_qbsolv import QBSolv 27 | from greedy import SteepestDescentSolver 28 | from hybrid.reference.kerberos import KerberosSampler 29 | from neal import SimulatedAnnealingSampler 30 | from qmasm.solutions import Solutions 31 | from qmasm.utils import SpecializedPriorityQueue 32 | from tabu import TabuSampler 33 | 34 | class EmbeddingCache(object): 35 | "Read and write an embedding cache file." 36 | 37 | def __init__(self, qmasm, edges, adj, max_qubits, var2phys): 38 | # Ensure we have a valid cache directory. 39 | self.hash = None 40 | try: 41 | self.cachedir = os.environ["QMASMCACHE"] 42 | except KeyError: 43 | self.cachedir = None 44 | return None 45 | if not os.path.isdir(self.cachedir): 46 | qmasm.abend("QMASMCACHE is set to %s, which is not an extant directory" % self.cachedir) 47 | 48 | # Compute a SHA-1 sum of our inputs. 49 | sha = hashlib.sha1() 50 | sha.update(str(sorted(edges)).encode("utf-8")) 51 | sha.update(str(sorted(adj)).encode("utf-8")) 52 | sha.update(str(max_qubits).encode("utf-8")) 53 | sha.update(str(sorted(var2phys.items())).encode("utf-8")) 54 | self.hash = sha.hexdigest() 55 | 56 | def read(self): 57 | "Read an embedding from an embedding cache or None on a cache miss." 58 | if self.hash == None: 59 | return None 60 | try: 61 | h = open(os.path.join(self.cachedir, self.hash), "rb") 62 | except IOError: 63 | return None 64 | embedding = marshal.load(h) 65 | h.close() 66 | return embedding 67 | 68 | def write(self, embedding): 69 | "Write an embedding to an embedding cache." 70 | if self.hash == None: 71 | return 72 | try: 73 | h = open(os.path.join(self.cachedir, self.hash), "wb") 74 | except IOError: 75 | return None 76 | marshal.dump(embedding, h) 77 | h.close() 78 | 79 | class Sampler(object): 80 | "Interface to Ocean samplers." 81 | 82 | # Keep track of parameters rejected by the solver. 83 | unexp_arg_re = re.compile(r"got an unexpected keyword argument '(\w+)'") 84 | not_param_re = re.compile(r"(\w+) is not a parameter of this solver") 85 | unk_param_re = re.compile(r"Unknown parameter (\w+)") 86 | 87 | def __init__(self, qmasm, profile=None, solver=None): 88 | "Acquire either a software sampler or a sampler representing a hardware solver." 89 | self.qmasm = qmasm 90 | self.profile = profile 91 | self.sampler, self.client_info, self.extra_solver_params = self.get_sampler(profile, solver) 92 | self.rejected_params = [] 93 | self.rejected_params_lock = threading.Lock() 94 | self.final_params = {} # Final parameters associated with the first QMI 95 | self.final_params_sem = threading.Semaphore(0) 96 | 97 | def get_sampler(self, profile, solver): 98 | "Return a dimod.Sampler object and associated solver information." 99 | # Handle built-in software samplers as special cases. 100 | info = {} 101 | if solver != None: 102 | info["solver_name"] = solver 103 | if solver == "exact": 104 | return ExactSolver(), info, {} 105 | elif solver == "neal": 106 | return SimulatedAnnealingSampler(), info, {} 107 | elif solver == "tabu": 108 | return TabuSampler(), info, {} 109 | elif solver == "greedy": 110 | return SteepestDescentSolver(), info, {} 111 | elif solver == "kerberos" or (solver != None and solver[:9] == "kerberos,"): 112 | base_sampler = KerberosSampler() 113 | try: 114 | sub_sampler_name = solver.split(",")[1] 115 | except IndexError: 116 | sub_sampler_name = None 117 | sub_sampler, sub_info, params = self.get_sampler_from_config(profile, sub_sampler_name, "qpu") 118 | info.update(self._recursive_properties(sub_sampler)) 119 | info["solver_name"] = "kerberos + %s" % sub_info["solver_name"] 120 | params["qpu_sampler"] = sub_sampler 121 | return base_sampler, info, params 122 | elif solver == "qbsolv" or (solver != None and solver[:7] == "qbsolv,"): 123 | base_sampler = QBSolv() 124 | try: 125 | sub_sampler_name = solver.split(",")[1] 126 | except IndexError: 127 | sub_sampler_name = None 128 | sub_sampler, sub_info, params = self.get_sampler(profile, sub_sampler_name) 129 | if getattr(sub_sampler, "structure", None) != None: 130 | sub_sampler = EmbeddingComposite(sub_sampler) 131 | info.update(self._recursive_properties(sub_sampler)) 132 | info["solver_name"] = "QBSolv + %s" % sub_info["solver_name"] 133 | params["solver"] = sub_sampler 134 | return base_sampler, info, params 135 | 136 | # In the common case, read the configuration file, either the 137 | # default or the one named by the DWAVE_CONFIG_FILE environment 138 | # variable. 139 | return self.get_sampler_from_config(profile, solver) 140 | 141 | def get_sampler_from_config(self, profile=None, solver=None, sampler_type=None): 142 | """Return a dimod.Sampler object found in the user's configuration file, 143 | associated solver information, and any extra parameters needed.""" 144 | try: 145 | with Client.from_config(profile=profile, client=sampler_type) as client: 146 | if solver == None: 147 | solver = client.default_solver 148 | solver_name = solver["name__eq"] 149 | else: 150 | solver_name = solver 151 | solver = {"name": solver} 152 | if isinstance(client, hybrid.Client): 153 | sampler = LeapHybridSampler(profile=profile, solver=solver) 154 | elif isinstance(client, sw.Client): 155 | self.qmasm.abend("QMASM does not currently support remote software solvers") 156 | else: 157 | sampler = DWaveSampler(profile=profile, solver=solver) 158 | info = self._recursive_properties(sampler) 159 | info["solver_name"] = solver_name 160 | info["endpoint"] = client.endpoint 161 | if profile != None: 162 | info["profile"] = profile 163 | return sampler, info, {} 164 | except Exception as err: 165 | self.qmasm.abend("Failed to construct a sampler (%s)" % str(err)) 166 | 167 | def _recursive_properties(self, sampler): 168 | "Perform a postfix traversal of a sampler's children's properties." 169 | if sampler == None: 170 | return {} 171 | props = {} 172 | for c in getattr(sampler, "children", []): 173 | props.update(c.properties) 174 | props.update(sampler.properties) 175 | return props 176 | 177 | def show_properties(self, verbose): 178 | "Output either short or all solver properties." 179 | if verbose == 0: 180 | return 181 | 182 | # Determine the width of the widest key. 183 | props = self.client_info.copy() 184 | max_key_len = len("Parameter") 185 | prop_keys = sorted(props) 186 | for k in prop_keys: 187 | max_key_len = max(max_key_len, len(k)) 188 | 189 | # Output either "short" values (if verbose = 1) or all values (if 190 | # verbose > 1). 191 | short_value_len = 70 - max_key_len 192 | sys.stderr.write("Encountered the following solver properties:\n\n") 193 | sys.stderr.write(" %-*s Value\n" % (max_key_len, "Parameter")) 194 | sys.stderr.write(" %s %s\n" % ("-" * max_key_len, "-" * max(5, short_value_len))) 195 | for k in prop_keys: 196 | if isinstance(props[k], str): 197 | val_str = props[k] 198 | else: 199 | val_str = repr(props[k]) 200 | if verbose >= 2 or len(val_str) <= short_value_len: 201 | sys.stderr.write(" %-*s %s\n" % (max_key_len, k, val_str)) 202 | elif len(val_str) > short_value_len: 203 | # Value is too long for a single -v. 204 | if isinstance(props[k], list): 205 | val_str = "(%d items; use -v -v to view)" % len(props[k]) 206 | else: 207 | val_str = "(%d characters; use -v -v to view)" % len(val_str) 208 | sys.stderr.write(" %-*s %s\n" % (max_key_len, k, val_str)) 209 | sys.stderr.write("\n") 210 | 211 | def _find_adjacency(self, sampler): 212 | "Perform a depth-first search for an adjacency attribute." 213 | # Successful base case: The given sampler reports its adjacency 214 | # structure. 215 | try: 216 | # QPU and hybrid provide adjacency directly. 217 | return sampler.adjacency 218 | except AttributeError: 219 | try: 220 | # Software samplers provide adjacency as pairs of qubits. 221 | return self._pairs_to_dict(sampler.properties["couplers"]) 222 | except KeyError: 223 | pass 224 | 225 | # Failed base case: The given sampler has no children. 226 | try: 227 | children = sampler.children 228 | except AttributeError: 229 | return None 230 | 231 | # Recursive case: Search each child for its adjacency. 232 | for c in children: 233 | adj = self._find_adjacency(c) 234 | if adj != None: 235 | return adj 236 | return None 237 | 238 | def _pairs_to_dict(self, adj): 239 | """Convert from vertex pairs to a dictionary from a vertex to its 240 | neighbors. Note that we treat all edges as bidirectional.""" 241 | adj_dict = {} 242 | for u, v in adj: 243 | try: 244 | adj_dict[u].append(v) 245 | except KeyError: 246 | adj_dict[u] = [v] 247 | try: 248 | adj_dict[v].append(u) 249 | except KeyError: 250 | adj_dict[v] = [u] 251 | return adj_dict 252 | 253 | def get_hardware_adjacency(self): 254 | "Return the hardware adjacency structure, if any." 255 | return self._find_adjacency(self.sampler) 256 | 257 | def read_hardware_adjacency(self, fname, verbosity): 258 | """Read a hardware adjacency list from a file. Each line must contain 259 | a space-separated pair of vertex numbers.""" 260 | # Read from a file to a set of vertex pairs. 261 | adj = set() 262 | lineno = 0 263 | if verbosity >= 2: 264 | sys.stderr.write("Reading hardware adjacency from %s.\n" % fname) 265 | with open(fname) as f: 266 | for orig_line in f: 267 | # Discard comments then parse the line into exactly two vertices. 268 | lineno += 1 269 | line = orig_line.partition("#")[0] 270 | try: 271 | verts = [int(v) for v in line.split()] 272 | except ValueError: 273 | self.qmasm.abend('Failed to parse line %d of file %s ("%s")' % (lineno, fname, orig_line.strip())) 274 | if len(verts) == 0: 275 | continue 276 | if len(verts) != 2 or verts[0] == verts[1]: 277 | self.qmasm.abend('Failed to parse line %d of file %s ("%s")' % (lineno, fname, orig_line.strip())) 278 | 279 | # Canonicalize the vertex numbers and add the result to the set. 280 | if verts[1] > verts[0]: 281 | verts[0], verts[1] = verts[1], verts[0] 282 | adj.add((verts[0], verts[1])) 283 | if verbosity >= 2: 284 | sys.stderr.write("%d unique edges found\n\n" % len(adj)) 285 | return self._pairs_to_dict(adj) 286 | 287 | def _find_embedding(self, edges, adj, max_qubits, **kwargs): 288 | "Wrap minorminer.find_embedding with a version that intercepts its output." 289 | # Minorminer accepts the hardware adjacency as a list of 290 | # pairs, not a map from each node to its neighbors. 291 | mm_adj = [(u, v) for u in adj for v in adj[u]] 292 | 293 | # Given verbose=0, invoke minorminer directly. 294 | if "verbose" not in kwargs or kwargs["verbose"] == 0: 295 | # Convert all keys to integers for consistency. 296 | embedding = minorminer.find_embedding(edges, mm_adj, **kwargs) 297 | return {int(k): v for k, v in embedding.items()} 298 | 299 | # minorminer's find_embedding is hard-wired to write to stdout. 300 | # Trick it into writing into a pipe instead. 301 | sepLine = "=== EMBEDDING ===\n" 302 | r, w = os.pipe() 303 | pid = os.fork() 304 | if pid == 0: 305 | # Child -- perform the embedding. 306 | os.close(r) 307 | os.dup2(w, sys.stdout.fileno()) 308 | embedding = minorminer.find_embedding(edges, mm_adj, **kwargs) 309 | sys.stdout.flush() 310 | os.write(w, sepLine.encode()) 311 | os.write(w, (json.dumps(embedding) + "\n").encode()) 312 | os.close(w) 313 | os._exit(0) 314 | else: 315 | # Parent -- report the embedding's progress. 316 | os.close(w) 317 | pipe = os.fdopen(r, "r", 10000) 318 | while True: 319 | try: 320 | rstr = pipe.readline() 321 | if rstr == sepLine: 322 | break 323 | if rstr == "": 324 | self.qmasm.abend("Embedder failed to terminate properly") 325 | sys.stderr.write(" %s" % rstr) 326 | except: 327 | pass 328 | 329 | # Receive the embedding from the child. Convert all keys to 330 | # integers for consistency. 331 | embedding = json.loads(pipe.readline()) 332 | return {int(k): v for k, v in embedding.items()} 333 | 334 | def _adj_subset(self, hw_adj, num): 335 | "Return a subset of a given size of the hardware adjacency graph." 336 | # Construct a NetworkX graph from the given adjacency list. 337 | # Set each node's popularity to 0. 338 | G = nx.Graph() 339 | for n1, ns in hw_adj.items(): 340 | G.add_edges_from([(n1, n2) for n2 in ns]) 341 | s0 = min(G.nodes) 342 | 343 | # Construct a priority queue of nodes. 344 | shuffled_nodes = list(G.nodes) 345 | random.shuffle(shuffled_nodes) 346 | priQ = SpecializedPriorityQueue(shuffled_nodes) 347 | 348 | # Traverse the graph in order of decreasing node popularity. 349 | priQ.increase_priority(s0) 350 | visited = set() 351 | order = [] 352 | while len(order) < num: 353 | # Pop the most popular node. 354 | node = priQ.pop_max() 355 | if node in visited: 356 | continue 357 | visited.add(node) 358 | order.append(node) 359 | 360 | # Increase the popularity of the current node's immediate neighbors. 361 | for nn in G.neighbors(node): 362 | if nn not in visited: 363 | priQ.increase_priority(nn) 364 | 365 | # Find all edges connecting the first num nodes, and convert these to 366 | # the format Ocean expects. 367 | adj = defaultdict(lambda: []) 368 | for u, v in G.subgraph(order).edges: 369 | adj[u].append(v) 370 | adj[v].append(u) 371 | return adj 372 | 373 | def _impose_qubit_packing(self, max_qubits, edges, hw_adj, **embed_args): 374 | 'Pack qubits into a "corner" of the physical topology.' 375 | packed_adj = self._adj_subset(hw_adj, max_qubits) 376 | packed_embedding = self._find_embedding(edges, packed_adj, max_qubits, **embed_args) 377 | return packed_embedding 378 | 379 | def find_problem_embedding(self, logical, topology_file, max_qubits, physical_nums, verbosity): 380 | """Find an embedding of a problem on a physical topology, if 381 | necessary. Return a physical Sampler object.""" 382 | # Create a physical Problem. 383 | physical = copy.deepcopy(logical) 384 | physical.embedding = {} 385 | 386 | # Acquire the hardware topology unless we were given a specific 387 | # topology to use. 388 | if topology_file == None: 389 | hw_adj = self.get_hardware_adjacency() 390 | else: 391 | hw_adj = self.read_hardware_adjacency(topology_file, verbosity) 392 | physical.hw_adj = hw_adj 393 | 394 | # Identify any forced variable-to-qubit mappings. 395 | forced_mappings = {} 396 | if physical_nums: 397 | forced_mappings = physical.numeric_variables() 398 | 399 | # See if we already have an embedding in the embedding cache. 400 | edges = logical.bqm.quadratic 401 | if hw_adj == None or len(edges) == 0: 402 | # Either the sampler does not require embedding, or we have no work 403 | # to do. 404 | return physical 405 | if verbosity >= 2: 406 | sys.stderr.write("Minor-embedding the logical problem onto the physical topology:\n\n") 407 | ec = EmbeddingCache(self.qmasm, edges, hw_adj, max_qubits, forced_mappings) 408 | if verbosity >= 2: 409 | if ec.cachedir == None: 410 | sys.stderr.write(" No embedding cache directory was specified ($QMASMCACHE).\n") 411 | else: 412 | sys.stderr.write(" Using %s as the embedding cache directory.\n" % ec.cachedir) 413 | embedding = ec.read() 414 | if embedding == {}: 415 | # Cache hit, but embedding had failed 416 | if verbosity >= 2: 417 | sys.stderr.write(" Found failed embedding %s in the embedding cache.\n\n" % ec.hash) 418 | elif embedding != None: 419 | # Successful cache hit! 420 | if verbosity >= 2: 421 | sys.stderr.write(" Found successful embedding %s in the embedding cache.\n\n" % ec.hash) 422 | physical.embedding = embedding 423 | return physical 424 | if verbosity >= 2 and ec.cachedir != None: 425 | sys.stderr.write(" No existing embedding found in the embedding cache.\n") 426 | 427 | # Minor-embed the logical problem onto the hardware topology. 428 | embed_args = {"tries": 100, 429 | "max_no_improvement": 25, 430 | "fixed_chains": forced_mappings, 431 | "verbose": max(verbosity - 1, 0)} 432 | if max_qubits == None: 433 | if verbosity >= 2: 434 | sys.stderr.write(" Running the embedder.\n\n") 435 | physical.embedding = self._find_embedding(edges, hw_adj, max_qubits, **embed_args) 436 | else: 437 | if verbosity >= 2: 438 | sys.stderr.write(" Running the embedder, limiting it to %d qubits.\n\n" % max_qubits) 439 | physical.embedding = self._impose_qubit_packing(max_qubits, edges, hw_adj, **embed_args) 440 | if verbosity >= 2: 441 | sys.stderr.write("\n") 442 | if physical.embedding == {}: 443 | self.qmasm.abend("Failed to find an embedding") 444 | 445 | # Cache the embedding for next time. 446 | ec.write(physical.embedding) 447 | if verbosity >= 2 and ec.cachedir != None: 448 | sys.stderr.write(" Caching the embedding as %s.\n\n" % ec.hash) 449 | return physical 450 | 451 | def embed_problem(self, logical, topology_file, max_qubits, physical_nums, verbosity): 452 | "Embed a problem on a physical topology, if necessary." 453 | # Embed the problem. We first filter out isolated variables that don't 454 | # appear in the embedding graph to prevent embed_bqm from complaining. 455 | physical = self.find_problem_embedding(logical, topology_file, max_qubits, physical_nums, verbosity) 456 | if physical.embedding == {}: 457 | # No embedding is necessary. 458 | physical.logical = logical 459 | return physical 460 | physical.bqm.remove_variables_from([q 461 | for q in physical.bqm.linear 462 | if q not in physical.embedding and physical.bqm.linear[q] == 0.0]) 463 | for q, wt in physical.bqm.linear.items(): 464 | if q not in physical.embedding and wt != 0.0: 465 | self.qmasm.abend("Logical qubit %d has a nonzero weight (%.5g) but was not embedded" % (q, wt)) 466 | try: 467 | physical.bqm = embed_bqm(physical.bqm, physical.embedding, 468 | physical.hw_adj, -physical.qmasm.chain_strength) 469 | except Exception as e: 470 | self.qmasm.abend("Failed to embed the problem: %s" % e) 471 | 472 | # Update weights and strengths. Maintain a reference to the 473 | # logical problem. 474 | physical.logical = logical 475 | physical.weights = physical.bqm.linear 476 | physical.strengths = physical.bqm.quadratic 477 | 478 | # Some problem parameters are not relevant in the physical problem. 479 | physical.chains = None 480 | physical.antichains = None 481 | physical.pinned = None 482 | physical.known_values = None 483 | return physical 484 | 485 | def _get_default_annealing_time(self): 486 | "Determine a suitable annealing time to use if none was specified." 487 | try: 488 | # Use the default value. 489 | anneal_time = self.sampler.properties["default_annealing_time"] 490 | except KeyError: 491 | try: 492 | # If the default value is undefined, use the minimum allowed 493 | # value. 494 | anneal_time = self.sampler.properties["annealing_time_range"][0] 495 | except KeyError: 496 | # If all else fails, use 20 as a reasonable default. 497 | anneal_time = 20 498 | return anneal_time 499 | 500 | def _compute_sample_counts(self, samples, anneal_time): 501 | "Return a list of sample counts to request of the hardware." 502 | # The formula in the D-Wave System Documentation (under 503 | # "problem_run_duration_range") is Duration = (annealing_time + 504 | # readout_thermalization)*num_reads + programming_thermalization. We 505 | # split the number of samples so that each run time is less than the 506 | # maximum duration. 507 | props = self.sampler.properties 508 | try: 509 | max_run_duration = props["problem_run_duration_range"][1] 510 | prog_therm = props["default_programming_thermalization"] 511 | read_therm = props["default_readout_thermalization"] 512 | except KeyError: 513 | # Assume we're on a software solver. 514 | return [samples] 515 | max_samples = (max_run_duration - prog_therm)//(anneal_time + read_therm) 516 | # Independent of the maximum sample count just computed, the number of 517 | # samples can't exceed the maximum the hardware allows. 518 | try: 519 | max_samples = min(max_samples, props["num_reads_range"][1]) 520 | except KeyError: 521 | pass 522 | 523 | # Split the number of samples into pieces of size max_samples. 524 | if samples <= max_samples: 525 | samples_list = [samples] 526 | else: 527 | samples_list = [] 528 | s = samples 529 | while s > 0: 530 | if s >= max_samples: 531 | samples_list.append(max_samples) 532 | s -= max_samples 533 | else: 534 | samples_list.append(s) 535 | s = 0 536 | return samples_list 537 | 538 | def _compute_spin_rev_counts(self, spin_revs, samples_list): 539 | "Divide a total number of spin reversals across a set of samples." 540 | samples = sum(samples_list) 541 | spin_rev_frac = float(spin_revs)/float(samples) # We'll evenly divide our spin reversals among samples. 542 | spin_rev_list = [int(samps*spin_rev_frac) for samps in samples_list] 543 | missing_srs = spin_revs - sum(spin_rev_list) 544 | while missing_srs > 0: 545 | # Account for rounding errors by adding back in some missing spin 546 | # reversals. 547 | for i in range(samples): 548 | if samples_list[i] > spin_rev_list[i]: 549 | spin_rev_list[i] += 1 550 | missing_srs -= 1 551 | if missing_srs == 0: 552 | break 553 | return spin_rev_list 554 | 555 | def _merge_results(self, results): 556 | "Merge results into a single SampleSet." 557 | sum_keys = ["total_real_time", 558 | "qpu_access_overhead_time", 559 | "post_processing_overhead_time", 560 | "qpu_sampling_time", 561 | "total_post_processing_time", 562 | "qpu_programming_time", 563 | "run_time_chip", 564 | "qpu_access_time"] 565 | avg_keys = ["anneal_time_per_run", 566 | "readout_time_per_run", 567 | "qpu_delay_time_per_sample", 568 | "qpu_anneal_time_per_sample", 569 | "qpu_readout_time_per_sample"] 570 | timing = {} 571 | merged = dimod.concatenate(results) 572 | nsamples = len(merged) 573 | for sk in sum_keys: 574 | try: 575 | timing[sk] = sum([r.info["timing"][sk] for r in results]) 576 | except KeyError: 577 | pass 578 | for ak in avg_keys: 579 | try: 580 | timing[ak] = sum([r.info["timing"][ak]*len(r) for r in results])//nsamples 581 | except KeyError: 582 | pass 583 | merged.info["timing"] = timing 584 | return merged 585 | 586 | def _submit_and_block(self, sampler, bqm, store_params, **params): 587 | "Submit a job and wait for it to complete" 588 | # This method is a workaround for a bug in dwave-system. See 589 | # https://github.com/dwavesystems/dwave-system/issues/297#issuecomment-632384524 590 | sub_params = copy.copy(params) 591 | result = None 592 | if hasattr(sampler, "sample"): 593 | take_samples = sampler.sample 594 | elif hasattr(sampler, "sample_bqm"): 595 | take_samples = sampler.sample_bqm 596 | else: 597 | raise Exception("Sampler doesn't support either sample or sample_bqm") 598 | while result == None: 599 | # Not all solvers can be queried for the parameters they accept. 600 | # Hence, we resort to the grotesque hack of parsing the error 601 | # string to determine what is and isn't acceptable. 602 | try: 603 | result = take_samples(bqm, **sub_params) 604 | if store_params: 605 | result.resolve() 606 | except SolverFailureError as err: 607 | match = self.unk_param_re.search(str(err)) 608 | if match == None: 609 | raise err 610 | p = match[1] 611 | with self.rejected_params_lock: 612 | self.rejected_params.append(p) 613 | del sub_params[p] 614 | result = None 615 | except TypeError as err: 616 | match = self.unexp_arg_re.search(str(err)) 617 | if match == None: 618 | raise err 619 | p = match[1] 620 | with self.rejected_params_lock: 621 | self.rejected_params.append(p) 622 | del sub_params[p] 623 | result = None 624 | except KeyError as err: 625 | match = self.not_param_re.search(str(err)) 626 | if match == None: 627 | raise err 628 | p = match[1] 629 | with self.rejected_params_lock: 630 | self.rejected_params.append(p) 631 | del sub_params[p] 632 | result = None 633 | except: 634 | # An unexpected error happened. Throw an exception, which will 635 | # be caught by complete_sample_acquisition and abort the 636 | # program. 637 | self.final_params_sem.release() 638 | raise 639 | if store_params: 640 | self.final_params = sub_params 641 | self.final_params_sem.release() 642 | return result 643 | 644 | def _wrap_virtual_graph(self, sampler, bqm): 645 | "Return a VirtualGraphComposite and an associated BQM." 646 | # Explicitly scale the BQM to within the allowed h and J ranges. 647 | bqm = bqm.copy() 648 | props = sampler.properties 649 | h_range = props["h_range"] 650 | J_range = props["j_range"] 651 | scale = min([h_range[0]/min(bqm.linear.values()), 652 | h_range[1]/max(bqm.linear.values()), 653 | J_range[0]/min(bqm.quadratic.values()), 654 | J_range[1]/max(bqm.quadratic.values())]) 655 | bqm.scale(scale) 656 | 657 | # Define an identity mapping because the problem is already embedded. 658 | id_embed = {q: [q] for q in bqm.linear.keys()} 659 | id_embed.update({q[0]: [q[0]] for q in bqm.quadratic.keys()}) 660 | id_embed.update({q[1]: [q[1]] for q in bqm.quadratic.keys()}) 661 | 662 | # Return a VirtualGraph and the new BQM. 663 | return VirtualGraphComposite(sampler=self.sampler, embedding=id_embed), bqm 664 | 665 | def complete_sample_acquisition(self, verbosity, futures, results, overall_start_time, physical): 666 | "Wait for all results to complete then return a Solutions object." 667 | nqmis = len(results) 668 | if verbosity == 1: 669 | if nqmis == 1: 670 | sys.stderr.write("Waiting for the problem to complete.\n\n") 671 | else: 672 | sys.stderr.write("Waiting for all %d subproblems to complete.\n\n" % nqmis) 673 | elif verbosity >= 2: 674 | # Output our final parameters and their values. 675 | sys.stderr.write("Parameters accepted by %s (first subproblem):\n\n" % self.client_info["solver_name"]) 676 | self.final_params_sem.acquire() 677 | if len(self.final_params) == 0: 678 | sys.stderr.write(" [none]\n") 679 | else: 680 | max_param_name_len = max([len(k) for k in self.final_params]) 681 | max_param_value_len = max([len(repr(v)) for v in self.final_params.values()]) 682 | sys.stderr.write(" %-*.*s Value\n" % 683 | (max_param_name_len, max_param_name_len, "Parameter")) 684 | sys.stderr.write(" %s %s\n" % ("-"*max_param_name_len, "-"*max_param_value_len)) 685 | for k, v in sorted(self.final_params.items()): 686 | sys.stderr.write(" %-*.*s %s\n" % 687 | (max_param_name_len, max_param_name_len, k, repr(v))) 688 | sys.stderr.write("\n") 689 | 690 | # Keep track of the number of subproblems completed. 691 | sys.stderr.write("Number of subproblems completed:\n\n") 692 | cdigits = len(str(nqmis)) # Digits in the number of completed QMIs 693 | tdigits = len(str(nqmis*5)) # Estimate 5 seconds per QMI submission 694 | start_time = time.time_ns() 695 | ncomplete = 0 696 | prev_ncomplete = 0 697 | while ncomplete < nqmis: 698 | ncomplete = sum([int(r.done()) for r in results]) 699 | for f in futures: 700 | # Check for a failure in the job-submission thread. 701 | ex = f.exception() 702 | if ex != None: 703 | self.qmasm.abend("Job submission failed: %s" % str(ex)) 704 | if verbosity >= 2 and ncomplete > prev_ncomplete: 705 | end_time = time.time_ns() 706 | sys.stderr.write(" %*d of %d (%3.0f%%) after %*.0f second(s)\n" % 707 | (cdigits, ncomplete, nqmis, 708 | 100.0*float(ncomplete)/float(nqmis), 709 | tdigits, (end_time - start_time)/1e9)) 710 | prev_ncomplete = ncomplete 711 | if ncomplete < nqmis: 712 | time.sleep(1) 713 | overall_end_time = time.time_ns() 714 | if verbosity >= 2: 715 | sys.stderr.write("\n") 716 | sys.stderr.write(" Average time per subproblem: %.2g second(s)\n\n" % ((overall_end_time - overall_start_time)/(nqmis*1e9))) 717 | if "problem_id" in results[0].info: 718 | sys.stderr.write("IDs of completed subproblems:\n\n") 719 | for i in range(nqmis): 720 | sys.stderr.write(" %s\n" % results[i].info["problem_id"]) 721 | sys.stderr.write("\n") 722 | 723 | # Merge the result of seperate runs into a composite answer. 724 | try: 725 | answer = self._merge_results(results) 726 | except SolverFailureError as err: 727 | self.qmasm.abend("Solver error: %s" % err) 728 | answer.info["timing"]["round_trip_time"] = (overall_end_time - overall_start_time)//1000 729 | 730 | # Reset standard output. 731 | if getattr(self, "qbsolv_sampler", None) != None: 732 | os.dup2(stdout_fileno, sys.stdout.fileno()) 733 | 734 | # Return a Solutions object for further processing. 735 | return Solutions(results, answer, physical, verbosity >= 2) 736 | 737 | def acquire_samples(self, verbosity, composites, physical, anneal_sched, samples, anneal_time, spin_revs, postproc): 738 | "Acquire a number of samples from either a hardware or software sampler." 739 | # Wrap composites around our sampler if requested. 740 | sampler, bqm = self.sampler, physical.bqm 741 | for c in composites: 742 | try: 743 | if c == "VirtualGraph": 744 | sampler, bqm = self._wrap_virtual_graph(sampler, bqm) 745 | else: 746 | self.qmasm.abend('Internal error: unrecognized composite "%s"' % c) 747 | except SystemExit as err: 748 | raise err 749 | except: 750 | self.qmasm.abend("Failed to wrap a %s composite around the underlying sampler" % c) 751 | 752 | # Map abbreviated to full names for postprocessing types. 753 | postproc = {"none": "", "opt": "optimization", "sample": "sampling"}[postproc] 754 | 755 | # Determine the annealing time to use. 756 | if anneal_time == None: 757 | anneal_time = self._get_default_annealing_time() 758 | 759 | # Compute a list of the number of samples to take each iteration 760 | # and the number of spin reversals to perform. 761 | samples_list = self._compute_sample_counts(samples, anneal_time) 762 | spin_rev_list = self._compute_spin_rev_counts(spin_revs, samples_list) 763 | nqmis = len(samples_list) # Number of (non-unique) QMIs to submit 764 | 765 | # QBSolv writes to standard output, but we want it to write to standard 766 | # error instead. Note that the following is imperfect because of C 767 | # buffering. Setting PYTHONUNBUFFERED=1 in the environment seems to 768 | # help, though. 769 | if getattr(self, "qbsolv_sampler", None) != None: 770 | stdout_fileno = os.dup(sys.stdout.fileno()) 771 | os.dup2(sys.stderr.fileno(), sys.stdout.fileno()) 772 | 773 | # Submit all of our QMIs asynchronously. 774 | results = [None for i in range(nqmis)] 775 | futures = [None for i in range(nqmis)] 776 | executor = ThreadPoolExecutor() 777 | overall_start_time = time.time_ns() 778 | for i in range(nqmis): 779 | # Construct a set of solver parameters by combining typical 780 | # parameters (e.g., num_reads) with solver-specific parameters 781 | # (e.g., qpu_sampler), handling mutually exclusive parameters 782 | # (e.g., annealing_time and anneal_schedule), and subtracting off 783 | # any parameters previously rejected by the solver. 784 | solver_params = dict(chains=list(physical.embedding.values()), 785 | num_reads=samples_list[i], 786 | num_spin_reversal_transforms=spin_rev_list[i], 787 | postprocess=postproc) 788 | solver_params.update(self.extra_solver_params) 789 | if anneal_sched == None: 790 | solver_params["annealing_time"] = anneal_time 791 | else: 792 | solver_params["anneal_schedule"] = anneal_sched 793 | try: 794 | # Some, but not all, solvers report the parameters they accept. 795 | accepted_params = set(sampler.solver.properties["parameters"]) 796 | solver_params = {k: v for k, v in solver_params.items() if k in accepted_params} 797 | except AttributeError: 798 | pass 799 | with self.rejected_params_lock: 800 | for p in self.rejected_params: 801 | del solver_params[p] 802 | 803 | # Submit the QMI to the solver in a background thread. 804 | futures[i] = executor.submit(self._submit_and_block, sampler, bqm, i == 0, **solver_params) 805 | results[i] = SampleSet.from_future(futures[i]) 806 | 807 | # Wait for the QMIs to finish then return the results. 808 | executor.shutdown(wait=False) 809 | return self.complete_sample_acquisition(verbosity, futures, results, overall_start_time, physical) 810 | -------------------------------------------------------------------------------- /src/qmasm/utils.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # QMASM utility functions # 3 | # By Scott Pakin # 4 | ################################### 5 | 6 | from collections import defaultdict 7 | import copy 8 | import heapq 9 | import math 10 | import qmasm 11 | import sys 12 | 13 | class RemainingNextException(Exception): 14 | 'This exception is thrown if a "!next." directive can\'t be replaced.' 15 | pass 16 | 17 | class Utilities(object): 18 | "Provide various utility functions as mixins for QMASM." 19 | 20 | def abend(self, str): 21 | "Abort the program on an error." 22 | sys.stderr.write("%s: %s\n" % (sys.argv[0], str)) 23 | sys.exit(1) 24 | 25 | def warn(self, str): 26 | "Issue a warning message but continue execution." 27 | sys.stderr.write("%s: Warning: %s\n" % (sys.argv[0], str)) 28 | 29 | def symbol_to_number(self, sym, prefix=None, next_prefix=None): 30 | "Map from a symbol to a number, creating a new association if necessary." 31 | # Replace "!next." by substituting prefixes in the name. 32 | if "!next." in sym: 33 | if prefix == None or next_prefix == None: 34 | raise RemainingNextException 35 | sym = sym.replace(prefix + "!next.", next_prefix) 36 | 37 | # Return the symbol's logical qubit number or allocate a new one. 38 | try: 39 | return self.sym_map.to_number(sym) 40 | except KeyError: 41 | return self.sym_map.new_symbol(sym) 42 | 43 | def apply_prefix(self, sym, prefix=None, next_prefix=None): 44 | "Apply a prefix to a symbol name, replacing !next. with the next prefix." 45 | if prefix != None: 46 | sym = prefix + sym 47 | if "!next." in sym: 48 | if prefix == None or next_prefix == None: 49 | raise RemainingNextException 50 | sym = sym.replace(prefix + "!next.", next_prefix) 51 | return sym 52 | 53 | def canonicalize_strengths(self, strs): 54 | "Combine edges (A, B) and (B, A) into (A, B) with A < B." 55 | new_strs = defaultdict(lambda: 0.0) 56 | for (q1, q2), wt in strs.items(): 57 | if q1 == q2: 58 | continue # Discard vertex weights. 59 | if wt == 0.0: 60 | continue # Discard zero weights. 61 | if q1 > q2: 62 | q1, q2 = q2, q1 # Canonicalize vertex order. 63 | new_strs[(q1, q2)] += wt 64 | return new_strs 65 | 66 | class SymbolMapping: 67 | "Map between symbols and numbers." 68 | 69 | def __init__(self): 70 | self.sym2num = {} 71 | self.num2syms = {} 72 | self.next_sym_num = 0 73 | 74 | def new_symbol(self, sym): 75 | "Assign the next available number to a symbol." 76 | if sym in self.sym2num: 77 | raise Exception("Internal error: Symbol %s is already defined" % sym) 78 | self.sym2num[sym] = self.next_sym_num 79 | self.num2syms[self.next_sym_num] = set([sym]) 80 | self.next_sym_num += 1 81 | return self.sym2num[sym] 82 | 83 | def to_number(self, sym): 84 | "Map a symbol to a single number." 85 | return self.sym2num[sym] 86 | 87 | def to_symbols(self, num): 88 | "Map a number to a set of one or more symbols." 89 | return self.num2syms[num] 90 | 91 | def max_number(self): 92 | "Return the maximum symbol number used so far." 93 | return self.next_sym_num - 1 94 | 95 | def all_numbers(self): 96 | "Return an unordered list of all numbers used." 97 | return self.num2syms.keys() 98 | 99 | def all_symbols(self): 100 | "Return an unordered list of all symbols used." 101 | return self.sym2num.keys() 102 | 103 | def symbol_number_items(self): 104 | "Return a list of {symbol, number} pairs." 105 | return self.sym2num.items() 106 | 107 | def alias(self, sym1, sym2): 108 | "Make two symbols point to the same number." 109 | # Ensure that both symbols are defined. 110 | n_new_defs = 0 # Number of new definitions made 111 | try: 112 | num1 = self.sym2num[sym1] 113 | except KeyError: 114 | num1 = self.new_symbol(sym1) 115 | n_new_defs += 1 116 | try: 117 | num2 = self.sym2num[sym2] 118 | except KeyError: 119 | num2 = self.new_symbol(sym2) 120 | n_new_defs += 1 121 | 122 | # Do nothing if the symbols are already aliased. 123 | if num1 == num2: 124 | return num1 125 | 126 | # Abort if both symbols were previously defined (and do not alias each 127 | # other). In this case, we'd need to merge the associated weights and 128 | # strengths, and we don't currently do that. 129 | if n_new_defs == 0: 130 | abend("Unable to alias pre-existing variables %s and %s" % (sym1, sym2)) 131 | 132 | # Replace all occurrences of the larger number with the smaller in 133 | # num2syms. 134 | new_num = min(num1, num2) 135 | old_num = max(num1, num2) 136 | self.num2syms[new_num].update(self.num2syms[old_num]) 137 | del self.num2syms[old_num] 138 | 139 | # Regenerate sym2num from num2syms. 140 | self.sym2num = {} 141 | for n, ss in self.num2syms.items(): 142 | for s in ss: 143 | self.sym2num[s] = n 144 | return new_num 145 | 146 | def overwrite_with(self, sym2num): 147 | "Overwrite the map's contents with a given symbol-to-number map." 148 | self.sym2num = sym2num 149 | self.num2syms = {} 150 | for s, n in self.sym2num.items(): 151 | try: 152 | self.num2syms[n].add(s) 153 | except KeyError: 154 | self.num2syms[n] = set([s]) 155 | if len(self.num2syms.keys()) == 0: 156 | self.next_sym_num = 0 157 | else: 158 | self.next_sym_num = max(self.num2syms.keys()) + 1 159 | 160 | def replace_all(self, before_syms, after_syms): 161 | "Replace each symbol in before_syms with its peer in after_syms." 162 | sym2num = copy.deepcopy(self.sym2num) 163 | before_nums = [] 164 | for s in before_syms: 165 | try: 166 | before_nums.append(sym2num[s]) 167 | except KeyError: 168 | abend('Failed to rename nonexistent symbol "%s"' % s) 169 | del sym2num[s] 170 | for s, n in zip(after_syms, before_nums): 171 | sym2num[s] = n 172 | self.overwrite_with(sym2num) 173 | 174 | class SpecializedPriorityQueue(object): 175 | """Implement a specialized priority queue that supports priority 176 | replacement but not insertion. Elements are expected to be unique.""" 177 | 178 | def __init__(self, items): 179 | self.queue = [(0, i, items[i]) for i in range(len(items))] # {priority, original index, value} 180 | heapq.heapify(self.queue) 181 | self.val2pri = {k: 0 for k in items} 182 | self.val2dist = {items[i]: i for i in range(len(items))} 183 | 184 | def pop_max(self): 185 | "Pop the highest-priority item." 186 | valid = False 187 | while not valid: 188 | pri, dist, m = heapq.heappop(self.queue) 189 | valid = self.val2pri[m] == pri 190 | return m 191 | 192 | def increase_priority(self, m): 193 | "Increase the priority of a given item." 194 | new_pri = self.val2pri[m] - 1 # heapq return the smallest value in the heap. 195 | dist = self.val2dist[m] 196 | heapq.heappush(self.queue, (new_pri, dist, m)) 197 | self.val2pri[m] = new_pri 198 | --------------------------------------------------------------------------------