1use ndarray::prelude::*;
16use ndarray_linalg::Determinant;
17use num_complex::Complex;
18use rayon::prelude::*;
19
20use crate::error::{Result, TbError};
21use crate::{Gauge, Model, RMatrixData};
22
23use super::config::{
24 Integration, IntegrationDiagnostics, Parameters, mesh_array, parameters_occupation,
25 validate_broadening, validate_chemical_potentials, validate_direction_matrix,
26 validate_temperature,
27};
28use super::kernel::{eval_optical_kernel, quadrature_optical_simplex};
29use super::tracking::{build_tetrahedra_3d, build_triangles_2d, global_band_track};
30use super::types::{SIMPLEX_GAP_TOL, VertexKernel};
31
32fn direction_pairs<const DIM: usize>(params: &Parameters<DIM>) -> Result<Vec<Array2<f64>>> {
38 if params.direction.nrows() == 0 {
39 let mut pairs = Vec::with_capacity(DIM * DIM);
40 for first in 0..DIM {
41 for second in 0..DIM {
42 let mut matrix = Array2::<f64>::zeros((2, DIM));
43 matrix[[0, first]] = 1.0;
44 matrix[[1, second]] = 1.0;
45 pairs.push(matrix);
46 }
47 }
48 return Ok(pairs);
49 }
50 validate_direction_matrix(¶ms.direction, 2, DIM)?;
51 Ok(vec![params.direction.clone()])
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct OpticalConductivityResult<const DIM: usize> {
57 pub frequencies: Array1<f64>,
59 pub directions: Vec<Array2<f64>>,
62 pub conductivity: Array2<Complex<f64>>,
64 pub diagnostics: Option<IntegrationDiagnostics>,
66}
67
68impl<const DIM: usize> OpticalConductivityResult<DIM> {
69 pub fn component(&self, index: usize) -> Option<ArrayView1<'_, Complex<f64>>> {
71 (index < self.conductivity.nrows()).then(|| self.conductivity.row(index))
72 }
73}
74
75impl<const SPIN: bool, const DIM: usize, R: RMatrixData> Model<SPIN, DIM, R> {
76 pub fn optical_conductivity(
82 &self,
83 params: &Parameters<DIM>,
84 ) -> Result<OpticalConductivityResult<DIM>> {
85 validate_chemical_potentials(¶ms.mu)?;
86 if params.mu.len() != 1 {
87 return Err(TbError::InvalidResponseParameter {
88 parameter: "mu",
89 message: "optical_conductivity expects a single chemical potential".into(),
90 });
91 }
92 if params.omega.is_empty() {
93 return Err(TbError::InvalidResponseParameter {
94 parameter: "omega",
95 message: "must contain at least one value".into(),
96 });
97 }
98 if params.omega.iter().any(|frequency| !frequency.is_finite()) {
99 return Err(TbError::InvalidResponseParameter {
100 parameter: "omega",
101 message: "all values must be finite".into(),
102 });
103 }
104 validate_temperature(¶ms.T)?;
105 validate_broadening(params.eta)?;
106 crate::response::config::validate_k_mesh(¶ms.kmesh)?;
107 match params.integration {
108 Integration::Direct | Integration::Simplex => {}
109 Integration::EnergyCut => {
110 return Err(TbError::InvalidResponseParameter {
111 parameter: "integration",
112 message: "optical_conductivity supports Integration::Direct or Simplex, not EnergyCut".into(),
113 });
114 }
115 }
116 if params.integration == Integration::Simplex && DIM == 1 {
117 return Err(TbError::InvalidDimension {
118 dim: DIM,
119 supported: vec![2, 3],
120 });
121 }
122 let direction_pairs = direction_pairs(params)?;
123 let k_mesh = mesh_array(¶ms.kmesh);
124 let k_points = crate::kpoints::gen_kmesh::<f64>(&k_mesh)?;
125 let thermal_width = parameters_occupation(params).energy_width()?;
126 let chemical_potential = params.mu[0];
127 let determinant = self.lat.det()?;
128 let mut conductivity =
129 Array2::<Complex<f64>>::zeros((direction_pairs.len(), params.omega.len()));
130 let mut unsafe_simplex_count = 0usize;
131
132 for (component, directions) in direction_pairs.iter().enumerate() {
133 let direction_a = directions.row(0).to_owned();
134 let direction_b = directions.row(1).to_owned();
135 let mut vertices: Vec<VertexKernel> = (0..k_points.nrows())
136 .into_par_iter()
137 .map(|index| {
138 self.compute_velocity_kernel(
139 &k_points.row(index).to_owned(),
140 &direction_a,
141 &direction_b,
142 None,
143 Gauge::Atom,
144 None,
145 )
146 })
147 .collect();
148
149 match params.integration {
150 Integration::Direct => {
151 let values: Vec<Complex<f64>> = params
152 .omega
153 .par_iter()
154 .map(|&frequency| {
155 vertices
156 .iter()
157 .map(|vertex| {
158 eval_optical_kernel(
159 vertex.band.as_slice().unwrap(),
160 &vertex.k_ab,
161 frequency,
162 params.eta,
163 chemical_potential,
164 thermal_width,
165 self.nsta(),
166 )
167 })
168 .sum::<Complex<f64>>()
169 / k_points.nrows() as f64
170 / determinant
171 })
172 .collect();
173 conductivity
174 .row_mut(component)
175 .assign(&Array1::from_vec(values));
176 }
177 Integration::Simplex => {
178 global_band_track(&mut vertices, ¶ms.kmesh);
179 let values: Vec<(Complex<f64>, usize)> = params
180 .omega
181 .par_iter()
182 .map(|&frequency| {
183 integrate_simplex(
184 &vertices,
185 &k_mesh,
186 frequency,
187 params.eta,
188 chemical_potential,
189 thermal_width,
190 )
191 })
192 .collect();
193 for (frequency, (value, unsafe_count)) in values.into_iter().enumerate() {
194 conductivity[[component, frequency]] = value / determinant;
195 unsafe_simplex_count = unsafe_simplex_count.max(unsafe_count);
196 }
197 }
198 Integration::EnergyCut => unreachable!("rejected during validation"),
199 }
200 }
201
202 Ok(OpticalConductivityResult {
203 frequencies: params.omega.clone(),
204 directions: direction_pairs,
205 conductivity,
206 diagnostics: (params.integration == Integration::Simplex).then_some(
207 IntegrationDiagnostics {
208 unsafe_simplex_count,
209 },
210 ),
211 })
212 }
213}
214
215fn integrate_simplex(
216 vertices: &[VertexKernel],
217 k_mesh: &Array1<usize>,
218 frequency: f64,
219 broadening: f64,
220 chemical_potential: f64,
221 thermal_width: f64,
222) -> (Complex<f64>, usize) {
223 let mut total = Complex::new(0.0, 0.0);
224 let mut unsafe_simplex_count = 0usize;
225 match k_mesh.len() {
226 2 => {
227 let (nx, ny) = (k_mesh[0], k_mesh[1]);
228 for ix in 0..nx {
229 for iy in 0..ny {
230 for simplex in &build_triangles_2d(
231 ix,
232 iy,
233 nx,
234 ny,
235 1.0 / nx as f64,
236 1.0 / ny as f64,
237 vertices,
238 ) {
239 unsafe_simplex_count += usize::from(simplex.diag.min_gap < SIMPLEX_GAP_TOL);
240 total += quadrature_optical_simplex(
241 simplex,
242 frequency,
243 broadening,
244 chemical_potential,
245 thermal_width,
246 );
247 }
248 }
249 }
250 }
251 3 => {
252 let (nx, ny, nz) = (k_mesh[0], k_mesh[1], k_mesh[2]);
253 for ix in 0..nx {
254 for iy in 0..ny {
255 for iz in 0..nz {
256 for simplex in &build_tetrahedra_3d(
257 ix,
258 iy,
259 iz,
260 nx,
261 ny,
262 nz,
263 1.0 / nx as f64,
264 1.0 / ny as f64,
265 1.0 / nz as f64,
266 vertices,
267 ) {
268 unsafe_simplex_count +=
269 usize::from(simplex.diag.min_gap < SIMPLEX_GAP_TOL);
270 total += quadrature_optical_simplex(
271 simplex,
272 frequency,
273 broadening,
274 chemical_potential,
275 thermal_width,
276 );
277 }
278 }
279 }
280 }
281 }
282 _ => unreachable!("validated before simplex integration"),
283 }
284 (total, unsafe_simplex_count)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use ndarray::array;
291
292 fn qwz_model(mass: f64) -> Model<false, 2> {
293 let mut model = Model::<false, 2>::tb_model(
294 array![[1.0, 0.0], [0.0, 1.0]],
295 array![[0.0, 0.0], [0.0, 0.0]],
296 None,
297 )
298 .unwrap();
299 model.add_onsite(&array![mass, -mass], None);
300 model.add_hop(Complex::new(0.0, -0.5), 0, 1, &array![1, 0], None);
301 model.add_hop(Complex::new(0.0, 0.5), 0, 1, &array![-1, 0], None);
302 model.add_hop(-0.5, 0, 1, &array![0, 1], None);
303 model.add_hop(0.5, 0, 1, &array![0, -1], None);
304 for displacement in [array![1, 0], array![-1, 0], array![0, 1], array![0, -1]] {
305 model.add_hop(0.5, 0, 0, &displacement, None);
306 model.add_hop(-0.5, 1, 1, &displacement, None);
307 }
308 model
309 }
310
311 #[test]
312 fn cartesian_request_has_dim_squared_components() {
313 let model =
314 Model::<false, 2>::tb_model(array![[1.0, 0.0], [0.0, 1.0]], array![[0.0, 0.0]], None)
315 .unwrap();
316 let mut params = Parameters::at_mu([2, 2], Array2::zeros((0, 2)), 0.0);
318 params.omega = array![0.1, 0.2];
319 let result = model.optical_conductivity(¶ms).unwrap();
320 assert_eq!(result.conductivity.dim(), (4, 2));
321 assert_eq!(result.directions.len(), 4);
322 }
323
324 #[test]
325 fn direct_and_simplex_evaluate_the_same_optical_kernel() {
326 let model = qwz_model(-1.0);
327 let mut params = Parameters::rank2([31, 31], [1.0, 0.0], [1.0, 0.0], array![0.0]);
328 params.omega = array![0.2, 0.8];
329 params.eta = 0.1;
330 let direct = model.optical_conductivity(¶ms).unwrap();
331
332 params.integration = Integration::Simplex;
333 let simplex = model.optical_conductivity(¶ms).unwrap();
334 assert!(simplex.diagnostics.is_some());
335 for (&direct_value, &simplex_value) in
336 direct.conductivity.iter().zip(simplex.conductivity.iter())
337 {
338 let scale = direct_value.norm().max(simplex_value.norm()).max(1.0);
339 assert!((direct_value - simplex_value).norm() < 5e-2 * scale);
340 }
341 }
342}