├── .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 | 
2 | [](https://zenodo.org/doi/10.5281/zenodo.10697512)
3 |
4 |
5 |
6 |
7 |
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 |

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
--------------------------------------------------------------------------------