From fcf81f555850bbc63e62e58ec9144d35373a3da8 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 1 Feb 2025 14:40:42 -0500 Subject: [PATCH] - add CrystalSystem enum to represent crystal system based on space group number - add crystal_system field to MoyoDataset - implement crystal system mapping for space groups 1-230 - add Python bindings for CrystalSystem - update tests to verify crystal system detection for various structures --- moyo/src/base/error.rs | 2 + moyo/src/lib.rs | 42 ++++++++++ moyo/tests/test_moyo_dataset.rs | 99 +++++++++++++++++++++++- moyopy/python/tests/test_moyo_dataset.py | 44 +++++++++++ moyopy/src/lib.rs | 59 +++++++++++++- 5 files changed, 241 insertions(+), 5 deletions(-) diff --git a/moyo/src/base/error.rs b/moyo/src/base/error.rs index 2b86d02..f831315 100644 --- a/moyo/src/base/error.rs +++ b/moyo/src/base/error.rs @@ -43,4 +43,6 @@ pub enum MoyoError { UnknownHallNumberError, #[error("Unknown number")] UnknownNumberError, + #[error("Invalid space group number: {0} (must be between 1 and 230)")] + InvalidSpaceGroupNumber(i32), } diff --git a/moyo/src/lib.rs b/moyo/src/lib.rs index 5d80ce9..50fcfb0 100644 --- a/moyo/src/lib.rs +++ b/moyo/src/lib.rs @@ -85,6 +85,31 @@ use crate::symmetrize::{orbits_in_cell, StandardizedCell, StandardizedMagneticCe use nalgebra::Matrix3; +/// The crystal system of a structure. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum CrystalSystem { + /// space groups 1-2: no symmetry constraints on cell parameters + Triclinic, + /// space groups 3-15: one unique axis with α = γ = 90° + Monoclinic, + /// space groups 16-74: Three orthogonal axes with α = β = γ = 90° + Orthorhombic, + /// space groups 75-142: two equal axes with α = β = γ = 90° + Tetragonal, + /// space groups 143-167: three equal axes with α = β = γ ≠ 90° + Trigonal, + /// space groups 168-194: two equal axes with α = β = 90°, γ = 120° + Hexagonal, + /// space groups 195-230: three equal axes with α = β = γ = 90° + Cubic, +} + +impl std::fmt::Display for CrystalSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + #[derive(Debug)] /// A dataset containing symmetry information of the input crystal structure. pub struct MoyoDataset { @@ -95,6 +120,8 @@ pub struct MoyoDataset { pub number: Number, /// Hall symbol number. pub hall_number: HallNumber, + /// The crystal system based on the space group number. + pub crystal_system: Result, // ------------------------------------------------------------------------ // Symmetry operations in the input cell // ------------------------------------------------------------------------ @@ -204,10 +231,25 @@ impl MoyoDataset { let prim_std_origin_shift = prim_cell_linear_inv * std_cell.prim_transformation.origin_shift; + let crystal_system = match space_group.number { + n if (1..=230).contains(&n) => Ok(match n { + 1..=2 => CrystalSystem::Triclinic, + 3..=15 => CrystalSystem::Monoclinic, + 16..=74 => CrystalSystem::Orthorhombic, + 75..=142 => CrystalSystem::Tetragonal, + 143..=167 => CrystalSystem::Trigonal, + 168..=194 => CrystalSystem::Hexagonal, + 195..=230 => CrystalSystem::Cubic, + _ => unreachable!(), + }), + n => Err(MoyoError::InvalidSpaceGroupNumber(n)), + }; + Ok(Self { // Space-group type number: space_group.number, hall_number: space_group.hall_number, + crystal_system, // Symmetry operations in the input cell operations, // Standardized cell diff --git a/moyo/tests/test_moyo_dataset.rs b/moyo/tests/test_moyo_dataset.rs index 215cbbe..ee5db62 100644 --- a/moyo/tests/test_moyo_dataset.rs +++ b/moyo/tests/test_moyo_dataset.rs @@ -7,9 +7,11 @@ use std::fs; use std::path::Path; use test_log::test; -use moyo::base::{AngleTolerance, Cell, Lattice, Permutation, Rotation, Translation}; -use moyo::data::Setting; -use moyo::MoyoDataset; +use moyo::{ + base::{AngleTolerance, Cell, Lattice, Permutation, Rotation, Translation}, + data::Setting, + CrystalSystem, MoyoDataset, +}; /// Sanity-check MoyoDataset fn assert_dataset( @@ -150,6 +152,7 @@ fn test_with_fcc() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 225); // Fm-3m + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Cubic)); assert_eq!(dataset.hall_number, 523); assert_eq!(dataset.num_operations(), 48 * 4); assert_eq!(dataset.orbits, vec![0, 0, 0, 0]); @@ -186,12 +189,86 @@ fn test_with_rutile() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 136); // P4_2/mnm + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Tetragonal)); assert_eq!(dataset.hall_number, 419); assert_eq!(dataset.num_operations(), 16); assert_eq!(dataset.orbits, vec![0, 0, 2, 2, 2, 2]); assert_eq!(dataset.wyckoffs, vec!['a', 'a', 'f', 'f', 'f', 'f']); } +#[test] +fn test_with_perovskite() { + // SrTiO3 structure, Pm-3m (No. 221) + let a = 3.905; + let lattice = Lattice::new(matrix![ + a, 0.0, 0.0; + 0.0, a, 0.0; + 0.0, 0.0, a; + ]); + let positions = vec![ + vector![0.0, 0.0, 0.0], // Sr at 1a (0,0,0) + vector![0.5, 0.5, 0.5], // Ti at 1b (1/2,1/2,1/2) + vector![0.5, 0.5, 0.0], // O at 3c (1/2,1/2,0) + vector![0.5, 0.0, 0.5], // O at 3c (1/2,0,1/2) + vector![0.0, 0.5, 0.5], // O at 3c (0,1/2,1/2) + ]; + let numbers = vec![0, 1, 2, 2, 2]; // Sr = 0, Ti = 1, O = 2 + let cell = Cell::new(lattice, positions, numbers); + + let symprec = 1e-4; + let angle_tolerance = AngleTolerance::Default; + let setting = Setting::Standard; + + let dataset = assert_dataset(&cell, symprec, angle_tolerance, setting); + assert_dataset(&dataset.std_cell, symprec, angle_tolerance, setting); + assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); + + assert_eq!(dataset.number, 221); // Pm-3m + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Cubic)); + assert_eq!(dataset.hall_number, 517); + assert_eq!(dataset.num_operations(), 48); + assert_eq!(dataset.orbits, vec![0, 1, 2, 2, 2]); + assert_eq!(dataset.wyckoffs, vec!['a', 'b', 'c', 'c', 'c']); +} + +#[test] +fn test_with_distorted_perovskite() { + // Simple orthorhombic structure with small distortions + let a = 4.0; + let b = a * 1.001; // 0.1% distortion + let c = a * 0.999; + let lattice = Lattice::new(matrix![ + a, 0.0, 0.0; + 0.0, b, 0.0; + 0.0, 0.0, c; + ]); + // Simple cubic-like positions with small displacements + let positions = vec![ + vector![0.0, 0.0, 0.0], // Origin + vector![0.5, 0.5, 0.5], // Body center + ]; + let numbers = vec![0, 0]; // Same atom type + let cell = Cell::new(lattice, positions, numbers); + + // Test with different tolerance levels + let settings = [ + // With tight tolerance, should find orthorhombic symmetry + (1e-4, 71), // Immm (orthorhombic) + // With loose tolerance, should find cubic symmetry + (1e-2, 229), // Im-3m (cubic) + ]; + + for (symprec, expected_number) in settings.iter() { + let dataset = + MoyoDataset::new(&cell, *symprec, AngleTolerance::Default, Setting::Standard).unwrap(); + assert_eq!( + dataset.number, *expected_number, + "With symprec={}, expected space group {} but got {}", + symprec, expected_number, dataset.number + ); + } +} + #[test] fn test_with_hcp() { // hcp, P6_3/mmc (No. 194) @@ -220,6 +297,7 @@ fn test_with_hcp() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 194); + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Hexagonal)); assert_eq!(dataset.hall_number, 488); assert_eq!(dataset.num_operations(), 24); assert_eq!(dataset.orbits, vec![0, 0]); @@ -263,6 +341,7 @@ fn test_with_wurtzite() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 186); + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Hexagonal)); assert_eq!(dataset.hall_number, 480); assert_eq!(dataset.num_operations(), 12); assert_eq!(dataset.orbits, vec![0, 0, 2, 2]); @@ -332,6 +411,7 @@ fn test_with_corundum() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 167); + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Trigonal)); assert_eq!(dataset.hall_number, 460); // Hexagonal setting assert_eq!(dataset.num_operations(), 36); assert_eq!( @@ -384,6 +464,7 @@ fn test_with_hexagonal_Sc() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 178); + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Hexagonal)); assert_eq!(dataset.hall_number, 472); assert_eq!(dataset.num_operations(), 12); assert_eq!(dataset.orbits, vec![0, 0, 0, 0, 0, 0]); @@ -412,6 +493,7 @@ fn test_with_trigonal_Sc() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 166); + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Trigonal)); assert_eq!(dataset.hall_number, 458); assert_eq!(dataset.num_operations(), 12); // Rhombohedral setting assert_eq!(dataset.orbits, vec![0]); @@ -437,6 +519,7 @@ fn test_with_clathrate_Si() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 205); + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Cubic)); assert_eq!(dataset.hall_number, 501); assert_eq!(dataset.num_operations(), 24); } @@ -456,6 +539,7 @@ fn test_with_mp_1197586() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 194); // P6_3/mmc + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Hexagonal)); assert_eq!(dataset.hall_number, 488); assert_eq!(dataset.num_operations(), 24); } @@ -475,6 +559,7 @@ fn test_with_mp_1185639() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 187); // P-6m2 + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Hexagonal)); assert_eq!(dataset.hall_number, 481); assert_eq!(dataset.num_operations(), 12); } @@ -494,6 +579,7 @@ fn test_with_mp_1221598() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 225); // Fm-3m + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Cubic)); } #[test] @@ -510,6 +596,7 @@ fn test_with_mp_569901() { assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); assert_eq!(dataset.number, 118); // P-4n2 + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Tetragonal)); } #[test] @@ -524,6 +611,9 @@ fn test_with_mp_30665() { let dataset = assert_dataset(&cell, symprec, angle_tolerance, setting); assert_dataset(&dataset.std_cell, symprec, angle_tolerance, setting); assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); + + assert_eq!(dataset.number, 116); // Pm-3m + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Tetragonal)); } #[test] @@ -555,6 +645,9 @@ fn test_with_mp_550745() { let dataset = assert_dataset(&cell, symprec, angle_tolerance, setting); assert_dataset(&dataset.std_cell, symprec, angle_tolerance, setting); assert_dataset(&dataset.prim_std_cell, symprec, angle_tolerance, setting); + + assert_eq!(dataset.number, 1); // P-4n2 + assert_eq!(dataset.crystal_system, Ok(CrystalSystem::Triclinic)); } #[test] diff --git a/moyopy/python/tests/test_moyo_dataset.py b/moyopy/python/tests/test_moyo_dataset.py index 3a3c9bc..68496b9 100644 --- a/moyopy/python/tests/test_moyo_dataset.py +++ b/moyopy/python/tests/test_moyo_dataset.py @@ -1,5 +1,7 @@ from __future__ import annotations +import numpy as np + import moyopy @@ -34,3 +36,45 @@ def test_moyo_dataset_repr(wurtzite: moyopy.Cell): # Test that repr() gives different output assert str(dataset) != repr(dataset) + + +def test_crystal_system(wurtzite: moyopy.Cell): + # Test wurtzite structure (space group 186, hexagonal) + # Use higher symprec since wurtzite structure is slightly distorted + dataset = moyopy.MoyoDataset(wurtzite, symprec=1e-2) + assert dataset.crystal_system == moyopy.CrystalSystem.Hexagonal + assert str(dataset.crystal_system) == "Hexagonal" + assert repr(dataset.crystal_system) == "CrystalSystem.Hexagonal" + + # Test FCC structure (space group 225, cubic) + positions = [ + [0.0, 0.0, 0.0], + [0.0, 0.5, 0.5], + [0.5, 0.0, 0.5], + [0.5, 0.5, 0.0], + ] + fcc = moyopy.Cell(basis=np.eye(3), positions=positions, numbers=[0, 0, 0, 0]) + dataset = moyopy.MoyoDataset(fcc) + crystal_system = dataset.crystal_system + assert crystal_system == moyopy.CrystalSystem.Cubic + + # Test that crystal_system appears in string representation + assert f"{crystal_system=!s}" in str(dataset) + + # Test all crystal systems are accessible as enum members + for crys_sys in ( + "Triclinic", + "Monoclinic", + "Orthorhombic", + "Tetragonal", + "Trigonal", + "Hexagonal", + "Cubic", + ): + assert str(getattr(moyopy.CrystalSystem, crys_sys)) == crys_sys + + assert moyopy.CrystalSystem.__module__ == "moyopy" + assert moyopy.CrystalSystem.__name__ == "CrystalSystem" + assert ( + repr(moyopy.CrystalSystem) == str(moyopy.CrystalSystem) == "" + ) diff --git a/moyopy/src/lib.rs b/moyopy/src/lib.rs index 5356c32..dd9c296 100644 --- a/moyopy/src/lib.rs +++ b/moyopy/src/lib.rs @@ -6,11 +6,54 @@ pub mod data; use moyo::base::AngleTolerance; use moyo::data::Setting; -use moyo::MoyoDataset; +use moyo::{CrystalSystem, MoyoDataset}; use crate::base::{PyMoyoError, PyOperations, PyStructure}; use crate::data::{operations_from_number, PyCentering, PyHallSymbolEntry, PySetting}; +/// The crystal system of a space group. +#[derive(Debug, PartialEq, Eq)] +#[pyclass(name = "CrystalSystem", frozen)] +pub enum PyCrystalSystem { + Triclinic, + Monoclinic, + Orthorhombic, + Tetragonal, + Trigonal, + Hexagonal, + Cubic, +} + +impl From for PyCrystalSystem { + fn from(cs: CrystalSystem) -> Self { + match cs { + CrystalSystem::Triclinic => Self::Triclinic, + CrystalSystem::Monoclinic => Self::Monoclinic, + CrystalSystem::Orthorhombic => Self::Orthorhombic, + CrystalSystem::Tetragonal => Self::Tetragonal, + CrystalSystem::Trigonal => Self::Trigonal, + CrystalSystem::Hexagonal => Self::Hexagonal, + CrystalSystem::Cubic => Self::Cubic, + } + } +} + +impl std::fmt::Display for PyCrystalSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pymethods] +impl PyCrystalSystem { + fn __str__(&self) -> String { + self.to_string() + } + + #[classattr] + const __module__: &'static str = "moyopy"; +} + #[derive(Debug)] #[pyclass(name = "MoyoDataset", frozen)] #[pyo3(module = "moyopy")] @@ -47,6 +90,16 @@ impl PyMoyoDataset { self.0.number } + /// The crystal system based on the space group number. + #[getter] + pub fn crystal_system(&self) -> PyResult { + self.0 + .crystal_system + .clone() + .map_err(|err| PyMoyoError::from(err).into()) + .map(Into::into) + } + #[getter] pub fn hall_number(&self) -> i32 { self.0.hall_number @@ -131,8 +184,9 @@ impl PyMoyoDataset { fn __str__(&self) -> String { format!( - "MoyoDataset(number={}, hall_number={}, operations=<{} operations>, orbits={:?}, wyckoffs={:?}, site_symmetry_symbols={:?})", + "MoyoDataset(number={}, crystal_system={}, hall_number={}, operations=<{} operations>, orbits={:?}, wyckoffs={:?}, site_symmetry_symbols={:?})", self.0.number, + self.0.crystal_system.map_or("Unknown".to_string(), |cs| format!("{:?}", cs)), self.0.hall_number, self.0.operations.len(), self.0.orbits, @@ -172,6 +226,7 @@ fn moyopy(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // data m.add_class::()?;