use std::f64::consts::FRAC_1_SQRT_2;
use crate::error::Result;
use crate::math::{CMatrix, Complex};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DOrbital {
Dxy,
Dyz,
Dzx,
Dx2y2,
Dz2,
}
impl DOrbital {
pub const COUNT: usize = 5;
#[inline]
pub fn all() -> [DOrbital; 5] {
[
DOrbital::Dxy,
DOrbital::Dyz,
DOrbital::Dzx,
DOrbital::Dx2y2,
DOrbital::Dz2,
]
}
#[inline]
pub fn index(self) -> usize {
match self {
DOrbital::Dxy => 0,
DOrbital::Dyz => 1,
DOrbital::Dzx => 2,
DOrbital::Dx2y2 => 3,
DOrbital::Dz2 => 4,
}
}
#[inline]
pub fn label(self) -> &'static str {
match self {
DOrbital::Dxy => "xy",
DOrbital::Dyz => "yz",
DOrbital::Dzx => "zx",
DOrbital::Dx2y2 => "x2-y2",
DOrbital::Dz2 => "z2",
}
}
#[inline]
pub fn symmetry(self) -> OrbitalSymmetry {
match self {
DOrbital::Dxy | DOrbital::Dyz | DOrbital::Dzx => OrbitalSymmetry::T2,
DOrbital::Dx2y2 | DOrbital::Dz2 => OrbitalSymmetry::E,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum OrbitalSymmetry {
E,
T2,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CrystalFieldEnvironment {
Octahedral,
Tetrahedral,
TetragonalDistorted {
ds: f64,
dt: f64,
},
}
const M_VALUES: [i32; 5] = [-2, -1, 0, 1, 2];
fn l_z_complex() -> CMatrix {
CMatrix::from_diagonal(&[-2.0, -1.0, 0.0, 1.0, 2.0])
}
fn l_plus_complex() -> Result<CMatrix> {
let mut rows = vec![vec![Complex::ZERO; 5]; 5];
for (col, &m) in M_VALUES.iter().enumerate().take(4) {
let m = f64::from(m);
let coeff = (6.0 - m * (m + 1.0)).sqrt();
rows[col + 1][col] = Complex::from_real(coeff);
}
CMatrix::from_rows(rows)
}
fn l_minus_complex() -> Result<CMatrix> {
Ok(l_plus_complex()?.conj_transpose())
}
fn l_x_complex() -> Result<CMatrix> {
let lp = l_plus_complex()?;
let lm = l_minus_complex()?;
Ok(lp.add(&lm)?.scale_real(0.5))
}
fn l_y_complex() -> Result<CMatrix> {
let lp = l_plus_complex()?;
let lm = l_minus_complex()?;
let diff = lp.sub(&lm)?;
Ok(diff.scale(Complex::new(0.0, -0.5)))
}
fn u_transform() -> Result<CMatrix> {
let s = Complex::from_real(FRAC_1_SQRT_2);
let is = Complex::new(0.0, FRAC_1_SQRT_2);
let z = Complex::ZERO;
let rows = vec![
vec![is, z, z, s, z], vec![z, is, s, z, z], vec![z, z, z, z, Complex::ONE], vec![z, is, s.neg(), z, z], vec![is.neg(), z, z, s, z], ];
CMatrix::from_rows(rows)
}
pub fn orbital_angular_momentum_operators() -> Result<(CMatrix, CMatrix, CMatrix)> {
let u = u_transform()?;
let u_dag = u.conj_transpose();
let lx = u_dag.matmul(&l_x_complex()?)?.matmul(&u)?;
let ly = u_dag.matmul(&l_y_complex()?)?.matmul(&u)?;
let lz = u_dag.matmul(&l_z_complex())?.matmul(&u)?;
Ok((lx, ly, lz))
}
pub fn orbital_l_squared() -> Result<CMatrix> {
let (lx, ly, lz) = orbital_angular_momentum_operators()?;
let lx2 = lx.matmul(&lx)?;
let ly2 = ly.matmul(&ly)?;
let lz2 = lz.matmul(&lz)?;
lx2.add(&ly2)?.add(&lz2)
}
pub fn crystal_field_hamiltonian(
environment: CrystalFieldEnvironment,
ten_dq: f64,
) -> Result<CMatrix> {
let mut energies = [0.0_f64; 5];
match environment {
CrystalFieldEnvironment::Octahedral => {
for orbital in DOrbital::all() {
energies[orbital.index()] = match orbital.symmetry() {
OrbitalSymmetry::T2 => -0.4 * ten_dq,
OrbitalSymmetry::E => 0.6 * ten_dq,
};
}
},
CrystalFieldEnvironment::Tetrahedral => {
for orbital in DOrbital::all() {
energies[orbital.index()] = match orbital.symmetry() {
OrbitalSymmetry::T2 => 0.4 * ten_dq,
OrbitalSymmetry::E => -0.6 * ten_dq,
};
}
},
CrystalFieldEnvironment::TetragonalDistorted { ds, dt } => {
energies[DOrbital::Dz2.index()] = 0.6 * ten_dq - 2.0 * ds - 6.0 * dt;
energies[DOrbital::Dx2y2.index()] = 0.6 * ten_dq + 2.0 * ds - dt;
energies[DOrbital::Dxy.index()] = -0.4 * ten_dq + 2.0 * ds - dt;
energies[DOrbital::Dyz.index()] = -0.4 * ten_dq - ds + 4.0 * dt;
energies[DOrbital::Dzx.index()] = -0.4 * ten_dq - ds + 4.0 * dt;
},
}
Ok(CMatrix::from_diagonal(&energies))
}
pub(crate) fn pauli_x() -> Result<CMatrix> {
CMatrix::from_rows(vec![
vec![Complex::ZERO, Complex::ONE],
vec![Complex::ONE, Complex::ZERO],
])
}
pub(crate) fn pauli_y() -> Result<CMatrix> {
CMatrix::from_rows(vec![
vec![Complex::ZERO, Complex::new(0.0, -1.0)],
vec![Complex::new(0.0, 1.0), Complex::ZERO],
])
}
pub(crate) fn pauli_z() -> Result<CMatrix> {
CMatrix::from_rows(vec![
vec![Complex::ONE, Complex::ZERO],
vec![Complex::ZERO, Complex::new(-1.0, 0.0)],
])
}
pub(crate) fn kron(a: &CMatrix, b: &CMatrix) -> Result<CMatrix> {
let na = a.n();
let nb = b.n();
let n = na * nb;
let mut rows = vec![vec![Complex::ZERO; n]; n];
for i in 0..na {
for j in 0..na {
let aij = a.get(i, j);
if aij.re == 0.0 && aij.im == 0.0 {
continue;
}
for p in 0..nb {
for q in 0..nb {
rows[i * nb + p][j * nb + q] = aij.mul(&b.get(p, q));
}
}
}
}
CMatrix::from_rows(rows)
}
pub fn soc_hamiltonian(
environment: CrystalFieldEnvironment,
ten_dq: f64,
soc_lambda: f64,
) -> Result<CMatrix> {
let h_cf = crystal_field_hamiltonian(environment, ten_dq)?;
let identity2 = CMatrix::eye(2);
let h_cf_ext = kron(&h_cf, &identity2)?;
let (lx, ly, lz) = orbital_angular_momentum_operators()?;
let sx = pauli_x()?;
let sy = pauli_y()?;
let sz = pauli_z()?;
let lx_sx = kron(&lx, &sx)?;
let ly_sy = kron(&ly, &sy)?;
let lz_sz = kron(&lz, &sz)?;
let l_dot_s = lx_sx.add(&ly_sy)?.add(&lz_sz)?.scale_real(soc_lambda * 0.5);
h_cf_ext.add(&l_dot_s)
}
#[cfg(test)]
mod tests {
use super::*;
fn hermitian_defect(m: &CMatrix) -> f64 {
let diff = m.sub(&m.conj_transpose()).expect("same dimension");
diff.frobenius_norm()
}
#[test]
fn test_dorbital_index_roundtrip() {
for (expected_idx, orbital) in DOrbital::all().into_iter().enumerate() {
assert_eq!(orbital.index(), expected_idx);
}
}
#[test]
fn test_dorbital_symmetry_grouping() {
assert_eq!(DOrbital::Dxy.symmetry(), OrbitalSymmetry::T2);
assert_eq!(DOrbital::Dyz.symmetry(), OrbitalSymmetry::T2);
assert_eq!(DOrbital::Dzx.symmetry(), OrbitalSymmetry::T2);
assert_eq!(DOrbital::Dx2y2.symmetry(), OrbitalSymmetry::E);
assert_eq!(DOrbital::Dz2.symmetry(), OrbitalSymmetry::E);
}
#[test]
fn test_u_transform_is_unitary() {
let u = u_transform().expect("u_transform builds");
let u_dag = u.conj_transpose();
let should_be_identity = u_dag.matmul(&u).expect("5x5 matmul");
let identity = CMatrix::eye(5);
let defect = should_be_identity
.sub(&identity)
.expect("same dim")
.frobenius_norm();
assert!(defect < 1e-12, "U is not unitary: defect = {}", defect);
}
#[test]
fn test_operators_are_hermitian() {
let (lx, ly, lz) = orbital_angular_momentum_operators().expect("operators build");
assert!(hermitian_defect(&lx) < 1e-12, "L_x not Hermitian");
assert!(hermitian_defect(&ly) < 1e-12, "L_y not Hermitian");
assert!(hermitian_defect(&lz) < 1e-12, "L_z not Hermitian");
}
#[test]
fn test_commutator_lx_ly_is_i_lz() {
let (lx, ly, lz) = orbital_angular_momentum_operators().expect("operators build");
let lxly = lx.matmul(&ly).expect("matmul");
let lylx = ly.matmul(&lx).expect("matmul");
let commutator = lxly.sub(&lylx).expect("same dim");
let i_lz = lz.scale(Complex::I);
let defect = commutator.sub(&i_lz).expect("same dim").frobenius_norm();
assert!(defect < 1e-10, "[L_x,L_y] != i L_z: defect = {}", defect);
}
#[test]
fn test_commutator_ly_lz_is_i_lx() {
let (lx, ly, lz) = orbital_angular_momentum_operators().expect("operators build");
let lylz = ly.matmul(&lz).expect("matmul");
let lzly = lz.matmul(&ly).expect("matmul");
let commutator = lylz.sub(&lzly).expect("same dim");
let i_lx = lx.scale(Complex::I);
let defect = commutator.sub(&i_lx).expect("same dim").frobenius_norm();
assert!(defect < 1e-10, "[L_y,L_z] != i L_x: defect = {}", defect);
}
#[test]
fn test_commutator_lz_lx_is_i_ly() {
let (lx, ly, lz) = orbital_angular_momentum_operators().expect("operators build");
let lzlx = lz.matmul(&lx).expect("matmul");
let lxlz = lx.matmul(&lz).expect("matmul");
let commutator = lzlx.sub(&lxlz).expect("same dim");
let i_ly = ly.scale(Complex::I);
let defect = commutator.sub(&i_ly).expect("same dim").frobenius_norm();
assert!(defect < 1e-10, "[L_z,L_x] != i L_y: defect = {}", defect);
}
#[test]
fn test_l_squared_is_six_identity() {
let l2 = orbital_l_squared().expect("l_squared builds");
let six_identity = CMatrix::eye(5).scale_real(6.0);
let defect = l2.sub(&six_identity).expect("same dim").frobenius_norm();
assert!(defect < 1e-9, "L^2 != 6I: defect = {}", defect);
}
#[test]
fn test_l_z_eigenvalue_spectrum() {
let (_, _, lz) = orbital_angular_momentum_operators().expect("operators build");
let (mut eigenvalues, _) = lz.hermitian_eigendecomposition().expect("Hermitian eig");
eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let expected = [-2.0, -1.0, 0.0, 1.0, 2.0];
for (got, want) in eigenvalues.iter().zip(expected.iter()) {
assert!((got - want).abs() < 1e-9, "eigenvalue {} != {}", got, want);
}
}
#[test]
fn test_real_orbitals_have_zero_diagonal_angular_momentum() {
let (lx, ly, lz) = orbital_angular_momentum_operators().expect("operators build");
for orbital in DOrbital::all() {
let idx = orbital.index();
assert!(
lx.get(idx, idx).norm() < 1e-10,
"Lx diagonal nonzero for {:?}",
orbital
);
assert!(
ly.get(idx, idx).norm() < 1e-10,
"Ly diagonal nonzero for {:?}",
orbital
);
assert!(
lz.get(idx, idx).norm() < 1e-10,
"Lz diagonal nonzero for {:?}",
orbital
);
}
}
#[test]
fn test_l_z_hand_derived_matrix_elements() {
let (_, _, lz) = orbital_angular_momentum_operators().expect("operators build");
let xy = DOrbital::Dxy.index();
let x2y2 = DOrbital::Dx2y2.index();
let yz = DOrbital::Dyz.index();
let zx = DOrbital::Dzx.index();
let element_xy_x2y2 = lz.get(xy, x2y2);
assert!((element_xy_x2y2.re).abs() < 1e-9);
assert!(
(element_xy_x2y2.im - 2.0).abs() < 1e-9,
"got {:?}",
element_xy_x2y2
);
let element_yz_zx = lz.get(yz, zx);
assert!((element_yz_zx.re).abs() < 1e-9);
assert!(
(element_yz_zx.im - 1.0).abs() < 1e-9,
"got {:?}",
element_yz_zx
);
}
#[test]
fn test_octahedral_splitting_pattern() {
let ten_dq = 2.0;
let h =
crystal_field_hamiltonian(CrystalFieldEnvironment::Octahedral, ten_dq).expect("builds");
for orbital in DOrbital::all() {
let e = h.get(orbital.index(), orbital.index()).re;
let expected = match orbital.symmetry() {
OrbitalSymmetry::T2 => -0.4 * ten_dq,
OrbitalSymmetry::E => 0.6 * ten_dq,
};
assert!(
(e - expected).abs() < 1e-12,
"Oh energy mismatch for {:?}",
orbital
);
}
assert!(
h.trace().re.abs() < 1e-12,
"Oh crystal field must be traceless"
);
}
#[test]
fn test_tetrahedral_splitting_is_inverted() {
let ten_dq = 2.0;
let h_oh =
crystal_field_hamiltonian(CrystalFieldEnvironment::Octahedral, ten_dq).expect("builds");
let h_td = crystal_field_hamiltonian(CrystalFieldEnvironment::Tetrahedral, ten_dq)
.expect("builds");
for orbital in DOrbital::all() {
let idx = orbital.index();
let e_oh = h_oh.get(idx, idx).re;
let e_td = h_td.get(idx, idx).re;
assert!(
(e_td + e_oh).abs() < 1e-12,
"Td energy should be -1 * Oh energy for {:?}",
orbital
);
}
assert!(
h_td.trace().re.abs() < 1e-12,
"Td crystal field must be traceless"
);
}
#[test]
fn test_tetragonal_reduces_to_octahedral_at_zero_distortion() {
let ten_dq = 2.0;
let h_oh =
crystal_field_hamiltonian(CrystalFieldEnvironment::Octahedral, ten_dq).expect("builds");
let h_tet = crystal_field_hamiltonian(
CrystalFieldEnvironment::TetragonalDistorted { ds: 0.0, dt: 0.0 },
ten_dq,
)
.expect("builds");
let defect = h_oh.sub(&h_tet).expect("same dim").frobenius_norm();
assert!(
defect < 1e-12,
"tetragonal(ds=dt=0) should equal octahedral"
);
}
#[test]
fn test_tetragonal_is_traceless() {
let h = crystal_field_hamiltonian(
CrystalFieldEnvironment::TetragonalDistorted { ds: 0.3, dt: 0.05 },
2.0,
)
.expect("builds");
assert!(
h.trace().re.abs() < 1e-10,
"tetragonal crystal field must be traceless"
);
}
#[test]
fn test_tetragonal_splits_xy_from_xz_yz_degeneracy() {
let h = crystal_field_hamiltonian(
CrystalFieldEnvironment::TetragonalDistorted { ds: 0.3, dt: 0.05 },
2.0,
)
.expect("builds");
let e_xy = h.get(DOrbital::Dxy.index(), DOrbital::Dxy.index()).re;
let e_yz = h.get(DOrbital::Dyz.index(), DOrbital::Dyz.index()).re;
let e_zx = h.get(DOrbital::Dzx.index(), DOrbital::Dzx.index()).re;
assert!((e_yz - e_zx).abs() < 1e-12, "xz/yz must stay degenerate");
assert!(
(e_xy - e_yz).abs() > 1e-6,
"xy should split away from xz/yz"
);
}
#[test]
fn test_soc_hamiltonian_is_hermitian_and_10_dimensional() {
let h = soc_hamiltonian(CrystalFieldEnvironment::Octahedral, 2.0, 0.05).expect("builds");
assert_eq!(h.n(), 10);
assert!(hermitian_defect(&h) < 1e-10);
}
#[test]
fn test_soc_hamiltonian_zero_lambda_is_pure_crystal_field() {
let h = soc_hamiltonian(CrystalFieldEnvironment::Octahedral, 2.0, 0.0).expect("builds");
let h_cf =
crystal_field_hamiltonian(CrystalFieldEnvironment::Octahedral, 2.0).expect("builds");
let h_cf_ext = kron(&h_cf, &CMatrix::eye(2)).expect("kron");
let defect = h.sub(&h_cf_ext).expect("same dim").frobenius_norm();
assert!(
defect < 1e-10,
"lambda=0 SOC Hamiltonian should reduce to H_CF ⊗ I2"
);
}
#[test]
fn test_kron_dimensions_and_identity() {
let a = CMatrix::eye(2);
let b = CMatrix::eye(3);
let prod = kron(&a, &b).expect("kron builds");
assert_eq!(prod.n(), 6);
let identity6 = CMatrix::eye(6);
let defect = prod.sub(&identity6).expect("same dim").frobenius_norm();
assert!(defect < 1e-12, "I2 ⊗ I3 should equal I6");
}
}