Skip to main content

oxiphysics_materials/
superconductor.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Superconductor material models.
5//!
6//! Implements Ginzburg–Landau phenomenological theory, BCS gap equations,
7//! London electrodynamics, flux-vortex lattice mechanics, and Type I / II
8//! classification for superconducting materials.
9//!
10//! All quantities use SI units unless explicitly noted.
11
12#![allow(dead_code)]
13#![allow(clippy::too_many_arguments)]
14
15use std::f64::consts::PI;
16
17// ---------------------------------------------------------------------------
18// Physical constants
19// ---------------------------------------------------------------------------
20
21/// Flux quantum Φ₀ = h/(2e) (Wb).
22pub const FLUX_QUANTUM: f64 = 2.067833848e-15;
23/// Boltzmann constant k_B (J/K).
24const K_B: f64 = 1.380649e-23;
25/// Threshold: kappa > 1/√2 → Type II.
26const KAPPA_THRESHOLD: f64 = std::f64::consts::FRAC_1_SQRT_2;
27
28// ---------------------------------------------------------------------------
29// Superconductor type
30// ---------------------------------------------------------------------------
31
32/// Classification of superconducting materials.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum SuperconductorType {
35    /// Type I: complete Meissner effect, single critical field H_c.
36    TypeI,
37    /// Type II: mixed Abrikosov state between H_c1 and H_c2.
38    TypeII,
39    /// High-temperature cuprate / unconventional superconductor (extreme Type II).
40    HighTc,
41}
42
43// ---------------------------------------------------------------------------
44// SuperconductorProps
45// ---------------------------------------------------------------------------
46
47/// Intrinsic physical properties of a superconducting material.
48#[derive(Clone, Debug)]
49pub struct SuperconductorProps {
50    /// Critical temperature T_c (K) below which superconductivity exists.
51    pub critical_temperature: f64,
52    /// Lower critical field H_c1 (A/m) – vortex entry field (Type II).
53    pub critical_field_hc1: f64,
54    /// Upper critical field H_c2 (A/m) – superconductivity destroyed above this.
55    pub critical_field_hc2: f64,
56    /// London penetration depth λ (m): characteristic magnetic field screening length.
57    pub london_penetration_depth: f64,
58    /// Ginzburg–Landau coherence length ξ (m): spatial scale of order parameter.
59    pub coherence_length: f64,
60    /// Ginzburg–Landau parameter κ = λ/ξ (dimensionless).
61    pub kappa: f64,
62}
63
64impl SuperconductorProps {
65    /// Construct a `SuperconductorProps` with all fields specified.
66    #[allow(clippy::too_many_arguments)]
67    pub fn new(
68        critical_temperature: f64,
69        critical_field_hc1: f64,
70        critical_field_hc2: f64,
71        london_penetration_depth: f64,
72        coherence_length: f64,
73    ) -> Self {
74        let kappa = london_penetration_depth / coherence_length.max(1e-300);
75        Self {
76            critical_temperature,
77            critical_field_hc1,
78            critical_field_hc2,
79            london_penetration_depth,
80            coherence_length,
81            kappa,
82        }
83    }
84
85    /// Example Nb (niobium) properties at T → 0.
86    pub fn niobium() -> Self {
87        Self::new(9.25, 1.58e5, 2.44e5, 39e-9, 38e-9)
88    }
89
90    /// Example Pb (lead) – Type I superconductor.
91    pub fn lead() -> Self {
92        // Lead is Type I: κ ≈ 0.48 < 1/√2.
93        Self::new(7.19, 6.4e4, 6.4e4, 37e-9, 83e-9)
94    }
95
96    /// Example YBCO (yttrium barium copper oxide) – HiTc superconductor.
97    pub fn ybco() -> Self {
98        Self::new(93.0, 2.0e7, 1.2e8, 150e-9, 1.5e-9)
99    }
100
101    /// Return the classification of this material.
102    pub fn superconductor_type(&self) -> SuperconductorType {
103        if self.kappa > 10.0 {
104            SuperconductorType::HighTc
105        } else if is_type_ii(self.kappa) {
106            SuperconductorType::TypeII
107        } else {
108            SuperconductorType::TypeI
109        }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// Ginzburg–Landau model
115// ---------------------------------------------------------------------------
116
117/// Ginzburg–Landau phenomenological model for an order parameter field.
118///
119/// The order parameter |ψ|² represents the local superfluid density
120/// (|ψ|² = 1 in the fully superconducting state, 0 in the normal state).
121#[derive(Clone, Debug)]
122pub struct GinzburgLandauModel {
123    /// Spatial grid of order parameter magnitudes |ψ| (dimensionless, 0–1).
124    pub order_parameter: Vec<f64>,
125    /// Corresponding magnetic field values B (T) on the same grid.
126    pub magnetic_field: Vec<f64>,
127    /// Ginzburg–Landau coherence length ξ (m).
128    pub coherence_length: f64,
129    /// London penetration depth λ (m).
130    pub penetration_depth: f64,
131    /// GL parameter κ = λ/ξ.
132    pub kappa: f64,
133}
134
135impl GinzburgLandauModel {
136    /// Create a new GL model with `n` grid points.
137    pub fn new(n: usize, coherence_length: f64, penetration_depth: f64) -> Self {
138        let kappa = penetration_depth / coherence_length.max(1e-300);
139        Self {
140            order_parameter: vec![1.0; n],
141            magnetic_field: vec![0.0; n],
142            coherence_length,
143            penetration_depth,
144            kappa,
145        }
146    }
147
148    /// Solve the linearised GL equation using a simple Euler relaxation.
149    ///
150    /// Returns the steady-state order parameter profile `|ψ(x)|`.
151    /// The boundary condition is |ψ(0)| = 0 (surface) and |ψ(∞)| = 1 (bulk).
152    /// `n_iter` relaxation sweeps are performed.
153    pub fn solve_gl(&mut self, n_iter: usize) -> Vec<f64> {
154        let n = self.order_parameter.len();
155        if n < 2 {
156            return self.order_parameter.clone();
157        }
158        // Apply boundary conditions.
159        self.order_parameter[0] = 0.0;
160        *self
161            .order_parameter
162            .last_mut()
163            .expect("collection should not be empty") = 1.0;
164
165        // Simple Gauss–Seidel relaxation on ξ²∇²ψ - ψ(|ψ|² - 1) = 0.
166        // Linearised around ψ ≈ tanh(x/ξ√2) profile.
167        for _ in 0..n_iter {
168            for i in 1..n - 1 {
169                let psi_l = self.order_parameter[i - 1];
170                let psi_r = self.order_parameter[i + 1];
171                let psi = self.order_parameter[i];
172                // Laplacian term (dx = 1 normalised).
173                let laplacian = psi_l - 2.0 * psi + psi_r;
174                // GL: ξ²∇²ψ + ψ(1 - |ψ|²) = 0 → ψ_new via relaxation.
175                let rhs = self.coherence_length.powi(2) * laplacian + psi * (1.0 - psi * psi);
176                self.order_parameter[i] = (psi + 0.1 * rhs).clamp(0.0, 1.0);
177            }
178        }
179        self.order_parameter.clone()
180    }
181}
182
183// ---------------------------------------------------------------------------
184// BCS gap equation
185// ---------------------------------------------------------------------------
186
187/// BCS (Bardeen–Cooper–Schrieffer) energy gap model.
188///
189/// The BCS gap Δ(T) determines the binding energy of Cooper pairs.
190#[derive(Clone, Debug)]
191pub struct BcsGap {
192    /// Zero-temperature gap energy Δ(0) (J).
193    pub gap_energy: f64,
194    /// Electronic density of states at the Fermi level N(0) (J⁻¹ m⁻³).
195    pub density_of_states: f64,
196    /// Critical temperature T_c (K).
197    pub critical_temperature: f64,
198}
199
200impl BcsGap {
201    /// Construct from zero-temperature gap, density of states, and T_c.
202    pub fn new(gap_energy: f64, density_of_states: f64, critical_temperature: f64) -> Self {
203        Self {
204            gap_energy,
205            density_of_states,
206            critical_temperature,
207        }
208    }
209
210    /// Construct from T_c using the BCS relation Δ(0) = 1.764 k_B T_c.
211    pub fn from_tc(critical_temperature: f64, density_of_states: f64) -> Self {
212        let gap = 1.764 * K_B * critical_temperature;
213        Self::new(gap, density_of_states, critical_temperature)
214    }
215
216    /// Compute the temperature-dependent gap Δ(T) using the BCS interpolation:
217    ///
218    /// Δ(T) ≈ Δ(0) · tanh(1.74 √((T_c/T) - 1))  for T < T_c
219    ///
220    /// Returns 0.0 for T ≥ T_c.
221    pub fn compute_gap(&self, temperature: f64) -> f64 {
222        if temperature <= 0.0 {
223            return self.gap_energy;
224        }
225        if temperature >= self.critical_temperature {
226            return 0.0;
227        }
228        let ratio = self.critical_temperature / temperature;
229        let arg = 1.74 * (ratio - 1.0).sqrt();
230        self.gap_energy * arg.tanh()
231    }
232
233    /// Normalised gap Δ(T)/Δ(0).
234    pub fn normalised_gap(&self, temperature: f64) -> f64 {
235        if self.gap_energy < 1e-300 {
236            return 0.0;
237        }
238        self.compute_gap(temperature) / self.gap_energy
239    }
240}
241
242// ---------------------------------------------------------------------------
243// Flux vortex
244// ---------------------------------------------------------------------------
245
246/// A single Abrikosov flux vortex carrying one flux quantum Φ₀.
247#[derive(Clone, Debug)]
248pub struct FluxVortex {
249    /// 2-D position (x, y) of the vortex core in metres.
250    pub position: [f64; 2],
251    /// Quantised magnetic flux carried by this vortex (Wb).
252    ///
253    /// For a singly quantised vortex this equals [`FLUX_QUANTUM`].
254    pub flux_quantum: f64,
255}
256
257impl FluxVortex {
258    /// Create a singly-quantised vortex at `position`.
259    pub fn new(position: [f64; 2]) -> Self {
260        Self {
261            position,
262            flux_quantum: FLUX_QUANTUM,
263        }
264    }
265
266    /// Euclidean distance between this vortex and another.
267    pub fn distance_to(&self, other: &FluxVortex) -> f64 {
268        let dx = self.position[0] - other.position[0];
269        let dy = self.position[1] - other.position[1];
270        (dx * dx + dy * dy).sqrt()
271    }
272}
273
274// ---------------------------------------------------------------------------
275// Vortex lattice
276// ---------------------------------------------------------------------------
277
278/// An Abrikosov vortex lattice: a collection of flux vortices in a Type II
279/// superconductor at fields H_c1 < H < H_c2.
280#[derive(Clone, Debug)]
281pub struct VortexLattice {
282    /// All vortices in this lattice.
283    pub vortices: Vec<FluxVortex>,
284    /// London penetration depth λ (m).
285    pub penetration_depth: f64,
286}
287
288impl VortexLattice {
289    /// Create an empty lattice.
290    pub fn new(penetration_depth: f64) -> Self {
291        Self {
292            vortices: Vec::new(),
293            penetration_depth,
294        }
295    }
296
297    /// Add a vortex to the lattice.
298    pub fn add_vortex(&mut self, vortex: FluxVortex) {
299        self.vortices.push(vortex);
300    }
301
302    /// Build a triangular (Abrikosov) vortex lattice with spacing `a` over an
303    /// `nx × ny` grid.
304    pub fn triangular_lattice(nx: usize, ny: usize, spacing: f64, penetration_depth: f64) -> Self {
305        let mut lattice = Self::new(penetration_depth);
306        for j in 0..ny {
307            for i in 0..nx {
308                let x = i as f64 * spacing + if j % 2 == 1 { 0.5 * spacing } else { 0.0 };
309                let y = j as f64 * spacing * (3.0_f64.sqrt() / 2.0);
310                lattice.add_vortex(FluxVortex::new([x, y]));
311            }
312        }
313        lattice
314    }
315
316    /// Compute the pairwise inter-vortex repulsive force on each vortex.
317    ///
318    /// The London interaction force between two vortices at separation r is:
319    ///
320    /// F(r) = (Φ₀²)/(2π μ₀ λ²) · K₁(r/λ) / r
321    ///
322    /// Here we use the approximation K₁(r/λ) ≈ (λ/r) · exp(-r/λ) for r > 0.
323    ///
324    /// Returns a `Vec` of 2-D force vectors `[fx, fy]` (N/m) per vortex.
325    pub fn compute_inter_vortex_force(&self) -> Vec<[f64; 2]> {
326        let n = self.vortices.len();
327        let mut forces = vec![[0.0_f64; 2]; n];
328        let mu0 = 4.0 * PI * 1e-7_f64;
329        let lambda = self.penetration_depth;
330        // Prefactor C = Φ₀²/(2π μ₀ λ²).
331        let prefactor = FLUX_QUANTUM.powi(2) / (2.0 * PI * mu0 * lambda.powi(2));
332        #[allow(clippy::needless_range_loop)]
333        for i in 0..n {
334            for j in 0..n {
335                if i == j {
336                    continue;
337                }
338                let dx = self.vortices[i].position[0] - self.vortices[j].position[0];
339                let dy = self.vortices[i].position[1] - self.vortices[j].position[1];
340                let r = (dx * dx + dy * dy).sqrt().max(1e-15);
341                let xi = r / lambda;
342                // K₁ approximation.
343                let k1_approx = (1.0 / xi) * (-xi).exp();
344                let mag = prefactor * k1_approx / r;
345                forces[i][0] += mag * dx / r;
346                forces[i][1] += mag * dy / r;
347            }
348        }
349        forces
350    }
351
352    /// Total applied flux density B = n_v · Φ₀ (T·m²) where n_v is the
353    /// vortex areal density.  Assumes vortices are distributed over area
354    /// given by `cell_area` (m²).
355    pub fn flux_density(&self, cell_area: f64) -> f64 {
356        (self.vortices.len() as f64) * FLUX_QUANTUM / cell_area.max(1e-300)
357    }
358}
359
360// ---------------------------------------------------------------------------
361// Free functions
362// ---------------------------------------------------------------------------
363
364/// London equation screening factor for a uniform applied field.
365///
366/// The London equation ∇²B = B/λ² gives exponential screening:
367///
368/// B(x) = `field` · exp(−x/`lambda`)
369///
370/// This function returns the factor exp(−|field|/`lambda`) as a
371/// dimensionless attenuation coefficient for normalised depth units.
372pub fn london_equation_factor(lambda: f64, field: f64) -> f64 {
373    if lambda <= 0.0 {
374        return 0.0;
375    }
376    (-field.abs() / lambda).exp()
377}
378
379/// Returns `true` if the Ginzburg–Landau parameter κ = λ/ξ exceeds 1/√2,
380/// indicating a Type II superconductor with an Abrikosov mixed state.
381pub fn is_type_ii(kappa: f64) -> bool {
382    kappa > KAPPA_THRESHOLD
383}
384
385/// Upper critical field H_c2 = Φ₀ / (2π μ₀ ξ²) (SI, A/m).
386pub fn upper_critical_field(coherence_length: f64) -> f64 {
387    let mu0 = 4.0 * PI * 1e-7_f64;
388    FLUX_QUANTUM / (2.0 * PI * mu0 * coherence_length.powi(2).max(1e-300))
389}
390
391/// Thermodynamic critical field H_c = Φ₀ / (2π√2 μ₀ λ ξ) (A/m).
392pub fn thermodynamic_critical_field(lambda: f64, coherence_length: f64) -> f64 {
393    let mu0 = 4.0 * PI * 1e-7_f64;
394    FLUX_QUANTUM / (2.0 * PI * 2.0_f64.sqrt() * mu0 * lambda * coherence_length.max(1e-300))
395}
396
397// ---------------------------------------------------------------------------
398// Tests
399// ---------------------------------------------------------------------------
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    const EPS: f64 = 1e-10;
406
407    // 1. FLUX_QUANTUM has the accepted value.
408    #[test]
409    fn test_flux_quantum_value() {
410        assert!((FLUX_QUANTUM - 2.067833848e-15).abs() < 1e-22);
411    }
412
413    // 2. is_type_ii: kappa < threshold → false.
414    #[test]
415    fn test_is_type_ii_false() {
416        assert!(!is_type_ii(0.3)); // deep Type I
417    }
418
419    // 3. is_type_ii: kappa > threshold → true.
420    #[test]
421    fn test_is_type_ii_true() {
422        assert!(is_type_ii(1.0)); // clear Type II
423    }
424
425    // 4. Boundary kappa ≈ 1/√2.
426    #[test]
427    fn test_is_type_ii_boundary() {
428        let kappa = 1.0 / 2.0_f64.sqrt();
429        assert!(!is_type_ii(kappa)); // exactly at threshold → not Type II
430        assert!(is_type_ii(kappa + 1e-9));
431    }
432
433    // 5. SuperconductorProps::kappa = lambda / xi.
434    #[test]
435    fn test_props_kappa_computation() {
436        let props = SuperconductorProps::new(9.25, 1.58e5, 2.44e5, 40e-9, 20e-9);
437        assert!((props.kappa - 2.0).abs() < 1e-9);
438    }
439
440    // 6. Niobium is Type II.
441    #[test]
442    fn test_niobium_type_ii() {
443        let nb = SuperconductorProps::niobium();
444        assert_eq!(nb.superconductor_type(), SuperconductorType::TypeII);
445    }
446
447    // 7. Lead is Type I.
448    #[test]
449    fn test_lead_type_i() {
450        let pb = SuperconductorProps::lead();
451        assert_eq!(pb.superconductor_type(), SuperconductorType::TypeI);
452    }
453
454    // 8. YBCO is HighTc.
455    #[test]
456    fn test_ybco_high_tc() {
457        let y = SuperconductorProps::ybco();
458        assert_eq!(y.superconductor_type(), SuperconductorType::HighTc);
459    }
460
461    // 9. GL model initialises to |ψ| = 1 everywhere.
462    #[test]
463    fn test_gl_model_initial_order_parameter() {
464        let gl = GinzburgLandauModel::new(10, 10e-9, 40e-9);
465        assert!(gl.order_parameter.iter().all(|&v| (v - 1.0).abs() < EPS));
466    }
467
468    // 10. solve_gl boundary conditions: |ψ(0)| = 0, |ψ(n-1)| = 1.
469    #[test]
470    fn test_gl_solve_boundary_conditions() {
471        let mut gl = GinzburgLandauModel::new(20, 0.1, 0.4);
472        let psi = gl.solve_gl(200);
473        assert!(psi[0].abs() < EPS, "Surface must be 0, got {}", psi[0]);
474        assert!(
475            (psi[19] - 1.0).abs() < EPS,
476            "Bulk must be 1, got {}",
477            psi[19]
478        );
479    }
480
481    // 11. solve_gl output values are in [0, 1].
482    #[test]
483    fn test_gl_solve_range() {
484        let mut gl = GinzburgLandauModel::new(30, 0.1, 0.4);
485        let psi = gl.solve_gl(100);
486        for &v in &psi {
487            assert!(
488                (0.0..=1.0).contains(&v),
489                "Order parameter out of range: {v}"
490            );
491        }
492    }
493
494    // 12. solve_gl is monotonically non-decreasing from surface to bulk.
495    #[test]
496    fn test_gl_solve_monotone() {
497        let mut gl = GinzburgLandauModel::new(30, 0.1, 0.4);
498        let psi = gl.solve_gl(500);
499        for w in psi.windows(2) {
500            assert!(w[1] >= w[0] - 1e-8, "Not monotone: {} > {}", w[0], w[1]);
501        }
502    }
503
504    // 13. BcsGap::from_tc gives Δ(0) ≈ 1.764 k_B T_c.
505    #[test]
506    fn test_bcs_gap_from_tc() {
507        let tc = 9.25;
508        let bcs = BcsGap::from_tc(tc, 1.0);
509        let expected = 1.764 * K_B * tc;
510        assert!((bcs.gap_energy - expected).abs() < 1e-28);
511    }
512
513    // 14. BCS gap at T = 0 equals gap_energy.
514    #[test]
515    fn test_bcs_gap_at_zero_temp() {
516        let bcs = BcsGap::from_tc(9.25, 1.0);
517        assert!((bcs.compute_gap(0.0) - bcs.gap_energy).abs() < EPS);
518    }
519
520    // 15. BCS gap vanishes at T = T_c.
521    #[test]
522    fn test_bcs_gap_at_tc() {
523        let bcs = BcsGap::from_tc(9.25, 1.0);
524        assert!(bcs.compute_gap(9.25).abs() < EPS);
525    }
526
527    // 16. BCS gap vanishes above T_c.
528    #[test]
529    fn test_bcs_gap_above_tc() {
530        let bcs = BcsGap::from_tc(9.25, 1.0);
531        assert!(bcs.compute_gap(100.0).abs() < EPS);
532    }
533
534    // 17. BCS gap is monotonically decreasing with temperature.
535    #[test]
536    fn test_bcs_gap_monotone() {
537        let bcs = BcsGap::from_tc(9.25, 1.0);
538        let temps: Vec<f64> = (0..=9).map(|i| i as f64).collect();
539        let gaps: Vec<f64> = temps.iter().map(|&t| bcs.compute_gap(t)).collect();
540        for w in gaps.windows(2) {
541            assert!(
542                w[1] <= w[0] + 1e-15,
543                "Gap not monotone: {} -> {}",
544                w[0],
545                w[1]
546            );
547        }
548    }
549
550    // 18. Normalised gap at T = 0 is 1.0.
551    #[test]
552    fn test_bcs_normalised_gap_at_zero() {
553        let bcs = BcsGap::from_tc(9.25, 1.0);
554        assert!((bcs.normalised_gap(0.0) - 1.0).abs() < EPS);
555    }
556
557    // 19. Normalised gap at T_c is 0.
558    #[test]
559    fn test_bcs_normalised_gap_at_tc() {
560        let bcs = BcsGap::from_tc(9.25, 1.0);
561        assert!(bcs.normalised_gap(9.25).abs() < EPS);
562    }
563
564    // 20. FluxVortex carries one flux quantum.
565    #[test]
566    fn test_flux_vortex_carries_one_quantum() {
567        let v = FluxVortex::new([0.0, 0.0]);
568        assert!((v.flux_quantum - FLUX_QUANTUM).abs() < 1e-25);
569    }
570
571    // 21. FluxVortex distance computation.
572    #[test]
573    fn test_flux_vortex_distance() {
574        let v1 = FluxVortex::new([0.0, 0.0]);
575        let v2 = FluxVortex::new([3.0, 4.0]);
576        assert!((v1.distance_to(&v2) - 5.0).abs() < EPS);
577    }
578
579    // 22. VortexLattice starts empty.
580    #[test]
581    fn test_vortex_lattice_empty() {
582        let lat = VortexLattice::new(40e-9);
583        assert!(lat.vortices.is_empty());
584    }
585
586    // 23. add_vortex increases count.
587    #[test]
588    fn test_vortex_lattice_add() {
589        let mut lat = VortexLattice::new(40e-9);
590        lat.add_vortex(FluxVortex::new([0.0, 0.0]));
591        lat.add_vortex(FluxVortex::new([1e-6, 0.0]));
592        assert_eq!(lat.vortices.len(), 2);
593    }
594
595    // 24. Triangular lattice has nx * ny vortices.
596    #[test]
597    fn test_triangular_lattice_count() {
598        let lat = VortexLattice::triangular_lattice(4, 3, 100e-9, 40e-9);
599        assert_eq!(lat.vortices.len(), 12);
600    }
601
602    // 25. Inter-vortex force length matches vortex count.
603    #[test]
604    fn test_inter_vortex_force_length() {
605        let lat = VortexLattice::triangular_lattice(3, 3, 200e-9, 40e-9);
606        let forces = lat.compute_inter_vortex_force();
607        assert_eq!(forces.len(), 9);
608    }
609
610    // 26. Net force on a symmetric pair has equal and opposite x-components.
611    //
612    // Vortex 0 is at origin, vortex 1 is at +x.  Repulsion pushes vortex 0
613    // in the −x direction and vortex 1 in the +x direction.
614    #[test]
615    fn test_inter_vortex_force_newton_third() {
616        let mut lat = VortexLattice::new(40e-9);
617        lat.add_vortex(FluxVortex::new([0.0, 0.0]));
618        lat.add_vortex(FluxVortex::new([200e-9, 0.0]));
619        let f = lat.compute_inter_vortex_force();
620        // Force on 0 should be −x (pushed away from vortex 1 in +x).
621        assert!(
622            f[0][0] < 0.0,
623            "Vortex 0 should be pushed -x, got {}",
624            f[0][0]
625        );
626        // Force on 1 should be +x (pushed away from vortex 0 in -x direction).
627        assert!(
628            f[1][0] > 0.0,
629            "Vortex 1 should be pushed +x, got {}",
630            f[1][0]
631        );
632        // Newton's 3rd law: equal and opposite.
633        assert!(
634            (f[0][0] + f[1][0]).abs() < 1e-3 * f[0][0].abs(),
635            "Newton's 3rd law violated: {} vs {}",
636            f[0][0],
637            f[1][0]
638        );
639    }
640
641    // 27. london_equation_factor returns 1.0 at zero field.
642    #[test]
643    fn test_london_factor_zero_field() {
644        let f = london_equation_factor(40e-9, 0.0);
645        assert!((f - 1.0).abs() < EPS);
646    }
647
648    // 28. london_equation_factor decays with field depth.
649    #[test]
650    fn test_london_factor_decay() {
651        let lambda = 40e-9;
652        let f1 = london_equation_factor(lambda, lambda);
653        let f2 = london_equation_factor(lambda, 2.0 * lambda);
654        assert!(f2 < f1, "Factor should decay: f1={f1}, f2={f2}");
655        // f1 ≈ e^-1, f2 ≈ e^-2.
656        assert!((f1 - (-1.0_f64).exp()).abs() < 1e-12);
657    }
658
659    // 29. london_equation_factor is zero for non-positive lambda.
660    #[test]
661    fn test_london_factor_zero_lambda() {
662        assert!((london_equation_factor(0.0, 1.0)).abs() < EPS);
663        assert!((london_equation_factor(-1.0, 1.0)).abs() < EPS);
664    }
665
666    // 30. upper_critical_field scales as 1/ξ².
667    #[test]
668    fn test_upper_critical_field_scaling() {
669        let hc2_a = upper_critical_field(10e-9);
670        let hc2_b = upper_critical_field(20e-9); // double ξ
671        // H_c2 ∝ 1/ξ² → halving ξ quadruples H_c2.
672        assert!(
673            (hc2_a / hc2_b - 4.0).abs() < 1e-8,
674            "H_c2(10nm)/H_c2(20nm) should be 4, got {}",
675            hc2_a / hc2_b
676        );
677    }
678
679    // 31. thermodynamic_critical_field is positive.
680    #[test]
681    fn test_thermodynamic_critical_field_positive() {
682        let hc = thermodynamic_critical_field(40e-9, 20e-9);
683        assert!(hc > 0.0);
684    }
685
686    // 32. SuperconductorType enum variant equality.
687    #[test]
688    fn test_superconductor_type_eq() {
689        assert_eq!(SuperconductorType::TypeI, SuperconductorType::TypeI);
690        assert_ne!(SuperconductorType::TypeI, SuperconductorType::TypeII);
691    }
692
693    // 33. VortexLattice flux density scales with vortex count.
694    #[test]
695    fn test_flux_density_scaling() {
696        let mut lat1 = VortexLattice::new(40e-9);
697        let mut lat2 = VortexLattice::new(40e-9);
698        lat1.add_vortex(FluxVortex::new([0.0, 0.0]));
699        lat2.add_vortex(FluxVortex::new([0.0, 0.0]));
700        lat2.add_vortex(FluxVortex::new([1e-6, 0.0]));
701        let area = 1e-12;
702        assert!((lat2.flux_density(area) - 2.0 * lat1.flux_density(area)).abs() < 1e-25);
703    }
704
705    // 34. BCS gap at mid-temperature is between 0 and gap_energy.
706    #[test]
707    fn test_bcs_gap_mid_temperature() {
708        let tc = 9.25;
709        let bcs = BcsGap::from_tc(tc, 1.0);
710        let gap_mid = bcs.compute_gap(tc / 2.0);
711        assert!(
712            gap_mid > 0.0 && gap_mid < bcs.gap_energy,
713            "Mid-T gap should be in (0, Δ₀): {gap_mid}"
714        );
715    }
716}