Skip to content

Commit

Permalink
- add CrystalSystem enum to represent crystal system based on space g…
Browse files Browse the repository at this point in the history
…roup 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
  • Loading branch information
janosh committed Feb 1, 2025
1 parent b654463 commit fcf81f5
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 5 deletions.
2 changes: 2 additions & 0 deletions moyo/src/base/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
42 changes: 42 additions & 0 deletions moyo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<CrystalSystem, MoyoError>,
// ------------------------------------------------------------------------
// Symmetry operations in the input cell
// ------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down
99 changes: 96 additions & 3 deletions moyo/tests/test_moyo_dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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]);
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
44 changes: 44 additions & 0 deletions moyopy/python/tests/test_moyo_dataset.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import numpy as np

import moyopy


Expand Down Expand Up @@ -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) == "<class 'moyopy.CrystalSystem'>"
)
Loading

0 comments on commit fcf81f5

Please sign in to comment.