Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ uv.lock

# IDE
.vscode/

# duecredit
.duecredit.p
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,27 @@ TorchSim is released under an [MIT license](LICENSE).
## Citation

If you use TorchSim in your research, please cite our [publication](https://iopscience.iop.org/article/10.1088/3050-287X/ae1799).

```bibtex
@article{cohen2025torchsim,
title={TorchSim: An efficient atomistic simulation engine in PyTorch},
author={Cohen, Orion and Riebesell, Janosh and Goodall, Rhys and Kolluru, Adeesh and Falletta, Stefano and Krause, Joseph and Colindres, Jorge and Ceder, Gerbrand and Gangan, Abhijeet S},
journal={AI for Science},
volume={1},
number={2},
pages={025003},
year={2025},
publisher={IOP Publishing},
doi={10.1088/3050-287X/ae1799}
}
```

## Due Credit

We aim to recognize all [duecredit](https://github.com/duecredit/duecredit) for the decades of work that TorchSim builds on top of, an automated list of references can be obtained for the package by running `DUECREDIT_ENABLE=yes uv run --with . --extra docs --extra test python -m duecredit <(printf 'import pytest\nraise SystemExit(pytest.main(["-q"]))\n')`. This list is incomplete and we welcome PRs to help improve our citation coverage.

To collect citations for a specific tutorial run, for example autobatching, use:

```sh
DUECREDIT_ENABLE=yes uv run --with . --extra docs --extra test python -m duecredit examples/tutorials/autobatching_tutorial.py
```
9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,14 @@ dependencies = [

[project.optional-dependencies]
test = [
"ase>=3.26",
"phonopy>=2.37.0",
"torch-sim-atomistic[io,symmetry]",
"platformdirs>=4.0.0",
"psutil>=7.0.0",
"pymatgen>=2025.6.14",
"pytest-cov>=6",
"pytest>=8",
"moyopy>=0.3",
"spglib>=2.6",
]
io = ["ase>=3.26", "phonopy>=2.37.0", "pymatgen>=2025.6.14"]
symmetry = ["moyopy>=0.3"]
symmetry = ["moyopy>=0.3", "spglib>=2.6"]
mace = ["mace-torch>=0.3.14"]
mattersim = ["mattersim>=0.1.2"]
metatomic = ["metatomic-torch>=0.1.3", "metatrain[pet]>=2025.12"]
Expand All @@ -59,6 +55,7 @@ nequip = ["nequip>=0.16.2"]
fairchem = ["fairchem-core>=2.7", "scipy<1.17.0"]
docs = [
"autodoc_pydantic==2.2.0",
"duecredit>=0.11",
"furo==2024.8.6",
"ipython==8.34.0",
"ipykernel==6.30.1",
Expand Down
2 changes: 1 addition & 1 deletion tests/models/test_fairchem.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import torch_sim as ts
from torch_sim.models.fairchem import FairChemModel

except ImportError:
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(
f"FairChem not installed: {traceback.format_exc()}", allow_module_level=True
)
Expand Down
2 changes: 1 addition & 1 deletion tests/models/test_fairchem_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from torch_sim.models.fairchem_legacy import FairChemV1Model

except ImportError:
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(
f"FairChem not installed: {traceback.format_exc()}", allow_module_level=True
)
Expand Down
5 changes: 3 additions & 2 deletions tests/models/test_graphpes_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
make_model_calculator_consistency_test,
make_validate_model_outputs_test,
)
from torch_sim.models.graphpes import GraphPESWrapper
from torch_sim.testing import CONSISTENCY_SIMSTATES


try:
from graph_pes.atomic_graph import AtomicGraph, to_batch
from graph_pes.models import LennardJones, SchNet, TensorNet
except ImportError:

from torch_sim.models.graphpes_framework import GraphPESWrapper
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(
f"graph-pes not installed: {traceback.format_exc()}", allow_module_level=True
)
Expand Down
8 changes: 4 additions & 4 deletions tests/models/test_mace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
make_model_calculator_consistency_test,
make_validate_model_outputs_test,
)
from torch_sim.models.mace import MaceUrls
from torch_sim.testing import SIMSTATE_BULK_GENERATORS, SIMSTATE_MOLECULE_GENERATORS


try:
from mace.calculators import MACECalculator
from mace.calculators.foundations_models import mace_mp, mace_off

from torch_sim.models.mace import MaceModel
except (ImportError, ValueError):
from torch_sim.models.mace import MaceModel, MaceUrls

except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(f"MACE not installed: {traceback.format_exc()}", allow_module_level=True)

# mace_omol is optional (added in newer MACE versions)
Expand All @@ -28,7 +28,7 @@

raw_mace_omol = mace_omol(model="extra_large", return_raw_model=True)
HAS_MACE_OMOL = True
except ImportError:
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
raw_mace_omol = None
HAS_MACE_OMOL = False

Expand Down
2 changes: 1 addition & 1 deletion tests/models/test_mattersim.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from torch_sim.models.mattersim import MatterSimModel

except ImportError:
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(
f"mattersim not installed: {traceback.format_exc()}", allow_module_level=True
)
Expand Down
4 changes: 2 additions & 2 deletions tests/models/test_orb.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
make_validate_model_outputs_test,
)
from torch_sim import SimState
from torch_sim.models.orb import cell_to_cellpar
from torch_sim.testing import SIMSTATE_GENERATORS


try:
from orb_models.forcefield import pretrained
from orb_models.forcefield.calculator import ORBCalculator

from torch_sim.models.orb import OrbModel
from torch_sim.models.orb import OrbModel, cell_to_cellpar

except ImportError:
pytest.skip(f"ORB not installed: {traceback.format_exc()}", allow_module_level=True)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_elastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from mace.calculators.foundations_models import mace_mp

from torch_sim.models.mace import MaceModel
except ImportError:
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(f"MACE not installed: {traceback.format_exc()}", allow_module_level=True)


Expand Down
33 changes: 20 additions & 13 deletions tests/test_neighbors.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,24 +467,31 @@ def test_vesin_nl_edge_cases() -> None:
assert len(mapping[0]) > 0 # Should find neighbors


def test_torchsim_nl_availability() -> None:
def test_vesin_nl_availability() -> None:
"""Test that availability flags are correctly set."""
assert isinstance(neighbors.VESIN_AVAILABLE, bool)

assert callable(neighbors.vesin_nl)
assert callable(neighbors.vesin_nl_ts)

if not neighbors.VESIN_AVAILABLE:
with pytest.raises(ImportError, match="Vesin is not installed"):
neighbors.vesin_nl()
with pytest.raises(ImportError, match="Vesin is not installed"):
neighbors.vesin_nl_ts()


def test_alchemiops_nl_availability() -> None:
assert isinstance(neighbors.ALCHEMIOPS_AVAILABLE, bool)

if neighbors.VESIN_AVAILABLE:
assert neighbors.VesinNeighborList is not None
assert neighbors.VesinNeighborListTorch is not None
else:
assert neighbors.VesinNeighborList is None
assert neighbors.VesinNeighborListTorch is None
assert callable(neighbors.alchemiops_nl_n2)
assert callable(neighbors.alchemiops_nl_cell_list)

if neighbors.ALCHEMIOPS_AVAILABLE:
assert neighbors.alchemiops_nl_n2 is not None
assert neighbors.alchemiops_nl_cell_list is not None
else:
assert neighbors.alchemiops_nl_n2 is None
assert neighbors.alchemiops_nl_cell_list is None
if not neighbors.ALCHEMIOPS_AVAILABLE:
with pytest.raises(ImportError, match="nvalchemiops is not installed"):
neighbors.alchemiops_nl_n2()
with pytest.raises(ImportError, match="nvalchemiops is not installed"):
neighbors.alchemiops_nl_cell_list()


@pytest.mark.skipif(
Expand Down
23 changes: 8 additions & 15 deletions tests/test_optimizers_vs_ase.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@

import torch_sim as ts
from tests.conftest import DTYPE
from torch_sim.models.mace import MaceModel, MaceUrls


try:
from mace.calculators.foundations_models import mace_mp

from torch_sim.models.mace import MaceModel, MaceUrls
except (ImportError, OSError, RuntimeError, AttributeError, ValueError):
pytest.skip(f"MACE not installed: {traceback.format_exc()}", allow_module_level=True)


if TYPE_CHECKING:
Expand All @@ -21,13 +28,6 @@
@pytest.fixture
def ts_mace_mpa() -> MaceModel:
"""Provides a MACE MP model instance for the optimizer tests."""
try:
from mace.calculators.foundations_models import mace_mp
except ImportError:
pytest.skip(
f"MACE not installed: {traceback.format_exc()}", allow_module_level=True
)

# Use float64 for potentially higher precision needed in optimization
dtype = getattr(torch, dtype_str := "float64")
raw_mace = mace_mp(
Expand All @@ -45,13 +45,6 @@ def ts_mace_mpa() -> MaceModel:
@pytest.fixture
def ase_mace_mpa() -> "MACECalculator":
"""Provides an ASE MACECalculator instance using mace_mp."""
try:
from mace.calculators.foundations_models import mace_mp
except ImportError:
pytest.skip(
f"MACE not installed: {traceback.format_exc()}", allow_module_level=True
)

# Ensure dtype matches the one used in the torch-sim fixture (float64)
return mace_mp(model=MaceUrls.mace_mp_small, default_dtype="float64")

Expand Down
4 changes: 3 additions & 1 deletion torch_sim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime
from importlib.metadata import version

import torch_sim as ts
import torch_sim._duecredit
from torch_sim import (
autobatching,
constraints,
Expand Down Expand Up @@ -100,3 +100,5 @@
SCRIPTS_DIR = f"{ROOT}/examples"

__version__ = version("torch-sim-atomistic")

import torch_sim._citations # noqa: E402
47 changes: 47 additions & 0 deletions torch_sim/_citations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Package-level duecredit citations for TorchSim and its core dependencies.

This module must be imported at the end of torch_sim.__init__ so that all
packages are fully loaded before citations are registered.
"""

from torch_sim._duecredit import BibTeX, due


if due is not None:
due.cite(
BibTeX(
"""@article{cohen2025torchsim,
title={TorchSim: An efficient atomistic simulation engine in PyTorch},
author={Cohen, Orion and Riebesell, Janosh and Goodall, Rhys and
Kolluru, Adeesh and Falletta, Stefano and Krause, Joseph and
Colindres, Jorge and Ceder, Gerbrand and Gangan, Abhijeet S},
journal={AI for Science},
volume={1},
number={2},
pages={025003},
year={2025},
publisher={IOP Publishing},
doi={10.1088/3050-287X/ae1799}
}"""
),
description="TorchSim simulation engine",
path="torch_sim",
cite_module=True,
)
due.cite(
BibTeX(
"""@inproceedings{paszke2019pytorch,
title={PyTorch: An Imperative Style, High-Performance Deep Learning Library},
author={Paszke, Adam and Gross, Sam and Massa, Francisco and
Lerer, Adam and Bradbury, James and Chanan, Gregory and
Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and
Antiga, Luca and others},
booktitle={Advances in Neural Information Processing Systems},
volume={32},
year={2019}
}"""
),
description="PyTorch deep learning framework",
path="torch",
cite_module=True,
)
66 changes: 66 additions & 0 deletions torch_sim/_duecredit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Stub file for a guaranteed safe import of duecredit constructs."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any


if TYPE_CHECKING:
from collections.abc import Callable


class InactiveDueCreditCollector:
"""Just a stub at the Collector which would not do anything."""

def _donothing(self, *_args: Any, **_kwargs: Any) -> None:
"""Perform no good and no bad."""

def dcite(
self, *_args: Any, **_kwargs: Any
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""If I could cite I would."""

def nondecorating_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return func

return nondecorating_decorator

active = False
activate = add = cite = dump = load = _donothing

def __repr__(self) -> str:
return self.__class__.__name__ + "()"


def _donothing_func(*_args: Any, **_kwargs: Any) -> Any:
"""Perform no good and no bad."""
return None


def _disable_duecredit(exc: Exception) -> None:
import logging

logging.getLogger("duecredit").exception(
"Failed to import duecredit despite being installed: %s", exc
)


try:
from duecredit import BibTeX, Doi, Text, Url, due
except Exception as e: # noqa: BLE001
if not isinstance(e, ImportError):
_disable_duecredit(e)
due = InactiveDueCreditCollector()
BibTeX = Doi = Url = Text = _donothing_func


def dcite(
doi: str, description: str | None = None, *, path: str | None = None
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Create a duecredit decorator from a DOI and description."""
kwargs: dict[str, Any] = (
{"description": description} if description is not None else {}
)
if path is not None:
kwargs["path"] = path
return due.dcite(Doi(doi), **kwargs)
Loading