Skip to content

Fix non-periodic cell in get_neighborhood: extent-based padding + physical cell on partial PBC#1533

Open
aacostadiaz wants to merge 7 commits into
ACEsuit:developfrom
aacostadiaz:fix/neighborhood-cell-combined
Open

Fix non-periodic cell in get_neighborhood: extent-based padding + physical cell on partial PBC#1533
aacostadiaz wants to merge 7 commits into
ACEsuit:developfrom
aacostadiaz:fix/neighborhood-cell-combined

Conversation

@aacostadiaz

@aacostadiaz aacostadiaz commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Combines and reconciles two open fixes to get_neighborhood's handling of the fictitious cell used along non-periodic directions. They touch the same lines and conflict textually, so they are merged into one coherent change. Supersedes #1473 and #1523.

What changed

get_neighborhood now:

  1. Sizes the blown-up cell from the atom extent (extent + 2*cutoff + 1) instead of max(abs(positions)) * 5 * cutoff — origin-independent, far smaller, identical neighbour lists (fixes the OOM).
  2. Uses that extended cell only for the matscipy neighbour search.
  3. Returns the physical cell whenever any axis is periodic, so stress normalizes by the true volume (fixes the slab stress). Fully aperiodic systems keep the extended cell. The input array is no longer mutated.

Effect on electrostatic models (PolarMACE)

The returned cell also becomes AtomicData's volume/rcell, which drive PolarMACE's electrostatics. graph_longrange already applies the correct boundary treatment but they divide by det(cell). With the old inflated cell that volume was fake, so the corrections were silently scaled away; returning the physical cell gives them the
right volume. So this change is not only a stress fix, it also makes PolarMACE's slab/molecule electrostatic corrections numerically correct. (graph_longrange only supports TTT/TTF and raises otherwise, so no other partial-PBC case is affected.) Adequate vacuum along the non-periodic axis remains the caller's responsibility.


Note

Medium Risk
Touches core graph construction used by all models; slab stress and PolarMACE electrostatics numerics change by design, though behaviour is heavily regression-tested.

Overview
get_neighborhood no longer mutates the caller’s cell and fixes how fictitious non-periodic cell rows are built and what gets returned downstream.

For matscipy neighbour binning only, non-periodic axes use an extent + cutoff padded cell (extent + 2*cutoff + 1) instead of max(|positions|) * 5 * cutoff, so padding is origin-independent and much smaller—avoiding huge k-space boxes that could OOM PolarMACE electrostatics while keeping the same neighbour lists.

When any axis is periodic, the function returns the physical cell (with a fallback extended row if a non-periodic axis is all zeros), so AtomicData volume/rcell, slab stress (virial / det(cell)), and long-range dipole corrections use the real box volume. Fully aperiodic systems still return the extended cell.

Tests cover partial-PBC slabs (PolarMACE cell contract and vacuum convergence), brute-force neighbour parity on skewed slabs, extent-based aperiodic cells, slab stress vs finite differences, and looser cueq stress parity bounds after the smaller cell volume.

Reviewed by Cursor Bugbot for commit 6cca0b2. Bugbot is set up for automated code reviews on this repo. Configure here.

samblau and others added 5 commits July 22, 2026 10:08
get_neighborhood extends the cell along non-periodic directions so that
matscipy can bin atoms, but the extended cell leaked out of the function
and was stored in AtomicData. Stress is normalized by det(cell), so for
slabs the analytic stress was silently scaled down by the ratio of the
artificial extent to the true vacuum extent (~18x in the reported case),
while energies and forces were unaffected.

Keep the extended cell internal to the neighbour-list call and return
the physical cell whenever at least one direction is periodic. Shifts
are unchanged because unit_shifts are zero along non-periodic
directions. Fully non-periodic systems retain the previous behaviour.

Add a finite-difference regression test comparing analytic stress with
the strain derivative of the energy on a slab.
The combined ACEsuit#1473 + ACEsuit#1509 fix makes get_neighborhood return the physical
cell for partially periodic systems. That cell also becomes AtomicData's
volume/rcell, which drive PolarMACE's k-space electrostatics, so the change
is not stress-only for electrostatic models.

- test_polar_slab_partial_pbc_cell_contract: deterministic guard that a slab
  gets the physical cell (not the matscipy blow-up), that volume/rcell derive
  from it, that the short-range graph is invariant to the vacuum, and that the
  forward is finite on a partial-PBC electrostatic model (a path neither PR
  exercised).
- test_polar_slab_electrostatics_converge_with_vacuum: physics check for the
  reviewer's concern that the vacuum affects the electrostatic part. On a
  dipole-free slab the 3D k-space sum must converge as the vacuum grows;
  non-convergence would signal the need for a 2D / slab-corrected sum.
… cell

The extent-based non-periodic cell (from the neighborhood fix) has ~2 orders
smaller volume than the old max(|pos|)*5*cutoff cell, so stress = virial/det(cell)
scales up proportionally. The e3nn<->cueq parity is unchanged, but the absolute
stress bounds (1e-8 float32, 1e-11 float64) were calibrated against the old
inflated cell and now trip at ~2.3e-8. Loosen to 1e-6 / 1e-9; energy and force
bounds are untouched (they don't divide by det(cell)).
Mutation testing showed two gaps in the added tests:

- Nothing guarded the extent-based non-periodic cell (the ACEsuit#1473 OOM fix):
  reverting to max(|positions|)*5*cutoff kept every test green. Add
  test_nonperiodic_cell_is_extent_based, which asserts the fictitious cell is
  origin-independent (same molecule shifted far away gets the same cell) and
  tracks the atom extent + cutoff padding. Verified to fail on the old formula.

- test_polar_slab_electrostatics_converge_with_vacuum was documented as if it
  exercised the Yeh-Berkowitz slab correction, but its slab is dipole-free and
  converges with the correction on or off (verified by toggling
  include_pbc_corrections). Reword the docstring to scope it honestly as a
  finite/non-diverging smoke check, not a guard of the correction.
@bernstei

Copy link
Copy Markdown
Collaborator

atom extent (extent + 2*cutoff + 1)

Has anyone carefully thought about whether the implemented solution works when the cell vectors are not orthogonal? It's easy for naive heuristics to fail under these circumstances

@aacostadiaz
aacostadiaz marked this pull request as ready for review July 22, 2026 13:12
get_neighborhood replaces the non-periodic (vacuum) row of the cell with an
axis-aligned vector for the matscipy binning. This verifies the resulting
neighbour list is still exact on non-orthogonal slabs, where the periodic
vectors keep their true skewed directions. Parametrized over hexagonal,
monoclinic and triclinic in-plane lattices x two cutoffs x three random
configurations (18 cases), compared edge-by-edge (indices + shift vectors)
against a brute-force reference whose image range grows until the edge set
converges (complete regardless of skew).
@aacostadiaz
aacostadiaz deployed to gpu-internal July 24, 2026 10:59 — with GitHub Actions Active
@aacostadiaz

Copy link
Copy Markdown
Collaborator Author

atom extent (extent + 2*cutoff + 1)

Has anyone carefully thought about whether the implemented solution works when the cell vectors are not orthogonal? It's easy for naive heuristics to fail under these circumstances

Good point. I added tests that check the neighbour list on skewed (non-orthogonal) cells: hexagonal, monoclinic and triclinic slabs, with different cutoffs and random atom positions. Each one is compared atom by atom against a brute-force reference and they all match, so the extent-based cell works fine here.

Comment thread mace/data/neighborhood.py
for dim in range(3):
if not pbc[dim]:
extent = positions[:, dim].max() - positions[:, dim].min()
extended_cell[dim, :] = (extent + 2 * cutoff + 1) * identity[dim, :]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I want to think about it more, but I don't understand how this can possibly work in general. It replaces periodic vector a_i with a vector along Cartesian direction i, with 0-x, 1-y, and 2-z, which doesn't necessarily have anything to do with a_i.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For example, will this work with

[[0, 5, 0],
 [5, 0, 0],
 [0, 0, -5]]

and pbc=[False, True, True]? Won't it become

[[L, 0, 0],
 [5, 0, 0],
 [0, 0, -5]]

where L is the related to the x extent of the system?

If a system is non-periodic along a dimension, I think you need to make that vector along np.cross(a[i+1 % 3], a[i + 2 % 3]) with some sufficient magnitude, which is not entirely trivial to calculate. And the possibility of magnitude 0 vectors complicates this a bit more. You probably have to construct 1 or 2 (or 3) vectors that are independent, to use as dummy directions, before doing the cross product.

@gabor1

gabor1 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Can we propose that neighbourlist packages, such as matscipy take on this functionality, of determining the minimum padding?

@ilyes319

Copy link
Copy Markdown
Contributor

I agree that should be the case.

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.

6 participants