use crate::constants::MU_0;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub enum DefectType {
Vacancy,
Substitutional {
anisotropy: f64,
exchange_mod: f64,
},
Pinning {
strength: f64,
width: f64,
},
}
#[derive(Debug, Clone)]
pub struct DefectSite {
pub position: usize,
pub defect_type: DefectType,
}
impl DefectSite {
pub fn vacancy(position: usize) -> Self {
Self {
position,
defect_type: DefectType::Vacancy,
}
}
pub fn substitutional(position: usize, anisotropy: f64, exchange_mod: f64) -> Self {
Self {
position,
defect_type: DefectType::Substitutional {
anisotropy,
exchange_mod,
},
}
}
pub fn pinning(position: usize, strength: f64, width: f64) -> Self {
Self {
position,
defect_type: DefectType::Pinning { strength, width },
}
}
pub fn is_vacancy(&self) -> bool {
matches!(self.defect_type, DefectType::Vacancy)
}
pub fn effective_anisotropy(&self, bulk_anisotropy: f64) -> f64 {
match &self.defect_type {
DefectType::Vacancy => 0.0,
DefectType::Substitutional { anisotropy, .. } => *anisotropy,
DefectType::Pinning { .. } => bulk_anisotropy,
}
}
pub fn exchange_factor(&self) -> f64 {
match &self.defect_type {
DefectType::Vacancy => 0.0,
DefectType::Substitutional { exchange_mod, .. } => *exchange_mod,
DefectType::Pinning { .. } => 1.0,
}
}
}
pub fn pinning_potential(x: f64, x0: f64, v0: f64, w: f64) -> f64 {
if w.abs() < f64::EPSILON {
return if (x - x0).abs() < f64::EPSILON {
v0
} else {
0.0
};
}
let dx = x - x0;
v0 * (-dx * dx / (2.0 * w * w)).exp()
}
pub fn pinning_force(x: f64, x0: f64, v0: f64, w: f64) -> f64 {
if w.abs() < f64::EPSILON {
return 0.0;
}
let dx = x - x0;
v0 * dx / (w * w) * (-dx * dx / (2.0 * w * w)).exp()
}
#[derive(Debug, Clone)]
pub struct DepinningParams {
pub pinning_strength: f64,
pub saturation_magnetization: f64,
pub domain_wall_width: f64,
}
impl DepinningParams {
pub fn new(
pinning_strength: f64,
saturation_magnetization: f64,
domain_wall_width: f64,
) -> Result<Self> {
if pinning_strength <= 0.0 {
return Err(Error::InvalidParameter {
param: "pinning_strength".to_string(),
reason: "must be positive".to_string(),
});
}
if saturation_magnetization <= 0.0 {
return Err(Error::InvalidParameter {
param: "saturation_magnetization".to_string(),
reason: "must be positive".to_string(),
});
}
if domain_wall_width <= 0.0 {
return Err(Error::InvalidParameter {
param: "domain_wall_width".to_string(),
reason: "must be positive".to_string(),
});
}
Ok(Self {
pinning_strength,
saturation_magnetization,
domain_wall_width,
})
}
pub fn depinning_field(&self) -> f64 {
self.pinning_strength
/ (2.0 * MU_0 * self.saturation_magnetization * self.domain_wall_width)
}
pub fn depinning_field_thermal(&self, temperature: f64) -> Result<f64> {
if temperature < 0.0 {
return Err(Error::InvalidParameter {
param: "temperature".to_string(),
reason: "must be non-negative".to_string(),
});
}
let h_dep_0 = self.depinning_field();
if temperature < f64::EPSILON {
return Ok(h_dep_0);
}
let kb = crate::constants::KB;
let energy_barrier = self.pinning_strength * self.domain_wall_width;
let thermal_ratio = kb * temperature / energy_barrier;
if thermal_ratio >= 1.0 {
Ok(0.0)
} else {
Ok(h_dep_0 * (1.0 - thermal_ratio))
}
}
}
#[derive(Debug, Clone)]
pub struct GrainBoundary {
pub sites: (usize, usize),
pub coupling_reduction: f64,
}
impl GrainBoundary {
pub fn new(site_a: usize, site_b: usize, eta: f64) -> Result<Self> {
if !(0.0..=1.0).contains(&eta) {
return Err(Error::InvalidParameter {
param: "eta".to_string(),
reason: "coupling reduction must be in [0, 1]".to_string(),
});
}
Ok(Self {
sites: (site_a, site_b),
coupling_reduction: eta,
})
}
pub fn effective_exchange(&self, bulk_exchange: f64) -> f64 {
self.coupling_reduction * bulk_exchange
}
pub fn is_decoupled(&self) -> bool {
self.coupling_reduction < f64::EPSILON
}
}
#[derive(Debug, Clone)]
pub struct DefectCollection {
pub defects: Vec<DefectSite>,
pub total_sites: usize,
pub grain_boundaries: Vec<GrainBoundary>,
}
impl DefectCollection {
pub fn new(total_sites: usize) -> Result<Self> {
if total_sites == 0 {
return Err(Error::InvalidParameter {
param: "total_sites".to_string(),
reason: "must be greater than zero".to_string(),
});
}
Ok(Self {
defects: Vec::new(),
total_sites,
grain_boundaries: Vec::new(),
})
}
pub fn add_defect(&mut self, defect: DefectSite) -> Result<()> {
if defect.position >= self.total_sites {
return Err(Error::InvalidParameter {
param: "defect.position".to_string(),
reason: format!(
"position {} exceeds total sites {}",
defect.position, self.total_sites
),
});
}
self.defects.push(defect);
Ok(())
}
pub fn add_grain_boundary(&mut self, gb: GrainBoundary) -> Result<()> {
if gb.sites.0 >= self.total_sites || gb.sites.1 >= self.total_sites {
return Err(Error::InvalidParameter {
param: "grain_boundary.sites".to_string(),
reason: format!(
"site indices ({}, {}) exceed total sites {}",
gb.sites.0, gb.sites.1, self.total_sites
),
});
}
self.grain_boundaries.push(gb);
Ok(())
}
pub fn concentration(&self) -> f64 {
self.defects.len() as f64 / self.total_sites as f64
}
pub fn vacancy_concentration(&self) -> f64 {
let n_vac = self.defects.iter().filter(|d| d.is_vacancy()).count();
n_vac as f64 / self.total_sites as f64
}
pub fn num_pinning_sites(&self) -> usize {
self.defects
.iter()
.filter(|d| matches!(d.defect_type, DefectType::Pinning { .. }))
.count()
}
pub fn coercivity_estimate(&self, h_c0: f64, alpha: f64) -> f64 {
let n = self.concentration();
if n < f64::EPSILON {
return 0.0;
}
h_c0 * n.powf(alpha)
}
pub fn average_exchange_factor(&self) -> f64 {
if self.total_sites == 0 {
return 1.0;
}
let defect_sum: f64 = self.defects.iter().map(|d| d.exchange_factor()).sum();
let non_defect_count = self.total_sites - self.defects.len();
(defect_sum + non_defect_count as f64) / self.total_sites as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pinning_potential_maximum_at_center() {
let v0 = 1.0e-3; let w = 5.0e-9; let x0 = 50.0e-9;
let v_center = pinning_potential(x0, x0, v0, w);
let v_away = pinning_potential(x0 + 10.0e-9, x0, v0, w);
let v_far = pinning_potential(x0 + 50.0e-9, x0, v0, w);
assert!(
(v_center - v0).abs() < 1e-15,
"Potential at center should equal V₀"
);
assert!(
v_center > v_away,
"Potential should decrease away from center"
);
assert!(
v_away > v_far,
"Potential should decrease further from center"
);
}
#[test]
fn test_depinning_field_is_positive() {
let params =
DepinningParams::new(1.0e-3, 8.0e5, 20.0e-9).expect("valid depinning parameters");
let h_dep = params.depinning_field();
assert!(
h_dep > 0.0,
"Depinning field should be positive, got {}",
h_dep
);
let expected = 1.0e-3 / (2.0 * MU_0 * 8.0e5 * 20.0e-9);
assert!(
(h_dep - expected).abs() / expected < 1e-10,
"Depinning field mismatch: got {}, expected {}",
h_dep,
expected
);
}
#[test]
fn test_depinning_field_thermal_reduction() {
let params =
DepinningParams::new(1.0e-3, 8.0e5, 20.0e-9).expect("valid depinning parameters");
let h_0 = params.depinning_field();
let h_300k = params
.depinning_field_thermal(300.0)
.expect("valid temperature");
assert!(
h_300k < h_0,
"Thermal depinning field {} should be less than zero-T value {}",
h_300k,
h_0
);
assert!(h_300k >= 0.0, "Depinning field should be non-negative");
}
#[test]
fn test_vacancy_removes_local_contribution() {
let defect = DefectSite::vacancy(42);
assert!(defect.is_vacancy());
assert!(
defect.effective_anisotropy(5.0e4).abs() < f64::EPSILON,
"Vacancy should have zero anisotropy"
);
assert!(
defect.exchange_factor().abs() < f64::EPSILON,
"Vacancy should have zero exchange"
);
}
#[test]
fn test_substitutional_defect() {
let defect = DefectSite::substitutional(10, 2.0e4, 0.5);
assert!(!defect.is_vacancy());
assert!(
(defect.effective_anisotropy(5.0e4) - 2.0e4).abs() < f64::EPSILON,
"Substitutional should use its own anisotropy"
);
assert!(
(defect.exchange_factor() - 0.5).abs() < f64::EPSILON,
"Substitutional exchange mod should be 0.5"
);
}
#[test]
fn test_grain_boundary_reduces_exchange() {
let gb = GrainBoundary::new(0, 1, 0.3).expect("valid grain boundary");
let bulk_j = 1.0e-11;
let j_gb = gb.effective_exchange(bulk_j);
assert!(
j_gb < bulk_j,
"Grain boundary exchange {} should be less than bulk {}",
j_gb,
bulk_j
);
assert!(
(j_gb - 0.3 * bulk_j).abs() / bulk_j < 1e-15,
"J_gb should be η·J_bulk"
);
let gb_decoupled = GrainBoundary::new(0, 1, 0.0).expect("valid grain boundary");
assert!(gb_decoupled.is_decoupled());
assert!(gb_decoupled.effective_exchange(bulk_j).abs() < f64::EPSILON);
}
#[test]
fn test_grain_boundary_invalid_eta() {
assert!(GrainBoundary::new(0, 1, 1.5).is_err());
assert!(GrainBoundary::new(0, 1, -0.1).is_err());
}
#[test]
fn test_defect_concentration_scaling() {
let n_sites = 10000;
let mut collection = DefectCollection::new(n_sites).expect("valid collection");
for i in 0..100 {
collection
.add_defect(DefectSite::pinning(i, 1.0e-3, 5.0e-9))
.expect("valid defect");
}
let conc = collection.concentration();
assert!(
(conc - 0.01).abs() < 1e-10,
"Concentration should be 0.01, got {}",
conc
);
let h_c0 = 1.0e6; let alpha = 0.5;
let h_c = collection.coercivity_estimate(h_c0, alpha);
let expected = h_c0 * (0.01_f64).powf(0.5);
assert!(
(h_c - expected).abs() / expected < 1e-10,
"Coercivity scaling: got {}, expected {}",
h_c,
expected
);
let mut collection2 = DefectCollection::new(n_sites).expect("valid collection");
for i in 0..400 {
collection2
.add_defect(DefectSite::pinning(i, 1.0e-3, 5.0e-9))
.expect("valid defect");
}
let h_c2 = collection2.coercivity_estimate(h_c0, alpha);
assert!(
h_c2 > h_c,
"More defects should increase coercivity: {} vs {}",
h_c2,
h_c
);
}
#[test]
fn test_defect_collection_average_exchange() {
let mut collection = DefectCollection::new(100).expect("valid collection");
for i in 0..10 {
collection
.add_defect(DefectSite::vacancy(i))
.expect("valid defect");
}
let avg = collection.average_exchange_factor();
assert!(
(avg - 0.9).abs() < 1e-10,
"Average exchange factor should be 0.9, got {}",
avg
);
}
#[test]
fn test_pinning_force_direction() {
let v0 = 1.0e-3;
let w = 5.0e-9;
let x0 = 0.0;
let f_right = pinning_force(2.0e-9, x0, v0, w);
let f_left = pinning_force(-2.0e-9, x0, v0, w);
assert!(f_right > 0.0, "Force should push right for x > x₀");
assert!(f_left < 0.0, "Force should push left for x < x₀");
let f_center = pinning_force(x0, x0, v0, w);
assert!(
f_center.abs() < 1e-30,
"Force at center should be zero, got {}",
f_center
);
}
#[test]
fn test_depinning_invalid_params() {
assert!(DepinningParams::new(0.0, 8.0e5, 20.0e-9).is_err());
assert!(DepinningParams::new(1.0e-3, 0.0, 20.0e-9).is_err());
assert!(DepinningParams::new(1.0e-3, 8.0e5, 0.0).is_err());
assert!(DepinningParams::new(1.0e-3, 8.0e5, -1.0).is_err());
}
}