use ndarray::prelude::*;
use ndarray_linalg::Determinant;
use num_complex::Complex;
use rayon::prelude::*;
use crate::error::{Result, TbError};
use crate::{Gauge, Model, RMatrixData};
use super::config::{
Integration, IntegrationDiagnostics, Parameters, mesh_array, parameters_occupation,
validate_broadening, validate_chemical_potentials, validate_direction_matrix,
validate_temperature,
};
use super::kernel::{eval_optical_kernel, quadrature_optical_simplex};
use super::tracking::{build_tetrahedra_3d, build_triangles_2d, global_band_track};
use super::types::{SIMPLEX_GAP_TOL, VertexKernel};
fn direction_pairs<const DIM: usize>(params: &Parameters<DIM>) -> Result<Vec<Array2<f64>>> {
if params.direction.nrows() == 0 {
let mut pairs = Vec::with_capacity(DIM * DIM);
for first in 0..DIM {
for second in 0..DIM {
let mut matrix = Array2::<f64>::zeros((2, DIM));
matrix[[0, first]] = 1.0;
matrix[[1, second]] = 1.0;
pairs.push(matrix);
}
}
return Ok(pairs);
}
validate_direction_matrix(¶ms.direction, 2, DIM)?;
Ok(vec![params.direction.clone()])
}
#[derive(Clone, Debug, PartialEq)]
pub struct OpticalConductivityResult<const DIM: usize> {
pub frequencies: Array1<f64>,
pub directions: Vec<Array2<f64>>,
pub conductivity: Array2<Complex<f64>>,
pub diagnostics: Option<IntegrationDiagnostics>,
}
impl<const DIM: usize> OpticalConductivityResult<DIM> {
pub fn component(&self, index: usize) -> Option<ArrayView1<'_, Complex<f64>>> {
(index < self.conductivity.nrows()).then(|| self.conductivity.row(index))
}
}
impl<const SPIN: bool, const DIM: usize, R: RMatrixData> Model<SPIN, DIM, R> {
pub fn optical_conductivity(
&self,
params: &Parameters<DIM>,
) -> Result<OpticalConductivityResult<DIM>> {
validate_chemical_potentials(¶ms.mu)?;
if params.mu.len() != 1 {
return Err(TbError::InvalidResponseParameter {
parameter: "mu",
message: "optical_conductivity expects a single chemical potential".into(),
});
}
if params.omega.is_empty() {
return Err(TbError::InvalidResponseParameter {
parameter: "omega",
message: "must contain at least one value".into(),
});
}
if params.omega.iter().any(|frequency| !frequency.is_finite()) {
return Err(TbError::InvalidResponseParameter {
parameter: "omega",
message: "all values must be finite".into(),
});
}
validate_temperature(¶ms.T)?;
validate_broadening(params.eta)?;
crate::response::config::validate_k_mesh(¶ms.kmesh)?;
match params.integration {
Integration::Direct | Integration::Simplex => {}
Integration::EnergyCut => {
return Err(TbError::InvalidResponseParameter {
parameter: "integration",
message: "optical_conductivity supports Integration::Direct or Simplex, not EnergyCut".into(),
});
}
}
if params.integration == Integration::Simplex && DIM == 1 {
return Err(TbError::InvalidDimension {
dim: DIM,
supported: vec![2, 3],
});
}
let direction_pairs = direction_pairs(params)?;
let k_mesh = mesh_array(¶ms.kmesh);
let k_points = crate::kpoints::gen_kmesh::<f64>(&k_mesh)?;
let thermal_width = parameters_occupation(params).energy_width()?;
let chemical_potential = params.mu[0];
let determinant = self.lat.det()?;
let mut conductivity =
Array2::<Complex<f64>>::zeros((direction_pairs.len(), params.omega.len()));
let mut unsafe_simplex_count = 0usize;
for (component, directions) in direction_pairs.iter().enumerate() {
let direction_a = directions.row(0).to_owned();
let direction_b = directions.row(1).to_owned();
let mut vertices: Vec<VertexKernel> = (0..k_points.nrows())
.into_par_iter()
.map(|index| {
self.compute_velocity_kernel(
&k_points.row(index).to_owned(),
&direction_a,
&direction_b,
None,
Gauge::Atom,
None,
)
})
.collect();
match params.integration {
Integration::Direct => {
let values: Vec<Complex<f64>> = params
.omega
.par_iter()
.map(|&frequency| {
vertices
.iter()
.map(|vertex| {
eval_optical_kernel(
vertex.band.as_slice().unwrap(),
&vertex.k_ab,
frequency,
params.eta,
chemical_potential,
thermal_width,
self.nsta(),
)
})
.sum::<Complex<f64>>()
/ k_points.nrows() as f64
/ determinant
})
.collect();
conductivity
.row_mut(component)
.assign(&Array1::from_vec(values));
}
Integration::Simplex => {
global_band_track(&mut vertices, ¶ms.kmesh);
let values: Vec<(Complex<f64>, usize)> = params
.omega
.par_iter()
.map(|&frequency| {
integrate_simplex(
&vertices,
&k_mesh,
frequency,
params.eta,
chemical_potential,
thermal_width,
)
})
.collect();
for (frequency, (value, unsafe_count)) in values.into_iter().enumerate() {
conductivity[[component, frequency]] = value / determinant;
unsafe_simplex_count = unsafe_simplex_count.max(unsafe_count);
}
}
Integration::EnergyCut => unreachable!("rejected during validation"),
}
}
Ok(OpticalConductivityResult {
frequencies: params.omega.clone(),
directions: direction_pairs,
conductivity,
diagnostics: (params.integration == Integration::Simplex).then_some(
IntegrationDiagnostics {
unsafe_simplex_count,
},
),
})
}
}
fn integrate_simplex(
vertices: &[VertexKernel],
k_mesh: &Array1<usize>,
frequency: f64,
broadening: f64,
chemical_potential: f64,
thermal_width: f64,
) -> (Complex<f64>, usize) {
let mut total = Complex::new(0.0, 0.0);
let mut unsafe_simplex_count = 0usize;
match k_mesh.len() {
2 => {
let (nx, ny) = (k_mesh[0], k_mesh[1]);
for ix in 0..nx {
for iy in 0..ny {
for simplex in &build_triangles_2d(
ix,
iy,
nx,
ny,
1.0 / nx as f64,
1.0 / ny as f64,
vertices,
) {
unsafe_simplex_count += usize::from(simplex.diag.min_gap < SIMPLEX_GAP_TOL);
total += quadrature_optical_simplex(
simplex,
frequency,
broadening,
chemical_potential,
thermal_width,
);
}
}
}
}
3 => {
let (nx, ny, nz) = (k_mesh[0], k_mesh[1], k_mesh[2]);
for ix in 0..nx {
for iy in 0..ny {
for iz in 0..nz {
for simplex in &build_tetrahedra_3d(
ix,
iy,
iz,
nx,
ny,
nz,
1.0 / nx as f64,
1.0 / ny as f64,
1.0 / nz as f64,
vertices,
) {
unsafe_simplex_count +=
usize::from(simplex.diag.min_gap < SIMPLEX_GAP_TOL);
total += quadrature_optical_simplex(
simplex,
frequency,
broadening,
chemical_potential,
thermal_width,
);
}
}
}
}
}
_ => unreachable!("validated before simplex integration"),
}
(total, unsafe_simplex_count)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn qwz_model(mass: f64) -> Model<false, 2> {
let mut model = Model::<false, 2>::tb_model(
array![[1.0, 0.0], [0.0, 1.0]],
array![[0.0, 0.0], [0.0, 0.0]],
None,
)
.unwrap();
model.add_onsite(&array![mass, -mass], None);
model.add_hop(Complex::new(0.0, -0.5), 0, 1, &array![1, 0], None);
model.add_hop(Complex::new(0.0, 0.5), 0, 1, &array![-1, 0], None);
model.add_hop(-0.5, 0, 1, &array![0, 1], None);
model.add_hop(0.5, 0, 1, &array![0, -1], None);
for displacement in [array![1, 0], array![-1, 0], array![0, 1], array![0, -1]] {
model.add_hop(0.5, 0, 0, &displacement, None);
model.add_hop(-0.5, 1, 1, &displacement, None);
}
model
}
#[test]
fn cartesian_request_has_dim_squared_components() {
let model =
Model::<false, 2>::tb_model(array![[1.0, 0.0], [0.0, 1.0]], array![[0.0, 0.0]], None)
.unwrap();
let mut params = Parameters::at_mu([2, 2], Array2::zeros((0, 2)), 0.0);
params.omega = array![0.1, 0.2];
let result = model.optical_conductivity(¶ms).unwrap();
assert_eq!(result.conductivity.dim(), (4, 2));
assert_eq!(result.directions.len(), 4);
}
#[test]
fn direct_and_simplex_evaluate_the_same_optical_kernel() {
let model = qwz_model(-1.0);
let mut params = Parameters::rank2([31, 31], [1.0, 0.0], [1.0, 0.0], array![0.0]);
params.omega = array![0.2, 0.8];
params.eta = 0.1;
let direct = model.optical_conductivity(¶ms).unwrap();
params.integration = Integration::Simplex;
let simplex = model.optical_conductivity(¶ms).unwrap();
assert!(simplex.diagnostics.is_some());
for (&direct_value, &simplex_value) in
direct.conductivity.iter().zip(simplex.conductivity.iter())
{
let scale = direct_value.norm().max(simplex_value.norm()).max(1.0);
assert!((direct_value - simplex_value).norm() < 5e-2 * scale);
}
}
}