@@ -181,3 +181,112 @@ def strained_energy(strain: torch.Tensor) -> torch.Tensor:
181181test_d3_r2scan_outputs = make_validate_model_outputs_test (
182182 model_fixture_name = "d3_model_r2scan" , device = DEVICE , dtype = DTYPE
183183)
184+
185+
186+ def test_d3_molecular_neighbor_cap_is_exact (monkeypatch : pytest .MonkeyPatch ) -> None :
187+ """Non-periodic states size the neighbor matrix to the largest system, exactly."""
188+ from ase .build import molecule
189+
190+ import torch_sim .models .dispersion as dispersion_module
191+ from torch_sim .neighbors import torchsim_nl
192+
193+ state = ts .io .atoms_to_state (
194+ [molecule ("H2O" ), molecule ("C6H6" ), molecule ("CH3CH2OH" )],
195+ device = DEVICE ,
196+ dtype = DTYPE ,
197+ )
198+ kwargs = {
199+ ** PBE_BJ ,
200+ "d3_params" : _make_d3_params (),
201+ "cutoff" : 50.0 ,
202+ "device" : DEVICE ,
203+ "dtype" : DTYPE ,
204+ "compute_stress" : False ,
205+ }
206+ seen : list [int | None ] = []
207+
208+ def spy_nl (* args : object , ** nl_kwargs : object ) -> object :
209+ seen .append (nl_kwargs .get ("max_neighbors" ))
210+ return torchsim_nl (* args , ** nl_kwargs )
211+
212+ def uncapped_nl (* args : object ) -> object :
213+ # A different callable than torchsim_nl, so the model leaves the width alone.
214+ return torchsim_nl (* args )
215+
216+ capped = D3DispersionModel (** kwargs )
217+ uncapped = D3DispersionModel (** kwargs , neighbor_list_fn = uncapped_nl )
218+ monkeypatch .setattr (dispersion_module , "torchsim_nl" , spy_nl )
219+ capped .neighbor_list_fn = spy_nl
220+
221+ out_capped = capped (state )
222+ out_uncapped = uncapped (state )
223+
224+ assert seen == [11 ] # benzene, 12 atoms
225+ torch .testing .assert_close (out_capped ["energy" ], out_uncapped ["energy" ])
226+ torch .testing .assert_close (out_capped ["forces" ], out_uncapped ["forces" ])
227+
228+
229+ def test_d3_molecular_cap_never_widens_default (monkeypatch : pytest .MonkeyPatch ) -> None :
230+ """Systems larger than the bulk-density estimate keep the default width."""
231+ import torch_sim .models .dispersion as dispersion_module
232+ from torch_sim .neighbors import torchsim_nl
233+
234+ seen : list [int | None ] = []
235+
236+ def spy_nl (* args : object , ** nl_kwargs : object ) -> object :
237+ seen .append (nl_kwargs .get ("max_neighbors" ))
238+ return torchsim_nl (* args , ** nl_kwargs )
239+
240+ monkeypatch .setattr (dispersion_module , "torchsim_nl" , spy_nl )
241+ model = D3DispersionModel (
242+ ** PBE_BJ ,
243+ d3_params = _make_d3_params (),
244+ cutoff = 2.0 , # estimate_max_neighbors(2.0) == 16
245+ device = DEVICE ,
246+ dtype = DTYPE ,
247+ compute_stress = False ,
248+ neighbor_list_fn = spy_nl ,
249+ )
250+ n_atoms = 40
251+ positions = torch .zeros (n_atoms , 3 , dtype = DTYPE )
252+ positions [:, 0 ] = 1.5 * torch .arange (n_atoms , dtype = DTYPE )
253+ state = ts .SimState (
254+ positions = positions ,
255+ masses = torch .ones (n_atoms , dtype = DTYPE ),
256+ cell = torch .zeros (1 , 3 , 3 , dtype = DTYPE ),
257+ pbc = False ,
258+ atomic_numbers = torch .full ((n_atoms ,), 6 ),
259+ system_idx = torch .zeros (n_atoms , dtype = torch .int64 ),
260+ )
261+ model (state )
262+ assert seen == [None ]
263+
264+
265+ def test_d3_periodic_state_keeps_default_neighbor_width () -> None :
266+ """Periodic states must not be capped: images add neighbors beyond n_atoms - 1."""
267+ from torch_sim .neighbors import torchsim_nl
268+
269+ seen : list [int | None ] = []
270+
271+ def spy_nl (* args : object , ** nl_kwargs : object ) -> object :
272+ seen .append (nl_kwargs .get ("max_neighbors" ))
273+ return torchsim_nl (* args , ** nl_kwargs )
274+
275+ model = D3DispersionModel (
276+ ** PBE_BJ ,
277+ d3_params = _make_d3_params (),
278+ cutoff = 12.0 ,
279+ device = DEVICE ,
280+ dtype = DTYPE ,
281+ neighbor_list_fn = spy_nl ,
282+ )
283+ state = ts .SimState (
284+ positions = torch .tensor ([[0.0 , 0.0 , 0.0 ], [1.0 , 1.0 , 1.0 ]], dtype = DTYPE ),
285+ masses = torch .ones (2 , dtype = DTYPE ),
286+ cell = (4.0 * torch .eye (3 , dtype = DTYPE )).unsqueeze (0 ),
287+ pbc = True ,
288+ atomic_numbers = torch .tensor ([6 , 8 ]),
289+ system_idx = torch .zeros (2 , dtype = torch .int64 ),
290+ )
291+ model (state )
292+ assert seen == [None ]
0 commit comments