├── .gitignore ├── .travis.yml ├── README.md ├── assets ├── XOR.pdf ├── circ1.gif ├── circ2.gif ├── csat.png ├── full_adder.png ├── full_adder_figure.pdf ├── full_adder_figure.png ├── full_adder_spins.png ├── oneplusone.gif ├── oneplusone.mp4 ├── planar_nand.png ├── ripple_carry_adder.pdf └── ripple_carry_adder2.pdf ├── examples.ipynb ├── ising_compiler ├── __init__.py ├── alu_nx.py ├── gates.py ├── gates_nx.py ├── ising_numpy.py ├── ising_nx.py ├── ising_pytorch.py └── utils.py ├── paper.pdf ├── setup.py ├── tests_np.py └── tests_nx.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Mathematica notebooks 2 | *.nb 3 | 4 | # MacOS attributes files 5 | .DS_Store 6 | 7 | # Visual studio / PyCharm stuff 8 | .idea 9 | .vs 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | .dmypy.json 117 | dmypy.json 118 | 119 | /logs/ 120 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | # We don't actually use the Travis Python, but this keeps it organized. 4 | - "3.6" 5 | install: 6 | - sudo apt-get update 7 | # We do this conditionally because it saves us some downloading if the 8 | # version is the same. 9 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 10 | - bash miniconda.sh -b -p $HOME/miniconda 11 | - export PATH="$HOME/miniconda/bin:$PATH" 12 | - hash -r 13 | - conda config --set always_yes yes --set changeps1 no 14 | - conda update -q conda 15 | # Useful for debugging any issues with conda 16 | - conda info -a 17 | # Replace dep1 dep2 ... with your dependencies 18 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION 19 | - source activate test-environment 20 | - python setup.py install 21 | 22 | script: 23 | - python -m unittest discover -v -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ising-compiler [![Build Status](https://travis-ci.com/fancompute/ising-compiler.svg?branch=master)](https://travis-ci.com/fancompute/ising-compiler) 2 | 3 | 🍰 Compiling your code to an Ising Hamiltonian so you don't have to! 4 | 5 | ![Computing 1+1=2 in a fantastically roundabout manner](https://raw.githubusercontent.com/fancompute/ising-compiler/master/assets/oneplusone.gif) 6 | 7 | ## About 8 | 9 | This library was a final project for Stanford's graduate statistical mechanics class. The Ising model, despite its simplicity, has a rich array of properties that allow for universal computation. Each elementary Boolean logic gate can be implemented as an Ising system of 2-4 spins. This library allows you to compile a sequence of Boolean logic gates into a spin system where the result of the computation is encoded in the ground state of the Hamiltonian. I provide several demonstrations of compiling complex circuits into Ising spin systems and use Monte Carlo simulations to show that the compiled circuits encode the desired computations. 10 | 11 | See the [paper](https://github.com/fancompute/ising-compiler/blob/master/paper.pdf) for more details. 12 | 13 | ## Examples 14 | 15 | See this [example notebook](https://github.com/fancompute/ising-compiler/blob/master/examples.ipynb) for demonstrations of how this library can be used. -------------------------------------------------------------------------------- /assets/XOR.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/XOR.pdf -------------------------------------------------------------------------------- /assets/circ1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/circ1.gif -------------------------------------------------------------------------------- /assets/circ2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/circ2.gif -------------------------------------------------------------------------------- /assets/csat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/csat.png -------------------------------------------------------------------------------- /assets/full_adder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/full_adder.png -------------------------------------------------------------------------------- /assets/full_adder_figure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/full_adder_figure.pdf -------------------------------------------------------------------------------- /assets/full_adder_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/full_adder_figure.png -------------------------------------------------------------------------------- /assets/full_adder_spins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/full_adder_spins.png -------------------------------------------------------------------------------- /assets/oneplusone.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/oneplusone.gif -------------------------------------------------------------------------------- /assets/oneplusone.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/oneplusone.mp4 -------------------------------------------------------------------------------- /assets/planar_nand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/planar_nand.png -------------------------------------------------------------------------------- /assets/ripple_carry_adder.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/ripple_carry_adder.pdf -------------------------------------------------------------------------------- /assets/ripple_carry_adder2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/assets/ripple_carry_adder2.pdf -------------------------------------------------------------------------------- /ising_compiler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/ising_compiler/__init__.py -------------------------------------------------------------------------------- /ising_compiler/alu_nx.py: -------------------------------------------------------------------------------- 1 | from ising_compiler.gates_nx import IsingCircuit 2 | 3 | 4 | class IsingALU(IsingCircuit): 5 | 6 | def HALF_ADDER(self, A, B, S = None, C = None): 7 | S_bit = self.XOR(A, B, out = S) 8 | C_bit = self.AND(A, B, out = C) 9 | return S_bit, C_bit 10 | 11 | def FULL_ADDER(self, A, B, Cin, S = None, Cout = None): 12 | aXORb = self.XOR(A, B)#, out = "A^B") 13 | aANDb = self.AND(A, B)#, out = "A&B") 14 | S_bit = self.XOR(aXORb, Cin, out = S) 15 | aXORbANDc = self.AND(aXORb, Cin)#, out = "(A^B)&C") 16 | Cout_bit = self.OR(aANDb, aXORbANDc, out = Cout) 17 | return S_bit, Cout_bit 18 | 19 | def FULL_ADDER_NAND(self, A, B, Cin, S = None, Cout = None): 20 | u1 = self.NAND(A, B, out = "u1") 21 | u2 = self.NAND(A, u1, out = "u2") 22 | u3 = self.NAND(u1, B, out = "u3") 23 | u4 = self.NAND(u2, u3, out = "u4") 24 | u5 = self.NAND(u4, Cin, out = "u5") 25 | u6 = self.NAND(u4, u5, out = "u6") 26 | u7 = self.NAND(u5, Cin, out = "u7") 27 | S_bit = self.NAND(u6, u7, out = S) 28 | Cout_bit = self.NAND(u5, u1, out = Cout) 29 | return S_bit, Cout_bit 30 | 31 | def RIPPLE_CARRY_ADDER(self, num_bits): 32 | assert num_bits > 1 33 | # Set up a half adder for first bit 34 | A0 = self.INPUT("A0") 35 | B0 = self.INPUT("B0") 36 | S, C = self.HALF_ADDER(A0, B0, S = "S0", C = "C1") 37 | # Continue with full adders to desired size 38 | S_bits = [S] # list of all sum bits 39 | for i in range(1, num_bits): 40 | A = self.INPUT("A" + str(i)) 41 | B = self.INPUT("B" + str(i)) 42 | S, C = self.FULL_ADDER(A, B, C, S = "S" + str(i), Cout = "C" + str(i + 1)) 43 | S_bits.append(S) 44 | # Register sum bits and last carry as output 45 | for s in S_bits: 46 | self.OUTPUT(s) 47 | self.OUTPUT(C) 48 | # Return the sum bits 49 | return S_bits, C -------------------------------------------------------------------------------- /ising_compiler/gates.py: -------------------------------------------------------------------------------- 1 | from ising_compiler.ising_numpy import IsingModel 2 | from ising_compiler.utils import * 3 | 4 | 5 | class IsingLatticeGate: 6 | # footprint = () 7 | num_inputs = 0 8 | num_outputs = 0 9 | 10 | def __init__(self, circuit=None, spins = (), inputs = (), outputs = ()): 11 | self.circuit = circuit 12 | self.spins = spins 13 | self.inputs = inputs 14 | self.outputs = outputs 15 | self._apply() 16 | 17 | def _apply(self): 18 | raise NotImplementedError 19 | 20 | def evaluate(self, inputs): 21 | raise NotImplementedError 22 | 23 | 24 | class WIRE(IsingLatticeGate): 25 | 26 | # footprint = (2,1) 27 | num_inputs = 1 28 | num_outputs = 1 29 | wire_coupling = -1/2 30 | 31 | def _apply(self): 32 | i, o = self.inputs[0], self.outputs[0] 33 | self.circuit.set_coupling(i, o, value = self.wire_coupling) 34 | 35 | def evaluate(self, inputs): 36 | return inputs 37 | 38 | class NAND(IsingLatticeGate): 39 | 40 | num_inputs = 2 41 | num_outputs = 1 42 | 43 | def _apply(self): 44 | pass 45 | 46 | 47 | 48 | class IsingCircuit(IsingModel): 49 | def __init__(self, size = (100, 100), 50 | temperature = 0.5, 51 | initial_state = 'random'): 52 | super().__init__(size = size, 53 | temperature = temperature, 54 | initial_state = initial_state, 55 | periodic = False, 56 | all_to_all_couplings = False, 57 | coupling_strength = 0.0) 58 | 59 | def get_coupling_index(self, s1, s2): 60 | '''Compute the indices corresponding to the coupling between neighboring spins s1 and s2''' 61 | assert is_adjacent(s1, s2), f"Error trying to get coupling index between non-adjacent spins {s1} and {s2}" 62 | offset = np.array(s2) - np.array(s1) 63 | offset_dir = np.nonzero(offset) 64 | offset_amt = np.sum(offset) 65 | assert len(offset_dir) == 1 and offset_dir[0] < self.dimension and np.abs(offset_amt) == 1.0 66 | 67 | offset_dir = np.sum(offset_dir) 68 | 69 | if offset_amt == 1: # s2 is the "right" neighbor 70 | return (offset_dir,) + s1 71 | elif offset_amt == -1: # s2 is the "left" neighbor 72 | return (offset_dir,) + s2 73 | else: 74 | raise ValueError() 75 | 76 | def set_coupling(self, s1, s2, value = 0.0): 77 | '''Set the coupling value between neighboring spins s1 and s2. -1 = ferromagnetic, +1 = antiferromagnetic''' 78 | indices = self.get_coupling_index(s1, s2) 79 | self.couplings[indices] = value 80 | 81 | def set_field(self, s, value=0.0): 82 | '''Set the magnetic field applied to spin s''' 83 | self.fields[s] = value 84 | 85 | def set_spin(self, s, value=False): 86 | '''Sets a specified spin to be up or down by applying a strong magnetic field''' 87 | FIELD_STRENGTH = 10.0 88 | if type(value) is bool: 89 | self.set_field(s, value=(FIELD_STRENGTH if value else -FIELD_STRENGTH)) 90 | elif type(value) is int: 91 | if value == -1 or value == 0: 92 | self.set_field(s, value=-FIELD_STRENGTH) 93 | elif value == 1: 94 | self.set_field(s, value=FIELD_STRENGTH) 95 | else: 96 | raise ValueError() 97 | else: 98 | raise ValueError() 99 | 100 | # def WIRE(self, s1, s2): 101 | # '''Copy the spin at s1 to s2''' 102 | # assert is_adjacent(s1, s2) 103 | # self.set_coupling(s1, s2, -1 / 2) 104 | # 105 | # def OR(self, in1, in2, out): 106 | # '''OR gate, outputted to a third spin''' 107 | # pass 108 | # 109 | # def XOR(self, in1, in2, ancilla, out): 110 | # '''OR gate, outputted to a third spin''' 111 | # pass 112 | -------------------------------------------------------------------------------- /ising_compiler/gates_nx.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from json import dumps 3 | 4 | import networkx as nx 5 | from tqdm import tqdm 6 | 7 | from ising_compiler.ising_nx import IsingGraph 8 | from ising_compiler.utils import * 9 | 10 | 11 | class IsingCircuit(IsingGraph): 12 | wire_coupling = -1 13 | input_field_strength = 5 14 | 15 | ''' 16 | Class representing a graph which is sequentially built to emulate a Boolean circuit. Includes methods for adding 17 | various gates. By convention, all nodes which serve as inputs to the circuit or subcomponents are copied with wires, 18 | and all outputs are not. (e.g. NAND(A,B,C) will take inputs A' and B' and return spin C) 19 | ''' 20 | 21 | def __init__(self, temperature = 0.5, initializer = 'random', copy_mode = "auto"): 22 | super().__init__(nx.Graph(), temperature = temperature, initializer = initializer) 23 | self.inputs = [] 24 | self.outputs = [] 25 | self.copy_mode = copy_mode # "auto": copies are |V|, "primed": A -> A', "numeric": A -> A1 -> A2 26 | 27 | def get_spin(self, node, mode = 'spin'): 28 | if mode == 'spin': 29 | return self.graph.nodes[node]['spin'] 30 | elif mode == 'bool': 31 | return True if self.graph.nodes[node]['spin'] == 1 else False 32 | elif mode == 'binary': 33 | return 1 if self.graph.nodes[node]['spin'] == 1 else 0 34 | else: 35 | raise ValueError() 36 | 37 | def add_spin(self, node = None, field_strength = 0.0): 38 | if node is None: 39 | node = str(len(self.graph.nodes)) 40 | assert not self.graph.has_node(node), f"Node {node} already in graph {self.graph}" 41 | self.graph.add_node(node) 42 | # Initialize spin of new node 43 | if self.initializer == 'random': 44 | self.graph.nodes[node]['spin'] = 1 if np.random.rand() > .5 else -1 45 | elif self.initializer == 'up': 46 | self.graph.nodes[node]['spin'] = 1 47 | elif self.initializer == 'down': 48 | self.graph.nodes[node]['spin'] = -1 49 | self.graph.nodes[node]['field'] = field_strength 50 | return node 51 | 52 | def copy_inputs(self, *nodes): 53 | input_copies = [] 54 | for node in nodes: 55 | assert self.graph.has_node(node), f"Input node {node} missing in graph {self.graph}" 56 | if self.copy_mode == "auto": 57 | node_copy = self.add_spin() 58 | elif self.copy_mode == "numeric": 59 | i = 1 60 | while self.graph.has_node(node + str(i)): i += 1 61 | node_copy = self.add_spin(node + str(i)) 62 | elif self.copy_mode == "primed": 63 | i = 1 64 | while self.graph.has_node(node + "'" * i): i += 1 65 | node_copy = self.add_spin(node + "'" * i) 66 | else: 67 | raise ValueError(f"Invalid copy mode: {self.copy_mode}") 68 | input_copies.append(node_copy) 69 | self.COPY(node, node_copy) 70 | return input_copies 71 | 72 | def add_spins_not_present(self, *nodes): 73 | for node in nodes: 74 | if not self.graph.has_node(node): 75 | self.add_spin(node) 76 | return nodes 77 | 78 | def set_coupling(self, spin1, spin2, coupling_strength): 79 | if not self.graph.has_edge(spin1, spin2): 80 | self.graph.add_edge(spin1, spin2) 81 | self.graph.edges[(spin1, spin2)]['coupling'] = coupling_strength 82 | 83 | def set_field(self, spin, field_strength): 84 | self.graph.nodes[spin]['field'] = float(field_strength) 85 | 86 | def set_input_fields(self, input_dict, mode = 'spin'): 87 | for input_spin, input_value in input_dict.items(): 88 | if mode == 'bool': 89 | assert type(input_value) is bool 90 | self.set_field(input_spin, (-self.input_field_strength if input_value else self.input_field_strength)) 91 | elif mode == 'spin': 92 | if input_value == -1: 93 | self.set_field(input_spin, self.input_field_strength) 94 | elif input_value == 1: 95 | self.set_field(input_spin, -self.input_field_strength) 96 | else: 97 | raise ValueError() 98 | elif mode == 'binary': 99 | if input_value == 0: 100 | self.set_field(input_spin, self.input_field_strength) 101 | elif input_value == 1: 102 | self.set_field(input_spin, -self.input_field_strength) 103 | else: 104 | raise ValueError() 105 | else: 106 | raise ValueError() 107 | 108 | def INPUT(self, name, add_node = True): 109 | '''Designate a spin to be an input to the circuit. Returns a wired *copy* of the spin node''' 110 | if add_node: 111 | self.add_spin(name) 112 | else: 113 | assert self.graph.has_node(name), f"add_node is False and input node {name} missing in graph {self.graph}" 114 | self.inputs.append(name) 115 | return name 116 | 117 | def OUTPUT(self, name, add_node = False): 118 | if add_node: 119 | self.add_spin(name) 120 | else: 121 | assert self.graph.has_node(name), f"add_node is False and output node {name} missing in graph {self.graph}" 122 | self.outputs.append(name) 123 | return name 124 | 125 | def COPY(self, spin1, spin2): 126 | self.set_coupling(spin1, spin2, self.wire_coupling) 127 | 128 | def NOT(self, spin1, spin2): 129 | self.set_coupling(spin1, spin2, -self.wire_coupling) 130 | 131 | def AND(self, in1, in2, out = None): 132 | s1, s2 = self.copy_inputs(in1, in2) 133 | s3 = self.add_spin(out) 134 | self.set_field(s1, -1 / 2) 135 | self.set_field(s2, -1 / 2) 136 | self.set_field(s3, 1) 137 | self.set_coupling(s1, s2, 1 / 2) 138 | self.set_coupling(s1, s3, -1) 139 | self.set_coupling(s2, s3, -1) 140 | return s3 141 | 142 | def NAND(self, in1, in2, out = None): 143 | s1, s2 = self.copy_inputs(in1, in2) 144 | s3 = self.add_spin(out) 145 | self.set_field(s1, -1 / 2) 146 | self.set_field(s2, -1 / 2) 147 | self.set_field(s3, -1) 148 | self.set_coupling(s1, s2, 1 / 2) 149 | self.set_coupling(s1, s3, 1) 150 | self.set_coupling(s2, s3, 1) 151 | return s3 152 | 153 | def OR(self, in1, in2, out = None): 154 | s1, s2 = self.copy_inputs(in1, in2) 155 | s3 = self.add_spin(out) 156 | self.set_field(s1, 1 / 2) 157 | self.set_field(s2, 1 / 2) 158 | self.set_field(s3, -1) 159 | self.set_coupling(s1, s2, 1 / 2) 160 | self.set_coupling(s1, s3, -1) 161 | self.set_coupling(s2, s3, -1) 162 | return s3 163 | 164 | def NOR(self, in1, in2, out = None): 165 | s1, s2 = self.copy_inputs(in1, in2) 166 | s3 = self.add_spin(out) 167 | self.set_field(s1, 1 / 2) 168 | self.set_field(s2, 1 / 2) 169 | self.set_field(s3, 1) 170 | self.set_coupling(s1, s2, 1 / 2) 171 | self.set_coupling(s1, s3, 1) 172 | self.set_coupling(s2, s3, 1) 173 | return s3 174 | 175 | def XOR(self, in1, in2, out = None, anc = None): 176 | s1, s2 = self.copy_inputs(in1, in2) 177 | so = self.add_spin(out) 178 | sA = self.add_spin(anc) 179 | self.set_field(s1, 1 / 2) 180 | self.set_field(s2, 1 / 2) 181 | self.set_field(sA, 1) 182 | self.set_field(so, 1 / 2) 183 | self.set_coupling(s1, s2, 1 / 2) 184 | self.set_coupling(s1, sA, 1) 185 | self.set_coupling(s2, sA, 1) 186 | self.set_coupling(s1, so, 1 / 2) 187 | self.set_coupling(s2, so, 1 / 2) 188 | self.set_coupling(sA, so, 1) 189 | 190 | return so 191 | 192 | def XNOR(self, in1, in2, out = None, anc = None): 193 | s1, s2 = self.copy_inputs(in1, in2) 194 | so = self.add_spin(out) 195 | sA = self.add_spin(anc) 196 | self.set_field(s1, 1 / 2) 197 | self.set_field(s2, 1 / 2) 198 | self.set_field(sA, 1) 199 | self.set_field(so, -1 / 2) 200 | self.set_coupling(s1, s2, 1 / 2) 201 | self.set_coupling(s1, sA, 1) 202 | self.set_coupling(s2, sA, 1) 203 | self.set_coupling(s1, so, -1 / 2) 204 | self.set_coupling(s2, so, -1 / 2) 205 | self.set_coupling(sA, so, -1) 206 | 207 | return so 208 | 209 | def evaluate_input(self, input_dict, 210 | epochs = 10000, 211 | anneal_temperature_range = None, 212 | show_progress = False, 213 | mode = 'binary', 214 | video = False): 215 | '''Evaluates the circuit one time for a given input''' 216 | # re-initialize all spins 217 | IsingGraph.initialize_spins(self.graph) 218 | # set the input fields to the input of the circuit 219 | self.set_input_fields(input_dict, mode = mode) 220 | # run metropolis / annealing 221 | self.run_metropolis(epochs, anneal_temperature_range = anneal_temperature_range, show_progress = show_progress, 222 | video = video) 223 | # build a return dictionary of output spins 224 | output_dict = {} 225 | for output in self.outputs: 226 | output_dict[output] = self.get_spin(output, mode = mode) 227 | return output_dict 228 | 229 | def evaluate_expectations(self, input_dict, 230 | runs = 1000, 231 | epochs_per_run = 1000, 232 | anneal_temperature_range = None, 233 | show_progress = True): 234 | '''Evaluates the expectation of output spins over many runs''' 235 | output_dicts = [] 236 | iterator = tqdm(range(runs)) if show_progress else range(runs) 237 | for _ in iterator: 238 | output_dict = self.evaluate_input(input_dict, 239 | epochs = epochs_per_run, 240 | anneal_temperature_range = anneal_temperature_range, 241 | mode = 'binary', 242 | show_progress = False) 243 | output_dicts.append(output_dict) 244 | 245 | # Compute mean dictionary 246 | mean_dict = {} 247 | for key in output_dicts[0].keys(): 248 | mean_dict[key] = sum(d[key] for d in output_dicts) / len(output_dicts) 249 | return mean_dict 250 | 251 | def evaluate_expectation_evolution(self, input_dict, 252 | runs = 1000, 253 | epochs_per_run = 1000, 254 | epoch_step = 10, 255 | anneal_temperature_range = None, 256 | show_progress = True): 257 | '''Evaluates the evolution of the expectation of output spins over many runs''' 258 | spin_dicts_each_run = [] # will be a list of [run1[t1[], t2[], t3[], run2[...], ...] 259 | iterator_runs = tqdm(range(runs)) if show_progress else range(runs) 260 | for _ in iterator_runs: 261 | spin_dicts_each_time = [] 262 | IsingGraph.initialize_spins(self.graph) 263 | 264 | if anneal_temperature_range: 265 | temperatures = np.geomspace(anneal_temperature_range[0], anneal_temperature_range[1], epochs_per_run) 266 | else: 267 | temperatures = [self.temperature] * epochs_per_run 268 | 269 | self.set_input_fields(input_dict, mode = 'binary') 270 | for epoch_batch in range(0, epochs_per_run, epoch_step): 271 | for epoch in range(epoch_step): 272 | self.temperature = temperatures[epoch_batch + epoch] 273 | self.metropolis_step() 274 | # build a return dictionary of output spins 275 | spin_dict = {} 276 | for node in self.graph.nodes: 277 | spin_dict[node] = self.graph.nodes[node]['spin'] 278 | spin_dicts_each_time.append(spin_dict) 279 | spin_dicts_each_run.append(spin_dicts_each_time) 280 | 281 | # This is a little sloppy 282 | mean_dict_each_time = {} 283 | for key in spin_dicts_each_run[0][0].keys(): 284 | mean_dict_each_time[key] = [] # set each key value to an empty list to be populated for each time 285 | for t, _ in enumerate(spin_dicts_each_run[0]): 286 | for key in spin_dicts_each_run[0][0].keys(): 287 | avg_this_timestep = sum(run[t][key] for run in spin_dicts_each_run) / len(spin_dicts_each_run) 288 | mean_dict_each_time[key].append(avg_this_timestep) 289 | 290 | return mean_dict_each_time 291 | 292 | def evaluate_outcomes(self, input_dict, 293 | runs = 1000, 294 | epochs_per_run = 1000, 295 | anneal_temperature_range = None, 296 | show_progress = True): 297 | '''Evaluates an input many times and returns a Counter() of stringified dicts and frequenices''' 298 | output_dicts = [] 299 | iterator = tqdm(range(runs)) if show_progress else range(runs) 300 | for _ in iterator: 301 | output_dict = self.evaluate_input(input_dict, 302 | epochs = epochs_per_run, 303 | anneal_temperature_range = anneal_temperature_range, 304 | mode = 'binary', 305 | show_progress = False) 306 | output_dicts.append(output_dict) 307 | 308 | return Counter([dumps(d) for d in output_dicts]) 309 | 310 | def evaluate_circuit(self, 311 | runs = 1000, 312 | epochs_per_run = 1000, 313 | anneal_temperature_range = None, 314 | show_progress = True): 315 | '''Evaluates the expectation of output spins over many runs for every possible combination of inputs''' 316 | all_input_combos = [[int(x) for x in ('{:0' + str(len(self.inputs)) + 'b}').format(n)] 317 | for n in range(2 ** len(self.inputs))] 318 | all_output_dicts = {} 319 | 320 | iterator = tqdm(all_input_combos) if show_progress else all_input_combos 321 | for inputs in iterator: 322 | input_dict = {} 323 | for i, spin in enumerate(self.inputs): 324 | input_dict[spin] = inputs[i] 325 | all_output_dicts[str(input_dict)] = self.evaluate_expectations(input_dict, 326 | runs = runs, 327 | epochs_per_run = epochs_per_run, 328 | anneal_temperature_range = anneal_temperature_range, 329 | show_progress = False) 330 | 331 | return all_output_dicts 332 | -------------------------------------------------------------------------------- /ising_compiler/ising_numpy.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | 3 | import matplotlib.animation as anim 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from tqdm import tqdm 7 | 8 | 9 | class IsingModel: 10 | 11 | def __init__(self, size = (100, 100), 12 | temperature = 0.5, 13 | initial_state = 'random', 14 | periodic = False, 15 | all_to_all_couplings = False, 16 | coupling_strength = 1.0): 17 | self.shape = size 18 | self.dimension = len(size) 19 | # self.lin_size = size[0] 20 | self.temperature = temperature 21 | self.periodic = periodic 22 | 23 | if initial_state == 'random': 24 | self.spins = 2 * np.random.randint(0, 1 + 1, size = self.shape, dtype = np.int8) - 1 25 | elif initial_state == 'up': 26 | self.spins = np.ones(size = self.shape, dtype = np.int8) 27 | else: 28 | self.spins = np.ones(size = self.shape, dtype = np.int8) 29 | 30 | if all_to_all_couplings: 31 | # TODO: use torch.sparse for this 32 | raise NotImplementedError() 33 | 34 | else: 35 | edges_per_spin = self.dimension # in a square lattice 36 | s = (edges_per_spin,) + size 37 | # if self.periodic: 38 | # s = (edges_per_spin,) + size 39 | # else: 40 | # s = (edges_per_spin,) + tuple([d - 1 for d in self.size]) 41 | # Couplings are a [dimension, n_x, n_y ...] tuple 42 | self.couplings = -1 * coupling_strength * np.ones(s, dtype = np.float) # all ferromagnetic 43 | 44 | # Magnetic field at each point 45 | self.fields = np.zeros(self.shape, dtype = np.float) 46 | 47 | def get_random_index(self): 48 | """ 49 | Returns a random index in the spin lattice 50 | """ 51 | return [np.random.randint(0, dim) for dim in self.shape] 52 | 53 | def get_energy_at_site(self, position): 54 | """ 55 | Compute the energy at a given site in the lattice. 56 | """ 57 | 58 | e = 0.0 59 | 60 | pos = np.array(position) 61 | 62 | # Get the neighboring spins of pos 63 | for ax in range(self.dimension): 64 | # Make offset vector for axis like [0 0 0 ... 0 1 0 0] 65 | offset = np.zeros_like(pos) 66 | offset[ax] = 1 67 | # Add the "left" neighbor 68 | if (pos - offset)[ax] >= 0: 69 | e += self.spins[tuple(pos)] * self.spins[tuple(pos - offset)] * self.couplings[ax][tuple(pos - offset)] 70 | # Add the "right" neighbor 71 | if (pos + offset)[ax] < self.shape[ax]: 72 | e += self.spins[tuple(pos)] * self.spins[tuple(pos + offset)] * \ 73 | self.couplings[ax][tuple(pos)] # no plus offset here 74 | # TODO: handle periodic case 75 | 76 | # Add contribution from field 77 | e += self.spins[tuple(pos)] * self.fields[tuple(pos)] 78 | 79 | return e 80 | 81 | def get_energy_at_site_2d(self, i, j): 82 | """ 83 | Compute the energy at a given site in the lattice. Special optimized version for 2D case 84 | """ 85 | 86 | if 1 <= i < self.shape[0] - 1 and 1 <= j < self.shape[1] - 1: 87 | return self.spins[i, j] * ( 88 | self.spins[i + 1, j] * self.couplings[0, i, j] + 89 | self.spins[i - 1, j] * self.couplings[0, i - 1, j] + 90 | self.spins[i, j + 1] * self.couplings[1, i, j] + 91 | self.spins[i, j - 1] * self.couplings[1, i, j - 1] + 92 | self.fields[i, j] 93 | ) 94 | 95 | else: 96 | e = self.spins[i, j] * self.fields[i, j] 97 | if i < self.shape[0] - 1: 98 | e += self.spins[i, j] * self.spins[i + 1, j] * self.couplings[0, i, j] 99 | if i >= 1: 100 | e += self.spins[i, j] * self.spins[i - 1, j] * self.couplings[0, i - 1, j] 101 | if j < self.shape[1] - 1: 102 | e += self.spins[i, j] * self.spins[i, j + 1] * self.couplings[1, i, j] 103 | if j >= 1: 104 | e += self.spins[i, j] * self.spins[i, j - 1] * self.couplings[1, i, j - 1] 105 | return e 106 | 107 | def get_total_energy(self): 108 | """ 109 | Gets the total internal energy of the lattice system, not normalized by number of spins 110 | """ 111 | ranges = [range(x) for x in self.shape] 112 | all_indices = product(*ranges) 113 | 114 | return sum(self.get_energy_at_site(pos) for pos in all_indices) 115 | 116 | def metropolis_step(self): 117 | """ 118 | Runs one step of the Metropolis-Hastings algorithm 119 | :return: 120 | """ 121 | 122 | # Randomly select a site on the lattice 123 | # pos = self.get_random_index() 124 | i, j = [np.random.randint(0, dim) for dim in self.shape] 125 | 126 | # Calculate energy of the spin and energy if it is flipped 127 | # energy = self.get_energy_at_site(pos) 128 | energy = self.get_energy_at_site_2d(i, j) 129 | 130 | energy_flipped = -1 * energy 131 | 132 | # Flip the spin if it is energetically favorable. If not, flip based on Boltzmann factor 133 | if energy_flipped <= 0: 134 | self.spins[i, j] *= -1 135 | elif np.exp(-energy_flipped / self.temperature) > np.random.rand(): 136 | self.spins[i, j] *= -1 137 | 138 | def run(self, epochs, video = False, show_progress = False): 139 | 140 | iterator = tqdm(range(epochs)) if show_progress else range(epochs) 141 | 142 | if video: 143 | num_frames = 100 144 | FFMpegWriter = anim.writers['ffmpeg'] 145 | writer = FFMpegWriter(fps = 10) 146 | 147 | plt.ion() 148 | fig = plt.figure() 149 | 150 | with writer.saving(fig, "ising.mp4", 100): 151 | for epoch in iterator: 152 | self.metropolis_step() 153 | if epoch % (epochs // num_frames) == 0: 154 | img = plt.imshow(self.spins, interpolation = 'nearest') 155 | writer.grab_frame() 156 | img.remove() 157 | 158 | plt.close('all') 159 | 160 | 161 | else: 162 | for epoch in iterator: 163 | self.metropolis_step() 164 | 165 | 166 | if __name__ == "__main__": 167 | lattice = IsingModel((100, 100)) 168 | lattice.run(1000000, video = True, show_progress = True) 169 | -------------------------------------------------------------------------------- /ising_compiler/ising_nx.py: -------------------------------------------------------------------------------- 1 | import matplotlib as mpl 2 | import matplotlib.animation as anim 3 | import matplotlib.pyplot as plt 4 | import networkx as nx 5 | import numpy as np 6 | from mpl_toolkits.axes_grid1 import make_axes_locatable 7 | from tqdm import tqdm 8 | 9 | 10 | class IsingGraph: 11 | 12 | def __init__(self, graph: nx.Graph, temperature = 0.5, initializer = 'random', coupling_strength = 1.0): 13 | 14 | assert type(graph) is nx.Graph 15 | 16 | self.graph = graph 17 | # Initialize graph attributes 18 | if nx.get_node_attributes(graph, 'spin') == {}: 19 | self.graph = IsingGraph.initialize_spins(self.graph, initializer = initializer) 20 | if nx.get_node_attributes(graph, 'field') == {}: 21 | self.graph = IsingGraph.initialize_fields(self.graph, field_strength = 0.0) 22 | if nx.get_edge_attributes(graph, 'coupling') == {}: 23 | self.graph = IsingGraph.initialize_couplings(self.graph, coupling_strength = coupling_strength) 24 | 25 | self.temperature = temperature 26 | self.initializer = initializer 27 | 28 | @staticmethod 29 | def initialize_spins(graph, initializer = 'random'): 30 | '''Takes an undirected graph and stores spin information on the nodes''' 31 | for node in graph.nodes: 32 | if initializer == 'random': 33 | graph.nodes[node]['spin'] = 1 if np.random.rand() > .5 else -1 34 | elif initializer == 'up': 35 | graph.nodes[node]['spin'] = 1 36 | elif initializer == 'down': 37 | graph.nodes[node]['spin'] = -1 38 | else: 39 | raise ValueError() 40 | 41 | return graph 42 | 43 | @staticmethod 44 | def initialize_fields(graph, field_strength = 0.0): 45 | '''Takes an undirected graph and stores field information on the nodes ''' 46 | for node in graph.nodes: 47 | graph.nodes[node]['field'] = field_strength 48 | 49 | return graph 50 | 51 | @staticmethod 52 | def initialize_couplings(graph, coupling_strength = 1.0): 53 | '''Takes an undirected graph and stores coupling on the edges''' 54 | 55 | for edge in graph.edges: 56 | graph.edges[edge]['coupling'] = -1 * coupling_strength 57 | 58 | return graph 59 | 60 | @staticmethod 61 | def visualize_graph(graph, pos = None, fig = None, ax = None): 62 | 63 | if fig is None or ax is None: 64 | fig = plt.figure(figsize = (15, 10)) 65 | ax = fig.add_subplot(1, 1, 1) 66 | 67 | if pos is None: 68 | pos = nx.nx_pydot.graphviz_layout(graph) 69 | 70 | node_colors = list(nx.get_node_attributes(graph, 'spin').values()) 71 | edge_colors = list(nx.get_edge_attributes(graph, 'coupling').values()) 72 | 73 | node_size = 4000 * np.sqrt(1 / graph.number_of_nodes()) 74 | 75 | node_fields = list(nx.get_node_attributes(graph, 'field').values()) 76 | field_sizes = node_size * (1 + 2 * np.sqrt(np.abs(node_fields))) 77 | 78 | # color axis 79 | cmap = plt.get_cmap('PiYG') 80 | cmap_fields = plt.get_cmap('bwr') 81 | 82 | divider = make_axes_locatable(ax) 83 | cax1 = divider.append_axes('right', size = '2%', pad = '2%') 84 | mpl.colorbar.ColorbarBase(cax1, cmap = cmap, norm = mpl.colors.Normalize(-1, 1), ticks = [-1, +1]) 85 | cax1.set_ylabel('Spin', rotation = 270, labelpad = -5, fontsize = 14) 86 | 87 | cax2 = divider.append_axes('right', size = '2%', pad = '4%') 88 | mpl.colorbar.ColorbarBase(cax2, cmap = cmap_fields, norm = mpl.colors.Normalize(-1, 1), ticks = [-1, +1]) 89 | cax2.set_ylabel('Field strength, coupling', rotation = 270, labelpad = -5, fontsize = 14) 90 | 91 | node_labels = {n: n for n in graph.nodes() if not n.isdigit()} 92 | 93 | nx.draw_networkx_nodes(graph, pos, ax = ax, node_color = node_fields, vmax = 1.0, vmin = -1.0, 94 | cmap = cmap_fields, node_size = field_sizes, alpha = .25) 95 | 96 | nx.drawing.draw(graph, pos, ax = ax, node_color = node_colors, edge_color = edge_colors, 97 | node_size = node_size, vmax = 1.0, vmin = -1.0, width = 4, 98 | cmap = cmap, edge_cmap = cmap_fields, labels = node_labels, font_size = 14, 99 | font_color = "white") 100 | 101 | def get_energy_at_site(self, node): 102 | """ 103 | Compute the energy at a given site in the lattice. 104 | """ 105 | e = 0.0 106 | s1 = self.graph.nodes[node]['spin'] 107 | 108 | # Get the neighboring spins of pos 109 | for neighbor in self.graph.neighbors(node): 110 | J = self.graph.edges[node, neighbor]['coupling'] 111 | s2 = self.graph.nodes[neighbor]['spin'] 112 | e += J * s1 * s2 113 | 114 | # Add contribution from field 115 | h = self.graph.nodes[node]['field'] 116 | e += h * s1 117 | 118 | return e 119 | 120 | def metropolis_step(self): 121 | """ 122 | Runs one step of the Metropolis-Hastings algorithm 123 | :return: 124 | """ 125 | 126 | # Randomly select a site on the lattice 127 | node = np.random.choice(self.graph.nodes) 128 | 129 | # Calculate energy of the spin and energy if it is flipped 130 | energy = self.get_energy_at_site(node) 131 | 132 | energy_flipped = -1 * energy 133 | 134 | # Flip the spin if it is energetically favorable. If not, flip based on Boltzmann factor 135 | if energy_flipped <= 0: 136 | self.graph.nodes[node]['spin'] *= -1 137 | elif np.exp(-energy_flipped / self.temperature) > np.random.rand(): 138 | self.graph.nodes[node]['spin'] *= -1 139 | 140 | def run_metropolis(self, epochs, anneal_temperature_range = None, video = False, show_progress = False): 141 | 142 | iterator = tqdm(range(epochs)) if show_progress else range(epochs) 143 | if anneal_temperature_range: 144 | temperatures = np.geomspace(anneal_temperature_range[0], anneal_temperature_range[1], epochs) 145 | else: 146 | temperatures = [self.temperature] * epochs 147 | 148 | if video: 149 | num_frames = 100 150 | FFMpegWriter = anim.writers['ffmpeg'] 151 | # writer = FFMpegWriter(fps = 10) 152 | 153 | pos = nx.nx_pydot.graphviz_layout(self.graph) 154 | 155 | # with writer.saving(fig, "ising.mp4", 100): 156 | for epoch in iterator: 157 | self.temperature = temperatures[epoch] 158 | self.metropolis_step() 159 | if epoch % (epochs // num_frames) == 0: 160 | IsingGraph.visualize_graph(self.graph, pos = pos) 161 | title = str(epoch).zfill(5) 162 | plt.savefig(f"frames/{title}.png", dpi = 144) 163 | plt.close() 164 | # writer.grab_frame() 165 | # plt.clf() 166 | 167 | plt.close('all') 168 | 169 | else: 170 | for epoch in iterator: 171 | self.temperature = temperatures[epoch] 172 | self.metropolis_step() 173 | 174 | 175 | if __name__ == "__main__": 176 | graph = nx.convert_node_labels_to_integers(nx.generators.grid_2d_graph(20, 20)) 177 | lattice = IsingGraph(graph) 178 | lattice.run_metropolis(10000, video = True, show_progress = True) 179 | -------------------------------------------------------------------------------- /ising_compiler/ising_pytorch.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | 3 | import matplotlib.animation as anim 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import torch 7 | from tqdm import tqdm 8 | 9 | DEVICE = torch.device("cpu") 10 | 11 | 12 | class IsingModel: 13 | 14 | def __init__(self, size = (100, 100), 15 | temperature = 0.5, 16 | initial_state = 'random', 17 | periodic = False, 18 | all_to_all_couplings = False): 19 | self.size = size 20 | self.dimension = len(size) 21 | # self.lin_size = size[0] 22 | self.temperature = temperature 23 | self.periodic = periodic 24 | 25 | if initial_state == 'random': 26 | self.spins = 2 * torch.randint(0, 1 + 1, size = self.size, dtype = torch.int8) - 1 27 | else: 28 | self.spins = torch.ones(size = self.size, dtype = torch.int8) 29 | 30 | if all_to_all_couplings: 31 | # TODO: use torch.sparse for this 32 | raise NotImplementedError() 33 | 34 | else: 35 | edges_per_spin = self.dimension # in a square lattice 36 | s = (edges_per_spin,) + size 37 | # if self.periodic: 38 | # s = (edges_per_spin,) + size 39 | # else: 40 | # s = (edges_per_spin,) + tuple([d - 1 for d in self.size]) 41 | # Couplings are a [dimension, n_x, n_y ...] tuple 42 | self.couplings = -1 * torch.ones(size = s, dtype = torch.float) # all ferromagnetic 43 | 44 | # Magnetic field at each point 45 | self.fields = torch.zeros(size = self.size, dtype = torch.float) 46 | 47 | def get_random_index(self): 48 | """ 49 | Returns a random index in the spin lattice 50 | """ 51 | return [np.random.randint(0, dim) for dim in self.size] 52 | 53 | def get_energy_at_site(self, position): 54 | """ 55 | Compute the energy at a given site in the lattice. 56 | """ 57 | 58 | e = 0.0 59 | 60 | pos = torch.tensor(position) 61 | 62 | # Get the neighboring spins of pos 63 | for ax in range(self.dimension): 64 | # Make offset vector for axis like [0 0 0 ... 0 1 0 0] 65 | offset = torch.zeros_like(pos) 66 | offset[ax] = 1 67 | # Add the "left" neighbor 68 | if (pos - offset)[ax] >= 0: 69 | # print((pos - offset)) 70 | # print(pos, offset, self.spins[pos], self.spins[pos - offset], self.couplings[axis][pos - offset]) 71 | e += self.spins[tuple(pos)] * self.spins[tuple(pos - offset)] * self.couplings[ax][tuple(pos - offset)] 72 | # Add the "right" neighbor 73 | if (pos + offset)[ax] < self.size[ax]: 74 | # print((pos + offset)) 75 | e += self.spins[tuple(pos)] * self.spins[tuple(pos + offset)] * \ 76 | self.couplings[ax][tuple(pos)] # no plus offset here 77 | # TODO: handle periodic case 78 | 79 | # Add contribution from field 80 | e += self.spins[tuple(pos)] * self.fields[tuple(pos)] 81 | 82 | return e 83 | 84 | def get_energy_at_site_2d(self, i, j): 85 | """ 86 | Compute the energy at a given site in the lattice. Special optimized version for 2D case 87 | """ 88 | 89 | if 1 <= i < self.size[0] - 1 and 1 <= j < self.size[1] - 1: 90 | return self.spins[i, j] * ( 91 | self.spins[i + 1, j] * self.couplings[0, i, j] + 92 | self.spins[i - 1, j] * self.couplings[0, i - 1, j] + 93 | self.spins[i, j + 1] * self.couplings[1, i, j] + 94 | self.spins[i, j - 1] * self.couplings[1, i, j - 1] + 95 | self.fields[i, j] 96 | ) 97 | 98 | else: 99 | e = self.spins[i, j] * self.fields[i, j] 100 | if i < self.size[0] - 1: 101 | e += self.spins[i, j] * self.spins[i + 1, j] * self.couplings[0, i, j] 102 | if i >= 1: 103 | e += self.spins[i, j] * self.spins[i - 1, j] * self.couplings[0, i - 1, j] 104 | if j < self.size[1] - 1: 105 | e += self.spins[i, j] * self.spins[i, j + 1] * self.couplings[1, i, j] 106 | if j >= 1: 107 | e += self.spins[i, j] * self.spins[i, j - 1] * self.couplings[1, i, j - 1] 108 | return e 109 | 110 | def get_total_energy(self): 111 | """ 112 | Gets the total internal energy of the lattice system, not normalized by number of spins 113 | """ 114 | ranges = [range(x) for x in self.size] 115 | all_indices = product(*ranges) 116 | 117 | return sum(self.get_energy_at_site(pos) for pos in all_indices) 118 | 119 | def run(self, epochs, num_frames = 100, video = True): 120 | 121 | FFMpegWriter = anim.writers['ffmpeg'] 122 | writer = FFMpegWriter(fps = 10) 123 | 124 | plt.ion() 125 | fig = plt.figure() 126 | 127 | with writer.saving(fig, "ising.mp4", 100): 128 | for epoch in tqdm(range(epochs)): 129 | # Randomly select a site on the lattice 130 | pos = self.get_random_index() 131 | # i, j = [np.random.randint(0, dim) for dim in self.size] 132 | 133 | # Calculate energy of the spin and energy if it is flipped 134 | energy = self.get_energy_at_site(pos) 135 | # energy = self.get_energy_at_site_2d(i, j) 136 | 137 | energy_flipped = -1 * energy 138 | 139 | # Flip the spin if it is energetically favorable. If not, flip based on Boltzmann factor 140 | if energy_flipped <= 0: 141 | self.spins[pos] *= -1 142 | elif np.exp(-energy_flipped / self.temperature) > np.random.rand(): 143 | self.spins[pos] *= -1 144 | 145 | if epoch % (epochs // num_frames) == 0: 146 | if video: 147 | img = plt.imshow(self.spins.numpy(), interpolation = 'nearest') 148 | writer.grab_frame() 149 | img.remove() 150 | 151 | # tqdm.write("Net Magnetization: {:.2f}".format(self.magnetization)) 152 | 153 | plt.close('all') 154 | 155 | 156 | if __name__ == "__main__": 157 | lattice = IsingModel((100, 100)) 158 | lattice.run(1000000) 159 | -------------------------------------------------------------------------------- /ising_compiler/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def is_adjacent(s1, s2): 5 | '''Returns whether spin sites s1 and s2 are neighbors''' 6 | return np.linalg.norm(np.array(s2) - np.array(s1)) == 1.0 7 | 8 | 9 | def get_random_site(lattice_size): 10 | s = np.zeros(len(lattice_size), dtype = int) 11 | for i, max_dim in enumerate(lattice_size): 12 | s[i] = np.random.randint(0, max_dim) 13 | return tuple(s) 14 | 15 | 16 | def get_random_neighbor(s1, lattice_size = None): 17 | dim = len(s1) 18 | offset = np.zeros(dim, dtype = int) 19 | offset_dim = np.random.randint(0, dim) 20 | # Set offset[offset_dim] equal to +1 or -1 21 | if np.random.rand() < 0.5: 22 | offset[offset_dim] = -1 23 | else: 24 | offset[offset_dim] = 1 25 | 26 | s2 = tuple(np.array(s1) + offset) 27 | 28 | if lattice_size is not None: 29 | if np.min(s2) < 0 or any(map(lambda x: x < 1, np.array(lattice_size) - np.array(s2))): 30 | # Invalid neighbor 31 | return get_random_neighbor(s1, lattice_size = lattice_size) 32 | 33 | return s2 34 | -------------------------------------------------------------------------------- /paper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fancompute/ising-compiler/14cfb8982f161c48419c7d755329ef48494eb470/paper.pdf -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="ising-compiler", 8 | version="0.1.0", 9 | author="Ben Bartlett", 10 | author_email="benbartlett@stanford.edu", 11 | description="🍰 Compiling your code to an Ising Hamiltonian so you don't have to!", 12 | license="MIT", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/fancompute/ising-compiler", 16 | packages=setuptools.find_packages(), 17 | classifiers=( 18 | "Programming Language :: Python :: 3", 19 | "Operating System :: OS Independent", 20 | ), 21 | install_requires=[ 22 | "numpy", 23 | "scipy", 24 | "matplotlib", 25 | "tqdm", 26 | "networkx", 27 | "pydot" 28 | ], 29 | ) 30 | -------------------------------------------------------------------------------- /tests_np.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tqdm import tqdm 4 | 5 | from ising_compiler.gates import * 6 | from ising_compiler.utils import * 7 | 8 | 9 | class GateTests(unittest.TestCase): 10 | 11 | def test_couplings(self): 12 | size = (5, 5) 13 | for _ in tqdm(range(100), desc = "Testing coupling get/set"): 14 | coupling = 1 if np.random.rand() < .5 else -1 15 | c = IsingCircuit(size = size, initial_state = 'random', temperature = 1e-4) 16 | s1 = get_random_site(size) 17 | s2 = get_random_neighbor(s1, lattice_size = size) 18 | c.set_coupling(s1, s2, value = coupling) 19 | c.run(500, video = False, show_progress = False) 20 | spin1 = c.spins[s1] 21 | spin2 = c.spins[s2] 22 | assert spin1 == -1 * coupling * spin2 23 | 24 | def test_gates(self): 25 | gates = [WIRE] 26 | tests = [self._test_WIRE] 27 | 28 | for gate, test in zip(gates, tests): 29 | self._test_gate(gate, test) 30 | 31 | def _test_gate(self, gate_cls, test_fn, num_trials=100): 32 | all_input_combos = [[int(x) for x in ('{:0' + str(gate_cls.num_inputs) + 'b}').format(n)] 33 | for n in range(2 ** gate_cls.num_inputs)] 34 | 35 | for _ in tqdm(range(100), desc = "Testing {}".format(gate_cls.__name__)): 36 | for inputs in all_input_combos: 37 | test_fn(inputs) 38 | 39 | def _test_WIRE(self, inputs): 40 | size = (5, 5) 41 | c = IsingCircuit(size = size, initial_state = 'random', temperature = 1e-4) 42 | s1 = get_random_site(size) 43 | s2 = get_random_neighbor(s1, lattice_size = size) 44 | c.set_spin(s1, value = inputs[0]) 45 | WIRE(circuit = c, spins=(s1,s2), inputs=(s1,), outputs=(s2,)) 46 | c.run(500) 47 | assert c.spins[s1] == c.spins[s2] 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /tests_nx.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from json import loads 3 | 4 | import numpy as np 5 | from tqdm import tqdm 6 | 7 | from ising_compiler.alu_nx import IsingALU 8 | from ising_compiler.gates_nx import IsingCircuit 9 | 10 | 11 | class GateTests(unittest.TestCase): 12 | 13 | def test_gates(self): 14 | gates = [IsingCircuit.AND, 15 | IsingCircuit.NAND, 16 | IsingCircuit.OR, 17 | IsingCircuit.NOR, 18 | IsingCircuit.XOR, 19 | IsingCircuit.XNOR] 20 | tests = [lambda a, b: a & b, 21 | lambda a, b: not (a & b), 22 | lambda a, b: a | b, 23 | lambda a, b: not (a | b), 24 | lambda a, b: a ^ b, 25 | lambda a, b: not (a ^ b)] 26 | 27 | for gate, test in zip(gates, tests): 28 | self._test_gate_2in_1out(gate, test) 29 | 30 | def _test_gate_2in_1out(self, gate_fn, test_fn): 31 | 32 | circuit = IsingCircuit() 33 | a = circuit.INPUT("A") 34 | b = circuit.INPUT("B") 35 | c = gate_fn(circuit, a, b, "C") 36 | circuit.OUTPUT(c) 37 | 38 | all_input_combos = [[int(x) for x in ('{:02b}').format(n)] for n in range(2 ** 2)] 39 | 40 | for inputs in tqdm(all_input_combos, desc = gate_fn.__name__): 41 | expectations = circuit.evaluate_expectations({"A": inputs[0], "B": inputs[1]}, 42 | runs = 100, 43 | epochs_per_run = 1000, 44 | anneal_temperature_range = [.5, 1e-4], 45 | show_progress = False) 46 | c_exp = expectations["C"] 47 | c_ideal = float(test_fn(inputs[0], inputs[1])) 48 | self.assertEqual(c_exp, c_ideal) 49 | 50 | def test_half_adder(self): 51 | circuit = IsingALU() 52 | A = circuit.INPUT("A") 53 | B = circuit.INPUT("B") 54 | S, C = circuit.HALF_ADDER(A, B, "S", "C") 55 | circuit.OUTPUT(S) 56 | circuit.OUTPUT(C) 57 | 58 | all_input_combos = [[int(x) for x in ('{:02b}').format(n)] for n in range(2 ** 2)] 59 | 60 | for inputs in tqdm(all_input_combos, desc = "HALFADDR"): 61 | a, b = inputs 62 | 63 | expectations = circuit.evaluate_expectations({"A": a, "B": b}, 64 | runs = 200, 65 | epochs_per_run = 1000, 66 | anneal_temperature_range = [.5, 1e-4], 67 | show_progress = False) 68 | s_exp = expectations[S] 69 | cout_exp = expectations[C] 70 | 71 | self.assertEqual(a + b, s_exp + 2 * cout_exp) # , places=2) 72 | 73 | def test_full_adder(self): 74 | self._test_full_adder_circuits(use_nand_adder = False) 75 | 76 | def test_full_adder_nand(self): 77 | self._test_full_adder_circuits(use_nand_adder = True) 78 | 79 | def _test_full_adder_circuits(self, use_nand_adder = False): 80 | circuit = IsingALU() 81 | A = circuit.INPUT("A") 82 | B = circuit.INPUT("B") 83 | Cin = circuit.INPUT("Cin") 84 | if use_nand_adder: 85 | S, Cout = circuit.FULL_ADDER_NAND(A, B, Cin, S = "S", Cout = "Cout") 86 | print("Testing full adder (NAND construction)...\n") 87 | else: 88 | S, Cout = circuit.FULL_ADDER(A, B, Cin, S = "S", Cout = "Cout") 89 | print("Testing full adder...\n") 90 | 91 | circuit.OUTPUT(S) 92 | circuit.OUTPUT(Cout) 93 | 94 | all_input_combos = [[int(x) for x in ('{:03b}').format(n)] for n in range(2 ** 3)] 95 | 96 | for inputs in all_input_combos: 97 | a, b, cin = inputs 98 | input_dict = {"A": a, "B": b, "Cin": cin} 99 | print("Testing with inputs {}".format(input_dict)) 100 | outcomes = circuit.evaluate_outcomes(input_dict, 101 | runs = 20, 102 | epochs_per_run = 100000, 103 | anneal_temperature_range = [1, 1e-3], 104 | show_progress = True) 105 | 106 | most_common = outcomes.most_common()[0] 107 | most_common_outcome, most_common_frequency = most_common 108 | most_common_outcome = loads(most_common_outcome) 109 | desired_outcome = {"S" : (a + b + cin) % 2, 110 | "Cout": (a + b + cin) // 2} 111 | 112 | # most common outcome needs to be the correct one 113 | self.assertEqual(most_common_outcome, desired_outcome, msg = "Most common outcome is not desired one") 114 | 115 | # fail if below some normalized frequency 116 | total_trials = sum([tup[1] for tup in outcomes.most_common()]) 117 | correct_rate = most_common_frequency / total_trials 118 | 119 | print("Accuraccy rate: {:.2f}".format(correct_rate)) 120 | 121 | ACCURACCY_THRESHOLD = 0.6 122 | 123 | self.assertGreaterEqual(correct_rate, ACCURACCY_THRESHOLD, msg = "Accuraccy threshold not met") 124 | 125 | def test_ripple_carry_adder(self, num_bits = 4, num_trials = 3): 126 | circuit = IsingALU() 127 | S_bits, Cout = circuit.RIPPLE_CARRY_ADDER(num_bits) 128 | 129 | for _ in range(num_trials): 130 | num1, num2 = np.random.randint(0, 2 ** num_bits, size = 2) 131 | digs1 = [int(x) for x in ('{:0' + str(num_bits) + 'b}').format(num1)] 132 | digs2 = [int(x) for x in ('{:0' + str(num_bits) + 'b}').format(num2)] 133 | 134 | input_dict = {} 135 | for i, (a, b) in enumerate(zip(reversed(digs1), reversed(digs2))): 136 | input_dict["A" + str(i)] = a 137 | input_dict["B" + str(i)] = b 138 | 139 | digs_sum = [int(x) for x in ('{:0' + str(num_bits+1) + 'b}').format(num1+num2)] 140 | desired_output = {"C"+str(num_bits): digs_sum[0]} 141 | for i, dig in enumerate(reversed(digs_sum[1:])): 142 | desired_output["S"+str(i)] = dig 143 | 144 | print("Testing {} + {} = {} with inputs {}".format(num1, num2, num1 + num2, input_dict)) 145 | print("Desired output: {}".format(desired_output)) 146 | 147 | epochs_per_run = 100000 * 2 ** num_bits 148 | outcomes = circuit.evaluate_outcomes(input_dict, 149 | runs = 10, 150 | epochs_per_run = epochs_per_run, 151 | anneal_temperature_range = [1, 1e-4], 152 | show_progress = True) 153 | 154 | most_common = outcomes.most_common()[0] 155 | most_common_outcome, most_common_frequency = most_common 156 | most_common_outcome = loads(most_common_outcome) 157 | 158 | # most common outcome needs to be the correct one 159 | self.assertEqual(most_common_outcome, desired_output, msg = "Most common output is not desired one") 160 | 161 | # fail if below some normalized frequency 162 | total_trials = sum([tup[1] for tup in outcomes.most_common()]) 163 | correct_rate = most_common_frequency / total_trials 164 | 165 | print("Accuraccy rate: {:.2f}".format(correct_rate)) 166 | 167 | # ACCURACCY_THRESHOLD = 0.75 168 | # self.assertGreaterEqual(correct_rate, ACCURACCY_THRESHOLD, msg = "Accuraccy threshold not met") 169 | 170 | 171 | if __name__ == '__main__': 172 | unittest.main() 173 | --------------------------------------------------------------------------------