1use nalgebra::{DMatrix, SymmetricEigen};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BondOrderEntry {
14 pub atom_i: usize,
16 pub atom_j: usize,
18 pub distance: f64,
20 pub wiberg: f64,
22 pub mayer: f64,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct BondOrderResult {
29 pub bonds: Vec<BondOrderEntry>,
31 pub num_atoms: usize,
33 pub wiberg_valence: Vec<f64>,
35 pub mayer_valence: Vec<f64>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PopulationResult {
47 pub mulliken_charges: Vec<f64>,
49 pub lowdin_charges: Vec<f64>,
51 pub mulliken_populations: Vec<f64>,
53 pub num_atoms: usize,
55 pub total_charge_mulliken: f64,
57 pub total_charge_lowdin: f64,
59 pub charge_conservation_error: f64,
62}
63
64pub(crate) fn build_density_matrix(coefficients: &[Vec<f64>], n_electrons: usize) -> DMatrix<f64> {
71 let n_ao = coefficients.len();
72 let n_occupied = n_electrons / 2;
73 let mut p = DMatrix::zeros(n_ao, n_ao);
74
75 for i in 0..n_occupied {
76 for mu in 0..n_ao {
77 for nu in 0..n_ao {
78 p[(mu, nu)] += 2.0 * coefficients[mu][i] * coefficients[nu][i];
79 }
80 }
81 }
82
83 if n_electrons % 2 == 1 {
85 let i = n_occupied;
86 for mu in 0..n_ao {
87 for nu in 0..n_ao {
88 p[(mu, nu)] += coefficients[mu][i] * coefficients[nu][i];
89 }
90 }
91 }
92
93 p
94}
95
96pub(crate) fn valence_electrons(z: u8) -> f64 {
99 match z {
100 1 => 1.0, 2 => 2.0, 3 => 1.0, 4 => 2.0, 5 => 3.0, 6 => 4.0, 7 => 5.0, 8 => 6.0, 9 => 7.0, 10 => 8.0, 11 => 1.0, 12 => 2.0, 13 => 3.0, 14 => 4.0, 15 => 5.0, 16 => 6.0, 17 => 7.0, 18 => 8.0, 19 => 1.0, 20 => 2.0, 31 => 3.0, 32 => 4.0, 33 => 5.0, 34 => 6.0, 35 => 7.0, 36 => 8.0, 37 => 1.0, 38 => 2.0, 49 => 3.0, 50 => 4.0, 51 => 5.0, 52 => 6.0, 53 => 7.0, 54 => 8.0, 21 => 3.0, 22 => 4.0, 23 => 5.0, 24 => 6.0, 25 => 7.0, 26 => 8.0, 27 => 9.0, 28 => 10.0, 29 => 11.0, 30 => 12.0, 39 => 3.0, 40 => 4.0, 41 => 5.0, 42 => 6.0, 43 => 7.0, 44 => 8.0, 45 => 9.0, 46 => 10.0, 47 => 11.0, 48 => 12.0, 72 => 4.0, 73 => 5.0, 74 => 6.0, 75 => 7.0, 76 => 8.0, 77 => 9.0, 78 => 10.0, 79 => 11.0, 80 => 12.0, 55 => 1.0, 56 => 2.0, 81 => 3.0, 82 => 4.0, 83 => 5.0, 84 => 6.0, 85 => 7.0, 86 => 8.0, 57 => 3.0, 58 => 4.0, 59 => 5.0, 60 => 6.0, 61 => 7.0, 62 => 8.0, 63 => 9.0, 64 => 10.0, 65 => 11.0, 66 => 12.0, 67 => 13.0, 68 => 14.0, 69 => 15.0, 70 => 16.0, 71 => 17.0, _ => 0.0,
197 }
198}
199
200pub(crate) fn ao_to_atom_map(basis: &[crate::eht::basis::AtomicOrbital]) -> Vec<usize> {
202 basis.iter().map(|ao| ao.atom_index).collect()
203}
204
205fn lowdin_orthogonalized_density(overlap: &DMatrix<f64>, density: &DMatrix<f64>) -> DMatrix<f64> {
206 let n_ao = overlap.nrows();
207 let s_eigen = SymmetricEigen::new(overlap.clone());
208 let mut s_sqrt_diag = DMatrix::zeros(n_ao, n_ao);
209 for i in 0..n_ao {
210 let val = s_eigen.eigenvalues[i];
211 if val > 1e-10 {
212 s_sqrt_diag[(i, i)] = val.sqrt();
213 }
214 }
215 let s_sqrt = &s_eigen.eigenvectors * &s_sqrt_diag * s_eigen.eigenvectors.transpose();
216 &s_sqrt * density * &s_sqrt
217}
218
219pub fn mulliken_charges(
224 elements: &[u8],
225 basis: &[crate::eht::basis::AtomicOrbital],
226 overlap: &DMatrix<f64>,
227 coefficients: &[Vec<f64>],
228 n_electrons: usize,
229) -> Vec<f64> {
230 let n_ao = basis.len();
231 let n_atoms = elements.len();
232 let ao_map = ao_to_atom_map(basis);
233 let p = build_density_matrix(coefficients, n_electrons);
234
235 let ps = &p * overlap;
237
238 let mut atom_pop = vec![0.0; n_atoms];
240 for mu in 0..n_ao {
241 atom_pop[ao_map[mu]] += ps[(mu, mu)];
242 }
243
244 (0..n_atoms)
246 .map(|a| valence_electrons(elements[a]) - atom_pop[a])
247 .collect()
248}
249
250pub fn lowdin_charges(
255 elements: &[u8],
256 basis: &[crate::eht::basis::AtomicOrbital],
257 overlap: &DMatrix<f64>,
258 coefficients: &[Vec<f64>],
259 n_electrons: usize,
260) -> Vec<f64> {
261 let n_ao = basis.len();
262 let n_atoms = elements.len();
263 let ao_map = ao_to_atom_map(basis);
264 let p = build_density_matrix(coefficients, n_electrons);
265
266 let sps = lowdin_orthogonalized_density(overlap, &p);
268
269 let mut atom_pop = vec![0.0; n_atoms];
270 for mu in 0..n_ao {
271 atom_pop[ao_map[mu]] += sps[(mu, mu)];
272 }
273
274 (0..n_atoms)
275 .map(|a| valence_electrons(elements[a]) - atom_pop[a])
276 .collect()
277}
278
279pub fn compute_population(
281 elements: &[u8],
282 positions: &[[f64; 3]],
283 coefficients: &[Vec<f64>],
284 n_electrons: usize,
285) -> PopulationResult {
286 let basis = crate::eht::basis::build_basis(elements, positions);
287 let overlap = crate::eht::build_overlap_matrix(&basis);
288 let n_ao = basis.len();
289 let n_atoms = elements.len();
290 let ao_map = ao_to_atom_map(&basis);
291 let p = build_density_matrix(coefficients, n_electrons);
292
293 let ps = &p * &overlap;
295 let mut mulliken_pop = vec![0.0; n_atoms];
296 let mut mulliken_ao_pop = vec![0.0; n_ao];
297 for mu in 0..n_ao {
298 mulliken_ao_pop[mu] = ps[(mu, mu)];
299 mulliken_pop[ao_map[mu]] += ps[(mu, mu)];
300 }
301 let mulliken_charges: Vec<f64> = (0..n_atoms)
302 .map(|a| valence_electrons(elements[a]) - mulliken_pop[a])
303 .collect();
304 let total_mulliken: f64 = mulliken_charges.iter().sum();
305
306 let sps = lowdin_orthogonalized_density(&overlap, &p);
308 let mut lowdin_pop = vec![0.0; n_atoms];
309 for mu in 0..n_ao {
310 lowdin_pop[ao_map[mu]] += sps[(mu, mu)];
311 }
312 let lowdin_charges: Vec<f64> = (0..n_atoms)
313 .map(|a| valence_electrons(elements[a]) - lowdin_pop[a])
314 .collect();
315 let total_lowdin: f64 = lowdin_charges.iter().sum();
316
317 PopulationResult {
318 mulliken_charges,
319 lowdin_charges,
320 mulliken_populations: mulliken_ao_pop,
321 num_atoms: n_atoms,
322 total_charge_mulliken: total_mulliken,
323 total_charge_lowdin: total_lowdin,
324 charge_conservation_error: (total_mulliken - total_mulliken.round()).abs(),
325 }
326}
327
328#[cfg(feature = "parallel")]
330pub fn compute_population_parallel(
331 elements: &[u8],
332 positions: &[[f64; 3]],
333 coefficients: &[Vec<f64>],
334 n_electrons: usize,
335) -> PopulationResult {
336 use rayon::prelude::*;
337
338 let basis = crate::eht::basis::build_basis(elements, positions);
339 let overlap = crate::eht::build_overlap_matrix(&basis);
340 let n_ao = basis.len();
341 let n_atoms = elements.len();
342 let ao_map = ao_to_atom_map(&basis);
343 let p = build_density_matrix(coefficients, n_electrons);
344
345 let ps = &p * &overlap;
347 let mut mulliken_pop = vec![0.0; n_atoms];
348 let mut mulliken_ao_pop = vec![0.0; n_ao];
349 for mu in 0..n_ao {
350 mulliken_ao_pop[mu] = ps[(mu, mu)];
351 mulliken_pop[ao_map[mu]] += ps[(mu, mu)];
352 }
353 let mulliken_charges: Vec<f64> = (0..n_atoms)
354 .into_par_iter()
355 .map(|a| valence_electrons(elements[a]) - mulliken_pop[a])
356 .collect();
357 let total_mulliken: f64 = mulliken_charges.iter().sum();
358
359 let sps = lowdin_orthogonalized_density(&overlap, &p);
361 let lowdin_charges: Vec<f64> = (0..n_atoms)
362 .into_par_iter()
363 .map(|a| {
364 let mut pop = 0.0;
365 for mu in 0..n_ao {
366 if ao_map[mu] == a {
367 pop += sps[(mu, mu)];
368 }
369 }
370 valence_electrons(elements[a]) - pop
371 })
372 .collect();
373 let total_lowdin: f64 = lowdin_charges.iter().sum();
374
375 PopulationResult {
376 mulliken_charges,
377 lowdin_charges,
378 mulliken_populations: mulliken_ao_pop,
379 num_atoms: n_atoms,
380 total_charge_mulliken: total_mulliken,
381 total_charge_lowdin: total_lowdin,
382 charge_conservation_error: (total_mulliken - total_mulliken.round()).abs(),
383 }
384}
385
386pub fn compute_bond_orders(
388 elements: &[u8],
389 positions: &[[f64; 3]],
390 coefficients: &[Vec<f64>],
391 n_electrons: usize,
392) -> BondOrderResult {
393 let basis = crate::eht::basis::build_basis(elements, positions);
394 let overlap = crate::eht::build_overlap_matrix(&basis);
395 let ao_map = ao_to_atom_map(&basis);
396 let density = build_density_matrix(coefficients, n_electrons);
397 let ps = &density * &overlap;
398 let p_orth = lowdin_orthogonalized_density(&overlap, &density);
399
400 let mut atom_aos = vec![Vec::new(); elements.len()];
401 for (ao_idx, &atom_idx) in ao_map.iter().enumerate() {
402 atom_aos[atom_idx].push(ao_idx);
403 }
404
405 let mut bonds = Vec::new();
406 let mut wiberg_valence = vec![0.0; elements.len()];
407 let mut mayer_valence = vec![0.0; elements.len()];
408
409 for atom_i in 0..elements.len() {
410 for atom_j in (atom_i + 1)..elements.len() {
411 let mut wiberg = 0.0;
412 let mut mayer = 0.0;
413
414 for &mu in &atom_aos[atom_i] {
415 for &nu in &atom_aos[atom_j] {
416 let p_orth_mn = p_orth[(mu, nu)];
417 wiberg += p_orth_mn * p_orth_mn;
418 mayer += ps[(mu, nu)] * ps[(nu, mu)];
419 }
420 }
421
422 let dx = positions[atom_i][0] - positions[atom_j][0];
423 let dy = positions[atom_i][1] - positions[atom_j][1];
424 let dz = positions[atom_i][2] - positions[atom_j][2];
425 let distance = (dx * dx + dy * dy + dz * dz).sqrt();
426
427 wiberg_valence[atom_i] += wiberg;
428 wiberg_valence[atom_j] += wiberg;
429 mayer_valence[atom_i] += mayer;
430 mayer_valence[atom_j] += mayer;
431
432 bonds.push(BondOrderEntry {
433 atom_i,
434 atom_j,
435 distance,
436 wiberg,
437 mayer,
438 });
439 }
440 }
441
442 BondOrderResult {
443 bonds,
444 num_atoms: elements.len(),
445 wiberg_valence,
446 mayer_valence,
447 }
448}
449
450#[cfg(feature = "parallel")]
452pub fn compute_bond_orders_parallel(
453 elements: &[u8],
454 positions: &[[f64; 3]],
455 coefficients: &[Vec<f64>],
456 n_electrons: usize,
457) -> BondOrderResult {
458 use rayon::prelude::*;
459
460 let basis = crate::eht::basis::build_basis(elements, positions);
461 let overlap = crate::eht::build_overlap_matrix(&basis);
462 let ao_map = ao_to_atom_map(&basis);
463 let density = build_density_matrix(coefficients, n_electrons);
464 let ps = &density * &overlap;
465 let p_orth = lowdin_orthogonalized_density(&overlap, &density);
466
467 let mut atom_aos = vec![Vec::new(); elements.len()];
468 for (ao_idx, &atom_idx) in ao_map.iter().enumerate() {
469 atom_aos[atom_idx].push(ao_idx);
470 }
471
472 let n_atoms = elements.len();
474 let pairs: Vec<(usize, usize)> = (0..n_atoms)
475 .flat_map(|i| ((i + 1)..n_atoms).map(move |j| (i, j)))
476 .collect();
477
478 let bonds: Vec<BondOrderEntry> = pairs
479 .par_iter()
480 .map(|&(atom_i, atom_j)| {
481 let mut wiberg = 0.0;
482 let mut mayer = 0.0;
483
484 for &mu in &atom_aos[atom_i] {
485 for &nu in &atom_aos[atom_j] {
486 let p_orth_mn = p_orth[(mu, nu)];
487 wiberg += p_orth_mn * p_orth_mn;
488 mayer += ps[(mu, nu)] * ps[(nu, mu)];
489 }
490 }
491
492 let dx = positions[atom_i][0] - positions[atom_j][0];
493 let dy = positions[atom_i][1] - positions[atom_j][1];
494 let dz = positions[atom_i][2] - positions[atom_j][2];
495 let distance = (dx * dx + dy * dy + dz * dz).sqrt();
496
497 BondOrderEntry {
498 atom_i,
499 atom_j,
500 distance,
501 wiberg,
502 mayer,
503 }
504 })
505 .collect();
506
507 let mut wiberg_valence = vec![0.0; n_atoms];
508 let mut mayer_valence = vec![0.0; n_atoms];
509 for bond in &bonds {
510 wiberg_valence[bond.atom_i] += bond.wiberg;
511 wiberg_valence[bond.atom_j] += bond.wiberg;
512 mayer_valence[bond.atom_i] += bond.mayer;
513 mayer_valence[bond.atom_j] += bond.mayer;
514 }
515
516 BondOrderResult {
517 bonds,
518 num_atoms: n_atoms,
519 wiberg_valence,
520 mayer_valence,
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crate::eht::solve_eht;
528
529 fn h2_molecule() -> (Vec<u8>, Vec<[f64; 3]>) {
530 (vec![1, 1], vec![[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]])
531 }
532
533 fn water_molecule() -> (Vec<u8>, Vec<[f64; 3]>) {
534 (
535 vec![8, 1, 1],
536 vec![[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]],
537 )
538 }
539
540 #[test]
541 fn test_h2_symmetric_charges() {
542 let (elems, pos) = h2_molecule();
543 let result = solve_eht(&elems, &pos, None).unwrap();
544 let pop = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
545
546 assert!(
548 (pop.mulliken_charges[0] - pop.mulliken_charges[1]).abs() < 1e-6,
549 "H₂ Mulliken charges should be symmetric"
550 );
551 assert!(
552 pop.mulliken_charges[0].abs() < 0.01,
553 "H₂ charge should be ~0"
554 );
555 }
556
557 #[test]
558 fn test_water_oxygen_negative() {
559 let (elems, pos) = water_molecule();
560 let result = solve_eht(&elems, &pos, None).unwrap();
561 let pop = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
562
563 assert!(
565 pop.mulliken_charges[0] < 0.0,
566 "O in water should have negative Mulliken charge, got {}",
567 pop.mulliken_charges[0]
568 );
569 assert!(
570 pop.lowdin_charges[0] < 0.0,
571 "O in water should have negative Löwdin charge, got {}",
572 pop.lowdin_charges[0]
573 );
574 }
575
576 #[test]
577 fn test_charge_sum_conservation() {
578 let (elems, pos) = water_molecule();
579 let result = solve_eht(&elems, &pos, None).unwrap();
580 let pop = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
581
582 assert!(
584 pop.total_charge_mulliken.abs() < 0.01,
585 "Mulliken total charge should be ~0, got {}",
586 pop.total_charge_mulliken
587 );
588 assert!(
589 pop.total_charge_lowdin.abs() < 0.01,
590 "Löwdin total charge should be ~0, got {}",
591 pop.total_charge_lowdin
592 );
593 }
594
595 #[test]
596 fn test_hydrogen_symmetry_in_water() {
597 let (elems, pos) = water_molecule();
598 let result = solve_eht(&elems, &pos, None).unwrap();
599 let pop = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
600
601 assert!(
603 (pop.mulliken_charges[1] - pop.mulliken_charges[2]).abs() < 0.01,
604 "H charges in water should be symmetric: {} vs {}",
605 pop.mulliken_charges[1],
606 pop.mulliken_charges[2]
607 );
608 }
609
610 #[test]
611 fn test_lowdin_vs_mulliken_different() {
612 let (elems, pos) = water_molecule();
613 let result = solve_eht(&elems, &pos, None).unwrap();
614 let pop = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
615
616 let m_o = pop.mulliken_charges[0];
619 let l_o = pop.lowdin_charges[0];
620 assert!(
621 m_o.signum() == l_o.signum(),
622 "Both methods should agree on sign for O"
623 );
624 }
625
626 #[test]
627 fn test_gross_orbital_populations_sum() {
628 let (elems, pos) = h2_molecule();
629 let result = solve_eht(&elems, &pos, None).unwrap();
630 let pop = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
631
632 let total: f64 = pop.mulliken_populations.iter().sum();
634 assert!(
635 (total - result.n_electrons as f64).abs() < 0.01,
636 "AO pop sum {} should equal n_electrons {}",
637 total,
638 result.n_electrons
639 );
640 }
641
642 #[test]
643 fn test_h2_bond_order_is_positive() {
644 let (elems, pos) = h2_molecule();
645 let result = solve_eht(&elems, &pos, None).unwrap();
646 let bond_orders =
647 compute_bond_orders(&elems, &pos, &result.coefficients, result.n_electrons);
648
649 assert_eq!(bond_orders.bonds.len(), 1);
650 assert!(bond_orders.bonds[0].wiberg > 0.1);
651 assert!(bond_orders.bonds[0].mayer > 0.1);
652 }
653
654 #[test]
655 fn test_water_oh_bonds_exceed_hh_bond_order() {
656 let (elems, pos) = water_molecule();
657 let result = solve_eht(&elems, &pos, None).unwrap();
658 let bond_orders =
659 compute_bond_orders(&elems, &pos, &result.coefficients, result.n_electrons);
660
661 let oh_1 = bond_orders
662 .bonds
663 .iter()
664 .find(|bond| bond.atom_i == 0 && bond.atom_j == 1)
665 .unwrap();
666 let oh_2 = bond_orders
667 .bonds
668 .iter()
669 .find(|bond| bond.atom_i == 0 && bond.atom_j == 2)
670 .unwrap();
671 let hh = bond_orders
672 .bonds
673 .iter()
674 .find(|bond| bond.atom_i == 1 && bond.atom_j == 2)
675 .unwrap();
676
677 assert!(oh_1.wiberg > hh.wiberg);
678 assert!(oh_2.wiberg > hh.wiberg);
679 assert!(oh_1.mayer > hh.mayer);
680 assert!(oh_2.mayer > hh.mayer);
681 }
682
683 #[test]
688 fn test_valence_electrons_period1_period2() {
689 assert_eq!(valence_electrons(1), 1.0); assert_eq!(valence_electrons(2), 2.0); assert_eq!(valence_electrons(6), 4.0); assert_eq!(valence_electrons(7), 5.0); assert_eq!(valence_electrons(8), 6.0); assert_eq!(valence_electrons(9), 7.0); assert_eq!(valence_electrons(10), 8.0); }
697
698 #[test]
699 fn test_valence_electrons_period6_main_group() {
700 assert_eq!(valence_electrons(55), 1.0); assert_eq!(valence_electrons(56), 2.0); assert_eq!(valence_electrons(81), 3.0); assert_eq!(valence_electrons(82), 4.0); assert_eq!(valence_electrons(83), 5.0); assert_eq!(valence_electrons(84), 6.0); assert_eq!(valence_electrons(85), 7.0); assert_eq!(valence_electrons(86), 8.0); }
709
710 #[test]
711 fn test_valence_electrons_lanthanides() {
712 assert_eq!(valence_electrons(57), 3.0); assert_eq!(valence_electrons(58), 4.0); assert_eq!(valence_electrons(63), 9.0); assert_eq!(valence_electrons(64), 10.0); assert_eq!(valence_electrons(70), 16.0); assert_eq!(valence_electrons(71), 17.0); }
720
721 #[test]
722 fn test_valence_electrons_3d_transition_metals() {
723 assert_eq!(valence_electrons(21), 3.0); assert_eq!(valence_electrons(22), 4.0); assert_eq!(valence_electrons(26), 8.0); assert_eq!(valence_electrons(29), 11.0); assert_eq!(valence_electrons(30), 12.0); }
729
730 #[test]
731 fn test_valence_electrons_4d_transition_metals() {
732 assert_eq!(valence_electrons(39), 3.0); assert_eq!(valence_electrons(44), 8.0); assert_eq!(valence_electrons(46), 10.0); assert_eq!(valence_electrons(47), 11.0); assert_eq!(valence_electrons(48), 12.0); }
738
739 #[test]
740 fn test_valence_electrons_5d_transition_metals() {
741 assert_eq!(valence_electrons(72), 4.0); assert_eq!(valence_electrons(74), 6.0); assert_eq!(valence_electrons(76), 8.0); assert_eq!(valence_electrons(78), 10.0); assert_eq!(valence_electrons(79), 11.0); assert_eq!(valence_electrons(80), 12.0); }
748
749 #[test]
750 fn test_valence_electrons_unknown_returns_zero() {
751 assert_eq!(valence_electrons(87), 0.0); assert_eq!(valence_electrons(0), 0.0); assert_eq!(valence_electrons(255), 0.0);
755 }
756
757 #[cfg(feature = "parallel")]
762 #[test]
763 fn test_population_parallel_matches_serial() {
764 let (elems, pos) = water_molecule();
765 let result = solve_eht(&elems, &pos, None).unwrap();
766
767 let serial = compute_population(&elems, &pos, &result.coefficients, result.n_electrons);
768 let parallel =
769 compute_population_parallel(&elems, &pos, &result.coefficients, result.n_electrons);
770
771 for i in 0..elems.len() {
772 assert!(
773 (serial.mulliken_charges[i] - parallel.mulliken_charges[i]).abs() < 1e-10,
774 "Mulliken mismatch at atom {}: serial={:.6} vs parallel={:.6}",
775 i,
776 serial.mulliken_charges[i],
777 parallel.mulliken_charges[i]
778 );
779 assert!(
780 (serial.lowdin_charges[i] - parallel.lowdin_charges[i]).abs() < 1e-10,
781 "Löwdin mismatch at atom {}: serial={:.6} vs parallel={:.6}",
782 i,
783 serial.lowdin_charges[i],
784 parallel.lowdin_charges[i]
785 );
786 }
787 }
788
789 #[cfg(feature = "parallel")]
790 #[test]
791 fn test_bond_orders_parallel_matches_serial() {
792 let (elems, pos) = water_molecule();
793 let result = solve_eht(&elems, &pos, None).unwrap();
794
795 let serial = compute_bond_orders(&elems, &pos, &result.coefficients, result.n_electrons);
796 let parallel =
797 compute_bond_orders_parallel(&elems, &pos, &result.coefficients, result.n_electrons);
798
799 assert_eq!(serial.bonds.len(), parallel.bonds.len());
800 for (s, p) in serial.bonds.iter().zip(parallel.bonds.iter()) {
801 assert_eq!(s.atom_i, p.atom_i);
802 assert_eq!(s.atom_j, p.atom_j);
803 assert!(
804 (s.wiberg - p.wiberg).abs() < 1e-10,
805 "Wiberg mismatch ({},{}): {:.6} vs {:.6}",
806 s.atom_i,
807 s.atom_j,
808 s.wiberg,
809 p.wiberg
810 );
811 assert!(
812 (s.mayer - p.mayer).abs() < 1e-10,
813 "Mayer mismatch ({},{}): {:.6} vs {:.6}",
814 s.atom_i,
815 s.atom_j,
816 s.mayer,
817 p.mayer
818 );
819 }
820 }
821}