Skip to content

Classify linearity from COM-relocated inertia tensor - #51

Merged
galjos merged 1 commit into
mainfrom
fix-linearity-com-relocation
Jun 26, 2026
Merged

Classify linearity from COM-relocated inertia tensor#51
galjos merged 1 commit into
mainfrom
fix-linearity-com-relocation

Conversation

@galjos

@galjos galjos commented Jun 25, 2026

Copy link
Copy Markdown
Member

Closes #49.

linearity() built its tensor from raw atom positions using the second-moment form (Σ mᵢ rᵢ⊗rᵢ) and tested the smallest eigenvalue with an exact == 0. Two consequences:

  • Position-dependent: an off-origin linear molecule (e.g. CO₂ shifted away from the origin) was misclassified as non-linear.
  • Planar molecules misclassified: a planar molecule lying in a coordinate plane (e.g. water in the xy-plane) gets a spurious zero eigenvalue in the second-moment tensor and was called linear.

linearity() feeds dof() (3N−5 for linear vs 3N−6 for non-linear), so a wrong classification corrupts the vibrational degrees of freedom and thus the thermochemistry for any geometry that is not pre-centered.

Fix

Build the true inertia tensor (Σ mᵢ(|rᵢ|²I − rᵢ⊗rᵢ)) from center-of-mass-relocated coordinates — mirroring thermo.py::_compute_inertia_tensor — and decide the vanishing principal moment with a relative tolerance instead of an exact zero.

Tests

  • Translation invariance for a linear (CO₂) and a planar non-linear (water) molecule, including the in-coordinate-plane case that previously misfired.
  • An off-axis linear molecule (HCN along a diagonal) is still detected as linear.

No existing expectations changed (the old exact-zero check already returned non-linear for real off-axis geometries). 144 passed, 0 skipped (suite run with DFTB+ binaries); pylint 7.98/10.

linearity() built its tensor from raw atom positions using the second-moment
form (sum of mass-weighted outer products) and tested the smallest eigenvalue
with an exact == 0 check. That made the result position-dependent and gave a
planar molecule lying in a coordinate plane a spurious zero eigenvalue, so it
was misclassified as linear; off-origin linear molecules were misclassified as
non-linear. Build the true inertia tensor from center-of-mass-relocated
coordinates and use a relative tolerance for the vanishing principal moment.
This feeds dof() (3N-5 vs 3N-6), so the fix corrects the degrees of freedom for
any geometry that is not pre-centered.
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.83%. Comparing base (0d93878) to head (bfaaf20).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #51      +/-   ##
==========================================
+ Coverage   93.80%   93.83%   +0.03%     
==========================================
  Files          21       21              
  Lines        1194     1200       +6     
==========================================
+ Hits         1120     1126       +6     
  Misses         74       74              
Flag Coverage Δ
unittests 93.83% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@galjos
galjos merged commit abda77a into main Jun 26, 2026
3 checks passed
@galjos
galjos deleted the fix-linearity-com-relocation branch June 26, 2026 06:33
galjos added a commit that referenced this pull request Jun 29, 2026
Found by an audit of the RRHO math against ASE `IdealGasThermo`: the
nonlinear path is exact, but **linear and monatomic species were
broken**.

## Bug
The rotational block in `thermo.py` was hardwired to the nonlinear rigid
rotor — it multiplied all three rotational temperatures
(`theta_x*theta_y*theta_z`) and used `(3/2)RT` energy and `(3/2)R` heat
capacity unconditionally. A linear molecule has one ~zero principal
moment of inertia, so `theta -> inf`, `q_rot -> 0`, and:

- `S_rot = R(ln q_rot + 3/2) = -inf` → **total entropy = -inf, Gibbs =
+inf** for every linear molecule (CO2, N2, O2, CO, HCN, acetylene, …).
- `E_rot`/`Cv_rot` were 50% too high for linear (should be `RT`/`R`) and
nonzero for monatomic (should be 0).
- Monatomic species additionally hit a divide-by-zero in the vibrational
formula because `dof()` returned 3 (→ three zero-frequency "modes")
instead of 0.

The earlier `linearity()` fix (#51) only corrected the geometry
*classification*; the Thermo rotational math never consulted it.

## Fix
- Branch the rotational contribution on geometry using the existing
`linearity()` classifier: `n_rot = 0` (monatomic), `2` (linear), `3`
(nonlinear).
- General formulas: `S_rot = R(ln q_rot + n_rot/2)`, `E_rot =
(n_rot/2)RT`, `Cv_rot = (n_rot/2)R`; linear partition function `q_rot =
T/(sigma*theta)`. The nonlinear path is unchanged.
- `dof()` returns 0 for a monatomic species (no rotational/vibrational
DOF), so the vibrational formulas no longer divide by zero.
- Use `np.linalg.eigvalsh` (not `eig`) on the symmetric inertia tensor —
real, sorted eigenvalues.

## Validation (vs ASE IdealGasThermo, total entropy cal/(mol*K))
| molecule | geometry | TS | ASE |
|---|---|---|---|
| H2O | nonlinear | 45.135 | 45.135 |
| CO2 | linear | 51.064 | 51.063 |
| N2 | linear | 45.768 | 45.768 |
| Ar | monatomic | 36.983 | 36.983 |

New parametrized tests assert TS == ASE for all four geometry classes
(ASE is already a dependency, so this runs in CI with no DFTB+ binary),
plus a regression check that linear/monatomic rotational entropy is
finite. The nonlinear anthraquinone regression (internal `_rotational_*`
attributes) is unchanged.

## Not in this PR (filed separately, both LOW severity)
The same audit found two minor issues: imaginary/negative vibrational
modes silently produce NaN (`has_imaginary_frequencies` is computed but
ignored), and spin/multiplicity is inferred from charge parity only
(open-shell neutrals like O2/OH get the wrong electronic entropy).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

linearity() misclassifies off-center molecules (inertia tensor not COM-relocated)

1 participant