Skip to main content

Rustb/response/optical/
mod.rs

1//! Optical conductivity from gauge-invariant velocity kernels.
2//!
3//! The direct and simplex algorithms evaluate the same kernel,
4//!
5//! ```math
6//! \sigma^{ab}(\omega) = \sum_{n\ne m}\int_{BZ}
7//! \frac{(f_n-f_m)v^a_{nm}v^b_{mn}}
8//! {(E_n-E_m)^2-(\omega+i\eta)^2}\,dk,
9//! ```
10//!
11//! and therefore share one [`Parameters`] input and one named
12//! [`OpticalConductivityResult`] output. A component calculation and a full
13//! Cartesian tensor calculation differ only through the direction matrix.
14
15use 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
32/// Direction pairs requested from an optical calculation.
33///
34/// A `direction` matrix with two rows selects one projected component; an
35/// empty direction matrix selects every ordered Cartesian component in
36/// row-major order `(0,0), (0,1), ..., (DIM-1,DIM-1)`.
37fn 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(&params.direction, 2, DIM)?;
51    Ok(vec![params.direction.clone()])
52}
53
54/// Optical conductivity for one or more tensor components.
55#[derive(Clone, Debug, PartialEq)]
56pub struct OpticalConductivityResult<const DIM: usize> {
57    /// Frequencies corresponding to the columns of `conductivity`.
58    pub frequencies: Array1<f64>,
59    /// Direction matrix (shape `(2, DIM)`) corresponding to every row of
60    /// `conductivity`.
61    pub directions: Vec<Array2<f64>>,
62    /// Complex conductivity with shape `(number_of_components, frequencies)`.
63    pub conductivity: Array2<Complex<f64>>,
64    /// Present only for simplex integration.
65    pub diagnostics: Option<IntegrationDiagnostics>,
66}
67
68impl<const DIM: usize> OpticalConductivityResult<DIM> {
69    /// View one component by row index.
70    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    /// Compute a projected component or the full Cartesian optical tensor.
77    ///
78    /// Reads `kmesh`, `direction` (rank 2, or empty for the full Cartesian
79    /// tensor), `mu` (single value), `T`, `eta` and `omega` from the
80    /// parameter set; `spin` and `field_symmetry` are ignored.
81    pub fn optical_conductivity(
82        &self,
83        params: &Parameters<DIM>,
84    ) -> Result<OpticalConductivityResult<DIM>> {
85        validate_chemical_potentials(&params.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(&params.T)?;
105        validate_broadening(params.eta)?;
106        crate::response::config::validate_k_mesh(&params.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(&params.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, &params.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        // An empty direction matrix selects the full Cartesian tensor.
317        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(&params).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(&params).unwrap();
331
332        params.integration = Integration::Simplex;
333        let simplex = model.optical_conductivity(&params).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}