use std::fmt;
use std::path::PathBuf;
use anyhow::{bail, format_err};
use derive_builder::Builder;
use itertools::Itertools;
use log;
use nalgebra::{Point3, Vector3};
use serde::{Deserialize, Serialize};
use crate::auxiliary::atom::{Atom, AtomKind};
use crate::auxiliary::molecule::Molecule;
use crate::drivers::QSym2Driver;
use crate::io::format::{
log_subtitle, log_title, nice_bool, qsym2_output, write_subtitle, QSym2Output,
};
use crate::io::{write_qsym2_binary, QSym2FileType};
use crate::symmetry::symmetry_core::{PreSymmetry, Symmetry};
use crate::symmetry::symmetry_element::{AntiunitaryKind, SymmetryElementKind};
#[cfg(test)]
#[path = "symmetry_group_detection_tests.rs"]
mod symmetry_group_detection_tests;
fn default_thresholds() -> Vec<f64> {
vec![1.0e-4, 1e-5, 1.0e-6]
}
#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
pub struct SymmetryGroupDetectionParams {
#[builder(setter(custom), default = "vec![1.0e-4, 1.0e-5, 1.0e-6]")]
#[serde(default = "default_thresholds")]
pub moi_thresholds: Vec<f64>,
#[builder(setter(custom), default = "vec![1.0e-4, 1.0e-5, 1.0e-6]")]
#[serde(default = "default_thresholds")]
pub distance_thresholds: Vec<f64>,
#[builder(default = "false")]
#[serde(default)]
pub time_reversal: bool,
#[builder(default = "None")]
#[serde(default)]
pub fictitious_magnetic_fields: Option<Vec<(Point3<f64>, Vector3<f64>)>>,
#[builder(default = "None")]
#[serde(default)]
pub fictitious_electric_fields: Option<Vec<(Point3<f64>, Vector3<f64>)>>,
#[builder(default = "false")]
#[serde(default)]
pub field_origin_com: bool,
#[builder(default = "false")]
#[serde(default)]
pub write_symmetry_elements: bool,
#[builder(default = "None")]
#[serde(default)]
pub result_save_name: Option<PathBuf>,
}
impl SymmetryGroupDetectionParams {
pub fn builder() -> SymmetryGroupDetectionParamsBuilder {
SymmetryGroupDetectionParamsBuilder::default()
}
}
impl SymmetryGroupDetectionParamsBuilder {
pub fn moi_thresholds(&mut self, threshs: &[f64]) -> &mut Self {
self.moi_thresholds = Some(threshs.to_vec());
self
}
pub fn distance_thresholds(&mut self, threshs: &[f64]) -> &mut Self {
self.distance_thresholds = Some(threshs.to_vec());
self
}
}
impl Default for SymmetryGroupDetectionParams {
fn default() -> Self {
Self::builder()
.build()
.expect("Unable to construct a default `SymmetryGroupDetectionParams`.")
}
}
impl fmt::Display for SymmetryGroupDetectionParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let threshs = self
.moi_thresholds
.iter()
.cartesian_product(self.distance_thresholds.iter());
let nthreshs = threshs.clone().count();
if nthreshs == 1 {
writeln!(f, "Fixed thresholds:")?;
writeln!(f, " MoI threshold: {:.3e}", self.moi_thresholds[0])?;
writeln!(f, " Geo threshold: {:.3e}", self.distance_thresholds[0])?;
} else {
writeln!(f, "Variable thresholds:")?;
writeln!(
f,
" MoI thresholds: {}",
self.moi_thresholds
.iter()
.map(|v| format!("{v:.3e}"))
.join(", ")
)?;
writeln!(
f,
" Geo thresholds: {}",
self.distance_thresholds
.iter()
.map(|v| format!("{v:.3e}"))
.join(", ")
)?;
writeln!(f)?;
}
if self.fictitious_magnetic_fields.is_some() || self.fictitious_electric_fields.is_some() {
if self.field_origin_com {
writeln!(f, "Field origins relative to: molecule's centre of mass")?;
} else {
writeln!(f, "Field origins relative to: space-fixed origin")?;
}
}
if let Some(fictitious_magnetic_fields) = self.fictitious_magnetic_fields.as_ref() {
writeln!(f, "Fictitious magnetic fields:")?;
for (origin, field) in fictitious_magnetic_fields.iter() {
writeln!(
f,
" ({}) ± ({})",
origin.iter().map(|x| format!("{x:+.3}")).join(", "),
field.iter().map(|x| format!("{x:+.3}")).join(", "),
)?;
}
writeln!(f)?;
}
if let Some(fictitious_electric_fields) = self.fictitious_electric_fields.as_ref() {
writeln!(f, "Fictitious electric fields:")?;
for (origin, field) in fictitious_electric_fields.iter() {
writeln!(
f,
" ({}) + ({})",
origin.iter().map(|x| format!("{x:+.3}")).join(", "),
field.iter().map(|x| format!("{x:+.3}")).join(", "),
)?;
}
writeln!(f)?;
}
writeln!(
f,
"Consider time reversal: {}",
nice_bool(self.time_reversal)
)?;
writeln!(
f,
"Report symmetry elements/generators: {}",
nice_bool(self.write_symmetry_elements)
)?;
writeln!(
f,
"Save symmetry-group detection results to file: {}",
if let Some(name) = self.result_save_name.as_ref() {
let mut path = name.clone();
path.set_extension(QSym2FileType::Sym.ext());
path.display().to_string()
} else {
nice_bool(false)
}
)?;
writeln!(f)?;
Ok(())
}
}
#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
pub struct SymmetryGroupDetectionResult {
pub parameters: SymmetryGroupDetectionParams,
pub pre_symmetry: PreSymmetry,
pub unitary_symmetry: Symmetry,
#[builder(default = "None")]
pub magnetic_symmetry: Option<Symmetry>,
}
impl SymmetryGroupDetectionResult {
fn builder() -> SymmetryGroupDetectionResultBuilder {
SymmetryGroupDetectionResultBuilder::default()
}
fn write_symmetry_elements(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(magnetic_symmetry) = self.magnetic_symmetry.as_ref() {
write_subtitle(
f,
&format!(
"Symmetry element report for magnetic group {}",
magnetic_symmetry
.group_name
.as_ref()
.unwrap_or(&"?".to_string()),
),
)?;
writeln!(f)?;
write_element_table(f, magnetic_symmetry)?;
writeln!(f)?;
}
write_subtitle(
f,
&format!(
"Symmetry element report for unitary group {}",
self.unitary_symmetry
.group_name
.as_ref()
.unwrap_or(&"?".to_string())
),
)?;
writeln!(f)?;
write_element_table(f, &self.unitary_symmetry)?;
Ok(())
}
}
impl fmt::Display for SymmetryGroupDetectionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(highest_mag_sym) = self.magnetic_symmetry.as_ref() {
let n_mag_elements = if highest_mag_sym.is_infinite() {
"∞".to_string()
} else {
highest_mag_sym.n_elements().to_string()
};
writeln!(
f,
"Highest mag. group found: {} ({} {})",
highest_mag_sym
.group_name
.as_ref()
.unwrap_or(&"?".to_string()),
n_mag_elements,
if n_mag_elements != "1" {
"symmetry elements"
} else {
"symmetry element"
}
)?;
}
let n_uni_elements = if self.unitary_symmetry.is_infinite() {
"∞".to_string()
} else {
self.unitary_symmetry.n_elements().to_string()
};
writeln!(
f,
"Highest uni. group found: {} ({} {})",
self.unitary_symmetry
.group_name
.as_ref()
.unwrap_or(&"?".to_string()),
n_uni_elements,
if n_uni_elements != "1" {
"symmetry elements"
} else {
"symmetry element"
}
)?;
writeln!(
f,
" Associated MoI threshold: {:.3e}",
self.pre_symmetry.moi_threshold
)?;
writeln!(
f,
" Associated geo threshold: {:.3e}",
self.pre_symmetry.dist_threshold
)?;
writeln!(f)?;
if self.parameters.write_symmetry_elements {
self.write_symmetry_elements(f)?;
}
Ok(())
}
}
#[derive(Clone, Builder)]
pub struct SymmetryGroupDetectionDriver<'a> {
parameters: &'a SymmetryGroupDetectionParams,
#[builder(default = "None")]
xyz: Option<PathBuf>,
#[builder(default = "None")]
molecule: Option<&'a Molecule>,
#[builder(setter(skip), default = "None")]
result: Option<SymmetryGroupDetectionResult>,
}
impl<'a> SymmetryGroupDetectionDriver<'a> {
pub fn builder() -> SymmetryGroupDetectionDriverBuilder<'a> {
SymmetryGroupDetectionDriverBuilder::default()
}
fn detect_symmetry_group(&mut self) -> Result<(), anyhow::Error> {
log_title("Symmetry-Group Detection");
qsym2_output!("");
let params = self.parameters;
params.log_output_display();
let smallest_dist_thresh = *params
.distance_thresholds
.iter()
.min_by(|x, y| {
x.partial_cmp(y)
.expect("Unable to determine the smallest distance threshold.")
})
.ok_or_else(|| format_err!("Unable to determine the smallest distance threshold."))?;
let target_mol = match (self.molecule, self.xyz.as_ref()) {
(Some(molecule), None) => Molecule::from_atoms(
&molecule.get_all_atoms().into_iter().cloned().collect_vec(),
smallest_dist_thresh,
),
(None, Some(xyz)) => Molecule::from_xyz(xyz, smallest_dist_thresh),
_ => bail!("Neither or both `molecule` and `xyz` are specified."),
};
qsym2_output!("Molecule for symmetry-group detection:");
target_mol.log_output_display();
qsym2_output!("");
let threshs = params
.moi_thresholds
.iter()
.cartesian_product(params.distance_thresholds.iter());
let nthreshs = threshs.clone().count();
log_subtitle("Threshold-scanning symmetry-group detection");
qsym2_output!("");
let count_length = usize::try_from(nthreshs.ilog10() + 2).map_err(|_| {
format_err!("Unable to convert `{}` to `usize`.", nthreshs.ilog10() + 2)
})?;
qsym2_output!("{}", "┈".repeat(count_length + 75));
qsym2_output!(
"{:>width$} {:>12} {:>12} {:>14} {:>9} {:>12} {:>9}",
"#",
"MoI thresh",
"Geo thresh",
"Mag. group",
"Elements",
"Uni. group",
"Elements",
width = count_length
);
qsym2_output!("{}", "┈".repeat(count_length + 75));
let mut i = 0;
let syms = threshs.map(|(moi_thresh, dist_thresh)| {
let mut mol = match (self.molecule, self.xyz.as_ref()) {
(Some(molecule), None) => Molecule::from_atoms(
&molecule.get_all_atoms().into_iter().cloned().collect_vec(),
*dist_thresh
),
(None, Some(xyz)) => Molecule::from_xyz(
xyz,
*dist_thresh
),
_ => {
qsym2_output!("Neither or both `molecule` and `xyz` are specified.");
bail!("Neither or both `molecule` and `xyz` are specified.")
}
};
let global_origin = if params.field_origin_com {
mol.calc_com() - Point3::origin()
} else {
Vector3::zeros()
};
if let Some(fictitious_magnetic_fields) = params.fictitious_magnetic_fields.as_ref() {
if mol.magnetic_atoms.is_some() {
log::error!("Magnetic fields already present. Fictitious magnetic fields cannot be added.");
bail!("Magnetic fields already present. Fictitious magnetic fields cannot be added.")
} else {
let magnetic_atoms = fictitious_magnetic_fields.iter().flat_map(|(origin, vec)| {
Ok::<[Atom; 2], anyhow::Error>([
Atom::new_special(AtomKind::Magnetic(true), origin + global_origin + vec, *dist_thresh).ok_or_else(||
format_err!("Cannot construct a fictitious magnetic atom.")
)?,
Atom::new_special(AtomKind::Magnetic(false), origin + global_origin - vec, *dist_thresh).ok_or_else(||
format_err!("Cannot construct a fictitious magnetic atom.")
)?,
])
}).flatten().collect_vec();
mol.magnetic_atoms = Some(magnetic_atoms);
}
}
if let Some(fictitious_electric_fields) = params.fictitious_electric_fields.as_ref() {
if mol.electric_atoms.is_some() {
log::error!("Electric fields already present. Fictitious electric fields cannot be added.");
bail!("Electric fields already present. Fictitious electric fields cannot be added.")
} else {
let electric_atoms = fictitious_electric_fields.iter().flat_map(|(origin, vec)| {
Atom::new_special(AtomKind::Electric(true), origin + global_origin + vec, *dist_thresh).ok_or_else(||
format_err!("Cannot construct a fictitious electric atom.")
)
}).collect_vec();
mol.electric_atoms = Some(electric_atoms);
}
}
let presym = PreSymmetry::builder()
.moi_threshold(*moi_thresh)
.molecule(&mol)
.build()
.map_err(|_| format_err!("Cannot construct a pre-symmetry structure."))?;
let mut uni_sym = Symmetry::new();
let uni_res = uni_sym.analyse(&presym, false);
let uni_ok = uni_res.is_ok();
if !uni_ok {
log::error!("{}", uni_res.unwrap_err())
}
let uni_group_name = uni_sym.group_name.clone().unwrap_or("?".to_string());
let uni_group_nele = if uni_sym.is_infinite() {
"∞".to_string()
} else {
uni_sym.n_elements().to_string()
};
let (mag_sym_opt, mag_ok) = if params.time_reversal {
let mut mag_sym = Symmetry::new();
let mag_res = mag_sym.analyse(&presym, true);
let mag_ok = mag_res.is_ok();
if !mag_ok {
log::error!("{}", mag_res.unwrap_err())
}
(Some(mag_sym), mag_ok)
} else {
(None, true)
};
let mag_group_name = mag_sym_opt
.as_ref()
.map(|mag_sym| {
mag_sym
.group_name
.clone()
.unwrap_or("?".to_string())
})
.unwrap_or_else(|| "--".to_string());
let mag_group_nele = mag_sym_opt
.as_ref()
.map(|mag_sym| if mag_sym.is_infinite() {
"∞".to_string()
} else {
mag_sym.n_elements().to_string()
})
.unwrap_or_else(|| "--".to_string());
i += 1;
if uni_ok && mag_ok {
qsym2_output!(
"{:>width$} {:>12.3e} {:>12.3e} {:>14} {:>9} {:>12} {:>9}",
i,
moi_thresh,
dist_thresh,
mag_group_name,
mag_group_nele,
uni_group_name,
uni_group_nele,
width = count_length
);
Ok((presym, uni_sym, mag_sym_opt))
} else {
if !uni_ok {
log::debug!(
"Unitary group detection with MoI threshold {:.3e} and distance threshold {:.3e} has failed.",
moi_thresh,
dist_thresh
);
}
if !mag_ok {
log::debug!(
"Magnetic group detection with MoI threshold {:.3e} and distance threshold {:.3e} has failed.",
moi_thresh,
dist_thresh
);
}
qsym2_output!(
"{:>width$} {:>12.3e} {:>12.3e} {:>14} {:>9} {:>12} {:>9}",
i,
moi_thresh,
dist_thresh,
"--",
"--",
"--",
"--",
width = count_length
);
log::error!(
"Group determination with MoI threshold {:.3e} and distance threshold {:.3e} has failed.",
moi_thresh,
dist_thresh
);
bail!(
"Group determination with MoI threshold {:.3e} and distance threshold {:.3e} has failed.",
moi_thresh,
dist_thresh
)
}
})
.filter_map(|res_sym| res_sym.ok())
.collect_vec();
qsym2_output!("{}", "┈".repeat(count_length + 75));
qsym2_output!(
"(The number of symmetry elements is not the same as the order of the group.)"
);
qsym2_output!("");
let (highest_presym, highest_uni_sym, highest_mag_sym_opt) = syms
.into_iter()
.max_by(
|(presym_a, uni_sym_a, mag_sym_opt_a), (presym_b, uni_sym_b, mag_sym_opt_b)| {
(
mag_sym_opt_a
.as_ref()
.map(|mag_sym| mag_sym.is_infinite())
.unwrap_or(false),
mag_sym_opt_a
.as_ref()
.map(|mag_sym| mag_sym.n_elements())
.unwrap_or(0),
uni_sym_a.is_infinite(),
uni_sym_a.n_elements(),
1.0 / presym_a.moi_threshold,
1.0 / presym_a.dist_threshold,
)
.partial_cmp(&(
mag_sym_opt_b
.as_ref()
.map(|mag_sym| mag_sym.is_infinite())
.unwrap_or(false),
mag_sym_opt_b
.as_ref()
.map(|mag_sym| mag_sym.n_elements())
.unwrap_or(0),
uni_sym_b.is_infinite(),
uni_sym_b.n_elements(),
1.0 / presym_b.moi_threshold,
1.0 / presym_b.dist_threshold,
))
.expect("Unable to perform a comparison.")
},
)
.ok_or_else(|| {
format_err!("Unable to identify the highest-symmetry group.".to_string())
})?;
self.result = SymmetryGroupDetectionResult::builder()
.parameters(params.clone())
.pre_symmetry(highest_presym)
.unitary_symmetry(highest_uni_sym)
.magnetic_symmetry(highest_mag_sym_opt)
.build()
.ok();
if let Some(pd_res) = self.result.as_ref() {
pd_res.log_output_display();
if let Some(name) = params.result_save_name.as_ref() {
write_qsym2_binary(name, QSym2FileType::Sym, pd_res)?;
let mut path = name.to_path_buf();
path.set_extension(QSym2FileType::Sym.ext());
qsym2_output!(
"Symmetry-group detection results saved as {}.",
path.display().to_string()
);
qsym2_output!("");
}
}
Ok(())
}
}
impl QSym2Driver for SymmetryGroupDetectionDriver<'_> {
type Params = SymmetryGroupDetectionParams;
type Outcome = SymmetryGroupDetectionResult;
fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
self.result
.as_ref()
.ok_or_else(|| format_err!("No symmetry-group detection results found."))
}
fn run(&mut self) -> Result<(), anyhow::Error> {
self.detect_symmetry_group()
}
}
fn write_element_table(f: &mut fmt::Formatter<'_>, sym: &Symmetry) -> fmt::Result {
let all_elements = sym
.elements
.iter()
.map(|(kind, kind_elements)| (false, kind, kind_elements))
.chain(
sym.generators
.iter()
.map(|(kind, kind_generators)| (true, kind, kind_generators)),
);
all_elements
.sorted_by_key(|(generator, kind, _)| {
(
*generator,
kind.contains_antiunitary(),
!matches!(kind, SymmetryElementKind::Proper(_)),
)
})
.try_for_each(|(generator, kind, kind_elements)| {
if !sym.is_infinite() && generator {
Ok::<(), fmt::Error>(())
} else {
if generator {
writeln!(f, "> {kind} generators")?;
} else {
writeln!(f, "> {kind} elements")?;
}
writeln!(f, "{}", "┈".repeat(54))?;
writeln!(
f,
"{:>7} {:>7} {:>11} {:>11} {:>11}",
"", "Symbol", "x", "y", "z"
)?;
writeln!(f, "{}", "┈".repeat(54))?;
kind_elements.keys().sorted().try_for_each(|order| {
let order_elements = kind_elements.get(order).unwrap_or_else(|| {
panic!("Elements/generators of order `{order}` cannot be retrieved.")
});
let any_element = order_elements
.get_index(0)
.expect("Unable to retrieve an element/generator of order `{order}`.");
let kind_str = match any_element.kind() {
SymmetryElementKind::Proper(_) => "",
SymmetryElementKind::ImproperInversionCentre(_) => " (inversion-centre)",
SymmetryElementKind::ImproperMirrorPlane(_) => " (mirror-plane)",
};
let au_str = match any_element.contains_antiunitary() {
None => "",
Some(AntiunitaryKind::TimeReversal) => " (time-reversed)",
Some(AntiunitaryKind::ComplexConjugation) => " (complex-conjugated)",
};
writeln!(f, " Order: {order}{au_str}{kind_str}")?;
order_elements.iter().try_for_each(|element| {
let axis = element.raw_axis();
writeln!(
f,
"{:>7} {:>7} {:>+11.7} {:>+11.7} {:>+11.7}",
element.get_simplified_symbol(),
element.get_full_symbol(),
axis[0],
axis[1],
axis[2]
)?;
Ok::<(), fmt::Error>(())
})?;
Ok::<(), fmt::Error>(())
})?;
writeln!(f, "{}", "┈".repeat(54))?;
writeln!(f)?;
Ok::<(), fmt::Error>(())
}
})?;
Ok(())
}