use std::f64::consts::PI;
pub const MU_0: f64 = 4.0 * PI * 1e-7;
pub const E_CHARGE: f64 = 1.602_176_634e-19;
pub const M_E: f64 = 9.109_383_701_5e-31;
pub const M_P: f64 = 1.672_621_923_69e-27;
pub const K_B: f64 = 1.380_649e-23;
pub const ITER_R_MAJOR: f64 = 6.2;
pub const ITER_A_MINOR: f64 = 2.0;
pub const ITER_B_TOR: f64 = 5.3;
pub const ITER_I_PLASMA: f64 = 15e6;
pub const ITER_N_E: f64 = 1.0e20;
pub const ITER_T_E: f64 = 10e3 * E_CHARGE;
#[allow(clippy::float_cmp)]
const _: () = {
assert!(MU_0 > 0.0);
assert!(E_CHARGE > 0.0);
assert!(M_E > 0.0);
assert!(M_P > M_E);
assert!(K_B > 0.0);
assert!(ITER_R_MAJOR > 0.0);
assert!(ITER_A_MINOR > 0.0);
assert!(ITER_B_TOR > 0.0);
assert!(ITER_I_PLASMA > 0.0);
assert!(ITER_N_E > 0.0);
assert!(ITER_T_E > 0.0);
};
pub const NR: usize = 64;
pub const NTHETA: usize = 128;
pub const DT: f64 = 1e-6;
pub const T_TOTAL: f64 = 1e-3;
pub const NT: usize = (T_TOTAL / DT) as usize;
#[derive(Clone, Debug)]
pub struct MhdState {
pub psi: Vec<Vec<f64>>,
pub phi: Vec<Vec<f64>>,
pub omega: Vec<Vec<f64>>,
pub current: Vec<Vec<f64>>,
}
impl Default for MhdState {
fn default() -> Self {
Self::new()
}
}
impl MhdState {
pub fn new() -> Self {
let psi = vec![vec![0.0; NTHETA]; NR];
let phi = vec![vec![0.0; NTHETA]; NR];
let omega = vec![vec![0.0; NTHETA]; NR];
let current = vec![vec![0.0; NTHETA]; NR];
Self {
psi,
phi,
omega,
current,
}
}
}
pub fn initialize_equilibrium(n_fold: usize) -> MhdState {
let mut state = MhdState::new();
let psi_axis = MU_0 * ITER_I_PLASMA * ITER_A_MINOR / (2.0 * PI);
let epsilon = 0.05;
for ir in 0..NR {
let rho = (ir as f64 + 0.5) / NR as f64; for itheta in 0..NTHETA {
let theta = 2.0 * PI * itheta as f64 / NTHETA as f64;
let psi_eq =
psi_axis * (1.0 - rho * rho) * (1.0 + epsilon * (n_fold as f64 * theta).cos());
state.psi[ir][itheta] = psi_eq;
}
}
state.current = laplacian_cylindrical(&state.psi);
state.phi = vec![vec![0.0; NTHETA]; NR]; state.omega = laplacian_cylindrical(&state.phi);
state
}
pub fn laplacian_cylindrical(f: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut result = vec![vec![0.0; NTHETA]; NR];
let dr = 1.0 / NR as f64; let dtheta = 2.0 * PI / NTHETA as f64;
for ir in 1..(NR - 1) {
let rho = (ir as f64 + 0.5) / NR as f64;
for itheta in 0..NTHETA {
let im = (itheta + NTHETA - 1) % NTHETA;
let ip = (itheta + 1) % NTHETA;
let df_dr_plus = (f[ir + 1][itheta] - f[ir][itheta]) / dr;
let df_dr_minus = (f[ir][itheta] - f[ir - 1][itheta]) / dr;
let r_plus = (ir as f64 + 1.0) / NR as f64;
let r_minus = ir as f64 / NR as f64;
let radial = (r_plus * df_dr_plus - r_minus * df_dr_minus) / (rho * dr);
let poloidal =
(f[ir][ip] - 2.0 * f[ir][itheta] + f[ir][im]) / (rho * rho * dtheta * dtheta);
result[ir][itheta] = (radial + poloidal) / (ITER_A_MINOR * ITER_A_MINOR);
}
}
let (axis, rest) = result.split_at_mut(1);
axis[0].copy_from_slice(&rest[0]); let (inner, wall) = rest.split_at_mut(NR - 2);
wall[0].copy_from_slice(&inner[NR - 3]);
result
}
pub fn poisson_bracket(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut result = vec![vec![0.0; NTHETA]; NR];
let dr = 1.0 / NR as f64;
let dtheta = 2.0 * PI / NTHETA as f64;
for ir in 1..(NR - 1) {
for itheta in 0..NTHETA {
let im = (itheta + NTHETA - 1) % NTHETA;
let ip = (itheta + 1) % NTHETA;
let da_dr = (a[ir + 1][itheta] - a[ir - 1][itheta]) / (2.0 * dr);
let db_dr = (b[ir + 1][itheta] - b[ir - 1][itheta]) / (2.0 * dr);
let da_dtheta = (a[ir][ip] - a[ir][im]) / (2.0 * dtheta);
let db_dtheta = (b[ir][ip] - b[ir][im]) / (2.0 * dtheta);
result[ir][itheta] = (da_dr * db_dtheta - da_dtheta * db_dr) / ITER_A_MINOR;
}
}
result
}
pub fn time_step(state: &mut MhdState, eta: f64, nu: f64) {
let bracket_phi_psi = poisson_bracket(&state.phi, &state.psi);
let bracket_phi_omega = poisson_bracket(&state.phi, &state.omega);
let bracket_j_psi = poisson_bracket(&state.current, &state.psi);
let lap_omega = laplacian_cylindrical(&state.omega);
let j0 = compute_equilibrium_current(&state.psi);
for ir in 0..NR {
for itheta in 0..NTHETA {
let rhs =
bracket_phi_psi[ir][itheta] + eta * (state.current[ir][itheta] - j0[ir][itheta]);
state.psi[ir][itheta] += DT * rhs;
}
}
for ir in 0..NR {
for itheta in 0..NTHETA {
let rhs = bracket_phi_omega[ir][itheta]
+ bracket_j_psi[ir][itheta]
+ nu * lap_omega[ir][itheta];
state.omega[ir][itheta] += DT * rhs;
}
}
state.current = laplacian_cylindrical(&state.psi);
state.phi = solve_poisson_jacobi(&state.omega, 50);
}
fn compute_equilibrium_current(psi: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut j0 = vec![vec![0.0; NTHETA]; NR];
for ir in 0..NR {
let avg: f64 = psi[ir].iter().sum::<f64>() / NTHETA as f64;
let dr = 1.0 / NR as f64;
let lap_avg = if ir > 0 && ir < NR - 1 {
let rho = (ir as f64 + 0.5) / NR as f64;
let r_plus = (ir as f64 + 1.0) / NR as f64;
let r_minus = ir as f64 / NR as f64;
let df_plus = (psi[ir + 1].iter().sum::<f64>() / NTHETA as f64 - avg) / dr;
let df_minus = (avg - psi[ir - 1].iter().sum::<f64>() / NTHETA as f64) / dr;
(r_plus * df_plus - r_minus * df_minus) / (rho * dr)
} else {
0.0
};
j0[ir].fill(lap_avg / (ITER_A_MINOR * ITER_A_MINOR));
}
j0
}
fn solve_poisson_jacobi(f: &[Vec<f64>], max_iter: usize) -> Vec<Vec<f64>> {
let mut phi = vec![vec![0.0; NTHETA]; NR];
let mut phi_new = phi.clone();
let dr = 1.0 / NR as f64;
let dtheta = 2.0 * PI / NTHETA as f64;
for _ in 0..max_iter {
for ir in 1..(NR - 1) {
let rho = (ir as f64 + 0.5) / NR as f64;
for itheta in 0..NTHETA {
let im = (itheta + NTHETA - 1) % NTHETA;
let ip = (itheta + 1) % NTHETA;
let coeff_r = 1.0 / (dr * dr);
let coeff_theta = 1.0 / (rho * rho * dtheta * dtheta);
let denom = 2.0 * (coeff_r + coeff_theta);
let rhs = f[ir][itheta] * (ITER_A_MINOR * ITER_A_MINOR);
phi_new[ir][itheta] = (coeff_r * (phi[ir + 1][itheta] + phi[ir - 1][itheta])
+ coeff_theta * (phi[ir][ip] + phi[ir][im])
- rhs)
/ denom;
}
}
std::mem::swap(&mut phi, &mut phi_new);
}
phi
}
pub fn magnetic_energy(state: &MhdState) -> f64 {
let mut energy = 0.0;
let dr = 1.0 / NR as f64;
let dtheta = 2.0 * PI / NTHETA as f64;
for ir in 1..(NR - 1) {
let rho = (ir as f64 + 0.5) / NR as f64;
let r = rho * ITER_A_MINOR;
for itheta in 0..NTHETA {
let im = (itheta + NTHETA - 1) % NTHETA;
let ip = (itheta + 1) % NTHETA;
let dpsi_dr =
(state.psi[ir + 1][itheta] - state.psi[ir - 1][itheta]) / (2.0 * dr * ITER_A_MINOR);
let dpsi_dtheta = (state.psi[ir][ip] - state.psi[ir][im]) / (2.0 * dtheta);
let grad_psi_sq = dpsi_dr * dpsi_dr + (dpsi_dtheta / r) * (dpsi_dtheta / r);
energy += grad_psi_sq * r * dr * ITER_A_MINOR * dtheta;
}
}
energy / (2.0 * MU_0)
}
pub fn kinetic_energy(state: &MhdState) -> f64 {
let mut energy = 0.0;
let dr = 1.0 / NR as f64;
let dtheta = 2.0 * PI / NTHETA as f64;
for ir in 1..(NR - 1) {
let rho = (ir as f64 + 0.5) / NR as f64;
let r = rho * ITER_A_MINOR;
for itheta in 0..NTHETA {
let im = (itheta + NTHETA - 1) % NTHETA;
let ip = (itheta + 1) % NTHETA;
let dphi_dr =
(state.phi[ir + 1][itheta] - state.phi[ir - 1][itheta]) / (2.0 * dr * ITER_A_MINOR);
let dphi_dtheta = (state.phi[ir][ip] - state.phi[ir][im]) / (2.0 * dtheta);
let grad_phi_sq = dphi_dr * dphi_dr + (dphi_dtheta / r) * (dphi_dtheta / r);
energy += grad_phi_sq * r * dr * ITER_A_MINOR * dtheta;
}
}
energy / 2.0
}
pub fn symmetry_amplitude(state: &MhdState, n: usize) -> f64 {
let mut total_amp = 0.0;
let dtheta = 2.0 * PI / NTHETA as f64;
for ir in 0..NR {
let mut real = 0.0;
let mut imag = 0.0;
for itheta in 0..NTHETA {
let theta = itheta as f64 * dtheta;
real += state.psi[ir][itheta] * (n as f64 * theta).cos();
imag += state.psi[ir][itheta] * (n as f64 * theta).sin();
}
let amp = (real * real + imag * imag).sqrt() * dtheta / (2.0 * PI);
total_amp += amp;
}
total_amp / NR as f64
}
pub fn turbulence_level(state: &MhdState) -> f64 {
let mut rms = 0.0;
for ir in 0..NR {
let avg: f64 = state.psi[ir].iter().sum::<f64>() / NTHETA as f64;
let var: f64 = state.psi[ir]
.iter()
.map(|&x| (x - avg) * (x - avg))
.sum::<f64>()
/ NTHETA as f64;
rms += var.sqrt();
}
rms / NR as f64
}
pub fn confinement_quality(state: &MhdState) -> f64 {
let em = magnetic_energy(state);
let ek = kinetic_energy(state);
if ek > 1e-30 {
em / ek
} else {
f64::INFINITY
}
}
pub fn spitzer_resistivity() -> f64 {
let z_eff = 1.5;
let ln_lambda = 17.0;
let t_ev = ITER_T_E / E_CHARGE;
5.2e-5 * z_eff * ln_lambda / t_ev.powf(1.5)
}
pub fn kinematic_viscosity() -> f64 {
let reynolds = 1e6;
let sound_speed = (K_B * ITER_T_E / M_P).sqrt();
sound_speed * ITER_A_MINOR / reynolds
}
pub fn run_simulation(n_fold: usize) -> SimulationResult {
let mut state = initialize_equilibrium(n_fold);
let eta = spitzer_resistivity();
let nu = kinematic_viscosity();
let mut mag_energy = Vec::with_capacity(NT / 100 + 1);
let mut kin_energy = Vec::with_capacity(NT / 100 + 1);
let mut turb = Vec::with_capacity(NT / 100 + 1);
let mut confinement = Vec::with_capacity(NT / 100 + 1);
let mut sym_amp = Vec::with_capacity(NT / 100 + 1);
for step in 0..NT {
time_step(&mut state, eta, nu);
if step % 100 == 0 {
mag_energy.push(magnetic_energy(&state));
kin_energy.push(kinetic_energy(&state));
turb.push(turbulence_level(&state));
confinement.push(confinement_quality(&state));
sym_amp.push(symmetry_amplitude(&state, n_fold));
}
}
SimulationResult {
n_fold,
mag_energy,
kin_energy,
turbulence: turb,
confinement,
symmetry_amplitude: sym_amp,
}
}
#[derive(Debug)]
pub struct SimulationResult {
pub n_fold: usize,
pub mag_energy: Vec<f64>,
pub kin_energy: Vec<f64>,
pub turbulence: Vec<f64>,
pub confinement: Vec<f64>,
pub symmetry_amplitude: Vec<f64>,
}
impl SimulationResult {
pub fn avg_magnetic_energy(&self) -> f64 {
self.mag_energy.iter().sum::<f64>() / self.mag_energy.len() as f64
}
pub fn avg_kinetic_energy(&self) -> f64 {
self.kin_energy.iter().sum::<f64>() / self.kin_energy.len() as f64
}
pub fn avg_turbulence(&self) -> f64 {
self.turbulence.iter().sum::<f64>() / self.turbulence.len() as f64
}
pub fn avg_confinement(&self) -> f64 {
self.confinement.iter().sum::<f64>() / self.confinement.len() as f64
}
pub fn final_symmetry_amplitude(&self) -> f64 {
*self.symmetry_amplitude.last().unwrap_or(&0.0)
}
pub fn kinetic_growth_rate(&self) -> f64 {
if self.kin_energy.len() < 10 {
return 0.0;
}
let dt_sample = DT * 100.0; let e0 = self.kin_energy[0].max(1e-30);
let e1 = self.kin_energy[self.kin_energy.len() - 1].max(1e-30);
let t_total = dt_sample * (self.kin_energy.len() - 1) as f64;
(e1.ln() - e0.ln()) / t_total
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initialization() {
let state = initialize_equilibrium(13);
assert_eq!(state.psi.len(), NR);
assert_eq!(state.psi[0].len(), NTHETA);
}
#[test]
fn test_laplacian_of_constant_is_zero() {
let f = vec![vec![1.0; NTHETA]; NR];
let lap = laplacian_cylindrical(&f);
let max_lap: f64 = lap
.iter()
.flat_map(|row| row.iter())
.copied()
.map(f64::abs)
.fold(0.0, f64::max);
assert!(
max_lap < 1e-10,
"Laplacian of constant = {} (should be ~0)",
max_lap
);
}
#[test]
fn test_spitzer_resistivity_positive() {
let eta = spitzer_resistivity();
assert!(eta > 0.0, "Resistivity must be positive");
assert!(eta < 1.0, "Resistivity should be < 1 Ω·m for fusion plasma");
}
#[test]
fn test_kinematic_viscosity_positive() {
let nu = kinematic_viscosity();
assert!(nu > 0.0, "Viscosity must be positive");
}
#[test]
fn test_magnetic_energy_positive() {
let state = initialize_equilibrium(13);
let em = magnetic_energy(&state);
assert!(em > 0.0, "Magnetic energy must be positive");
}
#[test]
fn test_confinement_quality_finite() {
let state = initialize_equilibrium(13);
let q = confinement_quality(&state);
assert!(!q.is_nan(), "Confinement quality must not be NaN");
}
#[test]
fn test_simulation_runs() {
let result = run_simulation(13);
assert!(!result.mag_energy.is_empty());
assert!(result.avg_magnetic_energy() > 0.0);
}
#[test]
fn test_all_symmetries_run() {
for n in [12, 13, 18] {
let result = run_simulation(n);
assert!(result.avg_magnetic_energy() > 0.0, "n={} failed", n);
}
}
#[test]
fn test_12_vs_13_vs_18() {
let r12 = run_simulation(12);
let r13 = run_simulation(13);
let r18 = run_simulation(18);
println!("\n=== MHD SIMULATION RESULTS ===");
println!(
"Configuration | Avg Turbulence | Avg Confinement | Kinetic Growth | Final Symmetry"
);
println!(
"--------------|----------------|-----------------|----------------|---------------"
);
println!(
"12-fold | {:14.6e} | {:15.2e} | {:14.6e} | {:14.6e}",
r12.avg_turbulence(),
r12.avg_confinement(),
r12.kinetic_growth_rate(),
r12.final_symmetry_amplitude()
);
println!(
"13-fold | {:14.6e} | {:15.2e} | {:14.6e} | {:14.6e}",
r13.avg_turbulence(),
r13.avg_confinement(),
r13.kinetic_growth_rate(),
r13.final_symmetry_amplitude()
);
println!(
"18-fold | {:14.6e} | {:15.2e} | {:14.6e} | {:14.6e}",
r18.avg_turbulence(),
r18.avg_confinement(),
r18.kinetic_growth_rate(),
r18.final_symmetry_amplitude()
);
assert!(r12.avg_turbulence().is_finite());
assert!(r13.avg_turbulence().is_finite());
assert!(r18.avg_turbulence().is_finite());
}
}