phasesmith_io/
space_groups.rs1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use moyo::base::Operation;
7use moyo::data::{HallSymbol, HallSymbolEntry, Setting, hall_symbol_entry, operations_from_number};
8use phasesmith_crystallography::{Rational, SpaceGroup, SymmetryError, SymmetryOperation};
9
10const HALL_ENTRY_COUNT: i32 = 530;
11const TRANSLATION_DENOMINATOR: i64 = 12;
12const TRANSLATION_DENOMINATOR_F64: f64 = 12.0;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct SpaceGroupDatabaseProvenance {
17 pub provider: &'static str,
19 pub version: &'static str,
21 pub lineage: &'static str,
23 pub hall_setting_count: usize,
25}
26
27pub const SPACE_GROUP_DATABASE_PROVENANCE: SpaceGroupDatabaseProvenance =
29 SpaceGroupDatabaseProvenance {
30 provider: "moyo",
31 version: "0.15.0",
32 lineage: "spglib Hall-symbol database",
33 hall_setting_count: 530,
34 };
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct SpaceGroupInfo {
39 pub number: i32,
41 pub hm_symbol: String,
43 pub hall_symbol: String,
45 pub setting: String,
47 pub space_group: SpaceGroup,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub enum SpaceGroupLookupError {
54 InvalidNumber,
56 UnknownSymbol {
58 symbol: String,
60 },
61 InvalidDatabaseEntry,
63 Symmetry(SymmetryError),
65}
66
67impl Display for SpaceGroupLookupError {
68 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::InvalidNumber => {
71 formatter.write_str("space-group number must be an integer in [1, 230]")
72 }
73 Self::UnknownSymbol { symbol } => {
74 write!(
75 formatter,
76 "unknown or ambiguous space-group symbol {symbol:?}"
77 )
78 }
79 Self::InvalidDatabaseEntry => {
80 formatter.write_str("native space-group database entry is invalid")
81 }
82 Self::Symmetry(error) => Display::fmt(error, formatter),
83 }
84 }
85}
86
87impl Error for SpaceGroupLookupError {
88 fn source(&self) -> Option<&(dyn Error + 'static)> {
89 match self {
90 Self::Symmetry(error) => Some(error),
91 _ => None,
92 }
93 }
94}
95
96pub fn space_group_by_number(number: i32) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
103 if !(1..=230).contains(&number) {
104 return Err(SpaceGroupLookupError::InvalidNumber);
105 }
106 let hall_number = Setting::Standard
107 .hall_number(number)
108 .ok_or(SpaceGroupLookupError::InvalidDatabaseEntry)?;
109 info_from_hall_number(hall_number)
110}
111
112pub fn space_group_by_hall_symbol(symbol: &str) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
121 let requested = symbol.trim();
122 let entry = entries()
123 .find(|entry| symbol_key(entry.hall_symbol) == symbol_key(requested))
124 .ok_or_else(|| unknown_symbol(symbol))?;
125 info_from_hall_number(entry.hall_number)
126}
127
128pub fn space_group_from_hall_symbol(symbol: &str) -> Result<SpaceGroup, SpaceGroupLookupError> {
142 let requested = normalize_hall_expression(symbol);
143 if requested.is_empty() {
144 return Err(unknown_symbol(symbol));
145 }
146 let hall_symbol = HallSymbol::new(&requested).ok_or_else(|| unknown_symbol(symbol))?;
147 let coset = hall_symbol.traverse();
148 let mut operations = Vec::with_capacity(coset.len() * hall_symbol.centering.order());
149 for lattice_point in hall_symbol.centering.lattice_points() {
150 for operation in &coset {
151 let translation =
152 (lattice_point + operation.translation).map(|value| value.rem_euclid(1.0));
153 operations.push(Operation::new(operation.rotation, translation));
154 }
155 }
156 exact_space_group_from_moyo_operations(&operations)
157}
158
159pub fn space_group_by_symbol(symbol: &str) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
170 let requested = symbol.trim();
171 if requested.is_empty() {
172 return Err(unknown_symbol(symbol));
173 }
174
175 if let Ok(info) = space_group_by_hall_symbol(requested) {
176 return Ok(info);
177 }
178
179 let (hm, qualifier) = hm_and_qualifier(requested);
180 let hm_key = symbol_key(hm);
181 let full_matches = entries()
182 .filter(|entry| symbol_key(entry.hm_full) == hm_key)
183 .filter(|entry| qualifier.is_none_or(|value| entry.setting.eq_ignore_ascii_case(value)))
184 .collect::<Vec<_>>();
185 if full_matches.len() == 1 {
186 return info_from_hall_number(full_matches[0].hall_number);
187 }
188
189 let short_numbers = entries()
190 .filter(|entry| symbol_key(entry.hm_short) == hm_key)
191 .filter(|entry| qualifier.is_none_or(|value| entry.setting.eq_ignore_ascii_case(value)))
192 .map(|entry| entry.number)
193 .collect::<std::collections::BTreeSet<_>>();
194 if short_numbers.len() != 1 {
195 return Err(unknown_symbol(symbol));
196 }
197 let number = *short_numbers
198 .first()
199 .ok_or_else(|| unknown_symbol(symbol))?;
200 if qualifier.is_none() {
201 return space_group_by_number(number);
202 }
203 let matched = entries()
204 .find(|entry| {
205 entry.number == number
206 && symbol_key(entry.hm_short) == hm_key
207 && qualifier.is_some_and(|value| entry.setting.eq_ignore_ascii_case(value))
208 })
209 .ok_or_else(|| unknown_symbol(symbol))?;
210 info_from_hall_number(matched.hall_number)
211}
212
213fn entries() -> impl Iterator<Item = HallSymbolEntry> {
214 (1..=HALL_ENTRY_COUNT).filter_map(hall_symbol_entry)
215}
216
217fn info_from_hall_number(hall_number: i32) -> Result<SpaceGroupInfo, SpaceGroupLookupError> {
218 let entry =
219 hall_symbol_entry(hall_number).ok_or(SpaceGroupLookupError::InvalidDatabaseEntry)?;
220 let operations =
221 operations_from_number(entry.number, Setting::HallNumber(entry.hall_number), false)
222 .map_err(|_| SpaceGroupLookupError::InvalidDatabaseEntry)?;
223 let space_group = exact_space_group_from_moyo_operations(&operations)?;
224 Ok(SpaceGroupInfo {
225 number: entry.number,
226 hm_symbol: entry.hm_short.replace('_', ""),
227 hall_symbol: entry.hall_symbol.replace('=', "\""),
228 setting: entry.setting.to_owned(),
229 space_group,
230 })
231}
232
233fn exact_space_group_from_moyo_operations(
234 operations: &[Operation],
235) -> Result<SpaceGroup, SpaceGroupLookupError> {
236 let operations = operations
237 .iter()
238 .map(|operation| {
239 let rotation = operation.rotation_as_array();
240 let translation = operation
241 .translation_as_array()
242 .map(rational_from_database_translation)
243 .into_iter()
244 .collect::<Result<Vec<_>, _>>()?;
245 let translation: [Rational; 3] = translation
246 .try_into()
247 .map_err(|_| SpaceGroupLookupError::InvalidDatabaseEntry)?;
248 SymmetryOperation::new(rotation, translation).map_err(SpaceGroupLookupError::Symmetry)
249 })
250 .collect::<Result<Vec<_>, _>>()?;
251 SpaceGroup::new(operations).map_err(SpaceGroupLookupError::Symmetry)
252}
253
254fn rational_from_database_translation(value: f64) -> Result<Rational, SpaceGroupLookupError> {
255 let normalized = value.rem_euclid(1.0);
256 let scaled = normalized * TRANSLATION_DENOMINATOR_F64;
257 let rounded = scaled.round();
258 if !rounded.is_finite() || (scaled - rounded).abs() > 1.0e-8 {
259 return Err(SpaceGroupLookupError::InvalidDatabaseEntry);
260 }
261 #[allow(clippy::cast_possible_truncation)]
262 let numerator = rounded as i64;
263 Rational::new(numerator, TRANSLATION_DENOMINATOR).map_err(SpaceGroupLookupError::Symmetry)
264}
265
266fn symbol_key(value: &str) -> String {
267 value
268 .chars()
269 .filter(|character| !character.is_whitespace() && *character != '_')
270 .map(|character| if character == '"' { '=' } else { character })
273 .flat_map(char::to_lowercase)
274 .collect()
275}
276
277fn normalize_hall_expression(value: &str) -> String {
278 value
279 .trim()
280 .chars()
281 .map(|character| match character {
282 '"' => '=',
283 '_' => ' ',
284 other => other,
285 })
286 .collect()
287}
288
289fn hm_and_qualifier(value: &str) -> (&str, Option<&str>) {
290 if let Some((base, qualifier)) = value.rsplit_once(':') {
291 let qualifier = qualifier.trim();
292 if matches!(qualifier.to_ascii_lowercase().as_str(), "h" | "r") {
293 return (base.trim(), Some(qualifier));
294 }
295 }
296 let mut words = value.split_whitespace().collect::<Vec<_>>();
297 if words.len() > 1 {
298 let last = words.last().copied().unwrap_or_default();
299 if matches!(last.to_ascii_lowercase().as_str(), "h" | "r") {
300 words.pop();
301 let split = value.len() - last.len();
302 return (value[..split].trim(), Some(last));
303 }
304 }
305 (value, None)
306}
307
308fn unknown_symbol(symbol: &str) -> SpaceGroupLookupError {
309 SpaceGroupLookupError::UnknownSymbol {
310 symbol: symbol.to_owned(),
311 }
312}