Skip to content

Commit 5d5d1a4

Browse files
authored
Merge branch 'main' into fix-alchmtk-compute-distances
2 parents b035453 + 2c66ccd commit 5d5d1a4

2 files changed

Lines changed: 50 additions & 4 deletions

File tree

src/matgl/layers/_basis.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
from __future__ import annotations
3737

38-
from functools import lru_cache
3938
from math import pi, sqrt
4039

4140
import sympy
@@ -129,7 +128,6 @@ def __init__(self, max_l: int, max_n: int = 5, cutoff: float = 5.0, smooth: bool
129128
self.register_buffer("roots_slice", roots_slice, persistent=False)
130129
self.register_buffer("inv_norm", inv_norm, persistent=False)
131130

132-
@lru_cache(maxsize=128)
133131
def _calculate_symbolic_funcs(self) -> list:
134132
"""Generate spherical basis functions based on Rayleigh formula.
135133
@@ -140,9 +138,13 @@ def _calculate_symbolic_funcs(self) -> list:
140138
funcs = [sympy.expand_func(sympy.functions.special.bessel.jn(i, x)) for i in range(self.max_l + 1)]
141139
return [sympy.lambdify(x, func, torch) for func in funcs]
142140

143-
@lru_cache(maxsize=128)
144141
def _calculate_smooth_symbolic_funcs(self) -> list:
145-
return _get_lambda_func(max_n=self.max_n, cutoff=self.cutoff)
142+
# ``self.cutoff`` is a buffer whose dtype follows the ambient default;
143+
# feeding the tensor into the symbolic builder makes ``cutoff**1.5``
144+
# a float32 op under the default dtype, rounding the basis prefactor
145+
# once and scaling every generated coefficient by 1 + 2.6e-8. The
146+
# non-smooth branch already guards this with ``float(cutoff)``.
147+
return _get_lambda_func(max_n=self.max_n, cutoff=float(self.cutoff))
146148

147149
def forward(self, r: torch.Tensor) -> torch.Tensor:
148150
"""Compute the spherical Bessel function values.

tests/layers/test_basis.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import math
4+
35
import numpy as np
46
import pytest
57
import torch
@@ -15,6 +17,7 @@
1517
spherical_bessel_smooth,
1618
)
1719
from matgl.layers._three_body import combine_sbf_shf
20+
from matgl.utils.maths import _get_lambda_func
1821

1922

2023
def test_gaussian():
@@ -147,3 +150,44 @@ def test_fourier_expansion(learnable):
147150
assert fe.frequencies.requires_grad
148151
else:
149152
assert not fe.frequencies.requires_grad
153+
154+
155+
@pytest.fixture
156+
def restore_dtype():
157+
old = torch.get_default_dtype()
158+
yield
159+
torch.set_default_dtype(old)
160+
161+
162+
def test_smooth_sbf_matches_closed_form(restore_dtype):
163+
"""The n=0 basis function is sqrt(2)(2 sin(pi r/5) + sin(2 pi r/5))/(5 r)."""
164+
torch.set_default_dtype(torch.float32)
165+
sbf = SphericalBesselFunction(max_l=3, max_n=3, cutoff=5.0, smooth=True)
166+
r = torch.linspace(0.3, 5.0, 257, dtype=torch.float64)
167+
want = math.sqrt(2.0) * (2 * torch.sin(math.pi * r / 5) + torch.sin(2 * math.pi * r / 5)) / (5 * r)
168+
got = sbf(r)[:, 0]
169+
# `want` has an exact zero at r == cutoff, so the tolerance is normalised to
170+
# the amplitude of the basis function rather than applied pointwise.
171+
scale = want.abs().max()
172+
assert (got - want).abs().max() <= 1e-12 * scale
173+
174+
175+
def test_smooth_sbf_independent_of_default_dtype(restore_dtype):
176+
"""The basis must not change with the ambient default dtype."""
177+
r = torch.linspace(0.3, 5.0, 257, dtype=torch.float64)
178+
out = {}
179+
for dtype in (torch.float32, torch.float64):
180+
torch.set_default_dtype(dtype)
181+
out[dtype] = SphericalBesselFunction(3, 3, 5.0, smooth=True)(r)
182+
assert torch.equal(out[torch.float32], out[torch.float64])
183+
184+
185+
def test_smooth_sbf_lambda_cache_is_reused(restore_dtype):
186+
"""Identical modules must share the cached symbolic functions."""
187+
torch.set_default_dtype(torch.float32)
188+
_get_lambda_func.cache_clear()
189+
for _ in range(4):
190+
SphericalBesselFunction(3, 3, 5.0, smooth=True)
191+
info = _get_lambda_func.cache_info()
192+
assert info.currsize == 1, f"cache did not coalesce: {info}"
193+
assert info.hits == 3, f"cache never hit: {info}"

0 commit comments

Comments
 (0)