├── .github ├── release.yaml └── workflows │ ├── check.yaml │ ├── ci-python.yaml │ ├── ci-rust.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bench ├── mp │ ├── analysis.ipynb │ ├── get_mp.py │ ├── mp.png │ └── mp.py └── requirements.txt ├── deny.toml ├── justfile ├── moyo ├── Cargo.toml ├── README.md ├── benches │ ├── dataset.rs │ └── translation_search.rs ├── src │ ├── base.rs │ ├── base │ │ ├── action.rs │ │ ├── cell.rs │ │ ├── error.rs │ │ ├── lattice.rs │ │ ├── magnetic_cell.rs │ │ ├── operation.rs │ │ ├── permutation.rs │ │ ├── tolerance.rs │ │ └── transformation.rs │ ├── data.rs │ ├── data │ │ ├── arithmetic_crystal_class.rs │ │ ├── centering.rs │ │ ├── classification.rs │ │ ├── hall_symbol.rs │ │ ├── hall_symbol_database.rs │ │ ├── magnetic_hall_symbol_database.rs │ │ ├── magnetic_space_group.rs │ │ ├── point_group.rs │ │ ├── setting.rs │ │ └── wyckoff.rs │ ├── identify.rs │ ├── identify │ │ ├── magnetic_space_group.rs │ │ ├── normalizer.rs │ │ ├── point_group.rs │ │ ├── rotation_type.rs │ │ └── space_group.rs │ ├── lib.rs │ ├── math.rs │ ├── math │ │ ├── cycle_checker.rs │ │ ├── delaunay.rs │ │ ├── elementary.rs │ │ ├── hnf.rs │ │ ├── integer_system.rs │ │ ├── minkowski.rs │ │ ├── niggli.rs │ │ └── snf.rs │ ├── search.rs │ ├── search │ │ ├── primitive_cell.rs │ │ ├── primitive_symmetry_search.rs │ │ ├── solve.rs │ │ └── symmetry_search.rs │ ├── symmetrize.rs │ └── symmetrize │ │ ├── magnetic_standardize.rs │ │ └── standardize.rs └── tests │ ├── assets │ ├── AB_mC8_15_e_a.json │ ├── mp-1185639.json │ ├── mp-1197586.json │ ├── mp-1201492.json │ ├── mp-1221598.json │ ├── mp-1277787.json │ ├── mp-30665.json │ ├── mp-550745.json │ ├── mp-569901.json │ ├── wbm-1-29497.json │ ├── wbm-1-42389.json │ ├── wbm-1-42433.json │ └── wyckoff_edge_case.json │ ├── test_moyo_dataset.rs │ └── test_moyo_magnetic_dataset.rs ├── moyopy ├── Cargo.toml ├── README.md ├── docs │ ├── _static │ │ └── .gitkeep │ ├── conf.py │ ├── examples │ │ └── index.md │ ├── index.md │ └── references.bib ├── examples │ ├── basic.py │ ├── hm_to_number.py │ ├── pymatgen_structure.py │ └── space_group_type.py ├── pyproject.toml ├── python │ ├── moyopy │ │ ├── __init__.py │ │ ├── _base.pyi │ │ ├── _data.pyi │ │ ├── _dataset.pyi │ │ ├── _moyopy.pyi │ │ ├── interface.py │ │ └── py.typed │ └── tests │ │ ├── conftest.py │ │ ├── data │ │ ├── test_hall_symbol_entry.py │ │ ├── test_magnetic_space_group_type.py │ │ ├── test_space_group_type.py │ │ └── test_symmetry_data.py │ │ ├── test_cell.py │ │ ├── test_interface.py │ │ └── test_moyo_dataset.py └── src │ ├── base.rs │ ├── base │ ├── cell.rs │ ├── error.rs │ ├── magnetic_cell.rs │ └── operation.rs │ ├── data.rs │ ├── data │ ├── centering.rs │ ├── hall_symbol.rs │ ├── magnetic_space_group_type.rs │ ├── setting.rs │ └── space_group_type.rs │ ├── dataset.rs │ ├── dataset │ ├── magnetic_space_group.rs │ └── space_group.rs │ └── lib.rs ├── scripts ├── hall_symbol.py ├── magnetic_hall_symbol_database.py ├── magnetic_space_group_type.py ├── setting.py └── wyckoff.py └── typos.toml /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: 🛠 Breaking Changes 4 | labels: 5 | - Semver-Major 6 | - breaking-changes 7 | - title: 🎉 New Features 8 | labels: 9 | - Semver-Minor 10 | - enhancement 11 | - title: ⚠️ Deprecation 12 | labels: 13 | - deprecation 14 | - title: 🐞 Bug fixes 15 | labels: 16 | - bug 17 | - title: 📔 Documentation 18 | labels: 19 | - documentation 20 | - title: Other changes 21 | labels: 22 | - '*' 23 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [develop, main] 6 | pull_request: 7 | branches: [develop, main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | pre-commit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-python@v4 16 | - uses: pre-commit/action@v3.0.1 17 | with: 18 | extra_args: --all-files 19 | cargo-deny: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: Swatinem/rust-cache@v2 24 | - uses: EmbarkStudios/cargo-deny-action@v1 25 | with: 26 | rust-version: 1.83.0 27 | pass: 28 | name: ✅ Pass checks 29 | needs: [pre-commit, cargo-deny] 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Check all CI jobs 33 | uses: re-actors/alls-green@release/v1 34 | with: 35 | jobs: ${{ toJSON(needs) }} 36 | if: always() 37 | -------------------------------------------------------------------------------- /.github/workflows/ci-rust.yaml: -------------------------------------------------------------------------------- 1 | name: Test Rust implementation 2 | 3 | on: 4 | push: 5 | branches: [develop, main] 6 | pull_request: 7 | branches: [develop, main] 8 | workflow_dispatch: 9 | 10 | env: 11 | PYTHON_VERSION: "3.11" 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ env.PYTHON_VERSION }} 21 | - uses: Swatinem/rust-cache@v2 22 | - name: Install cargo-llvm-cov 23 | uses: taiki-e/install-action@cargo-llvm-cov 24 | - name: Test 25 | run: cargo llvm-cov --release --all-features --workspace --lcov --output-path lcov.info 26 | - name: Upload coverage to Codecov 27 | if: github.event.repository.fork == false 28 | uses: codecov/codecov-action@v5 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | files: lcov.info 32 | fail_ci_if_error: false 33 | pass: 34 | name: ✅ Pass ci-rust 35 | needs: [test] 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Check all CI jobs 39 | uses: re-actors/alls-green@release/v1 40 | with: 41 | jobs: ${{ toJSON(needs) }} 42 | if: always() 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v[0-9]+.[0-9]+.[0-9]+" 5 | - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" 6 | workflow_dispatch: {} 7 | jobs: 8 | release: 9 | name: Create release 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: softprops/action-gh-release@v2 16 | with: 17 | name: moyo ${{ github.ref_name }} 18 | draft: true 19 | prerelease: ${{ contains(github.ref, 'rc') }} 20 | generate_release_notes: true 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Rust 3 | ############################################################################### 4 | # will have compiled files and executables 5 | debug/ 6 | target/ 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # MSVC Windows builds of rustc generate these, which store debugging information 12 | *.pdb 13 | 14 | ############################################################################### 15 | # LaTeX 16 | ############################################################################### 17 | ## Build tool directories for auxiliary files 18 | # latexrun 19 | latex.out/ 20 | 21 | ## Auxiliary and intermediate files from other packages: 22 | # algorithms 23 | *.alg 24 | *.loa 25 | 26 | # achemso 27 | acs-*.bib 28 | 29 | # amsthm 30 | *.them 31 | 32 | # beamer 33 | *.nav 34 | *.pre 35 | *.snm 36 | *.vrb 37 | 38 | # changes 39 | *.soc 40 | 41 | # comment 42 | *.cut 43 | 44 | # cprotect 45 | *.cpt 46 | 47 | # elsarticle (documentclass of Elsevier journals) 48 | *.spl 49 | 50 | # endnotes 51 | *.ent 52 | 53 | # fixme 54 | *.lox 55 | 56 | # feynmf/feynmp 57 | *.mf 58 | *.mp 59 | *.t[1-9] 60 | *.t[1-9][0-9] 61 | *.tfm 62 | 63 | #(r)(e)ledmac/(r)(e)ledpar 64 | *.end 65 | *.?end 66 | *.[1-9] 67 | *.[1-9][0-9] 68 | *.[1-9][0-9][0-9] 69 | *.[1-9]R 70 | *.[1-9][0-9]R 71 | *.[1-9][0-9][0-9]R 72 | *.eledsec[1-9] 73 | *.eledsec[1-9]R 74 | *.eledsec[1-9][0-9] 75 | *.eledsec[1-9][0-9]R 76 | *.eledsec[1-9][0-9][0-9] 77 | *.eledsec[1-9][0-9][0-9]R 78 | 79 | # glossaries 80 | *.acn 81 | *.acr 82 | *.glg 83 | *.glo 84 | *.gls 85 | *.glsdefs 86 | *.lzo 87 | *.lzs 88 | *.slg 89 | *.slo 90 | *.sls 91 | 92 | # uncomment this for glossaries-extra (will ignore makeindex's style files!) 93 | # *.ist 94 | 95 | # gnuplot 96 | *.gnuplot 97 | *.table 98 | 99 | # gnuplottex 100 | *-gnuplottex-* 101 | 102 | # gregoriotex 103 | *.gaux 104 | *.glog 105 | *.gtex 106 | 107 | # htlatex 108 | *.4ct 109 | *.4tc 110 | *.idv 111 | *.lg 112 | *.trc 113 | *.xref 114 | 115 | # hyperref 116 | *.brf 117 | 118 | # knitr 119 | *-concordance.tex 120 | # *.tikz 121 | *-tikzDictionary 122 | 123 | # listings 124 | *.lol 125 | 126 | # luatexja-ruby 127 | *.ltjruby 128 | 129 | # makeidx 130 | *.idx 131 | *.ilg 132 | *.ind 133 | 134 | # minitoc 135 | *.maf 136 | *.mlf 137 | *.mlt 138 | *.mtc[0-9]* 139 | *.slf[0-9]* 140 | *.slt[0-9]* 141 | *.stc[0-9]* 142 | 143 | # minted 144 | _minted* 145 | *.pyg 146 | 147 | # morewrites 148 | *.mw 149 | 150 | # newpax 151 | *.newpax 152 | 153 | # nomencl 154 | *.nlg 155 | *.nlo 156 | *.nls 157 | 158 | # pax 159 | *.pax 160 | 161 | # pdfpcnotes 162 | *.pdfpc 163 | 164 | # sagetex 165 | *.sagetex.sage 166 | *.sagetex.py 167 | *.sagetex.scmd 168 | 169 | # scrwfile 170 | *.wrt 171 | 172 | # svg 173 | svg-inkscape/ 174 | 175 | # sympy 176 | *.sout 177 | *.sympy 178 | sympy-plots-for-*.tex/ 179 | 180 | # pdfcomment 181 | *.upa 182 | *.upb 183 | 184 | # pythontex 185 | *.pytxcode 186 | pythontex-files-*/ 187 | 188 | # tcolorbox 189 | *.listing 190 | 191 | # thmtools 192 | *.loe 193 | 194 | # TikZ & PGF 195 | *.dpth 196 | *.md5 197 | *.auxlock 198 | 199 | # titletoc 200 | *.ptc 201 | 202 | # vhistory 203 | *.hst 204 | *.ver 205 | 206 | # xcolor 207 | *.xcp 208 | 209 | # xmpincl 210 | *.xmpi 211 | 212 | # xindy 213 | *.xdy 214 | 215 | # xypic precompiled matrices and outlines 216 | *.xyc 217 | *.xyd 218 | 219 | # endfloat 220 | *.ttt 221 | *.fff 222 | 223 | # Latexian 224 | TSWLatexianTemp* 225 | 226 | ## Editors: 227 | # WinEdt 228 | *.bak 229 | *.save 230 | 231 | # Texpad 232 | .texpadtmp 233 | 234 | # LyX 235 | *.lyx~ 236 | 237 | # Kile 238 | *.backup 239 | 240 | # gummi 241 | .*.swp 242 | 243 | # KBibTeX 244 | *~[0-9]* 245 | 246 | # TeXnicCenter 247 | *.tps 248 | 249 | # auto folder when using emacs and auctex 250 | ./auto/* 251 | *.el 252 | 253 | # expex forward references with \gathertags 254 | *-tags.tex 255 | 256 | # standalone packages 257 | *.sta 258 | 259 | # Makeindex log files 260 | *.lpz 261 | 262 | # xwatermark package 263 | *.xwm 264 | 265 | # REVTeX puts footnotes in the bibliography by default, unless the nofootinbib 266 | # option is specified. Footnotes are the stored in a file with suffix Notes.bib. 267 | # Uncomment the next line to have this generated file ignored. 268 | #*Notes.bib 269 | 270 | ############################################################################### 271 | # Python 272 | ############################################################################### 273 | /target 274 | 275 | # Byte-compiled / optimized / DLL files 276 | __pycache__/ 277 | .pytest_cache/ 278 | *.py[cod] 279 | 280 | # C extensions 281 | *.so 282 | 283 | # Distribution / packaging 284 | .Python 285 | .venv/ 286 | env/ 287 | bin/ 288 | build/ 289 | develop-eggs/ 290 | dist/ 291 | eggs/ 292 | lib/ 293 | lib64/ 294 | parts/ 295 | sdist/ 296 | var/ 297 | include/ 298 | man/ 299 | venv/ 300 | *.egg-info/ 301 | .installed.cfg 302 | *.egg 303 | 304 | # Installer logs 305 | pip-log.txt 306 | pip-delete-this-directory.txt 307 | pip-selfcheck.json 308 | 309 | # Unit test / coverage reports 310 | htmlcov/ 311 | .tox/ 312 | .coverage 313 | .cache 314 | nosetests.xml 315 | coverage.xml 316 | 317 | # Translations 318 | *.mo 319 | 320 | # Mr Developer 321 | .mr.developer.cfg 322 | .project 323 | .pydevproject 324 | 325 | # Rope 326 | .ropeproject 327 | 328 | # Django stuff: 329 | *.log 330 | *.pot 331 | 332 | .DS_Store 333 | 334 | # Sphinx documentation 335 | docs/_build/ 336 | 337 | # PyCharm 338 | .idea/ 339 | 340 | # VSCode 341 | .vscode/ 342 | 343 | # Pyenv 344 | .python-version 345 | 346 | 347 | ############################################################################### 348 | # Others 349 | ############################################################################### 350 | .vscode/ 351 | flamegraph.svg 352 | output/ 353 | target/ 354 | *.so 355 | .ipynb_checkpoints/ 356 | lcov.info 357 | 358 | *.json 359 | !**/assets/*.json 360 | .env 361 | *.png 362 | *.cif 363 | *.html 364 | 365 | docs/note/ 366 | 367 | # python-interface docs 368 | _build/ 369 | apidocs/ 370 | 371 | debug.py 372 | 373 | # TODO: write implementation note 374 | docs/ 375 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Work around for some misconfigured virtualenv 2 | # https://github.com/pre-commit/pre-commit/issues/1375 3 | default_language_version: 4 | python: python3 5 | 6 | fail_fast: false 7 | 8 | ci: 9 | skip: [cargo-fmt, cargo-deny, cargo-check, cargo-clippy] 10 | 11 | repos: 12 | # Misc 13 | - repo: https://github.com/pre-commit/pre-commit-hooks 14 | rev: v5.0.0 15 | hooks: 16 | - id: trailing-whitespace 17 | - id: end-of-file-fixer 18 | - id: check-yaml 19 | - id: check-toml 20 | - id: check-json 21 | - id: check-added-large-files 22 | - id: check-merge-conflict 23 | - id: detect-private-key 24 | - repo: https://github.com/executablebooks/mdformat 25 | rev: 0.7.22 26 | hooks: 27 | - id: mdformat 28 | additional_dependencies: 29 | - mdformat-gfm 30 | - repo: https://github.com/python-jsonschema/check-jsonschema 31 | rev: 0.33.0 32 | hooks: 33 | - id: check-github-workflows 34 | - repo: https://github.com/crate-ci/typos 35 | rev: v1 36 | hooks: 37 | - id: typos 38 | # Rust 39 | - repo: local 40 | hooks: 41 | - id: cargo-fmt 42 | name: cargo fmt 43 | entry: cargo fmt -- 44 | language: system 45 | types: [rust] 46 | pass_filenames: false # This makes it a lot faster 47 | - id: cargo-deny # cargo install --locked cargo-deny 48 | name: cargo deny 49 | entry: cargo deny --all-features check -- 50 | language: system 51 | pass_filenames: false 52 | stages: [manual] # because it's slow 53 | - id: cargo-check 54 | name: cargo check 55 | entry: cargo check --all-features --all-targets -- 56 | language: system 57 | pass_filenames: false 58 | types: [rust] 59 | stages: [manual] # because it's slow 60 | - id: cargo-clippy 61 | name: cargo clippy 62 | entry: cargo clippy -- 63 | language: system 64 | pass_filenames: false 65 | types: [rust] 66 | stages: [manual] # because it's slow 67 | # Python 68 | - repo: https://github.com/astral-sh/ruff-pre-commit 69 | rev: v0.11.11 70 | hooks: 71 | - id: ruff-format 72 | args: ["--config", "moyopy/pyproject.toml"] 73 | - id: ruff 74 | args: [ "--fix", "--show-fixes", "--config", "moyopy/pyproject.toml"] 75 | types_or: [python, pyi] 76 | - repo: local 77 | hooks: 78 | - id: clear-notebook-output 79 | name: Clear Jupyter Notebook Outputs 80 | entry: jupyter nbconvert --clear-output --inplace 81 | language: python 82 | additional_dependencies: [jupyter] 83 | types: [jupyter] 84 | files: \.ipynb$ 85 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "moyo", 5 | "moyopy", 6 | ] 7 | 8 | resolver = "2" 9 | 10 | [workspace.package] 11 | authors = ["Kohei Shinohara "] 12 | description = "Library for Crystal Symmetry in Rust" 13 | edition = "2021" 14 | version = "0.4.3" 15 | license = "MIT OR Apache-2.0" 16 | repository = "https://github.com/spglib/moyo" 17 | 18 | [workspace.dependencies] 19 | nalgebra = { version = "0.33", features = ["serde-serialize"] } 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = "1.0" 22 | approx = "0.5" 23 | log = { version = "0.4", features = ["release_max_level_debug"] } 24 | 25 | [workspace.metadata.release] 26 | allow-branch = ["main"] 27 | shared-version = true 28 | tag-prefix = "" 29 | tag = false 30 | 31 | [profile.test] 32 | opt-level = 1 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kohei Shinohara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moyo 2 | 3 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/spglib/moyo/main.svg)](https://results.pre-commit.ci/latest/github/spglib/moyo/main) 4 | [![image](https://img.shields.io/pypi/l/moyopy.svg)](https://pypi.python.org/pypi/moyopy) 5 | [![moyo at crates.io](https://img.shields.io/crates/v/moyo.svg)](https://img.shields.io/crates/v/moyo) 6 | [![image](https://img.shields.io/pypi/v/moyopy.svg)](https://pypi.python.org/pypi/moyopy) 7 | 8 | A fast and robust crystal symmetry finder, written in Rust. 9 | 10 |
11 |
12 | 13 |
14 |
Several times faster symmetry detection than Spglib for Materials Project dataset
15 |
16 | 17 | - Rust support available via [crates.io](https://crates.io/crates/moyo): [Docs](https://docs.rs/moyo/latest/moyo/) 18 | - Python support available via [PyPI](https://pypi.org/project/moyopy/): [Docs](https://spglib.github.io/moyo/python/) 19 | 20 | ## Interfaces 21 | 22 | - [Rust](moyo/README.md): core implementation 23 | - [Python](moyopy/README.md) 24 | 25 | ## Dev 26 | 27 | ```shell 28 | cargo install cargo-release cargo-edit cargo-deny 29 | ``` 30 | 31 | ### How to release 32 | 33 | 1. `cargo install cargo-release cargo-edit cargo-semver-checks` 34 | 1. `cargo semver-checks` to lint a new release 35 | 1. `cargo set-version --bump patch` for patch version increment 36 | 1. Write change log and git-commit 37 | 1. `cargo release --execute` (If you already release the package in crates.io, run `cargo release --execute --no-publish`) 38 | 39 | ### Debugging 40 | 41 | ```shell 42 | RUST_LOG=debug cargo test -- --nocapture 43 | ``` 44 | 45 | ## Acknowledgments 46 | 47 | We thank Dr. Yusuke Seto for providing the crystallographic database. 48 | We thank Juan Rodríguez-Carvajal for providing the magnetic space-group database. 49 | -------------------------------------------------------------------------------- /bench/mp/get_mp.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from mp_api.client import MPRester 5 | from pymatgen.core import Structure 6 | 7 | import moyopy 8 | 9 | 10 | def main(): 11 | material_id = "mp-550745" 12 | print(f"{material_id=}") 13 | with MPRester(api_key=os.environ.get("MP_API_KEY")) as mpr: 14 | doc = mpr.materials.summary.search(material_ids=[material_id])[0] 15 | structure: Structure = doc.structure 16 | 17 | basis = structure.lattice.matrix 18 | positions = structure.frac_coords 19 | numbers = [site.specie.Z for site in structure] 20 | 21 | moyopy_cell = moyopy.Cell(basis.tolist(), positions.tolist(), numbers) 22 | with open(f"{material_id}.json", "w") as f: 23 | f.write(moyopy_cell.serialize_json()) 24 | 25 | for symprec in [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 3e-1]: 26 | moyopy_dataset = moyopy.MoyoDataset(moyopy_cell, symprec=symprec) 27 | print(symprec, moyopy_dataset.number) 28 | 29 | 30 | if __name__ == "__main__": 31 | assert load_dotenv() 32 | main() 33 | -------------------------------------------------------------------------------- /bench/mp/mp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spglib/moyo/e3be55a41ab067de2f14946fa848ec69c4fbba26/bench/mp/mp.png -------------------------------------------------------------------------------- /bench/mp/mp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from time import perf_counter 4 | 5 | import click 6 | import pandas as pd 7 | from matbench_discovery.data import DataFiles 8 | from pymatgen.entries.computed_entries import ComputedStructureEntry 9 | from pymatviz.enums import Key 10 | from spglib import get_symmetry_dataset 11 | from tqdm.auto import tqdm 12 | 13 | import moyopy 14 | from moyopy.interface import MoyoAdapter 15 | 16 | SYMPREC_LIST = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1] 17 | 18 | 19 | @click.group() 20 | def cli(): ... 21 | 22 | 23 | @cli.command() 24 | def perf_spglib(): 25 | data_path = DataFiles.mp_computed_structure_entries.path 26 | df = pd.read_json(data_path).set_index(Key.mat_id) 27 | 28 | all_stats = [] 29 | 30 | with tqdm(df.iterrows(), total=len(df)) as pbar: 31 | for material_id, row in pbar: 32 | structure = ComputedStructureEntry.from_dict(row["entry"]).structure 33 | basis = structure.lattice.matrix 34 | positions = structure.frac_coords 35 | numbers = [site.specie.Z for site in structure] 36 | spglib_cell = (basis, positions, numbers) 37 | 38 | for i, symprec in enumerate(SYMPREC_LIST): 39 | try: 40 | start = perf_counter() 41 | spglib_dataset = get_symmetry_dataset(spglib_cell, symprec=symprec) 42 | time_spglib = perf_counter() - start 43 | 44 | all_stats.append( 45 | { 46 | "id": f"{material_id}_{i}", 47 | "material_id": material_id, 48 | "symprec": symprec, 49 | "time_spglib": time_spglib, 50 | "num_atoms": len(numbers), 51 | "number_spglib": spglib_dataset["number"], 52 | } 53 | ) 54 | except: # noqa: E722 55 | print(f"Abort: {material_id=} {symprec=}") 56 | 57 | df_stats = pd.DataFrame(all_stats) 58 | df_stats.to_json("stats_spglib.json") 59 | 60 | 61 | @cli.command() 62 | def perf_moyopy(): 63 | data_path = DataFiles.mp_computed_structure_entries.path 64 | df = pd.read_json(data_path).set_index(Key.mat_id) 65 | 66 | all_stats = [] 67 | 68 | with tqdm(df.iterrows(), total=len(df)) as pbar: 69 | for material_id, row in pbar: 70 | structure = ComputedStructureEntry.from_dict(row["entry"]).structure 71 | numbers = [site.specie.Z for site in structure] 72 | moyopy_cell = MoyoAdapter.from_structure(structure) 73 | 74 | for i, symprec in enumerate(SYMPREC_LIST): 75 | try: 76 | start = perf_counter() 77 | moyopy_dataset = moyopy.MoyoDataset(moyopy_cell, symprec=symprec) 78 | time_moyopy = perf_counter() - start 79 | 80 | all_stats.append( 81 | { 82 | "id": f"{material_id}_{i}", 83 | "material_id": material_id, 84 | "symprec": symprec, 85 | "time_moyopy": time_moyopy, 86 | "num_atoms": len(numbers), 87 | "number_moyopy": moyopy_dataset.number, 88 | } 89 | ) 90 | except: # noqa: E722 91 | print(f"Abort: {material_id=} {symprec=}") 92 | 93 | with open(f"{material_id}.json", "w") as f: 94 | f.write(moyopy_cell.serialize_json()) 95 | return 96 | 97 | df_stats = pd.DataFrame(all_stats) 98 | df_stats.to_json("stats_moyopy.json") 99 | 100 | 101 | if __name__ == "__main__": 102 | cli() 103 | -------------------------------------------------------------------------------- /bench/requirements.txt: -------------------------------------------------------------------------------- 1 | matbench-discovery==1.3.1 2 | ipython 3 | nbformat 4 | mp_api 5 | seaborn 6 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set shell := ["zsh", "-uc"] 2 | set positional-arguments 3 | 4 | default: 5 | just --list 6 | 7 | pre-commit: 8 | pre-commit run --all-files 9 | 10 | pre-commit-all: 11 | pre-commit run --all-files --hook-stage manual 12 | 13 | clean: 14 | rm moyopy/python/moyopy/*.so 15 | rm -r moyopy/docs/_build 16 | 17 | ################################################################################ 18 | # Rust 19 | ################################################################################ 20 | 21 | [group('rust')] 22 | test: 23 | cargo test 24 | 25 | [group('rust')] 26 | doc: 27 | cargo doc --open 28 | 29 | [group('rust')] 30 | upgrade: 31 | cargo upgrade -i 32 | 33 | [group('rust')] 34 | [working-directory: 'moyo'] 35 | profile: 36 | CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --test test_moyo_dataset --root 37 | 38 | ################################################################################ 39 | # Python 40 | ################################################################################ 41 | 42 | [group('python')] 43 | py-build: 44 | maturin develop --release --manifest-path moyopy/Cargo.toml 45 | 46 | [group('python')] 47 | py-install: py-build 48 | python -m pip install -e "moyopy[dev]" 49 | 50 | [group('python')] 51 | py-test: 52 | python -m pytest -v moyopy/python/tests 53 | 54 | [group('python')] 55 | py-docs: 56 | sphinx-autobuild moyopy/docs moyopy/docs/_build 57 | -------------------------------------------------------------------------------- /moyo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moyo" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [package.metadata.release] 11 | tag = true 12 | 13 | [lib] 14 | name = "moyo" 15 | 16 | [dependencies] 17 | nalgebra.workspace = true 18 | serde.workspace = true 19 | serde_json.workspace = true 20 | approx.workspace = true 21 | log.workspace = true 22 | itertools = "0.14" 23 | thiserror = "2.0" 24 | union-find = "0.4" 25 | strum = "0.27" 26 | strum_macros = "0.27" 27 | kiddo = "5.0.3" 28 | once_cell = "1.21.3" 29 | 30 | [dev-dependencies] 31 | rand = "0.9" 32 | rstest = "0.25" 33 | criterion = { version = "0.5", features = ["html_reports"] } 34 | env_logger = "0.11" 35 | test-log = "0.2" 36 | 37 | [[bench]] 38 | name = "translation_search" 39 | path = "benches/translation_search.rs" 40 | harness = false 41 | 42 | [[bench]] 43 | name = "dataset" 44 | path = "benches/dataset.rs" 45 | harness = false 46 | -------------------------------------------------------------------------------- /moyo/README.md: -------------------------------------------------------------------------------- 1 | # moyo (Rust) 2 | 3 | [![CI](https://github.com/spglib/moyo/actions/workflows/ci-rust.yaml/badge.svg)](https://github.com/spglib/moyo/actions/workflows/ci-rust.yaml) 4 | [![moyo at crates.io](https://img.shields.io/crates/v/moyo.svg)](https://img.shields.io/crates/v/moyo) 5 | 6 | The core implementation of moyo in Rust 7 | 8 | - Crates.io: https://docs.rs/moyo/latest/moyo/ 9 | - Document: https://docs.rs/moyo/latest/moyo/ 10 | 11 | ## Module dependency 12 | 13 | ``` 14 | math <- base <- data <- identify <- standardize <- lib 15 | ^---- search <--------------| 16 | ``` 17 | 18 | ## Goals 19 | 20 | - Find symmetry operations of a given crystal structure, identify its crystallographic group, and symmetrize the given crystal structure 21 | - Well-defined tolerance for finding symmetry operations 22 | - No dependency on existing symmetry-finder packages 23 | - Simplify crystal symmetry algorithm by extensively exploiting the group structures of crystallographic groups 24 | 25 | ## Non-goals 26 | 27 | - Crystallographic groups in other than three dimensions 28 | - Matching two similar crystal structures 29 | -------------------------------------------------------------------------------- /moyo/benches/dataset.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | 3 | use serde_json; 4 | use std::fs; 5 | use std::path::Path; 6 | 7 | use moyo::base::{AngleTolerance, Cell}; 8 | use moyo::data::Setting; 9 | use moyo::MoyoDataset; 10 | 11 | pub fn benchmark(c: &mut Criterion) { 12 | let path = Path::new("tests/assets/mp-1201492.json"); 13 | let cell: Cell = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); 14 | let symprec = 1e-4; 15 | let angle_tolerance = AngleTolerance::Default; 16 | let setting = Setting::Standard; 17 | c.bench_function("dataset_clathrate_Si", |b| { 18 | b.iter(|| MoyoDataset::new(&cell, symprec, angle_tolerance, setting)) 19 | }); 20 | } 21 | 22 | criterion_group!(benches, benchmark); 23 | criterion_main!(benches); 24 | -------------------------------------------------------------------------------- /moyo/benches/translation_search.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; 2 | 3 | use nalgebra::{matrix, vector}; 4 | 5 | use moyo::base::{Cell, Lattice, Position}; 6 | use moyo::search::{solve_correspondence, solve_correspondence_naive, PeriodicKdTree}; 7 | 8 | /// O(num_atoms^3) 9 | fn naive(reduced_cell: &Cell) { 10 | let num_atoms = reduced_cell.num_atoms(); 11 | let symprec = 1e-5; 12 | for j in 0..num_atoms { 13 | let translation = reduced_cell.positions[j] - reduced_cell.positions[0]; 14 | let new_positions: Vec = reduced_cell 15 | .positions 16 | .iter() 17 | .map(|pos| pos + translation) 18 | .collect(); 19 | 20 | solve_correspondence_naive(reduced_cell, &new_positions, symprec); 21 | } 22 | } 23 | 24 | /// O(num_atoms^2 * log(num_atoms)) 25 | fn kdtree(reduced_cell: &Cell) { 26 | let num_atoms = reduced_cell.num_atoms(); 27 | let symprec = 1e-5; 28 | let pkdtree = PeriodicKdTree::new(reduced_cell, symprec); 29 | for j in 0..num_atoms { 30 | let translation = reduced_cell.positions[j] - reduced_cell.positions[0]; 31 | let new_positions: Vec = reduced_cell 32 | .positions 33 | .iter() 34 | .map(|pos| pos + translation) 35 | .collect(); 36 | 37 | solve_correspondence(&pkdtree, reduced_cell, &new_positions); 38 | } 39 | } 40 | 41 | fn cell_for_benchmark(n: usize) -> Cell { 42 | let mut positions = vec![]; 43 | let mut numbers = vec![]; 44 | for i in 0..n { 45 | for j in 0..n { 46 | for k in 0..n { 47 | positions.push(vector![ 48 | i as f64 / n as f64, 49 | j as f64 / n as f64, 50 | k as f64 / n as f64 51 | ]); 52 | numbers.push(0); 53 | } 54 | } 55 | } 56 | 57 | Cell::new( 58 | Lattice::new(matrix![ 59 | n as f64, 0.0, 0.0; 60 | 0.0, n as f64, 0.0; 61 | 0.0, 0.0, n as f64; 62 | ]), 63 | positions, 64 | numbers, 65 | ) 66 | } 67 | 68 | pub fn benchmark(c: &mut Criterion) { 69 | let mut group = c.benchmark_group("translation search"); 70 | for n in 1..=8 { 71 | let cell = cell_for_benchmark(n); 72 | group.throughput(Throughput::Elements(cell.num_atoms() as u64)); 73 | group.bench_with_input(BenchmarkId::new("naive", n), &cell, |b, cell| { 74 | b.iter(|| naive(&cell)); 75 | }); 76 | group.bench_with_input(BenchmarkId::new("kdtree", n), &cell, |b, cell| { 77 | b.iter(|| kdtree(&cell)); 78 | }); 79 | } 80 | group.finish(); 81 | } 82 | 83 | criterion_group!(benches, benchmark); 84 | criterion_main!(benches); 85 | -------------------------------------------------------------------------------- /moyo/src/base.rs: -------------------------------------------------------------------------------- 1 | mod action; 2 | mod cell; 3 | mod error; 4 | mod lattice; 5 | mod magnetic_cell; 6 | mod operation; 7 | mod permutation; 8 | mod tolerance; 9 | mod transformation; 10 | 11 | pub use action::RotationMagneticMomentAction; 12 | pub use cell::{AtomicSpecie, Cell, Position}; 13 | pub use error::MoyoError; 14 | pub use lattice::Lattice; 15 | pub use magnetic_cell::{Collinear, MagneticCell, MagneticMoment, NonCollinear}; 16 | pub use operation::{ 17 | MagneticOperation, MagneticOperations, Operation, Operations, Rotation, Rotations, 18 | TimeReversal, Translation, 19 | }; 20 | pub use permutation::Permutation; 21 | pub use tolerance::AngleTolerance; 22 | pub use transformation::{Linear, OriginShift}; 23 | 24 | pub(super) use cell::orbits_from_permutations; 25 | pub(super) use operation::project_rotations; 26 | #[allow(unused_imports)] 27 | pub(super) use operation::traverse; 28 | pub(super) use tolerance::{MagneticSymmetryTolerances, SymmetryTolerances, ToleranceHandler, EPS}; 29 | pub(super) use transformation::{Transformation, UnimodularLinear, UnimodularTransformation}; 30 | -------------------------------------------------------------------------------- /moyo/src/base/action.rs: -------------------------------------------------------------------------------- 1 | /// How rotation acts on magnetic moments. 2 | #[derive(Debug, Clone, Copy)] 3 | pub enum RotationMagneticMomentAction { 4 | /// m -> R @ m 5 | Polar, 6 | /// m -> (det R) R @ m 7 | Axial, 8 | } 9 | -------------------------------------------------------------------------------- /moyo/src/base/cell.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use nalgebra::{Matrix3, Vector3}; 4 | use serde::{Deserialize, Serialize}; 5 | use union_find::{QuickFindUf, UnionByRank, UnionFind}; 6 | 7 | use super::lattice::Lattice; 8 | use super::permutation::Permutation; 9 | 10 | /// Fractional coordinates 11 | pub type Position = Vector3; 12 | /// Atomic number 13 | pub type AtomicSpecie = i32; 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | /// Representing a crystal structure 17 | pub struct Cell { 18 | /// Lattice of the cell. 19 | pub lattice: Lattice, 20 | /// `positions[i]` is a fractional coordinates of the i-th site. 21 | pub positions: Vec, 22 | /// `numbers[i]` is an atomic number of the i-th site. 23 | pub numbers: Vec, 24 | } 25 | 26 | impl Cell { 27 | pub fn new(lattice: Lattice, positions: Vec, numbers: Vec) -> Self { 28 | if positions.len() != numbers.len() { 29 | panic!("positions and numbers should be the same length"); 30 | } 31 | Self { 32 | lattice, 33 | positions, 34 | numbers, 35 | } 36 | } 37 | 38 | /// Return the number of atoms in the cell. 39 | pub fn num_atoms(&self) -> usize { 40 | self.positions.len() 41 | } 42 | 43 | /// Rotate the cell by the given rotation matrix. 44 | pub fn rotate(&self, rotation_matrix: &Matrix3) -> Self { 45 | Self::new( 46 | self.lattice.rotate(rotation_matrix), 47 | self.positions.clone(), 48 | self.numbers.clone(), 49 | ) 50 | } 51 | } 52 | 53 | /// If and only if the `i`th and `j`th atoms are equivalent, `orbits[i] == orbits[j]`. 54 | /// For each orbit, only one of them satisfies `orbits[i] == i`. 55 | pub fn orbits_from_permutations(num_atoms: usize, permutations: &[Permutation]) -> Vec { 56 | let mut uf = QuickFindUf::::new(num_atoms); 57 | for permutation in permutations.iter() { 58 | for i in 0..num_atoms { 59 | uf.union(i, permutation.apply(i)); 60 | } 61 | } 62 | let mut identifier_mapping = BTreeMap::new(); 63 | for i in 0..num_atoms { 64 | identifier_mapping.entry(uf.find(i)).or_insert(i); 65 | } 66 | 67 | (0..num_atoms) 68 | .map(|i| *identifier_mapping.get(&uf.find(i)).unwrap()) 69 | .collect() 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use std::panic; 75 | 76 | use nalgebra::{vector, Matrix3}; 77 | 78 | use super::{orbits_from_permutations, Cell}; 79 | use crate::base::lattice::Lattice; 80 | use crate::base::permutation::Permutation; 81 | 82 | #[test] 83 | fn test_orbits_from_permutations() { 84 | { 85 | let num_atoms = 3; 86 | let permutations = vec![Permutation::new(vec![2, 1, 0])]; 87 | assert_eq!( 88 | orbits_from_permutations(num_atoms, &permutations), 89 | vec![0, 1, 0] 90 | ); 91 | } 92 | { 93 | let num_atoms = 3; 94 | let permutations = vec![Permutation::new(vec![1, 0, 2])]; 95 | assert_eq!( 96 | orbits_from_permutations(num_atoms, &permutations), 97 | vec![0, 0, 2] 98 | ); 99 | } 100 | } 101 | 102 | #[test] 103 | fn test_mismatched_length() { 104 | let lattice = Lattice::new(Matrix3::::identity()); 105 | let positions = vec![vector![0.0, 0.0, 0.0], vector![0.5, 0.5, 0.5]]; 106 | let numbers = vec![1]; 107 | 108 | let result = panic::catch_unwind(|| Cell::new(lattice, positions, numbers)); 109 | assert!(result.is_err()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /moyo/src/base/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug, PartialEq, Eq, Clone, Copy)] 4 | /// Error types for the **moyo** library 5 | pub enum MoyoError { 6 | #[error("Minkowski reduction failed")] 7 | MinkowskiReductionError, 8 | #[error("Niggli reduction failed")] 9 | NiggliReductionError, 10 | #[error("Delaunay reduction failed")] 11 | DelaunayReductionError, 12 | #[error("Too small tolerance")] 13 | TooSmallToleranceError, 14 | #[error("Too large tolerance")] 15 | TooLargeToleranceError, 16 | #[error("Primitive cell search failed")] 17 | PrimitiveCellError, 18 | #[error("Primitive symmetry search failed")] 19 | PrimitiveSymmetrySearchError, 20 | #[error("Primitive magnetic symmetry search failed")] 21 | PrimitiveMagneticSymmetrySearchError, 22 | #[error("Bravais group search failed")] 23 | BravaisGroupSearchError, 24 | #[error("Geometric crystal class identification failed")] 25 | GeometricCrystalClassIdentificationError, 26 | #[error("Arithmetic crystal class identification failed")] 27 | ArithmeticCrystalClassIdentificationError, 28 | #[error("Space group type identification failed")] 29 | SpaceGroupTypeIdentificationError, 30 | #[error("Construct type identification failed")] 31 | ConstructTypeIdentificationError, 32 | #[error("Magnetic space group type identification failed")] 33 | MagneticSpaceGroupTypeIdentificationError, 34 | #[error("Standardization failed")] 35 | StandardizationError, 36 | #[error("Magnetic standardization failed")] 37 | MagneticStandardizationError, 38 | #[error("Wyckoff position assignment failed")] 39 | WyckoffPositionAssignmentError, 40 | #[error("Hall symbol parsing failed")] 41 | HallSymbolParsingError, 42 | #[error("Unknown hall_number")] 43 | UnknownHallNumberError, 44 | #[error("Unknown number")] 45 | UnknownNumberError, 46 | } 47 | -------------------------------------------------------------------------------- /moyo/src/base/lattice.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::{Matrix3, OMatrix, RowVector3, Vector3}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::math::{ 5 | delaunay_reduce, is_minkowski_reduced, is_niggli_reduced, minkowski_reduce, niggli_reduce, 6 | }; 7 | 8 | use super::error::MoyoError; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | /// Representing basis vectors of a lattice 12 | pub struct Lattice { 13 | /// basis.column(i) is the i-th basis vector 14 | pub basis: Matrix3, 15 | } 16 | 17 | impl Lattice { 18 | /// Create a new lattice from row basis vectors 19 | pub fn new(row_basis: Matrix3) -> Self { 20 | Self { 21 | basis: row_basis.transpose(), 22 | } 23 | } 24 | 25 | pub fn from_basis(basis: [[f64; 3]; 3]) -> Self { 26 | Self::new(OMatrix::from_rows(&[ 27 | RowVector3::from(basis[0]), 28 | RowVector3::from(basis[1]), 29 | RowVector3::from(basis[2]), 30 | ])) 31 | } 32 | 33 | /// Return Minkowski reduced lattice and transformation matrix to it 34 | pub fn minkowski_reduce(&self) -> Result<(Self, Matrix3), MoyoError> { 35 | let (reduced_basis, trans_mat) = minkowski_reduce(&self.basis); 36 | let reduced_lattice = Self { 37 | basis: reduced_basis, 38 | }; 39 | 40 | if !reduced_lattice.is_minkowski_reduced() { 41 | return Err(MoyoError::MinkowskiReductionError); 42 | } 43 | 44 | Ok((reduced_lattice, trans_mat)) 45 | } 46 | 47 | /// Return true if basis vectors are Minkowski reduced 48 | pub fn is_minkowski_reduced(&self) -> bool { 49 | is_minkowski_reduced(&self.basis) 50 | } 51 | 52 | /// Return Niggli reduced lattice and transformation matrix to it 53 | pub fn niggli_reduce(&self) -> Result<(Self, Matrix3), MoyoError> { 54 | let (reduced_lattice, trans_mat) = self.unchecked_niggli_reduce(); 55 | 56 | if !reduced_lattice.is_niggli_reduced() { 57 | return Err(MoyoError::NiggliReductionError); 58 | } 59 | 60 | Ok((reduced_lattice, trans_mat)) 61 | } 62 | 63 | /// Return Niggli reduced lattice and transformation matrix to it without checking reduction condition 64 | pub fn unchecked_niggli_reduce(&self) -> (Self, Matrix3) { 65 | let (reduced_basis, trans_mat) = niggli_reduce(&self.basis); 66 | let reduced_lattice = Self { 67 | basis: reduced_basis, 68 | }; 69 | (reduced_lattice, trans_mat) 70 | } 71 | 72 | /// Return true if basis vectors are Niggli reduced 73 | pub fn is_niggli_reduced(&self) -> bool { 74 | is_niggli_reduced(&self.basis) 75 | } 76 | 77 | /// Return Delaunay reduced lattice and transformation matrix to it 78 | pub fn delaunay_reduce(&self) -> Result<(Self, Matrix3), MoyoError> { 79 | let (reduced_basis, trans_mat) = delaunay_reduce(&self.basis); 80 | let reduced_lattice = Self { 81 | basis: reduced_basis, 82 | }; 83 | 84 | Ok((reduced_lattice, trans_mat)) 85 | } 86 | 87 | /// Return metric tensor of the basis vectors 88 | pub fn metric_tensor(&self) -> Matrix3 { 89 | self.basis.transpose() * self.basis 90 | } 91 | 92 | /// Return cartesian coordinates from the given fractional coordinates 93 | pub fn cartesian_coords(&self, fractional_coords: &Vector3) -> Vector3 { 94 | self.basis * fractional_coords 95 | } 96 | 97 | /// Return volume of the cell 98 | pub fn volume(&self) -> f64 { 99 | self.basis.determinant().abs() 100 | } 101 | 102 | #[allow(dead_code)] 103 | pub(crate) fn lattice_constant(&self) -> [f64; 6] { 104 | let g = self.metric_tensor(); 105 | let a = g[(0, 0)].sqrt(); 106 | let b = g[(1, 1)].sqrt(); 107 | let c = g[(2, 2)].sqrt(); 108 | let alpha = (g[(1, 2)] / (b * c)).acos().to_degrees(); 109 | let beta = (g[(0, 2)] / (a * c)).acos().to_degrees(); 110 | let gamma = (g[(0, 1)] / (a * b)).acos().to_degrees(); 111 | [a, b, c, alpha, beta, gamma] 112 | } 113 | 114 | /// Rotate the lattice by the given rotation matrix 115 | pub fn rotate(&self, rotation_matrix: &Matrix3) -> Self { 116 | Self { 117 | basis: rotation_matrix * self.basis, 118 | } 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use nalgebra::matrix; 125 | 126 | use super::Lattice; 127 | 128 | #[test] 129 | fn test_metric_tensor() { 130 | let lattice = Lattice::new(matrix![ 131 | 1.0, 1.0, 1.0; 132 | 1.0, 1.0, 0.0; 133 | 1.0, -1.0, 0.0; 134 | ]); 135 | let metric_tensor = lattice.metric_tensor(); 136 | assert_relative_eq!( 137 | metric_tensor, 138 | matrix![ 139 | 3.0, 2.0, 0.0; 140 | 2.0, 2.0, 0.0; 141 | 0.0, 0.0, 2.0; 142 | ] 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /moyo/src/base/magnetic_cell.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::Vector3; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::action::RotationMagneticMomentAction; 5 | use super::cell::{AtomicSpecie, Cell, Position}; 6 | use super::lattice::Lattice; 7 | use super::operation::{CartesianRotation, TimeReversal}; 8 | 9 | pub trait MagneticMoment: Sized + Clone { 10 | fn act_rotation( 11 | &self, 12 | cartesian_rotation: &CartesianRotation, 13 | action: RotationMagneticMomentAction, 14 | ) -> Self; 15 | 16 | fn act_time_reversal(&self, time_reversal: TimeReversal) -> Self; 17 | 18 | fn is_close(&self, other: &Self, mag_symprec: f64) -> bool; 19 | 20 | fn average(magnetic_moments: &[Self]) -> Self; 21 | 22 | fn act_magnetic_operation( 23 | &self, 24 | cartesian_rotation: &CartesianRotation, 25 | time_reversal: TimeReversal, 26 | action: RotationMagneticMomentAction, 27 | ) -> Self { 28 | let rotated = self.act_rotation(cartesian_rotation, action); 29 | rotated.act_time_reversal(time_reversal) 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub struct Collinear(pub f64); 35 | 36 | impl MagneticMoment for Collinear { 37 | fn act_rotation( 38 | &self, 39 | cartesian_rotation: &CartesianRotation, 40 | action: RotationMagneticMomentAction, 41 | ) -> Self { 42 | match action { 43 | RotationMagneticMomentAction::Polar => Self(self.0), 44 | RotationMagneticMomentAction::Axial => { 45 | let det = cartesian_rotation.determinant().round(); 46 | Self(det * self.0) 47 | } 48 | } 49 | } 50 | 51 | fn act_time_reversal(&self, time_reversal: TimeReversal) -> Self { 52 | if time_reversal { 53 | Self(-self.0) 54 | } else { 55 | Self(self.0) 56 | } 57 | } 58 | 59 | fn is_close(&self, other: &Self, mag_symprec: f64) -> bool { 60 | (self.0 - other.0).abs() < mag_symprec 61 | } 62 | 63 | fn average(magnetic_moments: &[Self]) -> Self { 64 | let sum = magnetic_moments.iter().map(|m| m.0).sum::(); 65 | Collinear(sum / magnetic_moments.len() as f64) 66 | } 67 | } 68 | 69 | #[derive(Debug, Clone, Serialize, Deserialize)] 70 | pub struct NonCollinear(pub Vector3); 71 | 72 | impl MagneticMoment for NonCollinear { 73 | fn act_rotation( 74 | &self, 75 | cartesian_rotation: &CartesianRotation, 76 | action: RotationMagneticMomentAction, 77 | ) -> Self { 78 | match action { 79 | RotationMagneticMomentAction::Polar => Self(cartesian_rotation * self.0), 80 | RotationMagneticMomentAction::Axial => { 81 | let det = cartesian_rotation.determinant().round(); 82 | Self(det * cartesian_rotation * self.0) 83 | } 84 | } 85 | } 86 | 87 | fn act_time_reversal(&self, time_reversal: TimeReversal) -> Self { 88 | if time_reversal { 89 | Self(-self.0) 90 | } else { 91 | Self(self.0) 92 | } 93 | } 94 | 95 | fn is_close(&self, other: &Self, mag_symprec: f64) -> bool { 96 | // L2 norm 97 | (self.0 - other.0).norm() < mag_symprec 98 | } 99 | 100 | fn average(magnetic_moments: &[Self]) -> Self { 101 | let sum = magnetic_moments 102 | .iter() 103 | .map(|m| m.0) 104 | .fold(Vector3::zeros(), |acc, x| acc + x); 105 | NonCollinear(sum / magnetic_moments.len() as f64) 106 | } 107 | } 108 | 109 | #[derive(Debug, Clone, Serialize, Deserialize)] 110 | pub struct MagneticCell { 111 | pub cell: Cell, 112 | pub magnetic_moments: Vec, 113 | } 114 | 115 | impl MagneticCell { 116 | pub fn new( 117 | lattice: Lattice, 118 | positions: Vec, 119 | numbers: Vec, 120 | magnetic_moments: Vec, 121 | ) -> Self { 122 | let cell = Cell::new(lattice, positions, numbers); 123 | Self::from_cell(cell, magnetic_moments) 124 | } 125 | 126 | pub fn from_cell(cell: Cell, magnetic_moments: Vec) -> Self { 127 | if cell.positions.len() != magnetic_moments.len() { 128 | panic!("positions and magnetic_moments should be the same length"); 129 | } 130 | 131 | Self { 132 | cell, 133 | magnetic_moments, 134 | } 135 | } 136 | 137 | pub fn num_atoms(&self) -> usize { 138 | self.cell.num_atoms() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /moyo/src/base/operation.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet, VecDeque}; 2 | use std::fmt; 3 | use std::ops::Mul; 4 | 5 | use nalgebra::base::{Matrix3, Vector3}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use super::lattice::Lattice; 9 | 10 | /// Rotation matrix in a crystallographic basis 11 | pub type Rotation = Matrix3; 12 | pub type CartesianRotation = Matrix3; 13 | /// Translation vector in a crystallographic basis 14 | pub type Translation = Vector3; 15 | /// Time reversal operation 16 | pub type TimeReversal = bool; 17 | 18 | #[derive(Clone, Deserialize, Serialize)] 19 | pub struct Operation { 20 | pub rotation: Rotation, 21 | pub translation: Translation, 22 | } 23 | 24 | impl Operation { 25 | pub fn new(rotation: Rotation, translation: Translation) -> Self { 26 | Self { 27 | rotation, 28 | translation, 29 | } 30 | } 31 | 32 | /// Return rotation matrix in cartesian coordinates with respect to the given lattice 33 | pub fn cartesian_rotation(&self, lattice: &Lattice) -> CartesianRotation { 34 | let inv_basis = lattice.basis.try_inverse().unwrap(); 35 | lattice.basis * self.rotation.map(|e| e as f64) * inv_basis 36 | } 37 | 38 | pub fn identity() -> Self { 39 | Self::new(Rotation::identity(), Translation::zeros()) 40 | } 41 | } 42 | 43 | impl fmt::Debug for Operation { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | let symbols = ["x", "y", "z"]; 46 | let xyz = (0..3) 47 | .map(|i| { 48 | let rows = (0..3) 49 | .filter_map(|j| { 50 | if self.rotation[(i, j)] != 0 { 51 | Some(format!( 52 | "{}{}{}", 53 | if self.rotation[(i, j)] > 0 { "+" } else { "-" }, 54 | if self.rotation[(i, j)].abs() != 1 { 55 | self.rotation[(i, j)].abs().to_string() 56 | } else { 57 | "".to_string() 58 | }, 59 | symbols[j] 60 | )) 61 | } else { 62 | None 63 | } 64 | }) 65 | .collect::>() 66 | .concat(); 67 | format!( 68 | "{}{}{}", 69 | rows, 70 | if self.translation[i] > 0.0 { "+" } else { "" }, 71 | if self.translation[i] != 0.0 { 72 | self.translation[i].to_string() 73 | } else { 74 | "".to_string() 75 | } 76 | ) 77 | }) 78 | .collect::>(); 79 | let ret = format!("{},{},{}", xyz[0], xyz[1], xyz[2]); 80 | write!(f, "{}", ret) 81 | } 82 | } 83 | 84 | impl Mul for Operation { 85 | type Output = Self; 86 | 87 | fn mul(self, rhs: Self) -> Self::Output { 88 | // (r1, t1) * (r2, t2) = (r1 * r2, r1 * t2 + t1) 89 | let new_rotation = self.rotation * rhs.rotation; 90 | let new_translation = self.rotation.map(|e| e as f64) * rhs.translation + self.translation; 91 | Self::new(new_rotation, new_translation) 92 | } 93 | } 94 | 95 | #[derive(Clone, Deserialize, Serialize)] 96 | pub struct MagneticOperation { 97 | pub operation: Operation, 98 | pub time_reversal: TimeReversal, 99 | } 100 | 101 | impl MagneticOperation { 102 | pub fn new(rotation: Rotation, translation: Translation, time_reversal: TimeReversal) -> Self { 103 | let operation = Operation::new(rotation, translation); 104 | Self::from_operation(operation, time_reversal) 105 | } 106 | 107 | pub fn from_operation(operation: Operation, time_reversal: TimeReversal) -> Self { 108 | Self { 109 | operation, 110 | time_reversal, 111 | } 112 | } 113 | 114 | pub fn identity() -> Self { 115 | Self::from_operation(Operation::identity(), false) 116 | } 117 | } 118 | 119 | impl fmt::Debug for MagneticOperation { 120 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 121 | if self.time_reversal { 122 | write!(f, "{:?}'", self.operation) 123 | } else { 124 | write!(f, "{:?}", self.operation) 125 | } 126 | } 127 | } 128 | 129 | impl Mul for MagneticOperation { 130 | type Output = Self; 131 | 132 | fn mul(self, rhs: Self) -> Self::Output { 133 | let new_operation = self.operation * rhs.operation; 134 | let new_time_reversal = self.time_reversal ^ rhs.time_reversal; 135 | Self::from_operation(new_operation, new_time_reversal) 136 | } 137 | } 138 | 139 | pub type Rotations = Vec; 140 | pub type Operations = Vec; 141 | pub type MagneticOperations = Vec; 142 | 143 | pub fn project_rotations(operations: &Operations) -> Rotations { 144 | operations.iter().map(|ops| ops.rotation).collect() 145 | } 146 | 147 | #[allow(dead_code)] 148 | /// Used for testing 149 | pub fn traverse(generators: &Rotations) -> Rotations { 150 | let mut queue = VecDeque::new(); 151 | let mut visited = HashSet::new(); 152 | let mut group = Vec::with_capacity(48); 153 | 154 | queue.push_back(Rotation::identity()); 155 | 156 | while !queue.is_empty() { 157 | let element = queue.pop_front().unwrap(); 158 | if visited.contains(&element) { 159 | continue; 160 | } 161 | visited.insert(element); 162 | group.push(element); 163 | 164 | for generator in generators { 165 | let product = element * generator; 166 | queue.push_back(product); 167 | } 168 | } 169 | 170 | group 171 | } 172 | 173 | #[cfg(test)] 174 | mod tests { 175 | use nalgebra::{matrix, vector}; 176 | use test_log::test; 177 | 178 | use super::*; 179 | use crate::base::{lattice::Lattice, Operation}; 180 | 181 | #[test] 182 | fn test_cartesian_rotations() { 183 | let lattice = Lattice::new(matrix![ 184 | 1.0, 0.0, 0.0; 185 | -0.5, f64::sqrt(3.0) / 2.0, 0.0; 186 | 0.0, 0.0, 1.0; 187 | ]); 188 | let operation = Operation::new( 189 | matrix![ 190 | 0, -1, 0; 191 | 1, -1, 0; 192 | 0, 0, 1; 193 | ], 194 | Translation::zeros(), 195 | ); 196 | 197 | let actual = operation.cartesian_rotation(&lattice); 198 | let expect = matrix![ 199 | -0.5, -f64::sqrt(3.0) / 2.0, 0.0; 200 | f64::sqrt(3.0) / 2.0, -0.5, 0.0; 201 | 0.0, 0.0, 1.0; 202 | ]; 203 | assert_relative_eq!(actual, expect); 204 | } 205 | 206 | #[test] 207 | fn test_operation_format() { 208 | let operation = Operation::new( 209 | matrix![ 210 | 1, 0, 0; 211 | 2, -1, 0; 212 | 0, 0, 1; 213 | ], 214 | vector![0.0, 0.25, -0.75], 215 | ); 216 | assert_eq!(format!("{:?}", operation), "+x,+2x-y+0.25,+z-0.75") 217 | } 218 | 219 | #[test] 220 | fn test_magnetic_operation_format() { 221 | let magnetic_operation = MagneticOperation::new( 222 | matrix![ 223 | 1, 0, 0; 224 | 1, -1, 0; 225 | 0, 0, 1; 226 | ], 227 | vector![0.0, 0.25, -0.75], 228 | true, 229 | ); 230 | assert_eq!(format!("{:?}", magnetic_operation), "+x,+x-y+0.25,+z-0.75'") 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /moyo/src/base/permutation.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Hash)] 4 | pub struct Permutation { 5 | mapping: Vec, 6 | } 7 | 8 | impl Permutation { 9 | pub fn new(mapping: Vec) -> Self { 10 | Self { mapping } 11 | } 12 | 13 | pub fn identity(size: usize) -> Self { 14 | Self::new((0..size).collect()) 15 | } 16 | 17 | pub fn size(&self) -> usize { 18 | self.mapping.len() 19 | } 20 | 21 | pub fn apply(&self, i: usize) -> usize { 22 | self.mapping[i] 23 | } 24 | 25 | pub fn inverse(&self) -> Self { 26 | let mut inv = vec![0; self.size()]; 27 | for (i, &j) in self.mapping.iter().enumerate() { 28 | inv[j] = i; 29 | } 30 | Self::new(inv) 31 | } 32 | } 33 | 34 | impl Mul for Permutation { 35 | type Output = Self; 36 | 37 | fn mul(self, rhs: Self) -> Self::Output { 38 | let mapping = (0..self.size()).map(|i| self.apply(rhs.apply(i))).collect(); 39 | Self::new(mapping) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use std::vec; 46 | 47 | use super::Permutation; 48 | 49 | #[test] 50 | fn test_permutation() { 51 | let permutation = Permutation::new(vec![1, 2, 0]); 52 | assert_eq!(permutation.apply(0), 1); 53 | assert_eq!(permutation.inverse(), Permutation::new(vec![2, 0, 1])); 54 | assert_eq!( 55 | permutation.clone() * permutation.inverse(), 56 | Permutation::identity(3) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /moyo/src/base/tolerance.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt::Debug; 4 | 5 | use super::error::MoyoError; 6 | 7 | pub const EPS: f64 = 1e-8; 8 | 9 | const INITIAL_SYMMETRY_SEARCH_STRIDE: f64 = 2.0; 10 | 11 | #[derive(Debug, Copy, Clone, Deserialize, Serialize)] 12 | /// Tolerance for angle in comparing basis vectors in symmetry search. 13 | pub enum AngleTolerance { 14 | /// Tolerance in radian. 15 | Radian(f64), 16 | /// Default tolerance same as Spglib. 17 | Default, 18 | } 19 | 20 | pub trait Tolerances { 21 | fn increase_tolerances(&self, stride: f64) -> Self; 22 | fn reduce_tolerances(&self, stride: f64) -> Self; 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct SymmetryTolerances { 27 | pub symprec: f64, 28 | pub angle_tolerance: AngleTolerance, 29 | } 30 | 31 | impl Tolerances for SymmetryTolerances { 32 | fn increase_tolerances(&self, stride: f64) -> Self { 33 | let symprec = self.symprec * stride; 34 | let angle_tolerance = if let AngleTolerance::Radian(angle) = self.angle_tolerance { 35 | AngleTolerance::Radian(angle * stride) 36 | } else { 37 | AngleTolerance::Default 38 | }; 39 | Self { 40 | symprec, 41 | angle_tolerance, 42 | } 43 | } 44 | 45 | fn reduce_tolerances(&self, stride: f64) -> Self { 46 | let symprec = self.symprec / stride; 47 | let angle_tolerance = if let AngleTolerance::Radian(angle) = self.angle_tolerance { 48 | AngleTolerance::Radian(angle / stride) 49 | } else { 50 | AngleTolerance::Default 51 | }; 52 | Self { 53 | symprec, 54 | angle_tolerance, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug, Clone)] 60 | pub struct MagneticSymmetryTolerances { 61 | pub symprec: f64, 62 | pub angle_tolerance: AngleTolerance, 63 | pub mag_symprec: f64, 64 | } 65 | 66 | impl Tolerances for MagneticSymmetryTolerances { 67 | fn increase_tolerances(&self, stride: f64) -> Self { 68 | let symprec = self.symprec * stride; 69 | let angle_tolerance = if let AngleTolerance::Radian(angle) = self.angle_tolerance { 70 | AngleTolerance::Radian(angle * stride) 71 | } else { 72 | AngleTolerance::Default 73 | }; 74 | let mag_symprec = self.mag_symprec * stride; 75 | Self { 76 | symprec, 77 | angle_tolerance, 78 | mag_symprec, 79 | } 80 | } 81 | 82 | fn reduce_tolerances(&self, stride: f64) -> Self { 83 | let symprec = self.symprec / stride; 84 | let angle_tolerance = if let AngleTolerance::Radian(angle) = self.angle_tolerance { 85 | AngleTolerance::Radian(angle / stride) 86 | } else { 87 | AngleTolerance::Default 88 | }; 89 | let mag_symprec = self.mag_symprec / stride; 90 | Self { 91 | symprec, 92 | angle_tolerance, 93 | mag_symprec, 94 | } 95 | } 96 | } 97 | 98 | pub struct ToleranceHandler { 99 | pub tolerances: T, 100 | stride: f64, 101 | prev_error: Option, 102 | } 103 | 104 | impl ToleranceHandler { 105 | pub fn new(tolerances: T) -> Self { 106 | Self { 107 | tolerances, 108 | stride: INITIAL_SYMMETRY_SEARCH_STRIDE, 109 | prev_error: None, 110 | } 111 | } 112 | 113 | pub fn update(&mut self, err: MoyoError) { 114 | // Update stride 115 | if self.prev_error.is_some() && self.prev_error != Some(err) { 116 | self.stride = self.stride.sqrt() 117 | } 118 | self.prev_error = Some(err); 119 | 120 | // Update tolerances 121 | self.tolerances = match err { 122 | MoyoError::TooSmallToleranceError => { 123 | let new_tolerances = self.tolerances.increase_tolerances(self.stride); 124 | debug!("Increase tolerances: {:?}", new_tolerances); 125 | new_tolerances 126 | } 127 | _ => { 128 | let new_tolerances = self.tolerances.reduce_tolerances(self.stride); 129 | debug!("Reduce tolerances: {:?}", new_tolerances); 130 | new_tolerances 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /moyo/src/data.rs: -------------------------------------------------------------------------------- 1 | mod arithmetic_crystal_class; 2 | mod centering; 3 | mod classification; 4 | mod hall_symbol; 5 | mod hall_symbol_database; 6 | mod magnetic_hall_symbol_database; 7 | mod magnetic_space_group; 8 | mod point_group; 9 | mod setting; 10 | mod wyckoff; 11 | 12 | pub use arithmetic_crystal_class::{ 13 | arithmetic_crystal_class_entry, ArithmeticCrystalClassEntry, ArithmeticNumber, 14 | }; 15 | pub use centering::Centering; 16 | pub use classification::{ 17 | BravaisClass, CrystalFamily, CrystalSystem, GeometricCrystalClass, LatticeSystem, 18 | }; 19 | pub use hall_symbol::{HallSymbol, MagneticHallSymbol}; 20 | pub use hall_symbol_database::{hall_symbol_entry, HallNumber, HallSymbolEntry, Number}; 21 | pub use magnetic_hall_symbol_database::{magnetic_hall_symbol_entry, MagneticHallSymbolEntry}; 22 | pub use magnetic_space_group::{ 23 | get_magnetic_space_group_type, ConstructType, MagneticSpaceGroupType, UNINumber, 24 | NUM_MAGNETIC_SPACE_GROUP_TYPES, 25 | }; 26 | pub use setting::Setting; 27 | 28 | pub(super) use arithmetic_crystal_class::iter_arithmetic_crystal_entry; 29 | pub(super) use magnetic_space_group::uni_number_range; 30 | pub(super) use point_group::PointGroupRepresentative; 31 | pub(super) use wyckoff::{iter_wyckoff_positions, WyckoffPosition, WyckoffPositionSpace}; 32 | -------------------------------------------------------------------------------- /moyo/src/data/centering.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::{Matrix3, Vector3}; 2 | use strum_macros::EnumIter; 3 | 4 | use crate::base::{Linear, Translation}; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, EnumIter)] 7 | pub enum Centering { 8 | P, // Primitive 9 | A, // A-face centered 10 | B, // B-face centered 11 | C, // C-face centered 12 | I, // Body centered 13 | R, // Rhombohedral (obverse setting) 14 | F, // Face centered 15 | } 16 | 17 | impl Centering { 18 | pub fn order(&self) -> usize { 19 | match self { 20 | Centering::P => 1, 21 | Centering::A => 2, 22 | Centering::B => 2, 23 | Centering::C => 2, 24 | Centering::I => 2, 25 | Centering::R => 3, 26 | Centering::F => 4, 27 | } 28 | } 29 | 30 | /// Transformation matrix from primitive to conventional cell. 31 | // Inverse matrices of https://github.com/spglib/spglib/blob/39a95560dd831c2d16f162126921ac1e519efa31/src/spacegroup.c#L373-L384 32 | pub fn linear(&self) -> Linear { 33 | match self { 34 | Centering::P => Linear::identity(), 35 | Centering::A => Linear::new( 36 | 1, 0, 0, // 37 | 0, 1, 1, // 38 | 0, -1, 1, // 39 | ), 40 | Centering::B => Linear::new( 41 | 1, 0, -1, // 42 | 0, 1, 0, // 43 | 1, 0, 1, // 44 | ), 45 | Centering::C => Linear::new( 46 | 1, -1, 0, // 47 | 1, 1, 0, // 48 | 0, 0, 1, // 49 | ), 50 | Centering::R => Linear::new( 51 | 1, 0, 1, // 52 | -1, 1, 1, // 53 | 0, -1, 1, // 54 | ), 55 | Centering::I => Linear::new( 56 | 0, 1, 1, // 57 | 1, 0, 1, // 58 | 1, 1, 0, // 59 | ), 60 | Centering::F => Linear::new( 61 | -1, 1, 1, // 62 | 1, -1, 1, // 63 | 1, 1, -1, // 64 | ), 65 | } 66 | } 67 | 68 | /// Transformation matrix from conventional to primitive cell. 69 | pub fn inverse(&self) -> Matrix3 { 70 | self.linear().map(|e| e as f64).try_inverse().unwrap() 71 | } 72 | 73 | pub fn lattice_points(&self) -> Vec> { 74 | match self { 75 | Centering::P => { 76 | vec![Translation::new(0.0, 0.0, 0.0)] 77 | } 78 | Centering::A => { 79 | vec![ 80 | Translation::new(0.0, 0.0, 0.0), 81 | Translation::new(0.0, 0.5, 0.5), 82 | ] 83 | } 84 | Centering::B => { 85 | vec![ 86 | Translation::new(0.0, 0.0, 0.0), 87 | Translation::new(0.5, 0.0, 0.5), 88 | ] 89 | } 90 | Centering::C => { 91 | vec![ 92 | Translation::new(0.0, 0.0, 0.0), 93 | Translation::new(0.5, 0.5, 0.0), 94 | ] 95 | } 96 | Centering::I => { 97 | vec![ 98 | Translation::new(0.0, 0.0, 0.0), 99 | Translation::new(0.5, 0.5, 0.5), 100 | ] 101 | } 102 | Centering::R => { 103 | // obverse setting 104 | vec![ 105 | Translation::new(0.0, 0.0, 0.0), 106 | Translation::new(2.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0), 107 | Translation::new(1.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0), 108 | ] 109 | } 110 | // Centering::H => { 111 | // vec![ 112 | // Translation::new(0.0, 0.0, 0.0), 113 | // Translation::new(2.0 / 3.0, 1.0 / 3.0, 0.0), 114 | // Translation::new(1.0 / 3.0, 2.0 / 3.0, 0.0), 115 | // ] 116 | // } 117 | Centering::F => { 118 | vec![ 119 | Translation::new(0.0, 0.0, 0.0), 120 | Translation::new(0.0, 0.5, 0.5), 121 | Translation::new(0.5, 0.0, 0.5), 122 | Translation::new(0.5, 0.5, 0.0), 123 | ] 124 | } 125 | } 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use strum::IntoEnumIterator; 132 | 133 | use super::*; 134 | use crate::base::Transformation; 135 | 136 | #[test] 137 | fn test_conventional_transformation_matrix() { 138 | for centering in Centering::iter() { 139 | assert_eq!( 140 | Transformation::from_linear(centering.linear()).size, 141 | centering.order() 142 | ); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /moyo/src/data/setting.rs: -------------------------------------------------------------------------------- 1 | use super::hall_symbol_database::{HallNumber, Number}; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq)] 4 | /// Preference for the setting of the space group. 5 | pub enum Setting { 6 | /// Specific Hall number from 1 to 530 7 | HallNumber(HallNumber), 8 | /// The setting of the smallest Hall number 9 | Spglib, 10 | /// Unique axis b, cell choice 1 for monoclinic, hexagonal axes for rhombohedral, and origin choice 2 for centrosymmetric space groups 11 | Standard, 12 | } 13 | 14 | const SPGLIB_HALL_NUMBERS: [HallNumber; 230] = [ 15 | 1, 2, 3, 6, 9, 18, 21, 30, 39, 57, 60, 63, 72, 81, 90, 108, 109, 112, 115, 116, 119, 122, 123, 16 | 124, 125, 128, 134, 137, 143, 149, 155, 161, 164, 170, 173, 176, 182, 185, 191, 197, 203, 209, 17 | 212, 215, 218, 221, 227, 228, 230, 233, 239, 245, 251, 257, 263, 266, 269, 275, 278, 284, 290, 18 | 292, 298, 304, 310, 313, 316, 322, 334, 335, 337, 338, 341, 343, 349, 350, 351, 352, 353, 354, 19 | 355, 356, 357, 358, 359, 361, 363, 364, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 20 | 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 21 | 396, 397, 398, 399, 400, 401, 402, 404, 406, 407, 408, 410, 412, 413, 414, 416, 418, 419, 420, 22 | 422, 424, 425, 426, 428, 430, 431, 432, 433, 435, 436, 438, 439, 440, 441, 442, 443, 444, 446, 23 | 447, 448, 449, 450, 452, 454, 455, 456, 457, 458, 460, 462, 463, 464, 465, 466, 467, 468, 469, 24 | 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 25 | 489, 490, 491, 492, 493, 494, 495, 497, 498, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 26 | 510, 511, 512, 513, 514, 515, 516, 517, 518, 520, 521, 523, 524, 525, 527, 529, 530, 27 | ]; 28 | const STANDARD_HALL_NUMBERS: [HallNumber; 230] = [ 29 | 1, 2, 3, 6, 9, 18, 21, 30, 39, 57, 60, 63, 72, 81, 90, 108, 109, 112, 115, 116, 119, 122, 123, 30 | 124, 125, 128, 134, 137, 143, 149, 155, 161, 164, 170, 173, 176, 182, 185, 191, 197, 203, 209, 31 | 212, 215, 218, 221, 227, 229, 230, 234, 239, 245, 251, 257, 263, 266, 269, 275, 279, 284, 290, 32 | 292, 298, 304, 310, 313, 316, 323, 334, 336, 337, 338, 341, 343, 349, 350, 351, 352, 353, 354, 33 | 355, 356, 357, 358, 360, 362, 363, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 34 | 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 35 | 396, 397, 398, 399, 400, 401, 403, 405, 406, 407, 409, 411, 412, 413, 415, 417, 418, 419, 421, 36 | 423, 424, 425, 427, 429, 430, 431, 432, 433, 435, 436, 438, 439, 440, 441, 442, 443, 444, 446, 37 | 447, 448, 449, 450, 452, 454, 455, 456, 457, 458, 460, 462, 463, 464, 465, 466, 467, 468, 469, 38 | 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 39 | 489, 490, 491, 492, 493, 494, 496, 497, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 40 | 510, 511, 512, 513, 514, 515, 516, 517, 519, 520, 522, 523, 524, 526, 528, 529, 530, 41 | ]; 42 | 43 | impl Setting { 44 | pub fn hall_numbers(&self) -> Vec { 45 | match self { 46 | Setting::HallNumber(hall_number) => vec![*hall_number], 47 | Setting::Spglib => SPGLIB_HALL_NUMBERS.to_vec(), 48 | Setting::Standard => STANDARD_HALL_NUMBERS.to_vec(), 49 | } 50 | } 51 | 52 | pub fn hall_number(&self, number: Number) -> Option { 53 | match self { 54 | Setting::HallNumber(_) => None, 55 | Setting::Spglib => SPGLIB_HALL_NUMBERS.get(number as usize - 1).cloned(), 56 | Setting::Standard => STANDARD_HALL_NUMBERS.get(number as usize - 1).cloned(), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /moyo/src/identify.rs: -------------------------------------------------------------------------------- 1 | mod magnetic_space_group; 2 | mod normalizer; 3 | mod point_group; 4 | mod rotation_type; 5 | mod space_group; 6 | 7 | pub(super) use magnetic_space_group::{ 8 | family_space_group_from_magnetic_space_group, 9 | primitive_maximal_space_subgroup_from_magnetic_space_group, MagneticSpaceGroup, 10 | }; 11 | pub(super) use space_group::SpaceGroup; 12 | -------------------------------------------------------------------------------- /moyo/src/identify/normalizer.rs: -------------------------------------------------------------------------------- 1 | use super::point_group::{iter_trans_mat_basis, iter_unimodular_trans_mat}; 2 | use super::rotation_type::identify_rotation_type; 3 | use super::space_group::match_origin_shift; 4 | use crate::base::{project_rotations, Operations, UnimodularTransformation}; 5 | 6 | /// Return integral normalizer of the point group representative up to its centralizer. 7 | 8 | /// Integral normalizers for all arithmetic point groups up to their centralizers. 9 | 10 | /// Generate integral normalizer of the given space group up to its centralizer. 11 | /// Because the factor group of the integral normalizer by the centralizer is isomorphic to a finite permutation group, the output is guaranteed to be finite. 12 | pub fn integral_normalizer( 13 | prim_operations: &Operations, 14 | prim_generators: &Operations, 15 | epsilon: f64, 16 | ) -> Vec { 17 | let prim_rotations = project_rotations(prim_operations); 18 | let prim_rotation_generators = project_rotations(prim_generators); 19 | 20 | let rotation_types = prim_rotations 21 | .iter() 22 | .map(identify_rotation_type) 23 | .collect::>(); 24 | 25 | let mut conjugators = vec![]; 26 | for trans_mat_basis in 27 | iter_trans_mat_basis(prim_rotations, rotation_types, prim_rotation_generators) 28 | { 29 | for prim_trans_mat in iter_unimodular_trans_mat(trans_mat_basis) { 30 | if let Some(origin_shift) = 31 | match_origin_shift(prim_operations, &prim_trans_mat, prim_generators, epsilon) 32 | { 33 | conjugators.push(UnimodularTransformation::new(prim_trans_mat, origin_shift)); 34 | break; 35 | } 36 | } 37 | } 38 | conjugators 39 | } 40 | -------------------------------------------------------------------------------- /moyo/src/identify/rotation_type.rs: -------------------------------------------------------------------------------- 1 | use crate::base::Rotation; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq)] 4 | pub enum RotationType { 5 | Rotation1, // 1 6 | Rotation2, // 2 7 | Rotation3, // 3 8 | Rotation4, // 4 9 | Rotation6, // 6 10 | RotoInversion1, // -1 = S2 11 | RotoInversion2, // -2 = m = S1 12 | RotoInversion3, // -3 = S6^-1 13 | RotoInversion4, // -4 = S4^-1 14 | RotoInversion6, // -6 = S3^-1 15 | } 16 | 17 | pub fn identify_rotation_type(rotation: &Rotation) -> RotationType { 18 | let tr = rotation.trace(); 19 | let det = rotation.map(|e| e as f64).determinant().round() as i32; 20 | 21 | match (tr, det) { 22 | (3, 1) => RotationType::Rotation1, 23 | (-1, 1) => RotationType::Rotation2, 24 | (0, 1) => RotationType::Rotation3, 25 | (1, 1) => RotationType::Rotation4, 26 | (2, 1) => RotationType::Rotation6, 27 | (-3, -1) => RotationType::RotoInversion1, 28 | (1, -1) => RotationType::RotoInversion2, 29 | (0, -1) => RotationType::RotoInversion3, 30 | (-1, -1) => RotationType::RotoInversion4, 31 | (-2, -1) => RotationType::RotoInversion6, 32 | _ => unreachable!("Unknown rotation type"), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /moyo/src/math.rs: -------------------------------------------------------------------------------- 1 | mod cycle_checker; 2 | mod delaunay; 3 | mod elementary; 4 | mod hnf; 5 | mod integer_system; 6 | mod minkowski; 7 | mod niggli; 8 | mod snf; 9 | 10 | pub use hnf::HNF; 11 | pub use snf::SNF; 12 | 13 | pub(super) use delaunay::delaunay_reduce; 14 | pub(super) use integer_system::sylvester3; 15 | pub(super) use minkowski::{is_minkowski_reduced, minkowski_reduce}; 16 | pub(super) use niggli::{is_niggli_reduced, niggli_reduce}; 17 | -------------------------------------------------------------------------------- /moyo/src/math/cycle_checker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use nalgebra::base::allocator::Allocator; 4 | use nalgebra::{DefaultAllocator, Dim, OMatrix}; 5 | 6 | /// Record transformation matrices during lattice reduction 7 | #[derive(Debug)] 8 | pub struct CycleChecker 9 | where 10 | DefaultAllocator: Allocator, 11 | { 12 | visited: HashSet>, 13 | } 14 | 15 | impl CycleChecker 16 | where 17 | DefaultAllocator: Allocator, 18 | { 19 | pub fn new() -> Self { 20 | Self { 21 | visited: HashSet::new(), 22 | } 23 | } 24 | 25 | /// If `matrix` is not visited, insert it and return true. 26 | pub fn insert(&mut self, matrix: &OMatrix) -> bool { 27 | if self.visited.contains(matrix) { 28 | return false; 29 | } 30 | self.visited.insert(matrix.clone()); 31 | true 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use nalgebra::matrix; 38 | 39 | use super::CycleChecker; 40 | 41 | #[test] 42 | fn test_cycle_checker() { 43 | let mut cc = CycleChecker::new(); 44 | assert_eq!(cc.insert(&matrix![1, 0, 0; 0, 1, 0; 0, 0, 1]), true); 45 | assert_eq!(cc.insert(&matrix![1, 0, 0; 0, 1, 0; 0, 0, 1]), false); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /moyo/src/math/delaunay.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::{vector, Matrix3, Vector3, U3}; 2 | 3 | use super::cycle_checker::CycleChecker; 4 | use super::elementary::{adding_column_matrix, changing_column_sign_matrix}; 5 | 6 | const EPS: f64 = 1e-8; 7 | 8 | /// basis is column-wise 9 | pub fn delaunay_reduce(basis: &Matrix3) -> (Matrix3, Matrix3) { 10 | let mut reduced_basis = *basis; 11 | let mut trans_mat = Matrix3::::identity(); 12 | 13 | let mut cc = CycleChecker::new(); 14 | loop { 15 | let superbase = superbase(&reduced_basis); 16 | 17 | let mut update = false; 18 | for i in 0..3 { 19 | if update { 20 | break; 21 | } 22 | for j in i + 1..4 { 23 | if superbase[i].dot(&superbase[j]) > EPS { 24 | let mut trans_mat_tmp = Matrix3::::identity(); 25 | for k in 0..3 { 26 | if (k == i) || (k == j) { 27 | continue; 28 | } 29 | trans_mat_tmp *= adding_column_matrix(U3, i, k, 1); 30 | } 31 | trans_mat_tmp *= changing_column_sign_matrix(U3, i); 32 | 33 | reduced_basis *= trans_mat_tmp.map(|e| e as f64); 34 | trans_mat *= trans_mat_tmp; 35 | update = true; 36 | break; 37 | } 38 | } 39 | } 40 | 41 | // If not updated or the new basis is already visited, stop the loop 42 | if !update || !cc.insert(&trans_mat) { 43 | break; 44 | } 45 | } 46 | 47 | // Select three shortest vectors from {b1, b2, b3, b4, b1 + b2, b2 + b3, b3 + b1} 48 | let basis_candidates = [ 49 | vector![1, 0, 0], 50 | vector![0, 1, 0], 51 | vector![0, 0, 1], 52 | vector![-1, -1, -1], 53 | vector![1, 1, 0], 54 | vector![0, 1, 1], 55 | vector![1, 0, 1], 56 | ]; 57 | let norms = basis_candidates 58 | .iter() 59 | .map(|&v| (reduced_basis * v.map(|e| e as f64)).norm()) 60 | .collect::>(); 61 | let mut argsort = (0..7).collect::>(); 62 | argsort.sort_by(|&i, &j| norms[i].partial_cmp(&norms[j]).unwrap()); 63 | 64 | let trans_mat_shortest = Matrix3::::from_columns(&[ 65 | basis_candidates[argsort[0]], 66 | basis_candidates[argsort[1]], 67 | basis_candidates[argsort[2]], 68 | ]); 69 | trans_mat *= trans_mat_shortest; 70 | reduced_basis *= trans_mat_shortest.map(|e| e as f64); 71 | 72 | // Preserve parity 73 | if trans_mat.map(|e| e as f64).determinant() < 0. { 74 | reduced_basis *= -1.; 75 | trans_mat *= -1; 76 | } 77 | 78 | (reduced_basis, trans_mat) 79 | } 80 | 81 | fn superbase(basis: &Matrix3) -> Vec> { 82 | let mut superbase = vec![]; 83 | let mut sum_vec = Vector3::::zeros(); 84 | for base in basis.column_iter() { 85 | superbase.push(base.clone_owned()); 86 | sum_vec += base; 87 | } 88 | superbase.push(-sum_vec); 89 | superbase 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use nalgebra::{vector, Matrix3}; 95 | use rand::prelude::*; 96 | use rand::rngs::StdRng; 97 | use rand::SeedableRng; 98 | 99 | use super::delaunay_reduce; 100 | 101 | #[test] 102 | fn test_delaunay_small() { 103 | { 104 | // https://github.com/spglib/spglib/blob/develop/test/functional/c/test_delaunay.cpp 105 | let basis = Matrix3::from_columns(&[ 106 | vector![ 107 | -2.2204639179669590, 108 | -4.4409278359339179, 109 | 179.8575773553236843 110 | ], 111 | vector![1.2819854407640749, 0.0, 103.8408207018900669], 112 | vector![10.5158083946732219, 0.0, 883.3279051525505565], 113 | ]); 114 | let (reduced_basis, trans_mat) = delaunay_reduce(&basis); 115 | assert_relative_eq!( 116 | basis * trans_mat.map(|e| e as f64), 117 | reduced_basis, 118 | epsilon = 1e-4 119 | ); 120 | } 121 | } 122 | 123 | #[test] 124 | fn test_delaunay_random() { 125 | let mut rng: StdRng = SeedableRng::from_seed([0; 32]); 126 | 127 | for _ in 0..256 { 128 | let basis = Matrix3::::new( 129 | rng.random::() as f64, 130 | rng.random::() as f64, 131 | rng.random::() as f64, 132 | rng.random::() as f64, 133 | rng.random::() as f64, 134 | rng.random::() as f64, 135 | rng.random::() as f64, 136 | rng.random::() as f64, 137 | rng.random::() as f64, 138 | ); 139 | let (reduced_basis, trans_mat) = delaunay_reduce(&basis); 140 | assert_relative_eq!( 141 | basis * trans_mat.map(|e| e as f64), 142 | reduced_basis, 143 | epsilon = 1e-4 144 | ); 145 | } 146 | 147 | for _ in 0..256 { 148 | let basis = Matrix3::::new( 149 | rng.random(), 150 | rng.random(), 151 | rng.random(), 152 | rng.random(), 153 | rng.random(), 154 | rng.random(), 155 | rng.random(), 156 | rng.random(), 157 | rng.random(), 158 | ); 159 | let (reduced_basis, trans_mat) = delaunay_reduce(&basis); 160 | assert_relative_eq!( 161 | basis * trans_mat.map(|e| e as f64), 162 | reduced_basis, 163 | epsilon = 1e-4 164 | ); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /moyo/src/math/elementary.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::allocator::Allocator; 2 | use nalgebra::{DefaultAllocator, Dim, OMatrix}; 3 | 4 | /// Return elementary matrix swapping the `col1`th and `col2`th columns 5 | pub fn swapping_column_matrix(dim: D, col1: usize, col2: usize) -> OMatrix 6 | where 7 | DefaultAllocator: Allocator, 8 | { 9 | let mut trans_mat = OMatrix::zeros_generic(dim, dim); 10 | for i in 0..dim.value() { 11 | if i == col1 { 12 | trans_mat[(col1, col2)] = 1; 13 | } else if i == col2 { 14 | trans_mat[(col2, col1)] = 1; 15 | } else { 16 | trans_mat[(i, i)] = 1 17 | } 18 | } 19 | trans_mat 20 | } 21 | 22 | /// Return elementary matrix adding the `k`-multiplied `col1`th column into the `col2`th column 23 | pub fn adding_column_matrix(dim: D, col1: usize, col2: usize, k: i32) -> OMatrix 24 | where 25 | DefaultAllocator: Allocator, 26 | { 27 | let mut trans_mat = OMatrix::identity_generic(dim, dim); 28 | for i in 0..dim.value() { 29 | if i == col1 { 30 | trans_mat[(col1, col2)] = k; 31 | } 32 | } 33 | trans_mat 34 | } 35 | 36 | /// Return elementary matrix changing sign of the `col`th column 37 | pub fn changing_column_sign_matrix(dim: D, col: usize) -> OMatrix 38 | where 39 | DefaultAllocator: Allocator, 40 | { 41 | let mut trans_mat = OMatrix::identity_generic(dim, dim); 42 | trans_mat[(col, col)] = -1; 43 | trans_mat 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use nalgebra::{matrix, U3}; 49 | 50 | use super::{adding_column_matrix, changing_column_sign_matrix, swapping_column_matrix}; 51 | 52 | #[test] 53 | fn test_swapping_column_matrix() { 54 | let actual = swapping_column_matrix(U3, 0, 1); 55 | let expect = matrix![ 56 | 0, 1, 0; 57 | 1, 0, 0; 58 | 0, 0, 1; 59 | ]; 60 | assert_eq!(actual, expect); 61 | } 62 | 63 | #[test] 64 | fn test_adding_column_matrix() { 65 | let actual = adding_column_matrix(U3, 0, 2, -1); 66 | let expect = matrix![ 67 | 1, 0, -1; 68 | 0, 1, 0; 69 | 0, 0, 1; 70 | ]; 71 | assert_eq!(actual, expect); 72 | } 73 | 74 | #[test] 75 | fn test_changing_column_sign_matrix() { 76 | let actual = changing_column_sign_matrix(U3, 0); 77 | let expect = matrix![ 78 | -1, 0, 0; 79 | 0, 1, 0; 80 | 0, 0, 1; 81 | ]; 82 | assert_eq!(actual, expect); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /moyo/src/math/hnf.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::allocator::Allocator; 2 | use nalgebra::{DefaultAllocator, Dim, OMatrix}; 3 | 4 | use super::elementary::{ 5 | adding_column_matrix, changing_column_sign_matrix, swapping_column_matrix, 6 | }; 7 | 8 | /// Hermite normal form of (M, N) matrix such that h = basis * r 9 | #[derive(Debug)] 10 | #[allow(clippy::upper_case_acronyms)] 11 | pub struct HNF 12 | where 13 | DefaultAllocator: Allocator + Allocator, 14 | { 15 | pub h: OMatrix, 16 | pub r: OMatrix, 17 | } 18 | 19 | impl HNF 20 | where 21 | DefaultAllocator: Allocator + Allocator, 22 | { 23 | /// Return column-wise Hermite norm form 24 | pub fn new(basis: &OMatrix) -> Self { 25 | let (m, n) = basis.shape_generic(); 26 | let mut h = basis.clone(); 27 | let mut r = OMatrix::identity_generic(n, n); 28 | 29 | // Process the `s`th row 30 | for s in 0..m.value() { 31 | loop { 32 | if (s..n.value()).all(|j| h[(s, j)] == 0) { 33 | break; 34 | } 35 | 36 | // Choose pivot column with the smallest absolute value 37 | let pivot = (s..n.value()) 38 | .filter(|&j| h[(s, j)] != 0) 39 | .min_by_key(|&j| h[(s, j)].abs()) 40 | .unwrap(); 41 | h.swap_columns(s, pivot); 42 | r *= swapping_column_matrix(n, s, pivot); 43 | 44 | // Guarantee that h[(s, s)] is positive 45 | if h[(s, s)] < 0 { 46 | for i in 0..m.value() { 47 | h[(i, s)] *= -1; 48 | } 49 | r *= changing_column_sign_matrix(n, s); 50 | } 51 | assert_ne!(h[(s, s)], 0); 52 | 53 | // Add the `s`th column to the other columns 54 | let mut update = false; 55 | for j in 0..n.value() { 56 | if j == s { 57 | continue; 58 | } 59 | let k = h[(s, j)].div_euclid(h[(s, s)]); 60 | 61 | if k != 0 { 62 | update = true; 63 | // h[(:, j)] -= k * h[(:, s)] 64 | for i in 0..m.value() { 65 | h[(i, j)] -= k * h[(i, s)]; 66 | } 67 | r *= adding_column_matrix(n, s, j, -k); 68 | } 69 | } 70 | 71 | // Continue until updating 72 | if !update { 73 | break; 74 | } 75 | } 76 | } 77 | assert_eq!(h, basis * r.clone()); 78 | Self { h, r } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use nalgebra::{matrix, SMatrix}; 85 | use rand::prelude::*; 86 | use rand::rngs::StdRng; 87 | use rand::SeedableRng; 88 | 89 | use super::HNF; 90 | 91 | #[test] 92 | fn test_hnf_small() { 93 | { 94 | let m = matrix![ 95 | -1, 0, 0; 96 | 1, 2, 2; 97 | 0, -1, -2; 98 | ]; 99 | let hnf = HNF::new(&m); 100 | let expect = matrix![ 101 | 1, 0, 0; 102 | 1, 2, 0; 103 | 0, 0, 1; 104 | ]; 105 | assert_eq!(hnf.h, expect); 106 | } 107 | { 108 | let m = matrix![ 109 | 20, -6; 110 | -2, 1; 111 | ]; 112 | let hnf = HNF::new(&m); 113 | assert_eq!(hnf.h, matrix![2, 0; 1, 4]); 114 | } 115 | { 116 | let m = matrix![ 117 | 2, 3, 6, 2; 118 | 5, 6, 1, 6; 119 | 8, 3, 1, 1; 120 | ]; 121 | let hnf = HNF::new(&m); 122 | let expect = matrix![ 123 | 1, 0, 0, 0; 124 | 0, 1, 0, 0; 125 | 0, 0, 1, 0; 126 | ]; 127 | assert_eq!(hnf.h, expect); 128 | } 129 | } 130 | 131 | #[test] 132 | fn test_hnf_random() { 133 | let mut rng: StdRng = SeedableRng::from_seed([0; 32]); 134 | 135 | for _ in 0..256 { 136 | let m = SMatrix::::from_fn(|_, _| rng.random_range(-4..4)); 137 | let _ = HNF::new(&m); 138 | 139 | let m = SMatrix::::from_fn(|_, _| rng.random_range(-4..4)); 140 | let _ = HNF::new(&m); 141 | 142 | let m = SMatrix::::from_fn(|_, _| rng.random_range(-4..4)); 143 | let _ = HNF::new(&m); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /moyo/src/math/integer_system.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::allocator::Allocator; 2 | use nalgebra::{DefaultAllocator, Dim, DimMin, Dyn, Matrix3, OMatrix, OVector, U1, U9}; 3 | 4 | use super::snf::SNF; 5 | 6 | #[derive(Debug)] 7 | pub struct IntegerLinearSystem 8 | where 9 | DefaultAllocator: Allocator, 10 | { 11 | /// rank of the integer linear system 12 | #[allow(dead_code)] 13 | pub rank: usize, 14 | /// Special solution for a * x = b 15 | #[allow(dead_code)] 16 | pub x: OVector, 17 | /// Nullspace of a * x = 0 18 | pub nullspace: OMatrix, 19 | } 20 | 21 | impl IntegerLinearSystem 22 | where 23 | DefaultAllocator: Allocator, 24 | { 25 | /// Solve a * x = b 26 | /// If no solution, return None 27 | pub fn new>(a: &OMatrix, b: &OVector) -> Option 28 | where 29 | DefaultAllocator: Allocator + Allocator + Allocator + Allocator, 30 | { 31 | let (_, n) = a.shape_generic(); 32 | let snf = SNF::new(a); 33 | let rank = snf.rank(); 34 | if rank == n.value() { 35 | return None; 36 | } 37 | 38 | // Solve snf.d * y = snf.l * b (x = snf.r * y) 39 | let mut y = OVector::zeros_generic(n, U1); 40 | let lb = snf.l * b; 41 | for i in 0..rank { 42 | if lb[(i, 0)] % snf.d[(i, i)] != 0 { 43 | return None; 44 | } 45 | y[i] = lb[(i, 0)] / snf.d[(i, i)]; 46 | } 47 | let x = snf.r.clone() * y; 48 | 49 | let nullspace = snf.r.columns(rank, n.value() - rank).clone().transpose(); 50 | 51 | Some(Self { rank, x, nullspace }) 52 | } 53 | } 54 | 55 | /// Solve P^-1 * A[i] * P = B[i] (for all i) 56 | /// vec(A * P - P * B) = (I_3 \otimes A - B^T \otimes I_3) * vec(P) 57 | pub fn sylvester3(a: &[Matrix3], b: &[Matrix3]) -> Option>> { 58 | let size = a.len(); 59 | assert_eq!(size, b.len()); 60 | 61 | let mut coeffs = OMatrix::::zeros(9 * size); 62 | let identity = Matrix3::::identity(); 63 | for k in 0..size { 64 | let adj = identity.kronecker(&a[k]) - b[k].transpose().kronecker(&identity); 65 | for i in 0..9 { 66 | for j in 0..9 { 67 | coeffs[(9 * k + i, j)] = adj[(i, j)]; 68 | } 69 | } 70 | } 71 | let solution = IntegerLinearSystem::new(&coeffs, &OVector::::zeros(coeffs.nrows())); 72 | 73 | if let Some(solution) = solution { 74 | let basis: Vec<_> = solution 75 | .nullspace 76 | .row_iter() 77 | .map(|e| { 78 | // Vectorization operator is column-major 79 | Matrix3::::new( 80 | e[0], e[1], e[2], // 81 | e[3], e[4], e[5], // 82 | e[6], e[7], e[8], // 83 | ) 84 | .transpose() 85 | }) 86 | .collect(); 87 | Some(basis) 88 | } else { 89 | None 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use nalgebra::{matrix, vector}; 96 | 97 | use super::IntegerLinearSystem; 98 | 99 | #[test] 100 | fn test_integer_linear_system() { 101 | { 102 | let a = matrix![ 103 | 6, 4, 10; 104 | -1, 1, -5; 105 | ]; 106 | let b = vector![4, 11]; 107 | let solution = IntegerLinearSystem::new(&a, &b).unwrap(); 108 | assert_eq!(solution.rank, 2); 109 | assert_eq!(a * solution.x, b); 110 | } 111 | { 112 | let a = matrix![ 113 | 1, 1, 0; 114 | ]; 115 | let b = vector![2]; 116 | let solution = IntegerLinearSystem::new(&a, &b).unwrap(); 117 | assert_eq!(solution.rank, 1); 118 | assert_eq!(a * solution.x, b); 119 | } 120 | { 121 | let a = matrix![ 122 | 2, 4, 0; 123 | ]; 124 | let b = vector![1]; 125 | let solution = IntegerLinearSystem::new(&a, &b); 126 | assert!(solution.is_none()); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /moyo/src/math/snf.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::allocator::Allocator; 2 | use nalgebra::{DefaultAllocator, Dim, DimMin, OMatrix}; 3 | 4 | /// Hermite normal form of MxN matrix such that d = l * basis * r 5 | #[derive(Debug)] 6 | #[allow(clippy::upper_case_acronyms)] 7 | pub struct SNF, N: Dim> 8 | where 9 | DefaultAllocator: Allocator + Allocator + Allocator, 10 | { 11 | pub d: OMatrix, 12 | pub l: OMatrix, 13 | pub r: OMatrix, 14 | } 15 | 16 | impl, N: Dim> SNF 17 | where 18 | DefaultAllocator: Allocator + Allocator + Allocator, 19 | { 20 | pub fn new(basis: &OMatrix) -> SNF 21 | where 22 | DefaultAllocator: Allocator + Allocator + Allocator, 23 | { 24 | let (m, n) = basis.shape_generic(); 25 | let mut d = basis.clone(); 26 | let mut l = OMatrix::identity_generic(m, m); 27 | let mut r = OMatrix::identity_generic(n, n); 28 | 29 | // Process the `s`th column and row 30 | for s in 0..m.min(n).value() { 31 | // Choose pivot element with the nonzero smallest absolute value 32 | while let Some(pivot) = (s..m.value()) 33 | .flat_map(|i| (s..n.value()).map(move |j| (i, j))) 34 | .filter(|&(i, j)| d[(i, j)] != 0) 35 | .min_by_key(|&(i, j)| d[(i, j)].abs()) 36 | { 37 | // Move pivot element to (s, s) 38 | d.swap_rows(s, pivot.0); 39 | l.swap_rows(s, pivot.0); 40 | d.swap_columns(s, pivot.1); 41 | r.swap_columns(s, pivot.1); 42 | 43 | // Guarantee that h[(s, s)] is positive 44 | if d[(s, s)] < 0 { 45 | for i in 0..m.value() { 46 | d[(i, s)] *= -1; 47 | } 48 | // r *= changing_column_sign_matrix(n, s); 49 | for i in 0..n.value() { 50 | r[(i, s)] *= -1; 51 | } 52 | } 53 | assert_ne!(d[(s, s)], 0); 54 | 55 | // Eliminate (*, s) entries 56 | let mut update = false; 57 | for i in s + 1..m.value() { 58 | let k = d[(i, s)] / d[(s, s)]; 59 | 60 | if k != 0 { 61 | update = true; 62 | // d[(i, :)] -= k * d[(s, :)] 63 | for j in 0..n.value() { 64 | d[(i, j)] -= k * d[(s, j)]; 65 | } 66 | // l[(i, :)] -= k * l[(s, :)] 67 | for j in 0..m.value() { 68 | l[(i, j)] -= k * l[(s, j)]; 69 | } 70 | } 71 | } 72 | 73 | // Eliminate (s, *) entries 74 | for j in s + 1..n.value() { 75 | let k = d[(s, j)] / d[(s, s)]; 76 | 77 | if k != 0 { 78 | update = true; 79 | // d[(:, j)] -= k * d[(:, s)] 80 | for i in 0..m.value() { 81 | d[(i, j)] -= k * d[(i, s)]; 82 | } 83 | // r[(:, j)] -= k * r[(:, s)] 84 | for i in 0..n.value() { 85 | r[(i, j)] -= k * r[(i, s)]; 86 | } 87 | } 88 | } 89 | 90 | // Continue until updating 91 | if !update { 92 | break; 93 | } 94 | } 95 | } 96 | 97 | assert_eq!(d, l.clone() * basis * r.clone()); 98 | SNF:: { d, l, r } 99 | } 100 | 101 | pub fn rank(&self) -> usize { 102 | let (m, n) = self.d.shape_generic(); 103 | (0..m.min(n).value()) 104 | .filter(|&i| self.d[(i, i)] != 0) 105 | .count() 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use nalgebra::{matrix, SMatrix}; 112 | use rand::prelude::*; 113 | use rand::rngs::StdRng; 114 | use rand::SeedableRng; 115 | 116 | use super::SNF; 117 | 118 | #[test] 119 | fn test_snf_small() { 120 | { 121 | let m = matrix![ 122 | 2, 4, 4; 123 | -6, 6, 12; 124 | 10, -4, -16; 125 | ]; 126 | let snf = SNF::new(&m); 127 | assert_eq!(snf.d, matrix![2, 0, 0; 0, 6, 0; 0, 0, 12]); 128 | } 129 | } 130 | 131 | #[test] 132 | fn test_snf_random() { 133 | let mut rng: StdRng = SeedableRng::from_seed([0; 32]); 134 | 135 | for _ in 0..256 { 136 | let m = SMatrix::::from_fn(|_, _| rng.random_range(-4..4)); 137 | let _ = SNF::new(&m); 138 | 139 | let m = SMatrix::::from_fn(|_, _| rng.random_range(-4..4)); 140 | let _ = SNF::new(&m); 141 | 142 | let m = SMatrix::::from_fn(|_, _| rng.random_range(-4..4)); 143 | let _ = SNF::new(&m); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /moyo/src/search.rs: -------------------------------------------------------------------------------- 1 | mod primitive_cell; 2 | mod primitive_symmetry_search; 3 | mod solve; 4 | mod symmetry_search; 5 | 6 | pub use solve::{ 7 | solve_correspondence, solve_correspondence_naive, PeriodicKdTree, PeriodicNeighbor, 8 | }; 9 | 10 | pub(super) use primitive_cell::{PrimitiveCell, PrimitiveMagneticCell}; 11 | pub(super) use primitive_symmetry_search::{ 12 | magnetic_operations_in_magnetic_cell, operations_in_cell, PrimitiveMagneticSymmetrySearch, 13 | }; 14 | pub(super) use symmetry_search::{iterative_magnetic_symmetry_search, iterative_symmetry_search}; 15 | -------------------------------------------------------------------------------- /moyo/src/search/symmetry_search.rs: -------------------------------------------------------------------------------- 1 | use super::primitive_cell::{PrimitiveCell, PrimitiveMagneticCell}; 2 | use super::primitive_symmetry_search::{PrimitiveMagneticSymmetrySearch, PrimitiveSymmetrySearch}; 3 | use crate::base::{ 4 | AngleTolerance, Cell, MagneticCell, MagneticMoment, MagneticSymmetryTolerances, MoyoError, 5 | RotationMagneticMomentAction, SymmetryTolerances, ToleranceHandler, 6 | }; 7 | 8 | use log::debug; 9 | 10 | const MAX_SYMMETRY_SEARCH_TRIALS: usize = 16; 11 | const MAX_TOLERANCE_HANDLER_TRIALS: usize = 4; 12 | 13 | pub fn iterative_symmetry_search( 14 | cell: &Cell, 15 | symprec: f64, 16 | angle_tolerance: AngleTolerance, 17 | ) -> Result<(PrimitiveCell, PrimitiveSymmetrySearch, f64, AngleTolerance), MoyoError> { 18 | let mut tolerances = SymmetryTolerances { 19 | symprec, 20 | angle_tolerance, 21 | }; 22 | 23 | for _ in 0..MAX_TOLERANCE_HANDLER_TRIALS { 24 | let mut tolerance_handler = ToleranceHandler::new(tolerances); 25 | 26 | for _ in 0..MAX_SYMMETRY_SEARCH_TRIALS { 27 | match PrimitiveCell::new(cell, tolerance_handler.tolerances.symprec) { 28 | Ok(prim_cell) => { 29 | match PrimitiveSymmetrySearch::new( 30 | &prim_cell.cell, 31 | tolerance_handler.tolerances.symprec, 32 | tolerance_handler.tolerances.angle_tolerance, 33 | ) { 34 | Ok(symmetry_search) => { 35 | return Ok(( 36 | prim_cell, 37 | symmetry_search, 38 | tolerance_handler.tolerances.symprec, 39 | tolerance_handler.tolerances.angle_tolerance, 40 | )); 41 | } 42 | Err(err) => tolerance_handler.update(err), 43 | } 44 | } 45 | Err(err) => tolerance_handler.update(err), 46 | } 47 | } 48 | 49 | // When the maximum number of symmetry search trials is reached, restart ToleranceHandler to try larger strides 50 | tolerances = tolerance_handler.tolerances.clone(); 51 | debug!("Restart ToleranceHandler with {:?}", tolerances); 52 | } 53 | debug!("Reach the maximum number of symmetry search trials"); 54 | Err(MoyoError::PrimitiveSymmetrySearchError) 55 | } 56 | 57 | pub fn iterative_magnetic_symmetry_search( 58 | magnetic_cell: &MagneticCell, 59 | symprec: f64, 60 | angle_tolerance: AngleTolerance, 61 | mag_symprec: Option, 62 | action: RotationMagneticMomentAction, 63 | ) -> Result< 64 | ( 65 | PrimitiveMagneticCell, 66 | PrimitiveMagneticSymmetrySearch, 67 | f64, 68 | AngleTolerance, 69 | f64, 70 | ), 71 | MoyoError, 72 | > { 73 | let mut tolerances = MagneticSymmetryTolerances { 74 | symprec, 75 | angle_tolerance, 76 | mag_symprec: mag_symprec.unwrap_or(symprec), 77 | }; 78 | 79 | for _ in 0..MAX_TOLERANCE_HANDLER_TRIALS { 80 | let mut tolerance_handler = ToleranceHandler::new(tolerances); 81 | 82 | for _ in 0..MAX_SYMMETRY_SEARCH_TRIALS { 83 | match PrimitiveMagneticCell::new( 84 | magnetic_cell, 85 | tolerance_handler.tolerances.symprec, 86 | tolerance_handler.tolerances.mag_symprec, 87 | ) { 88 | Ok(prim_mag_cell) => { 89 | match PrimitiveMagneticSymmetrySearch::new( 90 | &prim_mag_cell.magnetic_cell, 91 | tolerance_handler.tolerances.symprec, 92 | tolerance_handler.tolerances.angle_tolerance, 93 | tolerance_handler.tolerances.mag_symprec, 94 | action, 95 | ) { 96 | Ok(magnetic_symmetry_search) => { 97 | return Ok(( 98 | prim_mag_cell, 99 | magnetic_symmetry_search, 100 | tolerance_handler.tolerances.symprec, 101 | tolerance_handler.tolerances.angle_tolerance, 102 | tolerance_handler.tolerances.mag_symprec, 103 | )); 104 | } 105 | Err(err) => tolerance_handler.update(err), 106 | } 107 | } 108 | Err(err) => tolerance_handler.update(err), 109 | } 110 | } 111 | 112 | // When the maximum number of symmetry search trials is reached, restart ToleranceHandler to try larger strides 113 | tolerances = tolerance_handler.tolerances.clone(); 114 | debug!("Restart ToleranceHandler with {:?}", tolerances); 115 | } 116 | debug!("Reach the maximum number of symmetry search trials"); 117 | Err(MoyoError::PrimitiveMagneticSymmetrySearchError) 118 | } 119 | -------------------------------------------------------------------------------- /moyo/src/symmetrize.rs: -------------------------------------------------------------------------------- 1 | mod magnetic_standardize; 2 | mod standardize; 3 | 4 | pub(super) use magnetic_standardize::StandardizedMagneticCell; 5 | pub(super) use standardize::{orbits_in_cell, StandardizedCell}; 6 | -------------------------------------------------------------------------------- /moyo/tests/assets/AB_mC8_15_e_a.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[4.807835971596717,0.0,5.95538517065568,-8.07000216883193e-16,13.1793137,8.07000216883193e-16,0.0,0.0,11.74174177]},"positions":[[0.0,0.57413983,0.25],[0.0,0.42586017,0.75],[0.5,0.07413983,0.25],[0.5,0.92586017,0.75],[0.0,0.0,0.0],[0.0,0.0,0.5],[0.5,0.5,0.0],[0.5,0.5,0.5]],"numbers":[20,20,20,20,26,26,26,26]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-1185639.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[13.77366283,-7.95229169,0.0,-0.00004934,15.90449586,0.0,0.0,0.0,15.58381769]},"positions":[[0.60168048,0.00020148,0.0],[0.39852221,0.00019769,0.0],[0.00040636000000000003,0.19641983000000002,0.0],[0.19601018,0.19641745,0.0],[0.59977792,0.19955914,0.0],[0.40009012,0.20004388,0.0],[0.79995576,0.20004224,0.0],[0.99980024,0.39832029999999996,0.0],[0.39852003,0.39831999,0.0],[0.19988151,0.39976313,0.0],[0.80044143,0.40022159,0.0],[0.59978058,0.40022202,0.0],[0.19988162,0.59994172,0.0],[0.40005973,0.59994077,0.0],[0.79995643,0.5999107300000001,0.0],[0.60167768,0.60147615,0.0],[0.9997995900000001,0.60147842,0.0],[0.40005906,0.8001206200000001,0.0],[0.60023543,0.80011664,0.0],[0.19988131,0.80011722,0.0],[0.00040783000000000003,0.80398599,0.0],[0.80358235,0.80398814,0.0],[0.19600909,0.99958738,0.0],[0.80357722,0.99959104,0.0],[0.06537437,0.13074783,0.16294452],[0.06537365,0.93462425,0.16294442],[0.86925062,0.93462347,0.16294309],[0.26647705,0.13323726,0.16590522],[0.86676105,0.13324001,0.165903],[0.86676128,0.73352121,0.16590511],[0.06665005,0.53332358,0.1664857],[0.46667703,0.5333255100000001,0.16648854],[0.46667441,0.93334936,0.16648799],[0.66666705,0.33333368,0.16631207],[0.26621612,0.33330315,0.16654716],[0.06708372,0.33330329000000003,0.16654539999999998],[0.06708445,0.73378087,0.16654483],[0.66669536,0.73377937,0.16654458],[0.6666966400000001,0.93291636,0.16654345],[0.26622084,0.93291743,0.16654409],[0.6669749500000001,0.13326839000000001,0.16665753],[0.4662921,0.1332698,0.16665729999999998],[0.46629348,0.33302476000000003,0.16665843],[0.86673124,0.33302377,0.16665687],[0.66697454,0.53370627,0.16666013],[0.86673392,0.53370857,0.16665754],[0.26672693000000003,0.53345116,0.16674288],[0.46655145000000003,0.73327477,0.16674108],[0.26672673,0.73327496,0.16674119],[0.19937261,0.19963783000000002,0.33201423],[0.0002646,0.19963627,0.33201458],[0.80036574,0.8006283,0.33201294000000003],[0.00026038,0.80062701,0.33201408],[0.80036619,0.99973711,0.33201208],[0.19936957,0.99973557,0.33201288],[0.6001841,0.9999855999999999,0.33310819999999997],[0.39979932,0.9999834200000001,0.33310582],[0.00001535,0.39981556,0.33310736],[0.39979839,0.39981498,0.33310755000000003],[0.60018518,0.60020169,0.3331054],[0.00001602,0.6002018100000001,0.33310582],[0.59991647,0.19983199000000001,0.33313511],[0.59991773,0.40008374,0.33313594],[0.8001684800000001,0.4000857,0.33313435],[-3.8e-7,-1.39e-6,0.33341419],[0.39986138,0.19993104,0.33338628],[0.80006888,0.19992903,0.3333835],[0.80006721,0.60013935,0.33338238000000003],[0.20012723000000002,0.40025527,0.33360198],[0.59974661,0.79987387,0.33360247],[0.20012692,0.79987266,0.33360138],[0.19989678,0.59994794,0.33347942],[0.40005008000000003,0.5999507900000001,0.33348086],[0.40005134000000003,0.8001027,0.33347872],[0.8668713,0.13312811,0.5],[0.26625645,0.13312745,0.5],[0.06661349,0.13322415,0.5],[0.66707464,0.13386867,0.5],[0.46679748,0.13387119,0.5],[0.06640553,0.33283342,0.5],[0.26642864,0.33283303000000003,0.5],[0.86613177,0.33292649999999996,0.5],[0.46679681,0.33292812,0.5],[0.66666658,0.33333221,0.5],[0.46706388,0.53293752,0.5],[0.06587386,0.53293508,0.5],[0.26647289,0.5329416100000001,0.5],[0.66707429,0.53320458,0.5],[0.86613434,0.53320648,0.5],[0.46705686,0.7335292999999999,0.5],[0.26646876,0.73352739,0.5],[0.06640515,0.73357107,0.5],[0.66716829,0.73357335,0.5],[0.86687364,0.7337446999999999,0.5],[0.86677969,0.93339019,0.5],[0.06661562,0.9333868200000001,0.5],[0.66716717,0.93359515,0.5],[0.26642945,0.9335977999999999,0.5],[0.46706517000000003,0.93412709,0.5],[0.19989678,0.59994794,0.66652058],[0.40005008000000003,0.5999507900000001,0.66651914],[0.40005134000000003,0.8001027,0.66652128],[0.20012723000000002,0.40025527,0.66639802],[0.59974661,0.79987387,0.66639753],[0.20012692,0.79987266,0.66639862],[0.39986138,0.19993104,0.66661372],[0.80006888,0.19992903,0.6666165],[0.80006721,0.60013935,0.66661762],[-3.8e-7,-1.39e-6,0.6665858100000001],[0.59991647,0.19983199000000001,0.66686489],[0.59991773,0.40008374,0.66686406],[0.8001684800000001,0.4000857,0.66686565],[0.6001841,0.9999855999999999,0.6668917999999999],[0.39979932,0.9999834200000001,0.66689418],[0.00001535,0.39981556,0.66689264],[0.39979839,0.39981498,0.66689245],[0.60018518,0.60020169,0.6668946],[0.00001602,0.6002018100000001,0.66689418],[0.19937261,0.19963783000000002,0.66798577],[0.0002646,0.19963627,0.66798542],[0.80036574,0.8006283,0.66798706],[0.00026038,0.80062701,0.66798592],[0.80036619,0.99973711,0.6679879200000001],[0.19936957,0.99973557,0.66798712],[0.26672693000000003,0.53345116,0.83325712],[0.46655145000000003,0.73327477,0.83325892],[0.26672673,0.73327496,0.83325881],[0.6669749500000001,0.13326839000000001,0.8333424700000001],[0.4662921,0.1332698,0.8333427],[0.46629348,0.33302476000000003,0.83334157],[0.86673124,0.33302377,0.83334313],[0.66697454,0.53370627,0.83333987],[0.86673392,0.53370857,0.83334246],[0.26621612,0.33330315,0.83345284],[0.06708372,0.33330329000000003,0.8334545999999999],[0.06708445,0.73378087,0.83345517],[0.66669536,0.73377937,0.8334554200000001],[0.6666966400000001,0.93291636,0.83345655],[0.26622084,0.93291743,0.83345591],[0.66666705,0.33333368,0.83368793],[0.06665005,0.53332358,0.8335142999999999],[0.46667703,0.5333255100000001,0.83351146],[0.46667441,0.93334936,0.83351201],[0.26647705,0.13323726,0.83409478],[0.86676105,0.13324001,0.834097],[0.86676128,0.73352121,0.83409489],[0.06537437,0.13074783,0.83705548],[0.06537365,0.93462425,0.83705558],[0.86925062,0.93462347,0.83705691],[6.5e-7,1.63e-6,0.0]],"numbers":[12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,22]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-1197586.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[0.0,0.0,-13.9744737,-7.15143326,-12.38674029,0.0,-7.15143326,12.38674029,0.0]},"positions":[[0.5,0.0,0.0],[0.0,0.0,0.0],[0.75,0.45582281,0.91164493],[0.75,0.45582212,0.54417788],[0.75,0.08835507000000001,0.54417719],[0.25,0.54417719,0.08835507000000001],[0.25,0.54417788,0.45582212],[0.25,0.91164493,0.45582281],[0.75,0.12803599999999998,0.25606556],[0.75,0.12804045,0.87195955],[0.75,0.74393444,0.871964],[0.25,0.871964,0.74393444],[0.25,0.87195955,0.12804045],[0.25,0.25606556,0.12803599999999998],[0.55210872,0.79418235,0.5883644],[0.55210676,0.79418507,0.20581493],[0.55210872,0.4116356,0.20581765],[0.44789128,0.20581765,0.4116356],[0.44789324,0.20581493,0.79418507],[0.44789128,0.5883644,0.79418235],[0.052108720000000004,0.20581765,0.4116356],[0.05210676,0.20581493,0.79418507],[0.052108720000000004,0.5883644,0.79418235],[0.94789128,0.79418235,0.5883644],[0.94789324,0.79418507,0.20581493],[0.94789128,0.4116356,0.20581765],[0.75,1.5e-6,0.9999984999999999],[0.25,0.9999984999999999,1.5e-6],[0.25,0.66667365,0.33332635],[0.75,0.33332635,0.66667365],[0.59605062,0.66667574,0.33332426],[0.40394938,0.33332426,0.66667574],[0.09605062,0.33332426,0.66667574],[0.90394938,0.66667574,0.33332426],[0.5,0.5,0.0],[0.5,0.5,0.5],[0.5,0.0,0.5],[0.0,0.5,0.0],[0.0,0.5,0.5],[0.0,0.0,0.5],[0.75,0.73039297,0.46078484000000003],[0.75,0.73039683,0.26960317],[0.75,0.53921516,0.26960703],[0.25,0.26960703,0.53921516],[0.25,0.26960317,0.73039683],[0.25,0.46078484000000003,0.73039297],[0.49999915,0.79883777,0.99999817],[0.5,0.20116169,0.20116169],[0.50000085,0.99999817,0.79883777],[0.50000085,0.20116223,1.83e-6],[0.49999915,1.83e-6,0.20116223],[0.5,0.79883831,0.79883831],[0.9999991500000001,0.20116223,1.83e-6],[0.0,0.79883831,0.79883831],[8.5e-7,1.83e-6,0.20116223],[8.5e-7,0.79883777,0.99999817],[0.9999991500000001,0.99999817,0.79883777],[0.0,0.20116169,0.20116169],[0.75,0.91569599,0.63457409],[0.75,0.71887639,0.08430007],[0.75,0.36542417,0.28112061],[0.75,0.71887939,0.6345758300000001],[0.75,0.36542591,0.08430401],[0.75,0.91569993,0.28112361],[0.25,0.08430401,0.36542591],[0.25,0.28112361,0.91569993],[0.25,0.6345758300000001,0.71887939],[0.25,0.28112061,0.36542417],[0.25,0.63457409,0.91569599],[0.25,0.08430007,0.71887639],[0.6568319,0.90501729,0.8100342500000001],[0.65683154,0.90501776,0.09498224],[0.6568319,0.18996575000000002,0.09498271],[0.3431681,0.09498271,0.18996575000000002],[0.34316846,0.09498224,0.90501776],[0.3431681,0.8100342500000001,0.90501729],[0.1568319,0.09498271,0.18996575000000002],[0.15683154,0.09498224,0.90501776],[0.1568319,0.8100342500000001,0.90501729],[0.8431681,0.90501729,0.8100342500000001],[0.84316846,0.90501776,0.09498224],[0.8431681,0.18996575000000002,0.09498271],[0.14185786,0.43676018,0.87352151],[0.14185264,0.43675433,0.56324567],[0.14185786,0.12647849,0.5632398200000001],[0.85814214,0.5632398200000001,0.12647849],[0.85814736,0.56324567,0.43675433],[0.85814214,0.87352151,0.43676018],[0.64185786,0.5632398200000001,0.12647849],[0.64185264,0.56324567,0.43675433],[0.64185786,0.87352151,0.43676018],[0.35814214,0.43676018,0.87352151],[0.35814736,0.43675433,0.56324567],[0.35814214,0.12647849,0.5632398200000001],[0.55100569,0.39864882,0.7972973],[0.5509953,0.39864667,0.60135333],[0.55100569,0.20270269999999999,0.60135118],[0.44899431,0.60135118,0.20270269999999999],[0.4490047,0.60135333,0.39864667],[0.44899431,0.7972973,0.39864882],[0.05100569,0.60135118,0.20270269999999999],[0.0509953,0.60135333,0.39864667],[0.05100569,0.7972973,0.39864882],[0.94899431,0.39864882,0.7972973],[0.9490046999999999,0.39864667,0.60135333],[0.94899431,0.20270269999999999,0.60135118],[0.15709854,0.76422755,0.5284516],[0.15709478,0.76422852,0.23577148],[0.15709854,0.4715484,0.23577245],[0.84290146,0.23577245,0.4715484],[0.84290522,0.23577148,0.76422852],[0.84290146,0.5284516,0.76422755],[0.65709854,0.23577245,0.4715484],[0.65709478,0.23577148,0.76422852],[0.65709854,0.5284516,0.76422755],[0.34290146,0.76422755,0.5284516],[0.34290522,0.76422852,0.23577148],[0.34290146,0.4715484,0.23577245],[0.60257127,0.63432879,0.96377419],[0.60257259,0.32944476,0.36567085],[0.60257159,0.03622366,0.6705532],[0.60257159,0.3294468,0.96377634],[0.60257127,0.036225810000000004,0.36567121],[0.60257259,0.63432915,0.67055524],[0.39742873,0.36567121,0.036225810000000004],[0.39742741,0.67055524,0.63432915],[0.39742841,0.96377634,0.3294468],[0.39742841,0.6705532,0.03622366],[0.39742873,0.96377419,0.63432879],[0.39742741,0.36567085,0.32944476],[0.10257127,0.36567121,0.036225810000000004],[0.10257259,0.67055524,0.63432915],[0.10257159,0.96377634,0.3294468],[0.10257159,0.6705532,0.03622366],[0.10257127,0.96377419,0.63432879],[0.10257259,0.36567085,0.32944476],[0.89742873,0.63432879,0.96377419],[0.89742741,0.32944476,0.36567085],[0.8974284100000001,0.03622366,0.6705532],[0.8974284100000001,0.3294468,0.96377634],[0.89742873,0.036225810000000004,0.36567121],[0.89742741,0.63432915,0.67055524]],"numbers":[67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,67,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-1221598.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[-4.097399,4.097402,-2e-6,-4.097394,-4.097398,3e-6,-2e-6,-4.9999999999999996e-6,8.189524]},"positions":[[0.99999,0.500001,0.249993],[0.5000089999999999,0.0,0.749993],[0.5,0.0,0.250001],[0.999999,0.5,0.75],[4e-6,1e-6,0.9999979999999999],[0.5,0.5,0.500003],[1e-6,0.9999979999999999,0.500003],[0.5,0.500001,1e-6],[0.49999899999999997,0.0,0.488517],[0.0,0.5,0.9885189999999999],[0.5,0.0,0.011484],[0.0,0.5,0.511482],[0.7384419999999999,0.761558,0.250001],[0.23843799999999998,0.261561,0.7500009999999999],[0.261557,0.238442,0.25],[0.7615609999999999,0.738439,0.7500009999999999],[0.261557,0.761558,0.250001],[0.7615609999999999,0.261561,0.7500009999999999],[0.7384419999999999,0.238442,0.25],[0.23843799999999998,0.738439,0.7500009999999999]],"numbers":[25,25,52,52,82,82,82,82,8,8,8,8,8,8,8,8,8,8,8,8]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-1277787.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[-1.673118,4.736188,-2.89992,3.34152,4.723757,5.788563,5.022775,0.000747,-2.899659]},"positions":[[0.000282,0.249963,0.499436],[0.499665,0.750002,0.000527],[0.500353,0.25,0.999461],[0.999733,0.750039,0.500568],[0.500031,0.500008,0.499971],[0.999895,0.999983,0.000069],[1e-6,0.5,0.999999],[0.500034,3e-6,0.500014],[0.500387,0.250355,0.49904],[0.99972,0.7504,0.000675],[0.000284,0.249599,0.999329],[0.499616,0.749647,0.50095],[0.737555,0.000036,0.737726],[0.251308,0.500171,0.251642],[0.751414,0.000113,0.248466],[0.237742,0.499505,0.762423],[0.262437,0.99996,0.262259],[0.748697,0.499831,0.748347],[0.24859,0.999888,0.75153],[0.762257,0.500496,0.237566]],"numbers":[56,56,56,56,26,26,42,42,8,8,8,8,8,8,8,8,8,8,8,8]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-30665.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[5.879876,0.0,0.0,0.0,5.879876,0.0,0.0,0.0,48.233912]},"positions":[[0.171668,0.171668,0.25],[0.171668,0.828332,0.75],[0.828332,0.171668,0.75],[0.828332,0.828332,0.25],[0.347099,0.233069,0.015158999999999999],[0.233069,0.652901,0.984841],[0.7669309999999999,0.347099,0.984841],[0.652901,0.7669309999999999,0.015158999999999999],[0.233069,0.347099,0.48484099999999997],[0.7669309999999999,0.652901,0.48484099999999997],[0.652901,0.233069,0.5151589999999999],[0.347099,0.7669309999999999,0.5151589999999999],[0.798904,0.156272,0.044989],[0.156272,0.201096,0.9550109999999999],[0.8437279999999999,0.798904,0.9550109999999999],[0.201096,0.8437279999999999,0.044989],[0.156272,0.798904,0.455011],[0.8437279999999999,0.201096,0.455011],[0.201096,0.156272,0.544989],[0.798904,0.8437279999999999,0.544989],[0.166935,0.322716,0.073782],[0.322716,0.8330649999999999,0.926218],[0.677284,0.166935,0.926218],[0.8330649999999999,0.677284,0.073782],[0.322716,0.166935,0.426218],[0.677284,0.8330649999999999,0.426218],[0.8330649999999999,0.322716,0.573782],[0.166935,0.677284,0.573782],[0.662565,0.315479,0.10252499999999999],[0.315479,0.337435,0.8974749999999999],[0.6845209999999999,0.662565,0.8974749999999999],[0.337435,0.6845209999999999,0.10252499999999999],[0.315479,0.662565,0.39747499999999997],[0.6845209999999999,0.337435,0.39747499999999997],[0.337435,0.315479,0.602525],[0.662565,0.6845209999999999,0.602525],[0.288671,0.15441000000000002,0.131541],[0.15441000000000002,0.711329,0.868459],[0.8455900000000001,0.288671,0.868459],[0.711329,0.8455900000000001,0.131541],[0.15441000000000002,0.288671,0.368459],[0.8455900000000001,0.711329,0.368459],[0.711329,0.15441000000000002,0.631541],[0.288671,0.8455900000000001,0.631541],[0.847729,0.244666,0.161634],[0.244666,0.152271,0.838366],[0.755334,0.847729,0.838366],[0.152271,0.755334,0.161634],[0.244666,0.847729,0.338366],[0.755334,0.152271,0.338366],[0.152271,0.244666,0.6616339999999999],[0.847729,0.755334,0.6616339999999999],[0.278615,0.346489,0.191972],[0.346489,0.7213849999999999,0.808028],[0.653511,0.278615,0.808028],[0.7213849999999999,0.653511,0.191972],[0.346489,0.278615,0.30802799999999997],[0.653511,0.7213849999999999,0.30802799999999997],[0.7213849999999999,0.346489,0.6919719999999999],[0.278615,0.653511,0.6919719999999999],[0.659249,0.192039,0.221301],[0.192039,0.34075099999999997,0.7786989999999999],[0.8079609999999999,0.659249,0.7786989999999999],[0.34075099999999997,0.8079609999999999,0.221301],[0.192039,0.659249,0.278699],[0.8079609999999999,0.34075099999999997,0.278699],[0.34075099999999997,0.192039,0.721301],[0.659249,0.8079609999999999,0.721301],[0.5,0.5,0.25],[0.5,0.5,0.75],[0.0,0.0,0.0],[0.0,0.0,0.5],[0.0,0.0,0.098742],[0.0,0.0,0.901258],[0.0,0.0,0.401258],[0.0,0.0,0.598742],[0.0,0.0,0.201255],[0.0,0.0,0.7987449999999999],[0.0,0.0,0.298745],[0.0,0.0,0.701255],[0.5,0.5,0.051587],[0.5,0.5,0.948413],[0.5,0.5,0.448413],[0.5,0.5,0.5515869999999999],[0.5,0.5,0.14940499999999998],[0.5,0.5,0.850595],[0.5,0.5,0.350595],[0.5,0.5,0.649405],[0.0,0.5,0.025973999999999997],[0.5,0.0,0.974026],[0.5,0.0,0.474026],[0.0,0.5,0.5259739999999999],[0.0,0.5,0.123509],[0.5,0.0,0.8764909999999999],[0.5,0.0,0.37649099999999996],[0.0,0.5,0.623509],[0.0,0.5,0.22655699999999998],[0.5,0.0,0.773443],[0.5,0.0,0.273443],[0.0,0.5,0.726557],[0.0,0.5,0.324693],[0.5,0.0,0.675307],[0.5,0.0,0.175307],[0.0,0.5,0.824693],[0.0,0.5,0.424272],[0.5,0.0,0.575728],[0.5,0.0,0.07572799999999999],[0.0,0.5,0.924272]],"numbers":[31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-550745.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[3.305251,-4.654707,0.007306,6.62194,4.658906,5.819153,-4.88112,0.037734,8.621987]},"positions":[[0.182542,0.911722,0.364122],[0.430888,0.161826,0.863815],[0.682249,0.411298,0.36508],[0.931878,0.661449,0.864463],[0.558687,0.788855,0.115467],[0.806634,0.03841,0.616156],[0.055868,0.289123,0.113688],[0.305438,0.538383,0.614877],[0.128693,0.636852,0.254803],[0.376511,0.887491,0.754639],[0.629152,0.138233,0.254585],[0.876801,0.388132,0.753746],[0.000706,0.008168,0.002781],[0.251156,0.257972,0.502345],[0.500549,0.508566,0.004056],[0.751856,0.758243,0.503565],[0.918455,0.84341,0.193594],[0.171922,0.094804,0.693317],[0.420113,0.344614,0.19382],[0.67185,0.595353,0.694454],[0.142,0.70785,0.463453],[0.389558,0.958496,0.961187],[0.639838,0.208391,0.461837],[0.890737,0.45748,0.963741],[0.409853,0.797809,0.284667],[0.664001,0.04673,0.784206],[0.911709,0.297282,0.286281],[0.163019,0.546279,0.787094],[0.219252,0.678266,0.01593],[0.467142,0.92785,0.513968],[0.717844,0.176955,0.014488],[0.967243,0.427032,0.516608],[0.434934,0.46529,0.441752],[0.686419,0.716015,0.941121],[0.935656,0.965368,0.44233],[0.185968,0.215084,0.940658],[0.689493,0.571896,0.230268],[0.94018,0.821535,0.729133],[0.185202,0.073178,0.227883],[0.436997,0.32331,0.728025]],"numbers":[22,22,22,22,26,26,26,26,83,83,83,83,83,83,83,83,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/mp-569901.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[6.032743,0.0,0.0,0.0,6.032743,0.0,0.0,0.0,64.130743]},"positions":[[0.16408799999999998,0.843363,0.423591],[0.206067,0.149771,0.966128],[0.810894,0.149364,0.118257],[0.830973,0.653787,0.40282],[0.284001,0.14993099999999998,0.359724],[0.349321,0.25420699999999996,0.837048],[0.784001,0.35006899999999996,0.8597239999999999],[0.849505,0.7751009999999999,0.185636],[0.15067899999999998,0.754207,0.33704799999999996],[0.8507100000000001,0.697001,0.792078],[0.35006899999999996,0.215999,0.14027599999999998],[0.149364,0.189106,0.8817429999999999],[0.653787,0.16902699999999998,0.59718],[0.325424,0.651569,0.770787],[0.650679,0.7457929999999999,0.837048],[0.349505,0.724899,0.685636],[0.19700099999999998,0.35071,0.7079219999999999],[0.681437,0.849868,0.05552000000000001],[0.35022899999999996,0.293933,0.533872],[0.64929,0.19700099999999998,0.292078],[0.275101,0.349505,0.314364],[0.843363,0.835912,0.576409],[0.724899,0.6504949999999999,0.314364],[0.697001,0.14929,0.207922],[0.224899,0.849505,0.814364],[0.849321,0.24579299999999998,0.33704799999999996],[0.650368,0.234863,0.988598],[0.181437,0.6501319999999999,0.55552],[0.8496319999999999,0.7348629999999999,0.488598],[0.850069,0.284001,0.640276],[0.664088,0.6566369999999999,0.9235909999999999],[0.754207,0.849321,0.662952],[0.802999,0.64929,0.7079219999999999],[0.24579299999999998,0.15067899999999998,0.662952],[0.649771,0.706067,0.533872],[0.793933,0.850229,0.966128],[0.150495,0.224899,0.185636],[0.8484309999999999,0.17457599999999998,0.729213],[0.159877,0.6598769999999999,0.25],[0.348431,0.325424,0.229213],[0.8254239999999999,0.8484309999999999,0.270787],[0.6501319999999999,0.8185629999999999,0.44448000000000004],[0.234863,0.349632,0.011401999999999999],[0.156637,0.16408799999999998,0.576409],[0.706067,0.35022899999999996,0.466128],[0.349632,0.765137,0.988598],[0.17457599999999998,0.15156899999999998,0.270787],[0.293933,0.649771,0.466128],[0.6690269999999999,0.15378699999999998,0.9028200000000001],[0.7348629999999999,0.150368,0.511402],[0.346213,0.830973,0.59718],[0.850636,0.810894,0.8817429999999999],[0.8185629999999999,0.34986799999999996,0.55552],[0.343363,0.664088,0.07640899999999999],[0.835912,0.156637,0.423591],[0.150368,0.265137,0.488598],[0.25420699999999996,0.650679,0.16295199999999999],[0.7457929999999999,0.349321,0.16295199999999999],[0.849868,0.318563,0.9444800000000001],[0.15156899999999998,0.8254239999999999,0.729213],[0.15378699999999998,0.33097299999999996,0.09718],[0.674576,0.348431,0.770787],[0.7751009999999999,0.150495,0.814364],[0.16902699999999998,0.346213,0.40282],[0.7159989999999999,0.850069,0.359724],[0.215999,0.6499309999999999,0.8597239999999999],[0.30299899999999996,0.8507100000000001,0.207922],[0.335912,0.343363,0.9235909999999999],[0.765137,0.650368,0.011401999999999999],[0.340123,0.159877,0.75],[0.310894,0.350636,0.618257],[0.265137,0.8496319999999999,0.511402],[0.6493639999999999,0.310894,0.381743],[0.150132,0.681437,0.9444800000000001],[0.350636,0.689106,0.381743],[0.189106,0.850636,0.118257],[0.850229,0.206067,0.033872],[0.33097299999999996,0.846213,0.9028200000000001],[0.14929,0.30299899999999996,0.792078],[0.14993099999999998,0.7159989999999999,0.640276],[0.689106,0.6493639999999999,0.618257],[0.6504949999999999,0.275101,0.685636],[0.35071,0.802999,0.292078],[0.6598769999999999,0.840123,0.75],[0.318563,0.150132,0.05552000000000001],[0.6566369999999999,0.335912,0.07640899999999999],[0.840123,0.340123,0.25],[0.846213,0.6690269999999999,0.09718],[0.651569,0.674576,0.229213],[0.6499309999999999,0.784001,0.14027599999999998],[0.34986799999999996,0.181437,0.44448000000000004],[0.149771,0.793933,0.033872],[0.0,0.5,0.826627],[0.5,0.0,0.173373],[0.5,0.0,0.786906],[0.5,0.5,0.039882999999999995],[0.0,0.0,0.767762],[0.0,0.0,0.460117],[0.5,0.5,0.42247799999999996],[0.5,0.0,0.866606],[0.0,0.0,0.8466849999999999],[0.0,0.0,0.232238],[0.5,0.0,0.633394],[0.0,0.5,0.21309399999999998],[0.0,0.5,0.366606],[0.5,0.5,0.653315],[0.0,0.0,0.30666299999999996],[0.0,0.5,0.13339399999999998],[0.5,0.5,0.7322379999999999],[0.5,0.0,0.326627],[0.5,0.0,0.25],[0.0,0.5,0.673373],[0.0,0.0,0.077522],[0.5,0.5,0.113741],[0.0,0.5,0.75],[0.5,0.5,0.577522],[0.0,0.0,0.153315],[0.5,0.0,0.09503099999999999],[0.0,0.5,0.44071099999999996],[0.0,0.5,0.059288999999999994],[0.5,0.0,0.020002],[0.5,0.5,0.8862589999999999],[0.5,0.0,0.940711],[0.5,0.5,0.34668499999999997],[0.5,0.5,0.960117],[0.0,0.5,0.9799979999999999],[0.0,0.5,0.595031],[0.5,0.5,0.267762],[0.5,0.5,0.806663],[0.0,0.0,0.0],[0.0,0.0,0.693337],[0.5,0.0,0.713094],[0.0,0.5,0.9049689999999999],[0.0,0.5,0.520002],[0.0,0.0,0.539883],[0.5,0.0,0.5592889999999999],[0.0,0.5,0.286906],[0.5,0.0,0.40496899999999997],[0.5,0.0,0.479998],[0.5,0.5,0.19333699999999998],[0.0,0.0,0.613741],[0.5,0.5,0.5],[0.0,0.0,0.9224779999999999],[0.0,0.0,0.38625899999999996]],"numbers":[32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/wbm-1-29497.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[3.9654072417,-0.0006472088,-0.7686595013,-0.2799924824,5.5110023687,-1.4490818386,0.0130490884,-0.1425390523,10.6864239498]},"positions":[[0.8004699061,0.8000010486,0.6000019079000001],[0.4007723449,0.39999603850000004,0.7999922155],[0.1995300549,0.1999990969,0.399998091],[0.5992276144,0.6000038303,0.2000077513],[-1.32e-8,1.234e-7,3.48e-8],[0.8005351034,0.3000024112,0.6000047708],[0.40081083100000003,0.8999938615,0.7999881145000001],[0.5991891318,0.1000058037,0.2000119394],[0.1994649555,0.6999974685,0.3999952432],[-5e-8,0.5000000182000001,-6.7e-8],[0.30046467720000003,0.5499962967,0.5999901493],[0.0992753086,0.3499964238,0.2000087642],[0.49999993730000003,0.2500220433,-5.91e-8],[0.3004646111,0.0499938896,0.5999901164],[0.0992752335,0.8500124103000001,0.2000087669],[0.6995353506,0.4500038052,0.4000098636],[0.900724735,0.650003536,0.7999912864000001],[0.6995354726,0.9500062277,0.40000992160000004],[0.9007247138000001,0.1499877499,0.7999912396000001],[0.5000000815,0.7499779168,-5.04e-8]],"numbers":[25,25,25,25,25,31,31,31,31,31,27,27,27,27,27,27,27,27,27,27]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/wbm-1-42389.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[5.0420747259,0.0,2.9110431991,1.6806915770000002,4.7537136393,2.9110431991,0.0,0.0,5.8220864]},"positions":[[0.249999,0.25,0.250001],[0.75,0.75,0.75],[0.500002,0.5,0.499999],[0.0,0.0,0.0],[0.238851,0.761149,0.7611479999999999],[0.761149,0.238851,0.7611479999999999],[0.238851,0.7611500000000001,0.23885],[0.7611500000000001,0.7611479999999999,0.23885],[0.761149,0.238852,0.23885],[0.238852,0.238851,0.7611479999999999]],"numbers":[20,20,12,74,8,8,8,8,8,8]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/wbm-1-42433.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[5.0420747259,0.0,2.9110431991,1.6806915770000002,4.7537136393,2.9110431991,0.0,0.0,5.8220864]},"positions":[[0.249999,0.25,0.250001],[0.75,0.75,0.75],[0.500002,0.5,0.499999],[0.0,0.0,0.0],[0.238851,0.761149,0.7611479999999999],[0.761149,0.238851,0.7611479999999999],[0.238851,0.7611500000000001,0.23885],[0.7611500000000001,0.7611479999999999,0.23885],[0.761149,0.238852,0.23885],[0.238852,0.238851,0.7611479999999999]],"numbers":[56,56,12,42,8,8,8,8,8,8]} 2 | -------------------------------------------------------------------------------- /moyo/tests/assets/wyckoff_edge_case.json: -------------------------------------------------------------------------------- 1 | {"lattice":{"basis":[6.57275068253895,0.0,1.1376274714501016,-6.610122181503525e-16,10.79514875,6.610122181503525e-16,0.0,0.0,9.91142379]},"positions":[[0.0,0.59536646,0.25],[0.0,0.40463354,0.75],[0.5,0.09536646,0.25],[0.5,0.90463354,0.75],[0.0,0.0,0.0],[0.0,0.0,0.5],[0.5,0.5,0.0],[0.5,0.5,0.5]],"numbers":[20,20,20,20,26,26,26,26]} 2 | -------------------------------------------------------------------------------- /moyo/tests/test_moyo_magnetic_dataset.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate approx; 3 | 4 | use nalgebra::{matrix, vector, Matrix3}; 5 | use test_log::test; 6 | 7 | use moyo::base::{ 8 | AngleTolerance, Collinear, Lattice, MagneticCell, MagneticMoment, RotationMagneticMomentAction, 9 | }; 10 | use moyo::MoyoMagneticDataset; 11 | 12 | /// Sanity-check MoyoMagneticDataset 13 | fn assert_magnetic_dataset( 14 | magnetic_cell: &MagneticCell, 15 | symprec: f64, 16 | angle_tolerance: AngleTolerance, 17 | mag_symprec: Option, 18 | action: RotationMagneticMomentAction, 19 | ) -> MoyoMagneticDataset { 20 | let dataset = 21 | MoyoMagneticDataset::new(magnetic_cell, symprec, angle_tolerance, mag_symprec, action) 22 | .unwrap(); 23 | 24 | // std_mag_cell 25 | let std_dataset = MoyoMagneticDataset::new( 26 | &dataset.std_mag_cell, 27 | symprec, 28 | angle_tolerance, 29 | mag_symprec, 30 | action, 31 | ) 32 | .unwrap(); 33 | assert_eq!(std_dataset.uni_number, dataset.uni_number); 34 | 35 | // prim_std_mag_cell 36 | let prim_std_dataset = MoyoMagneticDataset::new( 37 | &dataset.prim_std_mag_cell, 38 | symprec, 39 | angle_tolerance, 40 | mag_symprec, 41 | action, 42 | ) 43 | .unwrap(); 44 | assert_eq!(prim_std_dataset.uni_number, dataset.uni_number); 45 | 46 | // prim_std_linear should be an inverse of an integer matrix 47 | let prim_std_linear_inv = dataset 48 | .prim_std_linear 49 | .map(|e| e as f64) 50 | .try_inverse() 51 | .unwrap(); 52 | assert_relative_eq!( 53 | prim_std_linear_inv, 54 | prim_std_linear_inv.map(|e| e.round()), 55 | epsilon = 1e-8 56 | ); 57 | 58 | // Check std_rotation_matrix and std_linear 59 | assert_relative_eq!( 60 | dataset.std_rotation_matrix * magnetic_cell.cell.lattice.basis * dataset.std_linear, 61 | dataset.std_mag_cell.cell.lattice.basis, 62 | epsilon = 1e-8 63 | ); 64 | // Check std_rotation_matrix and prim_std_linear 65 | assert_relative_eq!( 66 | dataset.std_rotation_matrix * magnetic_cell.cell.lattice.basis * dataset.prim_std_linear, 67 | dataset.prim_std_mag_cell.cell.lattice.basis, 68 | epsilon = 1e-8 69 | ); 70 | // TODO: std_origin_shift 71 | // TODO: prim_origin_shift 72 | 73 | assert_eq!(dataset.mapping_std_prim.len(), magnetic_cell.num_atoms()); 74 | 75 | dataset 76 | } 77 | 78 | #[test] 79 | fn test_with_rutile() { 80 | let lattice = Lattice::new(Matrix3::identity()); 81 | let positions = vec![ 82 | // Ti (2a) 83 | vector![0.0, 0.0, 0.0], 84 | vector![0.5, 0.5, 0.5], 85 | // O (4f) 86 | vector![0.3, 0.3, 0.0], 87 | vector![0.7, 0.7, 0.0], 88 | vector![0.2, 0.8, 0.5], 89 | vector![0.8, 0.2, 0.5], 90 | ]; 91 | let numbers = vec![0, 0, 1, 1, 1, 1]; 92 | 93 | let symprec = 1e-4; 94 | let angle_tolerance = AngleTolerance::Default; 95 | let mag_symprec = None; 96 | let action = RotationMagneticMomentAction::Polar; 97 | 98 | { 99 | // Type-I, 136.495: -P 4n 2n 100 | let magmoms = vec![ 101 | Collinear(0.7), 102 | Collinear(0.7), 103 | Collinear(0.0), 104 | Collinear(0.0), 105 | Collinear(0.0), 106 | Collinear(0.0), 107 | ]; 108 | let magnetic_cell = 109 | MagneticCell::new(lattice.clone(), positions.clone(), numbers.clone(), magmoms); 110 | let dataset = assert_magnetic_dataset( 111 | &magnetic_cell, 112 | symprec, 113 | angle_tolerance, 114 | mag_symprec, 115 | action, 116 | ); 117 | 118 | assert_eq!(dataset.uni_number, 1155); 119 | } 120 | 121 | { 122 | // Type-II, "136.496": -P 4n 2n 1' 123 | let magmoms = vec![ 124 | Collinear(0.0), 125 | Collinear(0.0), 126 | Collinear(0.0), 127 | Collinear(0.0), 128 | Collinear(0.0), 129 | Collinear(0.0), 130 | ]; 131 | let magnetic_cell = 132 | MagneticCell::new(lattice.clone(), positions.clone(), numbers.clone(), magmoms); 133 | let dataset = assert_magnetic_dataset( 134 | &magnetic_cell, 135 | symprec, 136 | angle_tolerance, 137 | mag_symprec, 138 | action, 139 | ); 140 | 141 | assert_eq!(dataset.uni_number, 1156); 142 | } 143 | 144 | { 145 | // Type-III, "136.498": -P 4n' 2n' 146 | let magmoms = vec![ 147 | Collinear(0.7), 148 | Collinear(-0.7), 149 | Collinear(0.0), 150 | Collinear(0.0), 151 | Collinear(0.0), 152 | Collinear(0.0), 153 | ]; 154 | let magnetic_cell = 155 | MagneticCell::new(lattice.clone(), positions.clone(), numbers.clone(), magmoms); 156 | let dataset = assert_magnetic_dataset( 157 | &magnetic_cell, 158 | symprec, 159 | angle_tolerance, 160 | mag_symprec, 161 | action, 162 | ); 163 | 164 | assert_eq!(dataset.uni_number, 1158); 165 | assert_eq!(dataset.num_magnetic_operations(), 16); 166 | assert_eq!(dataset.orbits, vec![0, 0, 2, 2, 2, 2]); 167 | assert_eq!(dataset.std_mag_cell.num_atoms(), 6); 168 | assert_eq!(dataset.prim_std_mag_cell.num_atoms(), 6); 169 | assert_eq!(dataset.mapping_std_prim, vec![0, 1, 2, 3, 4, 5]); 170 | } 171 | } 172 | 173 | #[test] 174 | fn test_with_rutile_type4() { 175 | let lattice = Lattice::new(matrix![ 176 | 5.0, 0.0, 0.0; 177 | 0.0, 5.0, 0.0; 178 | 0.0, 0.0, 6.0; 179 | ]); 180 | let positions = vec![ 181 | // Ti (2a) 182 | vector![0.0, 0.0, 0.0], 183 | vector![0.5, 0.5, 0.25], 184 | // O (4f) 185 | vector![0.3, 0.3, 0.0], 186 | vector![0.7, 0.7, 0.0], 187 | vector![0.2, 0.8, 0.25], 188 | vector![0.8, 0.2, 0.25], 189 | // Ti (2a) 190 | vector![0.0, 0.0, 0.5], 191 | vector![0.5, 0.5, 0.75], 192 | // O (4f) 193 | vector![0.3, 0.3, 0.5], 194 | vector![0.7, 0.7, 0.5], 195 | vector![0.2, 0.8, 0.75], 196 | vector![0.8, 0.2, 0.75], 197 | ]; 198 | let numbers = vec![0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1]; 199 | let magmoms = vec![ 200 | // Ti (2a) 201 | Collinear(0.3), 202 | Collinear(0.3), 203 | // O (4f) 204 | Collinear(0.0), 205 | Collinear(0.0), 206 | Collinear(0.0), 207 | Collinear(0.0), 208 | // Ti (2a) 209 | Collinear(-0.3), 210 | Collinear(-0.3), 211 | // O (4f) 212 | Collinear(0.0), 213 | Collinear(0.0), 214 | Collinear(0.0), 215 | Collinear(0.0), 216 | ]; 217 | let magnetic_cell = MagneticCell::new(lattice, positions, numbers, magmoms); 218 | 219 | let symprec = 1e-4; 220 | let angle_tolerance = AngleTolerance::Default; 221 | let mag_symprec = None; 222 | let action = RotationMagneticMomentAction::Polar; 223 | 224 | let dataset = assert_magnetic_dataset( 225 | &magnetic_cell, 226 | symprec, 227 | angle_tolerance, 228 | mag_symprec, 229 | action, 230 | ); 231 | 232 | assert_eq!(dataset.uni_number, 932); 233 | } 234 | -------------------------------------------------------------------------------- /moyopy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moyopy" 3 | authors.workspace = true 4 | description.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [package.metadata.release] 11 | release = false 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | [lib] 15 | name = "moyopy" 16 | crate-type = ["cdylib"] 17 | 18 | [dependencies] 19 | moyo = { path = "../moyo", version = "0.4.3" } 20 | nalgebra.workspace = true 21 | serde.workspace = true 22 | serde_json.workspace = true 23 | approx.workspace = true 24 | log.workspace = true 25 | pyo3-log = "0.12" 26 | pythonize = "0.24" 27 | 28 | 29 | [dependencies.pyo3] 30 | version = "0.24" 31 | features = ["abi3-py39"] 32 | -------------------------------------------------------------------------------- /moyopy/README.md: -------------------------------------------------------------------------------- 1 | # moyopy 2 | 3 | [![CI](https://github.com/spglib/moyo/actions/workflows/ci-python.yaml/badge.svg)](https://github.com/spglib/moyo/actions/workflows/ci-python.yaml) 4 | [![image](https://img.shields.io/pypi/l/moyopy.svg)](https://pypi.python.org/pypi/moyopy) 5 | [![image](https://img.shields.io/pypi/v/moyopy.svg)](https://pypi.python.org/pypi/moyopy) 6 | [![image](https://img.shields.io/pypi/pyversions/moyopy.svg)](https://pypi.python.org/pypi/moyopy) 7 | 8 | Python interface of [moyo](https://github.com/spglib/moyo), a fast and robust crystal symmetry finder. 9 | 10 | - Document: 11 | - PyPI: 12 | 13 | ## Installation 14 | 15 | ```shell 16 | pip install moyopy 17 | # If you want to convert structures into Pymatgen or ASE objects 18 | pip install moyopy[interface] 19 | ``` 20 | -------------------------------------------------------------------------------- /moyopy/docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spglib/moyo/e3be55a41ab067de2f14946fa848ec69c4fbba26/moyopy/docs/_static/.gitkeep -------------------------------------------------------------------------------- /moyopy/docs/conf.py: -------------------------------------------------------------------------------- 1 | # -- Project information ----------------------------------------------------- 2 | from moyopy import __version__ as version 3 | 4 | project = "moyopy" 5 | copyright = "2023, Kohei Shinohara" 6 | repository_url = "https://github.com/spglib/moyo" 7 | 8 | # -- General configuration --------------------------------------------------- 9 | extensions = [ 10 | "sphinx.ext.napoleon", 11 | "sphinx.ext.viewcode", 12 | "sphinx.ext.mathjax", 13 | "sphinx.ext.intersphinx", 14 | "sphinxcontrib.bibtex", 15 | # "nbsphinx", 16 | "myst_parser", 17 | "autoapi.extension", 18 | ] 19 | 20 | # Add any paths that contain templates here, relative to this directory. 21 | templates_path = ["_templates"] 22 | 23 | # List of patterns, relative to source directory, that match files and 24 | # directories to ignore when looking for source files. 25 | # This pattern also affects html_static_path and html_extra_path. 26 | exclude_patterns = ["_build"] 27 | 28 | # The suffix(es) of source filenames. 29 | source_suffix = { 30 | ".md": "markdown", 31 | ".rst": "restructuredtext", 32 | } 33 | 34 | # ----------------------------------------------------------------------------- 35 | # napoleon 36 | # ----------------------------------------------------------------------------- 37 | 38 | # napoleon_type_aliases = {} 39 | napoleon_use_rtype = True 40 | napoleon_use_ivar = True 41 | 42 | # ----------------------------------------------------------------------------- 43 | # bibtex 44 | # ----------------------------------------------------------------------------- 45 | 46 | # https://pypi.org/project/sphinxcontrib-bibtex/ 47 | bibtex_bibfiles = ["references.bib"] 48 | 49 | # ----------------------------------------------------------------------------- 50 | # intersphinx 51 | # ----------------------------------------------------------------------------- 52 | intersphinx_mapping = { 53 | "spglib": ("https://spglib.readthedocs.io/en/stable/", None), 54 | } 55 | 56 | # ----------------------------------------------------------------------------- 57 | # MyST 58 | # ----------------------------------------------------------------------------- 59 | myst_enable_extensions = [ 60 | "amsmath", 61 | "dollarmath", 62 | "html_admonition", 63 | "html_image", 64 | "linkify", 65 | "replacements", 66 | "smartquotes", 67 | "tasklist", 68 | ] 69 | myst_dmath_double_inline = True 70 | myst_heading_anchors = 3 71 | 72 | # ----------------------------------------------------------------------------- 73 | # autoapi 74 | # ----------------------------------------------------------------------------- 75 | autoapi_dirs = [ 76 | "../python/moyopy", 77 | ] 78 | 79 | # ----------------------------------------------------------------------------- 80 | # sphinx-book-theme 81 | # ----------------------------------------------------------------------------- 82 | html_theme = "sphinx_book_theme" 83 | html_title = project + " " + version 84 | html_theme_options = { 85 | "repository_url": repository_url, 86 | "use_repository_button": True, 87 | "navigation_with_keys": True, 88 | "globaltoc_includehidden": "true", 89 | "show_toc_level": 3, 90 | } 91 | 92 | # hide sphinx footer 93 | html_show_sphinx = False 94 | html_show_sourcelink = False 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ["_static"] 100 | -------------------------------------------------------------------------------- /moyopy/docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Basic Usage 4 | 5 | This example demonstrates the basic usage of the `moyopy` package. 6 | First, we create a {py:class}`moyopy.Cell` representing a crystal structure, and then create a {py:class}`moyopy.MoyoDataset`. 7 | The {py:class}`moyopy.MoyoDataset` contains symmetry information of the input crystal structure: for example, the space group number, symmetry operations, and standardized cell. 8 | When we need a secondary symmetry information such as Hermann-Mauguin symbol for the space group type, we can use the {py:class}`moyopy.HallSymbolEntry` to access the symmetry database. 9 | 10 | ```{literalinclude} ../../examples/basic.py 11 | ``` 12 | 13 | ## Accessing space-group type information 14 | 15 | You can access the space-group classification information using {py:class}`moyopy.SpaceGroupType`. 16 | 17 | ```{literalinclude} ../../examples/space_group_type.py 18 | ``` 19 | 20 | You can reverse-lookup an ITA space group number from a Hermann-Mauguin symbol by looping over {py:class}`moyopy.SpaceGroupType`. 21 | 22 | ```{literalinclude} ../../examples/hm_to_number.py 23 | ``` 24 | -------------------------------------------------------------------------------- /moyopy/docs/index.md: -------------------------------------------------------------------------------- 1 | ```{toctree} 2 | --- 3 | caption: Contents 4 | maxdepth: 1 5 | hidden: 6 | --- 7 | Introduction 8 | Examples 9 | ``` 10 | 11 | ```{include} ../README.md 12 | :relative-docs: docs/ 13 | ``` 14 | -------------------------------------------------------------------------------- /moyopy/docs/references.bib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spglib/moyo/e3be55a41ab067de2f14946fa848ec69c4fbba26/moyopy/docs/references.bib -------------------------------------------------------------------------------- /moyopy/examples/basic.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | import moyopy 4 | 5 | # https://next-gen.materialsproject.org/materials/mp-560588 6 | a = 3.81 7 | c = 6.24 8 | basis = [ 9 | [a, 0.0, 0.0], 10 | [-a / 2.0, a * sqrt(3.0) / 2.0, 0.0], 11 | [0.0, 0.0, c], 12 | ] 13 | z1_2b = 0.00014 14 | z2_2b = 0.37486 15 | positions = [ 16 | # 2b 17 | [1 / 3, 2 / 3, z1_2b], 18 | [2 / 3, 1 / 3, z1_2b + 0.5], 19 | # 2b 20 | [1 / 3, 2 / 3, z2_2b], 21 | [2 / 3, 1 / 3, z2_2b + 0.5], 22 | ] 23 | numbers = [0, 0, 1, 1] 24 | cell = moyopy.Cell(basis, positions, numbers) 25 | 26 | dataset = moyopy.MoyoDataset(cell, symprec=1e-4, angle_tolerance=None, setting=None) 27 | assert dataset.number == 186 28 | assert dataset.hall_number == 480 29 | 30 | hall_symbol_entry = moyopy.HallSymbolEntry(hall_number=dataset.hall_number) 31 | assert hall_symbol_entry.hm_short == "P 6_3 m c" 32 | 33 | # MoyoDataset can be serialized to Python dictionary 34 | dataset_as_dict = dataset.as_dict() 35 | dataset2 = moyopy.MoyoDataset.from_dict(dataset_as_dict) 36 | assert dataset2.number == dataset.number 37 | 38 | # MoyoDataset can be serialized to JSON string 39 | dataset_as_json = dataset.serialize_json() 40 | assert isinstance(dataset_as_json, str) 41 | dataset3 = moyopy.MoyoDataset.deserialize_json(dataset_as_json) 42 | assert dataset3.number == dataset.number 43 | -------------------------------------------------------------------------------- /moyopy/examples/hm_to_number.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: E501 2 | from moyopy import SpaceGroupType 3 | 4 | map_hm_short_to_number = { 5 | # SpaceGroupType(number).hm_short is separated by a space like "F m -3 m" 6 | SpaceGroupType(number).hm_short.replace(" ", ""): number 7 | for number in range(1, 230 + 1) 8 | } 9 | 10 | print(map_hm_short_to_number.get("Fm-3m")) # -> 225 11 | print(map_hm_short_to_number.get("P6_3/m")) # -> 176 12 | print(map_hm_short_to_number.get("Fmmm")) # -> 69 13 | -------------------------------------------------------------------------------- /moyopy/examples/pymatgen_structure.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from pymatgen.core import Structure 5 | 6 | import moyopy 7 | from moyopy.interface import MoyoAdapter 8 | 9 | 10 | class MoyoSpacegroupAnalyzer: 11 | def __init__( 12 | self, 13 | structure: Structure, 14 | symprec: float = 1e-4, 15 | angle_tolerance: float | None = None, 16 | setting: moyopy.Setting | None = None, 17 | ): 18 | self._cell = MoyoAdapter.from_structure(structure) 19 | self._dataset = moyopy.MoyoDataset( 20 | cell=self._cell, 21 | symprec=symprec, 22 | angle_tolerance=angle_tolerance, 23 | setting=setting, 24 | ) 25 | 26 | @property 27 | def number(self) -> int: 28 | return self._dataset.number 29 | 30 | @property 31 | def hall_number(self) -> int: 32 | return self._dataset.hall_number 33 | 34 | @property 35 | def rotations(self) -> np.ndarray: 36 | return np.array(self._dataset.operations.rotations) 37 | 38 | @property 39 | def translations(self) -> np.ndarray: 40 | return np.array(self._dataset.operations.translations) 41 | 42 | @property 43 | def orbits(self) -> list[int]: 44 | return self._dataset.orbits 45 | 46 | @property 47 | def wyckoffs(self) -> list[str]: 48 | return self._dataset.wyckoffs 49 | 50 | @property 51 | def site_symmetry_symbols(self) -> list[str]: 52 | return self._dataset.site_symmetry_symbols 53 | 54 | @property 55 | def std_structure(self) -> Structure: 56 | return MoyoAdapter.get_structure(self._dataset.std_cell) 57 | 58 | @property 59 | def std_linear(self) -> np.ndarray: 60 | return np.array(self._dataset.std_linear) 61 | 62 | @property 63 | def std_origin_shift(self) -> np.ndarray: 64 | return np.array(self._dataset.std_origin_shift) 65 | 66 | @property 67 | def std_rotation_matrix(self) -> np.ndarray: 68 | return np.array(self._dataset.std_rotation_matrix) 69 | 70 | @property 71 | def prim_std_structure(self) -> Structure: 72 | return MoyoAdapter.get_structure(self._dataset.prim_std_cell) 73 | 74 | @property 75 | def prim_std_linear(self) -> np.ndarray: 76 | return np.array(self._dataset.prim_std_linear) 77 | 78 | @property 79 | def prim_std_origin_shift(self) -> np.ndarray: 80 | return np.array(self._dataset.prim_std_origin_shift) 81 | 82 | @property 83 | def mapping_std_prim(self) -> list[int]: 84 | return self._dataset.mapping_std_prim 85 | 86 | @property 87 | def symprec(self) -> float: 88 | return self._dataset.symprec 89 | 90 | @property 91 | def angle_tolerance(self) -> float | None: 92 | return self._dataset.angle_tolerance 93 | 94 | 95 | if __name__ == "__main__": 96 | a = 4.0 97 | structure = Structure( 98 | lattice=np.eye(3) * a, 99 | species=["Al"] * 4, 100 | coords=np.array( 101 | [ 102 | [0.0, 0.0, 0.0], 103 | [0.0, 0.5, 0.5], 104 | [0.5, 0.0, 0.5], 105 | [0.5, 0.5, 0.0], 106 | ] 107 | ), 108 | ) 109 | msa = MoyoSpacegroupAnalyzer(structure) 110 | assert msa.number == 225 111 | assert msa.rotations.shape == (48 * 4, 3, 3) 112 | assert msa.orbits == [0, 0, 0, 0] 113 | assert msa.prim_std_structure.num_sites == 1 114 | -------------------------------------------------------------------------------- /moyopy/examples/space_group_type.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: E501 2 | from moyopy import SpaceGroupType 3 | 4 | s = SpaceGroupType(15) # ITA space group number (1 - 230) 5 | assert s.hm_short == "C 2/c" 6 | assert s.crystal_system == "Monoclinic" 7 | 8 | print(s) 9 | # -> PySpaceGroupType { number: 15, hm_short: "C 2/c", hm_full: "C 1 2/c 1", arithmetic_number: 8, arithmetic_symbol: "2/mC", geometric_crystal_class: "2/m", crystal_system: "Monoclinic", bravais_class: "mC", lattice_system: "Monoclinic", crystal_family: "Monoclinic" } 10 | -------------------------------------------------------------------------------- /moyopy/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1,<2"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "moyopy" 7 | authors = [ 8 | {name = "Kohei Shinohara", email = "kshinohara0508@gmail.com"}, 9 | ] 10 | description = "Python binding of Moyo" 11 | readme = "README.md" 12 | license = "MIT OR Apache-2.0" 13 | requires-python = ">=3.9" 14 | dynamic = [ 15 | 'version', 16 | ] 17 | classifiers = [ 18 | "Development Status :: 3 - Alpha", 19 | "Intended Audience :: Science/Research", 20 | "License :: OSI Approved :: Apache Software License", 21 | "License :: OSI Approved :: MIT License", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Topic :: Scientific/Engineering :: Mathematics", 28 | "Topic :: Scientific/Engineering :: Physics", 29 | ] 30 | dependencies = [ 31 | "typing-extensions", 32 | ] 33 | 34 | [project.optional-dependencies] 35 | interface = [ 36 | "numpy", 37 | "pymatgen", 38 | "ase>=3.23", 39 | ] 40 | testing = [ 41 | "moyopy[interface]", 42 | "pytest", 43 | "pre-commit", 44 | "numpy", 45 | ] 46 | docs = [ 47 | "Sphinx >= 7.0", 48 | "sphinx-autobuild", 49 | "sphinxcontrib-bibtex >= 2.5", 50 | "sphinx-book-theme", 51 | "sphinx-autoapi", 52 | "myst-parser >= 2.0", 53 | "linkify-it-py", 54 | ] 55 | dev = [ 56 | "moyopy[testing]", 57 | "moyopy[docs]", 58 | "nbconvert", 59 | ] 60 | 61 | [tool.maturin] 62 | python-source = "python" 63 | module-name = "moyopy._moyopy" 64 | # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) 65 | features = ["pyo3/extension-module"] 66 | 67 | [tool.ruff] 68 | line-length = 99 69 | [tool.ruff.lint] 70 | extend-select = [ 71 | "F", # pyflakes 72 | "E", # pycodestyle-errors 73 | "I", # isort 74 | # "D", # pydocstyle 75 | "UP", # pyupgrade 76 | ] 77 | extend-ignore = [ 78 | "D100", 79 | "D101", 80 | "D102", 81 | "D103", 82 | "D203", # Conflict with D211 83 | "D205", 84 | "D213", # Conflict with D212 85 | ] 86 | isort.known-first-party = ["moyopy"] 87 | 88 | [tool.mypy] 89 | mypy_path = ["python"] 90 | python_version = "3.11" 91 | warn_unused_configs = true 92 | show_error_codes = true 93 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] 94 | no_strict_optional = true 95 | -------------------------------------------------------------------------------- /moyopy/python/moyopy/__init__.py: -------------------------------------------------------------------------------- 1 | from ._moyopy import * # noqa: F403 2 | -------------------------------------------------------------------------------- /moyopy/python/moyopy/_base.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | class Cell: 4 | def __init__( 5 | self, 6 | basis: list[list[float]], 7 | positions: list[list[float]], 8 | numbers: list[int], 9 | ): ... 10 | @property 11 | def basis(self) -> list[list[float]]: ... 12 | @property 13 | def positions(self) -> list[list[float]]: ... 14 | @property 15 | def numbers(self) -> list[int]: ... 16 | @property 17 | def num_atoms(self) -> int: ... 18 | # Serialization and deserialization 19 | def serialize_json(self) -> str: 20 | """Serialize the cell to a JSON string""" 21 | @classmethod 22 | def deserialize_json(cls, json_str: str) -> Cell: 23 | """Deserialize a JSON string to a Cell object""" 24 | def as_dict(self) -> dict[str, Any]: 25 | """Convert the cell to a dictionary""" 26 | @classmethod 27 | def from_dict(cls, data: dict[str, Any]) -> Cell: 28 | """Create a cell from a dictionary""" 29 | 30 | class CollinearMagneticCell: 31 | def __init__( 32 | self, 33 | basis: list[list[float]], 34 | positions: list[list[float]], 35 | numbers: list[int], 36 | magnetic_moments: list[float], 37 | ): ... 38 | @property 39 | def basis(self) -> list[list[float]]: ... 40 | @property 41 | def positions(self) -> list[list[float]]: ... 42 | @property 43 | def numbers(self) -> list[int]: ... 44 | @property 45 | def magnetic_moments(self) -> list[float]: ... 46 | @property 47 | def num_atoms(self) -> int: ... 48 | # Serialization and deserialization 49 | def serialize_json(self) -> str: 50 | """Serialize the cell to a JSON string""" 51 | @classmethod 52 | def deserialize_json(cls, json_str: str) -> Cell: 53 | """Deserialize a JSON string to a Cell object""" 54 | def as_dict(self) -> dict[str, Any]: 55 | """Convert the cell to a dictionary""" 56 | @classmethod 57 | def from_dict(cls, data: dict[str, Any]) -> Cell: 58 | """Create a cell from a dictionary""" 59 | 60 | class NonCollinearMagneticCell: 61 | def __init__( 62 | self, 63 | basis: list[list[float]], 64 | positions: list[list[float]], 65 | numbers: list[int], 66 | magnetic_moments: list[list[float]], 67 | ): ... 68 | @property 69 | def basis(self) -> list[list[float]]: ... 70 | @property 71 | def positions(self) -> list[list[float]]: ... 72 | @property 73 | def numbers(self) -> list[int]: ... 74 | @property 75 | def magnetic_moments(self) -> list[list[float]]: ... 76 | @property 77 | def num_atoms(self) -> int: ... 78 | # Serialization and deserialization 79 | def serialize_json(self) -> str: 80 | """Serialize the cell to a JSON string""" 81 | @classmethod 82 | def deserialize_json(cls, json_str: str) -> Cell: 83 | """Deserialize a JSON string to a Cell object""" 84 | def as_dict(self) -> dict[str, Any]: 85 | """Convert the cell to a dictionary""" 86 | @classmethod 87 | def from_dict(cls, data: dict[str, Any]) -> Cell: 88 | """Create a cell from a dictionary""" 89 | 90 | class Operations: 91 | @property 92 | def rotations(self) -> list[list[list[float]]]: ... 93 | @property 94 | def translations(self) -> list[list[float]]: ... 95 | @property 96 | def num_operations(self) -> int: ... 97 | def __len__(self) -> int: ... 98 | 99 | class MagneticOperations: 100 | @property 101 | def rotations(self) -> list[list[list[float]]]: ... 102 | @property 103 | def translations(self) -> list[list[float]]: ... 104 | @property 105 | def time_reversals(self) -> list[bool]: ... 106 | @property 107 | def num_operations(self) -> int: ... 108 | def __len__(self) -> int: ... 109 | -------------------------------------------------------------------------------- /moyopy/python/moyopy/_data.pyi: -------------------------------------------------------------------------------- 1 | from moyopy._base import Operations 2 | 3 | class Setting: 4 | """Preference for the setting of the space group.""" 5 | @classmethod 6 | def spglib(cls) -> Setting: 7 | """The setting of the smallest Hall number.""" 8 | @classmethod 9 | def standard(cls) -> Setting: 10 | """Unique axis b, cell choice 1 for monoclinic, hexagonal axes for rhombohedral, 11 | and origin choice 2 for centrosymmetric space groups.""" 12 | @classmethod 13 | def hall_number(cls, hall_number: int) -> Setting: 14 | """Specific Hall number from 1 to 530.""" 15 | 16 | class Centering: 17 | @property 18 | def order(self) -> int: 19 | """Order of the centering.""" 20 | @property 21 | def linear(self) -> list[list[int]]: 22 | """Transformation matrix.""" 23 | @property 24 | def lattice_points(self) -> list[list[float]]: 25 | """Unique lattice points.""" 26 | 27 | class HallSymbolEntry: 28 | """An entry containing space-group information for a specified hall_number.""" 29 | def __init__(self, hall_number: int): ... 30 | @property 31 | def hall_number(self) -> int: 32 | """Number for Hall symbols (1 - 530).""" 33 | @property 34 | def number(self) -> int: 35 | """ITA number for space group types (1 - 230).""" 36 | @property 37 | def arithmetic_number(self) -> int: 38 | """Number for arithmetic crystal classes (1 - 73).""" 39 | @property 40 | def setting(self) -> Setting: 41 | """Setting.""" 42 | @property 43 | def hall_symbol(self) -> str: 44 | """Hall symbol.""" 45 | @property 46 | def hm_short(self) -> str: 47 | """Hermann-Mauguin symbol in short notation.""" 48 | @property 49 | def hm_full(self) -> str: 50 | """Hermann-Mauguin symbol in full notation.""" 51 | @property 52 | def centering(self) -> Centering: 53 | """Centering.""" 54 | 55 | class SpaceGroupType: 56 | """Space-group type information.""" 57 | def __init__(self, number: int): ... 58 | # Space group type 59 | @property 60 | def number(self) -> int: 61 | """ITA number for space group types (1 - 230).""" 62 | @property 63 | def hm_short(self) -> str: 64 | """Hermann-Mauguin symbol in short notation.""" 65 | @property 66 | def hm_full(self) -> str: 67 | """Hermann-Mauguin symbol in full notation.""" 68 | # Arithmetic crystal class 69 | @property 70 | def arithmetic_number(self) -> int: 71 | """Number for arithmetic crystal classes (1 - 73).""" 72 | @property 73 | def arithmetic_symbol(self) -> str: 74 | """Symbol for arithmetic crystal class. 75 | 76 | See https://github.com/spglib/moyo/blob/main/moyo/src/data/arithmetic_crystal_class.rs 77 | for string values. 78 | """ 79 | # Other classifications 80 | @property 81 | def geometric_crystal_class(self) -> str: 82 | """Geometric crystal class. 83 | 84 | See https://github.com/spglib/moyo/blob/main/moyo/src/data/classification.rs 85 | for string values. 86 | """ 87 | @property 88 | def crystal_system(self) -> str: 89 | """Crystal system. 90 | 91 | See https://github.com/spglib/moyo/blob/main/moyo/src/data/classification.rs 92 | for string values. 93 | """ 94 | @property 95 | def bravais_class(self) -> str: 96 | """Bravais class. 97 | 98 | See https://github.com/spglib/moyo/blob/main/moyo/src/data/classification.rs 99 | for string values. 100 | """ 101 | @property 102 | def lattice_system(self) -> str: 103 | """Lattice system. 104 | 105 | See https://github.com/spglib/moyo/blob/main/moyo/src/data/classification.rs 106 | for string values. 107 | """ 108 | @property 109 | def crystal_family(self) -> str: 110 | """Crystal family. 111 | 112 | See https://github.com/spglib/moyo/blob/main/moyo/src/data/classification.rs 113 | for string values. 114 | """ 115 | 116 | class MagneticSpaceGroupType: 117 | """Magnetic space-group type information.""" 118 | def __init__(self, uni_number: int): ... 119 | @property 120 | def uni_number(self) -> int: 121 | """Serial number of UNI (and BNS) symbols.""" 122 | @property 123 | def litvin_number(self) -> int: 124 | """Serial number in Litvin's `Magnetic group tables `_.""" 125 | @property 126 | def bns_number(self) -> str: 127 | """BNS number e.g. '151.32'""" 128 | @property 129 | def og_number(self) -> str: 130 | """OG number e.g. '153.4.1270'""" 131 | @property 132 | def number(self) -> int: 133 | """ITA number for reference space group in BNS setting.""" 134 | @property 135 | def construct_type(self) -> int: 136 | """Construct type of magnetic space group from 1 to 4.""" 137 | 138 | def operations_from_number( 139 | number: int, *, setting: Setting | None = None, primitive: bool = False 140 | ) -> Operations: ... 141 | -------------------------------------------------------------------------------- /moyopy/python/moyopy/_moyopy.pyi: -------------------------------------------------------------------------------- 1 | from moyopy._base import ( # noqa: F401 2 | Cell, 3 | CollinearMagneticCell, 4 | NonCollinearMagneticCell, 5 | Operations, 6 | ) 7 | from moyopy._data import ( 8 | Centering, 9 | HallSymbolEntry, 10 | MagneticSpaceGroupType, 11 | Setting, 12 | SpaceGroupType, 13 | operations_from_number, 14 | ) # noqa: F401 15 | from moyopy._dataset import ( 16 | MoyoCollinearMagneticDataset, 17 | MoyoDataset, 18 | MoyoNonCollinearMagneticDataset, 19 | ) # noqa: F401 20 | 21 | __version__: str 22 | 23 | __all__ = [ 24 | # base 25 | "Cell", 26 | "CollinearMagneticCell", 27 | "NonCollinearMagneticCell", 28 | "Operations", 29 | # data 30 | "Setting", 31 | "Centering", 32 | "HallSymbolEntry", 33 | "SpaceGroupType", 34 | "MagneticSpaceGroupType", 35 | "operations_from_number", 36 | # dataset 37 | "MoyoDataset", 38 | "MoyoCollinearMagneticDataset", 39 | "MoyoNonCollinearMagneticDataset", 40 | # lib 41 | "__version__", 42 | ] 43 | -------------------------------------------------------------------------------- /moyopy/python/moyopy/interface.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | try: 4 | import ase 5 | from pymatgen.core import Element, Structure 6 | from pymatgen.io.ase import MSONAtoms 7 | except ImportError: 8 | raise ImportError("Try installing dependencies with `pip install moyopy[interface]`") 9 | 10 | import moyopy 11 | 12 | 13 | class MoyoAdapter: 14 | @staticmethod 15 | def get_structure(cell: moyopy.Cell) -> Structure: 16 | species = [Element.from_Z(number) for number in cell.numbers] 17 | return Structure(lattice=cell.basis, species=species, coords=cell.positions) 18 | 19 | @staticmethod 20 | def get_atoms(cell: moyopy.Cell) -> ase.Atoms: 21 | atoms = ase.Atoms( 22 | cell=cell.basis, 23 | scaled_positions=cell.positions, 24 | numbers=cell.numbers, 25 | pbc=True, 26 | ) 27 | return atoms 28 | 29 | @staticmethod 30 | def from_structure(structure: Structure) -> moyopy.Cell: 31 | basis = structure.lattice.matrix 32 | positions = structure.frac_coords 33 | numbers = [site.specie.Z for site in structure] 34 | 35 | return moyopy.Cell( 36 | basis=basis.tolist(), 37 | positions=positions.tolist(), 38 | numbers=numbers, 39 | ) 40 | 41 | @staticmethod 42 | def from_atoms(atoms: ase.Atoms) -> moyopy.Cell: 43 | basis = list(atoms.cell) 44 | positions = atoms.get_scaled_positions() 45 | numbers = atoms.get_atomic_numbers() 46 | 47 | return moyopy.Cell( 48 | basis=basis, 49 | positions=positions, 50 | numbers=numbers, 51 | ) 52 | 53 | @staticmethod 54 | def from_py_obj(struct: Structure | ase.Atoms | MSONAtoms) -> moyopy.Cell: 55 | """Convert a Python atomic structure object to a Moyo Cell. 56 | 57 | Args: 58 | struct: Currently supports pymatgen Structure, ASE Atoms, and MSONAtoms 59 | 60 | Returns: 61 | moyopy.Cell: The converted Moyo cell 62 | """ 63 | if isinstance(struct, (ase.Atoms, MSONAtoms)): 64 | return MoyoAdapter.from_atoms(struct) 65 | elif isinstance(struct, Structure): 66 | return MoyoAdapter.from_structure(struct) 67 | else: 68 | cls_name = type(struct).__name__ 69 | raise TypeError(f"Expected Structure, Atoms, or MSONAtoms, got {cls_name}") 70 | -------------------------------------------------------------------------------- /moyopy/python/moyopy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spglib/moyo/e3be55a41ab067de2f14946fa848ec69c4fbba26/moyopy/python/moyopy/py.typed -------------------------------------------------------------------------------- /moyopy/python/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | import pytest 4 | 5 | from moyopy import Cell, CollinearMagneticCell 6 | 7 | 8 | @pytest.fixture 9 | def wurtzite() -> Cell: 10 | # https://next-gen.materialsproject.org/materials/mp-560588 11 | a = 3.81 12 | c = 6.24 13 | basis = [ 14 | [a, 0.0, 0.0], 15 | [-a / 2.0, a * sqrt(3.0) / 2.0, 0.0], 16 | [0.0, 0.0, c], 17 | ] 18 | z1_2b = 0.00014 19 | z2_2b = 0.37486 20 | positions = [ 21 | # 2b 22 | [1 / 3, 2 / 3, z1_2b], 23 | [2 / 3, 1 / 3, z1_2b + 0.5], 24 | # 2b 25 | [1 / 3, 2 / 3, z2_2b], 26 | [2 / 3, 1 / 3, z2_2b + 0.5], 27 | ] 28 | numbers = [1, 1, 2, 2] 29 | 30 | cell = Cell(basis, positions, numbers) 31 | return cell 32 | 33 | 34 | @pytest.fixture 35 | def rutile_type3() -> CollinearMagneticCell: 36 | basis = [ 37 | [1.0, 0.0, 0.0], 38 | [0.0, 1.0, 0.0], 39 | [0.0, 0.0, 1.0], 40 | ] 41 | positions = [ 42 | # Ti (2a) 43 | [0.0, 0.0, 0.0], 44 | [0.5, 0.5, 0.5], 45 | # O (4f) 46 | [0.3, 0.3, 0.0], 47 | [0.7, 0.7, 0.0], 48 | [0.2, 0.8, 0.5], 49 | [0.8, 0.2, 0.5], 50 | ] 51 | numbers = [0, 0, 1, 1, 1, 1] 52 | magnetic_moments = [0.7, -0.7, 0.0, 0.0, 0.0, 0.0] 53 | 54 | magnetic_cell = CollinearMagneticCell(basis, positions, numbers, magnetic_moments) 55 | return magnetic_cell 56 | -------------------------------------------------------------------------------- /moyopy/python/tests/data/test_hall_symbol_entry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from moyopy import HallSymbolEntry 4 | 5 | 6 | def test_hall_symbol_entry(): 7 | hs = HallSymbolEntry(hall_number=528) # No. 228, origin choice 2 8 | assert hs.hall_number == 528 9 | assert hs.number == 228 10 | assert hs.setting == "2" 11 | assert hs.hm_short == "F d -3 c" 12 | -------------------------------------------------------------------------------- /moyopy/python/tests/data/test_magnetic_space_group_type.py: -------------------------------------------------------------------------------- 1 | from moyopy import MagneticSpaceGroupType 2 | 3 | 4 | def test_magnetic_space_group_type(): 5 | msgt = MagneticSpaceGroupType(1262) 6 | assert msgt.bns_number == "151.32" 7 | assert msgt.og_number == "153.4.1270" 8 | assert msgt.construct_type == 4 9 | -------------------------------------------------------------------------------- /moyopy/python/tests/data/test_space_group_type.py: -------------------------------------------------------------------------------- 1 | from moyopy import SpaceGroupType 2 | 3 | 4 | def test_space_group_type(): 5 | s1 = SpaceGroupType(221) 6 | assert s1.number == 221 7 | assert s1.hm_short == "P m -3 m" 8 | assert s1.arithmetic_number == 71 9 | assert s1.arithmetic_symbol == "m-3mP" 10 | assert s1.geometric_crystal_class == "m-3m" 11 | assert s1.crystal_system == "Cubic" 12 | assert s1.bravais_class == "cP" 13 | assert s1.lattice_system == "Cubic" 14 | assert s1.crystal_family == "Cubic" 15 | 16 | s2 = SpaceGroupType(167) 17 | assert s2.number == 167 18 | assert s2.hm_short == "R -3 c" 19 | assert s2.arithmetic_number == 50 20 | assert s2.arithmetic_symbol == "-3mR" 21 | assert s2.geometric_crystal_class == "-3m" 22 | assert s2.crystal_system == "Trigonal" 23 | assert s2.bravais_class == "hR" 24 | assert s2.lattice_system == "Rhombohedral" 25 | assert s2.crystal_family == "Hexagonal" 26 | -------------------------------------------------------------------------------- /moyopy/python/tests/data/test_symmetry_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from moyopy import Operations, operations_from_number 6 | 7 | 8 | def _unique_sites_in_cell(position, operations: Operations) -> np.ndarray: 9 | def _is_close(site1, site2): 10 | diff = site1 - site2 11 | diff -= np.round(diff) 12 | return np.allclose(diff, 0, atol=1e-4) 13 | 14 | sites = [] 15 | for rotation, translation in zip(operations.rotations, operations.translations): 16 | new_position = np.array(rotation) @ np.array(position) + np.array(translation) 17 | if np.any([_is_close(site, new_position) for site in sites]): 18 | continue 19 | sites.append(new_position) 20 | return np.array(sites) 21 | 22 | 23 | def test_operations_from_number(): 24 | # Test with default primitive=False 25 | operations = operations_from_number(number=230) # Ia-3d 26 | num_operations = 48 * 2 27 | assert operations.num_operations == num_operations 28 | assert len(operations) == num_operations 29 | 30 | assert len(_unique_sites_in_cell([1 / 8, 1 / 8, 1 / 8], operations)) == 16 31 | 32 | # Test with primitive=True 33 | prim_operations = operations_from_number(number=230, primitive=True) # Ia-3d 34 | assert prim_operations.num_operations == 48 35 | assert len(prim_operations) == 48 36 | -------------------------------------------------------------------------------- /moyopy/python/tests/test_cell.py: -------------------------------------------------------------------------------- 1 | from moyopy import Cell 2 | 3 | 4 | def test_cell_serialization(wurtzite: Cell): 5 | serialized = wurtzite.serialize_json() 6 | deserialized = Cell.deserialize_json(serialized) 7 | assert len(wurtzite.positions) == len(deserialized.positions) 8 | 9 | 10 | def test_cell_py_obj_serialization(wurtzite: Cell): 11 | deserialized = wurtzite.as_dict() 12 | serialized = Cell.from_dict(deserialized) 13 | assert len(wurtzite.positions) == len(serialized.positions) 14 | -------------------------------------------------------------------------------- /moyopy/python/tests/test_interface.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | import moyopy 7 | from moyopy.interface import MoyoAdapter 8 | 9 | 10 | def test_pymatgen_moyopy(wurtzite: moyopy.Cell): 11 | structure = MoyoAdapter.get_structure(wurtzite) 12 | cell = MoyoAdapter.from_structure(structure) 13 | assert np.allclose(cell.basis, wurtzite.basis) 14 | assert np.allclose(cell.positions, wurtzite.positions) 15 | assert cell.numbers == wurtzite.numbers 16 | 17 | 18 | def test_ase_moyopy(wurtzite: moyopy.Cell): 19 | atoms = MoyoAdapter.get_atoms(wurtzite) 20 | cell = MoyoAdapter.from_atoms(atoms) 21 | assert np.allclose(cell.basis, wurtzite.basis) 22 | assert np.allclose(cell.positions, wurtzite.positions) 23 | assert cell.numbers == wurtzite.numbers 24 | 25 | 26 | def test_from_py_obj(wurtzite: moyopy.Cell): 27 | # Create different structure types 28 | structure = MoyoAdapter.get_structure(wurtzite) 29 | atoms = MoyoAdapter.get_atoms(wurtzite) 30 | mson_atoms = structure.to_ase_atoms() 31 | 32 | # Test conversion from each type 33 | for struct in [structure, atoms, mson_atoms]: 34 | cell = MoyoAdapter.from_py_obj(struct) 35 | assert np.allclose(cell.basis, wurtzite.basis) 36 | assert np.allclose(cell.positions, wurtzite.positions) 37 | assert cell.numbers == wurtzite.numbers 38 | 39 | # Test invalid input type 40 | with pytest.raises(TypeError, match="Expected Structure, Atoms, or MSONAtoms, got list"): 41 | MoyoAdapter.from_py_obj([1, 2, 3]) 42 | -------------------------------------------------------------------------------- /moyopy/python/tests/test_moyo_dataset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from moyopy import Cell, CollinearMagneticCell, MoyoCollinearMagneticDataset, MoyoDataset 4 | 5 | 6 | def test_moyo_dataset(wurtzite: Cell): 7 | dataset = MoyoDataset(wurtzite) 8 | assert dataset.number == 186 9 | assert dataset.hall_number == 480 10 | assert dataset.pearson_symbol == "hP4" 11 | 12 | 13 | def test_moyo_dataset_serialization(wurtzite: Cell): 14 | dataset = MoyoDataset(wurtzite) 15 | serialized = dataset.serialize_json() 16 | deserialized = MoyoDataset.deserialize_json(serialized) 17 | assert deserialized.number == dataset.number 18 | assert deserialized.std_cell.num_atoms == dataset.std_cell.num_atoms 19 | 20 | 21 | def test_moyo_dataset_py_obj_serialization(wurtzite: Cell): 22 | dataset = MoyoDataset(wurtzite) 23 | deserialized = dataset.as_dict() 24 | serialized = MoyoDataset.from_dict(deserialized) 25 | assert serialized.number == dataset.number 26 | assert serialized.std_cell.num_atoms == dataset.std_cell.num_atoms 27 | 28 | 29 | def test_moyo_dataset_repr(wurtzite: Cell): 30 | dataset = MoyoDataset(wurtzite) 31 | dataset_str = str(dataset) 32 | 33 | # Test that string representation of MoyoDataset contains key information 34 | assert "MoyoDataset" in dataset_str 35 | assert f"number={dataset.number}" in dataset_str 36 | assert f"hall_number={dataset.hall_number}" in dataset_str 37 | assert f"operations=<{len(dataset.operations)} operations>" in dataset_str 38 | assert f"orbits={dataset.orbits}" in dataset_str 39 | assert f"wyckoffs={dataset.wyckoffs}" in dataset_str 40 | 41 | # Test site_symmetry_symbols content without caring about quote style 42 | symbols = dataset.site_symmetry_symbols 43 | assert all(symbol in dataset_str for symbol in symbols) 44 | assert str(len(symbols)) in dataset_str 45 | 46 | # Test that repr() gives different output 47 | assert str(dataset) != repr(dataset) 48 | 49 | 50 | def test_moyo_collinear_magnetic_dataset(rutile_type3: CollinearMagneticCell): 51 | dataset = MoyoCollinearMagneticDataset(rutile_type3) 52 | assert dataset.uni_number == 1158 53 | -------------------------------------------------------------------------------- /moyopy/src/base.rs: -------------------------------------------------------------------------------- 1 | mod cell; 2 | mod error; 3 | mod magnetic_cell; 4 | mod operation; 5 | 6 | pub use cell::PyStructure; 7 | pub use error::PyMoyoError; 8 | pub use magnetic_cell::{PyCollinearMagneticCell, PyNonCollinearMagneticCell}; 9 | pub use operation::{PyMagneticOperations, PyOperations}; 10 | -------------------------------------------------------------------------------- /moyopy/src/base/cell.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::Vector3; 2 | use pyo3::exceptions::PyValueError; 3 | use pyo3::types::PyType; 4 | use pyo3::{prelude::*, IntoPyObjectExt}; 5 | use pythonize::{depythonize, pythonize}; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json; 8 | 9 | use moyo::base::{Cell, Lattice}; 10 | 11 | // Unfortunately, "PyCell" is already reversed by pyo3... 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | #[pyclass(name = "Cell", frozen)] 14 | #[pyo3(module = "moyopy")] 15 | pub struct PyStructure(Cell); 16 | 17 | #[pymethods] 18 | impl PyStructure { 19 | #[new] 20 | /// basis: row-wise basis vectors 21 | pub fn new( 22 | basis: [[f64; 3]; 3], 23 | positions: Vec<[f64; 3]>, 24 | numbers: Vec, 25 | ) -> PyResult { 26 | if positions.len() != numbers.len() { 27 | return Err(PyValueError::new_err( 28 | "positions and numbers should be the same length", 29 | )); 30 | } 31 | 32 | let lattice = Lattice::from_basis(basis); 33 | let positions = positions 34 | .iter() 35 | .map(|x| Vector3::new(x[0], x[1], x[2])) 36 | .collect::>(); 37 | let cell = Cell::new(lattice, positions, numbers); 38 | 39 | Ok(Self(cell)) 40 | } 41 | 42 | #[getter] 43 | pub fn basis(&self) -> [[f64; 3]; 3] { 44 | *self.0.lattice.basis.as_ref() 45 | } 46 | 47 | #[getter] 48 | pub fn positions(&self) -> Vec<[f64; 3]> { 49 | self.0.positions.iter().map(|x| [x.x, x.y, x.z]).collect() 50 | } 51 | 52 | #[getter] 53 | pub fn numbers(&self) -> Vec { 54 | self.0.numbers.clone() 55 | } 56 | 57 | #[getter] 58 | pub fn num_atoms(&self) -> usize { 59 | self.0.num_atoms() 60 | } 61 | 62 | // ------------------------------------------------------------------------ 63 | // Special methods 64 | // ------------------------------------------------------------------------ 65 | fn __repr__(&self) -> String { 66 | self.serialize_json() 67 | } 68 | 69 | fn __str__(&self) -> String { 70 | self.__repr__() 71 | } 72 | 73 | // ------------------------------------------------------------------------ 74 | // Serialization 75 | // ------------------------------------------------------------------------ 76 | pub fn serialize_json(&self) -> String { 77 | serde_json::to_string(&self.0).expect("Serialization should not fail") 78 | } 79 | 80 | #[classmethod] 81 | pub fn deserialize_json(_cls: &Bound<'_, PyType>, s: &str) -> PyResult { 82 | serde_json::from_str(s).map_err(|e| PyValueError::new_err(e.to_string())) 83 | } 84 | 85 | pub fn as_dict(&self) -> PyResult> { 86 | Python::with_gil(|py| { 87 | let obj = pythonize(py, &self.0).expect("Python object conversion should not fail"); 88 | obj.into_py_any(py) 89 | }) 90 | } 91 | 92 | #[classmethod] 93 | pub fn from_dict(_cls: &Bound<'_, PyType>, obj: &Bound<'_, PyAny>) -> PyResult { 94 | Python::with_gil(|_| { 95 | depythonize::(obj).map_err(|e| { 96 | PyErr::new::(format!("Deserialization failed: {}", e)) 97 | }) 98 | }) 99 | } 100 | } 101 | 102 | impl From for Cell { 103 | fn from(structure: PyStructure) -> Self { 104 | structure.0 105 | } 106 | } 107 | 108 | impl From for PyStructure { 109 | fn from(cell: Cell) -> Self { 110 | PyStructure(cell) 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | extern crate approx; 117 | 118 | use super::*; 119 | use approx::assert_relative_eq; 120 | use serde_json; 121 | 122 | #[test] 123 | fn test_serialization() { 124 | let structure = PyStructure::new( 125 | [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], 126 | vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], 127 | vec![1, 2], 128 | ) 129 | .unwrap(); 130 | 131 | let serialized = serde_json::to_string(&structure).unwrap(); 132 | let deserialized: PyStructure = serde_json::from_str(&serialized).unwrap(); 133 | 134 | for i in 0..3 { 135 | for j in 0..3 { 136 | assert_relative_eq!(structure.basis()[i][j], deserialized.basis()[i][j]); 137 | } 138 | } 139 | assert_eq!(structure.positions().len(), deserialized.positions().len()); 140 | for (actual, expect) in structure.positions().iter().zip(deserialized.positions()) { 141 | for i in 0..3 { 142 | assert_relative_eq!(actual[i], expect[i]); 143 | } 144 | } 145 | assert_eq!(structure.numbers(), deserialized.numbers()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /moyopy/src/base/error.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyValueError; 2 | use pyo3::prelude::*; 3 | 4 | use moyo::base::MoyoError; 5 | 6 | #[derive(Debug)] 7 | #[pyclass(name = "MoyoError", frozen)] 8 | #[pyo3(module = "moyopy")] 9 | pub struct PyMoyoError(MoyoError); 10 | 11 | impl From for PyErr { 12 | fn from(error: PyMoyoError) -> Self { 13 | PyValueError::new_err(error.0.to_string()) 14 | } 15 | } 16 | 17 | impl From for PyMoyoError { 18 | fn from(error: MoyoError) -> Self { 19 | PyMoyoError(error) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /moyopy/src/base/operation.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | use moyo::base::{MagneticOperations, Operations}; 4 | 5 | #[derive(Debug)] 6 | #[pyclass(name = "Operations", frozen)] 7 | #[pyo3(module = "moyopy")] 8 | pub struct PyOperations(Operations); 9 | 10 | #[pymethods] 11 | impl PyOperations { 12 | #[getter] 13 | pub fn rotations(&self) -> Vec<[[i32; 3]; 3]> { 14 | // Since nalgebra stores matrices in column-major order, we need to transpose them 15 | self.0 16 | .iter() 17 | .map(|x| *x.rotation.transpose().as_ref()) 18 | .collect() 19 | } 20 | 21 | #[getter] 22 | pub fn translations(&self) -> Vec<[f64; 3]> { 23 | self.0.iter().map(|x| *x.translation.as_ref()).collect() 24 | } 25 | 26 | #[getter] 27 | pub fn num_operations(&self) -> usize { 28 | self.0.len() 29 | } 30 | 31 | fn __len__(&self) -> usize { 32 | self.num_operations() 33 | } 34 | } 35 | 36 | impl From for Operations { 37 | fn from(operations: PyOperations) -> Self { 38 | operations.0 39 | } 40 | } 41 | 42 | impl From for PyOperations { 43 | fn from(operations: Operations) -> Self { 44 | PyOperations(operations) 45 | } 46 | } 47 | 48 | #[derive(Debug)] 49 | #[pyclass(name = "MagneticOperations", frozen)] 50 | #[pyo3(module = "moyopy")] 51 | pub struct PyMagneticOperations(MagneticOperations); 52 | 53 | #[pymethods] 54 | impl PyMagneticOperations { 55 | #[getter] 56 | pub fn rotations(&self) -> Vec<[[i32; 3]; 3]> { 57 | // Since nalgebra stores matrices in column-major order, we need to transpose them 58 | self.0 59 | .iter() 60 | .map(|x| *x.operation.rotation.transpose().as_ref()) 61 | .collect() 62 | } 63 | 64 | #[getter] 65 | pub fn translations(&self) -> Vec<[f64; 3]> { 66 | self.0 67 | .iter() 68 | .map(|x| *x.operation.translation.as_ref()) 69 | .collect() 70 | } 71 | 72 | #[getter] 73 | pub fn time_reversals(&self) -> Vec { 74 | self.0.iter().map(|x| x.time_reversal).collect() 75 | } 76 | 77 | #[getter] 78 | pub fn num_operations(&self) -> usize { 79 | self.0.len() 80 | } 81 | 82 | fn __len__(&self) -> usize { 83 | self.num_operations() 84 | } 85 | } 86 | 87 | impl From for MagneticOperations { 88 | fn from(operations: PyMagneticOperations) -> Self { 89 | operations.0 90 | } 91 | } 92 | 93 | impl From for PyMagneticOperations { 94 | fn from(operations: MagneticOperations) -> Self { 95 | PyMagneticOperations(operations) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /moyopy/src/data.rs: -------------------------------------------------------------------------------- 1 | mod centering; 2 | mod hall_symbol; 3 | mod magnetic_space_group_type; 4 | mod setting; 5 | mod space_group_type; 6 | 7 | pub use centering::PyCentering; 8 | pub use hall_symbol::PyHallSymbolEntry; 9 | pub use magnetic_space_group_type::PyMagneticSpaceGroupType; 10 | pub use setting::PySetting; 11 | pub use space_group_type::PySpaceGroupType; 12 | 13 | use pyo3::prelude::*; 14 | 15 | use super::base::{PyMoyoError, PyOperations}; 16 | use moyo::base::{MoyoError, Operation}; 17 | use moyo::data::{hall_symbol_entry, HallSymbol, Setting}; 18 | 19 | #[pyfunction] 20 | #[pyo3(signature = (number, *, setting=None, primitive=false))] 21 | pub fn operations_from_number( 22 | number: i32, 23 | setting: Option, 24 | primitive: bool, 25 | ) -> Result { 26 | let setting = if let Some(setting) = setting { 27 | setting 28 | } else { 29 | PySetting(Setting::Spglib) 30 | }; 31 | let hall_number = match setting.0 { 32 | Setting::HallNumber(hall_number) => hall_number, 33 | Setting::Spglib | Setting::Standard => *setting 34 | .0 35 | .hall_numbers() 36 | .get((number - 1) as usize) 37 | .ok_or(MoyoError::UnknownNumberError)?, 38 | }; 39 | let entry = hall_symbol_entry(hall_number).unwrap(); 40 | let hs = HallSymbol::new(entry.hall_symbol).ok_or(MoyoError::HallSymbolParsingError)?; 41 | 42 | let mut operations = vec![]; 43 | if primitive { 44 | for operation in hs.primitive_traverse().into_iter() { 45 | operations.push(operation) 46 | } 47 | } else { 48 | let coset = hs.traverse(); 49 | 50 | let lattice_points = hs.centering.lattice_points(); 51 | for t1 in lattice_points.iter() { 52 | for operation2 in coset.iter() { 53 | // (E, t1) (r2, t2) = (r2, t1 + t2) 54 | let t12 = (t1 + operation2.translation).map(|e| e % 1.); 55 | operations.push(Operation::new(operation2.rotation, t12)); 56 | } 57 | } 58 | } 59 | 60 | Ok(PyOperations::from(operations)) 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use nalgebra::vector; 66 | 67 | use super::operations_from_number; 68 | use moyo::base::{Operations, Position}; 69 | 70 | fn unique_sites(position: &Position, operations: &Operations) -> Vec { 71 | let mut sites: Vec = vec![]; 72 | for operation in operations.iter() { 73 | let new_site = operation.rotation.map(|e| e as f64) * position + operation.translation; 74 | let mut overlap = false; 75 | for site in sites.iter() { 76 | let mut diff = site - new_site; 77 | diff -= diff.map(|x| x.round()); 78 | if diff.iter().all(|x| x.abs() < 1e-4) { 79 | overlap = true; 80 | break; 81 | } 82 | } 83 | if !overlap { 84 | sites.push(new_site); 85 | } 86 | } 87 | sites 88 | } 89 | 90 | #[test] 91 | fn test_operations_from_number() { 92 | { 93 | // C2/c 94 | let operations = operations_from_number(15, None, false).unwrap(); 95 | let operations = Operations::from(operations); 96 | let x = 0.1234; 97 | let y = 0.5678; 98 | let z = 0.9012; 99 | assert!(unique_sites(&vector![0.0, 0.0, 0.0], &operations).len() == 4); 100 | assert!(unique_sites(&vector![0.0, 0.5, 0.0], &operations).len() == 4); 101 | assert!(unique_sites(&vector![0.25, 0.25, 0.0], &operations).len() == 4); 102 | assert!(unique_sites(&vector![0.25, 0.25, 0.5], &operations).len() == 4); 103 | assert!(unique_sites(&vector![0.0, y, 0.25], &operations).len() == 4); 104 | assert!(unique_sites(&vector![x, y, z], &operations).len() == 8); 105 | } 106 | { 107 | // Pm-3m 108 | let operations = operations_from_number(221, None, false).unwrap(); 109 | let operations = Operations::from(operations); 110 | let x = 0.1234; 111 | let y = 0.5678; 112 | let z = 0.9012; 113 | assert!(operations.len() == 48); 114 | assert!(unique_sites(&vector![0.0, 0.0, 0.0], &operations).len() == 1); 115 | assert!(unique_sites(&vector![0.5, 0.5, 0.5], &operations).len() == 1); 116 | assert!(unique_sites(&vector![0.0, 0.5, 0.5], &operations).len() == 3); 117 | assert!(unique_sites(&vector![0.5, 0.0, 0.0], &operations).len() == 3); 118 | assert!(unique_sites(&vector![x, 0.0, 0.0], &operations).len() == 6); 119 | assert!(unique_sites(&vector![x, 0.5, 0.5], &operations).len() == 6); 120 | assert!(unique_sites(&vector![x, x, x], &operations).len() == 8); 121 | assert!(unique_sites(&vector![x, 0.5, 0.0], &operations).len() == 12); 122 | assert!(unique_sites(&vector![0.0, y, y], &operations).len() == 12); 123 | assert!(unique_sites(&vector![0.5, y, y], &operations).len() == 12); 124 | assert!(unique_sites(&vector![0.0, y, z], &operations).len() == 24); 125 | assert!(unique_sites(&vector![0.5, y, z], &operations).len() == 24); 126 | assert!(unique_sites(&vector![x, x, z], &operations).len() == 24); 127 | assert!(unique_sites(&vector![x, y, z], &operations).len() == 48); 128 | } 129 | { 130 | // Im-3m 131 | let operations = operations_from_number(229, None, false).unwrap(); 132 | let operations = Operations::from(operations); 133 | let x = 0.1234; 134 | let y = 0.5678; 135 | let z = 0.9012; 136 | assert!(unique_sites(&vector![0.0, 0.0, 0.0], &operations).len() == 2); 137 | assert!(unique_sites(&vector![0.0, 0.5, 0.5], &operations).len() == 6); 138 | assert!(unique_sites(&vector![0.25, 0.25, 0.25], &operations).len() == 8); 139 | assert!(unique_sites(&vector![0.25, 0.0, 0.5], &operations).len() == 12); 140 | assert!(unique_sites(&vector![x, 0.0, 0.0], &operations).len() == 12); 141 | assert!(unique_sites(&vector![x, x, x], &operations).len() == 16); 142 | assert!(unique_sites(&vector![x, 0.0, 0.5], &operations).len() == 24); 143 | assert!(unique_sites(&vector![0.0, y, y], &operations).len() == 24); 144 | assert!(unique_sites(&vector![0.25, y, 0.5 - y], &operations).len() == 48); 145 | assert!(unique_sites(&vector![0.0, y, z], &operations).len() == 48); 146 | assert!(unique_sites(&vector![x, x, z], &operations).len() == 48); 147 | assert!(unique_sites(&vector![x, y, z], &operations).len() == 96); 148 | } 149 | { 150 | // Ia-3d 151 | let operations = operations_from_number(230, None, false).unwrap(); 152 | let operations = Operations::from(operations); 153 | let x = 0.1234; 154 | let y = 0.5678; 155 | let z = 0.9012; 156 | 157 | assert!(unique_sites(&vector![0.0, 0.0, 0.0], &operations).len() == 16); 158 | assert!(unique_sites(&vector![0.125, 0.125, 0.125], &operations).len() == 16); 159 | assert!(unique_sites(&vector![0.125, 0.0, 0.25], &operations).len() == 24); 160 | assert!(unique_sites(&vector![0.375, 0.0, 0.25], &operations).len() == 24); 161 | assert!(unique_sites(&vector![x, x, x], &operations).len() == 32); 162 | assert!(unique_sites(&vector![x, 0.0, 0.25], &operations).len() == 48); 163 | assert!(unique_sites(&vector![0.125, y, 0.25 - y], &operations).len() == 48); 164 | assert!(unique_sites(&vector![x, y, z], &operations).len() == 96); 165 | } 166 | { 167 | // primitive=true 168 | let prim_operations = operations_from_number(230, None, true).unwrap(); 169 | assert!(prim_operations.num_operations() == 48); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /moyopy/src/data/centering.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | use moyo::data::Centering; 4 | 5 | #[derive(Debug, Clone)] 6 | #[pyclass(name = "Centering", frozen)] 7 | #[pyo3(module = "moyopy")] 8 | pub struct PyCentering(pub Centering); 9 | 10 | #[pymethods] 11 | impl PyCentering { 12 | #[getter] 13 | pub fn order(&self) -> usize { 14 | self.0.order() 15 | } 16 | 17 | #[getter] 18 | pub fn linear(&self) -> [[i32; 3]; 3] { 19 | // Since nalgebra stores matrices in column-major order, we need to transpose them 20 | *self.0.linear().transpose().as_ref() 21 | } 22 | 23 | #[getter] 24 | pub fn lattice_points(&self) -> Vec<[f64; 3]> { 25 | self.0 26 | .lattice_points() 27 | .iter() 28 | .map(|x| *x.as_ref()) 29 | .collect() 30 | } 31 | } 32 | 33 | impl From for Centering { 34 | fn from(centering: PyCentering) -> Self { 35 | centering.0 36 | } 37 | } 38 | 39 | impl From for PyCentering { 40 | fn from(centering: Centering) -> Self { 41 | Self(centering) 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | 49 | #[test] 50 | fn test_centering() { 51 | let centering = PyCentering(Centering::C); 52 | assert_eq!(centering.order(), 2); 53 | let linear = centering.linear(); 54 | assert_eq!(linear[0], [1, -1, 0]); 55 | assert_eq!(linear[1], [1, 1, 0]); 56 | assert_eq!(linear[2], [0, 0, 1]); 57 | assert_eq!(centering.lattice_points().len(), 2); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /moyopy/src/data/hall_symbol.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | use crate::base::PyMoyoError; 4 | use moyo::base::MoyoError; 5 | use moyo::data::{hall_symbol_entry, ArithmeticNumber, HallNumber, HallSymbolEntry, Number}; 6 | 7 | use super::centering::PyCentering; 8 | 9 | #[derive(Debug, Clone)] 10 | #[pyclass(name = "HallSymbolEntry", frozen)] 11 | #[pyo3(module = "moyopy")] 12 | pub struct PyHallSymbolEntry(pub HallSymbolEntry); 13 | 14 | #[pymethods] 15 | impl PyHallSymbolEntry { 16 | #[new] 17 | pub fn new(hall_number: HallNumber) -> Result { 18 | let entry = hall_symbol_entry(hall_number).ok_or(MoyoError::UnknownHallNumberError)?; 19 | Ok(Self(entry)) 20 | } 21 | 22 | #[getter] 23 | pub fn hall_number(&self) -> HallNumber { 24 | self.0.hall_number 25 | } 26 | 27 | #[getter] 28 | pub fn number(&self) -> Number { 29 | self.0.number 30 | } 31 | 32 | #[getter] 33 | pub fn arithmetic_number(&self) -> ArithmeticNumber { 34 | self.0.arithmetic_number 35 | } 36 | 37 | #[getter] 38 | pub fn setting(&self) -> &str { 39 | self.0.setting 40 | } 41 | 42 | #[getter] 43 | pub fn hall_symbol(&self) -> &str { 44 | self.0.hall_symbol 45 | } 46 | 47 | #[getter] 48 | pub fn hm_short(&self) -> &str { 49 | self.0.hm_short 50 | } 51 | 52 | #[getter] 53 | pub fn hm_full(&self) -> &str { 54 | self.0.hm_full 55 | } 56 | 57 | #[getter] 58 | pub fn centering(&self) -> PyCentering { 59 | self.0.centering.into() 60 | } 61 | } 62 | 63 | impl From for HallSymbolEntry { 64 | fn from(hall_symbol_entry: PyHallSymbolEntry) -> Self { 65 | hall_symbol_entry.0 66 | } 67 | } 68 | 69 | impl From for PyHallSymbolEntry { 70 | fn from(hall_symbol_entry: HallSymbolEntry) -> Self { 71 | Self(hall_symbol_entry) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /moyopy/src/data/magnetic_space_group_type.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyValueError; 2 | use pyo3::prelude::*; 3 | 4 | use moyo::data::{ 5 | get_magnetic_space_group_type, ConstructType, MagneticSpaceGroupType, Number, UNINumber, 6 | }; 7 | 8 | #[derive(Debug, Clone)] 9 | #[pyclass(name = "MagneticSpaceGroupType", frozen)] 10 | pub struct PyMagneticSpaceGroupType(MagneticSpaceGroupType); 11 | 12 | #[pymethods] 13 | impl PyMagneticSpaceGroupType { 14 | #[new] 15 | pub fn new(uni_number: UNINumber) -> Result { 16 | let magnetic_space_group_type = get_magnetic_space_group_type(uni_number).ok_or( 17 | PyValueError::new_err(format!("Unknown uni_number: {}", uni_number)), 18 | )?; 19 | Ok(PyMagneticSpaceGroupType(magnetic_space_group_type)) 20 | } 21 | 22 | #[getter] 23 | pub fn uni_number(&self) -> UNINumber { 24 | self.0.uni_number 25 | } 26 | 27 | #[getter] 28 | pub fn litvin_number(&self) -> i32 { 29 | self.0.litvin_number 30 | } 31 | 32 | #[getter] 33 | pub fn bns_number(&self) -> String { 34 | self.0.bns_number.to_string() 35 | } 36 | 37 | #[getter] 38 | pub fn og_number(&self) -> String { 39 | self.0.og_number.to_string() 40 | } 41 | 42 | #[getter] 43 | pub fn number(&self) -> Number { 44 | self.0.number 45 | } 46 | 47 | #[getter] 48 | pub fn construct_type(&self) -> i32 { 49 | match self.0.construct_type { 50 | ConstructType::Type1 => 1, 51 | ConstructType::Type2 => 2, 52 | ConstructType::Type3 => 3, 53 | ConstructType::Type4 => 4, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /moyopy/src/data/setting.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PyType; 3 | 4 | use moyo::data::Setting; 5 | 6 | #[derive(Debug, Clone)] 7 | #[pyclass(name = "Setting", frozen)] 8 | #[pyo3(module = "moyopy")] 9 | pub struct PySetting(pub Setting); 10 | 11 | #[pymethods] 12 | impl PySetting { 13 | #[classmethod] 14 | pub fn spglib(_cls: &Bound<'_, PyType>) -> PyResult { 15 | Ok(Self(Setting::Spglib)) 16 | } 17 | 18 | #[classmethod] 19 | pub fn standard(_cls: &Bound<'_, PyType>) -> PyResult { 20 | Ok(Self(Setting::Standard)) 21 | } 22 | 23 | #[classmethod] 24 | pub fn hall_number(_cls: &Bound<'_, PyType>, hall_number: i32) -> PyResult { 25 | Ok(Self(Setting::HallNumber(hall_number))) 26 | } 27 | } 28 | 29 | impl From for Setting { 30 | fn from(setting: PySetting) -> Self { 31 | setting.0 32 | } 33 | } 34 | 35 | impl From for PySetting { 36 | fn from(setting: Setting) -> Self { 37 | Self(setting) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /moyopy/src/data/space_group_type.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyValueError; 2 | use pyo3::prelude::*; 3 | 4 | use moyo::data::{ 5 | arithmetic_crystal_class_entry, hall_symbol_entry, ArithmeticNumber, CrystalFamily, 6 | CrystalSystem, LatticeSystem, Number, Setting, 7 | }; 8 | 9 | #[derive(Debug, Clone)] 10 | #[pyclass(name = "SpaceGroupType", frozen)] 11 | pub struct PySpaceGroupType { 12 | // Space group type 13 | #[pyo3(get)] 14 | /// ITA number for space group types (1 - 230) 15 | number: Number, 16 | /// Hermann-Mauguin symbol in short notation 17 | #[pyo3(get)] 18 | hm_short: &'static str, 19 | /// Hermann-Mauguin symbol in full notation 20 | #[pyo3(get)] 21 | hm_full: &'static str, 22 | // Arithmetic crystal system 23 | /// Number for arithmetic crystal classes (1 - 73) 24 | #[pyo3(get)] 25 | arithmetic_number: ArithmeticNumber, 26 | /// Symbol for arithmetic crystal class 27 | #[pyo3(get)] 28 | arithmetic_symbol: &'static str, 29 | // Other classifications 30 | /// Geometric crystal class 31 | #[pyo3(get)] 32 | geometric_crystal_class: String, 33 | /// Crystal system 34 | #[pyo3(get)] 35 | crystal_system: String, 36 | /// Bravais class 37 | #[pyo3(get)] 38 | bravais_class: String, 39 | /// Lattice system 40 | #[pyo3(get)] 41 | lattice_system: String, 42 | /// Crystal family 43 | #[pyo3(get)] 44 | crystal_family: String, 45 | } 46 | 47 | #[pymethods] 48 | impl PySpaceGroupType { 49 | #[new] 50 | pub fn new(number: Number) -> Result { 51 | let ita_hall_number = Setting::Standard 52 | .hall_number(number) 53 | .ok_or(PyValueError::new_err(format!("Unknown number: {}", number)))?; 54 | let ita_hall_symbol = hall_symbol_entry(ita_hall_number).unwrap(); 55 | 56 | let arithmetic_number = ita_hall_symbol.arithmetic_number; 57 | let arithmetic_entry = arithmetic_crystal_class_entry(arithmetic_number).unwrap(); 58 | 59 | let geometric_crystal_class = arithmetic_entry.geometric_crystal_class; 60 | let crystal_system = CrystalSystem::from_geometric_crystal_class(geometric_crystal_class); 61 | 62 | let bravais_class = arithmetic_entry.bravais_class; 63 | let lattice_system = LatticeSystem::from_bravais_class(bravais_class); 64 | let crystal_family = CrystalFamily::from_lattice_system(lattice_system); 65 | 66 | Ok(Self { 67 | // Space group type 68 | number, 69 | hm_short: ita_hall_symbol.hm_short, 70 | hm_full: ita_hall_symbol.hm_full, 71 | // Arithmetic crystal system 72 | arithmetic_number, 73 | arithmetic_symbol: arithmetic_entry.symbol, 74 | // Other classifications 75 | geometric_crystal_class: geometric_crystal_class.to_string(), 76 | crystal_system: crystal_system.to_string(), 77 | bravais_class: bravais_class.to_string(), 78 | lattice_system: lattice_system.to_string(), 79 | crystal_family: crystal_family.to_string(), 80 | }) 81 | } 82 | 83 | fn __repr__(&self) -> String { 84 | format!("SpaceGroupType({})", self.number) 85 | } 86 | 87 | fn __str__(&self) -> String { 88 | format!("{:?}", self) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /moyopy/src/dataset.rs: -------------------------------------------------------------------------------- 1 | mod magnetic_space_group; 2 | mod space_group; 3 | 4 | pub use magnetic_space_group::{PyMoyoCollinearMagneticDataset, PyMoyoNonCollinearMagneticDataset}; 5 | pub use space_group::PyMoyoDataset; 6 | -------------------------------------------------------------------------------- /moyopy/src/dataset/space_group.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyValueError; 2 | use pyo3::types::PyType; 3 | use pyo3::{prelude::*, IntoPyObjectExt}; 4 | use pythonize::{depythonize, pythonize}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use moyo::base::AngleTolerance; 8 | use moyo::data::Setting; 9 | use moyo::MoyoDataset; 10 | 11 | use crate::base::{PyMoyoError, PyOperations, PyStructure}; 12 | use crate::data::PySetting; 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | #[pyclass(name = "MoyoDataset", frozen)] 16 | #[pyo3(module = "moyopy")] 17 | pub struct PyMoyoDataset(MoyoDataset); 18 | 19 | #[pymethods] 20 | impl PyMoyoDataset { 21 | #[new] 22 | #[pyo3(signature = (cell, *, symprec=1e-4, angle_tolerance=None, setting=None))] 23 | pub fn new( 24 | cell: &PyStructure, 25 | symprec: f64, 26 | angle_tolerance: Option, 27 | setting: Option, 28 | ) -> Result { 29 | let angle_tolerance = if let Some(angle_tolerance) = angle_tolerance { 30 | AngleTolerance::Radian(angle_tolerance) 31 | } else { 32 | AngleTolerance::Default 33 | }; 34 | 35 | let setting = if let Some(setting) = setting { 36 | setting.into() 37 | } else { 38 | Setting::Spglib 39 | }; 40 | 41 | let dataset = MoyoDataset::new(&cell.to_owned().into(), symprec, angle_tolerance, setting)?; 42 | Ok(PyMoyoDataset(dataset)) 43 | } 44 | 45 | // ------------------------------------------------------------------------ 46 | // Identification 47 | // ------------------------------------------------------------------------ 48 | #[getter] 49 | pub fn number(&self) -> i32 { 50 | self.0.number 51 | } 52 | 53 | #[getter] 54 | pub fn hall_number(&self) -> i32 { 55 | self.0.hall_number 56 | } 57 | 58 | // ------------------------------------------------------------------------ 59 | // Symmetry operations in the input cell 60 | // ------------------------------------------------------------------------ 61 | #[getter] 62 | pub fn operations(&self) -> PyOperations { 63 | self.0.operations.clone().into() 64 | } 65 | 66 | // ------------------------------------------------------------------------ 67 | // Site symmetry 68 | // ------------------------------------------------------------------------ 69 | #[getter] 70 | pub fn orbits(&self) -> Vec { 71 | self.0.orbits.clone() 72 | } 73 | 74 | #[getter] 75 | pub fn wyckoffs(&self) -> Vec { 76 | self.0.wyckoffs.clone() 77 | } 78 | 79 | #[getter] 80 | pub fn site_symmetry_symbols(&self) -> Vec { 81 | self.0.site_symmetry_symbols.clone() 82 | } 83 | 84 | // ------------------------------------------------------------------------ 85 | // Standardized cell 86 | // ------------------------------------------------------------------------ 87 | #[getter] 88 | pub fn std_cell(&self) -> PyStructure { 89 | self.0.std_cell.clone().into() 90 | } 91 | 92 | #[getter] 93 | pub fn std_linear(&self) -> [[f64; 3]; 3] { 94 | // Since nalgebra stores matrices in column-major order, we need to transpose them 95 | self.0.std_linear.transpose().into() 96 | } 97 | 98 | #[getter] 99 | pub fn std_origin_shift(&self) -> [f64; 3] { 100 | self.0.std_origin_shift.into() 101 | } 102 | 103 | #[getter] 104 | pub fn std_rotation_matrix(&self) -> [[f64; 3]; 3] { 105 | // Since nalgebra stores matrices in column-major order, we need to transpose them 106 | self.0.std_rotation_matrix.transpose().into() 107 | } 108 | 109 | #[getter] 110 | pub fn pearson_symbol(&self) -> String { 111 | self.0.pearson_symbol.clone() 112 | } 113 | 114 | // ------------------------------------------------------------------------ 115 | // Primitive standardized cell 116 | // ------------------------------------------------------------------------ 117 | #[getter] 118 | pub fn prim_std_cell(&self) -> PyStructure { 119 | self.0.prim_std_cell.clone().into() 120 | } 121 | 122 | #[getter] 123 | pub fn prim_std_linear(&self) -> [[f64; 3]; 3] { 124 | // Since nalgebra stores matrices in column-major order, we need to transpose them 125 | self.0.prim_std_linear.transpose().into() 126 | } 127 | 128 | #[getter] 129 | pub fn prim_std_origin_shift(&self) -> [f64; 3] { 130 | self.0.prim_std_origin_shift.into() 131 | } 132 | 133 | #[getter] 134 | pub fn mapping_std_prim(&self) -> Vec { 135 | self.0.mapping_std_prim.clone() 136 | } 137 | 138 | // ------------------------------------------------------------------------ 139 | // Final parameters 140 | // ------------------------------------------------------------------------ 141 | #[getter] 142 | pub fn symprec(&self) -> f64 { 143 | self.0.symprec 144 | } 145 | 146 | #[getter] 147 | pub fn angle_tolerance(&self) -> Option { 148 | if let AngleTolerance::Radian(angle_tolerance) = self.0.angle_tolerance { 149 | Some(angle_tolerance) 150 | } else { 151 | None 152 | } 153 | } 154 | 155 | // ------------------------------------------------------------------------ 156 | // Special methods 157 | // ------------------------------------------------------------------------ 158 | fn __str__(&self) -> String { 159 | format!( 160 | "MoyoDataset(number={}, hall_number={}, operations=<{} operations>, orbits={:?}, wyckoffs={:?}, site_symmetry_symbols={:?})", 161 | self.0.number, 162 | self.0.hall_number, 163 | self.0.operations.len(), 164 | self.0.orbits, 165 | self.0.wyckoffs, 166 | self.0.site_symmetry_symbols 167 | ) 168 | } 169 | 170 | fn __repr__(&self) -> String { 171 | self.serialize_json() 172 | } 173 | 174 | // ------------------------------------------------------------------------ 175 | // Serialization 176 | // ------------------------------------------------------------------------ 177 | pub fn serialize_json(&self) -> String { 178 | serde_json::to_string(&self.0).expect("Serialization should not fail") 179 | } 180 | 181 | #[classmethod] 182 | pub fn deserialize_json(_cls: &Bound<'_, PyType>, s: &str) -> PyResult { 183 | serde_json::from_str(s).map_err(|e| PyValueError::new_err(e.to_string())) 184 | } 185 | 186 | pub fn as_dict(&self) -> PyResult> { 187 | Python::with_gil(|py| { 188 | let obj = pythonize(py, &self.0).expect("Python object conversion should not fail"); 189 | obj.into_py_any(py) 190 | }) 191 | } 192 | 193 | #[classmethod] 194 | pub fn from_dict(_cls: &Bound<'_, PyType>, obj: &Bound<'_, PyAny>) -> PyResult { 195 | Python::with_gil(|_| { 196 | depythonize::(obj).map_err(|e| { 197 | PyErr::new::(format!("Deserialization failed: {}", e)) 198 | }) 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /moyopy/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use std::sync::OnceLock; 3 | 4 | pub mod base; 5 | pub mod data; 6 | pub mod dataset; 7 | 8 | use crate::base::{ 9 | PyCollinearMagneticCell, PyMagneticOperations, PyMoyoError, PyNonCollinearMagneticCell, 10 | PyOperations, PyStructure, 11 | }; 12 | use crate::data::{ 13 | operations_from_number, PyCentering, PyHallSymbolEntry, PyMagneticSpaceGroupType, PySetting, 14 | PySpaceGroupType, 15 | }; 16 | use crate::dataset::{ 17 | PyMoyoCollinearMagneticDataset, PyMoyoDataset, PyMoyoNonCollinearMagneticDataset, 18 | }; 19 | 20 | // https://github.com/pydantic/pydantic-core/blob/main/src/lib.rs 21 | fn moyopy_version() -> &'static str { 22 | static MOYOPY_VERSION: OnceLock = OnceLock::new(); 23 | 24 | MOYOPY_VERSION.get_or_init(|| { 25 | let version = env!("CARGO_PKG_VERSION"); 26 | // cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility, 27 | // but it's good enough for now 28 | // see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec 29 | // see https://peps.python.org/pep-0440/ for python spec 30 | // it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works 31 | version.replace("-alpha", "a").replace("-beta", "b") 32 | }) 33 | } 34 | 35 | /// A Python module implemented in Rust. 36 | #[pymodule] 37 | #[pyo3(name = "_moyopy")] 38 | fn moyopy(m: &Bound<'_, PyModule>) -> PyResult<()> { 39 | pyo3_log::init(); 40 | 41 | // lib 42 | m.add("__version__", moyopy_version())?; 43 | 44 | // dataset 45 | m.add_class::()?; 46 | m.add_class::()?; 47 | m.add_class::()?; 48 | 49 | // base 50 | m.add_class::()?; 51 | m.add_class::()?; 52 | m.add_class::()?; 53 | m.add_class::()?; 54 | m.add_class::()?; 55 | m.add_class::()?; 56 | 57 | // data 58 | m.add_class::()?; 59 | m.add_class::()?; 60 | m.add_class::()?; 61 | m.add_class::()?; 62 | m.add_class::()?; 63 | m.add_wrapped(wrap_pyfunction!(operations_from_number))?; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /scripts/hall_symbol.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pandas as pd 3 | 4 | 5 | def number_to_arithmetic_crystal_class(): 6 | # Ordered the same as https://dictionary.iucr.org/Arithmetic_crystal_class 7 | # fmt: off 8 | numbers = list(range(1, 230 + 1)) 9 | arithmetic_numbers = [ 10 | ## Triclinic 11 | # 1P 12 | 1, 13 | # -1P 14 | 2, 15 | ## Monoclinic 16 | # 2P 17 | 3, 3, 18 | # 2C 19 | 4, 20 | # mP 21 | 5, 5, 22 | # mC 23 | 6, 6, 24 | # 2/mP 25 | 7, 7, 26 | # 2/mC 27 | 8, 28 | # 2/mP 29 | 7, 7, 30 | # 2/mC 31 | 8, 32 | ## Orthorhombic 33 | # 222P 34 | 9, 9, 9, 9, 35 | # 222C 36 | 10, 10, 37 | # 222F 38 | 11, 39 | # 222I 40 | 12, 12, 41 | # mm2P 42 | 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 43 | # mm2C 44 | 14, 14, 14, 45 | # 2mmC 46 | 15, 15, 15, 15, 47 | # mm2F 48 | 16, 16, 49 | # mm2I 50 | 17, 17, 17, 51 | # mmmP 52 | 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 53 | # mmmC 54 | 19, 19, 19, 19, 19, 19, 55 | # mmmF 56 | 20, 20, 57 | # mmmI 58 | 21, 21, 21, 21, 59 | ## Tetragonal 60 | # 4P 61 | 22, 22, 22, 22, 62 | # 4I 63 | 23, 23, 64 | # -4P 65 | 24, 66 | # -4I 67 | 25, 68 | # 4/mP 69 | 26, 26, 26, 26, 70 | # 4/mI 71 | 27, 27, 72 | # 422P 73 | 28, 28, 28, 28, 28, 28, 28, 28, 74 | # 422I 75 | 29, 29, 76 | # 4mmP 77 | 30, 30, 30, 30, 30, 30, 30, 30, 78 | # 4mmI 79 | 31, 31, 31, 31, 80 | # -42mP 81 | 32, 32, 32, 32, 82 | # -4m2P 83 | 33, 33, 33, 33, 84 | # -4m2I 85 | 34, 34, 86 | # -42mI 87 | 35, 35, 88 | # 4/mmmP 89 | 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 90 | # 4/mmmI 91 | 37, 37, 37, 37, 92 | ## Trigonal 93 | # 3P 94 | 38, 38, 38, 95 | # 3R 96 | 39, 97 | # -3P 98 | 40, 99 | # -3R 100 | 41, 101 | # 312P 102 | 42, 103 | # 321P 104 | 43, 105 | # 312P 106 | 42, 107 | # 321P 108 | 43, 109 | # 312P 110 | 42, 111 | # 321P 112 | 43, 113 | # 32R 114 | 44, 115 | # 3m1P 116 | 45, 117 | # 31mP 118 | 46, 119 | # 3m1P 120 | 45, 121 | # 31mP 122 | 46, 123 | # 3mR 124 | 47, 47, 125 | # -31mP 126 | 48, 48, 127 | # -3m1P 128 | 49, 49, 129 | # -3mR 130 | 50, 50, 131 | ## Hexagonal 132 | # 6P 133 | 51, 51, 51, 51, 51, 51, 134 | # -6P 135 | 52, 136 | # 6/mP 137 | 53, 53, 138 | # 622P 139 | 54, 54, 54, 54, 54, 54, 140 | # 6mmP 141 | 55, 55, 55, 55, 142 | # -6m2P 143 | 57, 57, 144 | # -62mP 145 | 56, 56, 146 | # 6/mmmP 147 | 58, 58, 58, 58, 148 | # 23P 149 | 59, 150 | # 23F 151 | 60, 152 | # 23I 153 | 61, 154 | # 23P 155 | 59, 156 | # 23I 157 | 61, 158 | # m-3P 159 | 62, 62, 160 | # m-3F 161 | 63, 63, 162 | # m-3I 163 | 64, 164 | # m-3P 165 | 62, 166 | # m-3I 167 | 64, 168 | # 432P 169 | 65, 65, 170 | # 432F 171 | 66, 66, 172 | # 432I 173 | 67, 174 | # 432P 175 | 65, 65, 176 | # 432I 177 | 67, 178 | # -43mP 179 | 68, 180 | # -43mF 181 | 69, 182 | # -43mI 183 | 70, 184 | # -43mP 185 | 68, 186 | # -43mF 187 | 69, 188 | # -43mI 189 | 70, 190 | # m-3mP 191 | 71, 71, 71, 71, 192 | # m-3mF 193 | 72, 72, 72, 72, 194 | # m-3mI 195 | 73, 73, 196 | ] 197 | # fmt: on 198 | 199 | df = pd.DataFrame({"number": numbers, "arithmetic_number": arithmetic_numbers}) 200 | return df 201 | 202 | 203 | @click.command() 204 | @click.argument("spg_input", type=click.File("r")) 205 | def main(spg_input): 206 | names = [ 207 | "hall_number", 208 | "setting", 209 | "number", 210 | "hall_symbol", 211 | "HM_symbol_short_all", 212 | "HM_symbol_full", 213 | ] 214 | df_hall = pd.read_csv( 215 | spg_input, 216 | header=None, 217 | names=names, 218 | usecols=[ 219 | 0, # hall_number 220 | 2, # setting 221 | 4, # number 222 | 6, # hall symbol 223 | 7, # HM symbol (short) 224 | 8, # HM symbol (full) 225 | ], 226 | ) 227 | df_hall["setting"].fillna(value="", inplace=True) 228 | df_hall["HM_symbol_short"] = ( 229 | df_hall.HM_symbol_short_all.str.split("=").apply(lambda lst: lst[0]).str.rstrip(" ") 230 | ) 231 | df_hall.drop(columns=["HM_symbol_short_all"], inplace=True) 232 | df_hall["centering"] = df_hall["hall_symbol"].apply( 233 | lambda s: f"Centering::{s[0]}" if s[0] != "-" else f"Centering::{s[1]}" 234 | ) 235 | 236 | df_arith = number_to_arithmetic_crystal_class() 237 | 238 | df = pd.merge(df_hall, df_arith, on="number", how="left") 239 | 240 | for _, row in df.iterrows(): 241 | print( 242 | f'HallSymbolEntry::new({row["hall_number"]}, {row["number"]}, {row["arithmetic_number"]}, "{row["setting"]}", "{row["hall_symbol"]}", "{row["HM_symbol_short"]}", "{row["HM_symbol_full"]}", {row["centering"]}),' # noqa: E501 243 | ) 244 | 245 | 246 | if __name__ == "__main__": 247 | main() 248 | -------------------------------------------------------------------------------- /scripts/magnetic_hall_symbol_database.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import click 4 | from magnetic_space_group_type import get_msg_numbers, get_msg_table 5 | 6 | 7 | @click.command() 8 | @click.argument("spglib_dir", type=str) 9 | def main(spglib_dir: str): 10 | msg_dataset_dir = Path(spglib_dir) / "database" / "msg" 11 | msg_table = get_msg_table(msg_dataset_dir / "magnetic_hall_symbols.yaml") 12 | 13 | msg_numbers = get_msg_numbers(msg_dataset_dir / "msg_numbers.csv") 14 | bns_to_uni = {bns_number: uni_number for _, bns_number, _, uni_number in msg_numbers} 15 | 16 | contents = [] 17 | for hall_number, dct in msg_table.items(): 18 | for bns_number, mhall_symbol in dct.items(): 19 | uni_number = bns_to_uni[bns_number] 20 | mhall_symbol = mhall_symbol.replace('"', "=") 21 | line = f'MagneticHallSymbolEntry::new("{mhall_symbol}", {uni_number}),' 22 | contents.append(line) 23 | 24 | print("const MAGNETIC_HALL_SYMBOL_DATABASE: [MagneticHallSymbolEntry; 1651] = [") 25 | for line in contents: 26 | print(f" {line}") 27 | print("];") 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /scripts/magnetic_space_group_type.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import re 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | import click 7 | from ruamel.yaml import YAML 8 | 9 | MAX_BNS_NUMBER_LENGTH = 7 10 | MAX_OG_NUMBER_LENGTH = 11 11 | 12 | 13 | # hall_number -> bns_number -> magnetic Hall symbol 14 | def get_msg_table(path: Path) -> dict[str, dict[str, str]]: 15 | # Load MSG for ITA standard settings 16 | with open(path) as f: 17 | all_datum = dict(YAML().load(f)) 18 | return all_datum 19 | 20 | 21 | def get_msg_numbers(path: Path) -> list[dict[str, Any]]: 22 | all_datum = [] 23 | with open(path) as f: 24 | reader = csv.reader(f, delimiter=",") 25 | next(reader) # skip header 26 | for row in reader: 27 | if len(row) == 0: 28 | break 29 | 30 | litvin_number, bns_number, og_number, uni_number = row 31 | all_datum.append( 32 | ( 33 | int(litvin_number), 34 | bns_number, 35 | og_number, 36 | int(uni_number), 37 | ) 38 | ) 39 | 40 | assert len(all_datum) == 1651 41 | return all_datum 42 | 43 | 44 | def get_type_of_msg(hall_symbol: str) -> int: 45 | # awkward classification... 46 | if "'" not in hall_symbol: 47 | return 1 # type-I 48 | if " 1'" in hall_symbol: 49 | return 2 # type-II 50 | if len(re.findall(r" 1[a-z]+'", hall_symbol)) > 0: 51 | return 4 # type-IV 52 | return 3 # type-III 53 | 54 | 55 | @click.command() 56 | @click.argument("spglib_dir", type=str) 57 | def main(spglib_dir: str): 58 | msg_dataset_dir = Path(spglib_dir) / "database" / "msg" 59 | msg_dataset = get_msg_numbers(msg_dataset_dir / "msg_numbers.csv") 60 | 61 | msg_table = get_msg_table(msg_dataset_dir / "magnetic_hall_symbols.yaml") 62 | 63 | bns_to_mhall = {} 64 | for dct in msg_table.values(): 65 | bns_to_mhall.update(dct) 66 | 67 | contents = [] 68 | construct_type_counts = {t: 0 for t in [1, 2, 3, 4]} 69 | for i, (litvin_number, bns_number, og_number, uni_number) in enumerate(msg_dataset): 70 | assert uni_number == i + 1 71 | assert 0 < len(bns_number) <= MAX_BNS_NUMBER_LENGTH 72 | assert 0 < len(og_number) <= MAX_OG_NUMBER_LENGTH 73 | 74 | magnetic_hall_symbol = bns_to_mhall[bns_number] 75 | construct_type = get_type_of_msg(magnetic_hall_symbol) 76 | xsg_number = int(bns_number.split(".")[0]) 77 | construct_type_counts[construct_type] += 1 78 | 79 | line = f'MagneticSpaceGroupType::new({uni_number}, {litvin_number}, "{bns_number}", "{og_number}", {xsg_number}, ConstructType::Type{construct_type}),' # noqa: E501 80 | contents.append(line) 81 | 82 | # Sanity check 83 | assert construct_type_counts[1] == 230 84 | assert construct_type_counts[2] == 230 85 | assert construct_type_counts[3] == 674 86 | assert construct_type_counts[4] == 517 87 | 88 | print("const MAGNETIC_SPACE_GROUP_TYPES: [MagneticSpaceGroupType; 1651] = [") 89 | for line in contents: 90 | print(f" {line}") 91 | print("];") 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /scripts/setting.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pandas as pd 3 | 4 | 5 | @click.command() 6 | @click.argument("spg_input", type=click.File("r")) 7 | def main(spg_input): 8 | names = [ 9 | "hall_number", 10 | "setting", 11 | "number", 12 | "hall_symbol", 13 | "HM_symbol_short_all", 14 | "HM_symbol_full", 15 | ] 16 | df_hall = pd.read_csv( 17 | spg_input, 18 | header=None, 19 | names=names, 20 | usecols=[ 21 | 0, # hall_number 22 | 2, # setting 23 | 4, # number 24 | 6, # hall symbol 25 | 7, # HM symbol (short) 26 | 8, # HM symbol (full) 27 | ], 28 | ) 29 | df_hall["setting"].fillna(value="", inplace=True) 30 | 31 | print("spglib") 32 | spglib = [] 33 | for number in range(1, 231): 34 | df_tmp = df_hall[df_hall["number"] == number].sort_values("hall_number") 35 | spglib.append(df_tmp.iloc[0]["hall_number"]) 36 | print(spglib) 37 | 38 | print("standard") 39 | standard = [] 40 | for number in range(1, 231): 41 | df_tmp = df_hall[df_hall["number"] == number].sort_values("hall_number") 42 | if df_tmp[df_tmp["setting"] == "2"].empty: 43 | standard.append(df_tmp.iloc[0]["hall_number"]) 44 | else: 45 | standard.append(df_tmp[df_tmp["setting"] == "2"].iloc[0]["hall_number"]) 46 | print(standard) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /scripts/wyckoff.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.command() 5 | @click.argument("wyckoff-input", type=click.File("r")) 6 | def main(wyckoff_input): 7 | all_data = [] 8 | 9 | lines = wyckoff_input.readlines() 10 | hall_number = None 11 | for line in lines: 12 | line = line.strip("\n") 13 | if line[0].isdigit() or (line == "end of data"): 14 | if line[0].isdigit(): 15 | hall_number = int(line.split(":")[0]) 16 | else: 17 | contents = line.split(":") 18 | if contents[2] == "": 19 | continue 20 | multiplicity = int(contents[2]) 21 | letter = contents[3] 22 | site_symmetry = contents[4] 23 | coordinates = contents[5].strip("(").strip(")") 24 | all_data.append((hall_number, multiplicity, letter, site_symmetry, coordinates)) 25 | 26 | print(f"const WYCKOFF_DATABASE: [WyckoffPosition; {len(all_data)}] = [") 27 | for data in all_data: 28 | hall_number, multiplicity, letter, site_symmetry, coordinates = data 29 | print( 30 | f' WyckoffPosition::new({hall_number}, {multiplicity}, \'{letter}\', "{site_symmetry}", "{coordinates}"),' # noqa: E501 31 | ) 32 | print("];") 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | # ba-c 3 | ba = "ba" 4 | 5 | [files] 6 | extend-exclude = ['.gitignore', '*.ipynb'] 7 | --------------------------------------------------------------------------------