ising/lattice.rs
1use rand::{
2 distributions::{Bernoulli, Distribution, Uniform},
3 Rng,
4};
5
6/// A lattice spin.
7pub struct Spin {
8 pub x: usize,
9 pub y: usize,
10}
11
12/// The lattice holding the spin values.
13pub struct Lattice {
14 side_length: usize,
15 /// Quadratic lattice -> Number of spins is the square of the side length.
16 n_spins: i64,
17 /// Inverse stored to prevent repeated casting to float and division.
18 inverse_n_spins: f64,
19 /// Number of positive spins used to calculate the magnetization.
20 n_positive_spins: i64,
21 /// Vector of spin values. True is a positive spin +1, false is a negative spin -1.
22 vec: Vec<bool>,
23 /// Random spin distribution to get random spins.
24 random_spin_dist: Uniform<usize>,
25}
26
27impl Lattice {
28 /// Creates a new lattice initialized randomly with the given
29 /// random number generator `rng`.
30 ///
31 /// The lattice is quadratic with `side_length` as the side length.
32 /// It is initialized with the same probability for positive and negative spins.
33 ///
34 /// # Example
35 /// ```
36 /// use rand::{rngs::SmallRng, SeedableRng};
37 /// use ising::lattice::Lattice;
38 ///
39 /// let side_length = 8;
40 /// let mut rng = SmallRng::seed_from_u64(42);
41 /// let lattice = Lattice::new(side_length, &mut rng);
42 ///```
43 pub fn new(side_length: usize, rng: &mut impl Rng) -> Self {
44 // Quadratic lattice.
45 let n_spins = side_length * side_length;
46 let mut vec = Vec::with_capacity(n_spins);
47
48 // Initialize lattice with random spins.
49 // with equal probability (0.5) for spin up +1 or spin down -1.
50 // unwrap: 0.5 is obviously between 0 and 1.
51 let dist = Bernoulli::new(0.5).unwrap();
52 let mut n_positive_spins = 0;
53 vec.resize_with(n_spins, || {
54 let spin_value = dist.sample(rng);
55
56 // Update the number of positive spins.
57 if spin_value {
58 n_positive_spins += 1;
59 }
60
61 spin_value
62 });
63
64 Self {
65 side_length,
66 n_spins: n_spins as i64,
67 inverse_n_spins: 1.0 / n_spins as f64,
68 n_positive_spins,
69 vec,
70 random_spin_dist: Uniform::new(0, side_length),
71 }
72 }
73
74 /// The side length of the lattice.
75 ///
76 /// # Example
77 /// ```
78 /// # use rand::{rngs::SmallRng, SeedableRng};
79 /// # use ising::lattice::Lattice;
80 /// #
81 /// let side_length = 8;
82 /// # let mut rng = SmallRng::seed_from_u64(42);
83 /// let lattice = Lattice::new(side_length, &mut rng);
84 /// assert_eq!(side_length, lattice.side_length());
85 ///```
86 #[must_use]
87 pub const fn side_length(&self) -> usize {
88 self.side_length
89 }
90
91 /// Vector holding the spin values
92 /// (`true` -> `+1`, `false` -> `-1`).
93 ///
94 /// Can be used to plot the lattice.
95 ///
96 /// # Example
97 /// ```
98 /// # use rand::{rngs::SmallRng, SeedableRng};
99 /// # use ising::lattice::Lattice;
100 /// #
101 /// let side_length = 8;
102 /// # let mut rng = SmallRng::seed_from_u64(42);
103 /// let lattice = Lattice::new(side_length, &mut rng);
104 /// let magnetization_sum =
105 /// lattice.vec().iter().fold(
106 /// 0,
107 /// |mag_sum, value| if *value { mag_sum + 1 } else { mag_sum - 1 },
108 /// );
109 /// assert_eq!(magnetization_sum, lattice.magnetization());
110 ///```
111 #[must_use]
112 pub const fn vec(&self) -> &Vec<bool> {
113 &self.vec
114 }
115
116 /// Returns a random spin.
117 ///
118 /// Since the returned spin is granted to be inside the lattice,
119 /// [`value_unchecked`](Lattice::value_unchecked) and
120 /// [`set_value_unchecked`](Lattice::set_value_unchecked) are safe
121 /// to be used with the returned spin on the same lattice.
122 ///
123 /// # Example
124 /// ```
125 /// # use rand::{rngs::SmallRng, SeedableRng};
126 /// # use ising::lattice::Lattice;
127 ///
128 /// let side_length = 8;
129 /// # let mut rng = SmallRng::seed_from_u64(42);
130 /// let mut lattice = Lattice::new(side_length, &mut rng);
131 ///
132 /// let (random_spin, random_spin_value) = lattice.rand_spin_with_value(&mut rng);
133 ///
134 /// // Safe to use the unchecked methods on the same lattice.
135 /// unsafe { lattice.set_value_unchecked(&random_spin, !random_spin_value) };
136 /// ```
137 pub fn rand_spin_with_value(&self, rng: &mut impl Rng) -> (Spin, bool) {
138 let spin = Spin {
139 x: self.random_spin_dist.sample(rng),
140 y: self.random_spin_dist.sample(rng),
141 };
142
143 // Safety: The random spin is granted to be inside the lattice.
144 let value = unsafe { self.value_unchecked(&spin) };
145
146 (spin, value)
147 }
148
149 /// Index of the given spin in the vector holding the spin values.
150 const fn index(&self, spin: &Spin) -> usize {
151 // Matrix indexing.
152 spin.y * self.side_length + spin.x
153 }
154
155 /// The value of a spin (`true` -> `+1`, `false` -> `-1`).
156 #[must_use]
157 pub fn value(&self, spin: &Spin) -> bool {
158 let ind = self.index(spin);
159
160 self.vec[ind]
161 }
162
163 /// Same as [`value`](Self::value) but without bounds check.
164 ///
165 /// # Safety
166 /// Calling this method with a spin out of the lattice is undefined behavior!
167 ///
168 /// Test your code with [`value`](Self::value) first and make sure
169 /// that you access only valid lattice spins, then use this function
170 /// if you need more performance.
171 #[must_use]
172 pub unsafe fn value_unchecked(&self, spin: &Spin) -> bool {
173 let ind = self.index(spin);
174
175 unsafe { *self.vec.get_unchecked(ind) }
176 }
177
178 // Update the number of positive spins.
179 fn update_n_positive_spin(&mut self, new_val: bool) {
180 if new_val {
181 self.n_positive_spins += 1;
182 } else {
183 self.n_positive_spins -= 1;
184 }
185 }
186
187 /// Sets the value of the spin.
188 ///
189 /// # Example
190 /// ```
191 /// # use rand::{rngs::SmallRng, SeedableRng};
192 /// # use ising::lattice::Lattice;
193 /// use ising::lattice::Spin;
194 ///
195 /// let side_length = 8;
196 /// # let mut rng = SmallRng::seed_from_u64(42);
197 /// let mut lattice = Lattice::new(side_length, &mut rng);
198 /// let spin = Spin{x: 0, y: 0};
199 /// let old_spin_value = lattice.value(&spin);
200 ///
201 /// lattice.set_value(&spin, !old_spin_value);
202 /// assert_eq!(lattice.value(&spin), !old_spin_value);
203 ///```
204 pub fn set_value(&mut self, spin: &Spin, val: bool) {
205 let ind = self.index(spin);
206 self.vec[ind] = val;
207
208 self.update_n_positive_spin(val);
209 }
210
211 /// Same as [`set_value`](Self::set_value) but without bounds check.
212 ///
213 /// # Safety
214 /// Calling this method with a spin out of the lattice is undefined behavior!
215 ///
216 /// Like with [`value_unchecked`](Self::value_unchecked),
217 /// test your code with [`set_value`](Self::set_value) first and make sure
218 /// that you access only valid lattice spins, then use this function
219 /// if you need more performance.
220 pub unsafe fn set_value_unchecked(&mut self, spin: &Spin, val: bool) {
221 let ind = self.index(spin);
222 unsafe { *self.vec.get_unchecked_mut(ind) = val };
223
224 self.update_n_positive_spin(val);
225 }
226
227 /// The previous and next coordinate to a given coordinate
228 /// while applying the periodic boundary condition.
229 const fn previous_next_coordinate(&self, coordinate: usize) -> (usize, usize) {
230 let last_ind = self.side_length - 1;
231
232 if coordinate == 0 {
233 // First spin -> previous is last spin.
234 (last_ind, 1)
235 } else if coordinate == last_ind {
236 // Last spin -> next is first spin.
237 (last_ind - 1, 0)
238 } else {
239 (coordinate - 1, coordinate + 1)
240 }
241 }
242
243 /// The four neighbouring spins of a given spin with the periodic boundary condition.
244 ///
245 /// If the center spin is inside the lattice, the neighbouring spins are also inside
246 /// the lattice and safe to be used with the methods [`value_unchecked`](Self::value_unchecked)
247 /// and [`set_value_unchecked`](Self::set_value_unchecked).
248 #[must_use]
249 pub const fn neighbouring_spins(&self, spin: &Spin) -> [Spin; 4] {
250 let (previous_x, next_x) = self.previous_next_coordinate(spin.x);
251 let (previous_y, next_y) = self.previous_next_coordinate(spin.y);
252
253 // Respect order in memory for cache optimization.
254 [
255 Spin {
256 x: spin.x,
257 y: previous_y,
258 },
259 Spin {
260 x: previous_x,
261 y: spin.y,
262 },
263 Spin {
264 x: next_x,
265 y: spin.y,
266 },
267 Spin {
268 x: spin.x,
269 y: next_y,
270 },
271 ]
272 }
273
274 /// The current magnetization.
275 ///
276 /// The value is between `-N` and `N` (with step `2`) where `N` is the number of spins.
277 ///
278 /// # Example
279 /// ```
280 /// # use rand::{rngs::SmallRng, SeedableRng};
281 /// # use ising::lattice::Lattice;
282 /// # use ising::lattice::Spin;
283 /// #
284 /// let side_length = 8;
285 /// # let mut rng = SmallRng::seed_from_u64(42);
286 /// let mut lattice = Lattice::new(side_length, &mut rng);
287 /// let spin = Spin{x: 0, y: 0};
288 /// let old_value = lattice.value(&spin);
289 /// let old_magnetization = lattice.magnetization();
290 ///
291 /// // Invert spin value
292 /// lattice.set_value(&spin, !old_value);
293 /// let new_value_sign = if old_value { -1 } else { 1 };
294 /// assert_eq!(lattice.magnetization(), old_magnetization + 2 * new_value_sign);
295 ///```
296 ///
297 /// See also [`average_magnetization`](Self::average_magnetization).
298 #[must_use]
299 pub const fn magnetization(&self) -> i64 {
300 // magnetization = n_positive_spins - n_negative_spins
301 // = n_positive_spins - (n_spins - n_positive_spins)
302 // = 2 * n_positive_spins - n_spins
303 2 * self.n_positive_spins - self.n_spins
304 }
305
306 /// The current magnetization averaged over the number of spins.
307 ///
308 /// The value is between `-1.0` and `1.0`.
309 #[must_use]
310 pub fn average_magnetization(&self) -> f64 {
311 // average_magnetization = (n_positive_spins - n_negative_spins) / n_spins
312 // = (n_positive_spins - (n_spins - n_positive_spins)) / n_spins
313 // = (2 * n_positive_spins - n_spins) / n_spins
314 // = 2 * n_positive_spins / n_spins - 1
315 (2.0 * self.n_positive_spins as f64).mul_add(self.inverse_n_spins, -1.0)
316 }
317}