|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import math |
| 4 | + |
3 | 5 | import numpy as np |
4 | 6 | import pytest |
5 | 7 | import torch |
|
15 | 17 | spherical_bessel_smooth, |
16 | 18 | ) |
17 | 19 | from matgl.layers._three_body import combine_sbf_shf |
| 20 | +from matgl.utils.maths import _get_lambda_func |
18 | 21 |
|
19 | 22 |
|
20 | 23 | def test_gaussian(): |
@@ -147,3 +150,44 @@ def test_fourier_expansion(learnable): |
147 | 150 | assert fe.frequencies.requires_grad |
148 | 151 | else: |
149 | 152 | 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