use std::error::Error;
use std::fmt::{Display, Formatter};
use moyo::data::{HallSymbolEntry, Setting, hall_symbol_entry, operations_from_number};
use phasesmith_crystallography::{Rational, SpaceGroup, SymmetryError, SymmetryOperation};
const HALL_ENTRY_COUNT: i32 = 530;
const TRANSLATION_DENOMINATOR: i64 = 12;
const TRANSLATION_DENOMINATOR_F64: f64 = 12.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpaceGroupDatabaseProvenance {
pub provider: &'static str,
pub version: &'static str,
pub lineage: &'static str,
pub hall_setting_count: usize,
}
pub const SPACE_GROUP_DATABASE_PROVENANCE: SpaceGroupDatabaseProvenance =
SpaceGroupDatabaseProvenance {
provider: "moyo",
version: "0.15.0",
lineage: "spglib Hall-symbol database",
hall_setting_count: 530,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpaceGroupInfo {
pub number: i32,
pub hm_symbol: String,
pub hall_symbol: String,
pub setting: String,
pub space_group: SpaceGroup,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpaceGroupLookupError {
InvalidNumber,
UnknownSymbol {
symbol: String,
},
InvalidDatabaseEntry,
Symmetry(SymmetryError),
}
impl Display for SpaceGroupLookupError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidNumber => {
formatter.write_str("space-group number must be an integer in [1, 230]")
}
Self::UnknownSymbol { symbol } => {
write!(
formatter,
"unknown or ambiguous space-group symbol {symbol:?}"
)
}
Self::InvalidDatabaseEntry => {
formatter.write_str("native space-group database entry is invalid")
}
Self::Symmetry(error) => Display::fmt(error, formatter),
}
}
}
impl Error for SpaceGroupLookupError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Symmetry(error) => Some(error),
_ => None,
}
}
}
pub fn space_group_by_number(number: i32) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
if !(1..=230).contains(&number) {
return Err(SpaceGroupLookupError::InvalidNumber);
}
let hall_number = Setting::Standard
.hall_number(number)
.ok_or(SpaceGroupLookupError::InvalidDatabaseEntry)?;
info_from_hall_number(hall_number)
}
pub fn space_group_by_hall_symbol(symbol: &str) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
let requested = symbol.trim();
let entry = entries()
.find(|entry| symbol_key(entry.hall_symbol) == symbol_key(requested))
.ok_or_else(|| unknown_symbol(symbol))?;
info_from_hall_number(entry.hall_number)
}
pub fn space_group_by_symbol(symbol: &str) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
let requested = symbol.trim();
if requested.is_empty() {
return Err(unknown_symbol(symbol));
}
if let Ok(info) = space_group_by_hall_symbol(requested) {
return Ok(info);
}
let (hm, qualifier) = hm_and_qualifier(requested);
let hm_key = symbol_key(hm);
let full_matches = entries()
.filter(|entry| symbol_key(entry.hm_full) == hm_key)
.filter(|entry| qualifier.is_none_or(|value| entry.setting.eq_ignore_ascii_case(value)))
.collect::<Vec<_>>();
if full_matches.len() == 1 {
return info_from_hall_number(full_matches[0].hall_number);
}
let short_numbers = entries()
.filter(|entry| symbol_key(entry.hm_short) == hm_key)
.filter(|entry| qualifier.is_none_or(|value| entry.setting.eq_ignore_ascii_case(value)))
.map(|entry| entry.number)
.collect::<std::collections::BTreeSet<_>>();
if short_numbers.len() != 1 {
return Err(unknown_symbol(symbol));
}
let number = *short_numbers
.first()
.ok_or_else(|| unknown_symbol(symbol))?;
if qualifier.is_none() {
return space_group_by_number(number);
}
let matched = entries()
.find(|entry| {
entry.number == number
&& symbol_key(entry.hm_short) == hm_key
&& qualifier.is_some_and(|value| entry.setting.eq_ignore_ascii_case(value))
})
.ok_or_else(|| unknown_symbol(symbol))?;
info_from_hall_number(matched.hall_number)
}
fn entries() -> impl Iterator<Item = HallSymbolEntry> {
(1..=HALL_ENTRY_COUNT).filter_map(hall_symbol_entry)
}
fn info_from_hall_number(hall_number: i32) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
let entry =
hall_symbol_entry(hall_number).ok_or(SpaceGroupLookupError::InvalidDatabaseEntry)?;
let operations =
operations_from_number(entry.number, Setting::HallNumber(entry.hall_number), false)
.map_err(|_| SpaceGroupLookupError::InvalidDatabaseEntry)?;
let operations = operations
.iter()
.map(|operation| {
let rotation = operation.rotation_as_array();
let translation = operation
.translation_as_array()
.map(rational_from_database_translation)
.into_iter()
.collect::<Result<Vec<_>, _>>()?;
let translation: [Rational; 3] = translation
.try_into()
.map_err(|_| SpaceGroupLookupError::InvalidDatabaseEntry)?;
SymmetryOperation::new(rotation, translation).map_err(SpaceGroupLookupError::Symmetry)
})
.collect::<Result<Vec<_>, _>>()?;
let space_group = SpaceGroup::new(operations).map_err(SpaceGroupLookupError::Symmetry)?;
Ok(SpaceGroupInfo {
number: entry.number,
hm_symbol: entry.hm_short.replace('_', ""),
hall_symbol: entry.hall_symbol.replace('=', "\""),
setting: entry.setting.to_owned(),
space_group,
})
}
fn rational_from_database_translation(value: f64) -> Result<Rational, SpaceGroupLookupError> {
let normalized = value.rem_euclid(1.0);
let scaled = normalized * TRANSLATION_DENOMINATOR_F64;
let rounded = scaled.round();
if !rounded.is_finite() || (scaled - rounded).abs() > 1.0e-8 {
return Err(SpaceGroupLookupError::InvalidDatabaseEntry);
}
#[allow(clippy::cast_possible_truncation)]
let numerator = rounded as i64;
Rational::new(numerator, TRANSLATION_DENOMINATOR).map_err(SpaceGroupLookupError::Symmetry)
}
fn symbol_key(value: &str) -> String {
value
.chars()
.filter(|character| !character.is_whitespace() && *character != '_')
.map(|character| if character == '"' { '=' } else { character })
.flat_map(char::to_lowercase)
.collect()
}
fn hm_and_qualifier(value: &str) -> (&str, Option<&str>) {
if let Some((base, qualifier)) = value.rsplit_once(':') {
let qualifier = qualifier.trim();
if matches!(qualifier.to_ascii_lowercase().as_str(), "h" | "r") {
return (base.trim(), Some(qualifier));
}
}
let mut words = value.split_whitespace().collect::<Vec<_>>();
if words.len() > 1 {
let last = words.last().copied().unwrap_or_default();
if matches!(last.to_ascii_lowercase().as_str(), "h" | "r") {
words.pop();
let split = value.len() - last.len();
return (value[..split].trim(), Some(last));
}
}
(value, None)
}
fn unknown_symbol(symbol: &str) -> SpaceGroupLookupError {
SpaceGroupLookupError::UnknownSymbol {
symbol: symbol.to_owned(),
}
}