Skip to content

Commit 12ccdbb

Browse files
authored
Merge pull request #22 from caltechmsc/bugfix/21-inversion-terms-collapse-to-single-instance-per-planar-center
fix(builder): Generate All Three Inversion Terms per Planar Center
2 parents 51616a3 + 1df2196 commit 12ccdbb

File tree

7 files changed

+127
-87
lines changed

7 files changed

+127
-87
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ At a high level the library walks through:
66

77
1. **Perception:** six ordered passes (rings → Kekulé expansion → electron bookkeeping → aromaticity → resonance → hybridization) that upgrade raw connectivity into a rich `AnnotatedMolecule`.
88
2. **Typing:** an iterative, priority-sorted rule engine that resolves the final DREIDING atom label for every atom.
9-
3. **Building:** a pure graph traversal that emits canonical bonds, angles, and torsions as a `MolecularTopology`.
9+
3. **Building:** a pure graph traversal that emits canonical bonds, angles, torsions, and inversions as a `MolecularTopology`.
1010

1111
## Features
1212

1313
- **Chemically faithful perception:** built-in algorithms cover SSSR ring search, strict Kekulé expansion, charge/lone pair templates for heteroatoms, aromaticity categorization (including anti-aromatic detection), resonance propagation, and hybridization inference.
1414
- **Deterministic typing engine:** TOML rules are sorted by priority and evaluated until a fixed point, making neighbor-dependent rules (e.g., `H_HB`) converge without guesswork.
15-
- **Engine-agnostic topology:** outputs canonicalized bonds, angles, proper and improper dihedrals ready for any simulator that consumes DREIDING-style terms.
15+
- **Engine-agnostic topology:** outputs canonicalized bonds, angles, torsions, and inversions ready for any simulator that consumes DREIDING-style terms.
1616
- **Extensible ruleset:** ship with curated defaults (`resources/default.rules.toml`) and load or merge custom rule files at runtime.
1717
- **Rust-first ergonomics:** zero `unsafe`, comprehensive unit/integration tests, and precise error variants for validation, perception, and typing failures.
1818

docs/01_pipeline.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ Once a `MolecularGraph` enters the pipeline, it is immediately converted into an
4646

4747
The `MolecularTopology` is the final product of the pipeline. It is a clean, structured representation tailored specifically for consumption by molecular simulation engines.
4848

49-
- **Purpose:** To provide a complete list of all particles and interaction terms (bonds, angles, dihedrals) required to define a DREIDING force field model.
49+
- **Purpose:** To provide a complete list of all particles and interaction terms (bonds, angles, torsions, inversions) required to define a DREIDING force field model.
5050
- **Structure:**
5151
- A list of final `Atom`s, now including their assigned `atom_type`.
52-
- Deduplicated lists of `Bond`s, `Angle`s, `ProperDihedral`s, and `ImproperDihedral`s.
52+
- Deduplicated lists of `Bond`s, `Angle`s, `Torsion`s, and `Inversion`s.
5353
- **Design Rationale:**
5454
- **Simulation-Oriented:** The structure directly maps to the needs of a simulation setup. It discards intermediate perception data (like `lone_pairs` or `steric_number`) that is not directly part of the final force field definition.
55-
- **Canonical Representation:** Each topological component (`Angle`, `Dihedral`) is stored in a canonical form (e.g., atom indices are sorted). This simplifies consumption by downstream tools, as it eliminates ambiguity and the need for further deduplication.
55+
- **Canonical Representation:** Each topological component (`Angle`, `Torsion`, `Inversion`) is stored in a canonical form (e.g., atom indices are sorted). This simplifies consumption by downstream tools, as it eliminates ambiguity and the need for further deduplication.
5656

5757
## 2. The Data Flow: A Deterministic Transformation
5858

docs/04_topology_builder.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Phase 3: The Topology Builder
22

3-
With atom types resolved, the builder translates the annotated molecule into a `MolecularTopology`. This stage is pure graph traversal — no additional chemistry is inferred — but its where canonical force-field terms emerge.
3+
With atom types resolved, the builder translates the annotated molecule into a `MolecularTopology`. This stage is pure graph traversal — no additional chemistry is inferred — but it's where canonical force-field terms emerge.
44

55
`builder::build_topology` takes two inputs:
66

77
1. The immutable `AnnotatedMolecule` output of perception.
88
2. The `Vec<String>` of atom types returned by the typing engine.
99

10-
It produces `MolecularTopology { atoms, bonds, angles, propers, impropers }`, all deduplicated and ready for downstream MD engines.
10+
It produces `MolecularTopology { atoms, bonds, angles, torsions, inversions }`, all deduplicated and ready for downstream MD engines.
1111

1212
```mermaid
1313
graph TD
@@ -18,11 +18,11 @@ graph TD
1818

1919
## Atom Table
2020

21-
`build_atoms` walks the annotated atoms and copies their element, hybridization, and ID while splicing in the final type string (`atom_types[ann_atom.id]`). This produces the topologys `atoms` vector.
21+
`build_atoms` walks the annotated atoms and copies their element, hybridization, and ID while splicing in the final type string (`atom_types[ann_atom.id]`). This produces the topology's `atoms` vector.
2222

2323
## Connectivity Terms
2424

25-
Every interaction term uses the molecules adjacency lists and bond table, which already reflect Kekulé-expanded bond orders.
25+
Every interaction term uses the molecule's adjacency lists and bond table, which already reflect Kekulé-expanded bond orders.
2626

2727
### Bonds
2828

@@ -41,24 +41,32 @@ for center in atoms:
4141
angles.insert(Angle::new(i, center, k))
4242
```
4343

44-
### Proper Dihedrals (`build_propers`)
44+
### Torsions (`build_torsions`)
4545

46-
Proper torsions are enumerated around each bond `j-k`:
46+
Torsions are enumerated around each bond `j-k`:
4747

4848
1. Iterate over every stored bond.
49-
2. For each neighbor `i` of `j` (excluding `k`) and each neighbor `l` of `k` (excluding `j` and `i`), emit `ProperDihedral::new(i, j, k, l)`.
49+
2. For each neighbor `i` of `j` (excluding `k`) and each neighbor `l` of `k` (excluding `j` and `i`), emit `Torsion::new(i, j, k, l)`.
5050
3. The constructor compares `(i, j, k, l)` to its reverse `(l, k, j, i)` and keeps the lexicographically smaller tuple to guarantee uniqueness.
5151

5252
This approach naturally covers both directions (i.e., `i-j-k-l` and `l-k-j-i`) without generating duplicates.
5353

54-
### Improper Dihedrals (`build_impropers`)
54+
### Inversions (`build_inversions`)
5555

56-
Improper torsions enforce planarity at trigonal centers. The builder scans every atom and checks two conditions:
56+
Inversions enforce planarity at trigonal centers. The builder scans every atom and checks two conditions:
5757

5858
1. Degree equals 3.
5959
2. Hybridization equals `Hybridization::SP2` or `Hybridization::Resonant`.
6060

61-
If satisfied, the atom’s three neighbors form the outer atoms while the center occupies the third index of `ImproperDihedral::new(p1, p2, center, p3)`. The constructor sorts the three peripheral atoms but keeps the central atom fixed, delivering a canonical key.
61+
Per the DREIDING paper, **each planar center generates three inversion terms**, with each neighbor taking turn as the "axis":
62+
63+
For center I with neighbors {J, K, L}:
64+
65+
- Inversion(center=I, axis=J, plane={K, L})
66+
- Inversion(center=I, axis=K, plane={J, L})
67+
- Inversion(center=I, axis=L, plane={J, K})
68+
69+
The constructor `Inversion::new(center, axis, plane1, plane2)` sorts only the two plane atoms (not the axis), ensuring the three terms per center remain distinct.
6270

6371
## Why Canonical Forms Matter
6472

docs/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ graph TD
4646
Annotated -- "Provides geometry + resonance context" --> BuilderPhase;
4747
AtomTypes -- "Provides atom type info" --> BuilderPhase;
4848
49-
BuilderPhase -- "Generates bonds, angles, dihedrals" --> OutputTopology;
49+
BuilderPhase -- "Generates bonds, angles, torsions, inversions" --> OutputTopology;
5050
```
5151

5252
- **Phase 1: Perception (`perception::perceive`):** Takes the raw `MolecularGraph` and emits an `AnnotatedMolecule`. Six ordered passes (rings, kekulization, electrons, aromaticity, resonance, hybridization) enrich each atom with bonding, charge, lone-pair, ring, and delocalization metadata. The output is immutable and shared with later stages.
5353

5454
- **Phase 2: Typing (`typing::engine::assign_types`):** Runs a deterministic fixed-point solver over the `AnnotatedMolecule`. It evaluates TOML rules parsed via `typing::rules::parse_rules`, honoring priorities and neighbor-dependent constraints until every atom is assigned a DREIDING type.
5555

56-
- **Phase 3: Building (`builder::build_topology`):** Consumes the annotated atoms plus the final type vector to produce a canonical `MolecularTopology`. Helper routines enumerate bonds, angles, proper/ improper dihedrals, and collapse duplicates using canonical ordering so downstream engines receive stable identifiers.
56+
- **Phase 3: Building (`builder::build_topology`):** Consumes the annotated atoms plus the final type vector to produce a canonical `MolecularTopology`. Helper routines enumerate bonds, angles, torsions, and inversions, and collapse duplicates using canonical ordering so downstream engines receive stable identifiers.
5757

5858
## Directory of Architectural Documents
5959

src/builder/mod.rs

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
//! Converts annotated molecules and assigned atom types into a full molecular topology.
22
//!
33
//! The builder stage takes the perception output and typing assignments, emitting atoms, bonds,
4-
//! angles, proper dihedrals, and improper dihedrals expected by downstream force-field tooling.
4+
//! angles, torsions, and inversions expected by downstream force-field tooling.
55
66
use crate::core::properties::{GraphBondOrder, Hybridization, TopologyBondOrder};
7-
use crate::core::topology::{
8-
Angle, Atom, Bond, ImproperDihedral, MolecularTopology, ProperDihedral,
9-
};
7+
use crate::core::topology::{Angle, Atom, Bond, Inversion, MolecularTopology, Torsion};
108
use crate::perception::{AnnotatedMolecule, ResonanceSystem};
119
use std::collections::HashSet;
1210

@@ -22,24 +20,23 @@ use std::collections::HashSet;
2220
///
2321
/// # Returns
2422
///
25-
/// A populated [`MolecularTopology`] containing atoms, bonds, angles, proper dihedrals, and
26-
/// improper dihedrals.
23+
/// A populated [`MolecularTopology`] containing atoms, bonds, angles, torsions, and inversions.
2724
pub fn build_topology(
2825
annotated_molecule: &AnnotatedMolecule,
2926
atom_types: &[String],
3027
) -> MolecularTopology {
3128
let atoms = build_atoms(annotated_molecule, atom_types);
3229
let bonds = build_bonds(annotated_molecule);
3330
let angles = build_angles(annotated_molecule);
34-
let propers = build_propers(annotated_molecule);
35-
let impropers = build_impropers(annotated_molecule);
31+
let torsions = build_torsions(annotated_molecule);
32+
let inversions = build_inversions(annotated_molecule);
3633

3734
MolecularTopology {
3835
atoms,
3936
bonds: bonds.into_iter().collect(),
4037
angles: angles.into_iter().collect(),
41-
propers: propers.into_iter().collect(),
42-
impropers: impropers.into_iter().collect(),
38+
torsions: torsions.into_iter().collect(),
39+
inversions: inversions.into_iter().collect(),
4340
}
4441
}
4542

@@ -114,9 +111,9 @@ fn build_angles(annotated_molecule: &AnnotatedMolecule) -> HashSet<Angle> {
114111
angles
115112
}
116113

117-
/// Builds proper dihedrals by extending each bond to its neighboring atoms.
118-
fn build_propers(annotated_molecule: &AnnotatedMolecule) -> HashSet<ProperDihedral> {
119-
let mut propers = HashSet::new();
114+
/// Builds torsions by extending each bond to its neighboring atoms.
115+
fn build_torsions(annotated_molecule: &AnnotatedMolecule) -> HashSet<Torsion> {
116+
let mut torsions = HashSet::new();
120117
for bond_jk in &annotated_molecule.bonds {
121118
let (j, k) = bond_jk.atom_ids;
122119

@@ -128,16 +125,17 @@ fn build_propers(annotated_molecule: &AnnotatedMolecule) -> HashSet<ProperDihedr
128125
if l == j || l == i {
129126
continue;
130127
}
131-
propers.insert(ProperDihedral::new(i, j, k, l));
128+
torsions.insert(Torsion::new(i, j, k, l));
132129
}
133130
}
134131
}
135-
propers
132+
torsions
136133
}
137134

138-
/// Builds improper dihedrals for planar degree-three centers with SP2-like hybridization.
139-
fn build_impropers(annotated_molecule: &AnnotatedMolecule) -> HashSet<ImproperDihedral> {
140-
let mut impropers = HashSet::new();
135+
/// Builds inversions by identifying planar centers and generating three
136+
/// terms per center with each neighbor as axis.
137+
fn build_inversions(annotated_molecule: &AnnotatedMolecule) -> HashSet<Inversion> {
138+
let mut inversions = HashSet::new();
141139
for atom in &annotated_molecule.atoms {
142140
if atom.degree == 3
143141
&& matches!(
@@ -146,13 +144,19 @@ fn build_impropers(annotated_molecule: &AnnotatedMolecule) -> HashSet<ImproperDi
146144
)
147145
{
148146
let neighbors = &annotated_molecule.adjacency[atom.id];
149-
let p1 = neighbors[0].0;
150-
let p2 = neighbors[1].0;
151-
let p3 = neighbors[2].0;
152-
impropers.insert(ImproperDihedral::new(p1, p2, atom.id, p3));
147+
let n0 = neighbors[0].0;
148+
let n1 = neighbors[1].0;
149+
let n2 = neighbors[2].0;
150+
151+
// Term 1: axis=n0, plane={n1, n2}
152+
inversions.insert(Inversion::new(atom.id, n0, n1, n2));
153+
// Term 2: axis=n1, plane={n0, n2}
154+
inversions.insert(Inversion::new(atom.id, n1, n0, n2));
155+
// Term 3: axis=n2, plane={n0, n1}
156+
inversions.insert(Inversion::new(atom.id, n2, n0, n1));
153157
}
154158
}
155-
impropers
159+
inversions
156160
}
157161

158162
#[cfg(test)]
@@ -253,30 +257,35 @@ mod tests {
253257
}
254258

255259
#[test]
256-
fn build_propers_emits_all_valid_dihedrals() {
260+
fn build_torsions_emits_all_valid_dihedrals() {
257261
let (molecule, _) = planar_fragment();
258262

259-
let propers = build_propers(&molecule);
263+
let torsions = build_torsions(&molecule);
260264
let expected: HashSet<_> = vec![
261-
ProperDihedral::new(0, 1, 2, 4),
262-
ProperDihedral::new(3, 1, 2, 4),
263-
ProperDihedral::new(1, 2, 4, 5),
265+
Torsion::new(0, 1, 2, 4),
266+
Torsion::new(3, 1, 2, 4),
267+
Torsion::new(1, 2, 4, 5),
264268
]
265269
.into_iter()
266270
.collect();
267271

268-
assert_eq!(propers, expected);
272+
assert_eq!(torsions, expected);
269273
}
270274

271275
#[test]
272-
fn build_impropers_targets_planar_degree_three_centers() {
276+
fn build_inversions_generates_three_per_planar_center() {
273277
let (molecule, _) = planar_fragment();
274278

275-
let impropers = build_impropers(&molecule);
276-
let expected: HashSet<_> = vec![ImproperDihedral::new(0, 2, 1, 3)]
277-
.into_iter()
278-
.collect();
279+
let inversions = build_inversions(&molecule);
280+
let expected: HashSet<_> = vec![
281+
Inversion::new(1, 0, 2, 3),
282+
Inversion::new(1, 2, 0, 3),
283+
Inversion::new(1, 3, 0, 2),
284+
]
285+
.into_iter()
286+
.collect();
279287

280-
assert_eq!(impropers, expected);
288+
assert_eq!(inversions.len(), 3);
289+
assert_eq!(inversions, expected);
281290
}
282291
}

0 commit comments

Comments
 (0)