├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── ising.py ├── media └── ising.png ├── requirements.txt ├── setup.cfg └── test.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.7 6 | steps: 7 | - checkout 8 | - run: 9 | name: install dependencies 10 | command: | 11 | python3 -m venv venv 12 | . venv/bin/activate 13 | pip install -r requirements.txt 14 | - run: 15 | name: run tests 16 | command: | 17 | . venv/bin/activate 18 | flake8 --exclude=venv* --statistics 19 | pytest -v --cov=ellipse test.py 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.mp4 2 | venv/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![bdhammel](https://circleci.com/gh/bdhammel/ising-model.svg?style=shield)](https://app.circleci.com/pipelines/github/bdhammel/ising-model) 2 | 3 | # Ising Model 4 | 5 | Python code to simulate the Ising model of a Ferromagnet. 6 | 7 | For a discussion of the theory, visit [my blog post](http://www.bdhammel.com/ising-model/). 8 | 9 | ![](./media/ising.png) 10 | 11 | The initial conditions of the ising lattice can be specified by the `tempature`, `initial state`, and `size parameters` of the model. 12 | 13 | Running the simulation will output a video of system as it changes through out the run steps. 14 | 15 | 16 | ## Example 17 | 18 | The `--help` command can show to possible parameters for modifying the simulation 19 | 20 | ~~~bash 21 | $ python ising.py --help 22 | Usage: ising.py [OPTIONS] 23 | 24 | Options: 25 | -t, --temperature FLOAT temperature of the system [default: 0.5] 26 | -i, --initial-state [r|u] (R)andom or (U)niform initial state of the system [default: r] 27 | -s, --size INTEGER Number of sites, M, in the MxM lattice [default: 100] 28 | -e, --epochs INTEGER Number of iterations to run the simulation for [default: 1000000] 29 | --video Record a video of the simulation progression 30 | --help Show this message and exit. 31 | ~~~ 32 | 33 | For example: 34 | 35 | ~~~bash 36 | $ python ising.py --temperature .8 --initial-state r --video 37 | ~~~ 38 | 39 | 40 | ## FAQ 41 | 42 | If you get the error: 43 | 44 | ~~~bash 45 | MovieWriter stderr: 46 | dyld: Library not loaded: /usr/local/opt/x264/lib/libx264.152.dylib 47 | Referenced from: /usr/local/bin/ffmpeg 48 | Reason: image not found 49 | ~~~ 50 | 51 | Then you need to install ffmpeg 52 | 53 | ~~~bash 54 | $ brew install ffmpeg 55 | ~~~ 56 | -------------------------------------------------------------------------------- /ising.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import matplotlib.animation as manimation 4 | from tqdm import tqdm 5 | import click 6 | 7 | 8 | class IsingLattice: 9 | 10 | def __init__(self, temperature, initial_state, size): 11 | self.size = size 12 | self.T = temperature 13 | self.system = self._build_system(initial_state) 14 | 15 | @property 16 | def sqr_size(self): 17 | return (self.size, self.size) 18 | 19 | def _build_system(self, initial_state): 20 | """Build the system 21 | 22 | Build either a randomly distributed system or a homogeneous system (for 23 | watching the deterioration of magnetization 24 | 25 | Parameters 26 | ---------- 27 | initial_state : str: "r" or other 28 | Initial state of the lattice. currently only random ("r") initial 29 | state, or uniformly magnetized, is supported 30 | """ 31 | 32 | if initial_state == 'r': 33 | system = np.random.choice([-1, 1], self.sqr_size) 34 | elif initial_state == 'u': 35 | system = np.ones(self.sqr_size) 36 | else: 37 | raise ValueError( 38 | "Initial State must be 'r', random, or 'u', uniform" 39 | ) 40 | 41 | return system 42 | 43 | def _bc(self, i): 44 | """Apply periodic boundary condition 45 | 46 | Check if a lattice site coordinate falls out of bounds. If it does, 47 | apply periodic boundary condition 48 | 49 | Assumes lattice is square 50 | 51 | Parameters 52 | ---------- 53 | i : int 54 | lattice site coordinate 55 | 56 | Return 57 | ------ 58 | int 59 | corrected lattice site coordinate 60 | """ 61 | if i >= self.size: 62 | return 0 63 | if i < 0: 64 | return self.size - 1 65 | else: 66 | return i 67 | 68 | def energy(self, N, M): 69 | """Calculate the energy of spin interaction at a given lattice site 70 | i.e. the interaction of a Spin at lattice site n,m with its 4 neighbors 71 | 72 | - S_n,m*(S_n+1,m + Sn-1,m + S_n,m-1, + S_n,m+1) 73 | 74 | Parameters 75 | ---------- 76 | N : int 77 | lattice site coordinate 78 | M : int 79 | lattice site coordinate 80 | 81 | Return 82 | ------ 83 | float 84 | energy of the site 85 | """ 86 | return -2*self.system[N, M]*( 87 | self.system[self._bc(N - 1), M] + self.system[self._bc(N + 1), M] 88 | + self.system[N, self._bc(M - 1)] + self.system[N, self._bc(M + 1)] 89 | ) 90 | 91 | @property 92 | def internal_energy(self): 93 | e = 0 94 | E = 0 95 | E_2 = 0 96 | 97 | for i in range(self.size): 98 | for j in range(self.size): 99 | e = self.energy(i, j) 100 | E += e 101 | E_2 += e**2 102 | 103 | U = (1./self.size**2)*E 104 | U_2 = (1./self.size**2)*E_2 105 | 106 | return U, U_2 107 | 108 | @property 109 | def heat_capacity(self): 110 | U, U_2 = self.internal_energy 111 | return U_2 - U**2 112 | 113 | @property 114 | def magnetization(self): 115 | """Find the overall magnetization of the system 116 | """ 117 | return np.abs(np.sum(self.system)/self.size**2) 118 | 119 | 120 | def run(lattice, epochs, video=True): 121 | """Run the simulation 122 | """ 123 | 124 | FFMpegWriter = manimation.writers['ffmpeg'] 125 | writer = FFMpegWriter(fps=10) 126 | 127 | fig = plt.figure() 128 | 129 | with writer.saving(fig, "ising.mp4", 100): 130 | for epoch in tqdm(range(epochs)): 131 | # Randomly select a site on the lattice 132 | N, M = np.random.randint(0, lattice.size, 2) 133 | 134 | # Calculate energy of a flipped spin 135 | E = -1*lattice.energy(N, M) 136 | 137 | # "Roll the dice" to see if the spin is flipped 138 | if E <= 0.: 139 | lattice.system[N, M] *= -1 140 | elif np.exp(-E/lattice.T) > np.random.rand(): 141 | lattice.system[N, M] *= -1 142 | 143 | if video and epoch % (epochs//75) == 0: 144 | img = plt.imshow( 145 | lattice.system, interpolation='nearest', cmap='jet' 146 | ) 147 | writer.grab_frame() 148 | img.remove() 149 | 150 | plt.close('all') 151 | 152 | 153 | @click.command() 154 | @click.option( 155 | '--temperature', '-t', 156 | default=0.5, 157 | show_default=True, 158 | help='temperature of the system' 159 | ) 160 | @click.option( 161 | '--initial-state', '-i', 162 | default='r', 163 | type=click.Choice(['r', 'u'], case_sensitive=False), 164 | show_default=True, 165 | help='(R)andom or (U)niform initial state of the system' 166 | ) 167 | @click.option( 168 | '--size', '-s', 169 | default=100, 170 | show_default=True, 171 | help='Number of sites, M, in the MxM lattice' 172 | ) 173 | @click.option( 174 | '--epochs', '-e', 175 | default=1_000_000, 176 | type=int, 177 | show_default=True, 178 | help='Number of iterations to run the simulation for' 179 | ) 180 | @click.option( 181 | '--video', 182 | is_flag=True, 183 | help='Record a video of the simulation progression' 184 | ) 185 | def main(temperature, initial_state, size, epochs, video): 186 | lattice = IsingLattice( 187 | temperature=temperature, initial_state=initial_state, size=size 188 | ) 189 | run(lattice, epochs, video) 190 | 191 | print(f"{'Net Magnetization [%]:':.<25}{lattice.magnetization:.2f}") 192 | print(f"{'Heat Capacity [AU]:':.<25}{lattice.heat_capacity:.2f}") 193 | 194 | 195 | if __name__ == "__main__": 196 | plt.ion() 197 | main() 198 | -------------------------------------------------------------------------------- /media/ising.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdhammel/ising-model/bbf593d4d321094b77b47632869b05c4b7c17c8b/media/ising.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | tqdm 4 | click 5 | pytest 6 | pytest-cov 7 | flake8 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [yapf] 2 | based_on_style = google 3 | split_before_first_argument = false 4 | split_before_arithmetic_operator = true 5 | arithmetic_precedence_indication = true 6 | no_spaces_around_selected_binary_operators = *, /, // 7 | coalesce_brackets = true 8 | dedent_closing_brackets = true 9 | column_limit = 80 10 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from ising import IsingLattice 7 | 8 | 9 | @pytest.mark.parametrize('i, t', [ 10 | (100, 0), # a value at 100 wraps around to 0 # noqa 11 | (99, 99), # a value at 99 is in the lattice # noqa 12 | (0, 0), # a value at 0 is in the lattice # noqa 13 | (-1, 99) # a value at -1 wraps around to 99 # noqa 14 | ]) 15 | def test_boundary_conditions(i, t): 16 | mock_self = mock.Mock() 17 | mock_self.size = 100 18 | r = IsingLattice._bc(mock_self, i) 19 | assert r == t # is the returned value the true value # noqa 20 | 21 | 22 | @pytest.mark.parametrize('i, system, e', [ 23 | ((1, 1), np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]), -8), # most probable state # noqa 24 | ((1, 1), np.array([[-1, -1, -1], [-1, -1, -1], [-1, -1, -1]]), -8), # most probable state # noqa 25 | ((1, 1), np.array([[-1, -1, -1], [-1, 1, -1], [-1, -1, -1]]), 8), # least probable state # noqa 26 | ((1, 1), np.array([[1, -1, 1], [-1, 1, 1], [1, 1, 1]]), 0), # 50/50 state # noqa 27 | ]) 28 | def test_energy_calculation_is_correct(i, system, e): 29 | lattice = IsingLattice(temperature=1, initial_state='u', size=3) 30 | lattice.system = system 31 | _e = lattice.energy(*i) 32 | assert _e == e # is the returned value the true value 33 | 34 | 35 | @pytest.mark.skip("TODO") 36 | def check_internel_energy_is_correct(): 37 | pass 38 | 39 | 40 | @pytest.mark.parametrize('system, m', [ 41 | (np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]), 1), # uniform magnetization # noqa 42 | (np.array([[-1, -1, -1], [-1, -1, -1], [-1, -1, -1]]), 1), # uniform magnetization # noqa 43 | (np.array([[-1, -1, -1], [-1, -1, 1], [1, 1, 1]]), 1/9), # net magnetization (i.e. sum of system) 1/9 # noqa 44 | ]) 45 | def test_magnitization_correct(system, m): 46 | lattice = IsingLattice(temperature=1, initial_state='u', size=3) 47 | lattice.system = system 48 | _m = lattice.magnetization 49 | assert _m == m 50 | --------------------------------------------------------------------------------