Skip to content

fix: correct Guttman ring counts and speed up the search - #422

Open
Atilaac wants to merge 1 commit into
mainfrom
fix/guttman-rings-bug-perf
Open

fix: correct Guttman ring counts and speed up the search#422
Atilaac wants to merge 1 commit into
mainfrom
fix/guttman-rings-bug-perf

Conversation

@Atilaac

@Atilaac Atilaac commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Problem

compute_guttmann_rings contracted the T–O–T network into a T–T graph, discarding the bridging oxygens and adding an edge between every pair of formers on a shared oxygen. Three defects followed:

  1. Phantom 3-rings from oxygen triclusters. An oxygen bonded to three formers contributed C(3,2) = 3 edges — a triangle — reported as a 3-ring. In the real network that walk re-enters the same oxygen three times, so it is not a ring at all. It inflates the 3-ring count, which is the structural marker for the D₂ Raman band, and it bites hardest in aluminosilicates and borosilicates.
  2. Edge-sharing polyhedra were invisible. Two formers bridged by two oxygens produced the same edge twice, and nx.Graph collapses duplicates, so no 2-membered ring could be reported at any max_size.
  3. No periodic closure check. Neighbour lists return atom indices under minimum image, so a path that leaves the cell and re-enters through the opposite face comes back to the same index and looked like a ring. It is a helix, and it over-counts large rings in small cells.

Alongside these: nx.all_shortest_paths swept the entire graph for every edge with no early stop, making the search O(E·V); the parallel path shipped the whole edge list once per edge; max_size filtered after the fact rather than bounding the search; and _ring_is_primitive was dead code that both the module docstring and the theory doc claimed was applied. It was not — and Guttman shortest-path rings are a looser set than primitive rings, so those claims were wrong either way.

Solution

The search now runs on the bipartite T–O atom network: formers and oxygens are both nodes, and the only edges are T–O bonds. Both graph defects dissolve mechanically rather than by special-casing — BFS paths are simple, so a path cannot route through the same tricluster oxygen twice, and two bridging oxygens are two distinct nodes forming a genuine four-node cycle.

Candidate cycles must now close in real space: the minimum-image bond vectors summed around the loop must vanish, at a 1e-6 Å tolerance. When every shortest closure through a bond fails that test, the search deepens two atoms at a time until it finds the shortest one that does close, the same behaviour as the RINGS reference implementation.

The BFS stops as soon as the level containing the target is complete, touching only nodes within the ring's own radius instead of the whole network. That is where the speedup comes from, and it also makes max_size almost free.

networkx is no longer used here or anywhere else in the repo, so the dependency is dropped. Deduplication now keys on the full T–O cycle, keeping rings that share formers but differ in bridging oxygens distinct.

Performance

System max_size Before After
20Na2O–80SiO2, 300 atoms 12 0.017 s 0.008 s
20Na2O–10B2O3–70SiO2, 30,080 atoms 12 61.9 s 1.18 s
20Na2O–10B2O3–70SiO2, 30,080 atoms 40 64.3 s 1.15 s

Behaviour changes to be aware of

  • Ring histograms change for every structure. Jobs already cached in SQLite hold prefix numbers and need ?rerun=all.
  • n_cpus only engages above ~100,000 T–O bonds. Below that, the search stays sequential regardless, because starting worker processes costs more than it saves.
  • Public signature, return type, and the T-atom size convention are unchanged.

## Problem

`compute_guttmann_rings` contracted the T–O–T network into a T–T graph, discarding the
bridging oxygens and adding an edge between every pair of formers on a shared oxygen.
Three defects followed:

1. **Phantom 3-rings from oxygen triclusters.** An oxygen bonded to three formers
   contributed C(3,2) = 3 edges — a triangle — reported as a 3-ring. In the real network
   that walk re-enters the same oxygen three times, so it is not a ring at all. It
   inflates the 3-ring count, which is the structural marker for the D₂ Raman band, and
   it bites hardest in aluminosilicates and borosilicates.
2. **Edge-sharing polyhedra were invisible.** Two formers bridged by two oxygens produced
   the same edge twice, and `nx.Graph` collapses duplicates, so no 2-membered ring could
   be reported at any `max_size`.
3. **No periodic closure check.** Neighbour lists return atom indices under minimum image,
   so a path that leaves the cell and re-enters through the opposite face comes back to
   the same index and looked like a ring. It is a helix, and it over-counts large rings
   in small cells.

Alongside these: `nx.all_shortest_paths` swept the *entire* graph for every edge with no
early stop, making the search O(E·V); the parallel path shipped the whole edge list once
per edge; `max_size` filtered after the fact rather than bounding the search; and
`_ring_is_primitive` was dead code that both the module docstring and the theory doc
claimed was applied. It was not — and Guttman shortest-path rings are a looser set than
primitive rings, so those claims were wrong either way.

## Solution

The search now runs on the **bipartite T–O atom network**: formers and oxygens are both
nodes, and the only edges are T–O bonds. Both graph defects dissolve mechanically rather
than by special-casing — BFS paths are simple, so a path cannot route through the same
tricluster oxygen twice, and two bridging oxygens are two distinct nodes forming a genuine
four-node cycle.

Candidate cycles must now close in real space: the minimum-image bond vectors summed
around the loop must vanish, at a 1e-6 Å tolerance. When every shortest closure through a
bond fails that test, the search deepens two atoms at a time until it finds the shortest
one that does close, the same behaviour as the RINGS reference implementation.

The BFS stops as soon as the level containing the target is complete, touching only nodes
within the ring's own radius instead of the whole network. That is where the speedup comes
from, and it also makes `max_size` almost free.

`networkx` is no longer used, here or anywhere else in the repo, so the dependency is
dropped. Deduplication now keys on the full T–O cycle, keeping rings that share formers
but differ in bridging oxygens distinct.

## Performance

| System | `max_size` | Before | After |
|---|---|---|---|
| 20Na2O–80SiO2, 300 atoms | 12 | 0.017 s | 0.008 s |
| 20Na2O–10B2O3–70SiO2, 30,080 atoms | 12 | 61.9 s | 1.18 s |
| 20Na2O–10B2O3–70SiO2, 30,080 atoms | 40 | 64.3 s | 1.15 s |

## Behaviour changes to be aware of

- Ring histograms change for every structure. Jobs already cached in SQLite hold pre-fix
  numbers and need `?rerun=all`.
- The smallest reportable ring size is now **2**, previously 3. Downstream code assuming a
  floor of 3 should be checked.
- `n_cpus` only engages above ~100,000 T–O bonds. Below that the search stays sequential
  regardless, because starting worker processes costs more than it saves now that the
  search itself is ~35 µs/bond.
- Public signature, return type, and the T-atom size convention are unchanged.
@github-actions github-actions Bot added the type: bug Something isn't working label Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.02262% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...spy/src/amorphouspy/properties/structural/rings.py 95.02% 11 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Atilaac
Atilaac requested a review from ltalirz August 25, 2026 17:24
@Atilaac Atilaac added the integration Tag a PR with this to run integration tests label Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration Tag a PR with this to run integration tests type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant