Skip to content

Do not record the identity as a rotation axis in PointGroupAnalyzer - #134

Open
HiroYokoyama wants to merge 2 commits into
materialsproject:mainfrom
HiroYokoyama:fix/point-group-identity-recorded-as-axis
Open

Do not record the identity as a rotation axis in PointGroupAnalyzer#134
HiroYokoyama wants to merge 2 commits into
materialsproject:mainfrom
HiroYokoyama:fix/point-group-identity-recorded-as-axis

Conversation

@HiroYokoyama

Copy link
Copy Markdown

Summary

I found an issue in the symmetry analyzer: when a molecule whose only rotational symmetry is a single C2 axis (e.g., tetramethylhydrazine) is analyzed, sch_symbol reports it as D2, while get_symmetry_operations() returns only 2 operations. D2 is a group of order 4, so this is a self-contradiction.
I then used Claude Opus 5 to analyze the issue, and found that _check_rot_sym() registers a 360° rotation (the identity, which is valid about any axis) in rot_sym as a rotation axis, so the condition len(rot_sym) >= 2 in _proc_sym_top() routes the molecule to the dihedral branch.

Measured data:

C2 rotation residual about each principal axis (max atom displacement):
   axis [ 0.9866  0.122 -0.1085]   residual = 1.4231 A
   axis [-0.1375  0.2628 -0.955 ]  residual = 1.4232 A
   axis [-0.088   0.9571  0.2761]  residual = 0.0001 A
Best over 40000 random axes at least 10 deg away from that axis: 0.7332 A

                      before      after
tetramethylhydrazine  D2 / 2 ops  C2 / 2 ops
water C2v/4, methane Td/24, ammonia C3v/6, allene D2d/8,
benzene D6h/24, H2O2 C2/2, ethene D2h/8, HCl C*v/1, CO2 D*h/2   (unchanged)

This PR stops the loop in _check_rot_sym() at 2, and also removes the if len(self.rot_sym) > 0: guard in _proc_sym_top() so that the perpendicular C2 search always runs; that guard was only ever satisfied by the phantom entry, so fixing the loop alone would make these molecules report as C1 instead.

This PR also adds a regression test for tetramethylhydrazine in tests/symmetry/test_analyzer.py; the test fails on main with assert 'D2' == 'C2'.

This may be a breaking change:
sch_symbol changes for molecules whose unique axis of inertia carries no rotation of its own but which have one perpendicular C2 axis: they were reported as D2 and are now reported as C2. get_symmetry_operations() and get_pointgroup() are unaffected — they already returned the 2-operation group. Code that compared sch_symbol against the previous (incorrect) value must be updated; nothing else is affected.

Checklist

  • Tests for the affected code pass locally: pytest tests/symmetry (92 passed / 5 skipped, before the patch 91/5)
  • Lint passes: ruff check / ruff format --check passed

Breaking changes?

  • This PR contains breaking changes (removals/renames, signature or
    behavior changes, changed defaults/output, dropped Python versions, ...).
    If so, prefix the title with [breaking] and describe the change and
    migration steps under ## Breaking Changes below.

Breaking Changes

_check_rot_sym() walked idx down to 1, where the operation tested is a
360-degree rotation - the identity - which is_valid_op() accepts about any
axis. It therefore appended a (axis, 1) entry to self.rot_sym for every axis
it was given.

_proc_sym_top() reads len(self.rot_sym) >= 2 as "dihedral", so a molecule
whose unique axis carries no rotation of its own but which has one
perpendicular C2 ended up with rot_sym == [(axis, 1), (perp_axis, 2)] and was
named D{rot} by _proc_dihedral(). get_symmetry_operations() is generated from
the real generators and knows nothing about the phantom axis, so the analyzer
contradicted itself: sch_symbol "D2", a group of order 4, alongside 2
operations.

Tetramethylhydrazine, (CH3)2N-N(CH3)2, shows this on an undistorted geometry
at every tolerance from 0.01 to 0.5 A. Its only C2 axis reproduces the
structure to 1e-4 A; a C2 about either of the other two principal axes leaves
atoms 1.42 A from their nearest same-element neighbour, and a search over
20000 random axes finds no third one. The molecule is C2.

The loop now stops at 2, so rot_sym only ever holds genuine rotation axes.
_proc_sym_top() must then look for a perpendicular C2 unconditionally: that
search used to be gated on len(self.rot_sym) > 0, a condition only ever
satisfied by the phantom entry, and gating it on a now-empty rot_sym would
report C1 for these molecules instead.

tests/symmetry passes unchanged (91 passed, 5 skipped before and after) plus
the added regression test.

@shyuep shyuep left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review generated by Claude (posted by @shyuep's review bot).

Diagnosis and fix are correct. idx=1 in _check_rot_sym registered a 360° rotation (the identity) as a rotation axis about any axis, so len(rot_sym) >= 2 misrouted single-perpendicular-C2 molecules to _proc_dihedral (D2 symbol with only 2 ops — self-contradictory). Stopping the loop at 2 and unconditionally running _check_perpendicular_r2_axis is the right pair of changes; either alone would be wrong (C1 regression), as the PR notes.

Two points to verify before merge:

  1. rot_sym can now be empty after _check_rot_sym where the phantom identity entry previously guaranteed at least one element. Please audit the other callers (_proc_asym_top, _proc_cyclic, spherical-top path) for any self.rot_sym[0] access or len(rot_sym) > 0 logic that implicitly relied on the identity entry. The passing test suite suggests this is fine, but an explicit check is warranted since the change is in a shared helper.
  2. The body states this may change sch_symbol for affected molecules but the breaking-change box is unticked — per repo convention, tick it and prefix the title with [breaking].

Test is well-constructed (regression on Schoenflies symbol, operation count, and rot_sym order). CI is green. LGTM modulo point 1.

_proc_sym_top no longer guarantees a non-empty rot_sym, so the fall-through
into max(self.rot_sym) raised ValueError for an isotropic inertia tensor with
no rotational symmetry. Previously that molecule was reported as D1, which
is not a point group, from the same phantom identity entry.
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.

2 participants