phasesmith_io/
space_groups.rs1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use moyo::data::{HallSymbolEntry, Setting, hall_symbol_entry, operations_from_number};
7use phasesmith_crystallography::{Rational, SpaceGroup, SymmetryError, SymmetryOperation};
8
9const HALL_ENTRY_COUNT: i32 = 530;
10const TRANSLATION_DENOMINATOR: i64 = 12;
11const TRANSLATION_DENOMINATOR_F64: f64 = 12.0;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct SpaceGroupDatabaseProvenance {
16 pub provider: &'static str,
18 pub version: &'static str,
20 pub lineage: &'static str,
22 pub hall_setting_count: usize,
24}
25
26pub const SPACE_GROUP_DATABASE_PROVENANCE: SpaceGroupDatabaseProvenance =
28 SpaceGroupDatabaseProvenance {
29 provider: "moyo",
30 version: "0.15.0",
31 lineage: "spglib Hall-symbol database",
32 hall_setting_count: 530,
33 };
34
35#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct SpaceGroupInfo {
38 pub number: i32,
40 pub hm_symbol: String,
42 pub hall_symbol: String,
44 pub setting: String,
46 pub space_group: SpaceGroup,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum SpaceGroupLookupError {
53 InvalidNumber,
55 UnknownSymbol {
57 symbol: String,
59 },
60 InvalidDatabaseEntry,
62 Symmetry(SymmetryError),
64}
65
66impl Display for SpaceGroupLookupError {
67 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
68 match self {
69 Self::InvalidNumber => {
70 formatter.write_str("space-group number must be an integer in [1, 230]")
71 }
72 Self::UnknownSymbol { symbol } => {
73 write!(
74 formatter,
75 "unknown or ambiguous space-group symbol {symbol:?}"
76 )
77 }
78 Self::InvalidDatabaseEntry => {
79 formatter.write_str("native space-group database entry is invalid")
80 }
81 Self::Symmetry(error) => Display::fmt(error, formatter),
82 }
83 }
84}
85
86impl Error for SpaceGroupLookupError {
87 fn source(&self) -> Option<&(dyn Error + 'static)> {
88 match self {
89 Self::Symmetry(error) => Some(error),
90 _ => None,
91 }
92 }
93}
94
95pub fn space_group_by_number(number: i32) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
102 if !(1..=230).contains(&number) {
103 return Err(SpaceGroupLookupError::InvalidNumber);
104 }
105 let hall_number = Setting::Standard
106 .hall_number(number)
107 .ok_or(SpaceGroupLookupError::InvalidDatabaseEntry)?;
108 info_from_hall_number(hall_number)
109}
110
111pub fn space_group_by_hall_symbol(symbol: &str) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
120 let requested = symbol.trim();
121 let entry = entries()
122 .find(|entry| symbol_key(entry.hall_symbol) == symbol_key(requested))
123 .ok_or_else(|| unknown_symbol(symbol))?;
124 info_from_hall_number(entry.hall_number)
125}
126
127pub fn space_group_by_symbol(symbol: &str) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
138 let requested = symbol.trim();
139 if requested.is_empty() {
140 return Err(unknown_symbol(symbol));
141 }
142
143 if let Ok(info) = space_group_by_hall_symbol(requested) {
144 return Ok(info);
145 }
146
147 let (hm, qualifier) = hm_and_qualifier(requested);
148 let hm_key = symbol_key(hm);
149 let full_matches = entries()
150 .filter(|entry| symbol_key(entry.hm_full) == hm_key)
151 .filter(|entry| qualifier.is_none_or(|value| entry.setting.eq_ignore_ascii_case(value)))
152 .collect::<Vec<_>>();
153 if full_matches.len() == 1 {
154 return info_from_hall_number(full_matches[0].hall_number);
155 }
156
157 let short_numbers = entries()
158 .filter(|entry| symbol_key(entry.hm_short) == hm_key)
159 .filter(|entry| qualifier.is_none_or(|value| entry.setting.eq_ignore_ascii_case(value)))
160 .map(|entry| entry.number)
161 .collect::<std::collections::BTreeSet<_>>();
162 if short_numbers.len() != 1 {
163 return Err(unknown_symbol(symbol));
164 }
165 let number = *short_numbers
166 .first()
167 .ok_or_else(|| unknown_symbol(symbol))?;
168 if qualifier.is_none() {
169 return space_group_by_number(number);
170 }
171 let matched = entries()
172 .find(|entry| {
173 entry.number == number
174 && symbol_key(entry.hm_short) == hm_key
175 && qualifier.is_some_and(|value| entry.setting.eq_ignore_ascii_case(value))
176 })
177 .ok_or_else(|| unknown_symbol(symbol))?;
178 info_from_hall_number(matched.hall_number)
179}
180
181fn entries() -> impl Iterator<Item = HallSymbolEntry> {
182 (1..=HALL_ENTRY_COUNT).filter_map(hall_symbol_entry)
183}
184
185fn info_from_hall_number(hall_number: i32) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
186 let entry =
187 hall_symbol_entry(hall_number).ok_or(SpaceGroupLookupError::InvalidDatabaseEntry)?;
188 let operations =
189 operations_from_number(entry.number, Setting::HallNumber(entry.hall_number), false)
190 .map_err(|_| SpaceGroupLookupError::InvalidDatabaseEntry)?;
191 let operations = operations
192 .iter()
193 .map(|operation| {
194 let rotation = operation.rotation_as_array();
195 let translation = operation
196 .translation_as_array()
197 .map(rational_from_database_translation)
198 .into_iter()
199 .collect::<Result<Vec<_>, _>>()?;
200 let translation: [Rational; 3] = translation
201 .try_into()
202 .map_err(|_| SpaceGroupLookupError::InvalidDatabaseEntry)?;
203 SymmetryOperation::new(rotation, translation).map_err(SpaceGroupLookupError::Symmetry)
204 })
205 .collect::<Result<Vec<_>, _>>()?;
206 let space_group = SpaceGroup::new(operations).map_err(SpaceGroupLookupError::Symmetry)?;
207 Ok(SpaceGroupInfo {
208 number: entry.number,
209 hm_symbol: entry.hm_short.replace('_', ""),
210 hall_symbol: entry.hall_symbol.replace('=', "\""),
211 setting: entry.setting.to_owned(),
212 space_group,
213 })
214}
215
216fn rational_from_database_translation(value: f64) -> Result<Rational, SpaceGroupLookupError> {
217 let normalized = value.rem_euclid(1.0);
218 let scaled = normalized * TRANSLATION_DENOMINATOR_F64;
219 let rounded = scaled.round();
220 if !rounded.is_finite() || (scaled - rounded).abs() > 1.0e-8 {
221 return Err(SpaceGroupLookupError::InvalidDatabaseEntry);
222 }
223 #[allow(clippy::cast_possible_truncation)]
224 let numerator = rounded as i64;
225 Rational::new(numerator, TRANSLATION_DENOMINATOR).map_err(SpaceGroupLookupError::Symmetry)
226}
227
228fn symbol_key(value: &str) -> String {
229 value
230 .chars()
231 .filter(|character| !character.is_whitespace() && *character != '_')
232 .map(|character| if character == '"' { '=' } else { character })
235 .flat_map(char::to_lowercase)
236 .collect()
237}
238
239fn hm_and_qualifier(value: &str) -> (&str, Option<&str>) {
240 if let Some((base, qualifier)) = value.rsplit_once(':') {
241 let qualifier = qualifier.trim();
242 if matches!(qualifier.to_ascii_lowercase().as_str(), "h" | "r") {
243 return (base.trim(), Some(qualifier));
244 }
245 }
246 let mut words = value.split_whitespace().collect::<Vec<_>>();
247 if words.len() > 1 {
248 let last = words.last().copied().unwrap_or_default();
249 if matches!(last.to_ascii_lowercase().as_str(), "h" | "r") {
250 words.pop();
251 let split = value.len() - last.len();
252 return (value[..split].trim(), Some(last));
253 }
254 }
255 (value, None)
256}
257
258fn unknown_symbol(symbol: &str) -> SpaceGroupLookupError {
259 SpaceGroupLookupError::UnknownSymbol {
260 symbol: symbol.to_owned(),
261 }
262}