├── .gitattributes ├── .github └── workflows │ ├── docs.yml │ └── pythonpackage.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.rst ├── CITATION.cff ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── assets │ ├── chmpy_logo.png │ ├── chmpy_logo@0.5x.png │ ├── chmpy_logo_dark.png │ ├── chmpy_logo_small.png │ ├── chmpy_logo_small_no_text.png │ └── chmpy_logo_small_no_text_dark.png ├── index.md ├── parallel.md └── shape_descriptors.md ├── examples └── describe_cifs.py ├── mkdocs.yml ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── scripts └── mkdocs_gen_ref_pages.py ├── setup.py └── src └── chmpy ├── __init__.py ├── cmd ├── __init__.py └── convert.py ├── core ├── __init__.py ├── dimer.py ├── element.py ├── molecule.py └── wolf.py ├── crystal ├── __init__.py ├── asymmetric_unit.py ├── crystal.py ├── fingerprint.py ├── point_group.py ├── powder.py ├── sfac │ ├── __init__.py │ ├── _sfac.pyx │ └── waaskirf.npz ├── sgdata.json ├── space_group.py ├── symmetry_operation.py ├── unit_cell.py └── wulff.py ├── descriptors ├── __init__.py └── symmetry_function_ani1.py ├── exe ├── __init__.py ├── exe.py ├── gaussian.py ├── gulp.py ├── raspa.py ├── tonto.py └── xtb.py ├── ext ├── __init__.py ├── charges.py ├── cosmo.py ├── crystal.py ├── cx.py ├── elastic_tensor.py ├── excitations.py ├── solvation_parameters.py ├── traj.py └── vasp.py ├── fmt ├── __init__.py ├── ascii.py ├── cif.py ├── crystal17.py ├── cube.py ├── fchk.py ├── gaussian_log.py ├── gen.py ├── gmf.py ├── grd.py ├── gulp.py ├── mol2.py ├── nwchem.py ├── pdb.py ├── raspa.py ├── sdf.py ├── shelx.py ├── smiles.py ├── tmol.py ├── vasp.py ├── xtb.py └── xyz_file.py ├── interpolate ├── __init__.py ├── _density.pyx ├── density.py ├── lerp.py └── thakkar_interp.npz ├── ints ├── __init__.py ├── lebedev.py ├── lebedev_grids.npz └── solvation.py ├── mc ├── __init__.py ├── _mc.py ├── _mc_lewiner.pyx └── lookup_tables.py ├── opt ├── __init__.py ├── gulp.py └── xtb.py ├── sampling ├── __init__.py ├── _lds.pyx ├── _sobol.pyx └── _sobol_parameters.npz ├── shape ├── __init__.py ├── _invariants.pyx ├── _sht.pyx ├── assoc_legendre.py ├── convex_hull.py ├── reconstruct.py ├── shape_descriptors.py ├── sht.py └── spherical_harmonics.py ├── subgraphs ├── __init__.py └── carboxylic_acid.gt ├── surface.py ├── templates ├── __init__.py ├── crystal17.jinja2 ├── gaussian_scf.jinja2 ├── gulp.jinja2 ├── nwchem_input.jinja2 ├── tmol.jinja2 └── tonto_pair_energy.jinja2 ├── tests ├── __init__.py ├── acetic_acid.png ├── core │ ├── __init__.py │ ├── test_element.py │ └── test_molecule.py ├── crystal │ ├── __init__.py │ ├── test_asymmetric_unit.py │ ├── test_crystal.py │ ├── test_space_group.py │ ├── test_unit_cell.py │ └── test_wulff.py ├── exe │ ├── __init__.py │ └── test_raspa.py ├── ext │ ├── __init__.py │ ├── test_eem.py │ └── test_elastic_tensor.py ├── fmt │ ├── __init__.py │ ├── test_gen.py │ ├── test_raspa.py │ └── test_smiles.py ├── promolecule │ ├── __init__.py │ ├── test_density.py │ └── test_surface.py ├── sampling │ └── test_quasirandom.py ├── shape │ ├── __init__.py │ ├── test_shape_descriptors.py │ ├── test_sht.py │ └── test_spherical_harmonics.py └── test_files │ ├── DB09563.sdf │ ├── HXACAN01.pdb │ ├── acetic_acid.cif │ ├── acetic_acid.res │ ├── example.gen │ ├── iceII.cif │ ├── r3c_example.cif │ └── water.xyz └── util ├── __init__.py ├── color.py ├── dict.py ├── exe.py ├── mesh.py ├── num.py ├── path.py ├── text.py ├── unit.py └── util.py /.gitattributes: -------------------------------------------------------------------------------- 1 | promolecule/thakkar_interp.npz filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Configure Git Credentials 15 | run: | 16 | git config user.name github-actions 17 | git config user.email github-actions@github.com 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: 3.11 21 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 22 | - uses: actions/cache@v4 23 | with: 24 | key: mkdocs-${{ env.cache_id }} 25 | path: .cache 26 | restore-keys: | 27 | mkdocs- 28 | - run: pip install '.[docs]' 29 | - run: mkdocs gh-deploy --force 30 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macos-latest] 13 | python-version: ['3.10', '3.10', '3.11', '3.12', '3.13'] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - uses: actions/cache@v3 23 | name: Load cache (Linux) 24 | if: startsWith(runner.os, 'Linux') 25 | with: 26 | path: ~/.cache/pip 27 | key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} 28 | restore-keys: | 29 | ${{ runner.os }}-pip- 30 | 31 | - uses: actions/cache@v3 32 | name: Load cache (macOS) 33 | if: startsWith(runner.os, 'macOS') 34 | with: 35 | path: ~/Library/Caches/pip 36 | key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pip- 39 | 40 | - uses: actions/cache@v3 41 | name: Load cache (Windows) 42 | if: startsWith(runner.os, 'Windows') 43 | with: 44 | path: ~\AppData\Local\pip\Cache 45 | key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pip- 48 | 49 | - name: Install python build module 50 | run: | 51 | python -m pip install --upgrade pip build ruff pytest 52 | 53 | - name: Build the project 54 | run: | 55 | python -m pip install -e . -v 56 | 57 | - name: Lint (ruff) 58 | run: | 59 | # Check for Python syntax errors or undefined names 60 | python -m ruff check . --select=E9,F63,F7,F82 --output-format=github 61 | # Run full linting with the configured rules (warnings only) 62 | python -m ruff check . --output-format=github 63 | 64 | - name: Run tests 65 | run: | 66 | python -m pytest 67 | 68 | 69 | build_wheels: 70 | runs-on: ${{matrix.os}} 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | os: [ubuntu-latest, windows-latest, macos-latest] 75 | pyver: [cp310, cp311, cp312, cp313] 76 | 77 | steps: 78 | - name: Checkout code 79 | uses: actions/checkout@v3 80 | 81 | - name: Build wheels 82 | uses: pypa/cibuildwheel@v2.21.3 83 | env: 84 | CIBW_BUILD: ${{matrix.pyver}}-* 85 | CIBW_ARCHS_LINUX: auto 86 | CIBW_ARCHS_MACOS: auto universal2 87 | CIBW_ARCHS_WINDOWS: auto 88 | # musllinux tests fail with some pid mixup 89 | # cross-build macos images can't be tested on this runner. 90 | CIBW_TEST_SKIP: >- 91 | *-* 92 | 93 | - name: Upload artifacts 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 97 | path: ./wheelhouse/*.whl 98 | overwrite: true 99 | 100 | create-release: 101 | runs-on: ubuntu-latest 102 | needs: build_wheels 103 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 104 | steps: 105 | 106 | - name: Checkout code 107 | uses: actions/checkout@v2 108 | 109 | - name: Download Artifacts 110 | uses: actions/download-artifact@v4 111 | with: 112 | # unpacks all CIBW artifacts into dist/ 113 | pattern: cibw-* 114 | path: dist 115 | merge-multiple: true 116 | 117 | - name: Release 118 | uses: softprops/action-gh-release@v1 119 | with: 120 | files: | 121 | dist/* 122 | 123 | 124 | upload_all: 125 | runs-on: ubuntu-latest 126 | needs: build_wheels 127 | environment: 128 | name: pypi 129 | url: https://pypi.org/p/chmpy/ 130 | permissions: 131 | id-token: write 132 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 133 | steps: 134 | - uses: actions/download-artifact@v4.1.7 135 | with: 136 | pattern: cibw-* 137 | path: dist 138 | merge-multiple: true 139 | 140 | - uses: pypa/gh-action-pypi-publish@release/v1 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | dist/ 4 | *.egg-info/ 5 | __pycache__/ 6 | *.c 7 | *.so 8 | *.cpp 9 | .coverage 10 | junk/ 11 | .*.swp 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.5.1 # Use the latest version 4 | hooks: 5 | - id: ruff 6 | args: [--fix] 7 | - id: ruff-format 8 | 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v4.5.0 11 | hooks: 12 | - id: trailing-whitespace 13 | - id: end-of-file-fixer 14 | - id: check-yaml 15 | - id: check-toml 16 | - id: debug-statements -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/CHANGELOG.rst -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.1.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: Spackman 5 | given-names: "Peter R." 6 | orcid: "https://orcid.org/0000-0002-6532-8571" 7 | title: "Chmpy: A python library for computational chemistry" 8 | version: v1.1.5 9 | date-released: 2024-02-24 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Code under the chmpy/mc subdirectory contains some code modified 2 | from the scikit-image project, subject to the following license: 3 | 4 | Copyright (C) 2019, the scikit-image team 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are 9 | met: 10 | 11 | 1. Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 3. Neither the name of skimage nor the names of its contributors may be 18 | used to endorse or promote products derived from this software without 19 | specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AS IS AND ANY EXPRESS OR IMPLIED 22 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 23 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 24 | EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 26 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE), 29 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | POSSIBILITY OF SUCH DAMAGE. 31 | 32 | All other code is subject to the GPLv3. 33 | If you wish to distribute some of this code in a 34 | commercial program (i.e. the GPLv3 is not suitable for 35 | your purposes) then feel free to contact me. 36 | 37 | Copyright (C) 2021, Peter R. Spackman 38 | 39 | This program is free software: you can redistribute it and/or modify it under 40 | the terms of the GNU General Public License as published by the Free Software 41 | Foundation, either version 3 of the License, or (at your option) any later 42 | version. 43 | 44 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 45 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 46 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 47 | 48 | You should have received a copy of the GNU Public License along with this 49 | program. If not, see . 50 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft tests 2 | global-include *.jinja2 *.gt *.json *.npz *.py 3 | global-exclude *.py[cod] 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/peterspackman/chmpy/workflows/CI/badge.svg) 2 | [![DOI](https://zenodo.org/badge/211644812.svg)](https://zenodo.org/doi/10.5281/zenodo.10697512) 3 | 4 | 5 | 6 |
7 |
8 | 9 | chmpy logo 10 | 11 | 12 |

chmpy

13 | 14 |

15 | A library for computational chemistry in python. 16 |
17 | Documentation» 18 |
19 | Report a bug 20 | · 21 | Request a new feature 22 |

23 |
24 | 25 | chmpy supports handling molecules, crystals, Hirshfeld & promolecule 26 | density isosurfaces, spherical harmonic shape descriptors and much more... 27 | 28 | # Installation 29 | 30 | Basic installation can be done through the python package manager `pip`: 31 | 32 | ``` bash 33 | pip install chmpy 34 | # or to install directly from GitHub: 35 | pip install git+https://github.com/peterspackman/chmpy.git 36 | ``` 37 | 38 | For development or modifications, install locally using pip: 39 | 40 | ``` bash 41 | pip install -e . 42 | ``` 43 | 44 | # Features 45 | While the library is intended to be flexible and make it easy to build 46 | complex pipelines or properties, the following is a brief summary of 47 | intended features: 48 | 49 | - Load crystal structures from `.cif`, `.res`, `POSCAR` files. 50 | - Evaluate promolecule and procrystal electron densities. 51 | - Easily generate Hirshfeld or promolecule isosurfaces and associated properties. 52 | - Easily generate spherical harmonic shape descriptors for atoms, molecules, or molecular fragments. 53 | - Efficiently calculate crystal slabs, periodic connectivity and more... 54 | - Automatic parallelization of some calculations using OpenMP (set the `OMP_NUM_THREADS` environment variable) 55 | 56 | It should also serve as a simple, easy to read library for learning 57 | how to represent crystal structures, molecules etc. and evaluate 58 | scientifically relevant information quickly and efficiently using 59 | python. 60 | 61 | # Examples 62 | 63 | ## Crystal structures and molecules 64 | 65 | Loading a crystal structure from a CIF (`.cif`) or SHELX (`.res`) 66 | file, or a molecule from an XMOL (`.xyz`) file is straightforward: 67 | 68 | ``` python 69 | from chmpy import Crystal, Molecule 70 | c = Crystal.load("tests/acetic_acid.cif") 71 | print(c) 72 | # 73 | # Calculate the unique molecules in this crystal 74 | c.symmetry_unique_molecules() 75 | # [] 76 | m = Molecule.load("tests/water.xyz") 77 | print(m) 78 | # 79 | ``` 80 | 81 | ## Hirshfeld and promolecule density isosurfaces 82 | 83 | Hirshfeld and promolecule density isosurfaces 84 | 85 | Generation of surfaces with the default settings can be done with 86 | minimal hassle, simply by using the corresponding members of the Crystal 87 | class: 88 | 89 | ``` python 90 | c = Crystal.load("tests/test_files/acetic_acid.cif") 91 | # This will generate a high resolution surface 92 | # for each symmetry unique molecule in the crystal 93 | surfaces = c.hirshfeld_surfaces() 94 | print(surfaces) 95 | # [] 96 | # We can generate lower resolution surfaces with the separation parameter 97 | surfaces = c.hirshfeld_surfaces(separation=0.5) 98 | print(surfaces) 99 | # [] 100 | # Surfaces can be saved via trimesh, or a utility function provided in chmpy 101 | from chmpy.util.mesh import save_mesh 102 | save_mesh(surfaces[0], "acetic_acid.ply") 103 | ``` 104 | 105 | The resulting surface should look something like this when visualized: 106 | 107 |
108 |
109 | Acetic acid 110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /docs/assets/chmpy_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/docs/assets/chmpy_logo.png -------------------------------------------------------------------------------- /docs/assets/chmpy_logo@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/docs/assets/chmpy_logo@0.5x.png -------------------------------------------------------------------------------- /docs/assets/chmpy_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/docs/assets/chmpy_logo_dark.png -------------------------------------------------------------------------------- /docs/assets/chmpy_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/docs/assets/chmpy_logo_small.png -------------------------------------------------------------------------------- /docs/assets/chmpy_logo_small_no_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/docs/assets/chmpy_logo_small_no_text.png -------------------------------------------------------------------------------- /docs/assets/chmpy_logo_small_no_text_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/docs/assets/chmpy_logo_small_no_text_dark.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # chmpy 2 | 3 | A library for computational chemistry in python. Featuring support for 4 | molecules, crystals, Hirshfeld & promolecule density isosurfaces, 5 | spherical harmonic shape descriptors and much more... 6 | 7 | ## Features 8 | While the library is intended to be flexible and make it easy to build 9 | complex pipelines or properties, the following is a brief summary of 10 | intended features: 11 | 12 | - Load crystal structures from ``.cif``, ``.res``, ``POSCAR`` files, 13 | and molecules from ``.xyz``, ``.sdf`` files. 14 | - Evaluate promolecule and procrystal electron densities. 15 | - Easily generate Hirshfeld or promolecule isosurfaces and associated properties. 16 | - Easily generate spherical harmonic shape descriptors for atoms, molecules, or molecular fragments. 17 | - Efficiently calculate crystal slabs, periodic connectivity and more... 18 | 19 | It should also serve as a simple, easy to read library for learning 20 | how to represent crystal structures, molecules etc. and evaluate 21 | scientifically relevant information quickly and efficiently using 22 | python. 23 | 24 | ## Crystal structures and molecules 25 | Loading a crystal structure from a CIF (`.cif`) or SHELX (``.res``) 26 | file, or a molecule from an XMOL (``.xyz``) file is straightforward: 27 | 28 | ``` py title="chmpy_basics.py" 29 | from chmpy import Crystal, Molecule 30 | c = Crystal.load("tests/test_files/acetic_acid.cif") 31 | c 32 | # 33 | 34 | # Calculate the unique molecules in this crystal 35 | c.symmetry_unique_molecules() 36 | # [] 37 | 38 | m = Molecule.load("tests/test_files/water.xyz") 39 | m 40 | # 41 | ``` 42 | 43 | 44 | ## Hirshfeld and promolecule density isosurfaces 45 | 46 | Generation of surfaces with the default settings can be done with 47 | minimal hassle, simply by using the corresponding members of the ``Crystal`` 48 | class: 49 | 50 | ``` python 51 | c = Crystal.load("tests/test_files/acetic_acid.cif") 52 | 53 | # This will generate a high resolution surface 54 | # for each symmetry unique molecule in the crystal 55 | surfaces = c.hirshfeld_surfaces() 56 | surfaces 57 | # [] 58 | 59 | # We can generate lower resolution surfaces with the separation parameter 60 | surfaces = c.hirshfeld_surfaces(separation=0.5) 61 | surfaces 62 | # [] 63 | 64 | # Surfaces can be saved via trimesh 65 | surfaces[0].export("acetic_acid_trimesh.ply", "ply") 66 | # or a utility function provided in chmpy 67 | from chmpy.util.mesh import save_mesh 68 | save_mesh(surfaces[0], "acetic_acid.ply") 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/parallel.md: -------------------------------------------------------------------------------- 1 | # Parallelization 2 | 3 | Some of the `Cython` code in `chmpy` makes use of OpenMP 4 | parallelism. If this is interfering with your own parallelism 5 | at a higher level, or you simply wish to modify how many cores 6 | the code should make use of, consider setting the environment variable 7 | `OMP_NUM_THREADS` to the desired number of threads. 8 | 9 | ``` bash 10 | export OMP_NUM_THREADS=1 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/shape_descriptors.md: -------------------------------------------------------------------------------- 1 | # Shape descriptors 2 | 3 | ## What they are 4 | 5 | A rotation invariant description of a shape. 6 | In essence, these are rotation invariants calculated from the coefficients of 7 | the spherical harmonic transform of a shape function, which in our 8 | case is either the radius (distance from the origin) of an isosurface 9 | as a function of the spherical angles theta and phi. 10 | 11 | 12 | ### References: 13 | 1. [PR Spackman et al. Sci. Rep. 6, 22204 (2016)](https://dx.doi.org/10.1038/srep22204) 14 | 2. [PR Spackman et al. Angew. Chem. 58 (47), 16780-16784 (2019)](https://dx.doi.org/10.1002/anie.201906602) 15 | 16 | 17 | ## How to calculate shape descriptors 18 | 19 | ### Molecules in crystals 20 | 21 | ``` python 22 | from chmpy import Crystal 23 | c = Crystal.load("tests/test_files/acetic_acid.cif") 24 | 25 | # calculate shape descriptors for each molecule in the asymmetric unit 26 | desc = c.molecular_shape_descriptors() 27 | ``` 28 | 29 | ### Atoms in crystals 30 | Likewise, atomic shape descriptors can be conveniently 31 | calculated directly from the `Crystal` object: 32 | 33 | ``` python 34 | from chmpy import Crystal 35 | c = Crystal.load("tests/test_files/acetic_acid.cif") 36 | 37 | # calculate shape descriptors for each atom in the asymmetric unit 38 | desc = c.atomic_shape_descriptors() 39 | ``` 40 | 41 | ### Isolated molecules 42 | 43 | Hirshfeld surfaces typically only have a sensible definition 44 | in a crystal (or at least in a environment where the molecule 45 | is not isolated). As such, the more sensible descriptor to 46 | utilise may be one of the **Promolecule density isosurface**. 47 | 48 | This can be readily calculated using the `Molecule` object: 49 | 50 | ``` python 51 | from chmpy import Molecule 52 | m = Molecule.load("tests/test_files/water.xyz") 53 | desc = m.shape_descriptors() 54 | 55 | # use EEM calculated charges to describe the shape and the ESP 56 | # up to maximum angular momentum 16 57 | desc_with_esp = m.shape_descriptors(l_max=16, with_property="esp") 58 | ``` 59 | 60 | However, another useful descriptor of atomic environments 61 | is a Hirshfeld-type descriptor in a molecule, where in order to 62 | 'close' the exterior of the surface we introduce a `background` 63 | density, as follows: 64 | 65 | ``` python 66 | from chmpy import Molecule 67 | m = Molecule.load("tests/test_files/water.xyz") 68 | # with the default background density 69 | desc = m.atomic_shape_descriptors() 70 | 71 | # or with, a larger background density, contracting the atoms 72 | desc = m.atomic_shape_descriptors(background=0.0001) 73 | ``` 74 | -------------------------------------------------------------------------------- /examples/describe_cifs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from concurrent.futures import ProcessPoolExecutor, as_completed 3 | from pathlib import Path 4 | 5 | import numpy as np 6 | from tqdm import tqdm 7 | 8 | from chmpy.crystal import Crystal 9 | 10 | LOG = logging.getLogger("test_describe") 11 | 12 | 13 | def describe(args, l_max=3): 14 | name, crystal = args 15 | name = Path(name).stem 16 | return ( 17 | name, 18 | crystal.asymmetric_unit.atomic_numbers, 19 | crystal.atomic_shape_descriptors(l_max=l_max), 20 | ) 21 | 22 | 23 | def main(): 24 | import argparse 25 | 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument("directory", help="cif files") 28 | parser.add_argument("-o", "--output", default="output_{l_max}.npz") 29 | parser.add_argument("--log-level", default="INFO") 30 | parser.add_argument("-l", "--lmax", default=7, help="maximum angular momenta") 31 | args = parser.parse_args() 32 | logging.basicConfig(level="INFO") 33 | 34 | crystals = [] 35 | cifs = list(Path(args.directory).glob("*.cif")) 36 | for path in tqdm(cifs, desc="Loading crystals"): 37 | try: 38 | crystals.append((str(path), Crystal.load(str(path)))) 39 | except Exception: 40 | LOG.error("Error reading %s, skipping", path.name) 41 | 42 | natoms = sum(len(x[1].asymmetric_unit) for x in crystals) 43 | print("Total atoms in all crystals: ", natoms) 44 | l_max = args.lmax 45 | with ProcessPoolExecutor(2) as e: 46 | descriptors = {} 47 | futures = [e.submit(describe, crystal, l_max=l_max) for crystal in crystals] 48 | with tqdm(total=natoms, desc=f"l_max={l_max}", unit="atom") as pbar: 49 | for f in as_completed(futures): 50 | name, nums, desc = f.result() 51 | descriptors[name + "desc"] = desc 52 | descriptors[name + "element"] = nums 53 | pbar.update(len(nums)) 54 | 55 | output = args.output.format(l_max=l_max) 56 | print("Saving to ", output) 57 | np.savez_compressed(output, **descriptors) 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: chmpy 2 | site_url: https://peterspackman.github.io/chmpy/ 3 | repo_url: https://github.com/peterspackman/chmpy 4 | repo_name: peterspackman/chmpy 5 | 6 | theme: 7 | name: material 8 | logo: assets/chmpy_logo_small_no_text_dark.png 9 | features: 10 | - content.code.copy 11 | palette: 12 | primary: blue grey 13 | scheme: preference 14 | font: 15 | text: Helvetica 16 | code: Noto Sans Mono 17 | 18 | extra: 19 | social: 20 | - icon: fontawesome/brands/github 21 | link: https://github.com/peterspackman 22 | 23 | plugins: 24 | - search 25 | - mkdocstrings 26 | - gen-files: 27 | scripts: 28 | - scripts/mkdocs_gen_ref_pages.py 29 | - literate-nav: 30 | nav_file: SUMMARY.md 31 | 32 | 33 | markdown_extensions: 34 | - pymdownx.highlight: 35 | anchor_linenums: true 36 | line_spans: __span 37 | pygments_lang_class: true 38 | - pymdownx.inlinehilite 39 | - pymdownx.snippets 40 | - pymdownx.superfences 41 | 42 | nav: 43 | - Home: index.md 44 | - Overview: 45 | - Shape Descriptors: shape_descriptors.md 46 | - Notes on Parallelization: parallel.md 47 | - Programming Interface (API): reference/ 48 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "chmpy" 3 | readme = "README.md" 4 | version = "1.1.6" 5 | requires-python = ">=3.10" 6 | description = "Molecules, crystals, promolecule and Hirshfeld surfaces using python." 7 | authors = [ 8 | { name = "Peter Spackman", email = "peterspackman@fastmail.com"} 9 | ] 10 | license = { file="LICENSE.txt" } 11 | dependencies = [ 12 | "numpy>=2.1.2", 13 | "scipy>=1.14.1", 14 | "trimesh>=4.5.0", 15 | "matplotlib>=3.9.2", 16 | "seaborn>=0.13.2", 17 | "Jinja2>=3.1.2" 18 | ] 19 | 20 | [project.optional-dependencies] 21 | graph = ["graph_tool"] 22 | docs = ["mkdocs", "mkdocs-material", "mkdocstrings", "mkdocstrings-python", "mkdocs-gen-files", "mkdocs-literate-nav"] 23 | 24 | [project.urls] 25 | homepage = "https://github.com/peterspackman/chmpy" 26 | repository = "https://github.com/peterspackman/chmpy" 27 | documentation = "https://peterspackman.github.io/chmpy/" 28 | 29 | [build-system] 30 | requires = ["setuptools>=68.0", "cython>=3.0", "numpy>=2.0"] 31 | build-backend = "setuptools.build_meta" 32 | 33 | [tool.setuptools] 34 | include-package-data = true 35 | 36 | [tool.setuptools.packages.find] 37 | where = ["src"] 38 | 39 | [tool.cibuildwheel] 40 | skip = "pp*" 41 | 42 | [tool.ruff.lint] 43 | select = ["E", "F"] 44 | ignore = ["E741"] 45 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = test*.py 3 | addopts = --doctest-modules --doctest-ignore-import-errors 4 | log_level = DEBUG 5 | norecursedirs = docs *.egg-info .git .tox 6 | testpaths = src/chmpy/tests 7 | filterwarnings = ignore::DeprecationWarning 8 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # Ruff configuration 2 | line-length = 88 3 | target-version = "py310" 4 | include = ["*.py", "*.pyi"] # Exclude .pyx files 5 | exclude = ["*.pyx", "*/_*.pyx"] 6 | 7 | [lint] 8 | select = [ 9 | "E", # pycodestyle errors 10 | "F", # pyflakes 11 | "I", # isort 12 | "W", # pycodestyle warnings 13 | "B", # flake8-bugbear 14 | "C4", # flake8-comprehensions 15 | "UP", # pyupgrade 16 | ] 17 | ignore = [ 18 | "E741", # ambiguous variable names like 'l', 'I', 'O' 19 | "E501", # line too long (handled by formatter) 20 | ] 21 | 22 | # Allow autofix for all enabled rules (when `--fix` is provided) 23 | fixable = ["ALL"] 24 | unfixable = [] 25 | 26 | # Add specific per-file ignores for math-heavy files 27 | [lint.per-file-ignores] 28 | "__init__.py" = ["F401"] # unused imports in __init__.py 29 | "src/chmpy/tests/**/*.py" = ["E501"] # allow long lines in tests 30 | "src/chmpy/shape/*.py" = ["E741"] # allow single-letter math variables 31 | "src/chmpy/crystal/*.py" = ["E741"] # allow single-letter math variables 32 | "src/chmpy/ints/*.py" = ["E741"] # allow single-letter math variables 33 | "src/chmpy/interpolate/*.py" = ["E741"] # allow single-letter math variables 34 | 35 | [format] 36 | quote-style = "double" 37 | indent-style = "space" 38 | line-ending = "auto" 39 | skip-magic-trailing-comma = false 40 | docstring-code-format = true 41 | 42 | [lint.isort] 43 | known-first-party = ["chmpy"] 44 | known-third-party = ["numpy", "scipy", "matplotlib", "seaborn", "trimesh", "jinja2"] -------------------------------------------------------------------------------- /scripts/mkdocs_gen_ref_pages.py: -------------------------------------------------------------------------------- 1 | """Generate the code reference pages.""" 2 | 3 | from pathlib import Path 4 | 5 | import mkdocs_gen_files 6 | 7 | nav = mkdocs_gen_files.Nav() 8 | src = Path(__file__).parent.parent / "src" 9 | 10 | for path in sorted(src.rglob("*.py")): 11 | if "test" in path.name: 12 | continue 13 | module_path = path.relative_to(src).with_suffix("") 14 | doc_path = path.relative_to(src).with_suffix(".md") 15 | full_doc_path = Path("reference", doc_path) 16 | 17 | parts = tuple(module_path.parts) 18 | 19 | if parts[-1] == "__init__": 20 | continue 21 | elif parts[-1] == "__main__": 22 | continue 23 | 24 | nav[parts] = doc_path.as_posix() 25 | 26 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 27 | identifier = ".".join(parts) 28 | print("::: " + identifier, file=fd) 29 | 30 | mkdocs_gen_files.set_edit_path(full_doc_path, path) 31 | 32 | 33 | with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: 34 | nav_file.writelines(nav.build_literate_nav()) 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from setuptools import Extension, setup 3 | 4 | np_defines = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] 5 | np_includes = [numpy.get_include()] 6 | 7 | extension_modules = [ 8 | Extension( 9 | "chmpy.interpolate._density", 10 | sources=["src/chmpy/interpolate/_density.pyx"], 11 | define_macros=np_defines, 12 | include_dirs=np_includes, 13 | ), 14 | Extension( 15 | "chmpy.shape._invariants", 16 | sources=["src/chmpy/shape/_invariants.pyx"], 17 | define_macros=np_defines, 18 | include_dirs=np_includes, 19 | ), 20 | Extension( 21 | "chmpy.shape._sht", 22 | sources=["src/chmpy/shape/_sht.pyx"], 23 | define_macros=np_defines, 24 | include_dirs=np_includes, 25 | ), 26 | Extension( 27 | "chmpy.crystal.sfac._sfac", 28 | sources=["src/chmpy/crystal/sfac/_sfac.pyx"], 29 | define_macros=np_defines, 30 | include_dirs=np_includes, 31 | ), 32 | Extension( 33 | "chmpy.mc._mc_lewiner", 34 | sources=["src/chmpy/mc/_mc_lewiner.pyx"], 35 | define_macros=np_defines, 36 | include_dirs=np_includes, 37 | ), 38 | Extension( 39 | "chmpy.sampling._lds", 40 | sources=["src/chmpy/sampling/_lds.pyx"], 41 | define_macros=np_defines, 42 | include_dirs=np_includes, 43 | ), 44 | Extension( 45 | "chmpy.sampling._sobol", 46 | sources=["src/chmpy/sampling/_sobol.pyx"], 47 | define_macros=np_defines, 48 | include_dirs=np_includes, 49 | ), 50 | ] 51 | 52 | setup( 53 | ext_modules=extension_modules, 54 | ) 55 | -------------------------------------------------------------------------------- /src/chmpy/__init__.py: -------------------------------------------------------------------------------- 1 | from . import surface 2 | from .core import Element, Molecule 3 | from .crystal import Crystal, SpaceGroup, UnitCell 4 | from .interpolate import PromoleculeDensity, StockholderWeight 5 | 6 | __all__ = [ 7 | "Crystal", 8 | "Element", 9 | "Molecule", 10 | "PromoleculeDensity", 11 | "SpaceGroup", 12 | "StockholderWeight", 13 | "UnitCell", 14 | "surface", 15 | ] 16 | -------------------------------------------------------------------------------- /src/chmpy/cmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/cmd/__init__.py -------------------------------------------------------------------------------- /src/chmpy/cmd/convert.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from pathlib import Path 4 | 5 | from chmpy import Crystal, Molecule 6 | 7 | LOG = logging.getLogger("chmpy-convert") 8 | 9 | 10 | def main(): 11 | import argparse 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("input") 15 | parser.add_argument("-o", "--output", required=True) 16 | parser.add_argument("-if", "--input-format", default="file_ext") 17 | parser.add_argument("-of", "--output-format", default="file_ext") 18 | parser.add_argument("--log-level", default="INFO") 19 | args = parser.parse_args() 20 | logging.basicConfig(level=args.log_level) 21 | 22 | Path(args.input) 23 | Path(args.output) 24 | 25 | in_kwargs = {} 26 | if args.input_format != "file_ext": 27 | in_kwargs["fmt"] = args.input_format 28 | out_kwargs = {} 29 | if args.output_format != "file_ext": 30 | out_kwargs["fmt"] = args.output_format 31 | 32 | x = None 33 | for cls in Molecule, Crystal: 34 | try: 35 | x = cls.load(args.input, **in_kwargs) 36 | break 37 | except KeyError: 38 | pass 39 | else: 40 | LOG.error("Could not delegate parser for '%s'", args.input) 41 | sys.exit(1) 42 | 43 | LOG.debug("Loaded %s from %s", x, args.input) 44 | 45 | try: 46 | x.save(args.output, **out_kwargs) 47 | except KeyError as e: 48 | LOG.error("No such writer available (%s) for file '%s'", e, args.output) 49 | sys.exit(1) 50 | 51 | LOG.debug("Saved %s to %s", x, args.output) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /src/chmpy/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | from .molecule import Molecule 3 | 4 | __all__ = ["Element", "Molecule"] 5 | -------------------------------------------------------------------------------- /src/chmpy/core/wolf.py: -------------------------------------------------------------------------------- 1 | def wolf_sum(crystal, cutoff=16.0, eta=0.2, charges=None): 2 | """ 3 | Compute the Coulomb interaction via Wolf sum and damped Coulomb potential 4 | using point charges. 5 | 6 | Arguments: 7 | crystal (Crystal): the crystal for which to compute the Wolf sum. 8 | cutoff (float, optional): the cutoff radius (in Angstroms) for 9 | which to compute the neighbouring charges (default=16) 10 | eta (float, optional): the eta parameter (1/Angstroms), if unsure 11 | just leave this at its default (default=0.2) 12 | charges (array_like, optional): charges of the atoms in the asymmetric 13 | unit, if not provided then they will be 'guessed' using the EEM 14 | method on the isolated molecules 15 | 16 | Returns: 17 | the total electrostatic energy of the asymmetric unit in the 18 | provided crystal (Hartrees) 19 | """ 20 | import numpy as np 21 | from scipy.special import erfc 22 | 23 | from chmpy.util.unit import ANGSTROM_TO_BOHR 24 | 25 | if charges is None: 26 | charges = np.empty(len(crystal.asym)) 27 | for mol in crystal.symmetry_unique_molecules(): 28 | pq = mol.partial_charges 29 | charges[mol.properties["asymmetric_unit_atoms"]] = pq 30 | else: 31 | charges = np.array(charges) 32 | 33 | # convert to Bohr (i.e. perform the calculation in au) 34 | eta /= ANGSTROM_TO_BOHR 35 | rc = cutoff * ANGSTROM_TO_BOHR 36 | trc = erfc(eta * rc) / rc 37 | sqrt_pi = np.sqrt(np.pi) 38 | 39 | self_term = 0 40 | pair_term = 0 41 | 42 | for surrounds in crystal.atomic_surroundings(radius=cutoff): # angstroms here 43 | i = surrounds["centre"]["asym_atom"] 44 | qi = charges[i] 45 | self_term += qi * qi 46 | qj = charges[surrounds["neighbours"]["asym_atom"]] 47 | rij = ANGSTROM_TO_BOHR * surrounds["neighbours"]["distance"] 48 | pair_term += np.sum(qi * qj * (erfc(eta * rij) / rij - trc)) 49 | 50 | self_term *= 0.5 * trc + eta / sqrt_pi 51 | 52 | return 0.5 * pair_term - self_term 53 | -------------------------------------------------------------------------------- /src/chmpy/crystal/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements funcionality associated with 3 | 3D periodic crystals (`Crystal`), including Bravais lattices/unit cells (`UnitCell`), 4 | space groups (`SpaceGroup`), point groups (`PointGroup`), symmetry operations in 5 | fractional coordinates (`SymmetryOperation`) and more. 6 | """ 7 | 8 | from .asymmetric_unit import AsymmetricUnit 9 | from .crystal import Crystal 10 | from .point_group import PointGroup 11 | from .space_group import SpaceGroup 12 | from .symmetry_operation import SymmetryOperation 13 | from .unit_cell import UnitCell 14 | 15 | __all__ = [ 16 | "AsymmetricUnit", 17 | "Crystal", 18 | "SpaceGroup", 19 | "PointGroup", 20 | "UnitCell", 21 | "SymmetryOperation", 22 | ] 23 | -------------------------------------------------------------------------------- /src/chmpy/crystal/asymmetric_unit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import defaultdict 3 | 4 | import numpy as np 5 | 6 | from chmpy.core.element import Element, chemical_formula 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | 11 | class AsymmetricUnit: 12 | """ 13 | Storage class for the coordinates and labels in a crystal 14 | asymmetric unit 15 | 16 | Attributes: 17 | elements (List[Element]): N length list of elements associated with 18 | the sites in this asymmetric unit 19 | positions (array_like): (N, 3) array of site positions in fractional 20 | coordinates 21 | labels (array_like): N length array of string labels for each site 22 | """ 23 | 24 | def __init__(self, elements, positions, labels=None, **kwargs): 25 | """ 26 | Create an asymmetric unit object from a list of Elements and 27 | an array of fractional coordinates. 28 | 29 | 30 | Arguments: 31 | elements (List[Element]): N length list of elements associated 32 | with the sites 33 | positions (array_like): (N, 3) array of site positions in 34 | fractional coordinates 35 | labels (array_like, optional): labels (array_like): N length 36 | array of string labels for each site 37 | **kwargs: Additional properties (will populate the properties member) 38 | to store in this asymmetric unit 39 | 40 | """ 41 | self.elements = elements 42 | self.atomic_numbers = np.asarray([x.atomic_number for x in elements]) 43 | self.positions = np.asarray(positions) 44 | self.properties = {} 45 | self.properties.update(kwargs) 46 | if labels is None: 47 | self.labels = [] 48 | label_index = defaultdict(int) 49 | for el in self.elements: 50 | label_index[el] += 1 51 | self.labels.append(f"{el}{label_index[el]}") 52 | else: 53 | self.labels = labels 54 | self.labels = np.array(self.labels) 55 | 56 | @property 57 | def formula(self): 58 | """Molecular formula for this asymmetric unit""" 59 | return chemical_formula(self.elements, subscript=False) 60 | 61 | def __len__(self): 62 | return len(self.elements) 63 | 64 | def __repr__(self): 65 | return f"<{self.formula}>" 66 | 67 | @classmethod 68 | def from_records(cls, records): 69 | """Initialize an AsymmetricUnit from a list of dictionary like objects 70 | 71 | Arguments: 72 | records (iterable): An iterable containing dict_like objects with `label`, 73 | `element`, `position` and optionally `occupation` stored. 74 | """ 75 | labels = [] 76 | elements = [] 77 | positions = [] 78 | occupation = [] 79 | for r in records: 80 | labels.append(r["label"]) 81 | elements.append(Element[r["element"]]) 82 | positions.append(r["position"]) 83 | occupation.append(r.get("occupation", 1.0)) 84 | positions = np.asarray(positions) 85 | return AsymmetricUnit(elements, positions, labels=labels, occupation=occupation) 86 | -------------------------------------------------------------------------------- /src/chmpy/crystal/fingerprint.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | 5 | def sample_face_points(vertices, faces, samples_per_edge=4): 6 | """Generate sample points within triangle faces using barycentric coordinates.""" 7 | points = [] 8 | weights = [] 9 | 10 | for i in range(samples_per_edge + 1): 11 | for j in range(samples_per_edge + 1 - i): 12 | a = i / samples_per_edge 13 | b = j / samples_per_edge 14 | c = 1.0 - a - b 15 | points.append([a, b, c]) 16 | weights.append(1.0 / ((samples_per_edge + 1) * (samples_per_edge + 2) / 2)) 17 | 18 | points = np.array(points) 19 | weights = np.array(weights) 20 | 21 | face_vertices = vertices[faces] 22 | 23 | points = points[:, None, :] 24 | 25 | interpolated = np.sum(points[..., None] * face_vertices[None, ...], axis=2) 26 | 27 | return interpolated, weights 28 | 29 | 30 | def filtered_histogram( 31 | mesh, internal, external, bins=200, xrange=None, yrange=None, samples_per_edge=4 32 | ): 33 | """Create histogram with multiple samples per face.""" 34 | di = mesh.vertex_attributes["d_i"] 35 | de = mesh.vertex_attributes["d_e"] 36 | if xrange is None: 37 | xrange = np.min(di), np.max(di) 38 | if yrange is None: 39 | yrange = np.min(de), np.max(de) 40 | 41 | di_atom = mesh.vertex_attributes["nearest_atom_internal"] 42 | de_atom = mesh.vertex_attributes["nearest_atom_external"] 43 | vertex_mask = (de_atom == external) & (di_atom == internal) 44 | 45 | face_mask = np.any(vertex_mask[mesh.faces], axis=1) 46 | filtered_faces = mesh.faces[face_mask] 47 | 48 | if len(filtered_faces) == 0: 49 | return np.histogram2d([], [], bins=bins, range=(xrange, yrange)) 50 | 51 | vertices = np.stack([di, de], axis=1) 52 | 53 | interpolated, weights = sample_face_points( 54 | vertices, filtered_faces, samples_per_edge 55 | ) 56 | 57 | di_samples = interpolated[..., 0].flatten() 58 | de_samples = interpolated[..., 1].flatten() 59 | 60 | weights_tiled = np.tile(weights, len(filtered_faces)) 61 | 62 | return np.histogram2d( 63 | di_samples, de_samples, bins=bins, range=(xrange, yrange), weights=weights_tiled 64 | ) 65 | 66 | 67 | def fingerprint_histogram(mesh, bins=200, xrange=None, yrange=None, samples_per_edge=4): 68 | """Create histogram for all faces with multiple samples per face.""" 69 | di = mesh.vertex_attributes["d_i"] 70 | de = mesh.vertex_attributes["d_e"] 71 | if xrange is None: 72 | xrange = np.min(di), np.max(di) 73 | if yrange is None: 74 | yrange = np.min(de), np.max(de) 75 | 76 | vertices = np.stack([di, de], axis=1) 77 | 78 | interpolated, weights = sample_face_points(vertices, mesh.faces, samples_per_edge) 79 | 80 | di_samples = interpolated[..., 0].flatten() 81 | de_samples = interpolated[..., 1].flatten() 82 | 83 | weights_tiled = np.tile(weights, len(mesh.faces)) 84 | 85 | return np.histogram2d( 86 | di_samples, de_samples, bins=bins, range=(xrange, yrange), weights=weights_tiled 87 | ) 88 | 89 | 90 | def plot_fingerprint_histogram( 91 | hist, ax=None, filename=None, cmap="coolwarm", xlim=(0.5, 2.5), ylim=(0.5, 2.5) 92 | ): 93 | if ax is None: 94 | fig, ax = plt.subplots() 95 | fig.set_size_inches(4, 4) 96 | 97 | H1, xedges, yedges = hist 98 | X, Y = np.meshgrid(xedges, yedges) 99 | H1[H1 == 0] = np.nan 100 | ax.pcolormesh(X, Y, H1, cmap=cmap) 101 | ax.set_xlabel(r"$d_i$") 102 | ax.set_ylabel(r"$d_e$") 103 | ax.set_xlim(*xlim) 104 | ax.set_ylim(*ylim) 105 | if filename is not None: 106 | plt.savefig(filename, dpi=300, bbox_inches="tight") 107 | 108 | 109 | def plot_filtered_histogram( 110 | hist_filtered, 111 | hist, 112 | ax=None, 113 | filename=None, 114 | cmap="coolwarm", 115 | xlim=(0.5, 2.5), 116 | ylim=(0.5, 2.5), 117 | ): 118 | if ax is None: 119 | fig, ax = plt.subplots() 120 | fig.set_size_inches(4, 4) 121 | H1, xedges1, yedges1 = hist 122 | H2, xedges2, yedges2 = hist_filtered 123 | X1, Y1 = np.meshgrid(xedges1, yedges1) 124 | H1_binary = np.where(H1 > 0, 1, np.nan) 125 | H2[H2 == 0] = np.nan 126 | ax.pcolormesh(X1, Y1, H1_binary, cmap="Greys_r", alpha=0.15) 127 | ax.pcolormesh(X1, Y1, H2, cmap=cmap) 128 | ax.set_xlabel(r"$d_i$") 129 | ax.set_ylabel(r"$d_e$") 130 | ax.set_xlim(*xlim) 131 | ax.set_ylim(*ylim) 132 | 133 | if filename is not None: 134 | plt.savefig(filename, dpi=300, bbox_inches="tight") 135 | -------------------------------------------------------------------------------- /src/chmpy/crystal/powder.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | from chmpy.crystal.sfac import LAMBDA_Cu 5 | 6 | 7 | class PowderPattern: 8 | def __init__(self, two_theta, f2, **kwargs): 9 | self.two_theta = two_theta 10 | self.f2 = f2 11 | self.two_theta_range = kwargs.get("two_theta_range", (5, 50)) 12 | self.wavelength = kwargs.get("wavelength", LAMBDA_Cu) 13 | self.source = kwargs.get( 14 | "source", "unknown" if self.wavelength != LAMBDA_Cu else "Cu" 15 | ) 16 | self.bins = kwargs.get( 17 | "bins", (self.two_theta_range[1] - self.two_theta_range[0]) * 10 18 | ) 19 | self.bin_edges, self.bin_heights = np.histogram( 20 | self.two_theta, bins=self.bins, weights=self.f2, range=self.two_theta_range 21 | ) 22 | 23 | def plot(self, ax=None, **kwargs): 24 | if ax is None: 25 | ax = plt.gca() 26 | ax.hist( 27 | self.two_theta, bins=self.bins, weights=self.f2, range=self.two_theta_range 28 | ) 29 | ax.set_xlabel(kwargs.get("xlabel", r"2$\theta$")) 30 | ax.set_ylabel(kwargs.get("ylabel", r"Intensity")) 31 | ax.set_title( 32 | kwargs.get("title", f"Powder pattern in range {self.two_theta_range}") 33 | ) 34 | return ax 35 | 36 | def binned(self): 37 | return np.histogram( 38 | self.two_theta, bins=self.bins, weights=self.f2, range=self.two_theta_range 39 | ) 40 | -------------------------------------------------------------------------------- /src/chmpy/crystal/sfac/_sfac.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3, boundscheck=False, wraparound=False 2 | """ 3 | Atomic Form factors taken from the following sources: 4 | 5 | [1] D. Waasmaier & A. Kirfel, Acta Cryst. (1995). A51, 416-413 6 | [2] P. Rez & I. Grant, Acta Cryst. (1994), A50, 481-497 7 | 8 | Fit parameters of all atoms/ions (with the exception of O1-) 9 | from [1], and the tit for O1- based on the tabulated values 10 | of Table 2 from [2] 11 | 12 | """ 13 | 14 | import numpy as np 15 | import cython 16 | from os.path import join, dirname 17 | from libc.math cimport exp 18 | _DATA_DIR = dirname(__file__) 19 | _FORM_FACTOR_DATA = np.load(join(_DATA_DIR, "waaskirf.npz")) 20 | _FORM_FACTOR_KEYS = _FORM_FACTOR_DATA.f.keys 21 | _FORM_FACTOR_KEYS_LIST = _FORM_FACTOR_KEYS.tolist() 22 | _FORM_FACTOR_VALUES = _FORM_FACTOR_DATA.f.values 23 | 24 | cdef void fill_unique_plane_factors(const int[:, ::] hkl, const double[:] q, int[:] fac) noexcept nogil: 25 | cdef int i, j 26 | cdef int N 27 | cdef int nh, nk, nl 28 | cdef int hj, kj, lj 29 | cdef double qj, qi 30 | N = len(q) 31 | for i in range(N): 32 | if fac[i] < 1: 33 | continue 34 | qi = q[i] 35 | nh = - hkl[i, 0] 36 | nk = - hkl[i, 1] 37 | nl = - hkl[i, 2] 38 | for j in range(i + 1, N): 39 | qj = q[j] 40 | if (qj - qi) > 1e-7: 41 | continue 42 | hj = hkl[j, 0] 43 | kj = hkl[j, 1] 44 | lj = hkl[j, 2] 45 | if hj == nh and kj == nk and lj == nl: 46 | fac[i] += 1 47 | fac[j] = 0 48 | 49 | 50 | cpdef calculate_unique_plane_factors(hkl, q): 51 | factors = np.ones(q.shape, dtype=np.int32) 52 | cdef const int[:, ::] hklview = hkl 53 | cdef const double[:] qview = q 54 | cdef int[:] facview = factors 55 | fill_unique_plane_factors(hklview, qview, facview) 56 | return factors 57 | 58 | 59 | def get_form_factor_index(key): 60 | return _FORM_FACTOR_KEYS_LIST.index(key) 61 | 62 | cpdef scattering_factors(idx, sintl): 63 | cdef Aff form_factor = Aff(idx) 64 | result = np.empty(sintl.shape, np.float64) 65 | form_factor.calc(sintl, result) 66 | return result 67 | 68 | @cython.final 69 | cdef class Aff: 70 | cdef double a1 71 | cdef double b1 72 | cdef double a2 73 | cdef double b2 74 | cdef double a3 75 | cdef double b3 76 | cdef double a4 77 | cdef double b4 78 | cdef double a5 79 | cdef double b5 80 | cdef double c 81 | def __init__(self, idx): 82 | "order = a1 a2 a3 a4 a5 c b1 b2 b3 b4 b5" 83 | v = _FORM_FACTOR_VALUES[idx] 84 | self.a1 = v[0] 85 | self.a2 = v[1] 86 | self.a3 = v[2] 87 | self.a4 = v[3] 88 | self.a5 = v[4] 89 | self.c = v[5] 90 | self.b1 = -v[6] 91 | self.b2 = -v[7] 92 | self.b3 = -v[8] 93 | self.b4 = -v[9] 94 | self.b5 = -v[10] 95 | 96 | cdef void calc(self, const double[::1] s, double[::1] fac) nogil: 97 | cdef int i = 0 98 | cdef int N = s.shape[0] 99 | cdef double sintl2 = 0 100 | for i in range(N): 101 | sintl2 = s[i] 102 | fac[i] = ( 103 | self.a1 * exp(self.b1 * sintl2) + 104 | self.a2 * exp(self.b2 * sintl2) + 105 | self.a3 * exp(self.b3 * sintl2) + 106 | self.a4 * exp(self.b4 * sintl2) + 107 | self.a5 * exp(self.b5 * sintl2) + 108 | self.c 109 | ) 110 | 111 | -------------------------------------------------------------------------------- /src/chmpy/crystal/sfac/waaskirf.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/crystal/sfac/waaskirf.npz -------------------------------------------------------------------------------- /src/chmpy/descriptors/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .symmetry_function_ani1 import SymmetryFunctionsANI1 4 | 5 | __all__ = [ 6 | "selling_scalars", 7 | "SymmetryFunctionsANI1", 8 | ] 9 | 10 | 11 | def selling_scalars(unit_cell): 12 | a = unit_cell.direct[0, :] 13 | b = unit_cell.direct[1, :] 14 | c = unit_cell.direct[2, :] 15 | d = -np.sum(unit_cell.direct, axis=0) 16 | return np.asarray( 17 | ( 18 | np.vdot(b, c), 19 | np.vdot(a, c), 20 | np.vdot(a, b), 21 | np.vdot(a, d), 22 | np.vdot(b, d), 23 | np.vdot(c, d), 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /src/chmpy/exe/__init__.py: -------------------------------------------------------------------------------- 1 | from subprocess import PIPE, CalledProcessError, TimeoutExpired 2 | 3 | from .exe import AbstractExecutable, ReturnCodeError, run_subprocess 4 | from .gaussian import Gaussian 5 | from .gulp import Gulp 6 | from .raspa import Raspa 7 | from .tonto import Tonto 8 | from .xtb import Xtb 9 | 10 | __all__ = [ 11 | "AbstractExecutable", 12 | "CalledProcessError", 13 | "Gaussian", 14 | "Gulp", 15 | "PIPE", 16 | "Raspa", 17 | "ReturnCodeError", 18 | "TimeoutExpired", 19 | "Tonto", 20 | "Xtb", 21 | "run_subprocess", 22 | ] 23 | -------------------------------------------------------------------------------- /src/chmpy/exe/gaussian.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from chmpy.util.exe import which 5 | 6 | from .exe import AbstractExecutable, ReturnCodeError, run_subprocess 7 | 8 | LOG = logging.getLogger("gaussian") 9 | 10 | GAUSSIAN_EXEC = which("g09") 11 | FORMCHK_EXEC = which("formchk") 12 | 13 | 14 | class Gaussian(AbstractExecutable): 15 | _executable_location = GAUSSIAN_EXEC 16 | _JOB_FILE_FMT = "{}.gjf" 17 | _LOG_FILE_FMT = "{}.log" 18 | 19 | def __init__( 20 | self, 21 | input_file, 22 | name="job", 23 | run_formchk=None, 24 | working_directory=".", 25 | output_file=None, 26 | ): 27 | """ 28 | Parameters 29 | ---------- 30 | input_file : str 31 | string of gaussian input format 32 | output_file : str, optional 33 | output_file to store gaussian output in, 34 | by default will be returned as the result 35 | """ 36 | assert isinstance(input_file, str) 37 | 38 | self.name = name 39 | self.working_directory = working_directory 40 | self._fchk_filename = None 41 | self.input_file_contents = input_file 42 | self.output_file = str(self.log_file) 43 | if output_file: 44 | self.output_file = str(output_file) 45 | self.run_formchk = run_formchk 46 | if self.run_formchk: 47 | self._fchk_filename = self.run_formchk.replace(".chk", ".fchk") 48 | self._chk_filename = self.run_formchk 49 | self.log_contents = None 50 | self.fchk_contents = None 51 | 52 | @property 53 | def job_file(self): 54 | return Path(self.working_directory, self._JOB_FILE_FMT.format(self.name)) 55 | 56 | @property 57 | def log_file(self): 58 | return Path(self.working_directory, self._LOG_FILE_FMT.format(self.name)) 59 | 60 | @property 61 | def fchk_file(self): 62 | if self._fchk_filename is not None: 63 | return Path(self.working_directory, self._fchk_filename) 64 | 65 | @property 66 | def chk_file(self): 67 | if self._chk_filename is not None: 68 | return Path(self.working_directory, self._chk_filename) 69 | 70 | def write_inputs(self): 71 | self.job_file.write_text(self.input_file_contents) 72 | 73 | def resolve_dependencies(self): 74 | """Do whatever needs to be done before running 75 | the job (e.g. write input file etc.)""" 76 | self.write_inputs() 77 | 78 | def result(self): 79 | return self.log_contents 80 | 81 | def post_process(self): 82 | self.log_contents = self.log_file.read_text() 83 | 84 | output_file = Path(self.output_file) 85 | if not output_file.exists(): 86 | output_file.write_text(self.log_contents) 87 | 88 | if self.run_formchk: 89 | self._run_formchk() 90 | assert self.fchk_file.exists(), f"{self.fchk_file} not found" 91 | self.fchk_contents = self.fchk_file.read_text() 92 | 93 | def _run_formchk(self): 94 | """Run formchk, may throw exceptions""" 95 | cmd_list = [FORMCHK_EXEC, str(self.chk_file), str(self.fchk_file)] 96 | with open("/dev/null", "w+") as of: 97 | command = run_subprocess(cmd_list, stdout=of, timeout=self.timeout) 98 | result = command.returncode 99 | if result != 0: 100 | raise ReturnCodeError( 101 | "Command '{}' exited with return code {}".format( 102 | " ".join(cmd_list), result 103 | ) 104 | ) 105 | return result 106 | 107 | def run(self, *args, **kwargs): 108 | self._run_raw(self.job_file) 109 | -------------------------------------------------------------------------------- /src/chmpy/exe/gulp.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | from os import environ 4 | from pathlib import Path 5 | from tempfile import TemporaryFile 6 | 7 | from chmpy.util.exe import which 8 | 9 | from .exe import AbstractExecutable, ReturnCodeError 10 | 11 | GULP_EXEC = which("gulp") 12 | LOG = logging.getLogger("gulp") 13 | 14 | 15 | class Gulp(AbstractExecutable): 16 | _input_file = "gulp_job.gin" 17 | _output_file = "gulp_job.gout" 18 | _drv_file = "gulp_job.drv" 19 | _res_file = "gulp_job.res" 20 | _executable_location = GULP_EXEC 21 | _timeout = 10086400.0 22 | 23 | def __init__(self, input_contents, *args, working_directory=".", **kwargs): 24 | self._timeout = kwargs.get("timeout", self._timeout) 25 | self.name = kwargs.get("name", "gulp_job") 26 | self.solvent = kwargs.get("solvent", None) 27 | self.threads = kwargs.get("threads", 1) 28 | self.input_contents = input_contents 29 | self.output_contents = None 30 | self.restart_contents = None 31 | self.kwargs = kwargs.copy() 32 | self.working_directory = working_directory 33 | self.arg = Path(self.input_file).with_suffix("") 34 | LOG.debug("Initializing gulp calculation, timeout = %s", self.timeout) 35 | self.error_contents = None 36 | 37 | @property 38 | def input_file(self): 39 | return Path(self.working_directory, self._input_file) 40 | 41 | @property 42 | def output_file(self): 43 | return Path(self.working_directory, self._output_file) 44 | 45 | @property 46 | def drv_file(self): 47 | return Path(self.working_directory, self._drv_file) 48 | 49 | def resolve_dependencies(self): 50 | """Do whatever needs to be done before running 51 | the job (e.g. write input file etc.)""" 52 | LOG.debug("Writing GULP input file to %s", self.input_file) 53 | Path(self.input_file).write_text( 54 | self.input_contents + f"\noutput drv {self.drv_file}" 55 | ) 56 | 57 | def result(self): 58 | return self.output_contents 59 | 60 | def post_process(self): 61 | self.output_contents = Path(self.output_file).read_text() 62 | if Path(self.drv_file).exists(): 63 | self.drv_contents = Path(self.drv_file).read_text() 64 | else: 65 | self.drv_contents = "" 66 | 67 | def run(self, *args, **kwargs): 68 | LOG.debug("Running %s %s", self._executable_location, self.arg) 69 | try: 70 | with TemporaryFile() as tmp: 71 | env = copy.deepcopy(environ) 72 | env.update( 73 | { 74 | "OMP_NUM_THREADS": str(self.threads) + ",1", 75 | "OMP_MAX_ACTIVE_LEVELS": "1", 76 | "MKL_NUM_THREADS": str(self.threads), 77 | } 78 | ) 79 | self._run_raw(self.arg, stderr=tmp, env=env) 80 | tmp.seek(0) 81 | self.error_contents = tmp.read().decode("utf-8") 82 | except ReturnCodeError as e: 83 | from shutil import copytree 84 | 85 | from chmpy.util.path import list_directory 86 | 87 | LOG.error("GULP execution failed: %s", e) 88 | self.post_process() 89 | LOG.error("output: %s", self.output_contents) 90 | LOG.error("Directory contents\n%s", list_directory(self.working_directory)) 91 | copytree(self.working_directory, "failed_job") 92 | raise e 93 | -------------------------------------------------------------------------------- /src/chmpy/exe/tonto.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from chmpy.util.exe import which 5 | 6 | from .exe import AbstractExecutable 7 | 8 | LOG = logging.getLogger("tonto") 9 | 10 | TONTO_EXEC = which("tonto") 11 | 12 | 13 | class Tonto(AbstractExecutable): 14 | _executable_location = TONTO_EXEC 15 | _STDIN = "stdin" 16 | _STDOUT = "stdout" 17 | _STDERR = "stderr" 18 | 19 | def __init__( 20 | self, 21 | input_file, 22 | name="tonto_job", 23 | working_directory=".", 24 | output_file=None, 25 | extra_inputs=(), 26 | extra_outputs=(), 27 | ): 28 | """ 29 | Parameters 30 | ---------- 31 | input_file : str 32 | string of tonto input format 33 | output_file : str, optional 34 | output_file to store tonto output in, 35 | by default will be returned as the result 36 | """ 37 | assert isinstance(input_file, str) 38 | 39 | self.name = name 40 | self.working_directory = working_directory 41 | self.input_file_contents = input_file 42 | self.output_file = str(self.stdout_file) 43 | if output_file: 44 | self.output_file = str(output_file) 45 | self.stdin_contents = None 46 | self.stdout_contents = None 47 | self.stderr_contents = None 48 | self.extra_inputs = extra_inputs 49 | self.extra_outputs = extra_outputs 50 | self.basis_set_directory = "" 51 | 52 | @property 53 | def stdin_file(self): 54 | return Path(self.working_directory, self._STDIN) 55 | 56 | @property 57 | def stdout_file(self): 58 | return Path(self.working_directory, self._STDOUT) 59 | 60 | @property 61 | def stderr_file(self): 62 | return Path(self.working_directory, self._STDERR) 63 | 64 | def read_stderr(self): 65 | if self.stderr_file.exists(): 66 | return self.stderr_file.read_text() 67 | return "" 68 | 69 | def read_stdout(self): 70 | if self.stdout_file.exists(): 71 | return self.stdout_file.read_text() 72 | return "" 73 | 74 | def write_inputs(self): 75 | self.stdin_file.write_text(self.input_file_contents) 76 | 77 | def resolve_dependencies(self): 78 | """Do whatever needs to be done before running 79 | the job (e.g. write input file etc.)""" 80 | self.write_inputs() 81 | 82 | def result(self): 83 | return self.stdout_contents 84 | 85 | def post_process(self): 86 | self.stdout_contents = self.read_stdout() 87 | self.stderr_contents = self.read_stderr() 88 | 89 | if self.stdin_file.exists(): 90 | self.stdin_file.unlink() 91 | if self.stderr_file.exists(): 92 | self.stderr_file.unlink() 93 | if self.stdout_file.exists(): 94 | self.stdout_file.unlink() 95 | 96 | output_file = Path(self.output_file) 97 | if not output_file.exists(): 98 | output_file.write_text(self.stdout_contents) 99 | 100 | def run(self, *args, **kwargs): 101 | from copy import deepcopy 102 | from os import environ 103 | 104 | env = deepcopy(environ) 105 | env.update( 106 | { 107 | "TONTO_BASIS_SET_DIRECTORY": str(self.basis_set_directory), 108 | } 109 | ) 110 | self._run_raw() 111 | -------------------------------------------------------------------------------- /src/chmpy/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/ext/__init__.py -------------------------------------------------------------------------------- /src/chmpy/ext/charges.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | EEM_KAPPA = 0.529176 4 | EEM_PARAMETERS = { 5 | "H": (0.20606, 1.31942), 6 | "Li": (0.36237, 0.65932), 7 | "B": (0.36237, 0.65932), 8 | "C": (0.36237, 0.65932), 9 | "N": (0.49279, 0.69038), 10 | "O": (0.73013, 1.08856), 11 | "F": (0.72052, 1.45328), 12 | "Na": (0.36237, 0.65932), 13 | "Mg": (0.36237, 0.65932), 14 | "Si": (0.36237, 0.65932), 15 | "P": (0.36237, 0.65932), 16 | "S": (0.62020, 0.41280), 17 | "Cl": (0.36237, 0.65932), 18 | "K": (0.36237, 0.65932), 19 | "Ca": (0.36237, 0.65932), 20 | "Fe": (0.36237, 0.65932), 21 | "Cu": (0.36237, 0.65932), 22 | "Zn": (0.36237, 0.65932), 23 | "Br": (0.70052, 1.09108), 24 | "I": (0.68052, 0.61328), 25 | "*": (0.20606, 1.31942), 26 | } 27 | 28 | 29 | class EEM: 30 | "Class to handle calculation of electronegativity equilibration method charges" 31 | 32 | @staticmethod 33 | def calculate_charges(mol): 34 | """ 35 | Calculate the partial atomic charges based on the EEM method. 36 | 37 | Args: 38 | mol (Molecule): The molecule with atoms where partial charges are desired 39 | 40 | Returns: 41 | np.ndarray: the partial charges associated the atoms in `mol` 42 | """ 43 | A = [] 44 | B = [] 45 | for el in mol.elements: 46 | a, b = EEM_PARAMETERS.get(el.symbol, EEM_PARAMETERS["*"]) 47 | A.append(a) 48 | B.append(b) 49 | N = len(mol) 50 | M = np.zeros((N + 1, N + 1)) 51 | M[-1, :-1] = 1 52 | M[:-1, -1] = -1 53 | dists = mol.distance_matrix 54 | idx = np.triu_indices(N, k=1) 55 | M[idx] = EEM_KAPPA / dists[idx] 56 | idx = np.tril_indices(N, k=-1) 57 | M[idx] = EEM_KAPPA / dists[idx] 58 | np.fill_diagonal(M, B) 59 | M[N, N] = 0.0 60 | y = np.zeros(N + 1) 61 | y[:N] -= A 62 | y[N] = mol.charge 63 | return np.linalg.solve(M, y)[:N] 64 | -------------------------------------------------------------------------------- /src/chmpy/ext/cosmo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import namedtuple 3 | 4 | import numpy as np 5 | from scipy.spatial.distance import pdist 6 | 7 | from chmpy.ext.solvation_parameters import DIELECTRIC_CONSTANTS 8 | 9 | LOG = logging.getLogger(__name__) 10 | COSMOResult = namedtuple("COSMOResult", "qinit qmin total_energy") 11 | 12 | 13 | def surface_charge(charges, epsilon, x=0.5): 14 | return charges * (epsilon - 1) / (epsilon + x) 15 | 16 | 17 | def coulomb_matrix(points): 18 | N = points.shape[0] 19 | C = np.zeros((N, N)) 20 | np.fill_diagonal(C, 0.0) 21 | C[np.triu_indices(N, k=1)] = 1 / pdist(points) 22 | C += C.T 23 | return C 24 | 25 | 26 | def self_interaction_term(areas, k=1.0694): 27 | Sii = 3.8 / np.sqrt(areas) 28 | return Sii 29 | 30 | 31 | def minimize_cosmo_energy(points, areas, charges, **kwargs): 32 | from chmpy.util.unit import AU_TO_KJ_PER_MOL, BOHR_TO_ANGSTROM 33 | 34 | if kwargs.get("unit", "angstrom").lower() == "angstrom": 35 | points = points / BOHR_TO_ANGSTROM 36 | areas = areas / (BOHR_TO_ANGSTROM * BOHR_TO_ANGSTROM) 37 | 38 | diis_tolerance = kwargs.get("diis_tolerance", 1e-6) 39 | diis_start = 1 40 | convergence = kwargs.get("convergence", 1.0e-6) 41 | initial_charge_scale_factor = 0.0694 42 | 43 | solvent = kwargs.get("solvent", "water") 44 | epsilon = DIELECTRIC_CONSTANTS.get(solvent, 0.0) 45 | LOG.debug("Using dielectric constant of %.2f for solvent '%s'", epsilon, solvent) 46 | qinit = surface_charge(charges, epsilon) 47 | C = coulomb_matrix(points) 48 | Sii = self_interaction_term(areas) 49 | d0 = 1.0 / Sii 50 | 51 | qprev = initial_charge_scale_factor * qinit * d0 52 | prev_q = [] 53 | prev_dq = [] 54 | 55 | N = qinit.shape[0] 56 | 57 | qcur = np.empty_like(qprev) 58 | LOG.debug("{:>3s} {:>14s} {:>9s} {:>16s}".format("N", "Energy", "Q", "Error")) 59 | 60 | for k in range(1, kwargs.get("max_iter", 50)): 61 | vpot = np.sum(qprev * C, axis=1) 62 | qcur = (qinit - vpot) * d0 63 | dq = qcur - qprev 64 | 65 | if k >= diis_start: 66 | prev_q.append(qcur) 67 | prev_dq.append(dq) 68 | 69 | ndiis = len(prev_dq) 70 | if ndiis > 1: 71 | rhs = np.zeros(ndiis + 1) 72 | rhs[ndiis] = -1 73 | 74 | # setup system of linear equations 75 | B = np.zeros((ndiis + 1, ndiis + 1)) 76 | B[:, ndiis] = -1 77 | B = B.T 78 | B[:, ndiis] = -1 79 | B[ndiis, ndiis] = 0 80 | 81 | for i, qi in enumerate(prev_dq): 82 | for j, qj in enumerate(prev_dq): 83 | B[i, j] = qi.dot(qj) 84 | 85 | c = np.linalg.solve(B, rhs)[:ndiis] 86 | 87 | qcur = 0.0 88 | for i in range(ndiis): 89 | qcur += c[i] * prev_q[i] 90 | 91 | sel = np.where(np.abs(c) < diis_tolerance)[0] 92 | for i in reversed(sel): 93 | prev_q.pop(i) 94 | prev_dq.pop(i) 95 | 96 | rms_err = np.sqrt(dq.dot(dq) / N) 97 | e_q = -0.5 * qinit.dot(qcur) 98 | LOG.debug(f"{k:3d} {e_q:14.8f} {qcur.sum():9.5f} {rms_err:16.9f}") 99 | if rms_err < convergence: 100 | break 101 | qprev[:] = qcur[:] 102 | 103 | G = -0.5 * qinit.dot(qcur) 104 | LOG.debug("Energy: %16.9f kJ/mol", G * AU_TO_KJ_PER_MOL) 105 | return COSMOResult(qinit, qcur, G) 106 | -------------------------------------------------------------------------------- /src/chmpy/ext/crystal.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from spglib import get_symmetry_dataset, standardize_cell 5 | 6 | from chmpy import Element 7 | from chmpy.crystal import AsymmetricUnit, Crystal, UnitCell 8 | from chmpy.crystal.space_group import SpaceGroup 9 | 10 | LOG = logging.getLogger(__name__) 11 | 12 | 13 | def standardize_crystal(crystal, method="spglib", **kwargs): 14 | if method != "spglib": 15 | raise NotImplementedError("Only spglib is currently supported") 16 | 17 | lattice = crystal.unit_cell.direct 18 | uc_dict = crystal.unit_cell_atoms() 19 | positions = uc_dict["frac_pos"] 20 | elements = uc_dict["element"] 21 | asym_atoms = uc_dict["asym_atom"] 22 | asym_labels = uc_dict["label"] 23 | cell = lattice, positions, elements 24 | 25 | reduced_cell = standardize_cell(cell, **kwargs) 26 | 27 | if reduced_cell is None: 28 | LOG.warn("Could not find reduced cell for crystal %s", crystal) 29 | return None 30 | dataset = get_symmetry_dataset(reduced_cell) 31 | asym_idx = np.unique(dataset["equivalent_atoms"]) 32 | asym_idx = asym_idx[np.argsort(asym_atoms[asym_idx])] 33 | sg = SpaceGroup(dataset["number"], choice=dataset["choice"]) 34 | 35 | reduced_lattice, positions, elements = reduced_cell 36 | unit_cell = UnitCell(reduced_lattice) 37 | asym = AsymmetricUnit( 38 | [Element[x] for x in elements[asym_idx]], 39 | positions[asym_idx], 40 | labels=asym_labels[asym_idx], 41 | ) 42 | return Crystal(unit_cell, sg, asym) 43 | 44 | 45 | def detect_symmetry(crystal, method="spglib", **kwargs): 46 | if method != "spglib": 47 | raise NotImplementedError("Only spglib is currently supported") 48 | 49 | lattice = crystal.unit_cell.direct 50 | uc_dict = crystal.unit_cell_atoms() 51 | positions = uc_dict["frac_pos"] 52 | elements = uc_dict["element"] 53 | asym_atoms = uc_dict["asym_atom"] 54 | cell = lattice, positions, elements 55 | dataset = get_symmetry_dataset(cell, **kwargs) 56 | if dataset["number"] == crystal.space_group.international_tables_number: 57 | LOG.warn("Could not find additional symmetry for crystal %s", crystal) 58 | return None 59 | asym_idx = np.unique(dataset["equivalent_atoms"]) 60 | asym_idx = asym_idx[np.argsort(asym_atoms[asym_idx])] 61 | sg = SpaceGroup(dataset["number"], choice=dataset["choice"]) 62 | asym = AsymmetricUnit( 63 | [Element[x] for x in dataset["std_types"][asym_idx]], 64 | dataset["std_positions"][asym_idx], 65 | ) 66 | unit_cell = UnitCell(dataset["std_lattice"]) 67 | return Crystal(unit_cell, sg, asym) 68 | -------------------------------------------------------------------------------- /src/chmpy/ext/excitations.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | LOG = logging.getLogger(__name__) 6 | 7 | _FAC = 1.3062974e8 8 | 9 | 10 | def add_gaussian_curve_contribution(x, band, strength, std): 11 | std_i = 1 / std 12 | x_i = 1 / x 13 | band_i = 1 / band 14 | exponent = (x_i - band_i) / std_i 15 | return _FAC * (strength / (1e7 * std_i)) * np.exp(-exponent * exponent) 16 | 17 | 18 | def add_lorentz_curve_contribution(x, band, strength, std, gamma): 19 | std_i = 1 / std 20 | x_band = x - band 21 | x_band2 = x_band * x_band 22 | gamma2 = gamma * gamma 23 | return _FAC * (strength / (1e7 * std_i)) * (gamma2 / (x_band2 + gamma2)) 24 | 25 | 26 | def plot_spectra( 27 | energies, 28 | osc, 29 | bounds=(1, 1500), 30 | bins=1000, 31 | std=12398.4, 32 | kind="gaussian", 33 | gamma=12.5, 34 | label=None, 35 | **kwargs, 36 | ): 37 | """Plot the (UV-Vis) spectra. 38 | 39 | Args: 40 | energies (np.ndarray): excitation energies/bands in nm. 41 | osc (np.ndarray): oscillator strengths (dimensionless). 42 | 43 | """ 44 | import matplotlib.pyplot as plt 45 | 46 | x = np.linspace(bounds[0], bounds[1], bins) 47 | total = 0 48 | for e, f in zip(energies, osc, strict=False): 49 | if kind == "gaussian": 50 | peak = add_gaussian_curve_contribution(x, e, f, std) 51 | else: 52 | peak = add_lorentz_curve_contribution(x, e, f, std, gamma) 53 | total += peak 54 | 55 | ax = plt.gca() 56 | total = total / np.max(total) 57 | ax.plot(x, total, label=label, **kwargs) 58 | ax.set_xlabel(r"$\lambda$ (nm)") 59 | ax.set_ylim(0, 1.1) 60 | ax.set_ylabel(r"Intensity") 61 | return ax 62 | -------------------------------------------------------------------------------- /src/chmpy/ext/traj.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from chmpy.crystal import AsymmetricUnit, Crystal, SpaceGroup 4 | from chmpy.fmt.xyz_file import parse_traj_file 5 | 6 | 7 | def to_xyz_string(elements, positions, comment=""): 8 | lines = [f"{len(elements)}", comment] 9 | for el, (x, y, z) in zip(elements, positions, strict=False): 10 | lines.append(f"{el} {x: 20.12f} {y: 20.12f} {z: 20.12f}") 11 | return "\n".join(lines) 12 | 13 | 14 | def expand_periodic_images(cell, filename, dest=None, supercell=(1, 1, 1)): 15 | frames = parse_traj_file(filename) 16 | sg = SpaceGroup(1) 17 | xyz_strings = [] 18 | for elements, comment, positions in frames: 19 | asym = AsymmetricUnit(elements, cell.to_fractional(positions)) 20 | c = Crystal(cell, sg, asym).as_P1_supercell(supercell) 21 | pos = c.to_cartesian(c.asymmetric_unit.positions) 22 | el = c.asymmetric_unit.elements 23 | xyz_strings.append(to_xyz_string(el, pos, comment=comment)) 24 | if dest is not None: 25 | Path(dest).write_text("\n".join(xyz_strings)) 26 | else: 27 | return "\n".join(xyz_strings) 28 | -------------------------------------------------------------------------------- /src/chmpy/ext/vasp.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | import numpy as np 4 | 5 | from chmpy import Element 6 | 7 | 8 | def kpoints_string(): 9 | return "" 10 | 11 | 12 | def incar_string(): 13 | return "" 14 | 15 | 16 | def poscar_string(crystal, name="vasp_input"): 17 | uc = crystal.unit_cell_atoms(tolerance=1e-2) 18 | elements = uc["element"] 19 | pos = uc["frac_pos"] 20 | ordering = np.argsort(elements) 21 | coord = pos[ordering] 22 | elements = elements[ordering] 23 | element_counts = Counter(elements) 24 | direct = "\n".join( 25 | f"{x:12.8f} {y:12.8f} {z:12.8f}" for x, y, z in crystal.unit_cell.direct 26 | ) 27 | els = " ".join(f"{Element[x].symbol:>3s}" for x in element_counts.keys()) 28 | counts = " ".join(f"{x:>3d}" for x in element_counts.values()) 29 | coords = "\n".join(f"{x:12.8f} {y:12.8f} {z:12.8f}" for x, y, z in coord) 30 | return f"{name}\n1.0\n{direct}\n{els}\n{counts}\nDirect\n{coords}" 31 | 32 | 33 | def generate_vasp_inputs(crystal, dest="."): 34 | from pathlib import Path 35 | 36 | dest = Path(dest) 37 | if not dest.exists(): 38 | dest.mkdir() 39 | Path(dest, "POSCAR").write_text(poscar_string(crystal, name=dest.name)) 40 | Path(dest, "INCAR").write_text(incar_string()) 41 | Path(dest, "KPOINTS").write_text(kpoints_string()) 42 | 43 | 44 | def load_vasprun(filename): 45 | import xml.etree.ElementTree as ET 46 | 47 | from chmpy.crystal import AsymmetricUnit, Crystal, SpaceGroup, UnitCell 48 | 49 | xml = ET.parse(filename) 50 | root = xml.getroot() 51 | structures = {} 52 | atominfo = root.find("atominfo") 53 | elements = [] 54 | for child in atominfo.findall("array"): 55 | if child.get("name") == "atoms": 56 | atoms = child.find("set") 57 | for atom in atoms.findall("rc"): 58 | elements.append(Element[atom.find("c").text]) 59 | 60 | for structure in root.findall("structure"): 61 | name = structure.get("name") 62 | crystal = structure.find("crystal") 63 | positions = structure.find("varray") 64 | direct = None 65 | for child in crystal: 66 | if child.get("name") == "basis": 67 | basis = [] 68 | for row in child: 69 | basis.append([float(x) for x in row.text.split()]) 70 | direct = np.array(basis) 71 | pos = [] 72 | for row in positions: 73 | pos.append([float(x) for x in row.text.split()]) 74 | pos = np.array(pos) 75 | structures[name] = Crystal( 76 | UnitCell(direct), SpaceGroup(1), AsymmetricUnit(elements, pos) 77 | ) 78 | return structures 79 | -------------------------------------------------------------------------------- /src/chmpy/fmt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Format handling utilities for various file formats. 3 | """ 4 | 5 | from . import ( 6 | ascii, 7 | cif, 8 | crystal17, 9 | cube, 10 | fchk, 11 | gaussian_log, 12 | gen, 13 | gmf, 14 | grd, 15 | gulp, 16 | mol2, 17 | nwchem, 18 | pdb, 19 | raspa, 20 | sdf, 21 | shelx, 22 | smiles, 23 | tmol, 24 | vasp, 25 | xyz_file, 26 | ) 27 | -------------------------------------------------------------------------------- /src/chmpy/fmt/ascii.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from io import StringIO 3 | from pathlib import Path 4 | 5 | import numpy as np 6 | 7 | LOG = logging.getLogger(__name__) 8 | 9 | 10 | class PhonopyAscii: 11 | def __init__(self, filename=None): 12 | if filename is not None: 13 | self._parse_file(filename) 14 | 15 | def _parse_buf(self, buf): 16 | from chmpy import Element 17 | 18 | lines = buf.read().splitlines() 19 | latt = np.zeros((3, 3)) 20 | a, b1, b2 = (float(x) for x in lines[1].split()) 21 | c1, c2, c3 = (float(x) for x in lines[2].split()) 22 | latt[0, 0] = a 23 | latt[1, 0:2] = b1, b2 24 | latt[2, :] = c1, c2, c3 25 | LOG.debug("Read lattice: %s", latt) 26 | i = 3 27 | positions = [] 28 | elements = [] 29 | line = lines[i] 30 | while not line.startswith("#"): 31 | x, y, z, el = line.split() 32 | positions.append((float(x), float(y), float(z))) 33 | elements.append(el) 34 | i += 1 35 | line = lines[i] 36 | positions = np.array(positions) 37 | elements = [Element[x] for x in elements] 38 | metadata = "".join(lines[i:]).replace(" \\#", "").replace(";", ",") 39 | for mode in metadata.split("#metaData: qpt=[")[1:]: 40 | arr = np.fromstring(mode.rstrip("]"), sep=",") 41 | qpoint = arr[:3] 42 | arr[3] ** 2 43 | arr = arr[4:].reshape((-1, 6)) 44 | self.displacements = arr 45 | self.elements = elements 46 | self.positions = positions 47 | self.qpoints = np.repeat(qpoint[np.newaxis, :], len(elements), axis=0) 48 | 49 | @classmethod 50 | def from_string(cls, string): 51 | cube = cls() 52 | cube._parse_buf(StringIO(string)) 53 | return cube 54 | 55 | def _parse_file(self, filename): 56 | with Path(filename).open() as f: 57 | self._parse_buf(f) 58 | -------------------------------------------------------------------------------- /src/chmpy/fmt/crystal17.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | from chmpy import Element 7 | from chmpy.crystal import SymmetryOperation 8 | from chmpy.templates import load_template 9 | 10 | LOG = logging.getLogger(__name__) 11 | CRYSTAL17_TEMPLATE = load_template("crystal17") 12 | 13 | 14 | def to_crystal17_input(crystal, **kwargs): 15 | space_group = crystal.space_group.international_tables_number 16 | params = ( 17 | crystal.uc.parameters 18 | if crystal.space_group.lattice_type == "triclinic" 19 | else crystal.uc.unique_parameters_deg 20 | ) 21 | method = kwargs.get("method", "hf-3c") 22 | if method == "hf-3c": 23 | method = "HF3C\nRESCALES8\n0.70" 24 | kwargs["basis_set"] = "MINIX" 25 | else: 26 | method = "DFT\n" + method.upper() 27 | parameters = { 28 | "basis_set_keywords": kwargs.get("basis_set_keywords", {}), 29 | "shrink_factors": kwargs.get("shrink_factors", (4, 4)), 30 | "iflag": kwargs.get("iflag", 0), 31 | "ifhr": 1 if crystal.space_group.lattice_type == "rhombohedral" else 0, 32 | "ifso": 0, # change of origin 33 | "space_group": space_group, 34 | "cell_parameters": " ".join(f"{x:10.6f}" for x in params), 35 | "basis_set": kwargs.get("basis_set", "cc-pVDZ"), 36 | } 37 | return CRYSTAL17_TEMPLATE.render( 38 | title=crystal.titl, 39 | method=method, 40 | natoms=len(crystal.asym), 41 | atoms=zip(crystal.asym.positions, crystal.asym.elements, strict=False), 42 | **parameters, 43 | ) 44 | 45 | 46 | def load_crystal17_output_string(string): 47 | total_energy_line = "" 48 | for line in string.splitlines(): 49 | if "TOTAL ENERGY" in line: 50 | total_energy_line = line 51 | energy = float(total_energy_line.split(")")[-1].split()[0]) 52 | return energy 53 | 54 | 55 | def load_crystal17_output_file(filename): 56 | return load_crystal17_output_string(Path(filename).read_text()) 57 | 58 | 59 | def load_crystal17_geometry_string(string): 60 | lines = string.splitlines() 61 | tokens = lines[0].split() 62 | data = { 63 | "dimensionality": int(tokens[0]), 64 | "centering": int(tokens[1]), 65 | "crystal_type": int(tokens[2]), 66 | "energy": float(tokens[4]), 67 | "direct": np.fromstring(" ".join(lines[1:4]), sep=" ").reshape(3, 3), 68 | "symmetry_operations": [], 69 | "elements": [], 70 | "xyz": [], 71 | } 72 | nsymops = int(lines[4]) 73 | 74 | D = data["direct"] 75 | I = np.linalg.inv(D) 76 | 77 | for i in range(5, 5 + nsymops * 4, 4): 78 | rotation = np.fromstring(" ".join(lines[i : i + 3]), sep=" ").reshape(3, 3) 79 | translation = np.fromstring(lines[i + 3], sep=" ") 80 | rot_frac = np.dot(I.T, np.dot(rotation, D.T)).T.round(7).astype(int) 81 | t_frac = np.dot(translation, I) 82 | data["symmetry_operations"].append(SymmetryOperation(rot_frac, t_frac)) 83 | l = i + 4 84 | natoms = int(lines[l]) 85 | l += 1 86 | for i in range(l, l + natoms): 87 | tokens = lines[i].split() 88 | data["elements"].append(Element[tokens[0]]) 89 | data["xyz"].append(np.array([float(x) for x in tokens[1:]])) 90 | data["xyz"] = np.vstack(data["xyz"]) 91 | return data 92 | 93 | 94 | def load_crystal17_geometry_file(filename): 95 | return load_crystal17_geometry_string(Path(filename).read_text()) 96 | -------------------------------------------------------------------------------- /src/chmpy/fmt/cube.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | from chmpy.util.unit import units 7 | 8 | 9 | class CubeData: 10 | _filename = "unknown cube file" 11 | 12 | def __init__(self, filename=None): 13 | self._interpolator = None 14 | if filename is not None: 15 | self._parse_cube_file(filename) 16 | 17 | def _parse_title_lines(self, l1, l2): 18 | self.title, self.subtitle = l1.strip(), l2.strip 19 | 20 | def _parse_atom_lines(self, lines): 21 | elements = [] 22 | masses = [] 23 | positions = [] 24 | for line in lines: 25 | tokens = line.split() 26 | elements.append(int(tokens[0])) 27 | masses.append(float(tokens[1])) 28 | positions.append(np.fromstring(" ".join(tokens[2:]), sep=" ")) 29 | self.elements = np.array(elements) 30 | self.masses = np.array(masses) 31 | self._positions_bohr = np.vstack(positions) 32 | self.positions = units.angstrom(self._positions_bohr) 33 | 34 | def _parse_cube_buf(self, buf): 35 | self._parse_title_lines(buf.readline(), buf.readline()) 36 | tokens = buf.readline().split() 37 | self.natom = int(tokens[0]) 38 | self._volume_origin_bohr = np.fromstring(" ".join(tokens[1:]), sep=" ") 39 | self.volume_origin = units.angstrom(self._volume_origin_bohr) 40 | for ax in ("x", "y", "z"): 41 | tokens = buf.readline().split() 42 | setattr(self, f"n{ax}", int(tokens[0])) 43 | setattr( 44 | self, f"_{ax}_basis_bohr", np.fromstring(" ".join(tokens[1:]), sep=" ") 45 | ) 46 | setattr( 47 | self, f"{ax}_basis", units.angstrom(getattr(self, f"_{ax}_basis_bohr")) 48 | ) 49 | 50 | self.basis = np.vstack((self.x_basis, self.y_basis, self.z_basis)) 51 | self._parse_atom_lines(buf.readline() for i in range(self.natom)) 52 | self.data = np.fromstring(buf.read(), sep=" ") 53 | 54 | @classmethod 55 | def from_string(cls, string): 56 | cube = cls() 57 | cube._parse_cube_buf(StringIO(string)) 58 | return cube 59 | 60 | def _parse_cube_file(self, filename): 61 | self._filename = filename 62 | with Path(filename).open() as f: 63 | self._parse_cube_buf(f) 64 | 65 | def shift_origin_to(self, new_origin): 66 | shift = new_origin - self.volume_origin 67 | self.volume_origin = new_origin 68 | self.positions -= shift 69 | 70 | @property 71 | def xyz(self): 72 | x, y, z = np.mgrid[0 : self.nx, 0 : self.ny, 0 : self.nz] 73 | return np.c_[x.ravel(), y.ravel(), z.ravel()] @ self.basis + self.volume_origin 74 | 75 | def molecule(self): 76 | from chmpy import Molecule 77 | 78 | return Molecule.from_arrays( 79 | self.elements, self.positions, source_file=self._filename 80 | ) 81 | 82 | def interpolator(self): 83 | if self._interpolator is None: 84 | from sklearn.neighbors import KNeighborsRegressor 85 | 86 | self._interpolator = KNeighborsRegressor(n_neighbors=5, weights="distance") 87 | self._interpolator.fit(self.xyz, self.data) 88 | return self._interpolator 89 | 90 | def isosurface(self, isovalue=0.0): 91 | from trimesh import Trimesh 92 | 93 | from chmpy.mc import marching_cubes 94 | 95 | vol = self.data.reshape((self.nx, self.ny, self.nz)) 96 | seps = ( 97 | np.linalg.norm(self.x_basis), 98 | np.linalg.norm(self.y_basis), 99 | np.linalg.norm(self.z_basis), 100 | ) 101 | verts, faces, normals, _ = marching_cubes(vol, level=isovalue, spacing=seps) 102 | return Trimesh(vertices=verts, faces=faces, normals=normals) 103 | -------------------------------------------------------------------------------- /src/chmpy/fmt/fchk.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | 4 | class FchkFile: 5 | UNIT_CONVERSIONS = {"kj/mol": 2625.499638, "hartree": 1.0} 6 | CONVERSIONS = {"R": float, "I": int, "C": str} 7 | 8 | def __init__(self, file_content, parse=False): 9 | self.filename = "string" 10 | self.file_content = file_content 11 | if parse: 12 | self._parse() 13 | self._buf = StringIO(file_content) 14 | 15 | def _parse(self): 16 | contents = {} 17 | with StringIO(self.file_content) as f: 18 | contents["header"] = (f.readline(), f.readline()) 19 | line = f.readline() 20 | 21 | while line: 22 | name = line[:43].strip() 23 | kind = line[43] 24 | if line[47:49] == "N=": 25 | int(line[49:].strip()) 26 | value = [] 27 | line = f.readline() 28 | valid = True 29 | while valid: 30 | tokens = line.strip().split() 31 | convert = self.CONVERSIONS[kind] 32 | value += [convert(x) for x in tokens] 33 | line = f.readline() 34 | valid = line and line[0] == " " 35 | else: 36 | value = self.CONVERSIONS[kind](line[48:].strip()) 37 | line = f.readline() 38 | contents[name] = value 39 | self.contents = contents 40 | 41 | @classmethod 42 | def from_file(cls, filename, parse=False): 43 | from pathlib import Path 44 | 45 | contents = Path(filename).read_text() 46 | fchk = cls(contents) 47 | fchk.filename = filename 48 | if parse: 49 | fchk._parse() 50 | return fchk 51 | 52 | def __getitem__(self, key): 53 | return self.contents[key] 54 | 55 | def _parse_energy_only(self): 56 | with open(self.filename) as f: 57 | for line in f: 58 | if line.startswith("Total Energy"): 59 | return float(line[49:].strip()) 60 | 61 | @classmethod 62 | def scf_energy(cls, fname, units="hartree"): 63 | fchk = cls(fname, parse=False) 64 | return cls.UNIT_CONVERSIONS[units] * fchk._parse_energy_only() 65 | -------------------------------------------------------------------------------- /src/chmpy/fmt/gen.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | from chmpy.core.element import Element 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | 11 | def parse_gen_string(contents, filename=None): 12 | """Convert provided xmol .xyz file contents into an array of 13 | atomic numbers and cartesian positions 14 | 15 | Parameters 16 | ---------- 17 | contents: str 18 | text contents of the .xyz file to read 19 | 20 | Returns 21 | ------- 22 | tuple of :obj:`np.ndarray` 23 | List[Element], (N, 3) positions, (4, 3) lattice vectors, bool (if fractional) 24 | read from the given file 25 | """ 26 | lines = contents.splitlines() 27 | natom_str, kind = lines[0].split() 28 | natom = int(natom_str) 29 | kind = kind.strip() 30 | LOG.debug("Expecting %d atoms %s", natom, "in " + filename if filename else "") 31 | elements_map = [Element[x.strip()] for x in lines[1].split()] 32 | 33 | arr = [[float(x) for x in line.split()] for line in lines[natom + 2 : natom + 6]] 34 | elements = [] 35 | positions = [] 36 | for line in lines[2 : natom + 2]: 37 | if not line.strip(): 38 | break 39 | tokens = line.strip().split() 40 | xyz = tuple(float(x) for x in tokens[2:5]) 41 | positions.append(xyz) 42 | el = elements_map[int(tokens[1]) - 1] 43 | elements.append(el) 44 | LOG.debug( 45 | "Found %d atoms lines in %s", 46 | len(elements), 47 | "in " + filename if filename else "", 48 | ) 49 | return elements, np.asarray(positions), np.asarray(arr), kind == "F" 50 | 51 | 52 | def parse_gen_file(filename): 53 | """Convert a provided DFTB+ .gen file into an array of 54 | atomic numbers, positions and 55 | 56 | Parameters 57 | ---------- 58 | filename: str 59 | path to the .xyz file to read 60 | 61 | Returns 62 | ------- 63 | tuple of :obj:`np.ndarray` 64 | List[Element], (N, 3) positions, (4, 3) lattice vectors, bool (if fractional) 65 | read from the given file 66 | """ 67 | path = Path(filename) 68 | return parse_gen_string(path.read_text(), filename=str(path.absolute())) 69 | -------------------------------------------------------------------------------- /src/chmpy/fmt/gmf.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | 7 | @dataclass 8 | class GMF: 9 | hkl: np.ndarray 10 | cuts: np.ndarray 11 | energies: np.ndarray 12 | 13 | @classmethod 14 | def from_file(cls, filename): 15 | lines = Path(filename).read_text().splitlines() 16 | start_line = 0 17 | for i, line in enumerate(lines): 18 | start_line = i 19 | if not line: 20 | continue 21 | if "miller: " in line: 22 | break 23 | 24 | hkls = [] 25 | energies = [] 26 | cuts = [] 27 | for j in range(start_line, len(lines), 2): 28 | merged = lines[j] + lines[j + 1] 29 | tokens = merged.split() 30 | hkls.append(tuple(int(x) for x in tokens[1:4])) 31 | energies.append(float(tokens[7])) 32 | cuts.append(float(tokens[4])) 33 | 34 | return cls(np.array(hkls), np.array(energies), np.array(cuts)) 35 | -------------------------------------------------------------------------------- /src/chmpy/fmt/grd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def parse_grd_file(filename): 5 | contents = {} 6 | 7 | with open(filename) as f: 8 | contents["header"] = [f.readline().strip(), f.readline().strip()] 9 | f.readline() 10 | f.readline() 11 | contents["npts"] = tuple(int(x) for x in f.readline().split()) 12 | contents["origin"] = tuple(float(x) for x in f.readline().split()) 13 | contents["dimensions"] = tuple(float(x) for x in f.readline().split()) 14 | f.readline() 15 | nobjects = int(f.readline()) 16 | objects = [] 17 | for _i in range(nobjects): 18 | objects.append(f.readline()) 19 | f.readline() 20 | nconnections = int(f.readline()) 21 | connections = [] 22 | for _i in range(nconnections): 23 | connections.append(f.readline()) 24 | f.readline() 25 | contents["data"] = np.loadtxt(f) 26 | return contents 27 | -------------------------------------------------------------------------------- /src/chmpy/fmt/gulp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from collections import namedtuple 4 | 5 | from chmpy.templates import load_template 6 | 7 | LOG = logging.getLogger(__name__) 8 | GULP_TEMPLATE = load_template("gulp") 9 | NUMBER_REGEX = re.compile( 10 | r"([-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?)\s*([\w/\^\s]+)?" 11 | ) 12 | 13 | Cell = namedtuple("Cell", "a b c alpha beta gamma") 14 | 15 | 16 | def parse_value(string, with_units=False): 17 | """parse a value from a GULP output file to its appropriate type 18 | e.g. int, float, str etc. Will handle units with a space. 19 | 20 | Parameters 21 | ---------- 22 | string: str 23 | the string containing the value to parse 24 | 25 | with_uncertainty: bool, optional 26 | return a tuple including uncertainty if a numeric type is expected 27 | 28 | Returns 29 | ------- 30 | value 31 | the value coerced into the appropriate type 32 | 33 | >>> parse_value("2.3 kj/mol", with_units=True) 34 | (2.3, 'kj/mol') 35 | >>> parse_value("5 kgm^2", with_units=True) 36 | (5, 'kgm^2') 37 | >>> parse_value("string help") 38 | 'string help' 39 | >>> parse_value("3.1415") * 4 40 | 12.566 41 | """ 42 | match = NUMBER_REGEX.match(string) 43 | try: 44 | if match and match: 45 | groups = match.groups() 46 | number = groups[0] 47 | number = float(number) 48 | if number.is_integer(): 49 | number = int(number) 50 | if with_units and len(groups) > 1: 51 | return number, groups[1] 52 | return number 53 | else: 54 | s = string.strip() 55 | return s 56 | except Exception as e: 57 | print(e) 58 | return string 59 | 60 | 61 | def crystal_to_gulp_input(crystal, keywords=None, additional_keywords=None): 62 | if additional_keywords is None: 63 | additional_keywords = {} 64 | if keywords is None: 65 | keywords = [] 66 | pos = crystal.asymmetric_unit.positions 67 | el = crystal.asymmetric_unit.elements 68 | return GULP_TEMPLATE.render( 69 | keywords=keywords, 70 | frac=True, 71 | cell=" ".join(f"{x:10.6f}" for x in crystal.unit_cell.parameters), 72 | atoms=zip(el, pos, strict=False), 73 | spacegroup=crystal.space_group.crystal17_spacegroup_symbol(), 74 | additional_keywords=additional_keywords, 75 | ) 76 | 77 | 78 | def molecule_to_gulp_input(molecule, keywords=None, additional_keywords=None): 79 | if additional_keywords is None: 80 | additional_keywords = {} 81 | if keywords is None: 82 | keywords = [] 83 | pos = molecule.positions 84 | el = molecule.elements 85 | return GULP_TEMPLATE.render( 86 | keywords=keywords, 87 | frac=False, 88 | atoms=zip(el, pos, strict=False), 89 | additional_keywords=additional_keywords, 90 | ) 91 | 92 | 93 | def parse_single_line(line): 94 | toks = re.split(r"\s*[=:]\s*", line) 95 | if toks is not None and len(toks) == 2: 96 | return toks[0].strip(), toks[1].strip() 97 | return None 98 | 99 | 100 | def parse_gulp_output(contents): 101 | lines = contents.splitlines() 102 | lines_iter = iter(lines) 103 | outputs = {} 104 | for line in lines_iter: 105 | result = parse_single_line(line) 106 | if result is not None: 107 | k, v = result 108 | outputs[k] = v 109 | return outputs 110 | -------------------------------------------------------------------------------- /src/chmpy/fmt/mol2.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import defaultdict 3 | from pathlib import Path 4 | 5 | LOG = logging.getLogger(__name__) 6 | 7 | # Might be useful one day, currently mostly ignored 8 | _ATOM_TYPES = { 9 | "C.3": "carbon sp3", 10 | "C.2": "carbon sp2", 11 | "C.1": "carbon sp", 12 | "C.ar": "carbon aromatic", 13 | "C.cat": "cabocation (C+) used only in a guadinium group", 14 | "N.3": "nitrogen sp3", 15 | "N.2": "nitrogen sp2", 16 | "N.1": "nitrogen sp", 17 | "N.ar": "nitrogen aromatic", 18 | "N.am": "nitrogen amide", 19 | "N.pl3": "nitrogen trigonal planar", 20 | "N.4": "nitrogen sp3 positively charged", 21 | "O.3": "oxygen sp3", 22 | "O.2": "oxygen sp2", 23 | "O.co2": "oxygen in carboxylate and phosphate groups", 24 | "O.spc": "oxygen in Single Point Charge (SPC) water model", 25 | "O.t3p": "oxygen in Transferable Intermolecular Potential (TIP3P) water model", 26 | "S.3": "sulfur sp3", 27 | "S.2": "sulfur sp2", 28 | "S.O": "sulfoxide sulfur", 29 | "S.O2/S.o2": "sulfone sulfur", 30 | "P.3": "phosphorous sp3", 31 | "F": "fluorine", 32 | "H": "hydrogen", 33 | "H.spc": "hydrogen in Single Point Charge (SPC) water model", 34 | "H.t3p": "hydrogen in Transferable Intermolecular Potential (TIP3P) water model", 35 | "LP": "lone pair", 36 | "Du": "dummy atom", 37 | "Du.C": "dummy carbon", 38 | "Any": "any atom", 39 | "Hal": "halogen", 40 | "Het": "heteroatom = N, O, S, P", 41 | "Hev": "heavy atom (non hydrogen)", 42 | "Li": "lithium", 43 | "Na": "sodium", 44 | "Mg": "magnesium", 45 | "Al": "aluminum", 46 | "Si": "silicon", 47 | "K": "potassium", 48 | "Ca": "calcium", 49 | "Cr.thm": "chromium (tetrahedral)", 50 | "Cr.oh": "chromium (octahedral)", 51 | "Mn": "manganese", 52 | "Fe": "iron", 53 | "Co.oh": "cobalt (octahedral)", 54 | "Cu": "copper", 55 | } 56 | 57 | _ATOM_FIELDS = ( 58 | ("id", int), 59 | ("name", str), 60 | ("x", float), 61 | ("y", float), 62 | ("z", float), 63 | ("type", str), 64 | ("mol_id", int), 65 | ("mol_name", str), 66 | ("charge", float), 67 | ("status_bits", str), 68 | ) 69 | 70 | _BOND_FIELDS = ( 71 | ("bond_id", int), 72 | ("origin", int), 73 | ("target", int), 74 | ("type", str), 75 | ("status_bits", str), 76 | ) 77 | 78 | 79 | def parse_atom_lines(lines): 80 | atom_data = defaultdict(list) 81 | for line in lines[1:]: 82 | for (n, f), tok in zip(_ATOM_FIELDS, line.split(), strict=False): 83 | atom_data[n].append(f(tok)) 84 | return atom_data 85 | 86 | 87 | def parse_bond_lines(lines): 88 | bond_data = defaultdict(list) 89 | for line in lines[1:]: 90 | for (n, f), tok in zip(_BOND_FIELDS, line.split(), strict=False): 91 | bond_data[n].append(f(tok)) 92 | return bond_data 93 | 94 | 95 | def parse_mol2_string(string): 96 | # only parse bonds and atoms for now 97 | atom_section = "@ATOM" 98 | bond_section = "@BOND" 99 | atom_lines = [] 100 | bond_lines = [] 101 | lines = string.splitlines() 102 | unknown_lines = [] 103 | app = unknown_lines 104 | for i in range(len(lines)): 105 | line = lines[i].strip() 106 | if "@" in line: 107 | app = unknown_lines 108 | if atom_section in line: 109 | app = atom_lines 110 | elif bond_section in line: 111 | app = bond_lines 112 | if line: 113 | app.append(line) 114 | return parse_atom_lines(atom_lines), parse_bond_lines(bond_lines) 115 | 116 | 117 | def parse_mol2_file(filename): 118 | return parse_mol2_string(Path(filename).read_text()) 119 | -------------------------------------------------------------------------------- /src/chmpy/fmt/nwchem.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from chmpy.templates import load_template 4 | 5 | LOG = logging.getLogger(__name__) 6 | NWCHEM_INPUT_TEMPLATE = load_template("nwchem_input") 7 | 8 | 9 | def to_nwchem_input(molecule, **kwargs): 10 | blocks = {} 11 | tasks = ("scf",) 12 | 13 | method = kwargs.get("method", "scf") 14 | if method.lower() == "hf": 15 | method = "scf" 16 | basis_set = kwargs.get("basis_set", "3-21G") 17 | geometry_keywords = kwargs.get("geometry_keywords", "noautoz nocenter") 18 | 19 | blocks[f"geometry {geometry_keywords}"] = molecule.to_xyz_string(header=False) 20 | 21 | return NWCHEM_INPUT_TEMPLATE.render( 22 | title=kwargs.get("title", molecule.molecular_formula), 23 | method=method, 24 | charge=molecule.charge, 25 | multiplicity=molecule.multiplicity, 26 | basis_set=basis_set, 27 | cartesian_basis=True, 28 | blocks=blocks, 29 | tasks=tasks, 30 | ) 31 | -------------------------------------------------------------------------------- /src/chmpy/fmt/shelx.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from chmpy.crystal import SymmetryOperation 5 | 6 | LOG = logging.getLogger(__name__) 7 | 8 | 9 | def _parse_titl(line): 10 | return line.split()[-1] 11 | 12 | 13 | def _parse_cell(line): 14 | tokens = [float(x) for x in line.split()[1:]] 15 | return { 16 | "wavelength": tokens[0], 17 | "lengths": tuple(tokens[1:4]), 18 | "angles": tuple(tokens[4:7]), 19 | } 20 | 21 | 22 | def _parse_int(line): 23 | return int(line.split()[1]) 24 | 25 | 26 | def _parse_sfac(line): 27 | return tuple(line.split()[1:]) 28 | 29 | 30 | def _parse_symm(line): 31 | return SymmetryOperation.from_string_code(line[4:]) 32 | 33 | 34 | def _parse_atom_line(sfac, line): 35 | tokens = line.split() 36 | len(tokens) 37 | label = tokens[0] 38 | LOG.debug("Parsing atom line: %s", line) 39 | sfac_idx = int(tokens[1]) 40 | a, b, c = (float(x) for x in tokens[2:5]) 41 | occupation = 1.0 42 | if len(tokens) > 5: 43 | occupation = float(tokens[5]) 44 | return { 45 | "label": label, 46 | "element": sfac[sfac_idx - 1], 47 | "position": (a, b, c), 48 | "occupation": occupation, 49 | } 50 | 51 | 52 | SHELX_LINE_KEYS = { 53 | "TITL": _parse_titl, 54 | "CELL": _parse_cell, 55 | "ZERR": _parse_int, 56 | "LATT": _parse_int, 57 | "SFAC": _parse_sfac, 58 | "SYMM": _parse_symm, 59 | "FVAR": None, 60 | "UNIT": None, 61 | "REM ": None, 62 | "MORE": None, 63 | "TIME": None, 64 | "OMIT": None, 65 | "ESEL": None, 66 | "EGEN": None, 67 | "LIST": None, 68 | "FMAP": None, 69 | "PLAN": None, 70 | "MOLE": None, 71 | "HKLF": None, 72 | } 73 | 74 | 75 | def parse_shelx_file_content(file_content): 76 | """Read a SHELX formatted crystal structure from 77 | a string 78 | Parameters 79 | ---------- 80 | file_content: str 81 | text contents of the SHELX .res file to read 82 | 83 | Returns 84 | ------- 85 | dict 86 | dictionary of parsed shelx data 87 | """ 88 | contents = file_content.split("\n") 89 | shelx_dict = {"SYMM": [SymmetryOperation.from_string_code("x,y,z")], "ATOM": []} 90 | for line_number, line in enumerate(contents, start=1): 91 | try: 92 | line = line.strip() 93 | if not line: 94 | continue 95 | key = line[:4].upper() 96 | if key == "END": 97 | break 98 | elif key == "SYMM": 99 | shelx_dict[key].append(SHELX_LINE_KEYS[key](line)) 100 | elif key not in SHELX_LINE_KEYS: 101 | shelx_dict["ATOM"].append(_parse_atom_line(shelx_dict["SFAC"], line)) 102 | else: 103 | f = SHELX_LINE_KEYS[key] 104 | if f is None: 105 | continue 106 | shelx_dict[key] = f(line) 107 | except Exception as e: 108 | raise ValueError(f"Error parsing shelx string: line {line_number}") from e 109 | return shelx_dict 110 | 111 | 112 | def parse_shelx_file(filename): 113 | """Read a SHELX formatted .res file. 114 | Parameters 115 | ---------- 116 | filename: str 117 | path to the shelx .res file to read 118 | 119 | Returns 120 | ------- 121 | dict 122 | dictionary of parsed shelx data 123 | """ 124 | return parse_shelx_file_content(Path(filename).read_text()) 125 | 126 | 127 | def _cell_string(value): 128 | rvalues = [int(x) if x.is_integer() else round(x, 6) for x in value] 129 | return "CELL 0.7 {} {} {} {} {} {}".format(*rvalues) 130 | 131 | 132 | def _atom_lines(atoms): 133 | return "\n".join(atoms) 134 | 135 | 136 | def to_res_contents(shelx_data): 137 | """ 138 | Parameters 139 | ---------- 140 | shelx_data: dict 141 | dictionary of data to write into a SHELX .res format 142 | 143 | Returns 144 | ------- 145 | str 146 | the string encoded contents of this shelx_data 147 | """ 148 | SHELX_FORMATTERS = { 149 | "TITL": lambda x: f"TITL {x}", 150 | "CELL": _cell_string, 151 | "LATT": lambda x: f"LATT {x}", 152 | "SYMM": lambda symm: "\n".join(f"SYMM {x}" for x in symm), 153 | "SFAC": lambda x: "SFAC " + " ".join(x), 154 | "ATOM": _atom_lines, 155 | } 156 | sections = [] 157 | for key in SHELX_FORMATTERS: 158 | sections.append(SHELX_FORMATTERS[key](shelx_data[key])) 159 | return "\n".join(sections) 160 | -------------------------------------------------------------------------------- /src/chmpy/fmt/smiles.py: -------------------------------------------------------------------------------- 1 | import pyparsing as pp 2 | from pyparsing import Literal as Lit 3 | from pyparsing import Optional as Opt 4 | from pyparsing import Regex 5 | 6 | pp.ParserElement.enablePackrat() 7 | 8 | 9 | class SMILESParser: 10 | def __init__(self): 11 | OrganicSymbol = Regex("Br?|Cl?|N|O|P|S|F|I|At|Ts|b|c|n|o|p|s").setParseAction( 12 | self.parse 13 | ) 14 | Symbol = Regex( 15 | "A(c|g|l|m|r|s|t|u)|" 16 | "B(a|e|h|i|k|r)?|" 17 | "C(a|d|e|f|l|m|n|o|r|s|u)?|" 18 | "D(b|s|y)|" 19 | "E(r|s|u)|" 20 | "F(e|l|m|r)?|" 21 | "G(a|d|e)|" 22 | "H(e|f|g|o|s)?|" 23 | "I(n|r)?|" 24 | "Kr?|" 25 | "L(a|i|r|u|v)?|" 26 | "M(c|g|n|o|t)?|" 27 | "N(a|b|d|e|h|i|o|p)?|" 28 | "O(g|s)?|" 29 | "P(a|b|d|m|o|r|t|u)?|" 30 | "R(a|b|e|f|g|h|n|u)|" 31 | "S(b|c|e|g|i|m|n|r)?|" 32 | "T(a|b|c|e|h|i|l|m|s)|" 33 | "U|V|W|Xe|Yb?|Z(n|r)|" 34 | "b|c|n|o|p|se?|as" 35 | ) 36 | Chiral = Regex("@@?") 37 | Fifteen = Regex("1(0|1|2|3|4|5)|2|3|4|5|6|7|8|9") 38 | Charge = pp.MatchFirst( 39 | [ 40 | Lit("+") + Opt(pp.MatchFirst([Lit("+"), Fifteen])), 41 | Lit("-") + Opt(pp.MatchFirst([Lit("-") ^ Fifteen])), 42 | ] 43 | ).setParseAction(lambda x: int(x[0])) 44 | HCount = Regex("H[0-9]?") 45 | Isotope = Regex("[0-9]?[0-9]?[0-9]") 46 | Map = Regex(":[0-9]?[0-9]?[0-9]") 47 | Dot = Lit(".").setParseAction(self._parse_dot) 48 | Bond = Regex(r"-|=|#|$|\\").setParseAction(self._parse_bond) 49 | RNum = Regex("[0-9]|(%[0-9][0-9])").setParseAction(self._parse_ring_num) 50 | Line = pp.Forward() 51 | Atom = pp.Forward() 52 | LBr = Lit("(").setParseAction(self._parse_lbr) 53 | RBr = Lit(")").setParseAction(self._parse_rbr) 54 | Branch = LBr + (pp.OneOrMore(Opt(pp.MatchFirst([Bond, Dot])) + Line)) + RBr 55 | Chain = pp.OneOrMore( 56 | pp.MatchFirst( 57 | [ 58 | (Dot + Atom("atoms")), 59 | ( 60 | Opt(Bond("explicit_bonds")) 61 | + pp.MatchFirst([Atom("atoms"), RNum("rings")]) 62 | ), 63 | ] 64 | ) 65 | ) 66 | BracketAtom = Lit("[") + Opt(Isotope("isotopes")) + Symbol + Opt( 67 | Chiral("chirality") 68 | ) + Opt(HCount("explicit_hydrogens")) + Opt(Charge) ^ Opt(Map) + Lit("]") 69 | Atom << pp.MatchFirst([OrganicSymbol, BracketAtom]).setParseAction( 70 | self._parse_atom 71 | ) 72 | Line << Atom("atom") + pp.ZeroOrMore(pp.MatchFirst([Chain, Branch])) 73 | self.parser = Line 74 | 75 | def parse(self, tok): 76 | self.count += 1 77 | 78 | def _parse_atom(self, tok): 79 | N = len(self.atoms) 80 | if self._prev_idx > -1: 81 | self.bonds.append((self._prev_idx, N + 1, self._bond_type)) 82 | self._bond_type = "-" 83 | self.atoms.append(tok[0]) 84 | self._prev_idx = N + 1 85 | 86 | def _parse_dot(self, tok): 87 | self._prev_idx = -1 88 | 89 | def _parse_bond(self, tok): 90 | self._bond_type = tok[0] 91 | 92 | def _parse_ring_num(self, toks): 93 | idx = int(toks[0]) 94 | if idx in self._rings: 95 | a, b = self._rings[idx][0], len(self.atoms) 96 | self._rings[idx] = (a, b) 97 | self.bonds.append((a, b, "r")) 98 | else: 99 | self._rings[idx] = (len(self.atoms), -1) 100 | 101 | def _parse_lbr(self, toks): 102 | self._branch_idx = self._prev_idx 103 | 104 | def _parse_rbr(self, toks): 105 | self._prev_idx = self._branch_idx 106 | self._branch_idx = -1 107 | 108 | def parseString(self, s): 109 | self.count = 0 110 | self._prev_idx = -1 111 | self._branch_idx = -1 112 | self.atoms = [] 113 | self.bonds = [] 114 | self._bond_type = "-" 115 | self._rings = {} 116 | self.parser.parseString(s) 117 | return (self.atoms, self.bonds) 118 | 119 | 120 | _DEFAULT_PARSER = SMILESParser() 121 | 122 | 123 | def parse(s): 124 | return _DEFAULT_PARSER.parseString(s) 125 | -------------------------------------------------------------------------------- /src/chmpy/fmt/tmol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | from chmpy.core.element import Element 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | 11 | def parse_tmol_string(contents, filename=None): 12 | """Convert provided turbomole coord file contents into an array of 13 | atomic numbers and cartesian positions 14 | 15 | Parameters 16 | ---------- 17 | contents: str 18 | text contents of the .xyz file to read 19 | 20 | Returns 21 | ------- 22 | tuple of :obj:`np.ndarray` 23 | array of (N) atomic numbers and (N, 3) Cartesian positions 24 | read from the given file 25 | """ 26 | lines = contents.splitlines() 27 | angstroms = "angs" in lines[0] 28 | elements = [] 29 | positions = [] 30 | for line in lines[1:]: 31 | stripped = line.strip() 32 | if (not stripped) or "$end" in line: 33 | break 34 | if "$" in line: 35 | continue 36 | tokens = line.strip().split() 37 | xyz = tuple(float(x) for x in tokens[:3]) 38 | positions.append(xyz) 39 | elements.append(Element[tokens[3]]) 40 | LOG.debug( 41 | "Found %d atoms lines in %s", 42 | len(elements), 43 | "in " + filename if filename else "", 44 | ) 45 | positions = np.array(positions) 46 | if not angstroms: 47 | from chmpy.util.unit import units 48 | 49 | positions = units.angstrom(positions) 50 | return elements, positions 51 | 52 | 53 | def parse_tmol_file(filename): 54 | """Convert a provided turbomole coord file into an array of 55 | atomic numbers and cartesian positions 56 | 57 | Parameters 58 | ---------- 59 | filename: str 60 | path to the turbomole file to read 61 | 62 | Returns 63 | ------- 64 | tuple of :obj:`np.ndarray` 65 | array of (N) atomic numbers and (N, 3) Cartesian positions 66 | read from the given file 67 | """ 68 | path = Path(filename) 69 | return parse_tmol_string(path.read_text(), filename=str(path.absolute())) 70 | -------------------------------------------------------------------------------- /src/chmpy/fmt/vasp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | from chmpy.core.element import Element 6 | 7 | LOG = logging.getLogger(__name__) 8 | 9 | 10 | def parse_poscar(poscar_string): 11 | "Read in a VASP POSCAR or CONTCAR file" 12 | result = { 13 | "name": None, 14 | "direct": None, 15 | "elements": None, 16 | "positions": None, 17 | } 18 | lines = poscar_string.splitlines() 19 | result["name"] = lines[0].strip() 20 | scale_factor = float(lines[1]) 21 | result["direct"] = scale_factor * np.fromstring( 22 | " ".join(lines[2:5]), sep=" " 23 | ).reshape((3, 3)) 24 | elements = [] 25 | for el, x in zip(lines[5].split(), lines[6].split(), strict=False): 26 | elements += [Element[el]] * int(x) 27 | result["elements"] = elements 28 | result["coord_type"] = lines[7].strip().lower() 29 | N = len(elements) 30 | result["positions"] = np.fromstring(" ".join(lines[8 : 8 + N]), sep=" ").reshape( 31 | (-1, 3) 32 | ) 33 | return result 34 | -------------------------------------------------------------------------------- /src/chmpy/fmt/xtb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import namedtuple 3 | 4 | import numpy as np 5 | 6 | from chmpy.core import Element, Molecule 7 | from chmpy.crystal import AsymmetricUnit, SpaceGroup, UnitCell 8 | from chmpy.templates import load_template 9 | 10 | LOG = logging.getLogger(__name__) 11 | TMOL_TEMPLATE = load_template("turbomole") 12 | 13 | Cell = namedtuple("Cell", "a b c alpha beta gamma") 14 | 15 | 16 | def crystal_to_turbomole_string(crystal, **kwargs): 17 | uc_atoms = crystal.unit_cell_atoms() 18 | pos = uc_atoms["frac_pos"] 19 | el = [Element[x] for x in uc_atoms["element"]] 20 | return TMOL_TEMPLATE.render( 21 | periodic=True, 22 | lattice=crystal.unit_cell.lattice.T, 23 | lattice_units="angs", 24 | atoms=zip(pos, el, strict=False), 25 | units="frac", 26 | blocks=kwargs, 27 | ) 28 | 29 | 30 | def molecule_to_turbomole_string(molecule, **kwargs): 31 | return TMOL_TEMPLATE.render( 32 | atoms=zip(molecule.positions, molecule.elements, strict=False), 33 | units="angs", 34 | blocks=kwargs, 35 | ) 36 | 37 | 38 | def turbomole_string(obj, **kwargs): 39 | if isinstance(obj, Molecule): 40 | return molecule_to_turbomole_string(obj, **kwargs) 41 | else: 42 | return crystal_to_turbomole_string(obj, **kwargs) 43 | 44 | 45 | def load_turbomole_string(tmol_string): 46 | from chmpy.util.unit import units 47 | 48 | "Initialize from an xtb coord string resulting from optimization" 49 | data = {} 50 | sections = tmol_string.split("$") 51 | for section in sections: 52 | if not section or section.startswith("end"): 53 | continue 54 | lines = section.strip().splitlines() 55 | label = lines[0].strip() 56 | data[label] = [x.strip() for x in lines[1:]] 57 | lattice = [] if "lattice bohr" in data else None 58 | elements = [] 59 | positions = [] 60 | for line in data.pop("coord"): 61 | x, y, z, el = line.split() 62 | positions.append((float(x), float(y), float(z))) 63 | elements.append(Element[el]) 64 | ANGS = units.angstrom(1.0) 65 | pos_cart = np.array(positions) * ANGS 66 | result = { 67 | "positions": pos_cart, 68 | "elements": elements, 69 | } 70 | if lattice is not None: 71 | for line in data.pop("lattice bohr"): 72 | lattice.append([float(x) for x in line.split()]) 73 | direct = np.array(lattice) * ANGS 74 | uc = UnitCell(direct) 75 | pos_frac = uc.to_fractional(pos_cart) 76 | asym = AsymmetricUnit(elements, pos_frac) 77 | result["unit_cell"] = uc 78 | result["asymmetric_unit"] = asym 79 | result["space_group"] = SpaceGroup(1) 80 | 81 | result.update(**data) 82 | return result 83 | -------------------------------------------------------------------------------- /src/chmpy/fmt/xyz_file.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | 6 | from chmpy.core.element import Element 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | 11 | def parse_xyz_string(contents, filename=None): 12 | """Convert provided xmol .xyz file contents into an array of 13 | atomic numbers and cartesian positions 14 | 15 | Parameters 16 | ---------- 17 | contents: str 18 | text contents of the .xyz file to read 19 | 20 | Returns 21 | ------- 22 | tuple of :obj:`np.ndarray` 23 | array of (N) atomic numbers and (N, 3) Cartesian positions 24 | read from the given file 25 | """ 26 | lines = contents.splitlines() 27 | natom = int(lines[0].strip()) 28 | LOG.debug("Expecting %d atoms %s", natom, "in " + filename if filename else "") 29 | elements = [] 30 | positions = [] 31 | for line in lines[2:]: 32 | if not line.strip(): 33 | break 34 | tokens = line.strip().split() 35 | xyz = tuple(float(x) for x in tokens[1:4]) 36 | positions.append(xyz) 37 | elements.append(Element[tokens[0]]) 38 | LOG.debug( 39 | "Found %d atoms lines in %s", 40 | len(elements), 41 | "in " + filename if filename else "", 42 | ) 43 | return elements, np.asarray(positions) 44 | 45 | 46 | def parse_traj_string(contents, filename=None): 47 | """Convert provided xmol .xyz file contents into list of arrays of 48 | atomic numbers and cartesian positions 49 | 50 | Parameters 51 | ---------- 52 | contents: str 53 | text contents of the .xyz file to read 54 | 55 | Returns 56 | ------- 57 | list of tuple of :obj:`np.ndarray` 58 | list of (N) :obj:`Element` and (N, 3) Cartesian positions 59 | read from the given file 60 | """ 61 | lines = contents.splitlines() 62 | i = 0 63 | frames = [] 64 | while i < len(lines): 65 | natom = int(lines[i].strip()) 66 | elements = [] 67 | positions = [] 68 | i += 1 69 | comment = lines[i] 70 | i += 1 71 | for line in lines[i : i + natom]: 72 | if not line.strip(): 73 | continue 74 | tokens = line.strip().split() 75 | xyz = tuple(float(x) for x in tokens[1:4]) 76 | positions.append(xyz) 77 | elements.append(Element[tokens[0]]) 78 | frames.append((elements, comment, np.asarray(positions))) 79 | i += natom 80 | return frames 81 | 82 | 83 | def parse_xyz_file(filename): 84 | """Convert a provided xmol .xyz file into an array of 85 | atomic numbers and cartesian positions 86 | 87 | Parameters 88 | ---------- 89 | filename: str 90 | path to the .xyz file to read 91 | 92 | Returns 93 | ------- 94 | tuple of :obj:`np.ndarray` 95 | array of (N) atomic numbers and (N, 3) Cartesian positions 96 | read from the given file 97 | """ 98 | path = Path(filename) 99 | return parse_xyz_string(path.read_text(), filename=str(path.absolute())) 100 | 101 | 102 | def parse_traj_file(filename): 103 | """Convert a provided xmol .xyz file into an array of 104 | atomic numbers and cartesian positions 105 | 106 | Parameters 107 | ---------- 108 | filename: str 109 | path to the .xyz file to read 110 | 111 | Returns 112 | ------- 113 | tuple of :obj:`np.ndarray` 114 | array of (N) atomic numbers and (N, 3) Cartesian positions 115 | read from the given file 116 | """ 117 | path = Path(filename) 118 | return parse_traj_string(path.read_text(), filename=str(path.absolute())) 119 | -------------------------------------------------------------------------------- /src/chmpy/interpolate/__init__.py: -------------------------------------------------------------------------------- 1 | from .density import PromoleculeDensity, StockholderWeight 2 | 3 | __all__ = ["PromoleculeDensity", "StockholderWeight"] 4 | -------------------------------------------------------------------------------- /src/chmpy/interpolate/density.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, join 2 | 3 | import numpy as np 4 | from scipy.spatial import cKDTree as KDTree 5 | 6 | from chmpy.core.element import vdw_radii 7 | 8 | from ._density import PromoleculeDensity as cPromol 9 | from ._density import StockholderWeight as cStock 10 | 11 | _DATA_DIR = dirname(__file__) 12 | _INTERPOLATOR_DATA = np.load(join(_DATA_DIR, "thakkar_interp.npz")) 13 | _DOMAIN = _INTERPOLATOR_DATA.f.domain 14 | _RHO = _INTERPOLATOR_DATA.f.rho 15 | _GRAD_RHO = _INTERPOLATOR_DATA.f.grad_rho 16 | 17 | 18 | class PromoleculeDensity: 19 | def __init__(self, mol): 20 | n, pos = mol 21 | self.elements = np.asarray(n, dtype=np.int32) 22 | self.positions = np.asarray(pos, dtype=np.float32) 23 | if np.any(self.elements < 1) or np.any(self.elements > 103): 24 | raise ValueError("All elements must be atomic numbers between [1,103]") 25 | self.rho_data = np.empty( 26 | (self.elements.shape[0], _DOMAIN.shape[0]), dtype=np.float32 27 | ) 28 | for i, el in enumerate(self.elements): 29 | self.rho_data[i, :] = _RHO[el - 1, :] 30 | self.dens = cPromol(self.positions, _DOMAIN, self.rho_data) 31 | self.principal_axes, _, _ = np.linalg.svd((self.positions - self.centroid).T) 32 | self.vdw_radii = vdw_radii(self.elements) 33 | 34 | def rho(self, positions): 35 | positions = np.asarray(positions, dtype=np.float32) 36 | return self.dens.rho(positions) 37 | 38 | @property 39 | def centroid(self): 40 | return np.mean(self.positions, axis=0) 41 | 42 | @property 43 | def natoms(self): 44 | return len(self.elements) 45 | 46 | def bb(self, vdw_buffer=3.8): 47 | extra = self.vdw_radii[:, np.newaxis] + vdw_buffer 48 | return ( 49 | np.min(self.positions - extra, axis=0), 50 | np.max(self.positions + extra, axis=0), 51 | ) 52 | 53 | def __repr__(self): 54 | return f"" 55 | 56 | def d_norm(self, positions): 57 | pos = self.positions 58 | tree = KDTree(pos) 59 | # make sure k is enough should be enough for d_norm to be correct 60 | dists, idxs = tree.query(positions, k=min(6, self.natoms)) 61 | d_norm = np.empty(dists.shape[0]) 62 | vecs = np.empty(positions.shape) 63 | for j, (d, i) in enumerate(zip(dists, idxs, strict=False)): 64 | i = i[i < pos.shape[0]] 65 | vdw = self.vdw_radii[i] 66 | d_n = (d - vdw) / vdw 67 | p = np.argmin(d_n) 68 | d_norm[j] = d_n[p] 69 | vecs[j] = (pos[p] - positions[j]) / vdw[p] 70 | if dists.ndim == 1: 71 | return dists, d_norm, vecs 72 | return dists[:, 0], d_norm, vecs 73 | 74 | @classmethod 75 | def from_xyz_file(cls, filename): 76 | from chmpy.fmt.xyz_file import parse_xyz_file 77 | 78 | els, pos = parse_xyz_file(filename) 79 | els = np.array([x.atomic_number for x in els]) 80 | return cls((els, pos)) 81 | 82 | 83 | class StockholderWeight: 84 | def __init__(self, dens_a, dens_b, background=0.0): 85 | assert isinstance(dens_a, PromoleculeDensity) and isinstance( 86 | dens_b, PromoleculeDensity 87 | ), "Must be PromoleculeDensity instances" 88 | self.dens_a = dens_a 89 | self.dens_b = dens_b 90 | self.s = cStock(dens_a.dens, dens_b.dens, background=background) 91 | 92 | @property 93 | def positions(self): 94 | return np.r_[self.dens_a.positions, self.dens_b.positions] 95 | 96 | @property 97 | def vdw_radii(self): 98 | return np.r_[self.dens_a.vdw_radii, self.dens_b.vdw_radii] 99 | 100 | def weights(self, positions): 101 | positions.astype(np.float32) 102 | return self.s.weights(positions.astype(np.float32)) 103 | 104 | def d_norm(self, positions): 105 | d_a, d_norm_a, vecs_a = self.dens_a.d_norm(positions) 106 | d_b, d_norm_b, vecs_b = self.dens_b.d_norm(positions) 107 | dp = np.einsum("ij,ij->i", vecs_a, vecs_b) 108 | angles = dp / (np.linalg.norm(vecs_a, axis=1) * np.linalg.norm(vecs_b, axis=1)) 109 | return d_a, d_b, d_norm_a, d_norm_b, dp, angles 110 | 111 | @classmethod 112 | def from_xyz_files(cls, f1, f2): 113 | return cls( 114 | PromoleculeDensity.from_xyz_file(f1), PromoleculeDensity.from_xyz_file(f2) 115 | ) 116 | 117 | @classmethod 118 | def from_arrays(cls, n1, p1, n2, p2, unit="angstrom", **kwargs): 119 | return cls(PromoleculeDensity((n1, p1)), PromoleculeDensity((n2, p2)), **kwargs) 120 | 121 | def bb(self, vdw_buffer=3.8): 122 | extra = self.dens_a.vdw_radii[:, np.newaxis] + vdw_buffer 123 | return ( 124 | np.min(self.dens_a.positions - extra, axis=0), 125 | np.max(self.dens_a.positions + extra, axis=0), 126 | ) 127 | -------------------------------------------------------------------------------- /src/chmpy/interpolate/lerp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def vectorized_lerp(xs, xp, yp, l_fill=None, u_fill=None): 5 | N = xp.shape[0] 6 | dx = xp[1] - xp[0] # Assuming uniform spacing 7 | inv_dx = 1.0 / dx 8 | l = xp[0] 9 | u = xp[-1] 10 | if l_fill is None: 11 | l_fill = yp[0] 12 | 13 | if u_fill is None: 14 | u_fill = yp[-1] 15 | 16 | # Calculate js indices 17 | js = np.minimum((inv_dx * (xs - l)).astype(np.int32), N - 2) 18 | 19 | # Compute weights for interpolation 20 | w = (xs - xp[js]) * inv_dx 21 | 22 | # Linear interpolation 23 | results = (1.0 - w) * yp[js] + w * yp[js + 1] 24 | 25 | # Fill values outside domain 26 | results[xs < l] = l_fill 27 | results[xs > u] = u_fill 28 | 29 | return results 30 | -------------------------------------------------------------------------------- /src/chmpy/interpolate/thakkar_interp.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/interpolate/thakkar_interp.npz -------------------------------------------------------------------------------- /src/chmpy/ints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/ints/__init__.py -------------------------------------------------------------------------------- /src/chmpy/ints/lebedev.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | 5 | _GRIDS = dict(np.load(Path(__file__).parent / "lebedev_grids.npz").items()) 6 | _GRID_LMAX_LIST = tuple(sorted(int(k.split("_")[-1]) for k in _GRIDS.keys())) 7 | _GRIDS_NUM_POINTS = {k: v.shape[0] for k, v in _GRIDS.items()} 8 | 9 | 10 | def load_grid(l_max): 11 | for g in _GRID_LMAX_LIST: 12 | if g > l_max: 13 | break 14 | else: 15 | raise ValueError(f"No available Lebedev grid for l_max = {l_max}") 16 | grid = _GRIDS[f"l_max_{g}"] 17 | 18 | return grid 19 | 20 | 21 | def load_grid_num_points(num_points): 22 | ordered_grids = sorted( 23 | ((k, v) for k, v in _GRIDS_NUM_POINTS.items()), key=lambda x: x[1] 24 | ) 25 | grid = None 26 | for g, k in ordered_grids: 27 | grid = _GRIDS[g] 28 | if k > num_points: 29 | break 30 | else: 31 | raise ValueError(f"No available Lebedev grid for num_points = {num_points}") 32 | return grid 33 | -------------------------------------------------------------------------------- /src/chmpy/ints/lebedev_grids.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/ints/lebedev_grids.npz -------------------------------------------------------------------------------- /src/chmpy/ints/solvation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from scipy.spatial.kdtree import cKDTree as KDTree 5 | 6 | from .lebedev import load_grid_num_points 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | SOLVENT_RADII = [ 11 | 1.300, 12 | 1.638, 13 | 1.404, 14 | 1.053, 15 | 2.0475, 16 | 2.00, 17 | 1.830, 18 | 1.720, 19 | 1.720, 20 | 1.8018, 21 | 1.755, 22 | 1.638, 23 | 1.404, 24 | 2.457, 25 | 2.106, 26 | 2.160, 27 | 2.05, 28 | ] 29 | 30 | DEFAULT_RADIUS = 2.223 31 | 32 | 33 | def get_solvent_radii(atomic_numbers): 34 | radii = np.empty_like(atomic_numbers, dtype=np.float32) 35 | for i in range(len(atomic_numbers)): 36 | n = atomic_numbers[i] 37 | if n <= 17 and n > 0: 38 | radii[i] = SOLVENT_RADII[n - 1] 39 | else: 40 | radii[i] = DEFAULT_RADIUS 41 | return radii 42 | 43 | 44 | def solvent_surface(molecule, num_points_per_atom=140, delta=0.1): 45 | radii = get_solvent_radii(molecule.atomic_numbers) 46 | N = len(molecule) 47 | grid = load_grid_num_points(num_points_per_atom) 48 | num_points_per_atom = grid.shape[0] 49 | axes = molecule.axes() 50 | positions = molecule.positions_in_molecular_axis_frame() 51 | surface = np.empty((N * num_points_per_atom, 4)) 52 | atom_idx = np.empty(N * num_points_per_atom, dtype=np.int32) 53 | 54 | for i in range(N): 55 | r = radii[i] + delta 56 | l, u = num_points_per_atom * i, num_points_per_atom * (i + 1) 57 | surface[l:u, 3] = grid[:, 3] * 4 * np.pi * radii[i] * radii[i] 58 | surface[l:u, :3] = grid[:, :3] * r 59 | surface[l:u, :3] += positions[i, :] 60 | atom_idx[l:u] = i 61 | 62 | mask = np.ones_like(atom_idx, dtype=bool) 63 | 64 | tree = KDTree(surface[:, :3]) 65 | for i in range(N): 66 | p = positions[i] 67 | radius = radii[i] + delta 68 | idxs = tree.query_ball_point(p, radius - 1e-12) 69 | mask[idxs] = False 70 | 71 | surface = surface[mask, :] 72 | surface[:, :3] = ( 73 | np.dot(surface[:, :3], axes) + molecule.center_of_mass[np.newaxis, :] 74 | ) 75 | atom_idx = atom_idx[mask] 76 | return surface 77 | -------------------------------------------------------------------------------- /src/chmpy/mc/__init__.py: -------------------------------------------------------------------------------- 1 | from ._mc import marching_cubes 2 | 3 | __all__ = ["marching_cubes"] 4 | -------------------------------------------------------------------------------- /src/chmpy/opt/__init__.py: -------------------------------------------------------------------------------- 1 | from .xtb import XtbEnergyEvaluator, XtbOptimizer 2 | 3 | __all__ = [ 4 | "XtbEnergyEvaluator", 5 | "XtbOptimizer", 6 | ] 7 | -------------------------------------------------------------------------------- /src/chmpy/sampling/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is dedicated to sampling points, sequences and 3 | generation of random or quasi random object. 4 | """ 5 | 6 | import numpy as np 7 | 8 | from ._lds import quasirandom_kgf, quasirandom_kgf_batch 9 | from ._sobol import quasirandom_sobol, quasirandom_sobol_batch 10 | 11 | __all__ = [ 12 | "quasirandom_kgf", 13 | "quasirandom_kgf_batch", 14 | "quasirandom_sobol", 15 | "quasirandom_sobol_batch", 16 | ] 17 | 18 | _BATCH = { 19 | "sobol": quasirandom_sobol_batch, 20 | "kgf": quasirandom_kgf_batch, 21 | } 22 | 23 | _SINGLE = { 24 | "sobol": quasirandom_sobol, 25 | "kgf": quasirandom_kgf, 26 | } 27 | 28 | 29 | def quasirandom(d1: int, d2=None, method="sobol", seed=1) -> np.ndarray: 30 | """ 31 | Generate a quasirandom point, or sequence of points with coefficients 32 | in the interval [0, 1]. 33 | 34 | Args: 35 | d1 (int): number of points to generate (or number of dimensions if d2 is 36 | not provided) 37 | d2 (int, optional): number of dimensions 38 | method (str, optional): use the 'sobol' or 'kgf' sequences to generate 39 | points 40 | seed (int, optional): start seed for the sequence of numbers. if more than 41 | 1 point is generated then the seeds will be in the range 42 | [seed, seed + d1 -1] corresponding to each point in the resulting sequence. 43 | 44 | Returns: 45 | np.ndarray: The sequence of quasirandom vectors 46 | """ 47 | if d2 is None: 48 | return _SINGLE[method](seed, d1) 49 | else: 50 | return _BATCH[method](seed, seed + d1 - 1, d2) 51 | -------------------------------------------------------------------------------- /src/chmpy/sampling/_lds.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3, boundscheck=False, wraparound=False 2 | cimport numpy as cnp 3 | cimport cython 4 | import numpy as np 5 | from libc.math cimport pow 6 | 7 | cnp.import_array() 8 | 9 | @cython.cdivision(True) 10 | cdef double phi(const unsigned int d) noexcept nogil: 11 | cdef int iterations = 30 12 | cdef int i 13 | cdef double x = 2.0 14 | for i in range(iterations): 15 | x = pow(1 + x, 1.0 / (d + 1.0)) 16 | return x 17 | 18 | @cython.cdivision(True) 19 | cdef void alpha(double[::1] a) noexcept nogil: 20 | cdef int dims = a.shape[0] 21 | cdef double g = phi(dims) 22 | cdef int i 23 | for i in range(dims): 24 | a[i] = pow(1 / g, i + 1) % 1 25 | 26 | 27 | cpdef quasirandom_kgf(const unsigned int N, const unsigned int D): 28 | """ 29 | Generate an D dimensional Korobov type quasi-random vector 30 | based on the generalized Fibonacci sequence. 31 | 32 | Based on the R_1, R_2 sequences available here: 33 | `https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/` 34 | 35 | Parameters: 36 | N (int): seed 37 | D (int): number of dimensions 38 | 39 | Returns: 40 | np.ndarray: an (D) dimensional sampling point 41 | """ 42 | cdef double offset = 0.5 43 | cdef cnp.ndarray[cnp.float64_t, ndim=1] a = np.empty(D, dtype=np.float64) 44 | cdef double[::1] a_view = a 45 | with nogil: 46 | alpha(a_view) 47 | return (offset + a * (N + 1)) % 1 48 | 49 | cpdef quasirandom_kgf_batch(const unsigned int L, const unsigned int U, const unsigned int D): 50 | """ 51 | Generate a batch of D dimensional Korobov type quasi-random vectors 52 | based on the generalized Fibonacci sequence. 53 | 54 | Based on the R_1, R_2 sequences available here: 55 | `https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/` 56 | 57 | Parameters: 58 | L (int): Start seed 59 | U (int): End seed 60 | D (int): number of dimensions 61 | 62 | Returns: 63 | np.ndarray: an (U - L + 1, D) dimensional array of sampling points 64 | """ 65 | cdef double offset = 0.5 66 | cdef cnp.ndarray[cnp.float64_t, ndim=1] a = np.empty(D, dtype=np.float64) 67 | cdef cnp.ndarray[cnp.float64_t, ndim=2] result = np.empty((U - L + 1, D), dtype=np.float64) 68 | cdef cnp.ndarray[cnp.int32_t, ndim=1] N = np.arange(L, U + 1, dtype=np.int32) 69 | cdef double[::1] a_view = a 70 | with nogil: 71 | alpha(a_view) 72 | return (offset + a[np.newaxis, :] * (N[:, np.newaxis] + 1)) % 1 73 | -------------------------------------------------------------------------------- /src/chmpy/sampling/_sobol_parameters.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/sampling/_sobol_parameters.npz -------------------------------------------------------------------------------- /src/chmpy/shape/__init__.py: -------------------------------------------------------------------------------- 1 | from .assoc_legendre import AssocLegendre 2 | from .shape_descriptors import ( 3 | promolecule_density_descriptor, 4 | stockholder_weight_descriptor, 5 | ) 6 | from .sht import SHT 7 | 8 | __all__ = [ 9 | "AssocLegendre", 10 | "SHT", 11 | "stockholder_weight_descriptor", 12 | "promolecule_density_descriptor", 13 | ] 14 | -------------------------------------------------------------------------------- /src/chmpy/shape/assoc_legendre.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class AssocLegendre: 5 | def __init__(self, lm): 6 | self.lmax = lm 7 | self.a, self.b = self._compute_ab(self.lmax) 8 | self.cache = np.zeros((lm + 1, lm + 1)) 9 | self.norm = np.zeros((lm + 1, lm + 1)) 10 | 11 | @staticmethod 12 | def _amm(m): 13 | a = 1.0 14 | for k in range(1, np.abs(m) + 1): 15 | a *= (2 * k + 1) / (2 * k) 16 | return np.sqrt(a / (4 * np.pi)) 17 | 18 | @staticmethod 19 | def _amn(m, n): 20 | return np.sqrt((4 * n * n - 1) / (n * n - m * m)) 21 | 22 | @staticmethod 23 | def _bmn(m, n): 24 | return -np.sqrt( 25 | (2 * n + 1) * ((n - 1) * (n - 1) - m * m) / ((2 * n - 3) * (n * n - m * m)) 26 | ) 27 | 28 | @staticmethod 29 | def _compute_ab(lmax): 30 | a = np.empty((lmax + 1, lmax + 1)) 31 | b = np.empty((lmax + 1, lmax + 1)) 32 | for m in range(0, lmax + 1): 33 | a[m, m] = AssocLegendre._amm(m) 34 | for l in range(np.abs(m) + 1, lmax + 1): 35 | a[l, m] = AssocLegendre._amn(m, l) 36 | b[l, m] = AssocLegendre._bmn(m, l) 37 | return a, b 38 | 39 | def evaluate_batch(self, x, result=None): 40 | if result is None: 41 | result = np.zeros((self.lmax + 1) * (self.lmax + 2) // 2) 42 | idx = 0 43 | for m in range(0, self.lmax + 1): 44 | for l in range(m, self.lmax + 1): 45 | if l == m: 46 | result[idx] = self.a[l, m] * (1 - x * x) ** (0.5 * m) 47 | elif l == (m + 1): 48 | result[idx] = self.a[l, m] * x * self.cache[l - 1, m] 49 | else: 50 | result[idx] = ( 51 | self.a[l, m] * x * self.cache[l - 1, m] 52 | + self.b[l, m] * self.cache[l - 2, m] 53 | ) 54 | self.cache[l, m] = result[idx] 55 | idx += 1 56 | return result 57 | -------------------------------------------------------------------------------- /src/chmpy/shape/convex_hull.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def _ray_hull_intersection(direction, hull): 5 | eq = hull.equations 6 | X, y = eq[:, :-1], eq[:, -1] 7 | d_dot_X = np.dot(X, direction) 8 | d_dot_X[d_dot_X == 0.0] = 1e-6 9 | alpha = -y / d_dot_X 10 | d = np.min(alpha[alpha > 0]) 11 | return np.linalg.norm(d * direction) 12 | 13 | 14 | def _ray_hull_intersections_batch(directions, hull): 15 | eq = hull.equations 16 | X, y = eq[:, :-1], eq[:, -1] 17 | d_dot_X = directions @ X.T 18 | d_dot_X[d_dot_X <= 0] = 1e-6 19 | alpha = -y / d_dot_X 20 | d = np.min(alpha, axis=1) 21 | return np.linalg.norm(d[:, np.newaxis] * directions, axis=1) 22 | 23 | 24 | def ray_hull_intersections(directions, hull, method="fast"): 25 | """ 26 | Find the distance from the origin to the intersection with the 27 | given ConvexHull for a list of directions. Assumes `directions` 28 | is a (N, 3) array of unit vectors representing directions, and 29 | `hull` is a `ConvexHull` object centered about the origin. 30 | 31 | 32 | Args: 33 | directions (np.ndarray): (N, 3) array of unit vectors 34 | hull (ConvexHull): A ConvexHull for which to find intersections 35 | 36 | 37 | Returns: 38 | np.ndarray: (N,) array of the distances for each intersection 39 | """ 40 | if method == "fast": 41 | return _ray_hull_intersections_batch(directions, hull) 42 | else: 43 | return np.array([_ray_hull_intersection(p, hull) for p in directions]) 44 | 45 | 46 | def transform_hull(sht, hull, **kwargs): 47 | """ 48 | Calculate the spherical harmonic transform of the shape of the 49 | provided convex hull 50 | 51 | Args: 52 | sht (SHT): the spherical harmonic transform object handle 53 | n_i (ConvexHull): the convex hull (or shape to describe) 54 | kwargs (dict): keyword arguments for optional settings. 55 | Options include: 56 | ``` 57 | origin (np.ndarray): specify the center of the surface 58 | (default is the geometric centroid of the interior atoms) 59 | ``` 60 | distances (bool): also return the distances of intersection 61 | 62 | Returns: 63 | np.ndarray: the coefficients from the analysis step of the SHT 64 | """ 65 | 66 | x, y, z = sht.grid_cartesian 67 | directions = np.c_[x.flatten(), y.flatten(), z.flatten()] 68 | 69 | r = ray_hull_intersections(directions, hull).reshape(x.shape) 70 | coeffs = sht.analysis(r) 71 | if kwargs.get("distances", False): 72 | return coeffs, r 73 | else: 74 | return coeffs 75 | -------------------------------------------------------------------------------- /src/chmpy/shape/reconstruct.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | from chmpy.util.num import cartesian_to_spherical_mgrid 6 | 7 | from .sht import SHT 8 | 9 | LOG = logging.getLogger(__name__) 10 | 11 | 12 | def reconstruct(coefficients, real=True, pole_extension_factor=2): 13 | if real: 14 | n = len(coefficients) 15 | l_max = int((-3 + np.sqrt(8 * n + 1)) // 2) 16 | else: 17 | l_max = int(np.sqrt(len(coefficients))) - 1 18 | LOG.debug("Reconstructing deduced l_max = %d", l_max) 19 | sht = SHT(l_max) 20 | x, y, z = sht.grid_cartesian 21 | pts = np.c_[x.flatten(), y.flatten(), z.flatten()] 22 | pts = pts * sht.synthesis(coefficients).flatten()[:, np.newaxis].real 23 | # Add centroid points for the poles 24 | north_pole_indices = np.arange((sht.ntheta - 1) * sht.nphi, sht.ntheta * sht.nphi) 25 | south_pole_indices = np.arange(0, sht.nphi) 26 | north_pole_centroid = np.mean(pts[north_pole_indices], axis=0) 27 | south_pole_centroid = np.mean(pts[south_pole_indices], axis=0) 28 | 29 | # Compute the average distance of the surrounding points from the origin 30 | north_pole_radius = np.mean(np.linalg.norm(pts[north_pole_indices], axis=1)) 31 | south_pole_radius = np.mean(np.linalg.norm(pts[south_pole_indices], axis=1)) 32 | 33 | # Compute the extension factor based on the radius ratio 34 | north_pole_extension_factor = north_pole_radius / np.linalg.norm( 35 | north_pole_centroid 36 | ) 37 | south_pole_extension_factor = south_pole_radius / np.linalg.norm( 38 | south_pole_centroid 39 | ) 40 | 41 | # Extend the poles based on the extension factor 42 | north_pole_extension = north_pole_centroid * north_pole_extension_factor 43 | south_pole_extension = south_pole_centroid * south_pole_extension_factor 44 | 45 | pts = np.vstack((pts, north_pole_extension, south_pole_extension)) 46 | 47 | # Update the faces to include the centroid points 48 | faces = sht.faces() 49 | north_pole_index = len(pts) - 2 50 | south_pole_index = len(pts) - 1 51 | for i in range(sht.nphi): 52 | faces.append( 53 | [ 54 | north_pole_index, 55 | north_pole_indices[(i + 1) % sht.nphi], 56 | north_pole_indices[i], 57 | ] 58 | ) 59 | faces.append( 60 | [ 61 | south_pole_index, 62 | south_pole_indices[(i + 1) % sht.nphi], 63 | south_pole_indices[i], 64 | ] 65 | ) 66 | return pts, faces 67 | 68 | 69 | def reconstructed_surface_convex(coefficients, real=True): 70 | from scipy.spatial import ConvexHull 71 | from trimesh import Trimesh 72 | 73 | pts, _ = reconstruct(coefficients, real=real) 74 | cvx = ConvexHull(pts) 75 | return Trimesh(vertices=pts, faces=cvx.simplices) 76 | 77 | 78 | def reconstructed_surface(coefficients, real=True): 79 | from trimesh import Trimesh 80 | 81 | pts, faces = reconstruct(coefficients, real=real) 82 | mesh = Trimesh(vertices=pts, faces=faces) 83 | return mesh 84 | 85 | 86 | def reconstructed_surface_icosphere(coefficients, real=True, subdivisions=3): 87 | if real: 88 | n = len(coefficients) 89 | l_max = int((-3 + np.sqrt(8 * n + 1)) // 2) 90 | else: 91 | n = len(coefficients) 92 | l_max = int(np.sqrt(n)) - 1 93 | LOG.debug("Reconstructing deduced l_max = %d", l_max) 94 | sht = SHT(l_max) 95 | 96 | from trimesh.creation import icosphere 97 | 98 | datatype = np.float64 if real else np.complex128 99 | 100 | sphere = icosphere(subdivisions=subdivisions) 101 | r, theta, phi = cartesian_to_spherical_mgrid( 102 | sphere.vertices[:, 0], sphere.vertices[:, 1], sphere.vertices[:, 2] 103 | ) 104 | r = np.empty_like(phi, datatype) 105 | for i in range(phi.shape[0]): 106 | r[i] = sht.evaluate_at_points(coefficients, theta[i], phi[i]) 107 | sphere.vertices *= np.real(r[:, np.newaxis]) 108 | 109 | if not real: 110 | sphere.vertex_attributes["property"] = np.imag(r) 111 | return sphere 112 | -------------------------------------------------------------------------------- /src/chmpy/shape/spherical_harmonics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ._sht import AssocLegendre 4 | 5 | 6 | class SphericalHarmonics: 7 | def __init__(self, lm, phase=True): 8 | self.lmax = lm 9 | self.phase = phase 10 | self.plm = AssocLegendre(lm) 11 | self.plm_work_array = np.empty(self.nplm(), dtype=np.float64) 12 | 13 | def idx_c(self, l, m): 14 | return l * (l + 1) + m 15 | 16 | def nlm(self): 17 | "the number of complex SHT coefficients" 18 | return (self.lmax + 1) * (self.lmax + 1) 19 | 20 | def nplm(self): 21 | "the number of real SHT coefficients (i.e. legendre polynomial terms)" 22 | return (self.lmax + 1) * (self.lmax + 2) // 2 23 | 24 | def single_angular(self, theta, phi, result=None): 25 | if result is None: 26 | result = np.empty(self.nlm(), dtype=np.complex128) 27 | 28 | ct = np.cos(theta) 29 | self.plm.evaluate_batch(ct, result=self.plm_work_array) 30 | 31 | plm_idx = 0 32 | for l in range(0, self.lmax + 1): 33 | offset = l * (l + 1) 34 | result[offset] = self.plm_work_array[plm_idx] 35 | plm_idx += 1 36 | 37 | c = np.exp(phi * 1j) 38 | cm = c 39 | for m in range(1, self.lmax + 1): 40 | sign = 1 41 | if self.phase and (m & 1): 42 | sign = -1 43 | for l in range(m, self.lmax + 1): 44 | l_offset = l * (l + 1) 45 | rr = cm 46 | ii = np.conj(rr) 47 | rr = sign * self.plm_work_array[plm_idx] * rr 48 | ii = sign * self.plm_work_array[plm_idx] * ii 49 | if m & 1: 50 | ii = -ii 51 | result[l_offset - m] = ii 52 | result[l_offset + m] = rr 53 | plm_idx += 1 54 | cm *= c 55 | return result 56 | 57 | def batch_angular(self, pos, result=None): 58 | if result is None: 59 | result = np.empty((pos.shape[0], self.nlm()), dtype=np.complex128) 60 | 61 | for i in range(pos.shape[0]): 62 | result[i, :] = self.single_angular(*pos[i], result=result[i, :]) 63 | return result 64 | 65 | def single_cartesian(self, x, y, z, result=None): 66 | if result is None: 67 | result = np.empty(self.nlm(), dtype=np.complex128) 68 | pass 69 | epsilon = 1e-12 70 | ct = z 71 | self.plm.evaluate_batch(ct, result=self.plm_work_array) 72 | 73 | st = 0.0 74 | if abs(1.0 - ct) > epsilon: 75 | st = np.sqrt(1.0 - ct * ct) 76 | 77 | plm_idx = 0 78 | for l in range(0, self.lmax + 1): 79 | l_offset = l * (l + 1) 80 | result[l_offset] = self.plm_work_array[plm_idx] 81 | plm_idx += 1 82 | 83 | c = 0j 84 | if st > epsilon: 85 | c = complex(x / st, y / st) 86 | cm = c 87 | for m in range(1, self.lmax + 1): 88 | sign = 1 89 | if self.phase and (m & 1): 90 | sign = -1 91 | for l in range(m, self.lmax + 1): 92 | l_offset = l * (l + 1) 93 | rr = sign * cm * self.plm_work_array[plm_idx] 94 | ii = sign * np.conj(cm) * self.plm_work_array[plm_idx] 95 | if m & 1: 96 | ii = -ii 97 | result[l_offset - m] = ii 98 | result[l_offset + m] = rr 99 | plm_idx += 1 100 | cm *= c 101 | return result 102 | 103 | def batch_cartesian(self, pos, result=None): 104 | if result is None: 105 | result = np.empty((pos.shape[0], self.nlm()), dtype=np.complex128) 106 | pass 107 | 108 | def __eval__(self, *parameters, result=None, cartesian=False): 109 | pass 110 | -------------------------------------------------------------------------------- /src/chmpy/subgraphs/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | __all__ = ["load_data"] 5 | DIR = Path(__file__).parent 6 | LOG = logging.getLogger(__name__) 7 | 8 | 9 | def load_data(): 10 | import graph_tool as gt 11 | 12 | graphs = {} 13 | LOG.info("Loading graph data from %s", DIR) 14 | for fname in DIR.glob("*.gt"): 15 | graphs[fname.stem] = gt.load_graph(str(fname)) 16 | return graphs 17 | -------------------------------------------------------------------------------- /src/chmpy/subgraphs/carboxylic_acid.gt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/subgraphs/carboxylic_acid.gt -------------------------------------------------------------------------------- /src/chmpy/templates/__init__.py: -------------------------------------------------------------------------------- 1 | """Module to store jinja2 templates for ccpy""" 2 | 3 | import logging 4 | from os.path import abspath, dirname, split 5 | 6 | from jinja2 import Environment, FileSystemLoader, Template 7 | 8 | LOG = logging.getLogger(__name__) 9 | CHMPY_TEMPLATE_DIR = dirname(abspath(__file__)) 10 | CHMPY_TEMPLATE_ENV = Environment(loader=FileSystemLoader(CHMPY_TEMPLATE_DIR)) 11 | 12 | 13 | # Object to store all templates 14 | _ALL_TEMPLATES = { 15 | "turbomole": CHMPY_TEMPLATE_ENV.get_template("tmol.jinja2"), 16 | "crystal17": CHMPY_TEMPLATE_ENV.get_template("crystal17.jinja2"), 17 | "gaussian_scf": CHMPY_TEMPLATE_ENV.get_template("gaussian_scf.jinja2"), 18 | "tonto_pair_energy": CHMPY_TEMPLATE_ENV.get_template("tonto_pair_energy.jinja2"), 19 | "nwchem_input": CHMPY_TEMPLATE_ENV.get_template("nwchem_input.jinja2"), 20 | "gulp": CHMPY_TEMPLATE_ENV.get_template("gulp.jinja2"), 21 | } 22 | 23 | 24 | def add_template(text=None, filename=None, name="new_template"): 25 | if filename: 26 | path, filename = split(filename) 27 | _ALL_TEMPLATES[name] = Environment( 28 | loader=FileSystemLoader(path or "./") 29 | ).get_template(filename) 30 | elif text: 31 | _ALL_TEMPLATES[name] = Template(text) 32 | return _ALL_TEMPLATES[name] 33 | 34 | 35 | def load_template(name): 36 | result = _ALL_TEMPLATES.get(name) 37 | if result is None: 38 | try: 39 | return CHMPY_TEMPLATE_ENV.get_template(name) 40 | except Exception as e: 41 | LOG.error("Could not find template: %s (%s)", name, e) 42 | return result 43 | -------------------------------------------------------------------------------- /src/chmpy/templates/crystal17.jinja2: -------------------------------------------------------------------------------- 1 | {{title}} 2 | CRYSTAL 3 | {{iflag}} {{ifhr}} {{ifso}} 4 | {{space_group}} 5 | {{cell_parameters}} 6 | {{natoms}} 7 | {% for (x, y, z), e in atoms -%} 8 | {{e.atomic_number}} {{"%10.6f" | format(x)}} {{"%10.6f" | format(y)}} {{"%10.6f" | format(z)}} 9 | {% endfor -%} 10 | FRACTION 11 | BASISSET 12 | {{basis_set}} 13 | {% for k, v in basis_set_keywords.items() -%} 14 | {{k}}{% if v != None %}={% endif %}{{v}} 15 | {% endfor -%} 16 | {{method}} 17 | END 18 | SHRINK 19 | {{shrink_factors[0]}} {{shrink_factors[1]}} 20 | END 21 | END 22 | -------------------------------------------------------------------------------- /src/chmpy/templates/gaussian_scf.jinja2: -------------------------------------------------------------------------------- 1 | {%- for cmd, val in link0.items() -%} 2 | %{{cmd}}={{val}}{{'\n'}} 3 | {%- endfor -%} 4 | # {{method}}/{{basis}} {{route_commands}} 5 | 6 | {{title}} 7 | 8 | {{charge}} {{multiplicity}} 9 | {{geometry}} 10 | {{'\n'}}{{'\n'}} 11 | {%- for section in additional_sections -%} 12 | {{section}}{{'\n'}} 13 | {%- endfor -%} 14 | {{'\n'}} 15 | -------------------------------------------------------------------------------- /src/chmpy/templates/gulp.jinja2: -------------------------------------------------------------------------------- 1 | {{ keywords | join(' ') }} 2 | {% if cell %} 3 | cell 4 | {{cell}} 5 | {% endif %} 6 | {% if frac %}frac{% else %}cart{% endif %} 7 | {% for e, (x, y, z) in atoms -%} 8 | {{e}} core {{"%10.6f" | format(x)}} {{"%10.6f" | format(y)}} {{"%10.6f" | format(z)}} 9 | {% endfor -%} 10 | {% if spacegroup %} 11 | space 12 | {{spacegroup}} 13 | {% endif %} 14 | {% for keyword, argument in additional_keywords.items() %} 15 | {{keyword}} {{argument}} 16 | {% endfor %} 17 | -------------------------------------------------------------------------------- /src/chmpy/templates/nwchem_input.jinja2: -------------------------------------------------------------------------------- 1 | start 2 | title {{title}} 3 | 4 | {%- for block_name, block_contents in blocks.items() %} 5 | {{block_name}} 6 | {{block_contents}} 7 | end 8 | {% endfor -%} 9 | 10 | {% if charge != 0 -%}charge = {{charge}}{% endif %} 11 | {% if multiplicity != 1 -%}nopen = {{multiplicity - 1}}{% endif %} 12 | 13 | basis {{"cartesian" if cartesian_basis else ""}} 14 | * library {{basis_set}} 15 | end 16 | 17 | {%- for task_keywords in tasks %} 18 | task {{task_keywords}} 19 | {% endfor -%} 20 | -------------------------------------------------------------------------------- /src/chmpy/templates/tmol.jinja2: -------------------------------------------------------------------------------- 1 | {%- if periodic -%} 2 | $periodic 3 3 | $lattice {{lattice_units}} 4 | {% for x, y, z in lattice -%} 5 | {{"%10.6f" | format(x)}} {{"%10.6f" | format(y)}} {{"%10.6f" | format(z)}} 6 | {% endfor -%} 7 | {% endif -%} 8 | $coord {{units}} 9 | {% for (x, y, z), e in atoms -%} 10 | {{"%10.6f" | format(x)}} {{"%10.6f" | format(y)}} {{"%10.6f" | format(z)}} {{e.symbol}} 11 | {% endfor -%} 12 | {% for block_name, block in blocks.items() -%} 13 | ${{block_name}} 14 | {% for k, v in block.items() -%} 15 | {{k}}={{v}} 16 | {% endfor -%} 17 | {% endfor -%} 18 | $end 19 | -------------------------------------------------------------------------------- /src/chmpy/templates/tonto_pair_energy.jinja2: -------------------------------------------------------------------------------- 1 | { 2 | {% if basis_directory %} basis_directory= {{basis}} {% endif %} 3 | name= {{name}} 4 | atom_groups= { 5 | keys= { name= atom_indices= fchk_file= rotation= shift= } 6 | data= { 7 | Group1 { {{idxs_a}} } 8 | "{{fchk_a}}" 9 | {{rot_a}} 10 | {{shift_a}} 11 | 12 | Group2 { {{idxs_b}} } 13 | "{{fchk_b}}" 14 | {{rot_b}} 15 | {{shift_b}} 16 | } 17 | } 18 | 19 | put_group_12_polarization_energy 20 | put_group_12_energies 21 | put_group_12_grimme2006_energy 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/chmpy/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | TEST_FILES_PATH = Path(__file__).parent / "test_files" 4 | 5 | print("Loading test data...") 6 | 7 | TEST_FILES = {x.name: x for x in TEST_FILES_PATH.iterdir()} 8 | 9 | print("Finished loading test files") 10 | -------------------------------------------------------------------------------- /src/chmpy/tests/acetic_acid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/acetic_acid.png -------------------------------------------------------------------------------- /src/chmpy/tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/core/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/core/test_element.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy.core.element import ( 6 | Element, 7 | chemical_formula, 8 | cov_radii, 9 | element_names, 10 | element_symbols, 11 | vdw_radii, 12 | ) 13 | 14 | 15 | class ElementTestCase(unittest.TestCase): 16 | def test_construction(self): 17 | for s in (1, "D", "H", "hydrogen", "1"): 18 | e = Element[s] 19 | self.assertEqual(e.atomic_number, 1) 20 | self.assertEqual(e.symbol, "H") 21 | self.assertEqual(e.name, "hydrogen") 22 | 23 | for s in ("blah", "32.141", None, 1.5): 24 | with self.assertRaises(ValueError): 25 | e = Element[s] 26 | 27 | def test_vdw(self): 28 | e = Element[1] 29 | self.assertEqual(e.vdw, 1.09) 30 | self.assertEqual(e.vdw_radius, 1.09) 31 | self.assertEqual(e.cov, 0.23) 32 | self.assertEqual(e.covalent_radius, 0.23) 33 | 34 | def test_comparison(self): 35 | e1 = Element[1] 36 | e2 = Element["H"] 37 | b = Element["B"] 38 | c = Element["C"] 39 | o = Element["O"] 40 | self.assertEqual(e1, e2) 41 | 42 | with self.assertRaises(NotImplementedError): 43 | print(e1 == 1.3) 44 | 45 | with self.assertRaises(NotImplementedError): 46 | print(e1 < 1.3) 47 | 48 | self.assertTrue(c < o) 49 | self.assertTrue(b < o) 50 | self.assertTrue(c < e1) 51 | self.assertFalse(o < c) 52 | 53 | def test_chemical_formulat(self): 54 | self.assertEqual(chemical_formula(("H", "O", "H")), "H2O") 55 | self.assertEqual(chemical_formula(("H", "O", "H"), subscript=True), "H\u2082O") 56 | 57 | def test_radii(self): 58 | nums_valid = np.array([1, 2, 3]) 59 | nums_invalid = np.array([-1, 105]) 60 | np.testing.assert_allclose(cov_radii(nums_valid), [0.23, 1.5, 1.28]) 61 | np.testing.assert_allclose(vdw_radii(nums_valid), [1.09, 1.4, 1.82]) 62 | np.testing.assert_equal(element_symbols(nums_valid), ["H", "He", "Li"]) 63 | np.testing.assert_equal( 64 | element_names(nums_valid), ["hydrogen", "helium", "lithium"] 65 | ) 66 | 67 | for m in (cov_radii, vdw_radii, element_symbols, element_names): 68 | with self.assertRaises(ValueError): 69 | m(nums_invalid) 70 | -------------------------------------------------------------------------------- /src/chmpy/tests/core/test_molecule.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | from pathlib import Path 4 | from tempfile import TemporaryDirectory 5 | 6 | import numpy as np 7 | 8 | from chmpy import Molecule 9 | 10 | from .. import TEST_FILES 11 | 12 | LOG = logging.getLogger(__name__) 13 | _WATER = None 14 | 15 | 16 | class MoleculeTestCase(unittest.TestCase): 17 | pos = np.array([(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]) 18 | els = np.ones(2, dtype=int) 19 | 20 | @staticmethod 21 | def load_water(): 22 | global _WATER 23 | from copy import deepcopy 24 | 25 | if _WATER is None: 26 | _WATER = Molecule.load(TEST_FILES["water.xyz"]) 27 | return deepcopy(_WATER) 28 | 29 | def test_construction(self): 30 | bonds = np.diag(np.ones(2)) 31 | labels = np.array(["H1", "H2"]) 32 | Molecule.from_arrays(self.els, self.pos, bonds=bonds, labels=labels) 33 | 34 | def test_distances(self): 35 | m1 = self.load_water() 36 | m2 = self.load_water() 37 | m2.positions += (0, 3.0, 0) 38 | self.assertAlmostEqual(m1.distance_to(m2, method="center_of_mass"), 3.0) 39 | self.assertAlmostEqual( 40 | m1.distance_to(m2, method="nearest_atom"), 2.121545157481363 41 | ) 42 | self.assertAlmostEqual(m1.distance_to(m2, method="centroid"), 3.0) 43 | with self.assertRaises(ValueError): 44 | m1.distance_to(m2, method="unjknaskldfj") 45 | 46 | def test_xyz_file_read(self): 47 | mol = self.load_water() 48 | self.assertEqual(len(mol), 3) 49 | self.assertEqual(mol.positions.shape, (3, 3)) 50 | self.assertEqual(mol.molecular_formula, "H2O") 51 | 52 | def test_sdf_file_read(self): 53 | mol = Molecule.load(TEST_FILES["DB09563.sdf"]) 54 | self.assertEqual(len(mol), 21) 55 | self.assertEqual(mol.positions.shape, (21, 3)) 56 | self.assertEqual(mol.molecular_formula, "C5H14NO") 57 | 58 | def test_molecule_centroid(self): 59 | mol = self.load_water() 60 | cent = mol.centroid 61 | np.testing.assert_allclose( 62 | cent, (-0.488956, 0.277612, 0.001224), rtol=1e-3, atol=1e-5 63 | ) 64 | com = mol.center_of_mass 65 | np.testing.assert_allclose( 66 | com, (-0.6664043, -0.0000541773, 0.008478989), rtol=1e-3, atol=1e-5 67 | ) 68 | 69 | def test_repr(self): 70 | mol = self.load_water() 71 | expected = "" 72 | self.assertEqual(repr(mol), expected) 73 | 74 | def test_save(self): 75 | mol = self.load_water() 76 | with TemporaryDirectory() as tmpdirname: 77 | LOG.debug("created temp directory: %s", tmpdirname) 78 | mol.save(Path(tmpdirname, "tmp.xyz")) 79 | mol.save(Path(tmpdirname, "tmp.xyz"), header=False) 80 | 81 | def test_bbox(self): 82 | mol = self.load_water() 83 | bbox = mol.bbox_corners 84 | expected = (np.min(mol.positions, axis=0), np.max(mol.positions, axis=0)) 85 | np.testing.assert_allclose(bbox, expected, atol=1e-5) 86 | np.testing.assert_allclose(mol.bbox_size, expected[1] - expected[0]) 87 | -------------------------------------------------------------------------------- /src/chmpy/tests/crystal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/crystal/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/crystal/test_asymmetric_unit.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy import Element 6 | from chmpy.crystal import AsymmetricUnit 7 | 8 | _ICE_II_LABELS = ( 9 | "O1", 10 | "H1", 11 | "H2", 12 | "O2", 13 | "H3", 14 | "H4", 15 | "O3", 16 | "H5", 17 | "H6", 18 | "O4", 19 | "H7", 20 | "H8", 21 | "O5", 22 | "H9", 23 | "H10", 24 | "O6", 25 | "H11", 26 | "H12", 27 | "O7", 28 | "H13", 29 | "H14", 30 | "O8", 31 | "H15", 32 | "H16", 33 | "O9", 34 | "H17", 35 | "H18", 36 | "O10", 37 | "H19", 38 | "H20", 39 | "O11", 40 | "H21", 41 | "H22", 42 | "O12", 43 | "H23", 44 | "H24", 45 | ) 46 | _ICE_II_ELEMENTS = [Element[x] for x in _ICE_II_LABELS] 47 | 48 | _ICE_II_POSITIONS = np.array( 49 | [ 50 | 0.273328954083, 51 | 0.026479033257, 52 | 0.855073668062, 53 | 0.152000330304, 54 | 0.043488909374, 55 | 0.793595454907, 56 | 0.420775085827, 57 | 0.191165194485, 58 | 0.996362203192, 59 | 0.144924657237, 60 | 0.726669877048, 61 | 0.973520141937, 62 | 0.206402797363, 63 | 0.847998481439, 64 | 0.956510183901, 65 | 0.003636687868, 66 | 0.579223433079, 67 | 0.808833958746, 68 | 0.026477491142, 69 | 0.855072949204, 70 | 0.273328854276, 71 | 0.043487719387, 72 | 0.793594459529, 73 | 0.152000553858, 74 | 0.191163388489, 75 | 0.996362120061, 76 | 0.420774953988, 77 | 0.726670757782, 78 | 0.973520932681, 79 | 0.144926297633, 80 | 0.847999275418, 81 | 0.956510882297, 82 | 0.206404294889, 83 | 0.579224602173, 84 | 0.808834869258, 85 | 0.003637530197, 86 | 0.855073412561, 87 | 0.273329597478, 88 | 0.026478702027, 89 | 0.793594909621, 90 | 0.152000771295, 91 | 0.043488316376, 92 | 0.996362312075, 93 | 0.420775512814, 94 | 0.191164826329, 95 | 0.973520717390, 96 | 0.144925628579, 97 | 0.726671054509, 98 | 0.956510600982, 99 | 0.206403626100, 100 | 0.847999547813, 101 | 0.808834607385, 102 | 0.003637609551, 103 | 0.579224562315, 104 | 0.477029330652, 105 | 0.749805220756, 106 | 0.331717174202, 107 | 0.402360172390, 108 | 0.720795433576, 109 | 0.401054786853, 110 | 0.368036378343, 111 | 0.742284933413, 112 | 0.207434128329, 113 | 0.668282055550, 114 | 0.522969467265, 115 | 0.250193622013, 116 | 0.598945169999, 117 | 0.597639203188, 118 | 0.279204514235, 119 | 0.792565160978, 120 | 0.631962548905, 121 | 0.257714022497, 122 | 0.749805496250, 123 | 0.331717033025, 124 | 0.477029827575, 125 | 0.720795009402, 126 | 0.401054437437, 127 | 0.402360618546, 128 | 0.742284706875, 129 | 0.207433751728, 130 | 0.368036342085, 131 | 0.522969071341, 132 | 0.250193392512, 133 | 0.668282780114, 134 | 0.597638176364, 135 | 0.279203622225, 136 | 0.598945231951, 137 | 0.631962932785, 138 | 0.257715003205, 139 | 0.792566578018, 140 | 0.331715381178, 141 | 0.477028907327, 142 | 0.749804544234, 143 | 0.401053887354, 144 | 0.402360576463, 145 | 0.720795552111, 146 | 0.207432480540, 147 | 0.368035542438, 148 | 0.742284142147, 149 | 0.250193225247, 150 | 0.668282913065, 151 | 0.522970147212, 152 | 0.279203658434, 153 | 0.598945325854, 154 | 0.597639149965, 155 | 0.257715011998, 156 | 0.792566781760, 157 | 0.631964289620, 158 | ] 159 | ).reshape(-1, 3) 160 | 161 | 162 | def ice_ii_asym(): 163 | return AsymmetricUnit(_ICE_II_ELEMENTS, _ICE_II_POSITIONS, labels=_ICE_II_LABELS) 164 | 165 | 166 | class AsymmetricUnitTestCase(unittest.TestCase): 167 | def test_asymmetric_unit_constructor(self): 168 | asym = ice_ii_asym() 169 | self.assertTrue(len(asym) == 36) 170 | asym_generated_labels = AsymmetricUnit(_ICE_II_ELEMENTS, _ICE_II_POSITIONS) 171 | self.assertTrue(all(asym_generated_labels.labels == asym.labels)) 172 | 173 | def test_repr(self): 174 | asym = ice_ii_asym() 175 | self.assertTrue(asym.__repr__() == "") 176 | 177 | def test_from_records(self): 178 | records = [{"label": "H1", "element": "H", "position": (0, 0, 0)}] 179 | asym = AsymmetricUnit.from_records(records) 180 | self.assertTrue(len(asym) == 1) 181 | -------------------------------------------------------------------------------- /src/chmpy/tests/crystal/test_unit_cell.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | from os.path import dirname, join 4 | 5 | import numpy as np 6 | 7 | from chmpy.crystal import UnitCell 8 | 9 | LOG = logging.getLogger(__name__) 10 | _ICE_II = join(dirname(__file__), "iceII.cif") 11 | 12 | 13 | class UnitCellTestCase(unittest.TestCase): 14 | def test_unit_cell_lattice(self): 15 | c = UnitCell.cubic(2.0) 16 | np.testing.assert_allclose(c.lattice, 2.0 * np.eye(3), atol=1e-8) 17 | np.testing.assert_allclose(c.reciprocal_lattice, 0.5 * np.eye(3), atol=1e-8) 18 | 19 | def test_coordinate_transforms(self): 20 | c = UnitCell.cubic(2.0) 21 | np.testing.assert_allclose( 22 | c.to_fractional(np.eye(3)), 0.5 * np.eye(3), atol=1e-8 23 | ) 24 | np.testing.assert_allclose(c.to_cartesian(np.eye(3)), 2 * np.eye(3), atol=1e-8) 25 | 26 | def test_handle_bad_angles(self): 27 | # should warn 28 | UnitCell.from_lengths_and_angles([2.0] * 3, [90] * 3) 29 | 30 | def test_repr(self): 31 | c = UnitCell.cubic(2.0) 32 | self.assertTrue(str(c) == "") 33 | 34 | def test_cell_types(self): 35 | cubic = UnitCell.cubic(2.0) 36 | orthorhombic = UnitCell.orthorhombic(3.0, 4.0, 5.0) 37 | tetragonal = UnitCell.tetragonal(3.0, 4.0, unit="degrees") 38 | tetragonal = UnitCell.from_unique_parameters((3.0, 4.0), cell_type="tetragonal") 39 | monoclinic = UnitCell.monoclinic(3.0, 4.0, 5.0, 75, unit="degrees") 40 | monoclinic = UnitCell.monoclinic(3.0, 4.0, 5.0, 1.5) 41 | triclinic = UnitCell.triclinic(3.0, 4.0, 5.0, 45, 75, 90, unit="degrees") 42 | rhombohedral = UnitCell.rhombohedral(3.0, 97, unit="degrees") 43 | rhombohedral = UnitCell.rhombohedral(3.0, 1.35) 44 | hexagonal = UnitCell.hexagonal(3.0, 4.0) 45 | hexagonal = UnitCell.from_lengths_and_angles( 46 | [3.0, 3.0, 5.0], [90, 90, 120], unit="degrees" 47 | ) 48 | self.assertTrue(cubic.cell_type == "cubic") 49 | self.assertTrue(orthorhombic.cell_type == "orthorhombic") 50 | self.assertTrue(tetragonal.cell_type == "tetragonal") 51 | self.assertTrue(monoclinic.cell_type == "monoclinic") 52 | self.assertTrue(triclinic.cell_type == "triclinic") 53 | self.assertTrue(rhombohedral.cell_type == "rhombohedral") 54 | self.assertTrue(hexagonal.cell_type == "hexagonal") 55 | 56 | def test_angles(self): 57 | c = UnitCell.cubic(2.0) 58 | self.assertFalse(c.angles_different) 59 | self.assertTrue(c.is_cubic) 60 | self.assertFalse(c.is_triclinic) 61 | self.assertFalse(c.is_monoclinic) 62 | self.assertFalse(c.is_tetragonal) 63 | self.assertFalse(c.is_rhombohedral) 64 | self.assertFalse(c.is_hexagonal) 65 | self.assertFalse(c.is_orthorhombic) 66 | 67 | np.testing.assert_allclose([c.alpha_deg, c.beta_deg, c.gamma_deg], 90.0) 68 | np.testing.assert_allclose(c.parameters, [2, 2, 2, 90, 90, 90]) 69 | -------------------------------------------------------------------------------- /src/chmpy/tests/crystal/test_wulff.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import numpy as np 5 | 6 | from chmpy.crystal.wulff import WulffConstruction, WulffSHT 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | 11 | class WulffConstructionTestCase(unittest.TestCase): 12 | def test_cube(self): 13 | facets = np.array( 14 | ((1, 0, 0), (0, 1, 0), (0, 0, 1), (-1, 0, 0), (0, -1, 0), (0, 0, -1)) 15 | ) 16 | facet_energies = np.ones(6) 17 | w = WulffConstruction(facets, facet_energies) 18 | self.assertEqual(len(w.wulff_facets), 6) 19 | self.assertEqual(len(w.wulff_vertices), 8) 20 | 21 | mesh = w.to_trimesh() 22 | self.assertAlmostEqual(mesh.volume, 8.0) 23 | self.assertAlmostEqual(mesh.area, 24.0) 24 | 25 | def test_cube_sht(self): 26 | facets = np.array( 27 | ((1, 0, 0), (0, 1, 0), (0, 0, 1), (-1, 0, 0), (0, -1, 0), (0, 0, -1)) 28 | ) 29 | facet_energies = np.ones(6) 30 | s = WulffSHT(facets, facet_energies, l_max=2) 31 | coeffs_real_expected = np.array( 32 | ( 33 | 4.27895414e00, 34 | -5.55111512e-17, 35 | -7.04135032e-03, 36 | 2.05657853e-01, 37 | 0.00000000e00, 38 | 3.79056240e-02, 39 | ) 40 | ) 41 | 42 | np.testing.assert_allclose( 43 | s.coeffs.real, coeffs_real_expected, rtol=1e-6, atol=1e-7 44 | ) 45 | 46 | def test_cube_sht_invariants(self): 47 | from scipy.spatial.transform import Rotation 48 | 49 | facets = np.array( 50 | ((1, 0, 0), (0, 1, 0), (0, 0, 1), (-1, 0, 0), (0, -1, 0), (0, 0, -1)) 51 | ) 52 | 53 | facet_energies = np.ones(6) 54 | facet_energies[0] = 5 55 | 56 | l_max = 20 57 | s = WulffSHT(facets, facet_energies, l_max=l_max) 58 | invariants_expected = s.power_spectrum() 59 | 60 | for rot in Rotation.random(100): 61 | facets_r = facets @ rot.as_matrix() 62 | s = WulffSHT(facets_r, facet_energies, l_max=l_max) 63 | inv = s.power_spectrum() 64 | np.testing.assert_allclose(inv, invariants_expected, rtol=1e-3, atol=1e-1) 65 | -------------------------------------------------------------------------------- /src/chmpy/tests/exe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/exe/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/ext/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/ext/test_eem.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy import Molecule 6 | from chmpy.ext.charges import EEM 7 | 8 | from .. import TEST_FILES 9 | 10 | 11 | class EEMTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.eem = EEM() 14 | self.water = Molecule.load(TEST_FILES["water.xyz"]) 15 | 16 | def test_calculate_charges(self): 17 | charges = self.eem.calculate_charges(self.water) 18 | np.testing.assert_allclose(charges, [-0.64, 0.32, 0.32], atol=1e-2, rtol=1e-2) 19 | -------------------------------------------------------------------------------- /src/chmpy/tests/ext/test_elastic_tensor.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from chmpy.ext.elastic_tensor import ElasticTensor 4 | 5 | TENSOR = """ 6 | 48.137 11.411 12.783 0.000 -3.654 0.000 7 | 11.411 34.968 14.749 0.000 -0.094 0.000 8 | 12.783 14.749 26.015 0.000 -4.528 0.000 9 | 0.000 0.000 0.000 14.545 0.000 0.006 10 | -3.654 -0.094 -4.528 0.000 10.771 0.000 11 | 0.000 0.000 0.000 0.006 0.000 11.947 12 | """ 13 | 14 | 15 | class ElasticTensorTestCase(unittest.TestCase): 16 | def setUp(self): 17 | self.elastic = ElasticTensor.from_string(TENSOR) 18 | 19 | def test_youngs_modulus(self): 20 | ym = self.elastic.youngs_modulus([[0, 0, 1]]) 21 | self.assertAlmostEqual(ym[0], 16.95754579335107) 22 | 23 | def test_linear_compressibility(self): 24 | lc = self.elastic.linear_compressibility([[0, 0, 1]]) 25 | self.assertAlmostEqual(lc[0], 28.214314777188065) 26 | -------------------------------------------------------------------------------- /src/chmpy/tests/fmt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/fmt/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/fmt/test_gen.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy.fmt.gen import parse_gen_file 6 | 7 | from .. import TEST_FILES 8 | 9 | 10 | class GenFileTestCase(unittest.TestCase): 11 | def test_parse_file(self): 12 | els, pos, vecs, frac = parse_gen_file(TEST_FILES["example.gen"]) 13 | self.assertEqual(len(els), 76) 14 | self.assertEqual(els[0].atomic_number, 1) 15 | self.assertEqual(els[75].atomic_number, 8) 16 | 17 | expected_coords_46 = np.array((0.3623979304, 0.6637420302, 0.8677686357)) 18 | np.testing.assert_allclose(pos[46, :], expected_coords_46, atol=1e-5) 19 | 20 | np.testing.assert_allclose(np.zeros(3), vecs[0, :]) 21 | 22 | self.assertAlmostEqual(vecs[1, 0], 13.171) 23 | -------------------------------------------------------------------------------- /src/chmpy/tests/fmt/test_smiles.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | from chmpy.fmt.smiles import parse 5 | 6 | LOG = logging.getLogger(__name__) 7 | _WATER = "O" 8 | _CUBANE = "C12C3C4C1C5C4C3C25" 9 | _PYRIDINE = "c1cnccc1" 10 | 11 | 12 | class SMILESParserTest(unittest.TestCase): 13 | def test_parse_valid(self): 14 | from pyparsing import ParseException 15 | 16 | with self.assertRaises(ParseException): 17 | parse("invalid") 18 | parse(_WATER) 19 | parse(_PYRIDINE) 20 | 21 | def test_cubane_bonds(self): 22 | atoms, bonds = parse(_CUBANE) 23 | self.assertEqual(len(bonds), 12) 24 | -------------------------------------------------------------------------------- /src/chmpy/tests/promolecule/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/promolecule/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/promolecule/test_density.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy import PromoleculeDensity, StockholderWeight 6 | 7 | from .. import TEST_FILES 8 | 9 | 10 | class PromoleculeDensityTestCase(unittest.TestCase): 11 | pos = np.array([(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]) 12 | els = np.ones(2, dtype=int) 13 | 14 | def setUp(self): 15 | self.dens = PromoleculeDensity((self.els, self.pos)) 16 | 17 | def test_construction(self): 18 | self.assertEqual(self.dens.natoms, 2) 19 | with self.assertRaises(ValueError): 20 | _ = PromoleculeDensity((np.ones(2) * 300, self.pos)) 21 | 22 | def test_rho(self): 23 | pts = np.array(self.pos) + (1.0, 0.0, 0.0) 24 | rho = self.dens.rho(pts) 25 | # this will only be true if the interpolator data remains the same 26 | expected = np.array((0.375174, 0.005646)) 27 | np.testing.assert_allclose(rho, expected, atol=1e-5) 28 | 29 | def test_bb(self): 30 | bbox = self.dens.bb() 31 | expected = np.array(((-4.89, -4.89, -4.89), (5.89, 4.89, 4.89))) 32 | np.testing.assert_allclose(bbox, expected, atol=1e-5) 33 | 34 | def test_d_norm(self): 35 | pts = np.array(self.pos) + (1.0, 0.0, 0.0) 36 | d, d_norm, vecs = self.dens.d_norm(pts) 37 | expected = np.array((-1.0, -0.082569)) 38 | expected_d = np.array((0.0, 1.0)) 39 | expected_vecs = np.array(((-0.917431, 0, 0), (-1.834862, 0, 0))) 40 | np.testing.assert_allclose(d_norm, expected, atol=1e-5) 41 | np.testing.assert_allclose(d, expected_d, atol=1e-5) 42 | np.testing.assert_allclose(vecs, expected_vecs, atol=1e-5) 43 | 44 | def test_from_xyz_file(self): 45 | _ = PromoleculeDensity.from_xyz_file(TEST_FILES["water.xyz"]) 46 | 47 | 48 | class StockholderWeightTestCase(unittest.TestCase): 49 | pos = np.array([(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]) 50 | els = np.ones(2, dtype=int) 51 | 52 | def setUp(self): 53 | self.stock = StockholderWeight( 54 | PromoleculeDensity((self.els[:1], self.pos[:1, :])), 55 | PromoleculeDensity((self.els[1:], self.pos[1:, :])), 56 | ) 57 | 58 | def test_construction(self): 59 | np.testing.assert_allclose(self.stock.positions, self.pos) 60 | np.testing.assert_allclose(self.stock.vdw_radii, [1.09, 1.09]) 61 | self.stock = StockholderWeight.from_arrays( 62 | self.els[:1], self.pos[:1, :], self.els[1:], self.pos[1:, :] 63 | ) 64 | np.testing.assert_allclose(self.stock.positions, self.pos) 65 | np.testing.assert_allclose(self.stock.vdw_radii, [1.09, 1.09]) 66 | 67 | def test_weights(self): 68 | pts = np.array(((0.5, 0.0, 0.0), (0.5, 1.0, 0.0), (0.5, -1.0, 0.0))) 69 | np.testing.assert_allclose(self.stock.weights(pts), 0.5) 70 | 71 | def test_d_norm(self): 72 | pts = np.array(((0.5, 0.0, 0.0), (0.5, 1.0, 0.0), (0.5, -1.0, 0.0))) 73 | d_a, d_b, d_norm_a, d_norm_b, dp, angles = self.stock.d_norm(pts) 74 | expected_d_norm = (-0.541284, 0.025719, 0.025719) 75 | expected_d = (0.5, 1.118034, 1.118034) 76 | np.testing.assert_allclose(d_norm_a, expected_d_norm, atol=1e-5) 77 | np.testing.assert_allclose(d_norm_b, expected_d_norm, atol=1e-5) 78 | np.testing.assert_allclose(d_a, expected_d, atol=1e-5) 79 | np.testing.assert_allclose(d_b, expected_d, atol=1e-5) 80 | 81 | def test_from_xyz_files(self): 82 | stock = StockholderWeight.from_xyz_files( 83 | TEST_FILES["water.xyz"], TEST_FILES["water.xyz"] 84 | ) 85 | pts = np.array( 86 | ( 87 | (-0.7021961, -0.0560603, 0.0099423), 88 | (-1.0221932, 0.8467758, -0.0114887), 89 | (0.2575211, 0.0421215, 0.0052190), 90 | ) 91 | ) 92 | pts = np.vstack((pts, pts)) 93 | np.testing.assert_allclose(stock.positions, pts) 94 | np.testing.assert_allclose(stock.vdw_radii, [1.52, 1.09, 1.09] * 2) 95 | 96 | def test_bb(self): 97 | bbox = self.stock.bb() 98 | expected = np.array(((-4.89, -4.89, -4.89), (4.89, 4.89, 4.89))) 99 | np.testing.assert_allclose(bbox, expected, atol=1e-5) 100 | -------------------------------------------------------------------------------- /src/chmpy/tests/promolecule/test_surface.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from os.path import join 3 | from tempfile import TemporaryDirectory 4 | 5 | from chmpy.crystal import Crystal 6 | from chmpy.util.mesh import save_mesh 7 | 8 | from .. import TEST_FILES 9 | 10 | 11 | class SurfaceTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.acetic_acid = Crystal.load(TEST_FILES["acetic_acid.cif"]) 14 | 15 | def test_promolecule_surfaces(self): 16 | _ = self.acetic_acid.promolecule_density_isosurfaces(separation=1.0) 17 | 18 | def test_hirshfeld_surfaces(self): 19 | _ = self.acetic_acid.stockholder_weight_isosurfaces(separation=1.0, radius=3.8) 20 | _ = self.acetic_acid.hirshfeld_surfaces(separation=1.0, radius=3.8, kind="atom") 21 | 22 | def test_save(self): 23 | with TemporaryDirectory() as tmpdirname: 24 | surfaces = self.acetic_acid.promolecule_density_isosurfaces(separation=1.0) 25 | save_mesh(surfaces[0], join(tmpdirname, "tmp.ply")) 26 | -------------------------------------------------------------------------------- /src/chmpy/tests/sampling/test_quasirandom.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy.sampling import ( 6 | quasirandom_kgf as kgf, 7 | ) 8 | from chmpy.sampling import ( 9 | quasirandom_kgf_batch as kgf_b, 10 | ) 11 | from chmpy.sampling import ( 12 | quasirandom_sobol as sobol, 13 | ) 14 | from chmpy.sampling import ( 15 | quasirandom_sobol_batch as sobol_b, 16 | ) 17 | 18 | 19 | class QuasirandomTestCase(unittest.TestCase): 20 | def test_batch_single_equivalent(self): 21 | start, end = 1, 1000 22 | for dims in range(1, 10): 23 | for single, batch in ((kgf, kgf_b), (sobol, sobol_b)): 24 | pts_single = np.vstack( 25 | [single(seed, dims) for seed in range(start, end + 1)] 26 | ) 27 | pts_batch = batch(start, end, dims) 28 | np.testing.assert_allclose(pts_single, pts_batch) 29 | 30 | def test_approx_circle_area(self): 31 | start, end = 1, 1000 32 | for method in (kgf_b, sobol_b): 33 | pts = method(start, end, 2) 34 | norms = np.linalg.norm(pts, axis=1) 35 | ratio_inside = np.sum(norms <= 1.0) / norms.shape[0] 36 | estimated_area = 4 * ratio_inside 37 | self.assertAlmostEqual(estimated_area, np.pi, places=2) 38 | -------------------------------------------------------------------------------- /src/chmpy/tests/shape/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/tests/shape/__init__.py -------------------------------------------------------------------------------- /src/chmpy/tests/shape/test_shape_descriptors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from chmpy.crystal import Crystal 6 | 7 | from .. import TEST_FILES 8 | 9 | 10 | class ShapeDescriptorTestCase(unittest.TestCase): 11 | def setUp(self): 12 | self.water = Crystal.load(TEST_FILES["iceII.cif"]) 13 | self.acetic = Crystal.load(TEST_FILES["acetic_acid.cif"]) 14 | 15 | def test_atomic_descriptors(self): 16 | desc = self.acetic.atomic_shape_descriptors(l_max=3, radius=3.8) 17 | self.assertEqual(desc.shape, (8, 8)) 18 | 19 | def test_molecular_descriptors(self): 20 | desc = self.acetic.molecular_shape_descriptors(l_max=3, radius=3.8) 21 | self.assertEqual(desc.shape, (1, 8)) 22 | 23 | def test_atom_group_shape_descriptors(self): 24 | desc = self.acetic.atom_group_shape_descriptors([0, 1, 2], l_max=3, radius=3.8) 25 | self.assertEqual(desc.shape, (8,)) 26 | 27 | def test_invariants(self): 28 | from chmpy.shape.shape_descriptors import make_invariants, make_N_invariants 29 | 30 | coeffs = np.random.rand(16).astype(np.complex128) 31 | inv = make_N_invariants(coeffs) 32 | self.assertEqual(len(inv), 4) 33 | 34 | coeffs = np.random.rand(26 * 26).astype(np.complex128) 35 | inv = make_invariants(25, coeffs) 36 | self.assertEqual(len(inv), 1038) 37 | -------------------------------------------------------------------------------- /src/chmpy/tests/shape/test_spherical_harmonics.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from chmpy.shape.spherical_harmonics import SphericalHarmonics 5 | 6 | 7 | class TestSphericalHarmonics(unittest.TestCase): 8 | def setUp(self): 9 | self.sph = SphericalHarmonics(4, True) 10 | 11 | def cmp(self, a, b, delta=1e-12): 12 | self.assertAlmostEqual(a.real, b.real, delta=delta) 13 | self.assertAlmostEqual(a.imag, b.imag, delta=delta) 14 | 15 | def test_angular(self): 16 | eval_result = self.sph.single_angular(math.pi / 4, math.pi / 4) 17 | self.cmp(eval_result[0], complex(0.28209479177387814, 0.0)) 18 | self.cmp(eval_result[1], complex(0.17274707473566775, -0.17274707473566772)) 19 | self.cmp(eval_result[2], complex(0.34549414947133544, 0)) 20 | self.cmp(eval_result[3], complex(-0.17274707473566775, -0.17274707473566772)) 21 | self.cmp(eval_result[4], complex(1.1826236627522426e-17, -0.19313710101159473)) 22 | self.cmp(eval_result[5], complex(0.27313710764801974, -0.2731371076480197)) 23 | self.cmp(eval_result[6], complex(0.15769578262626002, 0)) 24 | self.cmp(eval_result[7], complex(-0.2731371076480198, -0.27313710764801974)) 25 | self.cmp(eval_result[8], complex(1.1826236627522428e-17, 0.19313710101159476)) 26 | self.cmp(eval_result[9], complex(-0.104305955908196, -0.10430595590819601)) 27 | self.cmp(eval_result[10], complex(2.212486281755292e-17, -0.3613264303300692)) 28 | self.cmp(eval_result[11], complex(0.24238513808561293, -0.24238513808561288)) 29 | self.cmp(eval_result[12], complex(-0.13193775767639848, 0)) 30 | self.cmp(eval_result[13], complex(-0.24238513808561296, -0.24238513808561293)) 31 | self.cmp(eval_result[14], complex(2.2124862817552912e-17, 0.3613264303300691)) 32 | self.cmp(eval_result[15], complex(0.104305955908196, -0.10430595590819601)) 33 | self.cmp( 34 | eval_result[16], complex(-0.11063317311124561, -1.3548656133020197e-17) 35 | ) 36 | self.cmp(eval_result[17], complex(-0.22126634622249125, -0.2212663462224913)) 37 | self.cmp(eval_result[18], complex(2.5604553376501068e-17, -0.4181540897233056)) 38 | self.cmp(eval_result[19], complex(0.08363081794466115, -0.08363081794466114)) 39 | self.cmp(eval_result[20], complex(-0.34380302747441394, 0)) 40 | self.cmp(eval_result[21], complex(-0.08363081794466115, -0.08363081794466114)) 41 | self.cmp(eval_result[22], complex(2.5604553376501068e-17, 0.4181540897233056)) 42 | self.cmp(eval_result[23], complex(0.22126634622249125, -0.2212663462224913)) 43 | self.cmp(eval_result[24], complex(-0.11063317311124561, 1.3548656133020197e-17)) 44 | 45 | def test_cartesian(self): 46 | pos = [2.0, 0.0, 1.0] 47 | theta = 0.0 48 | phi = 0.0 49 | eval_result = self.sph.single_cartesian(*pos) 50 | eval_ang = self.sph.single_angular(theta, phi) 51 | for i in range(25): 52 | self.cmp(eval_result[i], eval_ang[i]) 53 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/DB09563.sdf: -------------------------------------------------------------------------------- 1 | 449688 2 | -OEChem-10061700103D 3 | 4 | 21 20 0 0 0 0 0 0 0999 V2000 5 | 2.8438 -0.2447 -0.0001 O 0 0 0 0 0 0 0 0 0 0 0 0 6 | -0.8815 -0.0256 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0 7 | 0.4860 -0.6925 0.0389 C 0 0 0 0 0 0 0 0 0 0 0 0 8 | -1.0018 0.8116 -1.2653 C -1 0 0 0 0 0 0 0 0 0 0 0 9 | -1.9665 -1.0928 0.0074 C 0 0 0 0 0 0 0 0 0 0 0 0 10 | -1.0437 0.8711 1.2189 C 0 0 0 0 0 0 0 0 0 0 0 0 11 | 1.5636 0.3730 0.0002 C 0 0 0 0 0 0 0 0 0 0 0 0 12 | 0.5503 -1.2977 0.9513 H 0 0 0 0 0 0 0 0 0 0 0 0 13 | 0.5649 -1.3696 -0.8202 H 0 0 0 0 0 0 0 0 0 0 0 0 14 | -0.6981 0.2022 -2.1215 H 0 0 0 0 0 0 0 0 0 0 0 0 15 | -2.0502 1.1084 -1.3741 H 0 0 0 0 0 0 0 0 0 0 0 0 16 | -0.3944 1.7160 -1.1807 H 0 0 0 0 0 0 0 0 0 0 0 0 17 | -1.8610 -1.6856 0.9208 H 0 0 0 0 0 0 0 0 0 0 0 0 18 | -2.9431 -0.6003 -0.0166 H 0 0 0 0 0 0 0 0 0 0 0 0 19 | -1.8360 -1.7236 -0.8770 H 0 0 0 0 0 0 0 0 0 0 0 0 20 | -0.7933 0.2953 2.1147 H 0 0 0 0 0 0 0 0 0 0 0 0 21 | -0.4097 1.7559 1.1247 H 0 0 0 0 0 0 0 0 0 0 0 0 22 | -2.0891 1.1937 1.2633 H 0 0 0 0 0 0 0 0 0 0 0 0 23 | 1.5343 0.9794 -0.9066 H 0 0 0 0 0 0 0 0 0 0 0 0 24 | 1.5448 1.0391 0.8656 H 0 0 0 0 0 0 0 0 0 0 0 0 25 | 2.9121 -0.7787 0.8097 H 0 0 0 0 0 0 0 0 0 0 0 0 26 | 1 7 1 0 0 0 0 27 | 1 21 1 0 0 0 0 28 | 2 3 1 0 0 0 0 29 | 2 4 1 0 0 0 0 30 | 2 5 1 0 0 0 0 31 | 2 6 1 0 0 0 0 32 | 3 7 1 0 0 0 0 33 | 3 8 1 0 0 0 0 34 | 3 9 1 0 0 0 0 35 | 4 10 1 0 0 0 0 36 | 4 11 1 0 0 0 0 37 | 4 12 1 0 0 0 0 38 | 5 13 1 0 0 0 0 39 | 5 14 1 0 0 0 0 40 | 5 15 1 0 0 0 0 41 | 6 16 1 0 0 0 0 42 | 6 17 1 0 0 0 0 43 | 6 18 1 0 0 0 0 44 | 7 19 1 0 0 0 0 45 | 7 20 1 0 0 0 0 46 | M CHG 1 2 1 47 | M ISO 1 4 11 48 | M END 49 | > 50 | DB09563 51 | 52 | > 53 | drugbank 54 | 55 | > 56 | PUBCHEM 57 | 58 | > 59 | https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/inchikey/OEYIOHPDSNJKLS-BJUDXGSMSA-N/SDF?record_type=3d 60 | 61 | > 62 | C[N+](C)([11CH3])CCO 63 | 64 | > 65 | InChI=1S/C5H14NO/c1-6(2,3)4-5-7/h7H,4-5H2,1-3H3/q+1/i1-1 66 | 67 | > 68 | OEYIOHPDSNJKLS-BJUDXGSMSA-N 69 | 70 | > 71 | C5H14NO 72 | 73 | > 74 | 103.173 75 | 76 | > 77 | 103.118424313 78 | 79 | > 80 | 1 81 | 82 | > 83 | 21 84 | 85 | > 86 | 12.565789785108525 87 | 88 | > 89 | 1 90 | 91 | > 92 | 1 93 | 94 | > 95 | 1 96 | 97 | > 98 | 0 99 | 100 | > 101 | (2-hydroxyethyl)((11C)methyl)dimethylazanium 102 | 103 | > 104 | -3.59 105 | 106 | > 107 | -4.662269162805079 108 | 109 | > 110 | -1.59 111 | 112 | > 113 | 0 114 | 115 | > 116 | 0 117 | 118 | > 119 | 1 120 | 121 | > 122 | 13.968714076810006 123 | 124 | > 125 | -3.249884462803304 126 | 127 | > 128 | 20.23 129 | 130 | > 131 | 42.194 132 | 133 | > 134 | 2 135 | 136 | > 137 | 1 138 | 139 | > 140 | 3.61e+00 g/l 141 | 142 | > 143 | (2-hydroxyethyl)((11C)methyl)dimethylazanium 144 | 145 | > 146 | 1 147 | 148 | > 149 | DB09563 150 | 151 | > 152 | approved 153 | 154 | > 155 | Choline C-11 156 | 157 | > 158 | Choline C 11; Mic B12 159 | 160 | 161 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/HXACAN01.pdb: -------------------------------------------------------------------------------- 1 | HEADER CSD ENTRY HXACAN01 2 | CRYST1 12.9300 9.4000 7.1000 90.00 115.90 90.00 P21/a 3 | SCALE1 0.077340 0.000000 0.037554 0.000000 4 | SCALE2 0.000000 0.106383 0.000000 0.000000 5 | SCALE3 0.000000 0.000000 0.156571 0.000000 6 | HETATM 1 C1 UNK 1 -0.319 3.334 -0.970 1.00 0.00 C 7 | HETATM 2 C2 UNK 1 0.941 3.348 -1.560 1.00 0.00 C 8 | HETATM 3 C3 UNK 1 1.942 2.497 -1.099 1.00 0.00 C 9 | HETATM 4 C4 UNK 1 1.689 1.636 -0.044 1.00 0.00 C 10 | HETATM 5 C5 UNK 1 0.437 1.639 0.567 1.00 0.00 C 11 | HETATM 6 C6 UNK 1 -0.550 2.483 0.102 1.00 0.00 C 12 | HETATM 7 C7 UNK 1 -1.598 4.763 -2.557 1.00 0.00 C 13 | HETATM 8 C8 UNK 1 -2.885 5.542 -2.667 1.00 0.00 C 14 | HETATM 9 H1 UNK 1 1.101 3.957 -2.242 1.00 0.00 H 15 | HETATM 10 H2 UNK 1 2.847 2.557 -1.469 1.00 0.00 H 16 | HETATM 11 H3 UNK 1 0.272 1.062 1.303 1.00 0.00 H 17 | HETATM 12 H4 UNK 1 -1.422 2.482 0.505 1.00 0.00 H 18 | HETATM 13 H5 UNK 1 2.520 0.517 1.175 1.00 0.00 H 19 | HETATM 14 H6 UNK 1 -2.105 4.239 -0.805 1.00 0.00 H 20 | HETATM 15 H7 UNK 1 -3.215 5.480 -3.551 1.00 0.00 H 21 | HETATM 16 H8 UNK 1 -3.317 5.574 -1.903 1.00 0.00 H 22 | HETATM 17 H9 UNK 1 -2.648 6.589 -2.695 1.00 0.00 H 23 | HETATM 18 N1 UNK 1 -1.402 4.170 -1.370 1.00 0.00 N 24 | HETATM 19 O1 UNK 1 2.707 0.800 0.358 1.00 0.00 O 25 | HETATM 20 O2 UNK 1 -0.794 4.678 -3.486 1.00 0.00 O 26 | CONECT 1 2 6 18 27 | CONECT 2 1 3 9 28 | CONECT 3 2 4 10 29 | CONECT 4 3 5 19 30 | CONECT 5 4 6 11 31 | CONECT 6 1 5 12 32 | CONECT 7 8 18 20 33 | CONECT 8 7 15 16 17 34 | CONECT 9 2 35 | CONECT 10 3 36 | CONECT 11 5 37 | CONECT 12 6 38 | CONECT 13 19 39 | CONECT 14 18 40 | CONECT 15 8 41 | CONECT 16 8 42 | CONECT 17 8 43 | CONECT 18 1 7 14 44 | CONECT 19 4 13 45 | CONECT 20 7 46 | MASTER 0 0 0 0 0 0 0 3 20 0 20 0 47 | END 48 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/acetic_acid.cif: -------------------------------------------------------------------------------- 1 | data_acetic_acid 2 | _symmetry_Int_Tables_number 33 3 | loop_ 4 | _symmetry_equiv_pos_site_id 5 | _symmetry_equiv_pos_as_xyz 6 | 1 x,y,z 7 | 2 1/2-x,1/2+y,1/2+z 8 | 3 1/2+x,1/2-y,z 9 | 4 -x,-y,1/2+z 10 | _cell_length_a 13.31 11 | _cell_length_b 4.1 12 | _cell_length_c 5.75 13 | _cell_angle_alpha 90 14 | _cell_angle_beta 90 15 | _cell_angle_gamma 90 16 | _cell_volume 314.05 17 | loop_ 18 | _atom_site_label 19 | _atom_site_type_symbol 20 | _atom_site_fract_x 21 | _atom_site_fract_y 22 | _atom_site_fract_z 23 | C1 C 0.16510 0.28580 0.17090 24 | C2 C 0.08940 0.37620 0.34810 25 | H1 H 0.18200 0.05100 -0.11600 26 | H2 H 0.12800 0.51000 0.49100 27 | H3 H 0.03300 0.54000 0.27900 28 | H4 H 0.05300 0.16800 0.42100 29 | O1 O 0.12870 0.10750 0.00000 30 | O2 O 0.25290 0.37030 0.17690 31 | #END 32 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/acetic_acid.res: -------------------------------------------------------------------------------- 1 | TITL acetic_acid 2 | CELL 0.71073 13.31 4.09 5.769 90 90 90 3 | ZERR 4 0.001 0.001 0.001 0 0 0 4 | 5 | LATT -1 6 | SYMM 1/2-x,1/2+y,1/2+z 7 | SYMM 1/2+x,1/2-y,z 8 | SYMM -x,-y,1/2+z 9 | SFAC C H O 10 | UNIT 8 16 8 11 | FVAR 1.00 12 | C1 1 0.165100 0.285800 0.170900 1.0 13 | C2 1 0.089400 0.376200 0.348100 1.0 14 | H1 2 0.182000 0.051000 -0.116000 1.0 15 | H2 2 0.128000 0.510000 0.491000 1.0 16 | H3 2 0.033000 0.540000 0.279000 1.0 17 | H4 2 0.053000 0.168000 0.421000 1.0 18 | O1 3 0.128700 0.107500 0.000000 1.0 19 | O2 3 0.252900 0.370300 0.176900 1.0 20 | END 21 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/iceII.cif: -------------------------------------------------------------------------------- 1 | # ice II cif for testing purposes 2 | data_iceii 3 | _audit_creation_method 'testing purposes' 4 | _cell_length_a 7.78 5 | _cell_length_b 7.78 6 | _cell_length_c 7.78 7 | _cell_angle_alpha 113.1 8 | _cell_angle_beta 113.1 9 | _cell_angle_gamma 113.1 10 | _unnecessary_quoted_block 11 | ; 12 | this is a quoted block 13 | ; 14 | loop_ 15 | _symmetry_equiv_pos_site_id 16 | _symmetry_equiv_pos_as_xyz 17 | 1 +x,+y,+z 18 | loop_ 19 | _atom_site_label 20 | _atom_site_type_symbol 21 | _atom_site_fract_x 22 | _atom_site_fract_y 23 | _atom_site_fract_z 24 | _atom_site_occupancy 25 | O1 O 0.273328954083 0.026479033257 0.855073668062 1.000000000000 26 | H1 H 0.152000330304 0.043488909374 0.793595454907 1.000000000000 27 | H2 H 0.420775085827 0.191165194485 0.996362203192 1.000000000000 28 | O2 O 0.144924657237 0.726669877048 0.973520141937 1.000000000000 29 | H3 H 0.206402797363 0.847998481439 0.956510183901 1.000000000000 30 | H4 H 0.003636687868 0.579223433079 0.808833958746 1.000000000000 31 | O3 O 0.026477491142 0.855072949204 0.273328854276 1.000000000000 32 | H5 H 0.043487719387 0.793594459529 0.152000553858 1.000000000000 33 | H6 H 0.191163388489 0.996362120061 0.420774953988 1.000000000000 34 | O4 O 0.726670757782 0.973520932681 0.144926297633 1.000000000000 35 | H7 H 0.847999275418 0.956510882297 0.206404294889 1.000000000000 36 | H8 H 0.579224602173 0.808834869258 0.003637530197 1.000000000000 37 | O5 O 0.855073412561 0.273329597478 0.026478702027 1.000000000000 38 | H9 H 0.793594909621 0.152000771295 0.043488316376 1.000000000000 39 | H10 H 0.996362312075 0.420775512814 0.191164826329 1.000000000000 40 | O6 O 0.973520717390 0.144925628579 0.726671054509 1.000000000000 41 | H11 H 0.956510600982 0.206403626100 0.847999547813 1.000000000000 42 | H12 H 0.808834607385 0.003637609551 0.579224562315 1.000000000000 43 | O7 O 0.477029330652 0.749805220756 0.331717174202 1.000000000000 44 | H13 H 0.402360172390 0.720795433576 0.401054786853 1.000000000000 45 | H14 H 0.368036378343 0.742284933413 0.207434128329 1.000000000000 46 | O8 O 0.668282055550 0.522969467265 0.250193622013 1.000000000000 47 | H15 H 0.598945169999 0.597639203188 0.279204514235 1.000000000000 48 | H16 H 0.792565160978 0.631962548905 0.257714022497 1.000000000000 49 | O9 O 0.749805496250 0.331717033025 0.477029827575 1.000000000000 50 | H17 H 0.720795009402 0.401054437437 0.402360618546 1.000000000000 51 | H18 H 0.742284706875 0.207433751728 0.368036342085 1.000000000000 52 | O10 O 0.522969071341 0.250193392512 0.668282780114 1.000000000000 53 | H19 H 0.597638176364 0.279203622225 0.598945231951 1.000000000000 54 | H20 H 0.631962932785 0.257715003205 0.792566578018 1.000000000000 55 | O11 O 0.331715381178 0.477028907327 0.749804544234 1.000000000000 56 | H21 H 0.401053887354 0.402360576463 0.720795552111 1.000000000000 57 | H22 H 0.207432480540 0.368035542438 0.742284142147 1.000000000000 58 | O12 O 0.250193225247 0.668282913065 0.522970147212 1.000000000000 59 | H23 H 0.279203658434 0.598945325854 0.597639149965 1.000000000000 60 | H24 H 0.257715011998 0.792566781760 0.631964289620 1.000000000000 61 | #END 62 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/r3c_example.cif: -------------------------------------------------------------------------------- 1 | data_r3c 2 | _space_group_IT_number 161 3 | _symmetry_space_group_name_Hall 'R 3 -2"c' 4 | _symmetry_space_group_name_H-M 'R 3 c :H' 5 | _symmetry_cell_setting trigonal 6 | _cell_angle_alpha 90.00 7 | _cell_angle_beta 90.00 8 | _cell_angle_gamma 120.00 9 | _cell_formula_units_Z 18 10 | _cell_length_a 34.4501(4) 11 | _cell_length_b 34.4501(4) 12 | _cell_length_c 11.2367(3) 13 | _cell_volume 11549.2(4) 14 | loop_ 15 | _symmetry_equiv_pos_as_xyz 16 | 'x,y,z' 17 | '-y,x-y,z' 18 | '-x+y,-x, z' 19 | '-y,-x,z+1/2' 20 | '-x+y,y,z+1/2' 21 | 'x,x-y,z+1/2' 22 | 'x+2/3,y+1/3,z+1/3' 23 | '-y+2/3,x-y+1/3,z+1/3' 24 | '-x+y+2/3,-x+1/3,z+1/3' 25 | '-y+2/3,-x+1/3,z+5/6' 26 | '-x+y+2/3,y+1/3,z+5/6' 27 | 'x+2/3,x-y+1/3,z+5/6' 28 | 'x+1/3,y+2/3,z+2/3' 29 | '-y+1/3,x-y+2/3,z+2/3' 30 | '-x+y+1/3,-x+2/3,z+2/3' 31 | '-y+1/3,-x+2/3,z+1/6' 32 | '-x+y+1/3,y+2/3,z+1/6' 33 | 'x+1/3,x-y+2/3,z+1/6' 34 | loop_ 35 | _atom_site_label 36 | _atom_site_type_symbol 37 | _atom_site_fract_x 38 | _atom_site_fract_y 39 | _atom_site_fract_z 40 | _atom_site_U_iso_or_equiv 41 | _atom_site_adp_type 42 | _atom_site_occupancy 43 | _atom_site_symmetry_multiplicity 44 | _atom_site_calc_flag 45 | _atom_site_refinement_flags 46 | O1 O 0.04836(4) 0.88495(4) 0.15385(10) 0.0506(3) Uani 1 1 d . 47 | O2 O 0.03029(4) 0.81300(4) 0.13701(9) 0.0446(3) Uani 1 1 d . 48 | O3 O -0.01890(4) 0.78645(5) -0.13902(10) 0.0630(4) Uani 1 1 d . 49 | O4 O 0.08378(5) 0.84576(5) -0.13789(12) 0.0668(4) Uani 1 1 d . 50 | C1 C 0.10339(8) 0.77846(9) -0.0899(2) 0.0809(7) Uani 1 1 d . 51 | H1A H 0.1246 0.8034 -0.1317 0.097 Uiso 1 1 calc R 52 | C2 C 0.10903(14) 0.74234(14) -0.0757(3) 0.1207(11) Uani 1 1 d . 53 | H2A H 0.1344 0.7429 -0.1066 0.145 Uiso 1 1 calc R 54 | C3 C 0.07784(16) 0.70537(14) -0.0165(3) 0.1238(12) Uani 1 1 d . 55 | H3A H 0.0820 0.6808 -0.0076 0.149 Uiso 1 1 calc R 56 | C4 C 0.04034(12) 0.70393(9) 0.0302(3) 0.1081(10) Uani 1 1 d . 57 | H4A H 0.0189 0.6784 0.0695 0.130 Uiso 1 1 calc R 58 | C5 C 0.03479(9) 0.74081(7) 0.0182(2) 0.0754(6) Uani 1 1 d . 59 | H5A H 0.0098 0.7404 0.0512 0.090 Uiso 1 1 calc R 60 | C6 C 0.06622(7) 0.77821(6) -0.04252(15) 0.0545(4) Uani 1 1 d . 61 | C7 C -0.09894(6) 0.75491(7) -0.01827(18) 0.0622(5) Uani 1 1 d . 62 | H7A H -0.0991 0.7406 -0.0886 0.075 Uiso 1 1 calc R 63 | C8 C -0.13881(7) 0.74370(8) 0.0368(2) 0.0787(6) Uani 1 1 d . 64 | H8A H -0.1657 0.7213 0.0049 0.094 Uiso 1 1 calc R 65 | C9 C -0.13901(7) 0.76555(8) 0.1392(2) 0.0744(6) Uani 1 1 d . 66 | H9A H -0.1660 0.7585 0.1754 0.089 Uiso 1 1 calc R 67 | C10 C -0.09938(7) 0.79770(7) 0.18760(19) 0.0631(5) Uani 1 1 d . 68 | H10A H -0.0995 0.8123 0.2570 0.076 Uiso 1 1 calc R 69 | C11 C -0.05930(6) 0.80843(5) 0.13398(15) 0.0489(4) Uani 1 1 d . 70 | H11A H -0.0325 0.8300 0.1681 0.059 Uiso 1 1 calc R 71 | C12 C -0.05847(5) 0.78736(5) 0.02978(14) 0.0433(4) Uani 1 1 d . 72 | C13 C 0.02230(7) 0.89795(6) -0.15686(16) 0.0574(5) Uani 1 1 d . 73 | H13A H 0.0386 0.8876 -0.1990 0.069 Uiso 1 1 calc R 74 | C14 C -0.00124(8) 0.91433(7) -0.21755(18) 0.0697(6) Uani 1 1 d . 75 | H14A H -0.0008 0.9149 -0.3003 0.084 Uiso 1 1 calc R 76 | C15 C -0.02520(7) 0.92978(7) -0.1567(2) 0.0691(6) Uani 1 1 d . 77 | H15A H -0.0414 0.9405 -0.1978 0.083 Uiso 1 1 calc R 78 | C16 C -0.02521(7) 0.92934(7) -0.0352(2) 0.0692(6) Uani 1 1 d . 79 | H16A H -0.0412 0.9401 0.0064 0.083 Uiso 1 1 calc R 80 | C17 C -0.00173(7) 0.91301(6) 0.02626(18) 0.0580(5) Uani 1 1 d . 81 | H17A H -0.0018 0.9130 0.1090 0.070 Uiso 1 1 calc R 82 | C18 C 0.02186(5) 0.89670(5) -0.03423(14) 0.0440(4) Uani 1 1 d . 83 | C19 C 0.03020(5) 0.82653(5) 0.01748(13) 0.0415(4) Uani 1 1 d . 84 | C20 C 0.04791(5) 0.87844(5) 0.02849(14) 0.0449(4) Uani 1 1 d . 85 | H20A H 0.0789 0.8947 -0.0003 0.054 Uiso 1 1 calc R 86 | C21 C 0.05792(6) 0.85329(5) 0.20332(14) 0.0475(4) Uani 1 1 d . 87 | H21A H 0.0897 0.8628 0.1922 0.057 Uiso 1 1 calc R 88 | C22 C 0.04566(6) 0.84468(6) 0.33249(15) 0.0495(4) Uani 1 1 d . 89 | C23 C 0.02136(7) 0.86133(6) 0.38556(16) 0.0602(5) Uani 1 1 d . 90 | H23A H 0.0129 0.8787 0.3414 0.072 Uiso 1 1 calc R 91 | C24 C 0.00899(8) 0.85255(7) 0.50563(19) 0.0773(6) Uani 1 1 d . 92 | H24A H -0.0077 0.8640 0.5402 0.093 Uiso 1 1 calc R 93 | C25 C 0.02093(9) 0.82800(9) 0.57036(18) 0.0805(7) Uani 1 1 d . 94 | H25A H 0.0127 0.8229 0.6502 0.097 Uiso 1 1 calc R 95 | C26 C 0.04553(7) 0.80968(8) 0.52140(17) 0.0704(6) Uani 1 1 d . 96 | C27 C 0.05829(6) 0.81769(7) 0.39910(16) 0.0581(5) Uani 1 1 d . 97 | C28 C 0.08167(8) 0.79785(9) 0.3497(2) 0.0840(7) Uani 1 1 d . 98 | H28A H 0.0906 0.8032 0.2704 0.101 Uiso 1 1 calc R 99 | C29 C 0.09135(10) 0.77094(13) 0.4168(3) 0.1240(12) Uani 1 1 d . 100 | H29A H 0.1067 0.7578 0.3834 0.149 Uiso 1 1 calc R 101 | C30 C 0.07809(12) 0.76298(13) 0.5378(3) 0.1318(13) Uani 1 1 d . 102 | H30A H 0.0845 0.7443 0.5831 0.158 Uiso 1 1 calc R 103 | C31 C 0.05668(10) 0.78187(12) 0.5875(3) 0.1061(10) Uani 1 1 d . 104 | H31A H 0.0489 0.7766 0.6675 0.127 Uiso 1 1 calc R 105 | C32 C -0.01694(6) 0.79883(5) -0.03737(14) 0.0436(4) Uani 1 1 d . 106 | C33 C 0.06230(6) 0.81848(6) -0.06197(14) 0.0481(4) Uani 1 1 d . 107 | -------------------------------------------------------------------------------- /src/chmpy/tests/test_files/water.xyz: -------------------------------------------------------------------------------- 1 | 3 2 | 0 1 3 | O -0.7021961 -0.0560603 0.0099423 4 | H -1.0221932 0.8467758 -0.0114887 5 | H 0.2575211 0.0421215 0.0052190 6 | 7 | -------------------------------------------------------------------------------- /src/chmpy/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .unit import units 2 | 3 | __all__ = ["path", "unit", "units"] 4 | -------------------------------------------------------------------------------- /src/chmpy/util/color.py: -------------------------------------------------------------------------------- 1 | # TODO add LinearSegmentedColormap objects for other 2 | # CrystalExplorer default colors 3 | DEFAULT_COLORMAPS = { 4 | "d_norm": "bwr_r", 5 | "d_e": "viridis_r", 6 | "d_i": "viridis_r", 7 | "d_norm_i": "bwr", 8 | "d_norm_e": "bwr_r", 9 | "esp": "coolwarm_r", 10 | "fragment_patch": "tab20", 11 | } 12 | 13 | 14 | def property_to_color(prop, cmap="viridis", **kwargs): 15 | """ 16 | Convert a scalar array of property values to colors, 17 | given a provided color map (or property name). 18 | 19 | Args: 20 | prop (array_like): the scalar array of property values 21 | cmap (str): the color map name or property name 22 | vmin (float): the minimum value of the property color scale 23 | vmax (float): the maximum value of the property color scale 24 | kwargs (dict): optional keyword arguments 25 | 26 | Returns: 27 | array_like: the array of color values for the given property 28 | """ 29 | from matplotlib import colormaps 30 | 31 | colormap = colormaps[kwargs.get("colormap", DEFAULT_COLORMAPS.get(cmap, cmap))] 32 | 33 | norm = None 34 | vmin = kwargs.get("vmin", prop.min()) 35 | vmax = kwargs.get("vmax", prop.max()) 36 | midpoint = kwargs.get( 37 | "midpoint", 38 | max(min(0.0, vmax - 0.01), vmin + 0.01) if cmap in ("d_norm", "esp") else None, 39 | ) 40 | if midpoint is not None: 41 | try: 42 | from matplotlib.colors import TwoSlopeNorm 43 | except ImportError: 44 | from matplotlib.colors import DivergingNorm as TwoSlopeNorm 45 | assert vmin <= midpoint, f"vmin={vmin} midpoint={midpoint}" 46 | assert vmax >= midpoint, f"vmin={vmax} midpoint={midpoint}" 47 | norm = TwoSlopeNorm(vmin=vmin, vcenter=midpoint, vmax=vmax) 48 | prop = norm(prop) 49 | return colormap(prop) 50 | else: 51 | import numpy as np 52 | 53 | prop = np.clip(prop, vmin, vmax) - vmin 54 | return colormap(prop) 55 | -------------------------------------------------------------------------------- /src/chmpy/util/dict.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info.major == 3 and sys.version_info.minor >= 10: 4 | from collections.abc import Mapping, MutableMapping 5 | else: 6 | from collections.abc import Mapping, MutableMapping 7 | 8 | 9 | def recursive_dict_update(dict_to, dict_from): 10 | """ 11 | Iterate through a dictionary , updating items inplace 12 | recursively from a second dictionary. 13 | 14 | Args: 15 | dict_to (dict): the first dictionary (to update) 16 | dict_from (dict): the second dictionary (to pull updates from) 17 | 18 | Returns: 19 | dict: the modified dict_to 20 | 21 | Examples: 22 | >>> d1 = {"test": {"test_val": 3}} 23 | >>> d2 = {"test": {"test_val": 5}, "other": 3} 24 | >>> recursive_dict_update(d1, d2) 25 | {'test': {'test_val': 5}, 'other': 3} 26 | """ 27 | for key, val in dict_from.items(): 28 | if isinstance(val, Mapping): 29 | dict_to[key] = recursive_dict_update(dict_to.get(key, {}), val) 30 | else: 31 | dict_to[key] = val 32 | return dict_to 33 | 34 | 35 | def nested_dict_delete(root, key, sep="."): 36 | """ 37 | Iterate through a dict, deleting items 38 | recursively based on a key. 39 | 40 | Args: 41 | root (dict): dictionary to remove an entry from 42 | key (str): the string used to locate the key to delete in the root dictionary 43 | sep (str): the separator for dictionary key items 44 | 45 | Returns: 46 | dict: the modified dict_to 47 | 48 | Examples: 49 | >>> d1 = {"test": {"test_val": 3}} 50 | >>> d2 = {"test": {"test_val": 5, "test_val_2": 7}, "other": 3} 51 | >>> nested_dict_delete(d1, "test.test_val") 52 | >>> d1 53 | {} 54 | >>> nested_dict_delete(d2, "test.test_val") 55 | >>> d2 56 | {'test': {'test_val_2': 7}, 'other': 3} 57 | """ 58 | 59 | levels = key.split(sep) 60 | level_key = levels[0] 61 | if level_key in root: 62 | if isinstance(root[level_key], MutableMapping): 63 | nested_dict_delete(root[level_key], sep.join(levels[1:]), sep=sep) 64 | if not root[level_key]: 65 | del root[level_key] 66 | else: 67 | del root[level_key] 68 | else: 69 | raise KeyError 70 | -------------------------------------------------------------------------------- /src/chmpy/util/exe.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | 5 | def is_executable(path): 6 | return os.path.isfile(path) and os.access(path, os.X_OK) 7 | 8 | 9 | def which(prog): 10 | fpath, fname = os.path.split(prog) 11 | if fpath and is_executable(fname): 12 | return prog 13 | else: 14 | for path in os.environ["PATH"].split(os.pathsep): 15 | exe_file = os.path.join(path, prog) 16 | if is_executable(exe_file): 17 | return exe_file 18 | return None 19 | 20 | 21 | def linux_version(): 22 | """Get the version of linux this system is running 23 | If it's not linux, return None 24 | """ 25 | s = platform.platform() 26 | if not s.startswith("Linux"): 27 | return None 28 | version_string = s.split("-")[1] 29 | return version_string 30 | 31 | 32 | def libc_version(): 33 | """Get the version of glibc this python was compiled with 34 | return None if we don't have any info on it 35 | """ 36 | try: 37 | version = platform.libc_ver()[1] 38 | except Exception: 39 | version = None 40 | return version 41 | -------------------------------------------------------------------------------- /src/chmpy/util/mesh.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOG = logging.getLogger(__name__) 4 | 5 | 6 | def save_mesh(mesh, filename): 7 | """ 8 | Save the given Trimesh to a file. 9 | 10 | Args: 11 | mesh (trimesh.Trimesh): The mesh to save. 12 | filename (str): The path to the destination file. 13 | """ 14 | ext = filename.split(".")[-1] 15 | with open(filename, "wb") as f: 16 | mesh.export(f, ext) 17 | 18 | LOG.debug("Saved mesh %s to %s", mesh, filename) 19 | 20 | 21 | def molecule_to_meshes(molecule, **kwargs): 22 | """ 23 | Convert the provided molecule into a list 24 | of trimesh Meshes representing the molecule 25 | either as van der Waals spheres or as a CPK 26 | representation. 27 | 28 | Args: 29 | molecule (Molecule): The molecule to represent 30 | kwargs (dict): Optional Keyword arguments 31 | 32 | Returns: 33 | list: a list of meshes representing atoms and (optionally) bonds 34 | """ 35 | 36 | import numpy as np 37 | from trimesh.creation import cylinder, icosphere 38 | 39 | representation = kwargs.get("representation", "ball_stick") 40 | base_sphere = icosphere(subdivisions=3) 41 | n_points = len(base_sphere.vertices) 42 | meshes = {} 43 | for atom_index, (el, pos) in enumerate(molecule): 44 | m = base_sphere.copy() 45 | m.apply_scale(getattr(el, f"{representation}_radius")) 46 | m.apply_translation(pos) 47 | m.visual.vertex_colors = np.repeat([el.color], n_points, axis=0) 48 | meshes[f"atom_{molecule.labels[atom_index]}"] = m 49 | LOG.debug("Add atom %d", atom_index) 50 | if representation == "ball_stick": 51 | bond_thickness = 0.12 52 | for bond_index, (a, b, d) in enumerate(molecule.unique_bonds): 53 | x1 = molecule.positions[a] 54 | x3 = molecule.positions[b] 55 | cyl = cylinder(bond_thickness, d, segment=(x1, x3)) 56 | cyl.visual.vertex_colors = np.repeat( 57 | [ 58 | (100, 100, 100, 255), 59 | ], 60 | cyl.vertices.shape[0], 61 | axis=0, 62 | ) 63 | bond_label = f"bond_{molecule.labels[a]}_{molecule.labels[b]}" 64 | LOG.debug("Add bond %d", bond_index) 65 | meshes[bond_label] = cyl 66 | return meshes 67 | 68 | 69 | def face_centroids(mesh): 70 | import numpy as np 71 | 72 | return np.sum(mesh.vertices[mesh.faces], axis=1) / mesh.faces.shape[1] 73 | 74 | 75 | def color_mesh(f, mesh, faces=False, **kwargs): 76 | from chmpy.util.color import property_to_color 77 | 78 | is_function = callable(f) 79 | 80 | if faces: 81 | if is_function: 82 | pts = face_centroids(mesh) 83 | prop = f(pts) 84 | else: 85 | prop = f 86 | mesh.visual.face_colors = property_to_color(prop, **kwargs) * 255 87 | else: 88 | if is_function: 89 | prop = f(mesh.vertices) 90 | else: 91 | prop = f 92 | mesh.visual.vertex_colors = property_to_color(prop, **kwargs) * 255 93 | -------------------------------------------------------------------------------- /src/chmpy/util/path.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | ERR_INVALID_NAME = 123 # windows specific error code 7 | 8 | 9 | def list_directory(pathname): 10 | return "\n".join( 11 | f"{str(p):<60s} {p.lstat().st_size:>20d}B" for p in Path(pathname).iterdir() 12 | ) 13 | 14 | 15 | def is_valid_pathname(pathname): 16 | """Return `True` if the passed string is a valid 17 | pathname for the current OS, `False` otherwise""" 18 | try: 19 | if not isinstance(pathname, str) or not pathname: 20 | return False 21 | 22 | # Strip the drivename on windows 23 | _, pathname = os.path.splitdrive(pathname) 24 | 25 | root_dirname = ( 26 | os.environ.get("HOMEDRIVE", "C:") 27 | if sys.platform == "win32" 28 | else os.path.sep 29 | ) 30 | assert os.path.isdir(root_dirname) 31 | 32 | root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep 33 | 34 | for pathname_part in pathname.split(os.path.sep): 35 | try: 36 | os.lstat(root_dirname + pathname_part) 37 | except OSError as e: 38 | if hasattr(e, "winerror"): 39 | if e.winerror == ERR_INVALID_NAME: 40 | return False 41 | elif e.errno in {errno.ENAMETOOLONG, errno.ERANGE}: 42 | return False 43 | 44 | except TypeError: 45 | return False 46 | 47 | return True 48 | 49 | 50 | def is_path_creatable(pathname): 51 | """Return `True` if we have permissions to create 52 | the given pathname, `False` otherwise""" 53 | dirname = os.path.dirname(pathname) or os.getcwd() 54 | return os.access(dirname, os.W_OK) 55 | 56 | 57 | def path_exists_or_is_creatable(pathname): 58 | """Return `True` if the given pathname is valid for 59 | the current OS and currently exists or is createable 60 | by the current user, `False` otherwise.""" 61 | try: 62 | return is_valid_pathname(pathname) and ( 63 | os.path.exists(pathname) or is_path_creatable(pathname) 64 | ) 65 | except OSError: 66 | return False 67 | 68 | 69 | def dir_exists_or_is_creatable(pathname): 70 | """Return `True` if the given pathname is valid for 71 | the current OS and currently exists as a dir or is 72 | createable by the current user, `False` otherwise.""" 73 | try: 74 | if not is_valid_pathname(pathname): 75 | return False 76 | return ( 77 | os.path.exists(pathname) and os.path.isdir(pathname) 78 | ) or is_path_creatable(pathname) 79 | except OSError: 80 | return False 81 | 82 | 83 | def dir_exists(pathname): 84 | """Return `True` if the given pathname is valid for 85 | the current OS and currently exists as a dir, 86 | `False` otherwise.""" 87 | try: 88 | if not is_valid_pathname(pathname): 89 | return False 90 | return os.path.exists(pathname) and os.path.isdir(pathname) 91 | except OSError: 92 | return False 93 | -------------------------------------------------------------------------------- /src/chmpy/util/text.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | SUBSCRIPT_MAP = { 4 | "0": "₀", 5 | "1": "₁", 6 | "2": "₂", 7 | "3": "₃", 8 | "4": "₄", 9 | "5": "₅", 10 | "6": "₆", 11 | "7": "₇", 12 | "8": "₈", 13 | "9": "₉", 14 | "a": "ₐ", 15 | "b": "♭", 16 | "c": "꜀", 17 | "d": "ᑯ", 18 | "e": "ₑ", 19 | "f": "բ", 20 | "g": "₉", 21 | "h": "ₕ", 22 | "i": "ᵢ", 23 | "j": "ⱼ", 24 | "k": "ₖ", 25 | "l": "ₗ", 26 | "m": "ₘ", 27 | "n": "ₙ", 28 | "o": "ₒ", 29 | "p": "ₚ", 30 | "q": "૧", 31 | "r": "ᵣ", 32 | "s": "ₛ", 33 | "t": "ₜ", 34 | "u": "ᵤ", 35 | "v": "ᵥ", 36 | "w": "w", 37 | "x": "ₓ", 38 | "y": "ᵧ", 39 | "z": "₂", 40 | "A": "ₐ", 41 | "B": "₈", 42 | "C": "C", 43 | "D": "D", 44 | "E": "ₑ", 45 | "F": "բ", 46 | "G": "G", 47 | "H": "ₕ", 48 | "I": "ᵢ", 49 | "J": "ⱼ", 50 | "K": "ₖ", 51 | "L": "ₗ", 52 | "M": "ₘ", 53 | "N": "ₙ", 54 | "O": "ₒ", 55 | "P": "ₚ", 56 | "Q": "Q", 57 | "R": "ᵣ", 58 | "S": "ₛ", 59 | "T": "ₜ", 60 | "U": "ᵤ", 61 | "V": "ᵥ", 62 | "W": "w", 63 | "X": "ₓ", 64 | "Y": "ᵧ", 65 | "Z": "Z", 66 | "+": "₊", 67 | "-": "₋", 68 | "=": "₌", 69 | "(": "₍", 70 | ")": "₎", 71 | } 72 | 73 | 74 | def subscript(x: str) -> str: 75 | """ 76 | Convert the provided string to its subscript 77 | equivalent in unicode 78 | 79 | Args: 80 | x (str): the string to be converted 81 | 82 | Returns: 83 | str: the converted string 84 | """ 85 | return SUBSCRIPT_MAP.get(x, x) 86 | 87 | 88 | def overline(x: str) -> str: 89 | """ 90 | Add a unicode overline modifier 91 | to the provided string. 92 | 93 | Args: 94 | x (str): the string to be overlined 95 | 96 | Returns: 97 | str: the overlined string 98 | """ 99 | return f"\u0305{x}" 100 | 101 | 102 | def natural_sort_key(s: str, _nsre=re.compile(r"([a-zA-Z]+)(\d+)")): 103 | """ 104 | Utility function for sorting strings of the form A1, B_2, A12 105 | etc. so that the suffixes will be in numeric order rather than 106 | lexicographical order. 107 | 108 | Args: 109 | s (str): the string whose sort key to determine 110 | 111 | Returns: 112 | tuple: the (str, int) natural sort key for the provided string 113 | """ 114 | m = _nsre.match(s) 115 | if not m: 116 | return s 117 | c, i = m.groups(0) 118 | return (c, int(i)) 119 | -------------------------------------------------------------------------------- /src/chmpy/util/unit.py: -------------------------------------------------------------------------------- 1 | BOHR_TO_ANGSTROM = 0.52917749 2 | ANGSTROM_TO_BOHR = 1 / BOHR_TO_ANGSTROM 3 | AU_TO_KJ_PER_MOL = 2625.499639 4 | AU_TO_PER_CM = 219474.63 5 | AU_TO_KCAL_PER_MOL = 627.5096080305927 6 | KJ_TO_KCAL = 0.239006 7 | EV_TO_KJ_PER_MOL = 96.48530749925973 8 | AU_TO_EV = 27.211399 9 | AU_TO_KELVIN = 315777.09 10 | 11 | 12 | class units: 13 | factors = { 14 | ("angstrom", "angstrom"): 1, 15 | ("au", "au"): 1, 16 | ("au2", "au2"): 1, 17 | ("au", "bohr"): 1, 18 | ("au2", "bohr2"): 1, 19 | ("bohr", "au"): 1, 20 | ("bohr", "bohr"): 1, 21 | ("bohr2", "bohr2"): 1, 22 | ("kj_per_mol", "kj_per_mol"): 1, 23 | ("bohr", "angstrom"): BOHR_TO_ANGSTROM, 24 | ("bohr2", "angstrom2"): BOHR_TO_ANGSTROM * BOHR_TO_ANGSTROM, 25 | ("au", "angstrom"): BOHR_TO_ANGSTROM, 26 | ("angstrom", "bohr"): 1 / BOHR_TO_ANGSTROM, 27 | ("angstrom2", "bohr2"): 1 / (BOHR_TO_ANGSTROM * BOHR_TO_ANGSTROM), 28 | ("angstrom", "au"): 1 / BOHR_TO_ANGSTROM, 29 | ("au", "kj_per_mol"): AU_TO_KJ_PER_MOL, 30 | ("au", "ev"): AU_TO_EV, 31 | ("hartree", "kj_per_mol"): AU_TO_KJ_PER_MOL, 32 | ("hartree", "ev"): AU_TO_EV, 33 | ("ev", "kj_per_mol"): EV_TO_KJ_PER_MOL, 34 | ("ev", "au"): 1 / AU_TO_EV, 35 | ("ev", "hartree"): 1 / AU_TO_EV, 36 | ("kj_per_mol", "au"): 1 / AU_TO_KJ_PER_MOL, 37 | ("kj_per_mol", "hartree"): 1 / AU_TO_KJ_PER_MOL, 38 | ("kj_per_mol", "ev"): 1 / EV_TO_KJ_PER_MOL, 39 | } 40 | 41 | @classmethod 42 | def _s_unit(cls, unit): 43 | return unit.lower().replace("/", "_per_") 44 | 45 | @classmethod 46 | def _conversion_factor(cls, f, t): 47 | if (f, t) in cls.factors: 48 | return cls.factors[(f, t)] 49 | raise ValueError(f"No viable conversion from '{f}' to '{t}'") 50 | 51 | @classmethod 52 | def convert(cls, value, t="au", f="au"): 53 | tu = cls._s_unit(t) 54 | try: 55 | return getattr(cls, tu)(value, unit=f) 56 | except AttributeError as e: 57 | raise ValueError(f"Unknown unit {t}") from e 58 | 59 | @classmethod 60 | def bohr(cls, value, unit="au"): 61 | unit = cls._s_unit(unit) 62 | return value * cls._conversion_factor(unit, "bohr") 63 | 64 | @classmethod 65 | def bohr2(cls, value, unit="au2"): 66 | unit = cls._s_unit(unit) 67 | return value * cls._conversion_factor(unit, "bohr2") 68 | 69 | @classmethod 70 | def angstrom(cls, value, unit="au"): 71 | unit = cls._s_unit(unit) 72 | return value * cls._conversion_factor(unit, "angstrom") 73 | 74 | @classmethod 75 | def au(cls, value, unit="au"): 76 | unit = cls._s_unit(unit) 77 | return value * cls._conversion_factor(unit, "au") 78 | 79 | @classmethod 80 | def kj_per_mol(cls, value, unit="au"): 81 | unit = cls._s_unit(unit) 82 | return value * cls._conversion_factor(unit, "kj_per_mol") 83 | -------------------------------------------------------------------------------- /src/chmpy/util/util.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterspackman/chmpy/6e4e8ea3dfc7611dc081792ce84cd61db1c91a87/src/chmpy/util/util.py --------------------------------------------------------------------------------