furka 0.0.1a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- furka-0.0.1a1/.github/workflows/ci.yml +64 -0
- furka-0.0.1a1/.github/workflows/publish.yml +84 -0
- furka-0.0.1a1/.gitignore +45 -0
- furka-0.0.1a1/LICENSE +21 -0
- furka-0.0.1a1/PKG-INFO +73 -0
- furka-0.0.1a1/README.md +38 -0
- furka-0.0.1a1/pyproject.toml +84 -0
- furka-0.0.1a1/src/furka/__init__.py +27 -0
- furka-0.0.1a1/src/furka/py.typed +0 -0
- furka-0.0.1a1/tests/test_rmsd.py +74 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint-and-typecheck:
|
|
10
|
+
name: Ruff & mypy
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
run: uv python install 3.12
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: uv sync --python 3.12
|
|
23
|
+
|
|
24
|
+
- name: Check formatting and lint with Ruff
|
|
25
|
+
run: |
|
|
26
|
+
uv run ruff format --check
|
|
27
|
+
uv run ruff check
|
|
28
|
+
|
|
29
|
+
- name: Type-check with mypy
|
|
30
|
+
run: uv run mypy src tests
|
|
31
|
+
|
|
32
|
+
test:
|
|
33
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
strategy:
|
|
36
|
+
fail-fast: false
|
|
37
|
+
matrix:
|
|
38
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- name: Install uv
|
|
43
|
+
uses: astral-sh/setup-uv@v4
|
|
44
|
+
|
|
45
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
46
|
+
run: uv python install ${{ matrix.python-version }}
|
|
47
|
+
|
|
48
|
+
- name: Install dependencies
|
|
49
|
+
run: uv sync --python ${{ matrix.python-version }}
|
|
50
|
+
|
|
51
|
+
- name: Run pytest
|
|
52
|
+
run: uv run --python ${{ matrix.python-version }} pytest
|
|
53
|
+
|
|
54
|
+
build-check:
|
|
55
|
+
name: Build sdist and wheel
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
|
|
60
|
+
- name: Install uv
|
|
61
|
+
uses: astral-sh/setup-uv@v4
|
|
62
|
+
|
|
63
|
+
- name: Build distributions
|
|
64
|
+
run: uv build
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build wheel and sdist
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Verify tag matches package version
|
|
17
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
18
|
+
run: |
|
|
19
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
20
|
+
PKG_VERSION=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
21
|
+
if [ "${TAG_VERSION}" != "${PKG_VERSION}" ]; then
|
|
22
|
+
echo "::error::Tag version (${TAG_VERSION}) does not match pyproject.toml version (${PKG_VERSION})"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@v4
|
|
28
|
+
|
|
29
|
+
- name: Build distributions
|
|
30
|
+
run: uv build
|
|
31
|
+
|
|
32
|
+
- name: Upload distribution artifacts
|
|
33
|
+
uses: actions/upload-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: python-package-distributions
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
publish-pypi:
|
|
39
|
+
name: Publish to PyPI via Trusted Publishing
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment:
|
|
43
|
+
name: pypi
|
|
44
|
+
url: https://pypi.org/p/furka
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
steps:
|
|
48
|
+
- name: Download distribution artifacts
|
|
49
|
+
uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: python-package-distributions
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish distributions to PyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
|
|
57
|
+
github-release:
|
|
58
|
+
name: Create GitHub Release
|
|
59
|
+
needs: [build, publish-pypi]
|
|
60
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
permissions:
|
|
63
|
+
contents: write
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
|
|
67
|
+
- name: Download distribution artifacts
|
|
68
|
+
uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: python-package-distributions
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- name: Create GitHub Release
|
|
74
|
+
env:
|
|
75
|
+
GH_TOKEN: ${{ github.token }}
|
|
76
|
+
run: |
|
|
77
|
+
PRERELEASE_FLAG=""
|
|
78
|
+
if [[ "${GITHUB_REF_NAME}" =~ (a|b|rc|dev)[0-9]+$ ]]; then
|
|
79
|
+
PRERELEASE_FLAG="--prerelease"
|
|
80
|
+
fi
|
|
81
|
+
gh release create "${GITHUB_REF_NAME}" dist/* \
|
|
82
|
+
--title "${GITHUB_REF_NAME}" \
|
|
83
|
+
--generate-notes \
|
|
84
|
+
${PRERELEASE_FLAG}
|
furka-0.0.1a1/.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
|
|
25
|
+
# Virtual environments
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
env/
|
|
30
|
+
|
|
31
|
+
# Unit test / coverage / linter / type-checker caches
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
coverage.xml
|
|
35
|
+
htmlcov/
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
|
|
40
|
+
# IDEs and OS artifacts
|
|
41
|
+
.idea/
|
|
42
|
+
.vscode/
|
|
43
|
+
*.swp
|
|
44
|
+
*.swo
|
|
45
|
+
.DS_Store
|
furka-0.0.1a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alain C. Vaucher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
furka-0.0.1a1/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: furka
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: Composable transition-state (TS) and minimum-energy-path (MEP) algorithms for computational chemistry.
|
|
5
|
+
Project-URL: Homepage, https://github.com/avaucher/furka
|
|
6
|
+
Project-URL: Repository, https://github.com/avaucher/furka
|
|
7
|
+
Project-URL: Issues, https://github.com/avaucher/furka/issues
|
|
8
|
+
Author: Alain C. Vaucher
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: chemistry,intrinsic-reaction-coordinate,minimum-energy-path,potential-energy-surface,saddle-point,transition-state
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: scipy>=1.10
|
|
26
|
+
Provides-Extra: ase
|
|
27
|
+
Requires-Dist: ase>=3.22; extra == 'ase'
|
|
28
|
+
Provides-Extra: geometric
|
|
29
|
+
Requires-Dist: geometric>=1.0; extra == 'geometric'
|
|
30
|
+
Provides-Extra: sella
|
|
31
|
+
Requires-Dist: sella>=2.3; extra == 'sella'
|
|
32
|
+
Provides-Extra: viz
|
|
33
|
+
Requires-Dist: plotly>=5.18; extra == 'viz'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# furka
|
|
37
|
+
|
|
38
|
+
**`furka`** is a modular Python library for locating chemical transition states (first-order saddle points) and minimum energy paths (MEPs) on molecular potential energy surfaces.
|
|
39
|
+
|
|
40
|
+
Named after the Swiss Alpine **Furka Pass** (a saddle connecting two valleys) and Latin *furca* (a two-pronged bracket), `furka` decouples double-ended path/bracket generation (Stage 1) from local saddle-point refinement (Stage 2) and post-TS MEP/IRC verification (Stage 3).
|
|
41
|
+
|
|
42
|
+
> **Status:** Early alpha (`0.0.1a1`). Full release accompanying upcoming publication.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install furka
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Example
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import numpy as np
|
|
54
|
+
from furka import kabsch_rmsd
|
|
55
|
+
|
|
56
|
+
coords_a = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.15], [0.0, 1.0, -0.4]])
|
|
57
|
+
coords_b = coords_a + np.array([2.5, -1.0, 0.5]) # rigid translation
|
|
58
|
+
|
|
59
|
+
rmsd = kabsch_rmsd(coords_a, coords_b) # 0.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv sync
|
|
66
|
+
uv run pytest
|
|
67
|
+
uv run ruff check
|
|
68
|
+
uv run mypy src tests
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT License — see [LICENSE](LICENSE).
|
furka-0.0.1a1/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# furka
|
|
2
|
+
|
|
3
|
+
**`furka`** is a modular Python library for locating chemical transition states (first-order saddle points) and minimum energy paths (MEPs) on molecular potential energy surfaces.
|
|
4
|
+
|
|
5
|
+
Named after the Swiss Alpine **Furka Pass** (a saddle connecting two valleys) and Latin *furca* (a two-pronged bracket), `furka` decouples double-ended path/bracket generation (Stage 1) from local saddle-point refinement (Stage 2) and post-TS MEP/IRC verification (Stage 3).
|
|
6
|
+
|
|
7
|
+
> **Status:** Early alpha (`0.0.1a1`). Full release accompanying upcoming publication.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install furka
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Example
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import numpy as np
|
|
19
|
+
from furka import kabsch_rmsd
|
|
20
|
+
|
|
21
|
+
coords_a = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.15], [0.0, 1.0, -0.4]])
|
|
22
|
+
coords_b = coords_a + np.array([2.5, -1.0, 0.5]) # rigid translation
|
|
23
|
+
|
|
24
|
+
rmsd = kabsch_rmsd(coords_a, coords_b) # 0.0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv sync
|
|
31
|
+
uv run pytest
|
|
32
|
+
uv run ruff check
|
|
33
|
+
uv run mypy src tests
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT License — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "furka"
|
|
7
|
+
version = "0.0.1a1"
|
|
8
|
+
description = "Composable transition-state (TS) and minimum-energy-path (MEP) algorithms for computational chemistry."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Alain C. Vaucher" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"chemistry",
|
|
18
|
+
"transition-state",
|
|
19
|
+
"minimum-energy-path",
|
|
20
|
+
"saddle-point",
|
|
21
|
+
"intrinsic-reaction-coordinate",
|
|
22
|
+
"potential-energy-surface",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"numpy>=1.24",
|
|
39
|
+
"scipy>=1.10",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
ase = ["ase>=3.22"]
|
|
44
|
+
sella = ["sella>=2.3"]
|
|
45
|
+
geometric = ["geometric>=1.0"]
|
|
46
|
+
viz = ["plotly>=5.18"]
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/avaucher/furka"
|
|
50
|
+
Repository = "https://github.com/avaucher/furka"
|
|
51
|
+
Issues = "https://github.com/avaucher/furka/issues"
|
|
52
|
+
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = [
|
|
55
|
+
"mypy>=1.11",
|
|
56
|
+
"pytest>=8.0",
|
|
57
|
+
"pytest-cov>=5.0",
|
|
58
|
+
"ruff>=0.8",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/furka"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
addopts = ["-ra", "--strict-markers", "--strict-config"]
|
|
67
|
+
|
|
68
|
+
[tool.coverage.run]
|
|
69
|
+
source = ["furka"]
|
|
70
|
+
branch = true
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
target-version = "py310"
|
|
74
|
+
line-length = 100
|
|
75
|
+
src = ["src", "tests"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
python_version = "3.10"
|
|
82
|
+
strict = true
|
|
83
|
+
warn_return_any = true
|
|
84
|
+
warn_unused_configs = true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""furka: Composable transition-state and minimum-energy-path algorithms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike
|
|
7
|
+
|
|
8
|
+
__version__ = "0.0.1a1"
|
|
9
|
+
__all__ = ["__version__", "kabsch_rmsd"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def kabsch_rmsd(coords_a: ArrayLike, coords_b: ArrayLike) -> float:
|
|
13
|
+
"""Compute optimal rigid-body aligned RMSD (in Angstrom) between two (N, 3) geometries."""
|
|
14
|
+
a = np.asarray(coords_a, dtype=np.float64)
|
|
15
|
+
b = np.asarray(coords_b, dtype=np.float64)
|
|
16
|
+
if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 3 or a.shape[0] == 0:
|
|
17
|
+
raise ValueError(
|
|
18
|
+
f"Expected matching non-empty (N, 3) coordinate arrays, got {a.shape} and {b.shape}."
|
|
19
|
+
)
|
|
20
|
+
a_centered = a - a.mean(axis=0)
|
|
21
|
+
b_centered = b - b.mean(axis=0)
|
|
22
|
+
cov = a_centered.T @ b_centered
|
|
23
|
+
u, _, vt = np.linalg.svd(cov)
|
|
24
|
+
d = 1.0 if float(np.linalg.det(u @ vt)) >= 0.0 else -1.0
|
|
25
|
+
rot = u @ np.diag([1.0, 1.0, d]) @ vt
|
|
26
|
+
diff = a_centered @ rot - b_centered
|
|
27
|
+
return float(np.sqrt(np.mean(np.sum(diff * diff, axis=1))))
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
from furka import __version__, kabsch_rmsd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_version() -> None:
|
|
11
|
+
assert __version__ == "0.0.1a1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_kabsch_rmsd_identical_and_translated() -> None:
|
|
15
|
+
coords = np.array(
|
|
16
|
+
[
|
|
17
|
+
[0.000, 0.000, 0.117],
|
|
18
|
+
[0.000, 0.757, -0.469],
|
|
19
|
+
[0.000, -0.757, -0.469],
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
assert kabsch_rmsd(coords, coords) == pytest.approx(0.0, abs=1e-12)
|
|
23
|
+
translated = coords + np.array([12.3, -4.5, 6.7])
|
|
24
|
+
assert kabsch_rmsd(coords, translated) == pytest.approx(0.0, abs=1e-12)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_kabsch_rmsd_proper_rotation() -> None:
|
|
28
|
+
coords = np.array(
|
|
29
|
+
[
|
|
30
|
+
[0.0, 0.0, 0.0],
|
|
31
|
+
[1.4, 0.0, 0.0],
|
|
32
|
+
[0.0, 1.5, 0.0],
|
|
33
|
+
[0.0, 0.0, 1.6],
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
theta = 0.73
|
|
37
|
+
c, s = np.cos(theta), np.sin(theta)
|
|
38
|
+
rot_z = np.array([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]])
|
|
39
|
+
rot_x = np.array([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]])
|
|
40
|
+
transformed = coords @ (rot_z @ rot_x) + np.array([-3.0, 1.2, 8.4])
|
|
41
|
+
assert kabsch_rmsd(coords, transformed) == pytest.approx(0.0, abs=1e-12)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_kabsch_rmsd_chiral_reflection_not_collapsed() -> None:
|
|
45
|
+
chiral = np.array(
|
|
46
|
+
[
|
|
47
|
+
[0.0, 0.0, 0.0],
|
|
48
|
+
[1.5, 0.0, 0.0],
|
|
49
|
+
[0.0, 1.6, 0.0],
|
|
50
|
+
[0.0, 0.0, 1.7],
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
mirror = chiral * np.array([1.0, 1.0, -1.0])
|
|
54
|
+
assert kabsch_rmsd(chiral, mirror) > 0.5
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_kabsch_rmsd_collinear_geometry() -> None:
|
|
58
|
+
hcn = np.array([[0.0, 0.0, -1.06], [0.0, 0.0, 0.0], [0.0, 0.0, 1.15]])
|
|
59
|
+
hcn_rot = np.array([[-1.06, 0.0, 0.0], [0.0, 0.0, 0.0], [1.15, 0.0, 0.0]])
|
|
60
|
+
assert kabsch_rmsd(hcn, hcn_rot) == pytest.approx(0.0, abs=1e-12)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize(
|
|
64
|
+
("a", "b"),
|
|
65
|
+
[
|
|
66
|
+
(np.zeros((3, 3)), np.zeros((4, 3))),
|
|
67
|
+
(np.zeros((3, 2)), np.zeros((3, 2))),
|
|
68
|
+
(np.zeros((3,)), np.zeros((3,))),
|
|
69
|
+
(np.zeros((0, 3)), np.zeros((0, 3))),
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
def test_kabsch_rmsd_invalid_shapes(a: NDArray[np.float64], b: NDArray[np.float64]) -> None:
|
|
73
|
+
with pytest.raises(ValueError, match="Expected matching non-empty"):
|
|
74
|
+
kabsch_rmsd(a, b)
|