Skip to content

Commit 92cb3eb

Browse files
authored
Add integration tests checking the physical observables resulting from different the integrators (#521)
1 parent bcfa4d4 commit 92cb3eb

24 files changed

Lines changed: 3920 additions & 739 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ docs/reference/torch_sim.*
2121

2222
# ipynb files
2323
*.ipynb
24+
!docs/tutorials/integrator_tests_analysis.ipynb
2425

2526
# ignore trajectory files
2627
*.h5
@@ -38,6 +39,9 @@ coverage.xml
3839
# test cache (compiled models, etc.)
3940
tests/.cache/
4041

42+
# physical validation data and plots
43+
tests/physical_validation_data/
44+
4145
# env
4246
uv.lock
4347

docs/tutorials/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ versions of the tutorials can also be found in the `torch-sim /examples/tutorial
2020
hybrid_swap_tutorial
2121
using_graphpes_tutorial
2222
metatomic_tutorial
23+
integrator_tests_analysis

docs/tutorials/integrator_tests_analysis.ipynb

Lines changed: 1420 additions & 0 deletions
Large diffs are not rendered by default.

docs/user/reproducibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ sim_state.rng = 42 # required for reproducibility — torch.manual_seed() has n
5050

5151
### Deterministic vs stochastic integrators in TorchSim
5252

53-
- `ts.Integrator.nvt_langevin` and `ts.Integrator.npt_langevin` include stochastic
53+
- `ts.Integrator.nvt_langevin` and `ts.Integrator.npt_langevin_anisotropic` include stochastic
5454
terms by design. When seeded via `state.rng`, they produce identical trajectories.
5555
The `rng` generator controls **both** the initial momenta sampling **and** all per-step stochastic noise (Langevin OU noise, V-Rescale draws, C-Rescale barostat noise, etc.). It is stored on the state and automatically advances on every step, so running the same seed twice produces identical trajectories.
5656
- `ts.Integrator.nvt_nose_hoover` and `ts.Integrator.nve` are deterministic at the

examples/benchmarking/opt-throughput.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def run_ase_optimization(
199199
atoms = adaptor.get_atoms(Structure.from_dict(struct_dict))
200200
atoms.calc = calculator
201201
system: Any = cell_filter_cls(atoms) if cell_filter_cls is not None else atoms
202-
opt = ase_optimizer_cls(system, logfile=os.devnull) # type: ignore[arg-type]
202+
opt = ase_optimizer_cls(system, logfile=os.devnull)
203203
opt.run(fmax=f_max, steps=max_steps)
204204
if opt.get_number_of_steps() < max_steps:
205205
converged += 1

examples/scripts/3_dynamics.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@
323323
target_pressure = torch.tensor(0.0 * Units.pressure, device=device, dtype=dtype) # 0 bar
324324

325325
# Initialize NPT with NVT equilibration
326-
state = ts.npt_nose_hoover_init(
326+
state = ts.npt_nose_hoover_isotropic_init(
327327
state=state, model=mace_model_stress, kT=kT, dt=torch.tensor(dt)
328328
)
329329

@@ -337,12 +337,14 @@
337337
/ Units.temperature
338338
)
339339
invariant = float(
340-
ts.npt_nose_hoover_invariant(state, kT=kT, external_pressure=target_pressure)
340+
ts.npt_nose_hoover_isotropic_invariant(
341+
state, kT=kT, external_pressure=target_pressure
342+
)
341343
)
342344
log.info(
343345
f"Step {step}: Temperature: {temp.item():.4f} K, Invariant: {invariant:.4f}"
344346
)
345-
state = ts.npt_nose_hoover_step(
347+
state = ts.npt_nose_hoover_isotropic_step(
346348
state=state,
347349
model=mace_model_stress,
348350
dt=torch.tensor(dt),
@@ -351,7 +353,7 @@
351353
)
352354

353355
# Reinitialize for NPT phase
354-
state = ts.npt_nose_hoover_init(
356+
state = ts.npt_nose_hoover_isotropic_init(
355357
state=state, model=mace_model_stress, kT=kT, dt=torch.tensor(dt)
356358
)
357359

@@ -365,7 +367,9 @@
365367
/ Units.temperature
366368
)
367369
invariant = float(
368-
ts.npt_nose_hoover_invariant(state, kT=kT, external_pressure=target_pressure)
370+
ts.npt_nose_hoover_isotropic_invariant(
371+
state, kT=kT, external_pressure=target_pressure
372+
)
369373
)
370374
stress = mace_model_stress(state)["stress"]
371375
volume = torch.det(state.current_cell)
@@ -379,7 +383,7 @@
379383
f"Pressure: {pressure:.4f} eV/ų, "
380384
f"Cell: [{xx.item():.4f}, {yy.item():.4f}, {zz.item():.4f}]"
381385
)
382-
state = ts.npt_nose_hoover_step(
386+
state = ts.npt_nose_hoover_isotropic_step(
383387
state=state,
384388
model=mace_model_stress,
385389
dt=torch.tensor(dt),

pyproject.toml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies = [
3939
[project.optional-dependencies]
4040
test = [
4141
"torch-sim-atomistic[io,symmetry,vesin]",
42+
"physical-validation>=1.0.5",
4243
"platformdirs>=4.0.0",
4344
"psutil>=7.0.0",
4445
"pymatgen>=2025.6.14",
@@ -127,20 +128,25 @@ isort.lines-after-imports = 2
127128
pep8-naming.ignore-names = ["get_kT", "kT"]
128129

129130
[tool.ruff.lint.per-file-ignores]
130-
"**/tests/*" = ["ANN201", "D", "INP001", "S101"]
131-
"examples/**/*" = ["B018", "T201"]
131+
"**/tests/*" = ["ANN001", "ANN201", "ANN202", "D", "INP001", "S101", "T201"]
132+
"examples/**/*" = ["ANN001", "ANN201", "ANN202", "B018", "T201"]
132133
"examples/tutorials/**/*" = ["ALL"]
134+
"docs/tutorials/*.ipynb" = ["ANN001", "ANN201", "B905", "BLE001", "E501", "F841", "N816", "PLR1714", "RUF001", "T201"]
133135

134136
[tool.ruff.format]
135137
docstring-code-format = true
136138

137139
[tool.codespell]
138140
check-filenames = true
139141
ignore-words-list = ["convertor"] # codespell:ignore convertor
142+
skip = "docs/tutorials/integrator_tests_analysis.ipynb"
140143

141144
[tool.pytest.ini_options]
142-
addopts = ["-p no:warnings"]
145+
addopts = ["-p no:warnings", "-m not physical_validation"]
143146
testpaths = ["tests"]
147+
markers = [
148+
"physical_validation: long-running physical validation tests (run with: pytest -m physical_validation)",
149+
]
144150

145151
[tool.uv]
146152
# make these dependencies mutually exclusive since they use incompatible e3nn versions
@@ -220,18 +226,32 @@ include = [
220226
[tool.ty.overrides.rules]
221227
unresolved-import = "ignore"
222228

229+
[[tool.ty.overrides]]
230+
include = [
231+
"torch_sim/models/dispersion.py",
232+
"torch_sim/neighbors/vesin.py",
233+
]
234+
[tool.ty.overrides.rules]
235+
invalid-argument-type = "ignore"
236+
invalid-assignment = "ignore"
237+
223238
[[tool.ty.overrides]]
224239
include = ["tests/**/*.py"]
225240

226241
[tool.ty.overrides.rules]
227242
invalid-argument-type = "ignore"
243+
invalid-assignment = "ignore"
228244
no-matching-overload = "ignore"
229245
unresolved-attribute = "ignore"
230246
unresolved-import = "ignore"
231247

232248
[[tool.ty.overrides]]
233-
include = ["docs/**/*.py", "examples/**/*.py"]
249+
include = ["docs/**/*.py", "docs/**/*.ipynb", "examples/**/*.py"]
234250
[tool.ty.overrides.rules]
251+
invalid-argument-type = "ignore"
252+
not-iterable = "ignore"
253+
not-subscriptable = "ignore"
254+
unresolved-attribute = "ignore"
235255
unresolved-import = "ignore"
236256

237257

tests/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@
1515

1616
torch.set_num_threads(4)
1717

18+
19+
def pytest_addoption(parser: pytest.Parser) -> None:
20+
parser.addoption(
21+
"--validation-plots",
22+
action="store_true",
23+
default=False,
24+
help="Save physical validation plots to tests/physical_validation_data/plots/",
25+
)
26+
parser.addoption(
27+
"--clean-validation-data",
28+
action="store_true",
29+
default=False,
30+
help="Delete saved physical validation data before running tests",
31+
)
32+
33+
1834
DEVICE = torch.device("cpu")
1935
DTYPE = torch.float64
2036

tests/test_constraints.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,8 @@ def test_constraint_validation_errors(
555555
[
556556
("nve", FixAtoms(atom_idx=[0, 1]), 100),
557557
("nvt_nose_hoover", FixCom([0]), 200),
558-
("npt_langevin", FixAtoms(atom_idx=[0, 3]), 200),
559-
("npt_nose_hoover", FixCom([0]), 200),
558+
("npt_langevin_anisotropic", FixAtoms(atom_idx=[0, 3]), 200),
559+
("npt_nose_hoover_isotropic", FixCom([0]), 200),
560560
],
561561
)
562562
def test_integrators_with_constraints(
@@ -591,20 +591,20 @@ def test_integrators_with_constraints(
591591
state = ts.nvt_nose_hoover_init(cu_sim_state, lj_model, kT=kT, dt=dt)
592592
for _ in range(n_steps):
593593
state = ts.nvt_nose_hoover_step(state, lj_model, dt=dt, kT=kT)
594-
elif integrator == "npt_langevin":
595-
state = ts.npt_langevin_init(cu_sim_state, lj_model, kT=kT, dt=dt)
594+
elif integrator == "npt_langevin_anisotropic":
595+
state = ts.npt_langevin_anisotropic_init(cu_sim_state, lj_model, kT=kT, dt=dt)
596596
for _ in range(n_steps):
597-
state = ts.npt_langevin_step(
597+
state = ts.npt_langevin_anisotropic_step(
598598
state,
599599
lj_model,
600600
dt=dt,
601601
kT=kT,
602602
external_pressure=torch.tensor(0.0, dtype=DTYPE),
603603
)
604-
else: # npt_nose_hoover
605-
state = ts.npt_nose_hoover_init(cu_sim_state, lj_model, kT=kT, dt=dt)
604+
else: # npt_nose_hoover_isotropic
605+
state = ts.npt_nose_hoover_isotropic_init(cu_sim_state, lj_model, kT=kT, dt=dt)
606606
for _ in range(n_steps):
607-
state = ts.npt_nose_hoover_step(
607+
state = ts.npt_nose_hoover_isotropic_step(
608608
state,
609609
lj_model,
610610
dt=torch.tensor(0.001, dtype=DTYPE),

tests/test_fix_symmetry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,9 @@ def test_build_symmetry_map_chunked_matches_vectorized() -> None:
539539

540540
old_threshold = sym_mod._SYMM_MAP_CHUNK_THRESHOLD # noqa: SLF001
541541
try:
542-
sym_mod._SYMM_MAP_CHUNK_THRESHOLD = len(state.positions) + 1 # noqa: SLF001 # ty: ignore[invalid-assignment]
542+
sym_mod._SYMM_MAP_CHUNK_THRESHOLD = len(state.positions) + 1 # noqa: SLF001
543543
vectorized = build_symmetry_map(rotations, translations, frac)
544-
sym_mod._SYMM_MAP_CHUNK_THRESHOLD = 0 # noqa: SLF001 # ty: ignore[invalid-assignment]
544+
sym_mod._SYMM_MAP_CHUNK_THRESHOLD = 0 # noqa: SLF001
545545
chunked = build_symmetry_map(rotations, translations, frac)
546546
finally:
547547
sym_mod._SYMM_MAP_CHUNK_THRESHOLD = old_threshold # noqa: SLF001

0 commit comments

Comments
 (0)