spintronics 0.3.2

Pure Rust library for simulating spin dynamics, spin current generation, and conversion phenomena in magnetic and topological materials
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
//! NIST muMAG Standard Problem #3 — single-domain limit for a Permalloy cube.
//!
//! ## Problem statement (muMAG specification)
//!
//! Find the critical cube edge length L_c such that:
//! * For L < L_c: the equilibrium remanence state is a near-uniform "flower" state
//!   (exchange energy dominates, keeps spins aligned).
//! * For L > L_c: the minimum-energy state is a vortex (flux-closure) configuration
//!   (magnetostatic energy dominates, drives spins into a curl pattern).
//!
//! The crossover is governed by the balance between exchange stiffness A [J/m] and
//! magnetostatic self-energy (proportional to μ₀ Ms²). The relevant length scale is
//! the exchange length:
//! ```text
//! l_ex = sqrt( A / (½ μ₀ Ms²) )
//! ```
//! For Permalloy (Ms = 800 kA/m, A = 13 pJ/m):
//! ```text
//! l_ex ≈ sqrt(13e-12 / (0.5 × 4π×10⁻⁷ × (800e3)²)) ≈ 5.69 nm
//! ```
//!
//! The muMAG reference value is L_c ≈ 8.47 l_ex ≈ 48 nm.
//!
//! ## Material parameters (muMAG specification)
//!
//! | Parameter | Value         | Notes                          |
//! |-----------|---------------|--------------------------------|
//! | Ms        | 800 kA/m      | Permalloy saturation mag.      |
//! | A         | 13 pJ/m       | Exchange stiffness             |
//! | K         | 0             | No crystalline anisotropy      |
//! | α         | 0.5           | High damping for fast relaxation |
//!
//! ## Validation approach
//!
//! For two cube sizes (L ≪ L_c and L ≫ L_c), relax both the flower and the vortex
//! initial states using LLG with high damping. Compare final energies: the
//! lower-energy state identifies the stable phase. The validation passes if:
//! 1. For L < L_c (e.g. L = 4 l_ex): flower energy < vortex energy.
//! 2. For L > L_c (e.g. L = 12 l_ex): vortex energy ≤ flower energy.
//!
//! Note: with a coarse grid the quantitative crossover point may shift slightly
//! from the theoretical 8.47 l_ex. The qualitative ordering of the two extremes
//! (L ≪ L_c and L ≫ L_c) should be robust.
//!
//! # References
//! - muMAG Standard Problems: <https://www.ctcms.nist.gov/~rdm/mumag.org.html>
//! - R. D. McMichael and M. J. Donahue, IEEE Trans. Magn. 33, 4167 (1997)

use crate::constants::MU_0;
use crate::error::{invalid_param, Result};
use crate::material::Ferromagnet;
use crate::micromagnetics::grid::{GridConfig, MicromagneticGrid};
use crate::validation::experimental::ValidationResult;
use crate::vector3::Vector3;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

// ─── StableState ─────────────────────────────────────────────────────────────

/// Which equilibrium state is energetically stable for a given cube size.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum StableState {
    /// Near-uniform magnetization aligned with an easy direction.
    /// Energetically favoured for small cubes (L ≪ L_c).
    Flower,
    /// Flux-closure vortex with closed-loop in-plane magnetization.
    /// Energetically favoured for large cubes (L ≫ L_c).
    Vortex,
}

// ─── Sp3Config ───────────────────────────────────────────────────────────────

/// Configuration for the SP#3 single-domain limit simulation.
///
/// For accuracy, the cell size should be ≲ l_ex (≈ 5.7 nm for Permalloy).
/// The default uses 6 nm cells and a coarse 4×4×4 grid for fast tests.
/// For production results, decrease `cell_size` and increase `relax_steps`.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Sp3Config {
    /// Material parameters (should have K = 0 for muMAG spec, high α for relaxation).
    pub material: Ferromagnet,
    /// Cell size (same in x, y, z — cubic cell) \[m\].
    pub cell_size: f64,
    /// Number of LLG relaxation steps.
    pub relax_steps: usize,
    /// Time step for relaxation \[s\].
    pub relax_dt: f64,
}

impl Default for Sp3Config {
    fn default() -> Self {
        let mut mat = Ferromagnet::permalloy();
        // muMAG SP#3 specifies α = 0.5 for fast energy relaxation.
        // Ms and exchange_a are already correct from permalloy().
        mat.alpha = 0.5;
        mat.anisotropy_k = 0.0;

        Self {
            material: mat,
            cell_size: 6e-9,  // 6 nm — coarse but demonstrates the physics
            relax_steps: 500, // sufficient for high-damping relaxation
            relax_dt: 1e-12,  // 1 ps
        }
    }
}

// ─── Sp3Result ────────────────────────────────────────────────────────────────

/// Result of a single SP#3 cube-size test.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Sp3Result {
    /// Cube edge length divided by exchange length: L / l_ex.
    pub l_over_lex: f64,
    /// Total energy of the relaxed flower state \[J\].
    pub flower_energy: f64,
    /// Total energy of the relaxed vortex state \[J\].
    pub vortex_energy: f64,
    /// Which state has lower energy (the stable state).
    pub stable_state: StableState,
    /// Exchange length l_ex \[m\] for the material used.
    pub exchange_length_m: f64,
}

// ─── StandardProblem3 ────────────────────────────────────────────────────────

/// NIST muMAG Standard Problem #3 runner and validator.
///
/// Determines the single-domain limit of a Permalloy cube by comparing the
/// relaxed energies of flower and vortex initial conditions.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StandardProblem3 {
    /// Simulation configuration
    pub config: Sp3Config,
}

impl StandardProblem3 {
    /// Create a new SP#3 runner with the given configuration.
    pub fn new(config: Sp3Config) -> Self {
        Self { config }
    }

    /// Create a new SP#3 runner with default (fast / coarse) settings.
    pub fn new_default() -> Self {
        Self::new(Sp3Config::default())
    }

    /// Compute the exchange length \[m\] for the configured material.
    ///
    /// ```text
    /// l_ex = sqrt( A / (½ μ₀ Ms²) )
    /// ```
    ///
    /// For Permalloy (Ms = 800 kA/m, A = 13 pJ/m):
    /// l_ex ≈ 5.69 nm.
    pub fn exchange_length(&self) -> f64 {
        let a = self.config.material.exchange_a;
        let ms = self.config.material.ms;
        // l_ex = sqrt(A / (0.5 * μ₀ * Ms²))
        let denom = 0.5 * MU_0 * ms * ms;
        (a / denom).sqrt()
    }

    /// The theoretical critical L/l_ex from the muMAG reference literature.
    ///
    /// McMichael & Donahue (1997) and the muMAG reference solution give L_c ≈ 8.47 l_ex.
    pub fn critical_l_over_lex_theoretical() -> f64 {
        8.47
    }

    /// Relax either a flower or vortex state for a cube of edge length `l_m` \[m\].
    ///
    /// Returns the total energy \[J\] after relaxation.
    ///
    /// # Arguments
    /// * `l_m` — cube edge length \[m\]
    /// * `use_vortex` — if true, initialise as vortex; otherwise initialise as uniform +z (flower seed)
    fn relax_cube(&self, l_m: f64, use_vortex: bool) -> Result<f64> {
        // Number of cells along each edge: at least 2, rounded to nearest integer
        let n_cells_float = (l_m / self.config.cell_size).round();
        let n = (n_cells_float as usize).max(2);

        // Actual cell size after rounding
        let d = l_m / n as f64;

        let cfg = GridConfig {
            dx: d,
            dy: d,
            dz: d,
            nx: n,
            ny: n,
            nz: n,
            dt: self.config.relax_dt,
            n_steps: self.config.relax_steps,
            // No intermediate recording needed for energy comparison
            record_every: self.config.relax_steps + 1,
        };

        let mut grid = MicromagneticGrid::new(cfg, self.config.material.clone(), Vector3::zero())?;

        if use_vortex {
            // Flux-closure vortex: m = (−y_rel, x_rel, ε).normalize()
            grid.set_vortex();
        } else {
            // Flower seed: uniform along +z
            // Using +z rather than +x avoids easy-axis bias for K = 0 Permalloy
            grid.set_uniform(Vector3::new(0.0, 0.0, 1.0));
        }

        // Relax with high damping (α = 0.5 from muMAG spec)
        let _result = grid.run();

        Ok(grid.total_energy())
    }

    /// Run SP#3 for a single cube edge length.
    ///
    /// Performs two relaxations (flower seed and vortex seed) and compares
    /// their final energies to determine which phase is stable.
    ///
    /// # Arguments
    /// * `l_m` — cube edge length \[m\]. Must be positive.
    ///
    /// # Errors
    /// Returns `Err` if `l_m` is non-positive or if grid construction fails.
    pub fn run_for_l(&self, l_m: f64) -> Result<Sp3Result> {
        if l_m <= 0.0 {
            return Err(invalid_param("l_m", "cube edge length must be positive"));
        }

        let lex = self.exchange_length();
        let l_over_lex = l_m / lex;

        // Relax both initial conditions
        let flower_energy = self.relax_cube(l_m, false)?;
        let vortex_energy = self.relax_cube(l_m, true)?;

        // Determine stable state (lower energy wins)
        let stable_state = if flower_energy <= vortex_energy {
            StableState::Flower
        } else {
            StableState::Vortex
        };

        Ok(Sp3Result {
            l_over_lex,
            flower_energy,
            vortex_energy,
            stable_state,
            exchange_length_m: lex,
        })
    }

    /// Validate the single-domain crossover using two representative cube sizes.
    ///
    /// ## Validation logic
    ///
    /// * For L = 4 l_ex (well below L_c ≈ 8.47 l_ex): expect flower wins.
    ///   Error = max(0, (E_flower − E_vortex) / |E_max|) — penalty if vortex wins.
    ///
    /// * For L = 12 l_ex (well above L_c): expect vortex wins.
    ///   Error = max(0, (E_vortex − E_flower) / |E_max|) — penalty if flower wins.
    ///
    /// The [`ValidationResult`] passes when both errors are ≤ `tolerance`.
    ///
    /// With the coarse default grid the quantitative crossover position may differ
    /// from 8.47 l_ex, but the qualitative ordering at the extremes is robust.
    ///
    /// # Arguments
    /// * `tolerance` — relative energy tolerance for the pass/fail criterion.
    ///   A value of 0.0 requires the correct state to strictly win; a small
    ///   positive value (e.g. 0.01) allows a 1 % margin.
    pub fn validate_single_domain_limit(&self, tolerance: f64) -> Result<ValidationResult> {
        let lex = self.exchange_length();

        // Test two sizes that are clearly below and above the theoretical L_c
        let l_small = 4.0 * lex; // expect flower
        let l_large = 12.0 * lex; // expect vortex

        let res_small = self.run_for_l(l_small)?;
        let res_large = self.run_for_l(l_large)?;

        // Compute relative energy errors (penalty for wrong ordering)
        let err_small = compute_phase_error(&res_small, StableState::Flower);
        let err_large = compute_phase_error(&res_large, StableState::Vortex);

        let errors = [err_small, err_large];
        Ok(ValidationResult::new(
            "muMAG SP#3 single-domain limit (flower vs vortex, coarse grid)",
            &errors,
            tolerance,
        ))
    }
}

/// Compute the relative energy penalty for the wrong stable state.
///
/// Returns 0.0 if the expected state has lower (or equal) energy than the
/// other state; returns a positive value proportional to the relative energy
/// difference if the wrong state won.
fn compute_phase_error(result: &Sp3Result, expected: StableState) -> f64 {
    let e_flower = result.flower_energy;
    let e_vortex = result.vortex_energy;
    let e_max = e_flower.abs().max(e_vortex.abs()).max(1e-30);

    match expected {
        StableState::Flower => {
            // Expect flower < vortex.  Penalty if vortex < flower.
            let diff = e_flower - e_vortex; // negative → flower wins (good)
            (diff / e_max).max(0.0)
        },
        StableState::Vortex => {
            // Expect vortex < flower.  Penalty if flower < vortex.
            let diff = e_vortex - e_flower; // negative → vortex wins (good)
            (diff / e_max).max(0.0)
        },
    }
}

// ─── Tests ───────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    /// Exchange length for Permalloy (Ms=800 kA/m, A=13 pJ/m) should be ~5.7 nm.
    #[test]
    fn test_exchange_length_permalloy() {
        let sp3 = StandardProblem3::new_default();
        let lex = sp3.exchange_length();
        // Expected: ~5.69 nm (within 10 %)
        assert!(
            (lex - 5.69e-9).abs() < 0.6e-9,
            "Exchange length {:.3e} m should be ≈ 5.69 nm",
            lex
        );
    }

    /// The theoretical critical L/l_ex from the muMAG reference is 8.47.
    #[test]
    fn test_critical_l_over_lex_theoretical() {
        let lc = StandardProblem3::critical_l_over_lex_theoretical();
        assert!(
            (lc - 8.47).abs() < 1e-9,
            "Critical L/l_ex should be 8.47, got {lc}"
        );
    }

    /// run_for_l should reject non-positive cube sizes.
    #[test]
    fn test_run_for_l_invalid_size() {
        let sp3 = StandardProblem3::new_default();
        assert!(sp3.run_for_l(-1.0).is_err());
        assert!(sp3.run_for_l(0.0).is_err());
    }

    /// For a very small cube (L ≈ 2 l_ex) the flower state should be stable.
    /// We use an extremely coarse 2×2×2 grid (n = 2 cells per edge) for speed.
    #[test]
    fn test_small_cube_flower_stable() {
        // Coarse config: fewer steps for a fast test
        let cfg = Sp3Config {
            relax_steps: 50,
            ..Sp3Config::default()
        };

        let sp3 = StandardProblem3::new(cfg);
        let lex = sp3.exchange_length();
        // Use a small cube (L ≈ 2 l_ex)
        let l = 2.0 * lex;
        let result = sp3.run_for_l(l).expect("SP3 run should succeed");

        // With a coarse 2×2×2 grid and point-dipole non-self demag, the quantitative
        // flower↔vortex crossover is not accurately captured. Only check:
        // 1. Simulation ran without error (guaranteed by expect above)
        // 2. Both energies are finite
        // 3. stable_state reflects the lower-energy state (internal consistency)
        assert!(
            result.flower_energy.is_finite(),
            "flower energy must be finite"
        );
        assert!(
            result.vortex_energy.is_finite(),
            "vortex energy must be finite"
        );
        match result.stable_state {
            StableState::Flower => assert!(result.flower_energy <= result.vortex_energy),
            StableState::Vortex => assert!(result.vortex_energy < result.flower_energy),
        }
    }

    /// For a large cube (L ≈ 14 l_ex) the vortex state should be stable.
    #[test]
    fn test_large_cube_vortex_stable() {
        let cfg = Sp3Config {
            // Finiteness / internal-consistency smoke test (it does NOT assert the
            // flower↔vortex physics ordering), so a short high-damping relaxation on
            // the full ~13³ grid is sufficient; 25 steps keeps the test well under the
            // runner's slow-test threshold on the O(N²) demag path.
            relax_steps: 25,
            ..Sp3Config::default()
        };

        let sp3 = StandardProblem3::new(cfg);
        let lex = sp3.exchange_length();
        // Use a large cube (L ≈ 14 l_ex)
        let l = 14.0 * lex;
        let result = sp3.run_for_l(l).expect("SP3 run should succeed");

        // The cube is L = 14·l_ex (~80 nm) on a ~13×13×13 finite-difference grid using
        // the exact Newell demagnetization tensor. Because this is a coarse
        // discretization (cell size ≈ l_ex), the quantitative flower↔vortex crossover
        // is not captured exactly. The test therefore only verifies that:
        // 1. The solver runs without error (guaranteed by `expect` above)
        // 2. Both relaxed energies are finite
        // 3. `stable_state` is consistent with whichever energy is lower
        assert!(
            result.flower_energy.is_finite(),
            "flower energy must be finite"
        );
        assert!(
            result.vortex_energy.is_finite(),
            "vortex energy must be finite"
        );
        match result.stable_state {
            StableState::Flower => {
                assert!(result.flower_energy <= result.vortex_energy);
            },
            StableState::Vortex => {
                assert!(result.vortex_energy < result.flower_energy);
            },
        }
    }

    /// Sp3Result stable_state is consistent with the energy comparison.
    #[test]
    fn test_stable_state_consistent_with_energies() {
        let cfg = Sp3Config {
            relax_steps: 20,
            ..Sp3Config::default()
        };
        let sp3 = StandardProblem3::new(cfg);
        let lex = sp3.exchange_length();

        let result = sp3.run_for_l(3.0 * lex).expect("run");
        match result.stable_state {
            StableState::Flower => {
                assert!(
                    result.flower_energy <= result.vortex_energy,
                    "stable_state = Flower but flower_energy > vortex_energy"
                );
            },
            StableState::Vortex => {
                assert!(
                    result.vortex_energy < result.flower_energy,
                    "stable_state = Vortex but vortex_energy >= flower_energy"
                );
            },
        }
    }

    /// The exchange_length_m field in Sp3Result should match the standalone calculation.
    #[test]
    fn test_sp3_result_exchange_length_field() {
        let cfg = Sp3Config {
            relax_steps: 10,
            ..Sp3Config::default()
        };
        let sp3 = StandardProblem3::new(cfg);
        let lex_standalone = sp3.exchange_length();
        let lex_trial = sp3.exchange_length(); // should be deterministic
        assert!((lex_standalone - lex_trial).abs() < 1e-30);

        let result = sp3.run_for_l(3.0 * lex_standalone).expect("run");
        assert!(
            (result.exchange_length_m - lex_standalone).abs() < 1e-30,
            "exchange_length_m field should match standalone calculation"
        );
    }

    /// compute_phase_error should return 0.0 when the expected state wins,
    /// and a positive value when the wrong state wins.
    #[test]
    fn test_compute_phase_error_logic() {
        let result_flower_wins = Sp3Result {
            l_over_lex: 4.0,
            flower_energy: -1.0e-18, // flower lower
            vortex_energy: -0.5e-18, // vortex higher
            stable_state: StableState::Flower,
            exchange_length_m: 5.7e-9,
        };
        // Expect flower → error should be 0
        let err = compute_phase_error(&result_flower_wins, StableState::Flower);
        assert!(
            err < 1e-10,
            "No penalty when flower wins as expected, got {err:.3e}"
        );
        // Expect vortex but flower won → positive penalty
        let err2 = compute_phase_error(&result_flower_wins, StableState::Vortex);
        assert!(
            err2 > 0.0,
            "Penalty should be positive when wrong state wins, got {err2:.3e}"
        );
    }
}