use crate::error::{Error, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
pub struct Xorshift64 {
state: u64,
}
impl Xorshift64 {
pub fn new(seed: u64) -> Self {
Self {
state: if seed == 0 {
0x5EED_DEAD_BEEF_CAFE
} else {
seed
},
}
}
pub fn next_u64(&mut self) -> u64 {
let mut s = self.state;
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
self.state = s;
s
}
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
}
pub fn next_normal_pair(&mut self) -> (f64, f64) {
let mut u1 = self.next_f64();
while u1 <= f64::EPSILON {
u1 = self.next_f64();
}
let u2 = self.next_f64();
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f64::consts::PI * u2;
(r * theta.cos(), r * theta.sin())
}
pub fn next_normal(&mut self) -> f64 {
self.next_normal_pair().0
}
pub fn next_unit_vector(&mut self) -> Vector3<f64> {
loop {
let x1 = 2.0 * self.next_f64() - 1.0;
let x2 = 2.0 * self.next_f64() - 1.0;
let s = x1 * x1 + x2 * x2;
if s < 1.0 && s > f64::EPSILON {
let factor = (1.0 - s).sqrt();
return Vector3::new(2.0 * x1 * factor, 2.0 * x2 * factor, 1.0 - 2.0 * s);
}
}
}
}
#[derive(Debug, Clone)]
pub enum DisorderType {
RandomAnisotropy,
RandomField,
SiteDisorder {
vacancy_fraction: f64,
},
}
#[derive(Debug, Clone)]
pub struct DisorderConfig {
pub disorder_type: DisorderType,
pub strength: f64,
pub correlation_length: f64,
pub seed: u64,
}
#[derive(Debug, Clone)]
pub struct RandomAnisotropyModel {
pub num_grains: usize,
pub grain_size: f64,
pub bulk_anisotropy: f64,
pub exchange_stiffness: f64,
pub grain_axes: Vec<Vector3<f64>>,
pub effective_anisotropy: f64,
}
impl RandomAnisotropyModel {
pub fn new(
num_grains: usize,
grain_size: f64,
bulk_anisotropy: f64,
exchange_stiffness: f64,
seed: u64,
) -> Result<Self> {
if num_grains == 0 {
return Err(Error::InvalidParameter {
param: "num_grains".to_string(),
reason: "must be greater than zero".to_string(),
});
}
if grain_size <= 0.0 {
return Err(Error::InvalidParameter {
param: "grain_size".to_string(),
reason: "must be positive".to_string(),
});
}
if bulk_anisotropy <= 0.0 {
return Err(Error::InvalidParameter {
param: "bulk_anisotropy".to_string(),
reason: "must be positive".to_string(),
});
}
if exchange_stiffness <= 0.0 {
return Err(Error::InvalidParameter {
param: "exchange_stiffness".to_string(),
reason: "must be positive".to_string(),
});
}
let mut rng = Xorshift64::new(seed);
let grain_axes: Vec<Vector3<f64>> =
(0..num_grains).map(|_| rng.next_unit_vector()).collect();
let l_ex = Self::exchange_length_static(exchange_stiffness, bulk_anisotropy);
let n_grains_in_volume = (l_ex / grain_size).powi(3);
let effective_anisotropy = if n_grains_in_volume > 1.0 {
bulk_anisotropy / n_grains_in_volume.sqrt()
} else {
bulk_anisotropy
};
Ok(Self {
num_grains,
grain_size,
bulk_anisotropy,
exchange_stiffness,
grain_axes,
effective_anisotropy,
})
}
pub fn exchange_length(&self) -> f64 {
Self::exchange_length_static(self.exchange_stiffness, self.bulk_anisotropy)
}
fn exchange_length_static(exchange_stiffness: f64, bulk_anisotropy: f64) -> f64 {
(exchange_stiffness / bulk_anisotropy).sqrt()
}
pub fn grains_in_exchange_volume(&self) -> f64 {
let l_ex = self.exchange_length();
(l_ex / self.grain_size).powi(3)
}
pub fn herzer_scaling_ratio(&self) -> f64 {
let numerator = self.effective_anisotropy;
let denominator = self.bulk_anisotropy.powi(4) * self.grain_size.powi(6)
/ self.exchange_stiffness.powi(3);
if denominator.abs() < f64::EPSILON {
return 0.0;
}
numerator / denominator
}
}
#[derive(Debug, Clone)]
pub struct Grain {
pub center: Vector3<f64>,
pub easy_axis: Vector3<f64>,
pub diameter: f64,
}
#[derive(Debug, Clone)]
pub struct GrainStructure {
pub grains: Vec<Grain>,
pub system_size: Vector3<f64>,
}
impl GrainStructure {
pub fn generate(
num_grains: usize,
system_size: Vector3<f64>,
mean_diameter: f64,
diameter_spread: f64,
seed: u64,
) -> Result<Self> {
if num_grains == 0 {
return Err(Error::InvalidParameter {
param: "num_grains".to_string(),
reason: "must be greater than zero".to_string(),
});
}
if mean_diameter <= 0.0 {
return Err(Error::InvalidParameter {
param: "mean_diameter".to_string(),
reason: "must be positive".to_string(),
});
}
if diameter_spread < 0.0 {
return Err(Error::InvalidParameter {
param: "diameter_spread".to_string(),
reason: "must be non-negative".to_string(),
});
}
if system_size.x <= 0.0 || system_size.y <= 0.0 || system_size.z <= 0.0 {
return Err(Error::InvalidParameter {
param: "system_size".to_string(),
reason: "all dimensions must be positive".to_string(),
});
}
let mut rng = Xorshift64::new(seed);
let mut grains = Vec::with_capacity(num_grains);
for _ in 0..num_grains {
let center = Vector3::new(rng.next_f64(), rng.next_f64(), rng.next_f64());
let easy_axis = rng.next_unit_vector();
let diameter = if diameter_spread > f64::EPSILON {
let u = rng.next_f64();
let d = mean_diameter + diameter_spread * (2.0 * u - 1.0);
if d > 0.0 {
d
} else {
mean_diameter * 0.1
}
} else {
mean_diameter
};
grains.push(Grain {
center,
easy_axis,
diameter,
});
}
Ok(Self {
grains,
system_size,
})
}
pub fn nearest_grain(&self, point: Vector3<f64>) -> Option<usize> {
if self.grains.is_empty() {
return None;
}
let mut best_idx = 0;
let mut best_dist_sq = f64::MAX;
for (i, grain) in self.grains.iter().enumerate() {
let dx = point.x - grain.center.x;
let dy = point.y - grain.center.y;
let dz = point.z - grain.center.z;
let dist_sq = dx * dx + dy * dy + dz * dz;
if dist_sq < best_dist_sq {
best_dist_sq = dist_sq;
best_idx = i;
}
}
Some(best_idx)
}
}
#[derive(Debug, Clone)]
pub struct RandomFieldDisorder {
pub fields: Vec<Vector3<f64>>,
pub field_strength: f64,
pub correlation_length: f64,
}
impl RandomFieldDisorder {
pub fn generate(
num_sites: usize,
field_strength: f64,
correlation_length: f64,
seed: u64,
) -> Result<Self> {
if num_sites == 0 {
return Err(Error::InvalidParameter {
param: "num_sites".to_string(),
reason: "must be greater than zero".to_string(),
});
}
if field_strength < 0.0 {
return Err(Error::InvalidParameter {
param: "field_strength".to_string(),
reason: "must be non-negative".to_string(),
});
}
let mut rng = Xorshift64::new(seed);
let mut fields = Vec::with_capacity(num_sites);
for _ in 0..num_sites {
let hx = field_strength * rng.next_normal();
let hy = field_strength * rng.next_normal();
let hz = field_strength * rng.next_normal();
fields.push(Vector3::new(hx, hy, hz));
}
Ok(Self {
fields,
field_strength,
correlation_length,
})
}
pub fn mean_magnitude(&self) -> f64 {
if self.fields.is_empty() {
return 0.0;
}
let sum: f64 = self
.fields
.iter()
.map(|f| (f.x * f.x + f.y * f.y + f.z * f.z).sqrt())
.sum();
sum / self.fields.len() as f64
}
}
#[derive(Debug, Clone)]
pub struct SurfaceRoughness {
pub height_map: Vec<Vec<f64>>,
pub nx: usize,
pub ny: usize,
pub rms_roughness: f64,
pub correlation_length: f64,
pub grid_spacing: f64,
}
impl SurfaceRoughness {
pub fn generate(
nx: usize,
ny: usize,
grid_spacing: f64,
rms_roughness: f64,
correlation_length: f64,
seed: u64,
) -> Result<Self> {
if nx == 0 || ny == 0 {
return Err(Error::InvalidParameter {
param: "grid dimensions".to_string(),
reason: "nx and ny must be greater than zero".to_string(),
});
}
if grid_spacing <= 0.0 {
return Err(Error::InvalidParameter {
param: "grid_spacing".to_string(),
reason: "must be positive".to_string(),
});
}
if rms_roughness < 0.0 {
return Err(Error::InvalidParameter {
param: "rms_roughness".to_string(),
reason: "must be non-negative".to_string(),
});
}
if correlation_length < 0.0 {
return Err(Error::InvalidParameter {
param: "correlation_length".to_string(),
reason: "must be non-negative".to_string(),
});
}
let mut rng = Xorshift64::new(seed);
let mut height_map: Vec<Vec<f64>> = Vec::with_capacity(nx);
for _ in 0..nx {
let mut row = Vec::with_capacity(ny);
for _ in 0..ny {
row.push(rms_roughness * rng.next_normal());
}
height_map.push(row);
}
if correlation_length > grid_spacing {
let kernel_radius = (correlation_length / grid_spacing).ceil() as usize;
let sigma_grid = correlation_length / grid_spacing;
let mut smoothed = vec![vec![0.0; ny]; nx];
for ix in 0..nx {
for iy in 0..ny {
let mut weight_sum = 0.0;
let mut val_sum = 0.0;
let jx_lo = ix.saturating_sub(kernel_radius);
let jx_hi = (ix + kernel_radius + 1).min(nx);
let jy_lo = iy.saturating_sub(kernel_radius);
let jy_hi = (iy + kernel_radius + 1).min(ny);
for (jx, height_row) in height_map.iter().enumerate().take(jx_hi).skip(jx_lo) {
for (jy, height_val) in
height_row.iter().enumerate().take(jy_hi).skip(jy_lo)
{
let dx = jx as f64 - ix as f64;
let dy = jy as f64 - iy as f64;
let r2 = dx * dx + dy * dy;
let w = (-r2 / (2.0 * sigma_grid * sigma_grid)).exp();
weight_sum += w;
val_sum += w * height_val;
}
}
smoothed[ix][iy] = if weight_sum > f64::EPSILON {
val_sum / weight_sum
} else {
height_map[ix][iy]
};
}
}
let current_rms = Self::compute_rms(&smoothed, nx, ny);
if current_rms > f64::EPSILON {
let scale = rms_roughness / current_rms;
for row in &mut smoothed {
for h in row.iter_mut() {
*h *= scale;
}
}
}
height_map = smoothed;
}
Ok(Self {
height_map,
nx,
ny,
rms_roughness,
correlation_length,
grid_spacing,
})
}
fn compute_rms(map: &[Vec<f64>], nx: usize, ny: usize) -> f64 {
let n = (nx * ny) as f64;
if n < 1.0 {
return 0.0;
}
let sum_sq: f64 = map.iter().flat_map(|row| row.iter()).map(|h| h * h).sum();
(sum_sq / n).sqrt()
}
pub fn actual_rms(&self) -> f64 {
Self::compute_rms(&self.height_map, self.nx, self.ny)
}
pub fn anisotropy_modification(&self, intrinsic_surface_anisotropy: f64) -> f64 {
if self.correlation_length < f64::EPSILON {
return 0.0;
}
let ratio = self.rms_roughness / self.correlation_length;
intrinsic_surface_anisotropy * ratio * ratio
}
}
fn erf_approx(x: f64) -> f64 {
const A1: f64 = 0.254_829_592;
const A2: f64 = -0.284_496_736;
const A3: f64 = 1.421_413_741;
const A4: f64 = -1.453_152_027;
const A5: f64 = 1.061_405_429;
const P: f64 = 0.327_591_1;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let ax = x.abs();
let t = 1.0 / (1.0 + P * ax);
let poly = (((A5 * t + A4) * t + A3) * t + A2) * t + A1;
let y = 1.0 - poly * t * (-(ax * ax)).exp();
sign * y
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GradingLaw {
Linear,
Exponential,
ErrorFunction,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GradedInterface {
pub law: GradingLaw,
pub center: f64,
pub width: f64,
pub value_left: f64,
pub value_right: f64,
pub center_offsets: Vec<f64>,
pub jitter_rms: f64,
pub seed: u64,
}
impl GradedInterface {
pub fn generate(
law: GradingLaw,
center: f64,
width: f64,
value_left: f64,
value_right: f64,
num_sites: usize,
jitter_rms: f64,
seed: u64,
) -> Result<Self> {
if !center.is_finite() {
return Err(Error::InvalidParameter {
param: "center".to_string(),
reason: "must be finite".to_string(),
});
}
if !(width.is_finite() && width > 0.0) {
return Err(Error::InvalidParameter {
param: "width".to_string(),
reason: "must be finite and strictly positive".to_string(),
});
}
if !value_left.is_finite() {
return Err(Error::InvalidParameter {
param: "value_left".to_string(),
reason: "must be finite".to_string(),
});
}
if !value_right.is_finite() {
return Err(Error::InvalidParameter {
param: "value_right".to_string(),
reason: "must be finite".to_string(),
});
}
if num_sites == 0 {
return Err(Error::InvalidParameter {
param: "num_sites".to_string(),
reason: "must be greater than zero".to_string(),
});
}
if !(jitter_rms.is_finite() && jitter_rms >= 0.0) {
return Err(Error::InvalidParameter {
param: "jitter_rms".to_string(),
reason: "must be finite and non-negative".to_string(),
});
}
let mut rng = Xorshift64::new(seed);
let center_offsets: Vec<f64> = (0..num_sites)
.map(|_| jitter_rms * rng.next_normal())
.collect();
Ok(Self {
law,
center,
width,
value_left,
value_right,
center_offsets,
jitter_rms,
seed,
})
}
pub fn value_at(&self, position: f64) -> f64 {
self.value_at_center(position, self.center)
}
pub fn value_at_site(&self, position: f64, site_index: usize) -> f64 {
let local_center =
self.center + self.center_offsets.get(site_index).copied().unwrap_or(0.0);
self.value_at_center(position, local_center)
}
pub fn barycenter(&self) -> f64 {
0.5 * (self.value_left + self.value_right)
}
fn value_at_center(&self, position: f64, center: f64) -> f64 {
let d = position - center;
let mean = 0.5 * (self.value_left + self.value_right);
let half_delta = 0.5 * (self.value_right - self.value_left);
match self.law {
GradingLaw::Linear => {
let t = (d / (0.5 * self.width)).clamp(-1.0, 1.0);
mean + half_delta * t
},
GradingLaw::Exponential => {
if d >= 0.0 {
self.value_right - half_delta * (-d / self.width).exp()
} else {
self.value_left + half_delta * (d / self.width).exp()
}
},
GradingLaw::ErrorFunction => mean + half_delta * erf_approx(d / self.width),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xorshift64_deterministic() {
let mut rng1 = Xorshift64::new(12345);
let mut rng2 = Xorshift64::new(12345);
for _ in 0..100 {
assert_eq!(rng1.next_u64(), rng2.next_u64());
}
}
#[test]
fn test_exchange_length_calculation() {
let ram =
RandomAnisotropyModel::new(10, 10.0e-9, 1.0e4, 1.0e-11, 42).expect("valid parameters");
let l_ex = ram.exchange_length();
let expected = (1.0e-11_f64 / 1.0e4_f64).sqrt();
assert!(
(l_ex - expected).abs() / expected < 1e-10,
"Exchange length mismatch: got {}, expected {}",
l_ex,
expected
);
}
#[test]
fn test_herzer_scaling_k_eff_less_than_bulk() {
let ram =
RandomAnisotropyModel::new(1000, 5.0e-9, 1.0e4, 1.0e-11, 99).expect("valid parameters");
assert!(
ram.effective_anisotropy < ram.bulk_anisotropy,
"K_eff ({}) should be less than K₁ ({}) for small grains",
ram.effective_anisotropy,
ram.bulk_anisotropy
);
}
#[test]
fn test_herzer_scaling_proportionality() {
let d1 = 5.0e-9;
let d2 = 10.0e-9;
let k1_bulk = 1.0e4;
let a = 1.0e-11;
let ram1 = RandomAnisotropyModel::new(100, d1, k1_bulk, a, 1).expect("valid parameters");
let ram2 = RandomAnisotropyModel::new(100, d2, k1_bulk, a, 1).expect("valid parameters");
let ratio = ram2.effective_anisotropy / ram1.effective_anisotropy;
let expected_ratio = (d2 / d1).powf(1.5);
assert!(
(ratio - expected_ratio).abs() / expected_ratio < 0.01,
"Herzer scaling ratio: got {}, expected {}",
ratio,
expected_ratio
);
}
#[test]
fn test_random_anisotropy_axes_are_unit_vectors() {
let ram =
RandomAnisotropyModel::new(200, 10.0e-9, 5.0e4, 1.0e-11, 7).expect("valid parameters");
for (i, axis) in ram.grain_axes.iter().enumerate() {
let norm = (axis.x * axis.x + axis.y * axis.y + axis.z * axis.z).sqrt();
assert!(
(norm - 1.0).abs() < 1e-10,
"Grain axis {} has norm {}, expected 1.0",
i,
norm
);
}
}
#[test]
fn test_grain_structure_generation() {
let gs = GrainStructure::generate(
50,
Vector3::new(100.0e-9, 100.0e-9, 20.0e-9),
10.0e-9,
2.0e-9,
42,
)
.expect("valid parameters");
assert_eq!(gs.grains.len(), 50);
for grain in &gs.grains {
assert!(grain.center.x >= 0.0 && grain.center.x < 1.0);
assert!(grain.center.y >= 0.0 && grain.center.y < 1.0);
assert!(grain.center.z >= 0.0 && grain.center.z < 1.0);
assert!(grain.diameter > 0.0);
}
}
#[test]
fn test_nearest_grain_assignment() {
let gs = GrainStructure::generate(
10,
Vector3::new(100.0e-9, 100.0e-9, 100.0e-9),
20.0e-9,
0.0,
123,
)
.expect("valid parameters");
let center = gs.grains[0].center;
let idx = gs.nearest_grain(center).expect("should find nearest grain");
assert_eq!(idx, 0);
}
#[test]
fn test_random_field_disorder() {
let rfd = RandomFieldDisorder::generate(500, 0.01, 0.0, 55).expect("valid parameters");
assert_eq!(rfd.fields.len(), 500);
let mean_mag = rfd.mean_magnitude();
let expected_order = 0.01 * (8.0_f64 / std::f64::consts::PI).sqrt();
assert!(
(mean_mag - expected_order).abs() / expected_order < 0.3,
"Mean random field magnitude {} deviates too far from expected {}",
mean_mag,
expected_order
);
}
#[test]
fn test_surface_roughness_rms() {
let sr =
SurfaceRoughness::generate(64, 64, 1.0e-9, 0.5e-9, 0.0, 77).expect("valid parameters");
let actual = sr.actual_rms();
let target = 0.5e-9;
assert!(
(actual - target).abs() / target < 0.15,
"RMS roughness {} deviates too far from target {}",
actual,
target
);
}
#[test]
fn test_surface_anisotropy_modification() {
let sr = SurfaceRoughness::generate(32, 32, 1.0e-9, 0.3e-9, 5.0e-9, 88)
.expect("valid parameters");
let k_s = 1.0e-3; let delta_k = sr.anisotropy_modification(k_s);
let expected = k_s * (0.3e-9 / 5.0e-9) * (0.3e-9 / 5.0e-9);
assert!(
(delta_k - expected).abs() / expected < 1e-10,
"Anisotropy modification {} != expected {}",
delta_k,
expected
);
}
#[test]
fn test_invalid_parameters() {
assert!(RandomAnisotropyModel::new(0, 10.0e-9, 1.0e4, 1.0e-11, 1).is_err());
assert!(RandomAnisotropyModel::new(10, -1.0, 1.0e4, 1.0e-11, 1).is_err());
assert!(RandomAnisotropyModel::new(10, 10.0e-9, -1.0, 1.0e-11, 1).is_err());
assert!(RandomAnisotropyModel::new(10, 10.0e-9, 1.0e4, -1.0, 1).is_err());
assert!(RandomFieldDisorder::generate(0, 0.01, 0.0, 1).is_err());
assert!(SurfaceRoughness::generate(0, 10, 1.0e-9, 0.5e-9, 0.0, 1).is_err());
}
#[test]
fn test_graded_interface_linear_monotonic_and_endpoints() {
let profile =
GradedInterface::generate(GradingLaw::Linear, 0.0, 4.0e-9, 1.0e5, 3.0e5, 1, 0.0, 1)
.expect("valid parameters");
assert!((profile.value_at(-10.0e-9) - 1.0e5).abs() < 1e-6);
assert!((profile.value_at(10.0e-9) - 3.0e5).abs() < 1e-6);
assert!((profile.value_at(0.0) - 2.0e5).abs() < 1e-6);
let mut prev = profile.value_at(-10.0e-9);
for i in 1..=400 {
let x = -10.0e-9 + i as f64 * (20.0e-9 / 400.0);
let v = profile.value_at(x);
assert!(
v >= prev - 1e-12,
"Linear profile not monotonic at x = {}: {} < {}",
x,
v,
prev
);
prev = v;
}
}
#[test]
fn test_graded_interface_exponential_monotonic_and_endpoints() {
let width = 2.0e-9;
let profile =
GradedInterface::generate(GradingLaw::Exponential, 0.0, width, 1.0e5, 3.0e5, 1, 0.0, 2)
.expect("valid parameters");
let far = 10.0 * width;
let v_left = profile.value_at(-far);
let v_right = profile.value_at(far);
assert!(
(v_left - 1.0e5).abs() / 1.0e5 < 1e-3,
"Exponential left endpoint {} != 1e5",
v_left
);
assert!(
(v_right - 3.0e5).abs() / 3.0e5 < 1e-3,
"Exponential right endpoint {} != 3e5",
v_right
);
let mut prev = profile.value_at(-far);
for i in 1..=400 {
let x = -far + i as f64 * (2.0 * far / 400.0);
let v = profile.value_at(x);
assert!(
v >= prev - 1e-9,
"Exponential profile not monotonic at x = {}: {} < {}",
x,
v,
prev
);
prev = v;
}
}
#[test]
fn test_graded_interface_error_function_monotonic_and_endpoints() {
let width = 1.5e-9;
let profile = GradedInterface::generate(
GradingLaw::ErrorFunction,
0.0,
width,
2.0e5,
6.0e5,
1,
0.0,
3,
)
.expect("valid parameters");
let far = 10.0 * width;
let v_left = profile.value_at(-far);
let v_right = profile.value_at(far);
assert!((v_left - 2.0e5).abs() < 1.0, "erf left endpoint {}", v_left);
assert!(
(v_right - 6.0e5).abs() < 1.0,
"erf right endpoint {}",
v_right
);
let mut prev = profile.value_at(-far);
for i in 1..=400 {
let x = -far + i as f64 * (2.0 * far / 400.0);
let v = profile.value_at(x);
assert!(
v >= prev - 1e-9,
"ErrorFunction profile not monotonic at x = {}: {} < {}",
x,
v,
prev
);
prev = v;
}
}
#[test]
fn test_graded_interface_barycenter_symmetric_profile() {
for law in [
GradingLaw::Linear,
GradingLaw::Exponential,
GradingLaw::ErrorFunction,
] {
let profile = GradedInterface::generate(law, 5.0e-9, 3.0e-9, 1.0e5, 7.0e5, 1, 0.0, 7)
.expect("valid parameters");
let expected_sum = profile.value_left + profile.value_right;
assert!(
(profile.barycenter() - 4.0e5).abs() < 1e-6,
"barycenter mismatch for {:?}",
law
);
for &d in &[0.0, 0.5e-9, 1.0e-9, 3.0e-9, 6.0e-9, 15.0e-9, 30.0e-9] {
let v_plus = profile.value_at(profile.center + d);
let v_minus = profile.value_at(profile.center - d);
let sum = v_plus + v_minus;
assert!(
(sum - expected_sum).abs() / expected_sum.abs() < 1e-6,
"{:?}: value(c+{}) + value(c-{}) = {}, expected {}",
law,
d,
d,
sum,
expected_sum
);
}
let n = 201usize; let half_span = 20.0 * profile.width;
let mut sum = 0.0;
for i in 0..n {
let frac = i as f64 / (n - 1) as f64; let x = profile.center - half_span + frac * 2.0 * half_span;
sum += profile.value_at(x);
}
let mean = sum / n as f64;
assert!(
(mean - profile.barycenter()).abs() / profile.barycenter().abs() < 1e-3,
"{:?}: discretised mean {} != barycenter {}",
law,
mean,
profile.barycenter()
);
}
}
#[test]
fn test_graded_interface_invalid_parameters() {
assert!(
GradedInterface::generate(GradingLaw::Linear, 0.0, 0.0, 0.0, 1.0, 1, 0.0, 1).is_err()
);
assert!(
GradedInterface::generate(GradingLaw::Linear, 0.0, -1.0e-9, 0.0, 1.0, 1, 0.0, 1)
.is_err()
);
assert!(
GradedInterface::generate(GradingLaw::Linear, 0.0, f64::NAN, 0.0, 1.0, 1, 0.0, 1)
.is_err()
);
assert!(GradedInterface::generate(
GradingLaw::Linear,
0.0,
1.0e-9,
f64::NAN,
1.0,
1,
0.0,
1
)
.is_err());
assert!(GradedInterface::generate(
GradingLaw::Linear,
0.0,
1.0e-9,
0.0,
f64::INFINITY,
1,
0.0,
1
)
.is_err());
assert!(GradedInterface::generate(
GradingLaw::Linear,
f64::NAN,
1.0e-9,
0.0,
1.0,
1,
0.0,
1
)
.is_err());
assert!(
GradedInterface::generate(GradingLaw::Linear, 0.0, 1.0e-9, 0.0, 1.0, 0, 0.0, 1)
.is_err()
);
assert!(
GradedInterface::generate(GradingLaw::Linear, 0.0, 1.0e-9, 0.0, 1.0, 1, -1.0, 1)
.is_err()
);
assert!(GradedInterface::generate(
GradingLaw::Linear,
0.0,
1.0e-9,
0.0,
1.0,
1,
f64::NAN,
1
)
.is_err());
assert!(
GradedInterface::generate(GradingLaw::Linear, 0.0, 1.0e-9, 0.0, 1.0, 1, 0.0, 1).is_ok()
);
}
#[test]
fn test_graded_interface_roughness_jitter_deterministic_and_scaled() {
let n_sites = 200usize;
let jitter_rms = 0.5e-9;
let p1 = GradedInterface::generate(
GradingLaw::ErrorFunction,
0.0,
2.0e-9,
1.0e5,
3.0e5,
n_sites,
jitter_rms,
123,
)
.expect("valid parameters");
let p2 = GradedInterface::generate(
GradingLaw::ErrorFunction,
0.0,
2.0e-9,
1.0e5,
3.0e5,
n_sites,
jitter_rms,
123,
)
.expect("valid parameters");
assert_eq!(p1.center_offsets.len(), n_sites);
for (a, b) in p1.center_offsets.iter().zip(p2.center_offsets.iter()) {
assert_eq!(a, b);
}
let mean_sq: f64 = p1.center_offsets.iter().map(|o| o * o).sum::<f64>() / n_sites as f64;
let actual_rms = mean_sq.sqrt();
assert!(
(actual_rms - jitter_rms).abs() / jitter_rms < 0.3,
"actual jitter RMS {} deviates too far from requested {}",
actual_rms,
jitter_rms
);
let fallback = p1.value_at_site(0.0, n_sites + 1000);
let nominal = p1.value_at(0.0);
assert!((fallback - nominal).abs() < 1e-9);
}
}