fix: correct Guttman ring counts and speed up the search - #422
Open
Atilaac wants to merge 1 commit into
Open
Conversation
## 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.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
compute_guttmann_ringscontracted 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:nx.Graphcollapses duplicates, so no 2-membered ring could be reported at anymax_size.Alongside these:
nx.all_shortest_pathsswept 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_sizefiltered after the fact rather than bounding the search; and_ring_is_primitivewas 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_sizealmost free.networkxis 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
max_sizeBehaviour changes to be aware of
?rerun=all.n_cpusonly engages above ~100,000 T–O bonds. Below that, the search stays sequential regardless, because starting worker processes costs more than it saves.