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.
@@ -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}
@@ -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).
@@ -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)