Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.pixi/envs/dev/bin/python"
"python.defaultInterpreterPath": "${workspaceFolder}/.pixi/envs/dev/bin/python",
"python-envs.defaultEnvManager": "renan-r-santos.pixi-code:pixi",
"python-envs.defaultPackageManager": "renan-r-santos.pixi-code:pixi"
}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]
### Added
- `geom.transform.transition()` for determining the transition-state geometry between two geometries via `StereoCondensedReactionGraph`.

## [0.0.19] - 2026-07-17
### Added
Expand Down
179 changes: 116 additions & 63 deletions pixi.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ ipykernel = "*"

[feature.dev.pypi-dependencies]
keepachangelog = "*"
xyzrender = ">=0.3.1, <0.4"

[feature.docs.dependencies]
sphinx = "*"
Expand All @@ -51,6 +50,7 @@ dev = { features = ["dev", "docs"], solve-group = "default"}
[pypi-dependencies]
stereomolgraph = ">=0.0.22b0"
irmsd = "==0.1.1"
xyzrender = ">=0.3.1, <0.4"

[feature.dev.tasks]
# Manage pyproject.toml with uv
Expand Down
4 changes: 4 additions & 0 deletions src/automol/geom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .core import (
Geometry,
from_rdkit_mol,
from_stereo_mol_graph,
from_xyz_block,
from_xyz_file,
hill_formula,
Expand All @@ -23,6 +24,7 @@
)
from .internal import angles, bonds, dihedrals, set_distance
from .properties import adjacency_matrix, center_of_mass, distance_keys, distance_matrix
from .transform import transition
from .vibration import (
harmonic_zpv,
mass_weight_vector,
Expand All @@ -45,6 +47,7 @@
"distance_matrix",
"eckart_frame",
"from_rdkit_mol",
"from_stereo_mol_graph",
"from_xyz_block",
"from_xyz_file",
"harmonic_zpv",
Expand All @@ -62,6 +65,7 @@
"set_distance",
"stereo_mol_graph",
"transform",
"transition",
"translational_normal_modes",
"vibrational_analysis",
"view",
Expand Down
6 changes: 6 additions & 0 deletions src/automol/geom/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ def stereo_mol_graph(geo: Geometry) -> StereoMolGraph:
return StereoMolGraph.from_geometry(sm_geo) # ty:ignore[invalid-argument-type]


def from_stereo_mol_graph(smg: StereoMolGraph, *, charge: int = 0) -> Geometry:
"""Instantiate a Geometry from a StereoMolGraph."""
mol = smg.to_rdmol(charge=charge)
return from_rdkit_mol(mol)


def hill_formula(geo: Geometry) -> str:
"""Render the molecular formula in Hill order."""
counts = Counter(s.capitalize() for s in geo.symbols)
Expand Down
39 changes: 39 additions & 0 deletions src/automol/geom/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import numpy as np
from numpy.typing import ArrayLike
from scipy.spatial.transform import Rotation
from stereomolgraph import StereoCondensedReactionGraph

from .core import from_stereo_mol_graph, stereo_mol_graph

if TYPE_CHECKING:
from .core import Geometry
Expand Down Expand Up @@ -93,3 +96,39 @@ def rotate(
mask = slice(None) if keys is None else list(keys)
geo.coordinates[mask] = rot.apply(geo.coordinates[mask])
return geo


def transition(geo1: "Geometry", geo2: "Geometry") -> "Geometry":
"""Determine the transition geometry between two geometries.

Parameters
----------
geo1
Initial geometry.
geo2
Final geometry.

Returns
-------
Geometry.
"""
if geo1.spin != geo2.spin:
msg = f"Geometries must have the same spin: {geo1.spin} != {geo2.spin}"
raise ValueError(msg)

smg1 = stereo_mol_graph(geo1)
smg2 = stereo_mol_graph(geo2)
scrg = StereoCondensedReactionGraph.from_graphs(smg1, smg2)

active_h = [a for a in scrg.active_atoms() if scrg.get_atom_type(a) == 1]
for h in active_h:
scrg.set_atom_attribute(h, "atom_type", 8)

ts_smg = scrg.ts()
ts_geo = from_stereo_mol_graph(ts_smg)
ts_geo.spin = geo1.spin

for h in active_h:
ts_geo.symbols[h] = "H"

return ts_geo
43 changes: 43 additions & 0 deletions tests/test_transform.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Geometry transform tests."""

import numpy as np
import pytest
from scipy.spatial.transform import Rotation

from automol import Geometry, geom
Expand Down Expand Up @@ -58,3 +59,45 @@ def test__rotate_in_place(water: Geometry) -> None:
result = geom.transform.rotate(water, rot, in_place=True)
assert result is water
assert np.allclose(water.coordinates, expected)


def test__transition_raises_for_mismatched_spin(water: Geometry) -> None:
"""Test that transition() rejects geometries with different spins."""
water_triplet = water.model_copy(update={"spin": 2})
assert water.spin != water_triplet.spin
with pytest.raises(ValueError, match="spin"):
geom.transform.transition(water, water_triplet)


def test__transition_identity(water: Geometry) -> None:
"""Test the (degenerate) transition between a geometry and itself."""
ts_geo = geom.transform.transition(water, water)
assert ts_geo.symbols == water.symbols
assert ts_geo.spin == water.spin
assert ts_geo.coordinates.shape == water.coordinates.shape
assert np.all(np.isfinite(ts_geo.coordinates))


def test__transition_hydrogen_abstraction() -> None:
"""Test the transition geometry for an H-abstraction reaction.

F-H + Cl -> F + H-Cl: the migrating H atom breaks its bond to F and forms
a new bond to Cl.
"""
reactant = Geometry(
symbols=["F", "H", "Cl"],
coordinates=[[0, 0, 0], [0.92, 0, 0], [3.5, 0, 0]],
charge=0,
spin=0,
)
product = Geometry(
symbols=["F", "H", "Cl"],
coordinates=[[0, 0, 0], [3.0, 0, 0], [4.27, 0, 0]],
charge=0,
spin=0,
)
ts_geo = geom.transform.transition(reactant, product)
assert ts_geo.symbols == reactant.symbols
assert ts_geo.spin == reactant.spin
assert ts_geo.coordinates.shape == reactant.coordinates.shape
assert np.all(np.isfinite(ts_geo.coordinates))