1use std::fmt;
23use std::path::PathBuf;
24
25use anyhow::{ensure, format_err};
26use derive_builder::Builder;
27use itertools::Itertools;
28use nalgebra::Point3;
29use ndarray::{Array2, Axis};
30use num_traits::ToPrimitive;
31use rayon::prelude::*;
32use serde::{Deserialize, Serialize};
33
34use crate::auxiliary::geometry::Transform;
35use crate::auxiliary::molecule::Molecule;
36use crate::drivers::QSym2Driver;
37use crate::drivers::symmetry_group_detection::{
38 SymmetryGroupDetectionDriver, SymmetryGroupDetectionParams,
39};
40use crate::io::QSym2FileType;
41use crate::io::format::{QSym2Output, log_subtitle, log_title, nice_bool, qsym2_output};
42use crate::permutation::{IntoPermutation, Permutation};
43use crate::symmetry::symmetry_core::{PreSymmetry, Symmetry};
44
45#[cfg(test)]
46#[path = "molecule_symmetrisation_bootstrap_tests.rs"]
47mod molecule_symmetrisation_bootstrap_tests;
48
49fn default_true() -> bool {
58 true
59}
60fn default_max_iterations() -> usize {
61 50
62}
63fn default_consistent_iterations() -> usize {
64 10
65}
66fn default_loose_threshold() -> f64 {
67 1e-2
68}
69fn default_tight_threshold() -> f64 {
70 1e-7
71}
72
73#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
75pub struct MoleculeSymmetrisationBootstrapParams {
76 #[builder(default = "true")]
81 #[serde(default = "default_true")]
82 pub reorientate_molecule: bool,
83
84 #[builder(default = "1e-2")]
87 #[serde(default = "default_loose_threshold")]
88 pub loose_moi_threshold: f64,
89
90 #[builder(default = "1e-2")]
93 #[serde(default = "default_loose_threshold")]
94 pub loose_distance_threshold: f64,
95
96 #[builder(default = "1e-7")]
98 #[serde(default = "default_tight_threshold")]
99 pub target_moi_threshold: f64,
100
101 #[builder(default = "1e-7")]
103 #[serde(default = "default_tight_threshold")]
104 pub target_distance_threshold: f64,
105
106 #[builder(default = "50")]
108 #[serde(default = "default_max_iterations")]
109 pub max_iterations: usize,
110
111 #[builder(default = "10")]
115 #[serde(default = "default_consistent_iterations")]
116 pub consistent_target_symmetry_iterations: usize,
117
118 #[builder(default = "None")]
121 #[serde(default)]
122 pub infinite_order_to_finite: Option<u32>,
123
124 #[builder(default = "false")]
127 #[serde(default)]
128 pub use_magnetic_group: bool,
129
130 #[builder(default = "0")]
132 #[serde(default)]
133 pub verbose: u8,
134
135 #[builder(default = "None")]
138 #[serde(default)]
139 pub symmetrised_result_xyz: Option<PathBuf>,
140
141 #[builder(default = "None")]
145 #[serde(default)]
146 pub symmetrised_result_save_name: Option<PathBuf>,
147}
148
149impl MoleculeSymmetrisationBootstrapParams {
150 pub fn builder() -> MoleculeSymmetrisationBootstrapParamsBuilder {
152 MoleculeSymmetrisationBootstrapParamsBuilder::default()
153 }
154}
155
156impl Default for MoleculeSymmetrisationBootstrapParams {
157 fn default() -> Self {
158 Self::builder()
159 .build()
160 .expect("Unable to construct a default `MoleculeSymmetrisationBootstrapParams`.")
161 }
162}
163
164impl fmt::Display for MoleculeSymmetrisationBootstrapParams {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 writeln!(f, "Loose MoI threshold: {:.3e}", self.loose_moi_threshold)?;
167 writeln!(
168 f,
169 "Loose geo threshold: {:.3e}",
170 self.loose_distance_threshold
171 )?;
172 writeln!(f, "Target MoI threshold: {:.3e}", self.target_moi_threshold)?;
173 writeln!(
174 f,
175 "Target geo threshold: {:.3e}",
176 self.target_distance_threshold
177 )?;
178 writeln!(f)?;
179 writeln!(
180 f,
181 "Group used for symmetrisation: {}",
182 if self.use_magnetic_group {
183 "magnetic group"
184 } else {
185 "unitary group"
186 }
187 )?;
188 if let Some(finite_order) = self.infinite_order_to_finite {
189 writeln!(f, "Infinite order to finite: {finite_order}")?;
190 }
191 writeln!(
192 f,
193 "Maximum symmetrisation iterations: {}",
194 self.max_iterations
195 )?;
196 writeln!(
197 f,
198 "Target symmetry consistent iterations: {}",
199 self.consistent_target_symmetry_iterations
200 )?;
201 writeln!(f, "Output level: {}", self.verbose)?;
202 writeln!(
203 f,
204 "Save symmetrised molecule to XYZ file: {}",
205 if let Some(name) = self.symmetrised_result_xyz.as_ref() {
206 let mut path = name.clone();
207 path.set_extension("xyz");
208 path.display().to_string()
209 } else {
210 nice_bool(false)
211 }
212 )?;
213 writeln!(
214 f,
215 "Save symmetry-group detection results of symmetrised system to file: {}",
216 if let Some(name) = self.symmetrised_result_save_name.as_ref() {
217 let mut path = name.clone();
218 path.set_extension(QSym2FileType::Sym.ext());
219 path.display().to_string()
220 } else {
221 nice_bool(false)
222 }
223 )?;
224 writeln!(f)?;
225
226 Ok(())
227 }
228}
229
230#[derive(Clone, Builder, Debug)]
236pub struct MoleculeSymmetrisationBootstrapResult<'a> {
237 parameters: &'a MoleculeSymmetrisationBootstrapParams,
239
240 pub symmetrised_molecule: Molecule,
242}
243
244impl<'a> MoleculeSymmetrisationBootstrapResult<'a> {
245 fn builder() -> MoleculeSymmetrisationBootstrapResultBuilder<'a> {
246 MoleculeSymmetrisationBootstrapResultBuilder::default()
247 }
248}
249
250#[derive(Clone, Builder)]
256#[builder(build_fn(validate = "Self::validate"))]
257pub struct MoleculeSymmetrisationBootstrapDriver<'a> {
258 parameters: &'a MoleculeSymmetrisationBootstrapParams,
260
261 molecule: &'a Molecule,
263
264 #[builder(setter(skip), default = "None")]
266 result: Option<MoleculeSymmetrisationBootstrapResult<'a>>,
267}
268
269impl<'a> MoleculeSymmetrisationBootstrapDriverBuilder<'a> {
270 fn validate(&self) -> Result<(), String> {
271 let params = self
272 .parameters
273 .ok_or("No molecule symmetrisation parameters found.".to_string())?;
274 if params.consistent_target_symmetry_iterations > params.max_iterations {
275 return Err(format!(
276 "The number of consistent target-symmetry iterations, `{}`, cannot exceed the \
277 maximum number of iterations, `{}`.",
278 params.consistent_target_symmetry_iterations, params.max_iterations,
279 ));
280 }
281 if params.target_moi_threshold < 0.0
282 || params.loose_moi_threshold < 0.0
283 || params.target_distance_threshold < 0.0
284 || params.loose_distance_threshold < 0.0
285 {
286 return Err("The thresholds cannot be negative.".to_string());
287 }
288 if params.target_moi_threshold > params.loose_moi_threshold {
289 return Err(format!(
290 "The target MoI threshold, `{:.3e}`, cannot be larger than the \
291 loose MoI threshold, `{:.3e}`.",
292 params.target_moi_threshold, params.loose_moi_threshold
293 ));
294 }
295 if params.target_distance_threshold > params.loose_distance_threshold {
296 return Err(format!(
297 "The target distance threshold, `{:.3e}`, cannot be larger than the \
298 loose distance threshold, `{:.3e}`.",
299 params.target_distance_threshold, params.loose_distance_threshold
300 ));
301 }
302 Ok(())
303 }
304}
305
306impl<'a> MoleculeSymmetrisationBootstrapDriver<'a> {
307 pub fn builder() -> MoleculeSymmetrisationBootstrapDriverBuilder<'a> {
309 MoleculeSymmetrisationBootstrapDriverBuilder::default()
310 }
311
312 fn symmetrise_molecule(&mut self) -> Result<(), anyhow::Error> {
314 log_title("Molecule Symmetrisation by Bootstrapping");
315 qsym2_output!("");
316 let params = self.parameters;
317 params.log_output_display();
318
319 let mut trial_mol = self.molecule.recentre();
320
321 if params.verbose >= 1 {
322 let orig_mol = self
323 .molecule
324 .adjust_threshold(params.target_distance_threshold);
325 qsym2_output!("Unsymmetrised original molecule:");
326 orig_mol.log_output_display();
327 qsym2_output!("");
328
329 qsym2_output!("Unsymmetrised recentred molecule:");
330 trial_mol.log_output_display();
331 qsym2_output!("");
332 }
333
334 if params.reorientate_molecule {
335 trial_mol.reorientate_mut(params.target_moi_threshold);
341 qsym2_output!("Unsymmetrised recentred and reoriented molecule:");
342 trial_mol.log_output_display();
343 qsym2_output!("");
344 };
345
346 log_subtitle("Iterative molecule symmetry bootstrapping");
347 qsym2_output!("");
348 qsym2_output!("Thresholds:");
349 qsym2_output!(
350 " Loose : {:.3e} (MoI) - {:.3e} (distance)",
351 params.loose_distance_threshold,
352 params.loose_moi_threshold,
353 );
354 qsym2_output!(
355 " Target: {:.3e} (MoI) - {:.3e} (distance)",
356 params.target_moi_threshold,
357 params.target_distance_threshold
358 );
359 qsym2_output!("");
360 qsym2_output!("Convergence criteria:");
361 qsym2_output!(
362 " either: (1) when the loose-threshold symmetry agrees with the target-threshold symmetry,",
363 );
364 qsym2_output!(
365 " or : (2) when the target-threshold symmetry contains more elements than the loose-threshold symmetry and has been consistently identified for {} consecutive iteration{}.",
366 params.consistent_target_symmetry_iterations,
367 if params.consistent_target_symmetry_iterations == 1 {
368 ""
369 } else {
370 "s"
371 }
372 );
373 qsym2_output!("");
374
375 let count_length = usize::try_from(params.max_iterations.ilog10() + 1).map_err(|_| {
376 format_err!(
377 "Unable to convert `{}` to `usize`.",
378 params.max_iterations.ilog10() + 1
379 )
380 })?;
381 qsym2_output!("{}", "┈".repeat(count_length + 101));
382 qsym2_output!(
383 " {:>count_length$} {:>22} {:>19} {:>22} {:>19} {:>10}",
384 "#",
385 "Rot. sym. (loose)",
386 "Group (loose)",
387 "Rot. sym. (target)",
388 "Group (target)",
389 "Converged?",
390 );
391 qsym2_output!("{}", "┈".repeat(count_length + 101));
392
393 let mut symmetrisation_count = 0;
394 let mut consistent_target_sym_count = 0;
395 let mut loose_ops = vec![];
396 let mut prev_target_sym_group_name: Option<String> = None;
397 let mut converged = false;
398 while symmetrisation_count == 0
399 || (!converged && symmetrisation_count < params.max_iterations)
400 {
401 symmetrisation_count += 1;
402
403 let mut loose_mol =
407 trial_mol.adjust_threshold(self.parameters.loose_distance_threshold);
408 let loose_presym = PreSymmetry::builder()
409 .moi_threshold(self.parameters.loose_moi_threshold)
410 .molecule(&loose_mol)
411 .build()
412 .map_err(|_| {
413 format_err!("Cannot construct a loose-threshold pre-symmetry structure.")
414 })?;
415
416 let mut loose_sym = Symmetry::new();
417
418 let _loose_res = loose_sym.analyse(&loose_presym, self.parameters.use_magnetic_group);
420
421 loose_ops.extend_from_slice(
426 &loose_sym.generate_all_operations(self.parameters.infinite_order_to_finite),
427 );
428 let n_ops_f64 = loose_ops.len().to_f64().ok_or_else(|| {
429 format_err!("Unable to convert the number of operations to `f64`.")
430 })?;
431
432 let ts = loose_ops
434 .into_par_iter()
435 .flat_map(|op| {
436 let tmat = op
437 .get_3d_spatial_matrix()
438 .select(Axis(0), &[2, 0, 1])
439 .select(Axis(1), &[2, 0, 1])
440 .reversed_axes();
441
442 let ord_perm = op
443 .act_permute(&loose_mol.molecule_ordinary_atoms())
444 .ok_or_else(|| {
445 format_err!(
446 "Unable to determine the ordinary-atom permutation corresponding to `{op}`."
447 )
448 })?;
449 let mag_perm_opt = loose_mol
450 .molecule_magnetic_atoms()
451 .as_ref()
452 .and_then(|loose_mag_mol| op.act_permute(loose_mag_mol));
453 let elec_perm_opt = loose_mol
454 .molecule_electric_atoms()
455 .as_ref()
456 .and_then(|loose_elec_mol| op.act_permute(loose_elec_mol));
457 Ok::<_, anyhow::Error>((tmat, ord_perm, mag_perm_opt, elec_perm_opt))
458 })
459 .collect::<Vec<_>>();
460
461 let loose_ord_coords = Array2::from_shape_vec(
463 (loose_mol.atoms.len(), 3),
464 loose_mol
465 .atoms
466 .iter()
467 .flat_map(|atom| atom.coordinates.coords.iter().cloned())
468 .collect::<Vec<_>>(),
469 )?;
470 let ave_ord_coords = ts.iter().fold(
471 Array2::<f64>::zeros(loose_ord_coords.raw_dim()),
474 |acc, (tmat, ord_perm, _, _)| {
475 acc + loose_ord_coords.dot(tmat).select(Axis(0), ord_perm.image())
479 },
480 ) / n_ops_f64;
481 loose_mol
482 .atoms
483 .par_iter_mut()
484 .enumerate()
485 .for_each(|(i, atom)| {
486 atom.coordinates = Point3::<f64>::from_slice(
487 ave_ord_coords
488 .row(i)
489 .as_slice()
490 .expect("Unable to convert a row of averaged coordinates to a slice."),
491 )
492 });
493
494 if let Some(mag_atoms) = loose_mol.magnetic_atoms.as_mut() {
496 let loose_mag_coords = Array2::from_shape_vec(
497 (mag_atoms.len(), 3),
498 mag_atoms
499 .iter()
500 .flat_map(|atom| atom.coordinates.coords.iter().cloned())
501 .collect::<Vec<_>>(),
502 )?;
503 let ave_mag_coords =
504 ts.iter().try_fold(
505 Array2::<f64>::zeros(loose_mag_coords.raw_dim()),
506 |acc: Array2<f64>,
507 (tmat, _, mag_perm_opt, _): &(
508 Array2<_>,
509 _,
510 Option<Permutation<usize>>,
511 _,
512 )| {
513 Ok::<_, anyhow::Error>(
517 acc + loose_mag_coords.dot(tmat).select(
518 Axis(0),
519 mag_perm_opt
520 .as_ref()
521 .ok_or_else(|| {
522 format_err!(
523 "Expected magnetic atom permutation not found."
524 )
525 })?
526 .image(),
527 ),
528 )
529 },
530 )? / n_ops_f64;
531 mag_atoms.iter_mut().enumerate().for_each(|(i, atom)| {
532 atom.coordinates = Point3::<f64>::from_slice(
533 ave_mag_coords
534 .row(i)
535 .as_slice()
536 .expect("Unable to convert a row of averaged coordinates to a slice."),
537 )
538 });
539 }
540
541 if let Some(elec_atoms) = loose_mol.electric_atoms.as_mut() {
543 let loose_elec_coords = Array2::from_shape_vec(
544 (elec_atoms.len(), 3),
545 elec_atoms
546 .iter()
547 .flat_map(|atom| atom.coordinates.coords.iter().cloned())
548 .collect::<Vec<_>>(),
549 )?;
550 let ave_elec_coords = ts.iter().try_fold(
551 Array2::<f64>::zeros(loose_elec_coords.raw_dim()),
552 |acc: Array2<f64>,
553 (tmat, _, _, elec_perm_opt): &(
554 Array2<_>,
555 _,
556 Option<Permutation<usize>>,
557 _,
558 )| {
559 Ok::<_, anyhow::Error>(
563 acc + loose_elec_coords.dot(tmat).select(
564 Axis(0),
565 elec_perm_opt
566 .as_ref()
567 .ok_or_else(|| {
568 format_err!("Expected electric atom permutation not found.")
569 })?
570 .image(),
571 ),
572 )
573 },
574 )? / n_ops_f64;
575 elec_atoms.iter_mut().enumerate().for_each(|(i, atom)| {
576 atom.coordinates = Point3::<f64>::from_slice(
577 ave_elec_coords
578 .row(i)
579 .as_slice()
580 .expect("Unable to convert a row of averaged coordinates to a slice."),
581 )
582 });
583 }
584
585 trial_mol = loose_mol;
586
587 trial_mol.recentre_mut();
589 if params.reorientate_molecule {
590 trial_mol.reorientate_mut(params.target_moi_threshold);
596 };
597
598 let target_mol = trial_mol.adjust_threshold(self.parameters.target_distance_threshold);
602 let target_presym = PreSymmetry::builder()
603 .moi_threshold(self.parameters.target_moi_threshold)
604 .molecule(&target_mol)
605 .build()
606 .map_err(|_| {
607 format_err!("Cannot construct a target-threshold pre-symmetry structure.")
608 })?;
609
610 let mut target_sym = Symmetry::new();
611
612 let _ = target_sym.analyse(&target_presym, params.use_magnetic_group);
613
614 let target_loose_consistent = target_sym.n_elements() == loose_sym.n_elements()
615 && target_sym.group_name.is_some()
616 && target_sym.group_name == loose_sym.group_name;
617
618 if target_sym.group_name == prev_target_sym_group_name
619 && target_sym.n_elements() >= loose_sym.n_elements()
620 {
621 consistent_target_sym_count += 1;
622 } else {
623 consistent_target_sym_count = 0;
624 }
625 prev_target_sym_group_name = target_sym.group_name.clone();
626 let target_consistent =
627 consistent_target_sym_count >= params.consistent_target_symmetry_iterations;
628
629 converged = target_loose_consistent || target_consistent;
630 let converged_reason = [target_loose_consistent, target_consistent]
631 .iter()
632 .enumerate()
633 .filter_map(|(i, c)| {
634 if *c {
635 Some(format!("({})", i + 1))
636 } else {
637 None
638 }
639 })
640 .join("");
641
642 qsym2_output!(
643 " {:>count_length$} {:>22} {:>19} {:>22} {:>19} {:>10}",
644 symmetrisation_count,
645 loose_presym.rotational_symmetry.to_string(),
646 format!(
647 "{} ({})",
648 loose_sym.group_name.as_ref().unwrap_or(&"--".to_string()),
649 loose_sym.n_elements()
650 ),
651 target_presym.rotational_symmetry.to_string(),
652 format!(
653 "{} ({})",
654 target_sym.group_name.as_ref().unwrap_or(&"--".to_string()),
655 target_sym.n_elements()
656 ),
657 if converged {
658 "yes ".to_string() + &converged_reason
659 } else {
660 "no".to_string()
661 },
662 );
663
664 loose_ops =
665 target_sym.generate_all_operations(self.parameters.infinite_order_to_finite);
666 }
667 qsym2_output!("{}", "┈".repeat(count_length + 101));
668 qsym2_output!("");
669
670 qsym2_output!("Verifying symmetrisation results...");
674 qsym2_output!("");
675 let verifying_pd_params = SymmetryGroupDetectionParams::builder()
676 .moi_thresholds(&[params.target_moi_threshold])
677 .distance_thresholds(&[params.target_distance_threshold])
678 .time_reversal(params.use_magnetic_group)
679 .write_symmetry_elements(true)
680 .result_save_name(params.symmetrised_result_save_name.clone())
681 .build()?;
682 let mut verifying_pd_driver = SymmetryGroupDetectionDriver::builder()
683 .parameters(&verifying_pd_params)
684 .molecule(Some(&trial_mol))
685 .build()?;
686 verifying_pd_driver.run()?;
687 let verifying_pd_res = verifying_pd_driver.result()?;
688 let verifying_group_name = if params.use_magnetic_group {
689 verifying_pd_res
690 .magnetic_symmetry
691 .as_ref()
692 .and_then(|magsym| magsym.group_name.as_ref())
693 } else {
694 verifying_pd_res.unitary_symmetry.group_name.as_ref()
695 };
696 ensure!(
697 prev_target_sym_group_name.as_ref() == verifying_group_name,
698 "Mismatched symmetry: iterative symmetry bootstrapping found {}, but verification found {}.",
699 prev_target_sym_group_name
700 .as_ref()
701 .unwrap_or(&"--".to_string()),
702 verifying_group_name.unwrap_or(&"--".to_string()),
703 );
704 qsym2_output!("Verifying symmetrisation results... Done.");
705 qsym2_output!("");
706
707 self.result = Some(
711 MoleculeSymmetrisationBootstrapResult::builder()
712 .parameters(self.parameters)
713 .symmetrised_molecule(trial_mol.clone())
714 .build()?,
715 );
716
717 if let Some(xyz_name) = params.symmetrised_result_xyz.as_ref() {
718 let mut path = xyz_name.clone();
719 path.set_extension("xyz");
720 verifying_pd_res
721 .pre_symmetry
722 .recentred_molecule
723 .to_xyz(&path)?;
724 qsym2_output!("Symmetrised molecule written to: {}", path.display());
725 qsym2_output!("");
726 }
727
728 Ok(())
729 }
730}
731
732impl<'a> QSym2Driver for MoleculeSymmetrisationBootstrapDriver<'a> {
733 type Params = MoleculeSymmetrisationBootstrapParams;
734
735 type Outcome = MoleculeSymmetrisationBootstrapResult<'a>;
736
737 fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
738 self.result
739 .as_ref()
740 .ok_or_else(|| format_err!("No molecule sprucing results found."))
741 }
742
743 fn run(&mut self) -> Result<(), anyhow::Error> {
744 self.symmetrise_molecule()
745 }
746}