use crate::error::{Error, Result};
use crate::math::{CMatrix, Complex};
use crate::orbitronics::crystal_field::{
crystal_field_hamiltonian, kron, orbital_angular_momentum_operators, pauli_x, pauli_y, pauli_z,
soc_hamiltonian, CrystalFieldEnvironment, DOrbital,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const M_L_DESCENDING: [i32; 5] = [2, 1, 0, -1, -2];
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FreeIonTerm {
pub s: f64,
pub l: f64,
pub j: f64,
}
pub fn free_ion_term(n: u8) -> Result<FreeIonTerm> {
if n > 10 {
return Err(Error::InvalidParameter {
param: "n".to_string(),
reason: "a d-shell holds at most 10 electrons".to_string(),
});
}
let n_eff = n.min(10 - n);
let s = f64::from(n_eff) / 2.0;
let l_sum: i32 = M_L_DESCENDING.iter().take(n_eff as usize).sum();
let l = f64::from(l_sum);
let j = if n <= 5 { (l - s).abs() } else { l + s };
Ok(FreeIonTerm { s, l, j })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum GroundTermSymmetry {
A,
E,
T,
}
impl GroundTermSymmetry {
#[inline]
pub fn is_quenched(self) -> bool {
matches!(self, GroundTermSymmetry::A | GroundTermSymmetry::E)
}
}
#[derive(Debug, Clone)]
struct LevelOccupation {
members: Vec<DOrbital>,
electrons: usize,
}
impl LevelOccupation {
fn degeneracy(&self) -> usize {
self.members.len()
}
fn combinatorial_degeneracy(&self) -> u64 {
let degeneracy = self.degeneracy();
let k = self.electrons.min(2 * degeneracy - self.electrons);
binomial_coefficient(degeneracy as u64, k as u64)
}
fn unpaired_electrons(&self) -> usize {
let degeneracy = self.degeneracy();
if self.electrons <= degeneracy {
self.electrons
} else {
2 * degeneracy - self.electrons
}
}
}
fn binomial_coefficient(n: u64, k: u64) -> u64 {
if k > n {
return 0;
}
let k = k.min(n - k);
let mut result: u64 = 1;
for i in 0..k {
result = result * (n - i) / (i + 1);
}
result
}
fn fill_levels(
levels: &[(f64, Vec<DOrbital>)],
n: u8,
pairing_energy: f64,
) -> Vec<LevelOccupation> {
let mut occupations: Vec<LevelOccupation> = levels
.iter()
.map(|(_, members)| LevelOccupation {
members: members.clone(),
electrons: 0,
})
.collect();
for _ in 0..n {
let mut best_idx: Option<usize> = None;
let mut best_cost = f64::INFINITY;
for (idx, level) in occupations.iter().enumerate() {
let degeneracy = level.degeneracy();
if level.electrons >= 2 * degeneracy {
continue;
}
let level_energy = levels[idx].0;
let cost = if level.electrons < degeneracy {
level_energy
} else {
level_energy + pairing_energy
};
if cost < best_cost {
best_cost = cost;
best_idx = Some(idx);
}
}
if let Some(idx) = best_idx {
occupations[idx].electrons += 1;
}
}
occupations
}
fn orbital_sub_block(op: &CMatrix, members: &[DOrbital]) -> Result<CMatrix> {
let dim = members.len();
let mut rows = vec![vec![Complex::ZERO; dim]; dim];
for (a, &oa) in members.iter().enumerate() {
for (b, &ob) in members.iter().enumerate() {
rows[a][b] = op.get(oa.index(), ob.index());
}
}
CMatrix::from_rows(rows)
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CrystalFieldModel {
pub n: u8,
pub environment: CrystalFieldEnvironment,
pub ten_dq: f64,
pub pairing_energy: f64,
pub soc_lambda: f64,
}
impl CrystalFieldModel {
pub fn new(
n: u8,
environment: CrystalFieldEnvironment,
ten_dq: f64,
pairing_energy: f64,
soc_lambda: f64,
) -> Result<Self> {
if !(1..=9).contains(&n) {
return Err(Error::InvalidParameter {
param: "n".to_string(),
reason: "d-electron count must be between 1 and 9 for a partially-filled d-shell"
.to_string(),
});
}
if ten_dq < 0.0 {
return Err(Error::InvalidParameter {
param: "ten_dq".to_string(),
reason: "crystal-field splitting magnitude must be non-negative".to_string(),
});
}
if pairing_energy <= 0.0 {
return Err(Error::InvalidParameter {
param: "pairing_energy".to_string(),
reason: "mean pairing energy must be positive".to_string(),
});
}
Ok(Self {
n,
environment,
ten_dq,
pairing_energy,
soc_lambda,
})
}
pub fn free_ion_term(&self) -> Result<FreeIonTerm> {
free_ion_term(self.n)
}
fn orbital_energy_levels(&self) -> Result<Vec<(f64, Vec<DOrbital>)>> {
let h_cf = crystal_field_hamiltonian(self.environment, self.ten_dq)?;
let mut orbital_energies: Vec<(DOrbital, f64)> = DOrbital::all()
.into_iter()
.map(|orbital| (orbital, h_cf.get(orbital.index(), orbital.index()).re))
.collect();
orbital_energies.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let tolerance = 1e-9 * (self.ten_dq.abs() + 1.0);
let mut levels: Vec<(f64, Vec<DOrbital>)> = Vec::new();
for (orbital, e) in orbital_energies {
if let Some(last) = levels.last_mut() {
if (e - last.0).abs() < tolerance {
last.1.push(orbital);
continue;
}
}
levels.push((e, vec![orbital]));
}
Ok(levels)
}
fn ground_configuration(&self) -> Result<Vec<LevelOccupation>> {
let levels = self.orbital_energy_levels()?;
Ok(fill_levels(&levels, self.n, self.pairing_energy))
}
pub fn ground_state_spin(&self) -> Result<f64> {
let occupations = self.ground_configuration()?;
let unpaired: usize = occupations
.iter()
.map(LevelOccupation::unpaired_electrons)
.sum();
Ok(unpaired as f64 / 2.0)
}
pub fn is_high_spin(&self) -> Result<bool> {
let s_actual = self.ground_state_spin()?;
let s_free = free_ion_term(self.n)?.s;
Ok((s_actual - s_free).abs() < 1e-9)
}
fn ground_configuration_degeneracy(&self) -> Result<u64> {
let occupations = self.ground_configuration()?;
Ok(occupations
.iter()
.map(LevelOccupation::combinatorial_degeneracy)
.max()
.unwrap_or(1))
}
pub fn ground_term_symmetry(&self) -> Result<GroundTermSymmetry> {
let degeneracy = self.ground_configuration_degeneracy()?;
Ok(match degeneracy {
1 => GroundTermSymmetry::A,
2 => GroundTermSymmetry::E,
_ => GroundTermSymmetry::T,
})
}
pub fn is_orbitally_quenched(&self) -> Result<bool> {
Ok(self.ground_term_symmetry()?.is_quenched())
}
pub fn lande_g_factor(&self) -> Result<f64> {
let term = self.free_ion_term()?;
if term.j < 1e-9 {
return Err(Error::InvalidParameter {
param: "j".to_string(),
reason: format!(
"Lande g-factor is undefined for a J=0 free-ion ground term (d^{}: S={:.1}, \
L={:.0}, J=0 - the leading-order magnetic moment vanishes identically, e.g. \
Cr2+/Mn3+ high-spin 5D0)",
self.n, term.s, term.l
),
});
}
let FreeIonTerm { s, l, j } = term;
Ok(1.0 + (j * (j + 1.0) + s * (s + 1.0) - l * (l + 1.0)) / (2.0 * j * (j + 1.0)))
}
pub fn effective_moment_spin_only(&self) -> Result<f64> {
let s = self.ground_state_spin()?;
Ok(2.0 * (s * (s + 1.0)).sqrt())
}
pub fn effective_moment_lande(&self) -> Result<f64> {
let g = self.lande_g_factor()?;
let j = self.free_ion_term()?.j;
Ok(g * (j * (j + 1.0)).sqrt())
}
pub fn single_particle_hamiltonian(&self) -> Result<CMatrix> {
soc_hamiltonian(self.environment, self.ten_dq, self.soc_lambda)
}
fn frontier_level(&self) -> Result<Option<LevelOccupation>> {
let occupations = self.ground_configuration()?;
Ok(occupations
.into_iter()
.find(|o| o.combinatorial_degeneracy() > 1))
}
fn frontier_reduced_hamiltonian(&self) -> Result<Option<(CMatrix, CMatrix)>> {
let Some(level) = self.frontier_level()? else {
return Ok(None);
};
let (lx, ly, lz) = orbital_angular_momentum_operators()?;
let lx_sub = orbital_sub_block(&lx, &level.members)?;
let ly_sub = orbital_sub_block(&ly, &level.members)?;
let lz_sub = orbital_sub_block(&lz, &level.members)?;
let sx = pauli_x()?;
let sy = pauli_y()?;
let sz = pauli_z()?;
let lx_sx = kron(&lx_sub, &sx)?;
let ly_sy = kron(&ly_sub, &sy)?;
let lz_sz = kron(&lz_sub, &sz)?;
let h_reduced = lx_sx
.add(&ly_sy)?
.add(&lz_sz)?
.scale_real(self.soc_lambda * 0.5);
let lz_ext = kron(&lz_sub, &CMatrix::eye(2))?;
Ok(Some((h_reduced, lz_ext)))
}
pub fn expectation_lz_ground_state(&self) -> Result<f64> {
let Some((h_reduced, lz_ext)) = self.frontier_reduced_hamiltonian()? else {
return Ok(0.0);
};
let (_, eigenvectors) = h_reduced.hermitian_eigendecomposition()?;
let dim = h_reduced.n();
let ground: Vec<Complex> = (0..dim).map(|i| eigenvectors.get(i, 0)).collect();
let mut expectation = Complex::ZERO;
for (i, gi) in ground.iter().enumerate() {
let mut row_sum = Complex::ZERO;
for (j, gj) in ground.iter().enumerate() {
row_sum = row_sum.add(&lz_ext.get(i, j).mul(gj));
}
expectation = expectation.add(&gi.conj().mul(&row_sum));
}
Ok(expectation.re)
}
pub fn ground_manifold_orbital_moment_rms(&self) -> Result<f64> {
let Some((h_reduced, lz_ext)) = self.frontier_reduced_hamiltonian()? else {
return Ok(0.0);
};
let (energies, eigenvectors) = h_reduced.hermitian_eigendecomposition()?;
let dim = h_reduced.n();
let e0 = energies[0];
let tolerance = 1e-7 * (e0.abs() + 1.0);
let ground_indices: Vec<usize> = energies
.iter()
.enumerate()
.filter(|&(_, &e)| (e - e0).abs() < tolerance)
.map(|(idx, _)| idx)
.collect();
let mut sum_lz_squared = 0.0_f64;
for &k in &ground_indices {
let v: Vec<Complex> = (0..dim).map(|i| eigenvectors.get(i, k)).collect();
let mut lzv = vec![Complex::ZERO; dim];
for (i, slot) in lzv.iter_mut().enumerate() {
let mut acc = Complex::ZERO;
for (j, vj) in v.iter().enumerate() {
acc = acc.add(&lz_ext.get(i, j).mul(vj));
}
*slot = acc;
}
sum_lz_squared += lzv.iter().map(Complex::norm_sq).sum::<f64>();
}
Ok((sum_lz_squared / ground_indices.len() as f64).sqrt())
}
}
impl CrystalFieldModel {
pub fn ti3_plus() -> Result<Self> {
Self::new(1, CrystalFieldEnvironment::Octahedral, 2.5, 3.0, 0.019)
}
pub fn v3_plus() -> Result<Self> {
Self::new(2, CrystalFieldEnvironment::Octahedral, 2.3, 3.0, 0.026)
}
pub fn cr3_plus() -> Result<Self> {
Self::new(3, CrystalFieldEnvironment::Octahedral, 2.2, 3.0, 0.034)
}
pub fn mn3_plus_high_spin() -> Result<Self> {
Self::new(4, CrystalFieldEnvironment::Octahedral, 1.8, 3.3, 0.044)
}
pub fn fe3_plus() -> Result<Self> {
Self::new(5, CrystalFieldEnvironment::Octahedral, 1.7, 3.7, 0.057)
}
pub fn mn2_plus() -> Result<Self> {
Self::new(5, CrystalFieldEnvironment::Octahedral, 1.0, 3.2, 0.042)
}
pub fn fe2_plus_high_spin() -> Result<Self> {
Self::new(6, CrystalFieldEnvironment::Octahedral, 1.3, 2.6, 0.051)
}
pub fn fe2_plus_low_spin() -> Result<Self> {
Self::new(6, CrystalFieldEnvironment::Octahedral, 3.8, 2.6, 0.051)
}
pub fn co2_plus() -> Result<Self> {
Self::new(7, CrystalFieldEnvironment::Octahedral, 1.1, 2.8, 0.064)
}
pub fn ni2_plus() -> Result<Self> {
Self::new(8, CrystalFieldEnvironment::Octahedral, 1.05, 2.5, 0.078)
}
pub fn cu2_plus() -> Result<Self> {
Self::new(9, CrystalFieldEnvironment::Octahedral, 1.56, 2.5, 0.103)
}
pub fn co3_plus_low_spin() -> Result<Self> {
Self::new(6, CrystalFieldEnvironment::Octahedral, 3.0, 2.6, 0.069)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_free_ion_term_hund_rule_table() {
let expected: [(u8, f64, f64); 9] = [
(1, 0.5, 2.0),
(2, 1.0, 3.0),
(3, 1.5, 3.0),
(4, 2.0, 2.0),
(5, 2.5, 0.0),
(6, 2.0, 2.0),
(7, 1.5, 3.0),
(8, 1.0, 3.0),
(9, 0.5, 2.0),
];
for (n, s, l) in expected {
let term = free_ion_term(n).expect("valid n");
assert!(
(term.s - s).abs() < 1e-12,
"d{}: S expected {}, got {}",
n,
s,
term.s
);
assert!(
(term.l - l).abs() < 1e-12,
"d{}: L expected {}, got {}",
n,
l,
term.l
);
}
}
#[test]
fn test_electron_hole_symmetry_of_s_and_l() {
for n in 1..=4u8 {
let term_n = free_ion_term(n).expect("valid");
let term_hole = free_ion_term(10 - n).expect("valid");
assert!(
(term_n.l - term_hole.l).abs() < 1e-12,
"L({}) = {} != L({}) = {}",
n,
term_n.l,
10 - n,
term_hole.l
);
assert!(
(term_n.s - term_hole.s).abs() < 1e-12,
"S({}) != S({})",
n,
10 - n
);
}
}
#[test]
fn test_hund_third_rule_j_values() {
let ti = free_ion_term(1).expect("valid");
assert!((ti.j - 1.5).abs() < 1e-12);
let cu = free_ion_term(9).expect("valid");
assert!((cu.j - 2.5).abs() < 1e-12);
let d5 = free_ion_term(5).expect("valid");
assert!((d5.j - 2.5).abs() < 1e-12);
let d4 = free_ion_term(4).expect("valid");
assert!(d4.j.abs() < 1e-12);
}
#[test]
fn test_free_ion_term_rejects_out_of_range_n() {
assert!(free_ion_term(11).is_err());
}
#[test]
fn test_model_validation_rejects_bad_n() {
assert!(
CrystalFieldModel::new(0, CrystalFieldEnvironment::Octahedral, 2.0, 2.5, 0.05).is_err()
);
assert!(
CrystalFieldModel::new(10, CrystalFieldEnvironment::Octahedral, 2.0, 2.5, 0.05)
.is_err()
);
}
#[test]
fn test_model_validation_rejects_negative_ten_dq() {
assert!(
CrystalFieldModel::new(5, CrystalFieldEnvironment::Octahedral, -1.0, 2.5, 0.05)
.is_err()
);
}
#[test]
fn test_model_validation_rejects_non_positive_pairing_energy() {
assert!(
CrystalFieldModel::new(5, CrystalFieldEnvironment::Octahedral, 2.0, 0.0, 0.05).is_err()
);
assert!(
CrystalFieldModel::new(5, CrystalFieldEnvironment::Octahedral, 2.0, -1.0, 0.05)
.is_err()
);
}
#[test]
fn test_large_ten_dq_forces_low_spin_d4_to_d7() {
for n in 4..=7u8 {
let model =
CrystalFieldModel::new(n, CrystalFieldEnvironment::Octahedral, 1.0e6, 2.5, 0.05)
.expect("valid model");
assert!(
!model.is_high_spin().expect("computes"),
"d{} should be forced low-spin",
n
);
}
}
#[test]
fn test_lande_g_factor_ti3_plus() {
let ti = CrystalFieldModel::ti3_plus().expect("preset builds");
let g = ti.lande_g_factor().expect("J != 0");
assert!(
(g - 0.8).abs() < 1e-9,
"Ti3+ Lande g should be 0.8, got {}",
g
);
}
#[test]
fn test_lande_g_factor_is_two_when_l_is_zero() {
let fe3 = CrystalFieldModel::fe3_plus().expect("preset builds");
let g = fe3.lande_g_factor().expect("J != 0");
assert!(
(g - 2.0).abs() < 1e-9,
"L=0 term should have g=2, got {}",
g
);
let mu_lande = fe3.effective_moment_lande().expect("J != 0");
let mu_so = fe3.effective_moment_spin_only().expect("always defined");
assert!((mu_lande - mu_so).abs() < 1e-9);
}
#[test]
fn test_lande_g_factor_undefined_for_j_zero_term() {
let mn3 = CrystalFieldModel::mn3_plus_high_spin().expect("preset builds");
assert!(mn3.free_ion_term().expect("computes").j.abs() < 1e-9);
assert!(
mn3.lande_g_factor().is_err(),
"g should be undefined for a J=0 term"
);
assert!(
mn3.effective_moment_lande().is_err(),
"mu_lande should be undefined for J=0"
);
let mu_so = mn3
.effective_moment_spin_only()
.expect("spin-only always defined");
assert!(
(mu_so - 4.899).abs() < 1e-2,
"Mn3+ HS spin-only moment should be ~4.90, got {}",
mu_so
);
}
#[test]
fn test_spin_only_moments_match_textbook_values() {
let fe3 = CrystalFieldModel::fe3_plus()
.expect("preset")
.effective_moment_spin_only()
.expect("ok");
let ni2 = CrystalFieldModel::ni2_plus()
.expect("preset")
.effective_moment_spin_only()
.expect("ok");
let cr3 = CrystalFieldModel::cr3_plus()
.expect("preset")
.effective_moment_spin_only()
.expect("ok");
let cu2 = CrystalFieldModel::cu2_plus()
.expect("preset")
.effective_moment_spin_only()
.expect("ok");
assert!(
(fe3 - 5.92).abs() < 0.01,
"Fe3+ mu_so expected 5.92, got {}",
fe3
);
assert!(
(ni2 - 2.83).abs() < 0.01,
"Ni2+ mu_so expected 2.83, got {}",
ni2
);
assert!(
(cr3 - 3.87).abs() < 0.01,
"Cr3+ mu_so expected 3.87, got {}",
cr3
);
assert!(
(cu2 - 1.73).abs() < 0.01,
"Cu2+ mu_so expected 1.73, got {}",
cu2
);
}
#[test]
fn test_is_orbitally_quenched_matches_symmetry() {
let ti = CrystalFieldModel::ti3_plus().expect("preset");
assert!(
!ti.is_orbitally_quenched().expect("computes"),
"Ti3+ (T2g) should be unquenched"
);
let cr = CrystalFieldModel::cr3_plus().expect("preset");
assert!(
cr.is_orbitally_quenched().expect("computes"),
"Cr3+ (A2g) should be quenched"
);
let cu = CrystalFieldModel::cu2_plus().expect("preset");
assert!(
cu.is_orbitally_quenched().expect("computes"),
"Cu2+ (Eg) should be quenched"
);
}
#[test]
fn test_all_presets_match_expected_physics() {
struct Expected {
name: &'static str,
n: u8,
s: f64,
high_spin: bool,
symmetry: GroundTermSymmetry,
}
let cases = [
Expected {
name: "Ti3+",
n: 1,
s: 0.5,
high_spin: true,
symmetry: GroundTermSymmetry::T,
},
Expected {
name: "V3+",
n: 2,
s: 1.0,
high_spin: true,
symmetry: GroundTermSymmetry::T,
},
Expected {
name: "Cr3+",
n: 3,
s: 1.5,
high_spin: true,
symmetry: GroundTermSymmetry::A,
},
Expected {
name: "Mn3+ HS",
n: 4,
s: 2.0,
high_spin: true,
symmetry: GroundTermSymmetry::E,
},
Expected {
name: "Fe3+",
n: 5,
s: 2.5,
high_spin: true,
symmetry: GroundTermSymmetry::A,
},
Expected {
name: "Mn2+",
n: 5,
s: 2.5,
high_spin: true,
symmetry: GroundTermSymmetry::A,
},
Expected {
name: "Fe2+ HS",
n: 6,
s: 2.0,
high_spin: true,
symmetry: GroundTermSymmetry::T,
},
Expected {
name: "Fe2+ LS",
n: 6,
s: 0.0,
high_spin: false,
symmetry: GroundTermSymmetry::A,
},
Expected {
name: "Co2+",
n: 7,
s: 1.5,
high_spin: true,
symmetry: GroundTermSymmetry::T,
},
Expected {
name: "Ni2+",
n: 8,
s: 1.0,
high_spin: true,
symmetry: GroundTermSymmetry::A,
},
Expected {
name: "Cu2+",
n: 9,
s: 0.5,
high_spin: true,
symmetry: GroundTermSymmetry::E,
},
Expected {
name: "Co3+ LS",
n: 6,
s: 0.0,
high_spin: false,
symmetry: GroundTermSymmetry::A,
},
];
let models: Vec<CrystalFieldModel> = vec![
CrystalFieldModel::ti3_plus().expect("preset"),
CrystalFieldModel::v3_plus().expect("preset"),
CrystalFieldModel::cr3_plus().expect("preset"),
CrystalFieldModel::mn3_plus_high_spin().expect("preset"),
CrystalFieldModel::fe3_plus().expect("preset"),
CrystalFieldModel::mn2_plus().expect("preset"),
CrystalFieldModel::fe2_plus_high_spin().expect("preset"),
CrystalFieldModel::fe2_plus_low_spin().expect("preset"),
CrystalFieldModel::co2_plus().expect("preset"),
CrystalFieldModel::ni2_plus().expect("preset"),
CrystalFieldModel::cu2_plus().expect("preset"),
CrystalFieldModel::co3_plus_low_spin().expect("preset"),
];
for (case, model) in cases.iter().zip(models.iter()) {
assert_eq!(model.n, case.n, "{}: wrong electron count", case.name);
let s = model.ground_state_spin().expect("computes");
assert!(
(s - case.s).abs() < 1e-9,
"{}: S expected {}, got {}",
case.name,
case.s,
s
);
let hs = model.is_high_spin().expect("computes");
assert_eq!(
hs, case.high_spin,
"{}: high-spin classification mismatch",
case.name
);
let sym = model.ground_term_symmetry().expect("computes");
assert_eq!(
sym, case.symmetry,
"{}: ground term symmetry mismatch",
case.name
);
}
}
#[test]
fn test_expectation_lz_ground_state_is_finite_and_bounded() {
let ti = CrystalFieldModel::ti3_plus().expect("preset");
let lz = ti.expectation_lz_ground_state().expect("computes");
assert!(lz.is_finite());
assert!(
lz.abs() <= 1.001,
"<Lz> should be bounded by the effective t2g l=1 spectrum, got {}",
lz
);
}
#[test]
fn test_soc_lifts_orbital_moment_for_unquenched_t_term() {
let ti = CrystalFieldModel::ti3_plus().expect("preset");
assert_eq!(
ti.ground_term_symmetry().expect("computes"),
GroundTermSymmetry::T
);
let rms_t = ti.ground_manifold_orbital_moment_rms().expect("computes");
assert!(rms_t.is_finite());
assert!(
rms_t > 0.3,
"T-term ground level should carry substantial orbital moment, got {}",
rms_t
);
let cr = CrystalFieldModel::cr3_plus().expect("preset");
assert_eq!(
cr.ground_term_symmetry().expect("computes"),
GroundTermSymmetry::A
);
let rms_a = cr.ground_manifold_orbital_moment_rms().expect("computes");
assert_eq!(
rms_a, 0.0,
"A-term ground level has no frontier level: RMS <Lz> must be exactly 0"
);
assert_eq!(cr.expectation_lz_ground_state().expect("computes"), 0.0);
}
#[test]
fn test_e_term_orbital_moment_is_exactly_zero() {
let cu = CrystalFieldModel::cu2_plus().expect("preset");
assert_eq!(
cu.ground_term_symmetry().expect("computes"),
GroundTermSymmetry::E
);
assert_eq!(
cu.ground_manifold_orbital_moment_rms().expect("computes"),
0.0
);
assert_eq!(cu.expectation_lz_ground_state().expect("computes"), 0.0);
let mn3 = CrystalFieldModel::mn3_plus_high_spin().expect("preset");
assert_eq!(
mn3.ground_term_symmetry().expect("computes"),
GroundTermSymmetry::E
);
assert_eq!(
mn3.ground_manifold_orbital_moment_rms().expect("computes"),
0.0
);
}
#[test]
fn test_t_term_presets_all_show_unquenched_orbital_moment() {
for model in [
CrystalFieldModel::ti3_plus().expect("preset"),
CrystalFieldModel::v3_plus().expect("preset"),
CrystalFieldModel::fe2_plus_high_spin().expect("preset"),
CrystalFieldModel::co2_plus().expect("preset"),
] {
assert_eq!(
model.ground_term_symmetry().expect("computes"),
GroundTermSymmetry::T
);
let rms = model
.ground_manifold_orbital_moment_rms()
.expect("computes");
assert!(
rms > 0.1,
"T-term preset (d{}) should show unquenched orbital moment, got {}",
model.n,
rms
);
}
}
#[test]
fn test_single_particle_hamiltonian_dimension() {
let ti = CrystalFieldModel::ti3_plus().expect("preset");
let h = ti.single_particle_hamiltonian().expect("builds");
assert_eq!(h.n(), 10);
}
}