1use std::collections::HashSet;
4use std::fmt;
5
6use anyhow::{self, ensure, format_err};
7use approx;
8use derive_builder::Builder;
9use ndarray::{Array1, Array2, Ix2, s};
10use ndarray_einsum::*;
11use ndarray_linalg::types::Lapack;
12use num_complex::{Complex, ComplexFloat};
13use num_traits::float::{Float, FloatConst};
14
15use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled, StructureConstraint};
16use crate::auxiliary::molecule::Molecule;
17use crate::basis::ao::BasisAngularOrder;
18use crate::target::density::Density;
19
20#[cfg(test)]
21mod orbital_tests;
22
23pub mod orbital_analysis;
24pub mod orbital_projection;
25mod orbital_transformation;
26
27#[derive(Builder, Clone)]
34#[builder(build_fn(validate = "Self::validate"))]
35pub struct MolecularOrbital<'a, T, SC>
36where
37 T: ComplexFloat + Lapack,
38 SC: StructureConstraint,
39{
40 structure_constraint: SC,
42
43 component_index: usize,
46
47 #[builder(default = "false")]
50 complex_conjugated: bool,
51
52 baos: Vec<&'a BasisAngularOrder<'a>>,
56
57 complex_symmetric: bool,
60
61 mol: &'a Molecule,
63
64 coefficients: Array1<T>,
66
67 #[builder(default = "None")]
69 energy: Option<T>,
70
71 threshold: <T as ComplexFloat>::Real,
73}
74
75impl<'a, T, SC> MolecularOrbitalBuilder<'a, T, SC>
76where
77 T: ComplexFloat + Lapack,
78 SC: StructureConstraint,
79{
80 fn validate(&self) -> Result<(), String> {
81 let structcons = self
82 .structure_constraint
83 .as_ref()
84 .ok_or("No structure constraints found.".to_string())?;
85 let baos = self
86 .baos
87 .as_ref()
88 .ok_or("No `BasisAngularOrder`s found.".to_string())?;
89 let baos_length_check = baos.len() == structcons.n_explicit_comps_per_coefficient_matrix();
90 if !baos_length_check {
91 log::error!(
92 "The number of `BasisAngularOrder`s provided does not match the number of explicit components per coefficient matrix."
93 );
94 }
95
96 let nbas_tot = baos.iter().map(|bao| bao.n_funcs()).sum::<usize>();
97 let coefficients = self
98 .coefficients
99 .as_ref()
100 .ok_or("No coefficients found.".to_string())?;
101
102 let coefficients_shape_check = {
103 let nrows = nbas_tot;
104 if !coefficients.shape()[0] == nrows {
105 log::error!(
106 "Unexpected shapes of coefficient vector: {} {} expected, but {} found.",
107 nrows,
108 if nrows == 1 { "row" } else { "rows" },
109 coefficients.shape()[0],
110 );
111 false
112 } else {
113 true
114 }
115 };
116
117 let mol = self.mol.ok_or("No molecule found.".to_string())?;
118 let natoms_set = baos.iter().map(|bao| bao.n_atoms()).collect::<HashSet<_>>();
119 if natoms_set.len() != 1 {
120 return Err("Inconsistent numbers of atoms between `BasisAngularOrder`s of different explicit components.".to_string());
121 };
122 let n_atoms = natoms_set.iter().next().ok_or_else(|| {
123 "Unable to retrieve the number of atoms from the `BasisAngularOrder`s.".to_string()
124 })?;
125 let natoms_check = mol.atoms.len() == *n_atoms;
126 if !natoms_check {
127 log::error!(
128 "The number of atoms in the molecule does not match the number of local sites in the basis."
129 );
130 }
131
132 if baos_length_check && coefficients_shape_check && natoms_check {
133 Ok(())
134 } else {
135 Err(format!(
136 "Molecular orbital validation failed:
137 baos_length ({baos_length_check}),
138 coefficients_shape ({coefficients_shape_check}),
139 natoms ({natoms_check})."
140 ))
141 }
142 }
143}
144
145impl<'a, T, SC> MolecularOrbital<'a, T, SC>
146where
147 T: ComplexFloat + Clone + Lapack,
148 SC: StructureConstraint + Clone,
149{
150 pub fn builder() -> MolecularOrbitalBuilder<'a, T, SC> {
152 MolecularOrbitalBuilder::default()
153 }
154}
155
156impl<'a, T, SC> MolecularOrbital<'a, T, SC>
157where
158 T: ComplexFloat + Clone + Lapack,
159 SC: StructureConstraint,
160{
161 pub fn coefficients(&self) -> &Array1<T> {
163 &self.coefficients
164 }
165
166 pub fn structure_constraint(&self) -> &SC {
168 &self.structure_constraint
169 }
170
171 pub fn baos(&'_ self) -> &Vec<&'_ BasisAngularOrder<'_>> {
174 &self.baos
175 }
176
177 pub fn mol(&self) -> &Molecule {
179 self.mol
180 }
181
182 pub fn complex_symmetric(&self) -> bool {
184 self.complex_symmetric
185 }
186
187 pub fn energy(&self) -> Option<T> {
189 self.energy
190 }
191
192 pub fn threshold(&self) -> <T as ComplexFloat>::Real {
194 self.threshold
195 }
196}
197
198impl<'a, T> MolecularOrbital<'a, T, SpinConstraint>
199where
200 T: ComplexFloat + Clone + Lapack,
201{
202 pub fn to_generalised(&self) -> Self {
210 match self.structure_constraint {
211 SpinConstraint::Restricted(n) => {
212 let bao = self.baos[0];
213 let nbas = bao.n_funcs();
214
215 let cr = &self.coefficients;
216 let mut cg = Array1::<T>::zeros(nbas * usize::from(n));
217 let start = nbas * self.component_index;
218 let end = nbas * (self.component_index + 1);
219 cg.slice_mut(s![start..end]).assign(cr);
220 Self::builder()
221 .coefficients(cg)
222 .baos((0..n).map(|_| bao).collect::<Vec<_>>())
223 .mol(self.mol)
224 .structure_constraint(SpinConstraint::Generalised(n, false))
225 .component_index(0)
226 .complex_symmetric(self.complex_symmetric)
227 .threshold(self.threshold)
228 .build()
229 .expect("Unable to construct a generalised molecular orbital.")
230 }
231 SpinConstraint::Unrestricted(n, increasingm) => {
232 let bao = self.baos[0];
233 let nbas = bao.n_funcs();
234
235 let cr = &self.coefficients;
236 let mut cg = Array1::<T>::zeros(nbas * usize::from(n));
237 let start = nbas * self.component_index;
238 let end = nbas * (self.component_index + 1);
239 cg.slice_mut(s![start..end]).assign(cr);
240 Self::builder()
241 .coefficients(cg)
242 .baos((0..n).map(|_| bao).collect::<Vec<_>>())
243 .mol(self.mol)
244 .structure_constraint(SpinConstraint::Generalised(n, increasingm))
245 .component_index(0)
246 .complex_symmetric(self.complex_symmetric)
247 .threshold(self.threshold)
248 .build()
249 .expect("Unable to construct a generalised molecular orbital.")
250 }
251 SpinConstraint::Generalised(_, _) => self.clone(),
252 }
253 }
254}
255
256impl<'a> MolecularOrbital<'a, f64, SpinConstraint> {
257 pub fn to_total_density(&'a self) -> Result<Density<'a, f64>, anyhow::Error> {
259 match self.structure_constraint {
260 SpinConstraint::Restricted(nspins) => {
261 let denmat = f64::from(nspins)
262 * einsum(
263 "m,n->mn",
264 &[&self.coefficients.view(), &self.coefficients.view()],
265 )
266 .expect("Unable to construct a density matrix from the coefficient matrix.")
267 .into_dimensionality::<Ix2>()
268 .expect("Unable to convert the resultant density matrix to two dimensions.");
269 Density::<f64>::builder()
270 .density_matrix(denmat)
271 .bao(self.baos()[0])
272 .mol(self.mol())
273 .complex_symmetric(self.complex_symmetric())
274 .threshold(self.threshold())
275 .build()
276 .map_err(|err| format_err!(err))
277 }
278 SpinConstraint::Unrestricted(_, _) => {
279 let denmat = einsum(
280 "m,n->mn",
281 &[&self.coefficients.view(), &self.coefficients.view()],
282 )
283 .expect("Unable to construct a density matrix from the coefficient matrix.")
284 .into_dimensionality::<Ix2>()
285 .expect("Unable to convert the resultant density matrix to two dimensions.");
286 Density::<f64>::builder()
287 .density_matrix(denmat)
288 .bao(self.baos()[0])
289 .mol(self.mol())
290 .complex_symmetric(self.complex_symmetric())
291 .threshold(self.threshold())
292 .build()
293 .map_err(|err| format_err!(err))
294 }
295 SpinConstraint::Generalised(nspins, _) => {
296 let full_denmat = einsum(
297 "m,n->mn",
298 &[&self.coefficients.view(), &self.coefficients.view()],
299 )
300 .expect("Unable to construct a density matrix from the coefficient matrix.")
301 .into_dimensionality::<Ix2>()
302 .expect("Unable to convert the resultant density matrix to two dimensions.");
303
304 let nspatial_set = self
305 .baos()
306 .iter()
307 .map(|bao| bao.n_funcs())
308 .collect::<HashSet<_>>();
309 ensure!(
310 nspatial_set.len() == 1,
311 "Mismatched numbers of basis functions between the explicit components."
312 );
313 let nspatial = *nspatial_set.iter().next().ok_or_else(|| {
314 format_err!(
315 "Unable to extract the number of basis functions per explicit component."
316 )
317 })?;
318
319 let denmat = (0..usize::from(nspins)).fold(
320 Array2::<f64>::zeros((nspatial, nspatial)),
321 |acc, ispin| {
322 acc + full_denmat.slice(s![
323 ispin * nspatial..(ispin + 1) * nspatial,
324 ispin * nspatial..(ispin + 1) * nspatial
325 ])
326 },
327 );
328 Density::<f64>::builder()
329 .density_matrix(denmat)
330 .bao(self.baos()[0])
331 .mol(self.mol())
332 .complex_symmetric(self.complex_symmetric())
333 .threshold(self.threshold())
334 .build()
335 .map_err(|err| format_err!(err))
336 }
337 }
338 }
339}
340
341impl<'a, T> MolecularOrbital<'a, Complex<T>, SpinConstraint>
342where
343 T: Float + FloatConst + Lapack + From<u16>,
344 Complex<T>: Lapack,
345{
346 pub fn to_total_density(&'a self) -> Result<Density<'a, Complex<T>>, anyhow::Error> {
348 match self.structure_constraint {
349 SpinConstraint::Restricted(nspins) => {
350 let nspins_t = Complex::<T>::from(<T as From<u16>>::from(nspins));
351 let denmat = einsum(
352 "m,n->mn",
353 &[
354 &self.coefficients.view(),
355 &self.coefficients.map(Complex::conj).view(),
356 ],
357 )
358 .expect("Unable to construct a density matrix from the coefficient matrix.")
359 .into_dimensionality::<Ix2>()
360 .expect("Unable to convert the resultant density matrix to two dimensions.")
361 .map(|x| x * nspins_t);
362 Density::<Complex<T>>::builder()
363 .density_matrix(denmat)
364 .bao(self.baos()[0])
365 .mol(self.mol())
366 .complex_symmetric(self.complex_symmetric())
367 .threshold(self.threshold())
368 .build()
369 .map_err(|err| format_err!(err))
370 }
371 SpinConstraint::Unrestricted(_, _) => {
372 let denmat = einsum(
373 "m,n->mn",
374 &[
375 &self.coefficients.view(),
376 &self.coefficients.map(Complex::conj).view(),
377 ],
378 )
379 .expect("Unable to construct a density matrix from the coefficient matrix.")
380 .into_dimensionality::<Ix2>()
381 .expect("Unable to convert the resultant density matrix to two dimensions.");
382 Density::<Complex<T>>::builder()
383 .density_matrix(denmat)
384 .bao(self.baos()[0])
385 .mol(self.mol())
386 .complex_symmetric(self.complex_symmetric())
387 .threshold(self.threshold())
388 .build()
389 .map_err(|err| format_err!(err))
390 }
391 SpinConstraint::Generalised(nspins, _) => {
392 let full_denmat = einsum(
393 "m,n->mn",
394 &[
395 &self.coefficients.view(),
396 &self.coefficients.map(Complex::conj).view(),
397 ],
398 )
399 .expect("Unable to construct a density matrix from the coefficient matrix.")
400 .into_dimensionality::<Ix2>()
401 .expect("Unable to convert the resultant density matrix to two dimensions.");
402
403 let nspatial_set = self
404 .baos()
405 .iter()
406 .map(|bao| bao.n_funcs())
407 .collect::<HashSet<_>>();
408 ensure!(
409 nspatial_set.len() == 1,
410 "Mismatched numbers of basis functions between the explicit components in the generalised spin constraint."
411 );
412 let nspatial = *nspatial_set.iter().next().ok_or_else(|| {
413 format_err!(
414 "Unable to extract the number of basis functions per explicit component."
415 )
416 })?;
417
418 let denmat = (0..usize::from(nspins)).fold(
419 Array2::<Complex<T>>::zeros((nspatial, nspatial)),
420 |acc, ispin| {
421 acc + full_denmat.slice(s![
422 ispin * nspatial..(ispin + 1) * nspatial,
423 ispin * nspatial..(ispin + 1) * nspatial
424 ])
425 },
426 );
427 Density::<Complex<T>>::builder()
428 .density_matrix(denmat)
429 .bao(self.baos()[0])
430 .mol(self.mol())
431 .complex_symmetric(self.complex_symmetric())
432 .threshold(self.threshold())
433 .build()
434 .map_err(|err| format_err!(err))
435 }
436 }
437 }
438}
439
440impl<'a, T> MolecularOrbital<'a, Complex<T>, SpinOrbitCoupled>
441where
442 T: Float + FloatConst + Lapack + From<u16>,
443 Complex<T>: Lapack,
444{
445 pub fn to_total_density(&'a self) -> Result<Density<'a, Complex<T>>, anyhow::Error> {
447 Err(format_err!(
448 "The total density of a spin--orbit-coupled molecular orbital is not implemented."
449 ))
450 }
498}
499
500impl<'a, T, SC> From<MolecularOrbital<'a, T, SC>> for MolecularOrbital<'a, Complex<T>, SC>
508where
509 T: Float + FloatConst + Lapack,
510 Complex<T>: Lapack,
511 SC: StructureConstraint + Clone,
512{
513 fn from(value: MolecularOrbital<'a, T, SC>) -> Self {
514 MolecularOrbital::<'a, Complex<T>, SC>::builder()
515 .coefficients(value.coefficients.map(Complex::from))
516 .baos(value.baos.clone())
517 .mol(value.mol)
518 .structure_constraint(value.structure_constraint)
519 .component_index(value.component_index)
520 .complex_symmetric(value.complex_symmetric)
521 .threshold(value.threshold)
522 .build()
523 .expect("Unable to construct a complex molecular orbital.")
524 }
525}
526
527impl<'a, T, SC> PartialEq for MolecularOrbital<'a, T, SC>
531where
532 T: ComplexFloat<Real = f64> + Lapack,
533 SC: StructureConstraint + PartialEq,
534{
535 fn eq(&self, other: &Self) -> bool {
536 let thresh = (self.threshold * other.threshold).sqrt();
537 let coefficients_eq = approx::relative_eq!(
538 (&self.coefficients - &other.coefficients)
539 .map(|x| ComplexFloat::abs(*x).powi(2))
540 .sum()
541 .sqrt(),
542 0.0,
543 epsilon = thresh,
544 max_relative = thresh,
545 );
546 self.structure_constraint == other.structure_constraint
547 && self.component_index == other.component_index
548 && self.baos == other.baos
549 && self.mol == other.mol
550 && coefficients_eq
551 }
552}
553
554impl<'a, T, SC> Eq for MolecularOrbital<'a, T, SC>
558where
559 T: ComplexFloat<Real = f64> + Lapack,
560 SC: StructureConstraint + Eq,
561{
562}
563
564impl<'a, T, SC> fmt::Debug for MolecularOrbital<'a, T, SC>
568where
569 T: fmt::Debug + ComplexFloat + Lapack,
570 SC: StructureConstraint + fmt::Debug,
571{
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 write!(
574 f,
575 "MolecularOrbital[{:?} (spin index {}): coefficient array of length {}]",
576 self.structure_constraint,
577 self.component_index,
578 self.coefficients.len()
579 )?;
580 Ok(())
581 }
582}
583
584impl<'a, T, SC> fmt::Display for MolecularOrbital<'a, T, SC>
588where
589 T: fmt::Display + ComplexFloat + Lapack,
590 SC: StructureConstraint + fmt::Display,
591{
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 write!(
594 f,
595 "MolecularOrbital[{} (spin index {}): coefficient array of length {}]",
596 self.structure_constraint,
597 self.component_index,
598 self.coefficients.len()
599 )?;
600 Ok(())
601 }
602}