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}