From 57147275b553d96c130c8b9dacf9eb701f04f3ce Mon Sep 17 00:00:00 2001 From: lan496 Date: Mon, 10 Feb 2025 15:22:58 +0900 Subject: [PATCH 1/5] magnetic dataset with pyo3 --- justfile | 2 +- moyo/src/base/lattice.rs | 10 +- moyo/src/base/magnetic_cell.rs | 4 +- moyo/src/lib.rs | 2 +- moyopy/python/moyopy/_base.pyi | 57 ++++ moyopy/python/moyopy/_dataset.pyi | 270 ++++++++++++++++++ moyopy/python/moyopy/_moyopy.pyi | 111 ++------ moyopy/python/tests/conftest.py | 30 +- moyopy/python/tests/test_moyo_dataset.py | 19 +- moyopy/src/base.rs | 4 +- moyopy/src/base/cell.rs | 9 +- moyopy/src/base/magnetic_cell.rs | 234 ++++++++++++++++ moyopy/src/base/operation.rs | 53 +++- moyopy/src/dataset.rs | 5 + moyopy/src/dataset/magnetic_space_group.rs | 310 +++++++++++++++++++++ moyopy/src/dataset/space_group.rs | 165 +++++++++++ moyopy/src/lib.rs | 157 +---------- 17 files changed, 1182 insertions(+), 260 deletions(-) create mode 100644 moyopy/python/moyopy/_dataset.pyi create mode 100644 moyopy/src/base/magnetic_cell.rs create mode 100644 moyopy/src/dataset.rs create mode 100644 moyopy/src/dataset/magnetic_space_group.rs create mode 100644 moyopy/src/dataset/space_group.rs diff --git a/justfile b/justfile index 872b90e..60eb491 100644 --- a/justfile +++ b/justfile @@ -10,7 +10,7 @@ build-python: install-python: python -m pip install uv maturin maturin develop --release --manifest-path moyopy/Cargo.toml - python -m uv pip install -e "moyopy[dev]" + python -m pip install -e "moyopy[dev]" pre-commit install test-python: diff --git a/moyo/src/base/lattice.rs b/moyo/src/base/lattice.rs index 7c1c5ac..1eecd94 100644 --- a/moyo/src/base/lattice.rs +++ b/moyo/src/base/lattice.rs @@ -1,4 +1,4 @@ -use nalgebra::base::{Matrix3, Vector3}; +use nalgebra::base::{Matrix3, OMatrix, RowVector3, Vector3}; use serde::{Deserialize, Serialize}; use crate::math::{ @@ -22,6 +22,14 @@ impl Lattice { } } + pub fn from_basis(basis: [[f64; 3]; 3]) -> Self { + Self::new(OMatrix::from_rows(&[ + RowVector3::from(basis[0]), + RowVector3::from(basis[1]), + RowVector3::from(basis[2]), + ])) + } + /// Return Minkowski reduced lattice and transformation matrix to it pub fn minkowski_reduce(&self) -> Result<(Self, Matrix3), MoyoError> { let (reduced_basis, trans_mat) = minkowski_reduce(&self.basis); diff --git a/moyo/src/base/magnetic_cell.rs b/moyo/src/base/magnetic_cell.rs index 3332a43..f622bde 100644 --- a/moyo/src/base/magnetic_cell.rs +++ b/moyo/src/base/magnetic_cell.rs @@ -30,7 +30,7 @@ pub trait MagneticMoment: Sized + Clone { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Collinear(pub f64); impl MagneticMoment for Collinear { @@ -66,7 +66,7 @@ impl MagneticMoment for Collinear { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NonCollinear(pub Vector3); impl MagneticMoment for NonCollinear { diff --git a/moyo/src/lib.rs b/moyo/src/lib.rs index f6e496c..5bb04e3 100644 --- a/moyo/src/lib.rs +++ b/moyo/src/lib.rs @@ -90,7 +90,7 @@ use nalgebra::Matrix3; /// A dataset containing symmetry information of the input crystal structure. pub struct MoyoDataset { // ------------------------------------------------------------------------ - // Space-group type + // Identification // ------------------------------------------------------------------------ /// Space group number. pub number: Number, diff --git a/moyopy/python/moyopy/_base.pyi b/moyopy/python/moyopy/_base.pyi index cd70d93..858adc7 100644 --- a/moyopy/python/moyopy/_base.pyi +++ b/moyopy/python/moyopy/_base.pyi @@ -11,6 +11,52 @@ class Cell: def positions(self) -> list[list[float]]: ... @property def numbers(self) -> list[int]: ... + @property + def num_atoms(self) -> int: ... + def serialize_json(self) -> str: ... + @classmethod + def deserialize_json(cls, json_str: str) -> Cell: ... + +class CollinearMagneticCell: + def __init__( + self, + basis: list[list[float]], + positions: list[list[float]], + numbers: list[int], + magnetic_moments: list[float], + ): ... + @property + def basis(self) -> list[list[float]]: ... + @property + def positions(self) -> list[list[float]]: ... + @property + def numbers(self) -> list[int]: ... + @property + def magnetic_moments(self) -> list[float]: ... + @property + def num_atoms(self) -> int: ... + def serialize_json(self) -> str: ... + @classmethod + def deserialize_json(cls, json_str: str) -> Cell: ... + +class NonCollinearMagneticCell: + def __init__( + self, + basis: list[list[float]], + positions: list[list[float]], + numbers: list[int], + magnetic_moments: list[list[float]], + ): ... + @property + def basis(self) -> list[list[float]]: ... + @property + def positions(self) -> list[list[float]]: ... + @property + def numbers(self) -> list[int]: ... + @property + def magnetic_moments(self) -> list[list[float]]: ... + @property + def num_atoms(self) -> int: ... def serialize_json(self) -> str: ... @classmethod def deserialize_json(cls, json_str: str) -> Cell: ... @@ -23,3 +69,14 @@ class Operations: @property def num_operations(self) -> int: ... def __len__(self) -> int: ... + +class MagneticOperations: + @property + def rotations(self) -> list[list[list[float]]]: ... + @property + def translations(self) -> list[list[float]]: ... + @property + def time_reversals(self) -> list[bool]: ... + @property + def num_operations(self) -> int: ... + def __len__(self) -> int: ... diff --git a/moyopy/python/moyopy/_dataset.pyi b/moyopy/python/moyopy/_dataset.pyi new file mode 100644 index 0000000..06f9aec --- /dev/null +++ b/moyopy/python/moyopy/_dataset.pyi @@ -0,0 +1,270 @@ +from moyopy._base import ( + Cell, + CollinearMagneticCell, + MagneticOperations, + NonCollinearMagneticCell, + Operations, +) +from moyopy._data import ( + Setting, +) + +class MoyoDataset: + """A dataset containing symmetry information of the input crystal structure.""" + def __init__( + self, + cell: Cell, + symprec: float = 1e-4, + angle_tolerance: float | None = None, + setting: Setting | None = None, + ): + """ + Parameters + ---------- + cell: Cell + Input crystal structure. + symprec: float + Symmetry search tolerance in the unit of cell.basis. + angle_tolerance: float | None + Symmetry search tolerance in the unit of radians. + setting: Setting | None + Preference for the setting of the space group. + """ + # Space-group type + @property + def number(self) -> int: + """Space group number.""" + @property + def hall_number(self) -> int: + """Hall symbol number.""" + # Symmetry operations in the input cell + @property + def operations(self) -> Operations: + """Symmetry operations in the input cell.""" + # Site symmetry + @property + def orbits(self) -> list[int]: + """Spglib's `crystallographic_orbits` not `equivalent_atoms`. + + The `i`th atom in the input cell is equivalent to the `orbits[i]`th atom in the **input** + cell. For example, orbits=[0, 0, 2, 2, 2, 2] means the first two atoms are equivalent + and the last four atoms are equivalent to each other. + """ + @property + def wyckoffs(self) -> list[str]: + """Wyckoff letters for each site in the input cell.""" + @property + def site_symmetry_symbols(self) -> list[str]: + """Site symmetry symbols for each site in the input cell. + + The orientation of the site symmetry is w.r.t. the standardized cell. + """ + # Standardized cell + @property + def std_cell(self) -> Cell: + """Standardized cell.""" + @property + def std_linear(self) -> list[list[float]]: + """Linear part of transformation from the input cell to the standardized cell.""" + @property + def std_origin_shift(self) -> list[float]: + """Origin shift of transformation from the input cell to the standardized cell.""" + @property + def std_rotation_matrix(self) -> list[list[float]]: + """Rigid rotation.""" + @property + def pearson_symbol(self) -> str: + """Pearson symbol for standardized cell.""" + # Primitive standardized cell + @property + def prim_std_cell(self) -> Cell: + """Primitive standardized cell.""" + @property + def prim_std_linear(self) -> list[list[float]]: + """Linear part of transformation from the input cell to the primitive standardized cell.""" + @property + def prim_std_origin_shift(self) -> list[float]: + """Origin shift of transformation from the input cell to the primitive standardized + cell.""" + @property + def mapping_std_prim(self) -> list[int]: + """Mapping sites in the input cell to those in the primitive standardized cell. + + The `i`th atom in the input cell is mapped to the `mapping_to_std_prim[i]`th atom in the + primitive standardized cell. + """ + # Final parameters + @property + def symprec(self) -> float: + """Actually used `symprec` in iterative symmetry search.""" + @property + def angle_tolerance(self) -> float | None: + """Actually used `angle_tolerance` in iterative symmetry search.""" + +class MoyoCollinearMagneticDataset: + """A dataset containing magnetic symmetry information of the input collinear magnetic + structure.""" + def __init__( + self, + magnetic_cell: CollinearMagneticCell, + symprec: float = 1e-4, + angle_tolerance: float | None = None, + mag_symprec: float | None = None, + is_axial: bool = False, + ): + """ + Parameters + ---------- + magnetic_cell: CollinearMagneticCell + Input collinear magnetic structure. + symprec: float + Symmetry search tolerance in the unit of magnetic_cell.basis. + angle_tolerance: float | None + Symmetry search tolerance in the unit of radians. + mag_symprec: float | None + Symmetry search tolerance in the unit of magnetic moments. + is_axial: bool + Whether the magnetic moments are axial on improper operations. + """ + # Magnetic space-group type + @property + def uni_number(self) -> int: + """UNI number for magnetic space-group type.""" + # Magnetic symmetry operations in the input cell + @property + def magnetic_operations(self) -> MagneticOperations: + """Magnetic symmetry operations in the input cell.""" + # Site symmetry + @property + def orbits(self) -> list[int]: + """The `i`th atom in the input magnetic cell is equivalent to the `orbits[i]`th atom + in the **input** magnetic cell. For example, orbits=[0, 0, 2, 2, 2, 2] means + the first two atoms are equivalent and the last four atoms are equivalent to each other. + """ + # Standardized magnetic cell + @property + def std_mag_cell(self) -> CollinearMagneticCell: + """Standardized magnetic cell.""" + @property + def std_linear(self) -> list[list[float]]: + """Linear part of transformation from the input magnetic cell to the standardized + magnetic cell.""" + @property + def std_origin_shift(self) -> list[float]: + """Origin shift of transformation from the input magnetic cell to the standardized + magnetic cell.""" + @property + def std_rotation_matrix(self) -> list[list[float]]: + """Rigid rotation.""" + # Primitive standardized magnetic cell + @property + def prim_std_mag_cell(self) -> CollinearMagneticCell: + """Primitive standardized magnetic cell.""" + @property + def prim_std_linear(self) -> list[list[float]]: + """Linear part of transformation from the input magnetic cell to the primitive + standardized magnetic cell.""" + @property + def prim_std_origin_shift(self) -> list[float]: + """Origin shift of transformation from the input magnetic cell to the primitive + standardized magnetic cell.""" + @property + def mapping_std_prim(self) -> list[int]: + """Mapping sites in the input magnetic cell to those in the primitive standardized magnetic + cell. The `i`th atom in the input magnetic cell is mapped to the `mapping_to_std_prim[i]`th + atom in the primitive standardized magnetic cell. + """ + # Final parameters + @property + def symprec(self) -> float: + """Actually used `symprec` in iterative symmetry search.""" + @property + def angle_tolerance(self) -> float | None: + """Actually used `angle_tolerance` in iterative symmetry search.""" + @property + def mag_symprec(self) -> float | None: + """Actually used `mag_symprec` in iterative symmetry search.""" + +class MoyoNonCollinearMagneticDataset: + """A dataset containing magnetic symmetry information of the input non-collinear magnetic + structure.""" + def __init__( + self, + magnetic_cell: NonCollinearMagneticCell, + symprec: float = 1e-4, + angle_tolerance: float | None = None, + mag_symprec: float | None = None, + is_axial: bool = True, + ): + """ + Parameters + ---------- + magnetic_cell: NonCollinearMagneticCell + Input non-collinear magnetic structure. + symprec: float + Symmetry search tolerance in the unit of magnetic_cell.basis. + angle_tolerance: float | None + Symmetry search tolerance in the unit of radians. + mag_symprec: float | None + Symmetry search tolerance in the unit of magnetic moments. + is_axial: bool + Whether the magnetic moments are axial on improper operations. + """ + # Magnetic space-group type + @property + def uni_number(self) -> int: + """UNI number for magnetic space-group type.""" + # Magnetic symmetry operations in the input cell + @property + def magnetic_operations(self) -> MagneticOperations: + """Magnetic symmetry operations in the input cell.""" + # Site symmetry + @property + def orbits(self) -> list[int]: + """The `i`th atom in the input magnetic cell is equivalent to the `orbits[i]`th atom + in the **input** magnetic cell. For example, orbits=[0, 0, 2, 2, 2, 2] means + the first two atoms are equivalent and the last four atoms are equivalent to each other. + """ + # Standardized magnetic cell + @property + def std_mag_cell(self) -> NonCollinearMagneticCell: + """Standardized magnetic cell.""" + @property + def std_linear(self) -> list[list[float]]: + """Linear part of transformation from the input magnetic cell to the standardized + magnetic cell.""" + @property + def std_origin_shift(self) -> list[float]: + """Origin shift of transformation from the input magnetic cell to the standardized + magnetic cell.""" + @property + def std_rotation_matrix(self) -> list[list[float]]: + """Rigid rotation.""" + # Primitive standardized magnetic cell + @property + def prim_std_mag_cell(self) -> NonCollinearMagneticCell: + """Primitive standardized magnetic cell.""" + @property + def prim_std_linear(self) -> list[list[float]]: + """Linear part of transformation from the input magnetic cell to the primitive + standardized magnetic cell.""" + @property + def prim_std_origin_shift(self) -> list[float]: + """Origin shift of transformation from the input magnetic cell to the primitive + standardized magnetic cell.""" + @property + def mapping_std_prim(self) -> list[int]: + """Mapping sites in the input magnetic cell to those in the primitive standardized magnetic + cell. The `i`th atom in the input magnetic cell is mapped to the `mapping_to_std_prim[i]`th + atom in the primitive standardized magnetic cell. + """ + # Final parameters + @property + def symprec(self) -> float: + """Actually used `symprec` in iterative symmetry search.""" + @property + def angle_tolerance(self) -> float | None: + """Actually used `angle_tolerance` in iterative symmetry search.""" + @property + def mag_symprec(self) -> float | None: + """Actually used `mag_symprec` in iterative symmetry search.""" diff --git a/moyopy/python/moyopy/_moyopy.pyi b/moyopy/python/moyopy/_moyopy.pyi index 0974376..b2c87da 100644 --- a/moyopy/python/moyopy/_moyopy.pyi +++ b/moyopy/python/moyopy/_moyopy.pyi @@ -1,4 +1,9 @@ -from moyopy._base import Cell, Operations # noqa: F401 +from moyopy._base import ( # noqa: F401 + Cell, + CollinearMagneticCell, + NonCollinearMagneticCell, + Operations, +) from moyopy._data import ( Centering, HallSymbolEntry, @@ -6,104 +11,19 @@ from moyopy._data import ( SpaceGroupType, operations_from_number, ) # noqa: F401 +from moyopy._dataset import ( + MoyoCollinearMagneticDataset, + MoyoDataset, + MoyoNonCollinearMagneticDataset, +) # noqa: F401 __version__: str -class MoyoDataset: - """A dataset containing symmetry information of the input crystal structure.""" - def __init__( - self, - cell: Cell, - symprec: float = 1e-4, - angle_tolerance: float | None = None, - setting: Setting | None = None, - ): - """ - Parameters - ---------- - cell: Cell - Input crystal structure. - symprec: float - Symmetry search tolerance in the unit of cell.lattice. - angle_tolerance: float | None - Symmetry search tolerance in the unit of radians. - setting: Setting | None - Preference for the setting of the space group. - """ - # Space-group type - @property - def number(self) -> int: - """Space group number.""" - @property - def hall_number(self) -> int: - """Hall symbol number.""" - # Symmetry operations in the input cell - @property - def operations(self) -> Operations: - """Symmetry operations in the input cell.""" - # Site symmetry - @property - def orbits(self) -> list[int]: - """Spglib's `crystallographic_orbits` not `equivalent_atoms`. - - The `i`th atom in the input cell is equivalent to the `orbits[i]`th atom in the **input** - cell. For example, orbits=[0, 0, 2, 2, 2, 2] means the first two atoms are equivalent - and the last four atoms are equivalent to each other. - """ - @property - def wyckoffs(self) -> list[str]: - """Wyckoff letters for each site in the input cell.""" - @property - def site_symmetry_symbols(self) -> list[str]: - """Site symmetry symbols for each site in the input cell. - - The orientation of the site symmetry is w.r.t. the standardized cell. - """ - # Standardized cell - @property - def std_cell(self) -> Cell: - """Standardized cell.""" - @property - def std_linear(self) -> list[list[float]]: - """Linear part of transformation from the input cell to the standardized cell.""" - @property - def std_origin_shift(self) -> list[float]: - """Origin shift of transformation from the input cell to the standardized cell.""" - @property - def std_rotation_matrix(self) -> list[list[float]]: - """Rigid rotation.""" - @property - def pearson_symbol(self) -> str: - """Pearson symbol for standardized cell.""" - # Primitive standardized cell - @property - def prim_std_cell(self) -> Cell: - """Primitive standardized cell.""" - @property - def prim_std_linear(self) -> list[list[float]]: - """Linear part of transformation from the input cell to the primitive standardized cell.""" - @property - def prim_std_origin_shift(self) -> list[float]: - """Origin shift of transformation from the input cell to the primitive standardized - cell.""" - @property - def mapping_std_prim(self) -> list[int]: - """Mapping sites in the input cell to those in the primitive standardized cell. - - The `i`th atom in the input cell is mapped to the `mapping_to_std_prim[i]`th atom in the - primitive standardized cell. - """ - # Final parameters - @property - def symprec(self) -> float: - """Actually used `symprec` in iterative symmetry search.""" - @property - def angle_tolerance(self) -> float | None: - """Actually used `angle_tolerance` in iterative symmetry search.""" - __all__ = [ # base "Cell", + "CollinearMagneticCell", + "NonCollinearMagneticCell", "Operations", # data "Setting", @@ -111,7 +31,10 @@ __all__ = [ "HallSymbolEntry", "SpaceGroupType", "operations_from_number", + # dataset + "MoyoDataset", + "MoyoCollinearMagneticDataset", + "MoyoNonCollinearMagneticDataset", # lib "__version__", - "MoyoDataset", ] diff --git a/moyopy/python/tests/conftest.py b/moyopy/python/tests/conftest.py index 5a0f8bd..ab08b64 100644 --- a/moyopy/python/tests/conftest.py +++ b/moyopy/python/tests/conftest.py @@ -2,11 +2,11 @@ import pytest -import moyopy +from moyopy import Cell, CollinearMagneticCell @pytest.fixture -def wurtzite() -> moyopy.Cell: +def wurtzite() -> Cell: # https://next-gen.materialsproject.org/materials/mp-560588 a = 3.81 c = 6.24 @@ -27,5 +27,29 @@ def wurtzite() -> moyopy.Cell: ] numbers = [1, 1, 2, 2] - cell = moyopy.Cell(basis, positions, numbers) + cell = Cell(basis, positions, numbers) return cell + + +@pytest.fixture +def rutile_type3() -> CollinearMagneticCell: + basis = [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + positions = [ + # Ti (2a) + [0.0, 0.0, 0.0], + [0.5, 0.5, 0.5], + # O (4f) + [0.3, 0.3, 0.0], + [0.7, 0.7, 0.0], + [0.2, 0.8, 0.5], + [0.8, 0.2, 0.5], + ] + numbers = [0, 0, 1, 1, 1, 1] + magnetic_moments = [0.7, -0.7, 0.0, 0.0, 0.0, 0.0] + + magnetic_cell = CollinearMagneticCell(basis, positions, numbers, magnetic_moments) + return magnetic_cell diff --git a/moyopy/python/tests/test_moyo_dataset.py b/moyopy/python/tests/test_moyo_dataset.py index 1c7c197..e669b7f 100644 --- a/moyopy/python/tests/test_moyo_dataset.py +++ b/moyopy/python/tests/test_moyo_dataset.py @@ -1,23 +1,23 @@ from __future__ import annotations -import moyopy +from moyopy import Cell, CollinearMagneticCell, MoyoCollinearMagneticDataset, MoyoDataset -def test_moyo_dataset(wurtzite: moyopy.Cell): - dataset = moyopy.MoyoDataset(wurtzite) +def test_moyo_dataset(wurtzite: Cell): + dataset = MoyoDataset(wurtzite) assert dataset.number == 186 assert dataset.hall_number == 480 assert dataset.pearson_symbol == "hP4" -def test_serialization(wurtzite: moyopy.Cell): +def test_serialization(wurtzite: Cell): serialized = wurtzite.serialize_json() - deserialized = moyopy.Cell.deserialize_json(serialized) + deserialized = Cell.deserialize_json(serialized) assert len(wurtzite.positions) == len(deserialized.positions) -def test_moyo_dataset_repr(wurtzite: moyopy.Cell): - dataset = moyopy.MoyoDataset(wurtzite) +def test_moyo_dataset_repr(wurtzite: Cell): + dataset = MoyoDataset(wurtzite) dataset_str = str(dataset) # Test that string representation of MoyoDataset contains key information @@ -35,3 +35,8 @@ def test_moyo_dataset_repr(wurtzite: moyopy.Cell): # Test that repr() gives different output assert str(dataset) != repr(dataset) + + +def test_moyo_collinear_magnetic_dataset(rutile_type3: CollinearMagneticCell): + dataset = MoyoCollinearMagneticDataset(rutile_type3) + assert dataset.uni_number == 1158 diff --git a/moyopy/src/base.rs b/moyopy/src/base.rs index 1b9ee01..2bbddf2 100644 --- a/moyopy/src/base.rs +++ b/moyopy/src/base.rs @@ -1,7 +1,9 @@ mod cell; mod error; +mod magnetic_cell; mod operation; pub use cell::PyStructure; pub use error::PyMoyoError; -pub use operation::PyOperations; +pub use magnetic_cell::{PyCollinearMagneticCell, PyNonCollinearMagneticCell}; +pub use operation::{PyMagneticOperations, PyOperations}; diff --git a/moyopy/src/base/cell.rs b/moyopy/src/base/cell.rs index 3403f3f..affcdfa 100644 --- a/moyopy/src/base/cell.rs +++ b/moyopy/src/base/cell.rs @@ -1,4 +1,4 @@ -use nalgebra::{OMatrix, RowVector3, Vector3}; +use nalgebra::Vector3; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyType; @@ -29,12 +29,7 @@ impl PyStructure { )); } - // 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 lattice = Lattice::from_basis(basis); let positions = positions .iter() .map(|x| Vector3::new(x[0], x[1], x[2])) diff --git a/moyopy/src/base/magnetic_cell.rs b/moyopy/src/base/magnetic_cell.rs new file mode 100644 index 0000000..98d34fd --- /dev/null +++ b/moyopy/src/base/magnetic_cell.rs @@ -0,0 +1,234 @@ +use nalgebra::Vector3; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyType; +use serde::{Deserialize, Serialize}; +use serde_json; + +use moyo::base::{Collinear, Lattice, MagneticCell, NonCollinear}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +enum MagneticCellEnum { + CollinearMagneticCell(MagneticCell), + NonCollinearMagneticCell(MagneticCell), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[pyclass(name = "MagneticCell", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyMagneticCell { + magnetic_cell: MagneticCellEnum, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[pyclass(name = "CollinearMagneticCell", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyCollinearMagneticCell(MagneticCell); + +#[pymethods] +impl PyCollinearMagneticCell { + #[new] + /// basis: row-wise basis vectors + pub fn new( + basis: [[f64; 3]; 3], + positions: Vec<[f64; 3]>, + numbers: Vec, + magnetic_moments: Vec, + ) -> PyResult { + if numbers.len() != positions.len() { + return Err(PyValueError::new_err( + "positions and numbers should be the same length", + )); + } + if magnetic_moments.len() != positions.len() { + return Err(PyValueError::new_err( + "positions and magnetic_moments should be the same length", + )); + } + + let lattice = Lattice::from_basis(basis); + let positions = positions + .iter() + .map(|x| Vector3::new(x[0], x[1], x[2])) + .collect::>(); + let magnetic_moments = magnetic_moments.iter().map(|m| Collinear(*m)).collect(); + let magnetic_cell = MagneticCell::new(lattice, positions, numbers, magnetic_moments); + + Ok(Self(magnetic_cell)) + } + + #[getter] + pub fn basis(&self) -> [[f64; 3]; 3] { + *self.0.cell.lattice.basis.as_ref() + } + + #[getter] + pub fn positions(&self) -> Vec<[f64; 3]> { + self.0 + .cell + .positions + .iter() + .map(|x| [x.x, x.y, x.z]) + .collect() + } + + #[getter] + pub fn numbers(&self) -> Vec { + self.0.cell.numbers.clone() + } + + #[getter] + pub fn magnetic_moments(&self) -> Vec { + self.0.magnetic_moments.iter().map(|m| m.0).collect() + } + + #[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!( + "CollinearMagneticCell(basis={:?}, positions={:?}, numbers={:?}, magnetic_moments={:?})", + self.basis(), + self.positions(), + self.numbers(), + self.magnetic_moments(), + ) + } + + fn __str__(&self) -> String { + self.__repr__() + } +} + +impl From for MagneticCell { + fn from(cell: PyCollinearMagneticCell) -> Self { + cell.0 + } +} + +impl From> for PyCollinearMagneticCell { + fn from(cell: MagneticCell) -> Self { + PyCollinearMagneticCell(cell) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[pyclass(name = "NonCollinearMagneticCell", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyNonCollinearMagneticCell(MagneticCell); + +#[pymethods] +impl PyNonCollinearMagneticCell { + #[new] + /// basis: row-wise basis vectors + pub fn new( + basis: [[f64; 3]; 3], + positions: Vec<[f64; 3]>, + numbers: Vec, + magnetic_moments: Vec<[f64; 3]>, + ) -> PyResult { + if numbers.len() != positions.len() { + return Err(PyValueError::new_err( + "positions and numbers should be the same length", + )); + } + if magnetic_moments.len() != positions.len() { + return Err(PyValueError::new_err( + "positions and magnetic_moments should be the same length", + )); + } + + let lattice = Lattice::from_basis(basis); + let positions = positions + .iter() + .map(|x| Vector3::new(x[0], x[1], x[2])) + .collect::>(); + let magnetic_moments = magnetic_moments + .iter() + .map(|m| NonCollinear(Vector3::new(m[0], m[1], m[2]))) + .collect(); + let magnetic_cell = MagneticCell::new(lattice, positions, numbers, magnetic_moments); + + Ok(Self(magnetic_cell)) + } + + #[getter] + pub fn basis(&self) -> [[f64; 3]; 3] { + *self.0.cell.lattice.basis.as_ref() + } + + #[getter] + pub fn positions(&self) -> Vec<[f64; 3]> { + self.0 + .cell + .positions + .iter() + .map(|x| [x.x, x.y, x.z]) + .collect() + } + + #[getter] + pub fn numbers(&self) -> Vec { + self.0.cell.numbers.clone() + } + + #[getter] + pub fn magnetic_moments(&self) -> Vec<[f64; 3]> { + self.0 + .magnetic_moments + .iter() + .map(|m| [m.0[0], m.0[1], m.0[2]]) + .collect() + } + + #[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!( + "NonCollinearMagneticCell(basis={:?}, positions={:?}, numbers={:?}, magnetic_moments={:?})", + self.basis(), + self.positions(), + self.numbers(), + self.magnetic_moments(), + ) + } + + fn __str__(&self) -> String { + self.__repr__() + } +} + +impl From for MagneticCell { + fn from(cell: PyNonCollinearMagneticCell) -> Self { + cell.0 + } +} + +impl From> for PyNonCollinearMagneticCell { + fn from(cell: MagneticCell) -> Self { + PyNonCollinearMagneticCell(cell) + } +} diff --git a/moyopy/src/base/operation.rs b/moyopy/src/base/operation.rs index 736f232..77ca789 100644 --- a/moyopy/src/base/operation.rs +++ b/moyopy/src/base/operation.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; -use moyo::base::Operations; +use moyo::base::{MagneticOperations, Operations}; #[derive(Debug)] #[pyclass(name = "Operations", frozen)] @@ -44,3 +44,54 @@ impl From for PyOperations { PyOperations(operations) } } + +#[derive(Debug)] +#[pyclass(name = "MagneticOperations", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyMagneticOperations(MagneticOperations); + +#[pymethods] +impl PyMagneticOperations { + #[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.operation.rotation.transpose().as_ref()) + .collect() + } + + #[getter] + pub fn translations(&self) -> Vec<[f64; 3]> { + self.0 + .iter() + .map(|x| *x.operation.translation.as_ref()) + .collect() + } + + #[getter] + pub fn time_reversals(&self) -> Vec { + self.0.iter().map(|x| x.time_reversal).collect() + } + + #[getter] + pub fn num_operations(&self) -> usize { + self.0.len() + } + + fn __len__(&self) -> usize { + self.num_operations() + } +} + +impl From for MagneticOperations { + fn from(operations: PyMagneticOperations) -> Self { + operations.0 + } +} + +impl From for PyMagneticOperations { + fn from(operations: MagneticOperations) -> Self { + PyMagneticOperations(operations) + } +} diff --git a/moyopy/src/dataset.rs b/moyopy/src/dataset.rs new file mode 100644 index 0000000..bd3181f --- /dev/null +++ b/moyopy/src/dataset.rs @@ -0,0 +1,5 @@ +mod magnetic_space_group; +mod space_group; + +pub use magnetic_space_group::{PyMoyoCollinearMagneticDataset, PyMoyoNonCollinearMagneticDataset}; +pub use space_group::PyMoyoDataset; diff --git a/moyopy/src/dataset/magnetic_space_group.rs b/moyopy/src/dataset/magnetic_space_group.rs new file mode 100644 index 0000000..929c184 --- /dev/null +++ b/moyopy/src/dataset/magnetic_space_group.rs @@ -0,0 +1,310 @@ +use pyo3::prelude::*; + +use moyo::base::{AngleTolerance, Collinear, NonCollinear, RotationMagneticMomentAction}; +use moyo::MoyoMagneticDataset; + +use crate::base::{ + PyCollinearMagneticCell, PyMagneticOperations, PyMoyoError, PyNonCollinearMagneticCell, +}; + +#[derive(Debug)] +#[pyclass(name = "MoyoCollinearMagneticDataset", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyMoyoCollinearMagneticDataset(MoyoMagneticDataset); + +#[pymethods] +impl PyMoyoCollinearMagneticDataset { + #[new] + #[pyo3(signature = (magnetic_cell, *, symprec=1e-4, angle_tolerance=None, mag_symprec=None, is_axial=false))] + pub fn new( + magnetic_cell: &PyCollinearMagneticCell, + symprec: f64, + angle_tolerance: Option, + mag_symprec: Option, + is_axial: bool, + ) -> Result { + let angle_tolerance = if let Some(angle_tolerance) = angle_tolerance { + AngleTolerance::Radian(angle_tolerance) + } else { + AngleTolerance::Default + }; + let action = if is_axial { + RotationMagneticMomentAction::Axial + } else { + RotationMagneticMomentAction::Polar + }; + + let dataset = MoyoMagneticDataset::new( + &magnetic_cell.to_owned().into(), + symprec, + angle_tolerance, + mag_symprec, + action, + )?; + Ok(PyMoyoCollinearMagneticDataset(dataset)) + } + + // ------------------------------------------------------------------------ + // Magnetic space-group type + // ------------------------------------------------------------------------ + + #[getter] + pub fn uni_number(&self) -> i32 { + self.0.uni_number + } + + // ------------------------------------------------------------------------ + // Magnetic symmetry operations in the input cell + // ------------------------------------------------------------------------ + + #[getter] + pub fn magnetic_operations(&self) -> PyMagneticOperations { + self.0.magnetic_operations.clone().into() + } + + // ------------------------------------------------------------------------ + // Site symmetry + // ------------------------------------------------------------------------ + + #[getter] + pub fn orbits(&self) -> Vec { + self.0.orbits.clone() + } + + // ------------------------------------------------------------------------ + // Standardized magnetic cell + // ------------------------------------------------------------------------ + + #[getter] + pub fn std_mag_cell(&self) -> PyCollinearMagneticCell { + self.0.std_mag_cell.clone().into() + } + + #[getter] + pub fn std_linear(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.std_linear.transpose().into() + } + + #[getter] + pub fn std_origin_shift(&self) -> [f64; 3] { + self.0.std_origin_shift.into() + } + + #[getter] + pub fn std_rotation_matrix(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.std_rotation_matrix.transpose().into() + } + + // ------------------------------------------------------------------------ + // Primitive standardized magnetic cell + // ------------------------------------------------------------------------ + + #[getter] + pub fn prim_std_mag_cell(&self) -> PyCollinearMagneticCell { + self.0.prim_std_mag_cell.clone().into() + } + + #[getter] + pub fn prim_std_linear(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.prim_std_linear.transpose().into() + } + + #[getter] + pub fn prim_std_origin_shift(&self) -> [f64; 3] { + self.0.prim_std_origin_shift.into() + } + + #[getter] + pub fn mapping_std_prim(&self) -> Vec { + self.0.mapping_std_prim.clone() + } + + // ------------------------------------------------------------------------ + // Final parameters + // ------------------------------------------------------------------------ + + #[getter] + pub fn symprec(&self) -> f64 { + self.0.symprec + } + + #[getter] + pub fn angle_tolerance(&self) -> Option { + if let AngleTolerance::Radian(angle_tolerance) = self.0.angle_tolerance { + Some(angle_tolerance) + } else { + None + } + } + + #[getter] + pub fn mag_symprec(&self) -> f64 { + self.0.mag_symprec + } + + // ------------------------------------------------------------------------ + // Special methods + // ------------------------------------------------------------------------ + fn __str__(&self) -> String { + format!( + "MoyoMagneticDataset(uni_number={}, magnetic_operations=<{} magnetic operations>, orbits={:?})", + self.0.uni_number, + self.0.magnetic_operations.len(), + self.0.orbits + ) + } +} + +#[derive(Debug)] +#[pyclass(name = "MoyoNonCollinearMagneticDataset", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyMoyoNonCollinearMagneticDataset(MoyoMagneticDataset); + +#[pymethods] +impl PyMoyoNonCollinearMagneticDataset { + #[new] + #[pyo3(signature = (magnetic_cell, *, symprec=1e-4, angle_tolerance=None, mag_symprec=None, is_axial=true))] + pub fn new( + magnetic_cell: &PyNonCollinearMagneticCell, + symprec: f64, + angle_tolerance: Option, + mag_symprec: Option, + is_axial: bool, + ) -> Result { + let angle_tolerance = if let Some(angle_tolerance) = angle_tolerance { + AngleTolerance::Radian(angle_tolerance) + } else { + AngleTolerance::Default + }; + let action = if is_axial { + RotationMagneticMomentAction::Axial + } else { + RotationMagneticMomentAction::Polar + }; + + let dataset = MoyoMagneticDataset::new( + &magnetic_cell.to_owned().into(), + symprec, + angle_tolerance, + mag_symprec, + action, + )?; + Ok(PyMoyoNonCollinearMagneticDataset(dataset)) + } + + // ------------------------------------------------------------------------ + // Magnetic space-group type + // ------------------------------------------------------------------------ + + #[getter] + pub fn uni_number(&self) -> i32 { + self.0.uni_number + } + + // ------------------------------------------------------------------------ + // Magnetic symmetry operations in the input cell + // ------------------------------------------------------------------------ + + #[getter] + pub fn magnetic_operations(&self) -> PyMagneticOperations { + self.0.magnetic_operations.clone().into() + } + + // ------------------------------------------------------------------------ + // Site symmetry + // ------------------------------------------------------------------------ + + #[getter] + pub fn orbits(&self) -> Vec { + self.0.orbits.clone() + } + + // ------------------------------------------------------------------------ + // Standardized magnetic cell + // ------------------------------------------------------------------------ + + #[getter] + pub fn std_mag_cell(&self) -> PyNonCollinearMagneticCell { + self.0.std_mag_cell.clone().into() + } + + #[getter] + pub fn std_linear(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.std_linear.transpose().into() + } + + #[getter] + pub fn std_origin_shift(&self) -> [f64; 3] { + self.0.std_origin_shift.into() + } + + #[getter] + pub fn std_rotation_matrix(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.std_rotation_matrix.transpose().into() + } + + // ------------------------------------------------------------------------ + // Primitive standardized magnetic cell + // ------------------------------------------------------------------------ + + #[getter] + pub fn prim_std_mag_cell(&self) -> PyNonCollinearMagneticCell { + self.0.prim_std_mag_cell.clone().into() + } + + #[getter] + pub fn prim_std_linear(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.prim_std_linear.transpose().into() + } + + #[getter] + pub fn prim_std_origin_shift(&self) -> [f64; 3] { + self.0.prim_std_origin_shift.into() + } + + #[getter] + pub fn mapping_std_prim(&self) -> Vec { + self.0.mapping_std_prim.clone() + } + + // ------------------------------------------------------------------------ + // Final parameters + // ------------------------------------------------------------------------ + + #[getter] + pub fn symprec(&self) -> f64 { + self.0.symprec + } + + #[getter] + pub fn angle_tolerance(&self) -> Option { + if let AngleTolerance::Radian(angle_tolerance) = self.0.angle_tolerance { + Some(angle_tolerance) + } else { + None + } + } + + #[getter] + pub fn mag_symprec(&self) -> f64 { + self.0.mag_symprec + } + + // ------------------------------------------------------------------------ + // Special methods + // ------------------------------------------------------------------------ + fn __str__(&self) -> String { + format!( + "MoyoMagneticDataset(uni_number={}, magnetic_operations=<{} magnetic operations>, orbits={:?})", + self.0.uni_number, + self.0.magnetic_operations.len(), + self.0.orbits + ) + } +} diff --git a/moyopy/src/dataset/space_group.rs b/moyopy/src/dataset/space_group.rs new file mode 100644 index 0000000..5d8163f --- /dev/null +++ b/moyopy/src/dataset/space_group.rs @@ -0,0 +1,165 @@ +use pyo3::prelude::*; + +use moyo::base::AngleTolerance; +use moyo::data::Setting; +use moyo::MoyoDataset; + +use crate::base::{PyMoyoError, PyOperations, PyStructure}; +use crate::data::PySetting; + +#[derive(Debug)] +#[pyclass(name = "MoyoDataset", frozen)] +#[pyo3(module = "moyopy")] +pub struct PyMoyoDataset(MoyoDataset); + +#[pymethods] +impl PyMoyoDataset { + #[new] + #[pyo3(signature = (cell, *, symprec=1e-4, angle_tolerance=None, setting=None))] + pub fn new( + cell: &PyStructure, + symprec: f64, + angle_tolerance: Option, + setting: Option, + ) -> Result { + let angle_tolerance = if let Some(angle_tolerance) = angle_tolerance { + AngleTolerance::Radian(angle_tolerance) + } else { + AngleTolerance::Default + }; + + let setting = if let Some(setting) = setting { + setting.into() + } else { + Setting::Spglib + }; + + let dataset = MoyoDataset::new(&cell.to_owned().into(), symprec, angle_tolerance, setting)?; + Ok(PyMoyoDataset(dataset)) + } + + // ------------------------------------------------------------------------ + // Identification + // ------------------------------------------------------------------------ + #[getter] + pub fn number(&self) -> i32 { + self.0.number + } + + #[getter] + pub fn hall_number(&self) -> i32 { + self.0.hall_number + } + + // ------------------------------------------------------------------------ + // Symmetry operations in the input cell + // ------------------------------------------------------------------------ + #[getter] + pub fn operations(&self) -> PyOperations { + self.0.operations.clone().into() + } + + // ------------------------------------------------------------------------ + // Site symmetry + // ------------------------------------------------------------------------ + #[getter] + pub fn orbits(&self) -> Vec { + self.0.orbits.clone() + } + + #[getter] + pub fn wyckoffs(&self) -> Vec { + self.0.wyckoffs.clone() + } + + #[getter] + pub fn site_symmetry_symbols(&self) -> Vec { + self.0.site_symmetry_symbols.clone() + } + + // ------------------------------------------------------------------------ + // Standardized cell + // ------------------------------------------------------------------------ + #[getter] + pub fn std_cell(&self) -> PyStructure { + self.0.std_cell.clone().into() + } + + #[getter] + pub fn std_linear(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.std_linear.transpose().into() + } + + #[getter] + pub fn std_origin_shift(&self) -> [f64; 3] { + self.0.std_origin_shift.into() + } + + #[getter] + pub fn std_rotation_matrix(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.std_rotation_matrix.transpose().into() + } + + #[getter] + pub fn pearson_symbol(&self) -> String { + self.0.pearson_symbol.clone() + } + + // ------------------------------------------------------------------------ + // Primitive standardized cell + // ------------------------------------------------------------------------ + #[getter] + pub fn prim_std_cell(&self) -> PyStructure { + self.0.prim_std_cell.clone().into() + } + + #[getter] + pub fn prim_std_linear(&self) -> [[f64; 3]; 3] { + // Since nalgebra stores matrices in column-major order, we need to transpose them + self.0.prim_std_linear.transpose().into() + } + + #[getter] + pub fn prim_std_origin_shift(&self) -> [f64; 3] { + self.0.prim_std_origin_shift.into() + } + + #[getter] + pub fn mapping_std_prim(&self) -> Vec { + self.0.mapping_std_prim.clone() + } + + // ------------------------------------------------------------------------ + // Final parameters + // ------------------------------------------------------------------------ + #[getter] + pub fn symprec(&self) -> f64 { + self.0.symprec + } + + #[getter] + pub fn angle_tolerance(&self) -> Option { + if let AngleTolerance::Radian(angle_tolerance) = self.0.angle_tolerance { + Some(angle_tolerance) + } else { + None + } + } + + // ------------------------------------------------------------------------ + // Special methods + // ------------------------------------------------------------------------ + fn __str__(&self) -> String { + format!( + "MoyoDataset(number={}, hall_number={}, operations=<{} operations>, orbits={:?}, wyckoffs={:?}, site_symmetry_symbols={:?})", + self.0.number, + self.0.hall_number, + self.0.operations.len(), + self.0.orbits, + self.0.wyckoffs, + self.0.site_symmetry_symbols + ) + } +} diff --git a/moyopy/src/lib.rs b/moyopy/src/lib.rs index d7d5cb5..356c0a3 100644 --- a/moyopy/src/lib.rs +++ b/moyopy/src/lib.rs @@ -3,151 +3,18 @@ use std::sync::OnceLock; pub mod base; pub mod data; +pub mod dataset; -use moyo::base::AngleTolerance; -use moyo::data::Setting; -use moyo::MoyoDataset; - -use crate::base::{PyMoyoError, PyOperations, PyStructure}; +use crate::base::{ + PyCollinearMagneticCell, PyMagneticOperations, PyMoyoError, PyNonCollinearMagneticCell, + PyOperations, PyStructure, +}; use crate::data::{ operations_from_number, PyCentering, PyHallSymbolEntry, PySetting, PySpaceGroupType, }; - -#[derive(Debug)] -#[pyclass(name = "MoyoDataset", frozen)] -#[pyo3(module = "moyopy")] -pub struct PyMoyoDataset(MoyoDataset); - -#[pymethods] -impl PyMoyoDataset { - #[new] - #[pyo3(signature = (cell, *, symprec=1e-4, angle_tolerance=None, setting=None))] - pub fn new( - cell: &PyStructure, - symprec: f64, - angle_tolerance: Option, - setting: Option, - ) -> Result { - let angle_tolerance = if let Some(angle_tolerance) = angle_tolerance { - AngleTolerance::Radian(angle_tolerance) - } else { - AngleTolerance::Default - }; - - let setting = if let Some(setting) = setting { - setting.into() - } else { - Setting::Spglib - }; - - let dataset = MoyoDataset::new(&cell.to_owned().into(), symprec, angle_tolerance, setting)?; - Ok(PyMoyoDataset(dataset)) - } - - #[getter] - pub fn number(&self) -> i32 { - self.0.number - } - - #[getter] - pub fn hall_number(&self) -> i32 { - self.0.hall_number - } - - #[getter] - pub fn operations(&self) -> PyOperations { - self.0.operations.clone().into() - } - - #[getter] - pub fn orbits(&self) -> Vec { - self.0.orbits.clone() - } - - #[getter] - pub fn wyckoffs(&self) -> Vec { - self.0.wyckoffs.clone() - } - - #[getter] - pub fn site_symmetry_symbols(&self) -> Vec { - self.0.site_symmetry_symbols.clone() - } - - #[getter] - pub fn std_cell(&self) -> PyStructure { - self.0.std_cell.clone().into() - } - - #[getter] - pub fn std_linear(&self) -> [[f64; 3]; 3] { - // Since nalgebra stores matrices in column-major order, we need to transpose them - self.0.std_linear.transpose().into() - } - - #[getter] - pub fn std_origin_shift(&self) -> [f64; 3] { - self.0.std_origin_shift.into() - } - - #[getter] - pub fn std_rotation_matrix(&self) -> [[f64; 3]; 3] { - // Since nalgebra stores matrices in column-major order, we need to transpose them - self.0.std_rotation_matrix.transpose().into() - } - - #[getter] - pub fn pearson_symbol(&self) -> String { - self.0.pearson_symbol.clone() - } - - #[getter] - pub fn prim_std_cell(&self) -> PyStructure { - self.0.prim_std_cell.clone().into() - } - - #[getter] - pub fn prim_std_linear(&self) -> [[f64; 3]; 3] { - // Since nalgebra stores matrices in column-major order, we need to transpose them - self.0.prim_std_linear.transpose().into() - } - - #[getter] - pub fn prim_std_origin_shift(&self) -> [f64; 3] { - self.0.prim_std_origin_shift.into() - } - - #[getter] - pub fn mapping_std_prim(&self) -> Vec { - self.0.mapping_std_prim.clone() - } - - #[getter] - pub fn symprec(&self) -> f64 { - self.0.symprec - } - - #[getter] - pub fn angle_tolerance(&self) -> Option { - if let AngleTolerance::Radian(angle_tolerance) = self.0.angle_tolerance { - Some(angle_tolerance) - } else { - None - } - } - - fn __str__(&self) -> String { - format!( - "MoyoDataset(number={}, hall_number={}, operations=<{} operations>, orbits={:?}, wyckoffs={:?}, site_symmetry_symbols={:?})", - self.0.number, - self.0.hall_number, - self.0.operations.len(), - self.0.orbits, - self.0.wyckoffs, - self.0.site_symmetry_symbols - ) - } -} +use crate::dataset::{ + PyMoyoCollinearMagneticDataset, PyMoyoDataset, PyMoyoNonCollinearMagneticDataset, +}; // https://github.com/pydantic/pydantic-core/blob/main/src/lib.rs fn moyopy_version() -> &'static str { @@ -170,15 +37,21 @@ fn moyopy_version() -> &'static str { fn moyopy(m: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); + // lib m.add("__version__", moyopy_version())?; - // lib + // dataset m.add_class::()?; + m.add_class::()?; + m.add_class::()?; // base m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // data m.add_class::()?; From 374d2123e330332d020ffec8a88a78ca6769247c Mon Sep 17 00:00:00 2001 From: lan496 Date: Tue, 11 Feb 2025 18:50:05 +0900 Subject: [PATCH 2/5] Add python interface for magnetic space-group type --- moyo/src/data.rs | 3 +- moyo/src/data/magnetic_space_group.rs | 5 ++ moyopy/python/moyopy/_data.pyi | 22 ++++++++ moyopy/python/moyopy/_moyopy.pyi | 2 + .../{ => data}/test_hall_symbol_entry.py | 0 .../data/test_magnetic_space_group_type.py | 8 +++ .../tests/{ => data}/test_space_group_type.py | 0 .../tests/{ => data}/test_symmetry_data.py | 0 moyopy/src/data.rs | 2 + moyopy/src/data/magnetic_space_group_type.rs | 56 +++++++++++++++++++ moyopy/src/lib.rs | 4 +- 11 files changed, 100 insertions(+), 2 deletions(-) rename moyopy/python/tests/{ => data}/test_hall_symbol_entry.py (100%) create mode 100644 moyopy/python/tests/data/test_magnetic_space_group_type.py rename moyopy/python/tests/{ => data}/test_space_group_type.py (100%) rename moyopy/python/tests/{ => data}/test_symmetry_data.py (100%) create mode 100644 moyopy/src/data/magnetic_space_group_type.rs diff --git a/moyo/src/data.rs b/moyo/src/data.rs index ccd04dd..ce8ad6f 100644 --- a/moyo/src/data.rs +++ b/moyo/src/data.rs @@ -20,7 +20,8 @@ pub use hall_symbol::{HallSymbol, MagneticHallSymbol}; pub use hall_symbol_database::{hall_symbol_entry, HallNumber, HallSymbolEntry, Number}; pub use magnetic_hall_symbol_database::{magnetic_hall_symbol_entry, MagneticHallSymbolEntry}; pub use magnetic_space_group::{ - get_magnetic_space_group_type, ConstructType, UNINumber, NUM_MAGNETIC_SPACE_GROUP_TYPES, + get_magnetic_space_group_type, ConstructType, MagneticSpaceGroupType, UNINumber, + NUM_MAGNETIC_SPACE_GROUP_TYPES, }; pub use setting::Setting; diff --git a/moyo/src/data/magnetic_space_group.rs b/moyo/src/data/magnetic_space_group.rs index b241152..aff9be1 100644 --- a/moyo/src/data/magnetic_space_group.rs +++ b/moyo/src/data/magnetic_space_group.rs @@ -19,12 +19,17 @@ pub enum ConstructType { #[derive(Debug, Clone)] pub struct MagneticSpaceGroupType { + /// Serial number of UNI (and BNS) symbols pub uni_number: UNINumber, + /// Serial number in Litvin's [Magnetic group tables](https://www.iucr.org/publ/978-0-9553602-2-0) pub litvin_number: i32, + /// BNS number e.g. '151.32' pub bns_number: &'static str, + /// OG number e.g. '153.4.1270' pub og_number: &'static str, /// ITA number for reference space group in BNS setting pub number: Number, + /// Construct type of magnetic space group pub construct_type: ConstructType, } diff --git a/moyopy/python/moyopy/_data.pyi b/moyopy/python/moyopy/_data.pyi index 2fc6b64..44914ca 100644 --- a/moyopy/python/moyopy/_data.pyi +++ b/moyopy/python/moyopy/_data.pyi @@ -104,4 +104,26 @@ class SpaceGroupType: for string values. """ +class MagneticSpaceGroupType: + """Magnetic space-group type information.""" + def __init__(self, uni_number: int): ... + @property + def uni_number(self) -> int: + """Serial number of UNI (and BNS) symbols.""" + @property + def litvin_number(self) -> int: + """Serial number in Litvin's `Magnetic group tables `_.""" + @property + def bns_number(self) -> str: + """BNS number e.g. '151.32'""" + @property + def og_number(self) -> str: + """OG number e.g. '153.4.1270'""" + @property + def number(self) -> int: + """ITA number for reference space group in BNS setting.""" + @property + def construct_type(self) -> int: + """Construct type of magnetic space group from 1 to 4.""" + def operations_from_number(number: int, setting: Setting) -> Operations: ... diff --git a/moyopy/python/moyopy/_moyopy.pyi b/moyopy/python/moyopy/_moyopy.pyi index b2c87da..0f23dbd 100644 --- a/moyopy/python/moyopy/_moyopy.pyi +++ b/moyopy/python/moyopy/_moyopy.pyi @@ -7,6 +7,7 @@ from moyopy._base import ( # noqa: F401 from moyopy._data import ( Centering, HallSymbolEntry, + MagneticSpaceGroupType, Setting, SpaceGroupType, operations_from_number, @@ -30,6 +31,7 @@ __all__ = [ "Centering", "HallSymbolEntry", "SpaceGroupType", + "MagneticSpaceGroupType", "operations_from_number", # dataset "MoyoDataset", diff --git a/moyopy/python/tests/test_hall_symbol_entry.py b/moyopy/python/tests/data/test_hall_symbol_entry.py similarity index 100% rename from moyopy/python/tests/test_hall_symbol_entry.py rename to moyopy/python/tests/data/test_hall_symbol_entry.py diff --git a/moyopy/python/tests/data/test_magnetic_space_group_type.py b/moyopy/python/tests/data/test_magnetic_space_group_type.py new file mode 100644 index 0000000..52063dd --- /dev/null +++ b/moyopy/python/tests/data/test_magnetic_space_group_type.py @@ -0,0 +1,8 @@ +from moyopy import MagneticSpaceGroupType + + +def test_magnetic_space_group_type(): + msgt = MagneticSpaceGroupType(1262) + assert msgt.bns_number == "151.32" + assert msgt.og_number == "153.4.1270" + assert msgt.construct_type == 4 diff --git a/moyopy/python/tests/test_space_group_type.py b/moyopy/python/tests/data/test_space_group_type.py similarity index 100% rename from moyopy/python/tests/test_space_group_type.py rename to moyopy/python/tests/data/test_space_group_type.py diff --git a/moyopy/python/tests/test_symmetry_data.py b/moyopy/python/tests/data/test_symmetry_data.py similarity index 100% rename from moyopy/python/tests/test_symmetry_data.py rename to moyopy/python/tests/data/test_symmetry_data.py diff --git a/moyopy/src/data.rs b/moyopy/src/data.rs index 4bc69b1..c5bf6d3 100644 --- a/moyopy/src/data.rs +++ b/moyopy/src/data.rs @@ -1,8 +1,10 @@ mod hall_symbol; +mod magnetic_space_group_type; mod setting; mod space_group_type; pub use hall_symbol::{PyCentering, PyHallSymbolEntry}; +pub use magnetic_space_group_type::PyMagneticSpaceGroupType; pub use setting::PySetting; pub use space_group_type::PySpaceGroupType; diff --git a/moyopy/src/data/magnetic_space_group_type.rs b/moyopy/src/data/magnetic_space_group_type.rs new file mode 100644 index 0000000..fc4e1c8 --- /dev/null +++ b/moyopy/src/data/magnetic_space_group_type.rs @@ -0,0 +1,56 @@ +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +use moyo::data::{ + get_magnetic_space_group_type, ConstructType, MagneticSpaceGroupType, Number, UNINumber, +}; + +#[derive(Debug, Clone)] +#[pyclass(name = "MagneticSpaceGroupType", frozen)] +pub struct PyMagneticSpaceGroupType(MagneticSpaceGroupType); + +#[pymethods] +impl PyMagneticSpaceGroupType { + #[new] + pub fn new(uni_number: UNINumber) -> Result { + let magnetic_space_group_type = get_magnetic_space_group_type(uni_number).ok_or( + PyValueError::new_err(format!("Unknown uni_number: {}", uni_number)), + )?; + Ok(PyMagneticSpaceGroupType(magnetic_space_group_type)) + } + + #[getter] + pub fn uni_number(&self) -> UNINumber { + self.0.uni_number + } + + #[getter] + pub fn litvin_number(&self) -> i32 { + self.0.litvin_number + } + + #[getter] + pub fn bns_number(&self) -> String { + self.0.bns_number.to_string() + } + + #[getter] + pub fn og_number(&self) -> String { + self.0.og_number.to_string() + } + + #[getter] + pub fn number(&self) -> Number { + self.0.number + } + + #[getter] + pub fn construct_type(&self) -> i32 { + match self.0.construct_type { + ConstructType::Type1 => 1, + ConstructType::Type2 => 2, + ConstructType::Type3 => 3, + ConstructType::Type4 => 4, + } + } +} diff --git a/moyopy/src/lib.rs b/moyopy/src/lib.rs index 356c0a3..e3fdde3 100644 --- a/moyopy/src/lib.rs +++ b/moyopy/src/lib.rs @@ -10,7 +10,8 @@ use crate::base::{ PyOperations, PyStructure, }; use crate::data::{ - operations_from_number, PyCentering, PyHallSymbolEntry, PySetting, PySpaceGroupType, + operations_from_number, PyCentering, PyHallSymbolEntry, PyMagneticSpaceGroupType, PySetting, + PySpaceGroupType, }; use crate::dataset::{ PyMoyoCollinearMagneticDataset, PyMoyoDataset, PyMoyoNonCollinearMagneticDataset, @@ -58,6 +59,7 @@ fn moyopy(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_wrapped(wrap_pyfunction!(operations_from_number))?; Ok(()) From b768b76e93f8dffb7b9f226606627cc550bf8a4a Mon Sep 17 00:00:00 2001 From: lan496 Date: Tue, 11 Feb 2025 20:37:15 +0900 Subject: [PATCH 3/5] Fix magnetic cell standardization --- moyo/src/symmetrize/magnetic_standardize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moyo/src/symmetrize/magnetic_standardize.rs b/moyo/src/symmetrize/magnetic_standardize.rs index eaef3fc..3641497 100644 --- a/moyo/src/symmetrize/magnetic_standardize.rs +++ b/moyo/src/symmetrize/magnetic_standardize.rs @@ -114,7 +114,7 @@ impl StandardizedMagneticCell { action, ); let prim_std_mag_cell = - MagneticCell::from_cell(ref_std_cell.cell.clone(), prim_std_magnetic_moments); + MagneticCell::from_cell(ref_std_cell.prim_cell.clone(), prim_std_magnetic_moments); // To (conventional) standardized magnetic cell let refined_prim_mag_cell = ref_std_cell From 3e84a2d49fb3da81760e440fb2dae6a2010307a1 Mon Sep 17 00:00:00 2001 From: lan496 Date: Tue, 11 Feb 2025 20:37:43 +0900 Subject: [PATCH 4/5] Fix signiture --- moyopy/python/moyopy/_dataset.pyi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moyopy/python/moyopy/_dataset.pyi b/moyopy/python/moyopy/_dataset.pyi index 06f9aec..d80940f 100644 --- a/moyopy/python/moyopy/_dataset.pyi +++ b/moyopy/python/moyopy/_dataset.pyi @@ -14,6 +14,7 @@ class MoyoDataset: def __init__( self, cell: Cell, + *, symprec: float = 1e-4, angle_tolerance: float | None = None, setting: Setting | None = None, @@ -107,6 +108,7 @@ class MoyoCollinearMagneticDataset: def __init__( self, magnetic_cell: CollinearMagneticCell, + *, symprec: float = 1e-4, angle_tolerance: float | None = None, mag_symprec: float | None = None, @@ -191,6 +193,7 @@ class MoyoNonCollinearMagneticDataset: def __init__( self, magnetic_cell: NonCollinearMagneticCell, + *, symprec: float = 1e-4, angle_tolerance: float | None = None, mag_symprec: float | None = None, From 287a31177307f0337d46c0c3b88449e0a5e7dd82 Mon Sep 17 00:00:00 2001 From: lan496 Date: Tue, 11 Feb 2025 21:33:02 +0900 Subject: [PATCH 5/5] Update rust example --- moyo/src/lib.rs | 30 ++++++++++++++++++++++----- moyopy/examples/basic.py | 2 +- moyopy/examples/pymatgen_structure.py | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/moyo/src/lib.rs b/moyo/src/lib.rs index 5bb04e3..e47416f 100644 --- a/moyo/src/lib.rs +++ b/moyo/src/lib.rs @@ -18,10 +18,15 @@ moyo = "*" The basic usage of **moyo** is to create a [`moyo::base::Cell`](Cell) representing a crystal structure, and then create a [`moyo::MoyoDataset`](MoyoDataset) from the [`moyo::base::Cell`](Cell). The [`moyo::MoyoDataset`](MoyoDataset) contains symmetry information of the input crystal structure: for example, the space group number, symmetry operations, and standardized cell. +Magnetic symmetry is also supported in **moyo**. +Magnetic moments are represented by a struct implementing the [`moyo::base::MagneticMoment`](MagneticMoment) trait: for example, [`moyo::base::Collinear`](Collinear) or [`moyo::base::NonCollinear`](NonCollinear). +Magnetic cell is represented by a [`moyo::base::MagneticCell`](MagneticCell) struct. +The [`moyo::MoyoMagneticDataset`](MoyoMagneticDataset) contains magnetic symmetry information of the input magnetic cell: for example, the magnetic space-group type, magnetic symmetry operations, and standardized magnetic cell. + ``` use nalgebra::{matrix, vector, Matrix3, Vector3}; -use moyo::MoyoDataset; -use moyo::base::{Cell, AngleTolerance, Lattice}; +use moyo::{MoyoDataset, MoyoMagneticDataset}; +use moyo::base::{Cell, MagneticCell, AngleTolerance, Lattice, NonCollinear, RotationMagneticMomentAction}; use moyo::data::Setting; let lattice = Lattice::new(matrix![ @@ -39,15 +44,29 @@ let positions = vec![ Vector3::new(x_4f + 0.5, -x_4f + 0.5, 0.5), // O(4f) ]; let numbers = vec![0, 0, 1, 1, 1, 1]; -let cell = Cell::new(lattice, positions, numbers); +let cell = Cell::new(lattice.clone(), positions.clone(), numbers.clone()); -let symprec = 1e-5; +let symprec = 1e-4; let angle_tolerance = AngleTolerance::Default; let setting = Setting::Standard; let dataset = MoyoDataset::new(&cell, symprec, angle_tolerance, setting).unwrap(); - assert_eq!(dataset.number, 136); // P4_2/mnm + +let magnetic_moments = vec![ + NonCollinear(vector![0.0, 0.0, 0.7]), + NonCollinear(vector![0.0, 0.0, -0.7]), + NonCollinear(vector![0.0, 0.0, 0.0]), + NonCollinear(vector![0.0, 0.0, 0.0]), + NonCollinear(vector![0.0, 0.0, 0.0]), + NonCollinear(vector![0.0, 0.0, 0.0]), +]; +let magnetic_cell = MagneticCell::new(lattice, positions, numbers, magnetic_moments); + +let action = RotationMagneticMomentAction::Axial; + +let magnetic_dataset = MoyoMagneticDataset::new(&magnetic_cell, symprec, angle_tolerance, None, action).unwrap(); +assert_eq!(magnetic_dataset.uni_number, 1159); // BNS 136.499 ``` ## Features @@ -57,6 +76,7 @@ assert_eq!(dataset.number, 136); // P4_2/mnm - Space-group type identification - Wyckoff position assignment - Crystal structure symmetrization +- Magnetic space group support */ #[allow(unused_imports)] diff --git a/moyopy/examples/basic.py b/moyopy/examples/basic.py index e213137..e89697a 100644 --- a/moyopy/examples/basic.py +++ b/moyopy/examples/basic.py @@ -23,7 +23,7 @@ numbers = [0, 0, 1, 1] cell = moyopy.Cell(basis, positions, numbers) -dataset = moyopy.MoyoDataset(cell, symprec=1e-5, angle_tolerance=None, setting=None) +dataset = moyopy.MoyoDataset(cell, symprec=1e-4, angle_tolerance=None, setting=None) assert dataset.number == 186 assert dataset.hall_number == 480 diff --git a/moyopy/examples/pymatgen_structure.py b/moyopy/examples/pymatgen_structure.py index 0f86790..57d4cb6 100644 --- a/moyopy/examples/pymatgen_structure.py +++ b/moyopy/examples/pymatgen_structure.py @@ -11,7 +11,7 @@ class MoyoSpacegroupAnalyzer: def __init__( self, structure: Structure, - symprec: float = 1e-5, + symprec: float = 1e-4, angle_tolerance: float | None = None, setting: moyopy.Setting | None = None, ):