use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
use super::functions::*;
use super::functions::{BOLTZMANN_K, PLANCK_H};
#[allow(dead_code)]
pub struct RenormalizationGroup {
pub initial_coupling: f64,
pub scale_factor: f64,
}
impl RenormalizationGroup {
pub fn new(initial_coupling: f64, scale_factor: f64) -> Self {
Self {
initial_coupling,
scale_factor,
}
}
pub fn iterate<F>(&self, rg_map: F, n_steps: usize) -> Vec<f64>
where
F: Fn(f64) -> f64,
{
let mut trajectory = vec![self.initial_coupling];
let mut g = self.initial_coupling;
for _ in 0..n_steps {
g = rg_map(g);
trajectory.push(g);
}
trajectory
}
pub fn find_fixed_point<F>(&self, rg_map: &F, tol: f64, max_iter: usize) -> (f64, bool)
where
F: Fn(f64) -> f64,
{
let mut g = self.initial_coupling;
for _ in 0..max_iter {
let g_new = rg_map(g);
let residual = g_new - g;
if residual.abs() < tol {
return (g_new, true);
}
g = g + 0.5 * residual;
}
let converged = (rg_map(g) - g).abs() < tol;
(g, converged)
}
pub fn is_stable_fixed_point<F>(&self, rg_map: &F, g_star: f64) -> Option<bool>
where
F: Fn(f64) -> f64,
{
let dg = g_star.abs() * 1e-6 + 1e-10;
if dg == 0.0 {
return None;
}
let deriv = (rg_map(g_star + dg) - rg_map(g_star - dg)) / (2.0 * dg);
Some(deriv.abs() < 1.0)
}
pub fn scaling_exponent<F>(&self, rg_map: &F, g_star: f64) -> f64
where
F: Fn(f64) -> f64,
{
let dg = g_star.abs() * 1e-6 + 1e-10;
let deriv = (rg_map(g_star + dg) - rg_map(g_star - dg)) / (2.0 * dg);
if self.scale_factor > 1.0 && deriv.abs() > 0.0 {
deriv.abs().ln() / self.scale_factor.ln()
} else {
0.0
}
}
}
#[allow(dead_code)]
pub struct GrandCanonicalEnsemble {
pub energy_levels: Vec<f64>,
pub temperature: f64,
pub chemical_potential: f64,
pub is_fermionic: bool,
}
impl GrandCanonicalEnsemble {
pub fn new(energy_levels: Vec<f64>, temperature: f64, mu: f64, fermionic: bool) -> Self {
Self {
energy_levels,
temperature,
chemical_potential: mu,
is_fermionic: fermionic,
}
}
pub fn beta(&self) -> f64 {
1.0 / (BOLTZMANN_K * self.temperature)
}
pub fn mean_occupation(&self, k: usize) -> f64 {
let x = self.beta() * (self.energy_levels[k] - self.chemical_potential);
if self.is_fermionic {
1.0 / (x.exp() + 1.0)
} else {
if x <= 0.0 {
f64::INFINITY
} else {
1.0 / (x.exp() - 1.0)
}
}
}
pub fn grand_potential(&self) -> f64 {
let b = self.beta();
let sum: f64 = self
.energy_levels
.iter()
.map(|&eps| {
let x = b * (self.chemical_potential - eps);
if self.is_fermionic {
(1.0 + x.exp()).ln()
} else if x < -700.0 {
0.0
} else {
-(1.0 - x.exp()).abs().ln()
}
})
.sum();
-BOLTZMANN_K * self.temperature * sum
}
pub fn mean_particle_number(&self) -> f64 {
(0..self.energy_levels.len())
.filter_map(|k| {
let n = self.mean_occupation(k);
if n.is_finite() {
Some(n)
} else {
None
}
})
.sum()
}
pub fn mean_energy(&self) -> f64 {
self.energy_levels
.iter()
.enumerate()
.filter_map(|(k, &eps)| {
let n = self.mean_occupation(k);
if n.is_finite() {
Some(eps * n)
} else {
None
}
})
.sum()
}
}
#[allow(dead_code)]
pub struct CorrelationFunction {
pub samples: Vec<f64>,
}
impl CorrelationFunction {
pub fn new(samples: Vec<f64>) -> Self {
Self { samples }
}
pub fn mean(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
self.samples.iter().sum::<f64>() / self.samples.len() as f64
}
pub fn variance(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
let m = self.mean();
let m2 = self.samples.iter().map(|&x| x * x).sum::<f64>() / self.samples.len() as f64;
m2 - m * m
}
pub fn connected_correlator(&self, lag: usize) -> f64 {
let n = self.samples.len();
if lag >= n {
return 0.0;
}
let m = self.mean();
let count = (n - lag) as f64;
let corr: f64 = (0..n - lag)
.map(|i| self.samples[i] * self.samples[i + lag])
.sum::<f64>()
/ count;
corr - m * m
}
pub fn integrated_autocorrelation_time(&self, max_lag: usize) -> f64 {
let c0 = self.connected_correlator(0);
if c0.abs() < 1e-300 {
return 0.5;
}
let sum: f64 = (1..max_lag.min(self.samples.len()))
.map(|tau| self.connected_correlator(tau) / c0)
.sum();
0.5 + sum
}
pub fn susceptibility(&self) -> f64 {
self.variance() * self.samples.len() as f64
}
}
pub struct Ensemble {
pub energies: Vec<f64>,
pub temperature: f64,
pub degeneracies: Vec<u32>,
}
impl Ensemble {
pub fn new(energies: Vec<f64>, temperature: f64) -> Self {
let n = energies.len();
Self {
energies,
temperature,
degeneracies: vec![1; n],
}
}
pub fn with_degeneracies(energies: Vec<f64>, degeneracies: Vec<u32>, temperature: f64) -> Self {
Self {
energies,
temperature,
degeneracies,
}
}
pub fn beta(&self) -> f64 {
1.0 / (BOLTZMANN_K * self.temperature)
}
pub fn partition_function(&self) -> f64 {
let beta = self.beta();
self.energies
.iter()
.zip(self.degeneracies.iter())
.map(|(&e, &g)| (g as f64) * (-beta * e).exp())
.sum()
}
pub fn boltzmann_factor(&self, energy: f64) -> f64 {
(-self.beta() * energy).exp()
}
pub fn probability(&self, state_idx: usize) -> f64 {
let z = self.partition_function();
if z == 0.0 {
return 0.0;
}
let e = self.energies[state_idx];
let g = self.degeneracies[state_idx] as f64;
g * self.boltzmann_factor(e) / z
}
pub fn mean_energy(&self) -> f64 {
self.energies
.iter()
.enumerate()
.map(|(i, &e)| e * self.probability(i))
.sum()
}
pub fn entropy(&self) -> f64 {
let s: f64 = self
.energies
.iter()
.enumerate()
.filter_map(|(i, _)| {
let p = self.probability(i);
if p > 1e-300 {
Some(-p * p.ln())
} else {
None
}
})
.sum();
BOLTZMANN_K * s
}
pub fn free_energy(&self) -> f64 {
let z = self.partition_function();
if z <= 0.0 {
return f64::INFINITY;
}
-BOLTZMANN_K * self.temperature * z.ln()
}
pub fn heat_capacity(&self) -> f64 {
let dt = self.temperature * 1e-4;
let e_high = {
let e_high = Ensemble::with_degeneracies(
self.energies.clone(),
self.degeneracies.clone(),
self.temperature + dt,
);
e_high.mean_energy()
};
let e_low = {
let e_low = Ensemble::with_degeneracies(
self.energies.clone(),
self.degeneracies.clone(),
self.temperature - dt,
);
e_low.mean_energy()
};
(e_high - e_low) / (2.0 * dt)
}
pub fn max_prob_state(&self) -> usize {
self.energies
.iter()
.enumerate()
.map(|(i, _)| (i, self.probability(i)))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0)
}
}
pub struct MeanFieldIsing {
pub j_coupling: f64,
pub h_field: f64,
pub coordination: f64,
pub temperature: f64,
}
impl MeanFieldIsing {
pub fn new(j: f64, h: f64, z: f64, temp: f64) -> Self {
Self {
j_coupling: j,
h_field: h,
coordination: z,
temperature: temp,
}
}
pub fn critical_temperature(&self) -> f64 {
self.coordination * self.j_coupling / BOLTZMANN_K
}
pub fn reduced_temperature(&self) -> f64 {
let tc = self.critical_temperature();
(self.temperature - tc) / tc
}
fn residual(&self, m: f64) -> f64 {
let b = 1.0 / (BOLTZMANN_K * self.temperature);
let arg = b * (self.h_field + self.coordination * self.j_coupling * m);
m - arg.tanh()
}
pub fn solve(&self, initial_m: f64, max_iter: usize, tol: f64) -> (f64, bool) {
let mut m = initial_m;
for _ in 0..max_iter {
let b = 1.0 / (BOLTZMANN_K * self.temperature);
let arg = b * (self.h_field + self.coordination * self.j_coupling * m);
let m_new = arg.tanh();
if (m_new - m).abs() < tol {
return (m_new, true);
}
m = 0.8 * m + 0.2 * m_new;
}
let converged = self.residual(m).abs() < tol;
(m, converged)
}
pub fn find_all_solutions(&self) -> Vec<f64> {
let seeds = [-0.999, -0.5, 0.0, 0.5, 0.999];
let mut solutions: Vec<f64> = Vec::new();
for &seed in &seeds {
let (m, converged) = self.solve(seed, 2000, 1e-10);
if converged {
let is_new = solutions
.iter()
.all(|&existing| (existing - m).abs() > 1e-6);
if is_new {
solutions.push(m);
}
}
}
solutions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
solutions
}
pub fn free_energy_density(&self, m: f64) -> f64 {
let b = 1.0 / (BOLTZMANN_K * self.temperature);
let interaction = -0.5 * self.coordination * self.j_coupling * m * m;
let field_term = -self.h_field * m;
let h_eff = self.h_field + self.coordination * self.j_coupling * m;
let entropy_term = if (b * h_eff).abs() < 700.0 {
-(b * h_eff).cosh().ln() / b
} else {
-(b * h_eff).abs() / b
};
interaction + field_term + entropy_term
}
}
#[derive(Debug, Clone)]
pub struct CriticalExponents {
pub name: &'static str,
pub dimension: u8,
pub alpha: f64,
pub beta: f64,
pub gamma: f64,
pub delta: f64,
pub nu: f64,
pub eta: f64,
}
impl CriticalExponents {
pub fn check_widom(&self) -> bool {
let lhs = self.gamma;
let rhs = self.beta * (self.delta - 1.0);
(lhs - rhs).abs() < 0.01
}
pub fn check_rushbrooke(&self) -> bool {
let sum = self.alpha + 2.0 * self.beta + self.gamma;
(sum - 2.0).abs() < 0.01
}
pub fn check_fisher(&self) -> bool {
let lhs = self.gamma;
let rhs = self.nu * (2.0 - self.eta);
(lhs - rhs).abs() < 0.05
}
}
pub struct LandauFreeEnergy {
pub a: f64,
pub b: f64,
pub c: f64,
pub h: f64,
}
impl LandauFreeEnergy {
pub fn new_second_order(a: f64, b: f64, h: f64) -> Self {
Self { a, b, c: 0.0, h }
}
pub fn new_tricritical(a: f64, c: f64, h: f64) -> Self {
Self { a, b: 0.0, c, h }
}
pub fn eval(&self, m: f64, t: f64) -> f64 {
self.a * t * m * m + self.b * m * m * m * m + self.c * m.powf(6.0) + self.h * m
}
pub fn deriv(&self, m: f64, t: f64) -> f64 {
2.0 * self.a * t * m + 4.0 * self.b * m * m * m + 6.0 * self.c * m.powf(5.0) + self.h
}
pub fn deriv2(&self, m: f64, t: f64) -> f64 {
2.0 * self.a * t + 12.0 * self.b * m * m + 30.0 * self.c * m.powf(4.0)
}
pub fn minimize(&self, t: f64, initial_m: f64, tol: f64, max_iter: usize) -> (f64, f64, bool) {
let mut m = initial_m;
for _ in 0..max_iter {
let f1 = self.deriv(m, t);
let f2 = self.deriv2(m, t);
if f2.abs() < 1e-300 {
break;
}
let step = -f1 / f2;
let step = step.clamp(-0.5, 0.5);
m += step;
if step.abs() < tol {
return (m, self.eval(m, t), true);
}
}
(m, self.eval(m, t), self.deriv(m, t).abs() < tol * 100.0)
}
pub fn equilibrium_order_parameter(&self, t: f64) -> f64 {
if t >= 0.0 || self.b <= 0.0 {
let (m, _, _) = self.minimize(t, 0.01, 1e-10, 1000);
return m;
}
let m_mf = (-self.a * t / (2.0 * self.b)).sqrt();
let (m_pos, f_pos, _) = self.minimize(t, m_mf, 1e-10, 1000);
let (m_neg, f_neg, _) = self.minimize(t, -m_mf, 1e-10, 1000);
if f_pos <= f_neg {
m_pos
} else {
m_neg
}
}
pub fn spontaneous_magnetization_curve(&self, n_points: usize) -> Vec<(f64, f64)> {
(0..n_points)
.map(|i| {
let t = -1.0 + (i as f64) / (n_points as f64);
let m = self.equilibrium_order_parameter(t);
(t, m.abs())
})
.collect()
}
}
pub struct CanonicalEnsemble {
inner: Ensemble,
}
impl CanonicalEnsemble {
pub fn new(energies: Vec<f64>, temperature: f64) -> Self {
Self {
inner: Ensemble::new(energies, temperature),
}
}
pub fn with_degeneracies(energies: Vec<f64>, degeneracies: Vec<u32>, temperature: f64) -> Self {
Self {
inner: Ensemble::with_degeneracies(energies, degeneracies, temperature),
}
}
pub fn partition_function(&self) -> f64 {
self.inner.partition_function()
}
pub fn free_energy(&self) -> f64 {
self.inner.free_energy()
}
pub fn mean_energy(&self) -> f64 {
self.inner.mean_energy()
}
pub fn entropy(&self) -> f64 {
(self.inner.mean_energy() - self.inner.free_energy()) / self.inner.temperature
}
pub fn heat_capacity(&self) -> f64 {
self.inner.heat_capacity()
}
pub fn energy_variance(&self) -> f64 {
let mean_e = self.inner.mean_energy();
let mean_e2: f64 = self
.inner
.energies
.iter()
.enumerate()
.map(|(i, &e)| e * e * self.inner.probability(i))
.sum();
mean_e2 - mean_e * mean_e
}
pub fn probabilities(&self) -> Vec<f64> {
(0..self.inner.energies.len())
.map(|i| self.inner.probability(i))
.collect()
}
pub fn kl_from_uniform(&self) -> f64 {
let n = self.inner.energies.len() as f64;
if n == 0.0 {
return 0.0;
}
(0..self.inner.energies.len())
.filter_map(|i| {
let p = self.inner.probability(i);
if p > 1e-300 {
Some(p * (p * n).ln())
} else {
None
}
})
.sum()
}
}
#[allow(dead_code)]
pub struct VanDerWaalsGas {
pub a: f64,
pub b: f64,
pub temperature: f64,
}
impl VanDerWaalsGas {
pub fn new(a: f64, b: f64, temperature: f64) -> Self {
Self { a, b, temperature }
}
pub fn pressure(&self, v: f64) -> f64 {
if v <= self.b {
return f64::INFINITY;
}
BOLTZMANN_K * self.temperature / (v - self.b) - self.a / (v * v)
}
pub fn critical_temperature(&self) -> f64 {
8.0 * self.a / (27.0 * BOLTZMANN_K * self.b)
}
pub fn critical_pressure(&self) -> f64 {
self.a / (27.0 * self.b * self.b)
}
pub fn critical_volume(&self) -> f64 {
3.0 * self.b
}
pub fn reduced_temperature(&self) -> f64 {
self.temperature / self.critical_temperature()
}
pub fn is_supercritical(&self) -> bool {
self.temperature >= self.critical_temperature()
}
pub fn critical_compressibility() -> f64 {
3.0 / 8.0
}
}
#[allow(dead_code)]
pub struct VirialGas {
pub b2: f64,
pub b3: f64,
pub temperature: f64,
}
impl VirialGas {
pub fn new(b2: f64, b3: f64, temperature: f64) -> Self {
Self {
b2,
b3,
temperature,
}
}
pub fn pressure(&self, density: f64) -> f64 {
BOLTZMANN_K
* self.temperature
* density
* (1.0 + self.b2 * density + self.b3 * density * density)
}
pub fn compressibility_factor(&self, density: f64) -> f64 {
1.0 + self.b2 * density + self.b3 * density * density
}
pub fn hard_sphere_b2(sigma: f64) -> f64 {
(2.0 / 3.0) * std::f64::consts::PI * sigma * sigma * sigma
}
pub fn is_above_boyle_temperature(&self) -> bool {
self.b2 >= 0.0
}
}
pub struct IsingModel {
pub spins: Vec<Vec<i8>>,
pub j_coupling: f64,
pub temperature: f64,
}
impl IsingModel {
pub fn new(n: usize, j: f64, temp: f64) -> Self {
let mut rng_state: u64 = 12345;
let spins = (0..n)
.map(|_| {
(0..n)
.map(|_| {
let r = Self::lcg_next(&mut rng_state);
if r < 0.5 {
1i8
} else {
-1i8
}
})
.collect()
})
.collect();
Self {
spins,
j_coupling: j,
temperature: temp,
}
}
pub fn energy(&self) -> f64 {
let n = self.spins.len();
let mut e = 0.0;
for i in 0..n {
for j in 0..n {
let s = self.spins[i][j] as f64;
let s_right = self.spins[i][(j + 1) % n] as f64;
let s_down = self.spins[(i + 1) % n][j] as f64;
e -= self.j_coupling * s * (s_right + s_down);
}
}
e
}
pub fn magnetization(&self) -> f64 {
let n = self.spins.len();
let total: i64 = self
.spins
.iter()
.flat_map(|row| row.iter())
.map(|&s| s as i64)
.sum();
(total.abs() as f64) / ((n * n) as f64)
}
pub fn metropolis_step(&mut self, rng: &mut u64) {
let n = self.spins.len();
let r1 = Self::lcg_next(rng);
let r2 = Self::lcg_next(rng);
let r3 = Self::lcg_next(rng);
let i = (r1 * n as f64) as usize % n;
let j = (r2 * n as f64) as usize % n;
let s = self.spins[i][j] as f64;
let neighbors: f64 = [
self.spins[(i + n - 1) % n][j] as f64,
self.spins[(i + 1) % n][j] as f64,
self.spins[i][(j + n - 1) % n] as f64,
self.spins[i][(j + 1) % n] as f64,
]
.iter()
.sum();
let delta_e = 2.0 * self.j_coupling * s * neighbors;
let accept = if delta_e <= 0.0 {
true
} else {
let prob = (-delta_e / (BOLTZMANN_K * self.temperature)).exp();
r3 < prob
};
if accept {
self.spins[i][j] = -self.spins[i][j];
}
}
fn lcg_next(state: &mut u64) -> f64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(*state >> 33) as f64 / (u32::MAX as f64 + 1.0)
}
}
pub struct CriticalExponentTable {
pub entries: Vec<CriticalExponents>,
}
impl CriticalExponentTable {
pub fn standard() -> Self {
Self {
entries: vec![
CriticalExponents {
name: "2D Ising",
dimension: 2,
alpha: 0.0,
beta: 0.125,
gamma: 1.75,
delta: 15.0,
nu: 1.0,
eta: 0.25,
},
CriticalExponents {
name: "3D Ising",
dimension: 3,
alpha: 0.110,
beta: 0.326,
gamma: 1.237,
delta: 4.789,
nu: 0.630,
eta: 0.036,
},
CriticalExponents {
name: "3D XY",
dimension: 3,
alpha: -0.013,
beta: 0.346,
gamma: 1.316,
delta: 4.780,
nu: 0.671,
eta: 0.038,
},
CriticalExponents {
name: "3D Heisenberg",
dimension: 3,
alpha: -0.122,
beta: 0.365,
gamma: 1.386,
delta: 4.803,
nu: 0.707,
eta: 0.033,
},
CriticalExponents {
name: "Mean Field",
dimension: 4,
alpha: 0.0,
beta: 0.5,
gamma: 1.0,
delta: 3.0,
nu: 0.5,
eta: 0.0,
},
CriticalExponents {
name: "2D Potts q=3",
dimension: 2,
alpha: 0.333,
beta: 0.111,
gamma: 1.444,
delta: 14.0,
nu: 0.833,
eta: 0.148,
},
],
}
}
pub fn find(&self, name: &str) -> Option<&CriticalExponents> {
self.entries.iter().find(|e| e.name == name)
}
pub fn validate_scaling_relations(&self) -> Vec<(&'static str, bool, bool, bool)> {
self.entries
.iter()
.map(|e| {
(
e.name,
e.check_widom(),
e.check_rushbrooke(),
e.check_fisher(),
)
})
.collect()
}
}
pub struct IdealGas {
pub n_particles: u64,
pub temperature: f64,
pub volume: f64,
}
impl IdealGas {
pub fn new(n: u64, t: f64, v: f64) -> Self {
Self {
n_particles: n,
temperature: t,
volume: v,
}
}
pub fn pressure(&self) -> f64 {
(self.n_particles as f64) * BOLTZMANN_K * self.temperature / self.volume
}
pub fn mean_kinetic_energy(&self) -> f64 {
1.5 * BOLTZMANN_K * self.temperature
}
pub fn rms_speed(&self, mass: f64) -> f64 {
(3.0 * BOLTZMANN_K * self.temperature / mass).sqrt()
}
pub fn entropy(&self) -> f64 {
let n = self.n_particles as f64;
let m = 1.6726e-27_f64;
let mean_e = self.mean_kinetic_energy();
let lambda_arg = 4.0 * std::f64::consts::PI * m * mean_e / (3.0 * n * PLANCK_H * PLANCK_H);
if lambda_arg <= 0.0 {
return 0.0;
}
BOLTZMANN_K * n * ((self.volume / n) * lambda_arg.powf(1.5) + 2.5)
}
}
pub struct IsingModel1D {
pub n_sites: usize,
pub j_coupling: f64,
pub h_field: f64,
pub temperature: f64,
}
impl IsingModel1D {
pub fn new(n: usize, j: f64, h: f64, temp: f64) -> Self {
Self {
n_sites: n,
j_coupling: j,
h_field: h,
temperature: temp,
}
}
pub fn beta(&self) -> f64 {
1.0 / (BOLTZMANN_K * self.temperature)
}
pub fn eigenvalues(&self) -> (f64, f64) {
let b = self.beta();
let bj = b * self.j_coupling;
let bh = b * self.h_field;
let exp_bj = bj.exp();
let cosh_bh = bh.cosh();
let sinh_bh = bh.sinh();
let disc = sinh_bh * sinh_bh + (-4.0 * bj).exp();
let sqrt_disc = disc.sqrt();
let lambda_plus = exp_bj * (cosh_bh + sqrt_disc);
let lambda_minus = exp_bj * (cosh_bh - sqrt_disc);
(lambda_plus, lambda_minus)
}
pub fn partition_function(&self) -> f64 {
let (lp, lm) = self.eigenvalues();
let n = self.n_sites as f64;
lp.powf(n) + lm.powf(n)
}
pub fn free_energy_per_site(&self) -> f64 {
let z = self.partition_function();
if z <= 0.0 {
return f64::INFINITY;
}
-BOLTZMANN_K * self.temperature * z.ln() / (self.n_sites as f64)
}
pub fn zero_field_partition_function(&self) -> f64 {
let b = self.beta();
let bj = b * self.j_coupling;
(2.0 * bj.cosh()).powf(self.n_sites as f64)
}
pub fn magnetization_per_site(&self) -> f64 {
let dh = self.j_coupling.abs() * 1e-6 + 1e-30;
let z_plus = IsingModel1D::new(
self.n_sites,
self.j_coupling,
self.h_field + dh,
self.temperature,
)
.partition_function();
let z_minus = IsingModel1D::new(
self.n_sites,
self.j_coupling,
self.h_field - dh,
self.temperature,
)
.partition_function();
let z = self.partition_function();
if z <= 0.0 {
return 0.0;
}
(z_plus - z_minus) / (2.0 * dh * self.beta() * z)
}
pub fn susceptibility_per_site(&self) -> f64 {
let dh = self.j_coupling.abs() * 1e-4 + 1e-30;
let m_plus = IsingModel1D::new(
self.n_sites,
self.j_coupling,
self.h_field + dh,
self.temperature,
)
.magnetization_per_site();
let m_minus = IsingModel1D::new(
self.n_sites,
self.j_coupling,
self.h_field - dh,
self.temperature,
)
.magnetization_per_site();
(m_plus - m_minus) / (2.0 * dh)
}
}