diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 0000000..1f32bfa --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,22 @@ +changelog: + categories: + - title: 🛠 Breaking Changes + labels: + - Semver-Major + - breaking-changes + - title: 🎉 New Features + labels: + - Semver-Minor + - enhancement + - title: ⚠️ Deprecation + labels: + - deprecation + - title: 🐞 Bug fixes + labels: + - bug + - title: 📔 Documentation + labels: + - documentation + - title: Other changes + labels: + - '*' diff --git a/.gitignore b/.gitignore index df739d2..f7e4bf1 100644 --- a/.gitignore +++ b/.gitignore @@ -359,6 +359,7 @@ lcov.info !**/assets/*.json .env *.png +*.cif docs/note/ diff --git a/moyopy/python/moyopy/_base.pyi b/moyopy/python/moyopy/_base.pyi new file mode 100644 index 0000000..cd70d93 --- /dev/null +++ b/moyopy/python/moyopy/_base.pyi @@ -0,0 +1,25 @@ +class Cell: + def __init__( + self, + basis: list[list[float]], + positions: list[list[float]], + numbers: list[int], + ): ... + @property + def basis(self) -> list[list[float]]: ... + @property + def positions(self) -> list[list[float]]: ... + @property + def numbers(self) -> list[int]: ... + def serialize_json(self) -> str: ... + @classmethod + def deserialize_json(cls, json_str: str) -> Cell: ... + +class Operations: + @property + def rotations(self) -> list[list[list[float]]]: ... + @property + def translations(self) -> list[list[float]]: ... + @property + def num_operations(self) -> int: ... + def __len__(self) -> int: ... diff --git a/moyopy/python/moyopy/_data.pyi b/moyopy/python/moyopy/_data.pyi new file mode 100644 index 0000000..e1e1809 --- /dev/null +++ b/moyopy/python/moyopy/_data.pyi @@ -0,0 +1,46 @@ +from moyopy._base import Operations + +class Setting: + """Preference for the setting of the space group.""" + @classmethod + def spglib(cls) -> Setting: + """The setting of the smallest Hall number.""" + @classmethod + def standard(cls) -> Setting: + """Unique axis b, cell choice 1 for monoclinic, hexagonal axes for rhombohedral, + and origin choice 2 for centrosymmetric space groups.""" + @classmethod + def hall_number(cls, hall_number: int) -> Setting: + """Specific Hall number from 1 to 530.""" + +class Centering: ... + +class HallSymbolEntry: + """An entry containing space-group information for a specified hall_number.""" + def __init__(self, hall_number: int): ... + @property + def hall_number(self) -> int: + """Number for Hall symbols (1 - 530).""" + @property + def number(self) -> int: + """ITA number for space group types (1 - 230).""" + @property + def arithmetic_number(self) -> int: + """Number for arithmetic crystal classes (1 - 73).""" + @property + def setting(self) -> Setting: + """Setting.""" + @property + def hall_symbol(self) -> str: + """Hall symbol.""" + @property + def hm_short(self) -> str: + """Hermann-Mauguin symbol in short notation.""" + @property + def hm_full(self) -> str: + """Hermann-Mauguin symbol in full notation.""" + @property + def centering(self) -> Centering: + """Centering.""" + +def operations_from_number(number: int, setting: Setting) -> Operations: ... diff --git a/moyopy/python/moyopy/_moyopy.pyi b/moyopy/python/moyopy/_moyopy.pyi index 6ed75d0..804e2e0 100644 --- a/moyopy/python/moyopy/_moyopy.pyi +++ b/moyopy/python/moyopy/_moyopy.pyi @@ -1,90 +1,8 @@ -from __future__ import annotations +from moyopy._base import Cell, Operations # noqa: F401 +from moyopy._data import Centering, HallSymbolEntry, Setting, operations_from_number # noqa: F401 __version__: str -############################################################################### -# base -############################################################################### - -class Cell: - def __init__( - self, - basis: list[list[float]], - positions: list[list[float]], - numbers: list[int], - ): ... - @property - def basis(self) -> list[list[float]]: ... - @property - def positions(self) -> list[list[float]]: ... - @property - def numbers(self) -> list[int]: ... - def serialize_json(self) -> str: ... - @classmethod - def deserialize_json(cls, json_str: str) -> Cell: ... - -class Operations: - @property - def rotations(self) -> list[list[list[float]]]: ... - @property - def translations(self) -> list[list[float]]: ... - @property - def num_operations(self) -> int: ... - def __len__(self) -> int: ... - -############################################################################### -# data -############################################################################### - -class Setting: - """Preference for the setting of the space group.""" - @classmethod - def spglib(cls) -> Setting: - """The setting of the smallest Hall number.""" - @classmethod - def standard(cls) -> Setting: - """Unique axis b, cell choice 1 for monoclinic, hexagonal axes for rhombohedral, - and origin choice 2 for centrosymmetric space groups.""" - @classmethod - def hall_number(cls, hall_number: int) -> Setting: - """Specific Hall number from 1 to 530.""" - -def operations_from_number(number: int, setting: Setting) -> Operations: ... - -class Centering: ... - -class HallSymbolEntry: - """An entry containing space-group information for a specified hall_number.""" - def __init__(self, hall_number: int): ... - @property - def hall_number(self) -> int: - """Number for Hall symbols (1 - 530).""" - @property - def number(self) -> int: - """ITA number for space group types (1 - 230).""" - @property - def arithmetic_number(self) -> int: - """Number for arithmetic crystal classes (1 - 73).""" - @property - def setting(self) -> Setting: - """Setting.""" - @property - def hall_symbol(self) -> str: - """Hall symbol.""" - @property - def hm_short(self) -> str: - """Hermann-Mauguin symbol in short notation.""" - @property - def hm_full(self) -> str: - """Hermann-Mauguin symbol in full notation.""" - @property - def centering(self) -> Centering: - """Centering.""" - -############################################################################### -# lib -############################################################################### - class MoyoDataset: """A dataset containing symmetry information of the input crystal structure.""" def __init__( @@ -173,3 +91,17 @@ class MoyoDataset: @property def angle_tolerance(self) -> float | None: """Actually used `angle_tolerance` in iterative symmetry search.""" + +__all__ = [ + # base + "Cell", + "Operations", + # data + "Setting", + "Centering", + "HallSymbolEntry", + "operations_from_number", + # lib + "__version__", + "MoyoDataset", +] diff --git a/moyopy/src/base.rs b/moyopy/src/base.rs index 326af50..1b9ee01 100644 --- a/moyopy/src/base.rs +++ b/moyopy/src/base.rs @@ -1,213 +1,7 @@ -use nalgebra::{OMatrix, RowVector3, Vector3}; -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; -use pyo3::types::PyType; -use serde::de::{Deserialize, Deserializer}; -use serde::ser::{Serialize, Serializer}; -use serde_json; +mod cell; +mod error; +mod operation; -use moyo::base::{Cell, Lattice, MoyoError, Operations}; - -// Unfortunately, "PyCell" is already reversed by pyo3... -#[derive(Debug, Clone)] -#[pyclass(name = "Cell", frozen)] -#[pyo3(module = "moyopy")] -pub struct PyStructure(Cell); - -#[pymethods] -impl PyStructure { - #[new] - /// basis: row-wise basis vectors - pub fn new( - basis: [[f64; 3]; 3], - positions: Vec<[f64; 3]>, - numbers: Vec, - ) -> PyResult { - if positions.len() != numbers.len() { - return Err(PyValueError::new_err( - "positions and numbers should be the same length", - )); - } - - // let lattice = Lattice::new(OMatrix::from(basis)); - let lattice = Lattice::new(OMatrix::from_rows(&[ - RowVector3::from(basis[0]), - RowVector3::from(basis[1]), - RowVector3::from(basis[2]), - ])); - let positions = positions - .iter() - .map(|x| Vector3::new(x[0], x[1], x[2])) - .collect::>(); - let cell = Cell::new(lattice, positions, numbers); - - Ok(Self(cell)) - } - - #[getter] - pub fn basis(&self) -> [[f64; 3]; 3] { - *self.0.lattice.basis.as_ref() - } - - #[getter] - pub fn positions(&self) -> Vec<[f64; 3]> { - self.0.positions.iter().map(|x| [x.x, x.y, x.z]).collect() - } - - #[getter] - pub fn numbers(&self) -> Vec { - self.0.numbers.clone() - } - - #[getter] - pub fn num_atoms(&self) -> usize { - self.0.num_atoms() - } - - pub fn serialize_json(&self) -> PyResult { - serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[classmethod] - pub fn deserialize_json(_cls: &Bound<'_, PyType>, s: &str) -> PyResult { - serde_json::from_str(s).map_err(|e| PyValueError::new_err(e.to_string())) - } - - fn __repr__(&self) -> String { - format!( - "Cell(basis={:?}, positions={:?}, numbers={:?})", - self.basis(), - self.positions(), - self.numbers() - ) - } - - fn __str__(&self) -> String { - self.__repr__() - } -} - -impl From for Cell { - fn from(structure: PyStructure) -> Self { - structure.0 - } -} - -impl From for PyStructure { - fn from(cell: Cell) -> Self { - PyStructure(cell) - } -} - -impl Serialize for PyStructure { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Cell::from(self.clone()).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for PyStructure { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Cell::deserialize(deserializer).map(PyStructure::from) - } -} - -#[derive(Debug)] -#[pyclass(name = "MoyoError", frozen)] -#[pyo3(module = "moyopy")] -pub struct PyMoyoError(MoyoError); - -impl From for PyErr { - fn from(error: PyMoyoError) -> Self { - PyValueError::new_err(error.0.to_string()) - } -} - -impl From for PyMoyoError { - fn from(error: MoyoError) -> Self { - PyMoyoError(error) - } -} - -#[derive(Debug)] -#[pyclass(name = "Operations", frozen)] -#[pyo3(module = "moyopy")] -pub struct PyOperations(Operations); - -#[pymethods] -impl PyOperations { - #[getter] - pub fn rotations(&self) -> Vec<[[i32; 3]; 3]> { - // Since nalgebra stores matrices in column-major order, we need to transpose them - self.0 - .iter() - .map(|x| *x.rotation.transpose().as_ref()) - .collect() - } - - #[getter] - pub fn translations(&self) -> Vec<[f64; 3]> { - self.0.iter().map(|x| *x.translation.as_ref()).collect() - } - - #[getter] - pub fn num_operations(&self) -> usize { - self.0.len() - } - - fn __len__(&self) -> usize { - self.num_operations() - } -} - -impl From for Operations { - fn from(operations: PyOperations) -> Self { - operations.0 - } -} - -impl From for PyOperations { - fn from(operations: Operations) -> Self { - PyOperations(operations) - } -} - -#[cfg(test)] -mod tests { - extern crate approx; - - use super::PyStructure; - use approx::assert_relative_eq; - use serde_json; - - #[test] - fn test_serialization() { - let structure = PyStructure::new( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], - vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], - vec![1, 2], - ) - .unwrap(); - - let serialized = serde_json::to_string(&structure).unwrap(); - let deserialized: PyStructure = serde_json::from_str(&serialized).unwrap(); - - for i in 0..3 { - for j in 0..3 { - assert_relative_eq!(structure.basis()[i][j], deserialized.basis()[i][j]); - } - } - assert_eq!(structure.positions().len(), deserialized.positions().len()); - for (actual, expect) in structure.positions().iter().zip(deserialized.positions()) { - for i in 0..3 { - assert_relative_eq!(actual[i], expect[i]); - } - } - assert_eq!(structure.numbers(), deserialized.numbers()); - } -} +pub use cell::PyStructure; +pub use error::PyMoyoError; +pub use operation::PyOperations; diff --git a/moyopy/src/base/cell.rs b/moyopy/src/base/cell.rs new file mode 100644 index 0000000..3403f3f --- /dev/null +++ b/moyopy/src/base/cell.rs @@ -0,0 +1,153 @@ +use nalgebra::{OMatrix, RowVector3, Vector3}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyType; +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use serde_json; + +use moyo::base::{Cell, Lattice}; + +// Unfortunately, "PyCell" is already reversed by pyo3... +#[derive(Debug, Clone)] +#[pyclass(name = "Cell", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyStructure(Cell); + +#[pymethods] +impl PyStructure { + #[new] + /// basis: row-wise basis vectors + pub fn new( + basis: [[f64; 3]; 3], + positions: Vec<[f64; 3]>, + numbers: Vec, + ) -> PyResult { + if positions.len() != numbers.len() { + return Err(PyValueError::new_err( + "positions and numbers should be the same length", + )); + } + + // let lattice = Lattice::new(OMatrix::from(basis)); + let lattice = Lattice::new(OMatrix::from_rows(&[ + RowVector3::from(basis[0]), + RowVector3::from(basis[1]), + RowVector3::from(basis[2]), + ])); + let positions = positions + .iter() + .map(|x| Vector3::new(x[0], x[1], x[2])) + .collect::>(); + let cell = Cell::new(lattice, positions, numbers); + + Ok(Self(cell)) + } + + #[getter] + pub fn basis(&self) -> [[f64; 3]; 3] { + *self.0.lattice.basis.as_ref() + } + + #[getter] + pub fn positions(&self) -> Vec<[f64; 3]> { + self.0.positions.iter().map(|x| [x.x, x.y, x.z]).collect() + } + + #[getter] + pub fn numbers(&self) -> Vec { + self.0.numbers.clone() + } + + #[getter] + pub fn num_atoms(&self) -> usize { + self.0.num_atoms() + } + + pub fn serialize_json(&self) -> PyResult { + serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string())) + } + + #[classmethod] + pub fn deserialize_json(_cls: &Bound<'_, PyType>, s: &str) -> PyResult { + serde_json::from_str(s).map_err(|e| PyValueError::new_err(e.to_string())) + } + + fn __repr__(&self) -> String { + format!( + "Cell(basis={:?}, positions={:?}, numbers={:?})", + self.basis(), + self.positions(), + self.numbers() + ) + } + + fn __str__(&self) -> String { + self.__repr__() + } +} + +impl From for Cell { + fn from(structure: PyStructure) -> Self { + structure.0 + } +} + +impl From for PyStructure { + fn from(cell: Cell) -> Self { + PyStructure(cell) + } +} + +impl Serialize for PyStructure { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Cell::from(self.clone()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PyStructure { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Cell::deserialize(deserializer).map(PyStructure::from) + } +} + +#[cfg(test)] +mod tests { + extern crate approx; + + use super::*; + use approx::assert_relative_eq; + use serde_json; + + #[test] + fn test_serialization() { + let structure = PyStructure::new( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], + vec![1, 2], + ) + .unwrap(); + + let serialized = serde_json::to_string(&structure).unwrap(); + let deserialized: PyStructure = serde_json::from_str(&serialized).unwrap(); + + for i in 0..3 { + for j in 0..3 { + assert_relative_eq!(structure.basis()[i][j], deserialized.basis()[i][j]); + } + } + assert_eq!(structure.positions().len(), deserialized.positions().len()); + for (actual, expect) in structure.positions().iter().zip(deserialized.positions()) { + for i in 0..3 { + assert_relative_eq!(actual[i], expect[i]); + } + } + assert_eq!(structure.numbers(), deserialized.numbers()); + } +} diff --git a/moyopy/src/base/error.rs b/moyopy/src/base/error.rs new file mode 100644 index 0000000..804ed8d --- /dev/null +++ b/moyopy/src/base/error.rs @@ -0,0 +1,21 @@ +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +use moyo::base::MoyoError; + +#[derive(Debug)] +#[pyclass(name = "MoyoError", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyMoyoError(MoyoError); + +impl From for PyErr { + fn from(error: PyMoyoError) -> Self { + PyValueError::new_err(error.0.to_string()) + } +} + +impl From for PyMoyoError { + fn from(error: MoyoError) -> Self { + PyMoyoError(error) + } +} diff --git a/moyopy/src/base/operation.rs b/moyopy/src/base/operation.rs new file mode 100644 index 0000000..736f232 --- /dev/null +++ b/moyopy/src/base/operation.rs @@ -0,0 +1,46 @@ +use pyo3::prelude::*; + +use moyo::base::Operations; + +#[derive(Debug)] +#[pyclass(name = "Operations", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyOperations(Operations); + +#[pymethods] +impl PyOperations { + #[getter] + pub fn rotations(&self) -> Vec<[[i32; 3]; 3]> { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0 + .iter() + .map(|x| *x.rotation.transpose().as_ref()) + .collect() + } + + #[getter] + pub fn translations(&self) -> Vec<[f64; 3]> { + self.0.iter().map(|x| *x.translation.as_ref()).collect() + } + + #[getter] + pub fn num_operations(&self) -> usize { + self.0.len() + } + + fn __len__(&self) -> usize { + self.num_operations() + } +} + +impl From for Operations { + fn from(operations: PyOperations) -> Self { + operations.0 + } +} + +impl From for PyOperations { + fn from(operations: Operations) -> Self { + PyOperations(operations) + } +} diff --git a/moyopy/src/data.rs b/moyopy/src/data.rs index b6663f8..0deccd1 100644 --- a/moyopy/src/data.rs +++ b/moyopy/src/data.rs @@ -1,7 +1,7 @@ mod hall_symbol; mod setting; -pub use hall_symbol::PyHallSymbolEntry; +pub use hall_symbol::{PyCentering, PyHallSymbolEntry}; pub use setting::PySetting; use pyo3::prelude::*; diff --git a/moyopy/src/lib.rs b/moyopy/src/lib.rs index 134cd34..f053a9d 100644 --- a/moyopy/src/lib.rs +++ b/moyopy/src/lib.rs @@ -9,7 +9,7 @@ use moyo::data::Setting; use moyo::MoyoDataset; use crate::base::{PyMoyoError, PyOperations, PyStructure}; -use crate::data::{operations_from_number, PyHallSymbolEntry, PySetting}; +use crate::data::{operations_from_number, PyCentering, PyHallSymbolEntry, PySetting}; #[derive(Debug)] #[pyclass(name = "MoyoDataset", frozen)] @@ -164,6 +164,7 @@ fn moyopy(m: &Bound<'_, PyModule>) -> PyResult<()> { // data m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_wrapped(wrap_pyfunction!(operations_from_number))?; Ok(())