From 36319a5638d7669ecd6f56d46410ecb0a73872fc Mon Sep 17 00:00:00 2001 From: Guillaume Fraux Date: Tue, 12 May 2026 13:47:03 +0200 Subject: [PATCH 01/10] Add an empty metatomic-core python package, re-exporting metatomic-torch --- pyproject.toml | 13 +- python/metatomic_core/AUTHORS | 1 + .../CMakeLists.txt} | 0 python/metatomic_core/LICENSE | 1 + python/metatomic_core/MANIFEST.in | 6 + python/metatomic_core/metatomic/__init__.py | 0 python/metatomic_core/metatomic/torch.py | 14 ++ python/metatomic_core/pyproject.toml | 54 +++++++ python/metatomic_core/setup.py | 146 ++++++++++++++++++ python/metatomic_torch/CMakeLists.txt | 15 +- python/metatomic_torch/MANIFEST.in | 2 +- python/metatomic_torch/README.rst | 6 +- .../torch => metatomic_torch}/__init__.py | 8 + .../torch => metatomic_torch}/_c_lib.py | 0 .../torch => metatomic_torch}/_extensions.py | 0 .../ase_calculator.py | 0 .../documentation.py | 0 .../torch => metatomic_torch}/heat_flux.py | 2 +- .../torch => metatomic_torch}/model.py | 0 .../serialization.py | 0 .../systems_to_torch.py | 0 .../torch => metatomic_torch}/utils.py | 0 .../torch => metatomic_torch}/version.py | 0 python/metatomic_torch/pyproject.toml | 4 - python/metatomic_torch/setup.py | 8 +- scripts/clean-python.sh | 9 ++ setup.py | 20 ++- tox.ini | 22 ++- 28 files changed, 303 insertions(+), 28 deletions(-) create mode 120000 python/metatomic_core/AUTHORS rename python/{metatomic_torch/metatomic/__init__.py => metatomic_core/CMakeLists.txt} (100%) create mode 120000 python/metatomic_core/LICENSE create mode 100644 python/metatomic_core/MANIFEST.in create mode 100644 python/metatomic_core/metatomic/__init__.py create mode 100644 python/metatomic_core/metatomic/torch.py create mode 100644 python/metatomic_core/pyproject.toml create mode 100644 python/metatomic_core/setup.py rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/__init__.py (92%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/_c_lib.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/_extensions.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/ase_calculator.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/documentation.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/heat_flux.py (99%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/model.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/serialization.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/systems_to_torch.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/utils.py (100%) rename python/metatomic_torch/{metatomic/torch => metatomic_torch}/version.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 500e11a3..18e55f6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,11 +63,16 @@ filterwarnings = [ "ignore:ast.NameConstant is deprecated and will be removed in Python 3.14:DeprecationWarning", # TorchScript deprecation warnings "ignore:`torch.jit.script` is deprecated. Please switch to `torch.compile` or `torch.export`:DeprecationWarning", + "ignore:`torch.jit.script_method` is deprecated. Please switch to `torch.compile` or `torch.export`:DeprecationWarning", "ignore:`torch.jit.save` is deprecated. Please switch to `torch.export`:DeprecationWarning", - "ignore:.*vesin.metatomic was only tested with metatomic.torch >=0.1.3,<0.2.*:UserWarning", "ignore:`torch.jit.load` is deprecated. Please switch to `torch.export`.:DeprecationWarning", "ignore:`torch.jit.script` is not supported in Python 3.14+:DeprecationWarning", + "ignore:`torch.jit.script_method` is not supported in Python 3.14+:DeprecationWarning", "ignore:`torch.jit.save` is not supported in Python 3.14+:DeprecationWarning", + # vesin and metatomic warning + "ignore:.*vesin.metatomic was only tested with metatomic.torch >=0.1.3,<0.2.*:UserWarning", + # Warning from warp (dependency of nvalchemi) usage of ctypes + "ignore:Due to '_pack_', the 'APICLaunchParamRecord' Structure will use memory layout compatible with MSVC:DeprecationWarning", ] ### ======================================================================== ### @@ -93,6 +98,8 @@ docstring-code-format = true [tool.uv.pip] reinstall-package = [ - "metatomic-torch", - "metatomic-torchsim", + "metatomic_core", + "metatomic_torch", + "metatomic_torchsim", + "metatomic_ase", ] diff --git a/python/metatomic_core/AUTHORS b/python/metatomic_core/AUTHORS new file mode 120000 index 00000000..f04b7e8a --- /dev/null +++ b/python/metatomic_core/AUTHORS @@ -0,0 +1 @@ +../../AUTHORS \ No newline at end of file diff --git a/python/metatomic_torch/metatomic/__init__.py b/python/metatomic_core/CMakeLists.txt similarity index 100% rename from python/metatomic_torch/metatomic/__init__.py rename to python/metatomic_core/CMakeLists.txt diff --git a/python/metatomic_core/LICENSE b/python/metatomic_core/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/python/metatomic_core/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/python/metatomic_core/MANIFEST.in b/python/metatomic_core/MANIFEST.in new file mode 100644 index 00000000..02404051 --- /dev/null +++ b/python/metatomic_core/MANIFEST.in @@ -0,0 +1,6 @@ +include pyproject.toml +include CMakeLists.txt +include AUTHORS +include LICENSE + +include git_version_info diff --git a/python/metatomic_core/metatomic/__init__.py b/python/metatomic_core/metatomic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/metatomic_core/metatomic/torch.py b/python/metatomic_core/metatomic/torch.py new file mode 100644 index 00000000..060e7bcc --- /dev/null +++ b/python/metatomic_core/metatomic/torch.py @@ -0,0 +1,14 @@ +import sys + + +try: + import metatomic_torch +except ImportError as e: + raise ImportError( + "metatomic-torch is required to use the metatomic.torch module. " + "Please install it with `pip install metatomic-torch` or using " + "your favorite Python package manager." + ) from e + +# metatomic.torch is registered as an alias in metatomic_torch's __init__.py +assert sys.modules["metatomic.torch"] is metatomic_torch diff --git a/python/metatomic_core/pyproject.toml b/python/metatomic_core/pyproject.toml new file mode 100644 index 00000000..9107f680 --- /dev/null +++ b/python/metatomic_core/pyproject.toml @@ -0,0 +1,54 @@ +[project] +name = "metatomic-core" +dynamic = ["version", "authors", "dependencies"] +requires-python = ">=3.10" + +# readme = "TODO" +license = "BSD-3-Clause" +description = "Interface between atomistic machine learning models and simulation tools" + +keywords = ["machine learning", "molecular modeling"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Chemistry", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.urls] +homepage = "https://docs.metatensor.org/metatomic/" +documentation = "https://docs.metatensor.org/metatomic/" +repository = "https://github.com/metatensor/metatomic" +# changelog = "TODO" + +### ======================================================================== ### +[build-system] +requires = [ + "setuptools >=77", + "packaging >=26", + "cmake", + "metatensor-core >=0.2.0,<0.3", +] + +build-backend = "setuptools.build_meta" + + +[tool.setuptools] +zip-safe = false + +### ======================================================================== ### +[tool.pytest.ini_options] +python_files = ["*.py"] +testpaths = ["tests"] +filterwarnings = [ + "error", +] diff --git a/python/metatomic_core/setup.py b/python/metatomic_core/setup.py new file mode 100644 index 00000000..f2654eef --- /dev/null +++ b/python/metatomic_core/setup.py @@ -0,0 +1,146 @@ +import os +import subprocess +import sys + +import packaging.version +from setuptools import setup +from setuptools.command.bdist_egg import bdist_egg +from setuptools.command.sdist import sdist + + +ROOT = os.path.realpath(os.path.dirname(__file__)) + +METATOMIC_CORE_VERSION = "0.1.0" + +METATOMIC_BUILD_TYPE = os.environ.get("METATOMIC_BUILD_TYPE", "release") +if METATOMIC_BUILD_TYPE not in ["debug", "release"]: + raise Exception( + f"invalid build type passed: '{METATOMIC_BUILD_TYPE}', " + "expected 'debug' or 'release'" + ) + + +class bdist_egg_disabled(bdist_egg): + """Disabled version of bdist_egg + + Prevents setup.py install performing setuptools' default easy_install, + which it should never ever do. + """ + + def run(self): + sys.exit( + "Aborting implicit building of eggs.\nUse `pip install .` or " + "`python -m build --wheel . && pip install dist/metatomic_torch-*.whl` " + "to install from source." + ) + + +class sdist_generate_data(sdist): + """ + Create a sdist with an additional generated files: + - `git_version_info` + """ + + def run(self): + n_commits, git_hash = git_version_info() + with open("git_version_info", "w") as fd: + fd.write(f"{n_commits}\n{git_hash}\n") + + # run original sdist + super().run() + + os.unlink("git_version_info") + + +def git_version_info(): + """ + If git is available and we are building from a checkout, get the number of commits + since the last tag & full hash of the code. Otherwise, this always returns (0, ""). + """ + TAG_PREFIX = "metatomic-v" + + if os.path.exists("git_version_info"): + # we are building from a sdist, without git available, but the git + # version was recorded in the `git_version_info` file + with open("git_version_info") as fd: + n_commits = int(fd.readline().strip()) + git_hash = fd.readline().strip() + else: + script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") + assert os.path.exists(script) + + output = subprocess.run( + [sys.executable, script, TAG_PREFIX], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding="utf8", + ) + + if output.returncode != 0: + raise Exception( + "failed to get git version info.\n" + f"stdout: {output.stdout}\n" + f"stderr: {output.stderr}\n" + ) + elif output.stderr: + print(output.stderr, file=sys.stderr) + n_commits = 0 + git_hash = "" + else: + lines = output.stdout.splitlines() + n_commits = int(lines[0].strip()) + git_hash = lines[1].strip() + + return n_commits, git_hash + + +def create_version_number(version): + version = packaging.version.parse(version) + + n_commits, git_hash = git_version_info() + + if n_commits != 0: + # if we have commits since the last tag, this mean we are in a pre-release of + # the next version. So we increase either the minor version number or the + # release candidate number (if we are closing up on a release) + if version.pre is not None: + assert version.pre[0] == "rc" + pre = ("rc", version.pre[1] + 1) + release = version.release + else: + major, minor, _ = version.release + release = (major, minor + 1, 0) + pre = None + + version = version.__replace__( + release=release, + pre=pre, + dev=n_commits, + local=git_hash, + ) + + return str(version) + + +if __name__ == "__main__": + with open(os.path.join(ROOT, "AUTHORS")) as fd: + authors = fd.read().splitlines() + + if authors[0].startswith(".."): + # handle "raw" symlink files (on Windows or from full repo tarball) + with open(os.path.join(ROOT, authors[0])) as fd: + authors = fd.read().splitlines() + + install_requires = [ + "metatensor-core >=0.2.0,<0.3", + ] + + setup( + version=create_version_number(METATOMIC_CORE_VERSION), + author=", ".join(authors), + install_requires=install_requires, + cmdclass={ + "bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled, + "sdist": sdist_generate_data, + }, + ) diff --git a/python/metatomic_torch/CMakeLists.txt b/python/metatomic_torch/CMakeLists.txt index 0fb2d542..2ee4c2bb 100644 --- a/python/metatomic_torch/CMakeLists.txt +++ b/python/metatomic_torch/CMakeLists.txt @@ -63,6 +63,9 @@ else() add_subdirectory("${METATOMIC_TORCH_SOURCE_DIR}" metatomic-torch) + if (CMAKE_VERSION VERSION_LESS "3.25") + set(LINUX $) + endif() if (LINUX OR APPLE) if (LINUX) @@ -74,12 +77,12 @@ else() set(metatomic_install_rpath "${CMAKE_INSTALL_RPATH}") # when loading the libraries from a Python installation: - # - $ORIGIN/../../../../torch/lib is where libtorch.so will be - # - $ORIGIN/../../../../metatensor/lib is where libmetatensor.so will be - # - $ORIGIN/../../../../metatensor/torch/torch-x.y/lib is where libmetatensor_torch.so will be - set(metatomic_install_rpath "${metatomic_install_rpath};${rpath_origin}/../../../../torch/lib") - set(metatomic_install_rpath "${metatomic_install_rpath};${rpath_origin}/../../../../metatensor/lib") - set(metatomic_install_rpath "${metatomic_install_rpath};${rpath_origin}/../../../../metatensor/torch/torch-${Torch_VERSION_MAJOR}.${Torch_VERSION_MINOR}/lib") + # - $ORIGIN/../../../torch/lib is where libtorch.so will be + # - $ORIGIN/../../../metatensor/lib is where libmetatensor.so will be + # - $ORIGIN/../../../metatensor_torch/torch-${Torch_VERSION_MAJOR}.${Torch_VERSION_MINOR}/lib is where libmetatensor_torch.so will be + set(metatomic_install_rpath "${metatomic_install_rpath};${rpath_origin}/../../../torch/lib") + set(metatomic_install_rpath "${metatomic_install_rpath};${rpath_origin}/../../../metatensor/lib") + set(metatomic_install_rpath "${metatomic_install_rpath};${rpath_origin}/../../../metatensor_torch/torch-${Torch_VERSION_MAJOR}.${Torch_VERSION_MINOR}/lib") set_target_properties( metatomic_torch PROPERTIES INSTALL_RPATH "${metatomic_install_rpath}" diff --git a/python/metatomic_torch/MANIFEST.in b/python/metatomic_torch/MANIFEST.in index 6d341b48..9e6ef4ed 100644 --- a/python/metatomic_torch/MANIFEST.in +++ b/python/metatomic_torch/MANIFEST.in @@ -5,6 +5,6 @@ include LICENSE include git_version_info -include metatomic-torch-*.tar.gz +include metatomic-torch-cxx-*.tar.gz recursive-include build-backend *.py diff --git a/python/metatomic_torch/README.rst b/python/metatomic_torch/README.rst index f06f2b8a..994fda75 100644 --- a/python/metatomic_torch/README.rst +++ b/python/metatomic_torch/README.rst @@ -1,4 +1,4 @@ -metatensor-torch -================ +metatomic-torch +=============== -This package contains the TorchScript bindings to the core API of metatensor. +This package contains the TorchScript bindings to the core API of metatomic. diff --git a/python/metatomic_torch/metatomic/torch/__init__.py b/python/metatomic_torch/metatomic_torch/__init__.py similarity index 92% rename from python/metatomic_torch/metatomic/torch/__init__.py rename to python/metatomic_torch/metatomic_torch/__init__.py index a8bf363a..dc0ba38c 100644 --- a/python/metatomic_torch/metatomic/torch/__init__.py +++ b/python/metatomic_torch/metatomic_torch/__init__.py @@ -1,8 +1,11 @@ import os +import sys from typing import TYPE_CHECKING import torch +import metatomic + from ._c_lib import _load_library from .version import __version__ # noqa: F401 @@ -65,3 +68,8 @@ save_buffer, ) from .systems_to_torch import systems_to_torch # noqa: F401 + + +sys.modules["metatomic.torch"] = sys.modules[__name__] +if not hasattr(metatomic, "torch"): + metatomic.torch = sys.modules[__name__] diff --git a/python/metatomic_torch/metatomic/torch/_c_lib.py b/python/metatomic_torch/metatomic_torch/_c_lib.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/_c_lib.py rename to python/metatomic_torch/metatomic_torch/_c_lib.py diff --git a/python/metatomic_torch/metatomic/torch/_extensions.py b/python/metatomic_torch/metatomic_torch/_extensions.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/_extensions.py rename to python/metatomic_torch/metatomic_torch/_extensions.py diff --git a/python/metatomic_torch/metatomic/torch/ase_calculator.py b/python/metatomic_torch/metatomic_torch/ase_calculator.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/ase_calculator.py rename to python/metatomic_torch/metatomic_torch/ase_calculator.py diff --git a/python/metatomic_torch/metatomic/torch/documentation.py b/python/metatomic_torch/metatomic_torch/documentation.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/documentation.py rename to python/metatomic_torch/metatomic_torch/documentation.py diff --git a/python/metatomic_torch/metatomic/torch/heat_flux.py b/python/metatomic_torch/metatomic_torch/heat_flux.py similarity index 99% rename from python/metatomic_torch/metatomic/torch/heat_flux.py rename to python/metatomic_torch/metatomic_torch/heat_flux.py index 4de0828e..167149b0 100644 --- a/python/metatomic_torch/metatomic/torch/heat_flux.py +++ b/python/metatomic_torch/metatomic_torch/heat_flux.py @@ -4,7 +4,7 @@ from metatensor.torch import Labels, TensorBlock, TensorMap from vesin.metatomic import NeighborList -from metatomic.torch import ( +from . import ( AtomisticModel, ModelCapabilities, ModelOutput, diff --git a/python/metatomic_torch/metatomic/torch/model.py b/python/metatomic_torch/metatomic_torch/model.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/model.py rename to python/metatomic_torch/metatomic_torch/model.py diff --git a/python/metatomic_torch/metatomic/torch/serialization.py b/python/metatomic_torch/metatomic_torch/serialization.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/serialization.py rename to python/metatomic_torch/metatomic_torch/serialization.py diff --git a/python/metatomic_torch/metatomic/torch/systems_to_torch.py b/python/metatomic_torch/metatomic_torch/systems_to_torch.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/systems_to_torch.py rename to python/metatomic_torch/metatomic_torch/systems_to_torch.py diff --git a/python/metatomic_torch/metatomic/torch/utils.py b/python/metatomic_torch/metatomic_torch/utils.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/utils.py rename to python/metatomic_torch/metatomic_torch/utils.py diff --git a/python/metatomic_torch/metatomic/torch/version.py b/python/metatomic_torch/metatomic_torch/version.py similarity index 100% rename from python/metatomic_torch/metatomic/torch/version.py rename to python/metatomic_torch/metatomic_torch/version.py diff --git a/python/metatomic_torch/pyproject.toml b/python/metatomic_torch/pyproject.toml index 40259291..fe432a0a 100644 --- a/python/metatomic_torch/pyproject.toml +++ b/python/metatomic_torch/pyproject.toml @@ -48,10 +48,6 @@ backend-path = ["build-backend"] [tool.setuptools] zip-safe = false -[tool.setuptools.packages.find] -include = ["metatomic*"] -namespaces = true - ### ======================================================================== ### [tool.pytest.ini_options] python_files = ["*.py"] diff --git a/python/metatomic_torch/setup.py b/python/metatomic_torch/setup.py index 7f327b64..98b3d55c 100644 --- a/python/metatomic_torch/setup.py +++ b/python/metatomic_torch/setup.py @@ -24,6 +24,7 @@ METATOMIC_TORCH_SRC = os.path.realpath( os.path.join(ROOT, "..", "..", "metatomic-torch") ) +METATOMIC_CORE = os.path.realpath(os.path.join(ROOT, "..", "metatomic_core")) METATOMIC_ASE = os.path.realpath(os.path.join(ROOT, "..", "metatomic_ase")) @@ -50,7 +51,7 @@ def run(self): source_dir = ROOT build_dir = os.path.join(ROOT, "build", "cmake-build") - install_dir = os.path.join(os.path.realpath(self.build_lib), "metatomic/torch") + install_dir = os.path.join(os.path.realpath(self.build_lib), "metatomic_torch") os.makedirs(build_dir, exist_ok=True) @@ -325,11 +326,14 @@ def create_version_number(version): # when packaging a sdist for release, we should never use local dependencies METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_ASE): + if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_CORE): + assert os.path.exists(METATOMIC_ASE) # we are building from a git checkout or full repo archive + install_requires.append(f"metatomic-core @ file://{METATOMIC_CORE}") install_requires.append(f"metatomic-ase @ file://{METATOMIC_ASE}") else: # we are building from a sdist/installing from a wheel + install_requires.append("metatomic-core >=0.1.0,<0.2.0") install_requires.append("metatomic-ase >=0.1.1,<0.2.0") setup( diff --git a/scripts/clean-python.sh b/scripts/clean-python.sh index ba6a9e9f..81e69b26 100755 --- a/scripts/clean-python.sh +++ b/scripts/clean-python.sh @@ -14,9 +14,18 @@ rm -rf docs/build rm -rf docs/src/examples rm -rf docs/src/sg_execution_times.rst +rm -rf python/metatomic_core/dist +rm -rf python/metatomic_core/build + rm -rf python/metatomic_torch/dist rm -rf python/metatomic_torch/build +rm -rf python/metatomic_ase/dist +rm -rf python/metatomic_ase/build + +rm -rf python/metatomic_torchsim/dist +rm -rf python/metatomic_torchsim/build + find . -name "*.egg-info" -exec rm -rf "{}" + find . -name "__pycache__" -exec rm -rf "{}" + find . -name ".coverage" -exec rm -rf "{}" + diff --git a/setup.py b/setup.py index ced9f714..2124530b 100644 --- a/setup.py +++ b/setup.py @@ -4,29 +4,39 @@ ROOT = os.path.realpath(os.path.dirname(__file__)) +METATOMIC_CORE = os.path.join(ROOT, "python", "metatomic_core") METATOMIC_TORCH = os.path.join(ROOT, "python", "metatomic_torch") +METATOMIC_ASE = os.path.join(ROOT, "python", "metatomic_ase") METATOMIC_TORCHSIM = os.path.join(ROOT, "python", "metatomic_torchsim") if __name__ == "__main__": extras_require = {} + install_requires = [] # when packaging a sdist for release, we should never use local dependencies METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_TORCH): + if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_CORE): + assert os.path.exists(METATOMIC_TORCH) + assert os.path.exists(METATOMIC_ASE) + assert os.path.exists(METATOMIC_TORCHSIM) + # we are building from a git checkout + install_requires.append(f"metatomic-core @ file://{METATOMIC_CORE}") extras_require["torch"] = f"metatomic-torch @ file://{METATOMIC_TORCH}" + extras_require["ase"] = f"metatomic-ase @ file://{METATOMIC_ASE}" + extras_require["torchsim"] = f"metatomic-torchsim @ file://{METATOMIC_TORCHSIM}" else: # we are building from a sdist/installing from a wheel - extras_require["torch"] = "metatomic-torch" + install_requires.append("metatomic-core") - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_TORCHSIM): - extras_require["torchsim"] = f"metatomic-torchsim @ file://{METATOMIC_TORCHSIM}" - else: + extras_require["torch"] = "metatomic-torch" + extras_require["ase"] = "metatomic-ase" extras_require["torchsim"] = "metatomic-torchsim" setup( author=", ".join(open(os.path.join(ROOT, "AUTHORS")).read().splitlines()), + install_requires=install_requires, extras_require=extras_require, ) diff --git a/tox.ini b/tox.ini index 201c2845..6eae4e86 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,7 @@ packaging_deps = testing_deps = pytest pytest-cov + pytest-custom_exit_code metatensor_deps = metatensor-torch >=0.9.0,<0.10 @@ -133,6 +134,7 @@ deps = changedir = python/metatomic_torch commands = + pip install {[testenv]build_single_wheel} ../metatomic_core pip install {[testenv]build_single_wheel} . pip install {[testenv]build_single_wheel} ../metatomic_ase @@ -157,12 +159,23 @@ deps = vesin >=0.5.6,<0.6 ase + torch-sim-atomistic + +setenv = + # ignore the fact that metatensor.torch.operations was loaded from a file + # not in `metatensor/torch/operations` + PY_IGNORE_IMPORTMISMATCH = 1 commands = + pip install {[testenv]build_single_wheel} python/metatomic_core pip install {[testenv]build_single_wheel} python/metatomic_torch pip install {[testenv]build_single_wheel} python/metatomic_ase + pip install {[testenv]build_single_wheel} python/metatomic_torchsim - pytest --doctest-modules --pyargs metatomic + pytest --suppress-no-test-exit-code --doctest-modules --pyargs metatomic + pytest --suppress-no-test-exit-code --doctest-modules --pyargs metatomic_torch + pytest --suppress-no-test-exit-code --doctest-modules --pyargs metatomic_ase + pytest --suppress-no-test-exit-code --doctest-modules --pyargs metatomic_torchsim ################################################################################ @@ -192,8 +205,9 @@ deps = changedir = python/metatomic_ase commands = - pip install {[testenv]build_single_wheel} . + pip install {[testenv]build_single_wheel} ../metatomic_core pip install {[testenv]build_single_wheel} ../metatomic_torch + pip install {[testenv]build_single_wheel} . # use the reference LJ implementation for tests {[testenv]install_lj_tests} @@ -222,8 +236,9 @@ deps = changedir = python/metatomic_torchsim commands = - pip install {[testenv]build_single_wheel} . + pip install {[testenv]build_single_wheel} ../metatomic_core pip install {[testenv]build_single_wheel} ../metatomic_torch + pip install {[testenv]build_single_wheel} . # use the reference LJ implementation for tests {[testenv]install_lj_tests} @@ -292,6 +307,7 @@ deps = chemiscope commands = + pip install {[testenv]build_single_wheel} python/metatomic_core pip install {[testenv]build_single_wheel} python/metatomic_torch pip install {[testenv]build_single_wheel} python/metatomic_ase pip install {[testenv]build_single_wheel} python/metatomic_torchsim From e45ed6b66d52a92c6da1550b6ee875aca5cb18d2 Mon Sep 17 00:00:00 2001 From: Guillaume Fraux Date: Thu, 21 May 2026 10:26:30 +0200 Subject: [PATCH 02/10] Use pathlib for all path manipulations --- python/metatomic_ase/setup.py | 21 ++++++------ python/metatomic_core/setup.py | 15 +++++---- python/metatomic_torch/setup.py | 53 ++++++++++++++---------------- python/metatomic_torchsim/setup.py | 19 ++++++----- setup.py | 29 ++++++++-------- 5 files changed, 70 insertions(+), 67 deletions(-) diff --git a/python/metatomic_ase/setup.py b/python/metatomic_ase/setup.py index a1930193..83de5e00 100644 --- a/python/metatomic_ase/setup.py +++ b/python/metatomic_ase/setup.py @@ -1,4 +1,5 @@ import os +import pathlib import subprocess import sys @@ -8,8 +9,8 @@ from setuptools.command.sdist import sdist -ROOT = os.path.realpath(os.path.dirname(__file__)) -METATOMIC_TORCH = os.path.realpath(os.path.join(ROOT, "..", "metatomic_torch")) +ROOT = pathlib.Path(__file__).parent.resolve() +METATOMIC_TORCH = (ROOT / ".." / "metatomic_torch").resolve() METATOMIC_ASE_VERSION = "0.1.1" @@ -53,15 +54,15 @@ def git_version_info(): """ TAG_PREFIX = "metatomic-ase-v" - if os.path.exists("git_version_info"): + if (ROOT / "git_version_info").exists(): # we are building from a sdist, without git available, but the git # version was recorded in the `git_version_info` file - with open("git_version_info") as fd: + with open(ROOT / "git_version_info") as fd: n_commits = int(fd.readline().strip()) git_hash = fd.readline().strip() else: - script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") - assert os.path.exists(script) + script = (ROOT / ".." / ".." / "scripts" / "git-version-info.py").resolve() + assert script.exists() output = subprocess.run( [sys.executable, script, TAG_PREFIX], @@ -127,19 +128,19 @@ def create_version_number(version): # when packaging a sdist for release, we should never use local dependencies METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_TORCH): + if not METATOMIC_NO_LOCAL_DEPS and METATOMIC_TORCH.exists(): # we are building from a git checkout or full repo archive - install_requires.append(f"metatomic-torch @ file://{METATOMIC_TORCH}") + install_requires.append(f"metatomic-torch @ {METATOMIC_TORCH.as_uri()}") else: # we are building from a sdist/installing from a wheel install_requires.append("metatomic-torch >=0.1.12,<0.2") - with open(os.path.join(ROOT, "AUTHORS")) as fd: + with open(ROOT / "AUTHORS") as fd: authors = fd.read().splitlines() if authors[0].startswith(".."): # handle "raw" symlink files (on Windows or from full repo tarball) - with open(os.path.join(ROOT, authors[0])) as fd: + with open(ROOT / authors[0]) as fd: authors = fd.read().splitlines() setup( diff --git a/python/metatomic_core/setup.py b/python/metatomic_core/setup.py index f2654eef..35a2ef16 100644 --- a/python/metatomic_core/setup.py +++ b/python/metatomic_core/setup.py @@ -1,4 +1,5 @@ import os +import pathlib import subprocess import sys @@ -8,7 +9,7 @@ from setuptools.command.sdist import sdist -ROOT = os.path.realpath(os.path.dirname(__file__)) +ROOT = pathlib.Path(__file__).parent.resolve() METATOMIC_CORE_VERSION = "0.1.0" @@ -59,15 +60,15 @@ def git_version_info(): """ TAG_PREFIX = "metatomic-v" - if os.path.exists("git_version_info"): + if (ROOT / "git_version_info").exists(): # we are building from a sdist, without git available, but the git # version was recorded in the `git_version_info` file - with open("git_version_info") as fd: + with open(ROOT / "git_version_info") as fd: n_commits = int(fd.readline().strip()) git_hash = fd.readline().strip() else: - script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") - assert os.path.exists(script) + script = (ROOT / ".." / ".." / "scripts" / "git-version-info.py").resolve() + assert script.exists() output = subprocess.run( [sys.executable, script, TAG_PREFIX], @@ -123,12 +124,12 @@ def create_version_number(version): if __name__ == "__main__": - with open(os.path.join(ROOT, "AUTHORS")) as fd: + with open(ROOT / "AUTHORS") as fd: authors = fd.read().splitlines() if authors[0].startswith(".."): # handle "raw" symlink files (on Windows or from full repo tarball) - with open(os.path.join(ROOT, authors[0])) as fd: + with open(ROOT / authors[0]) as fd: authors = fd.read().splitlines() install_requires = [ diff --git a/python/metatomic_torch/setup.py b/python/metatomic_torch/setup.py index 98b3d55c..9524ac44 100644 --- a/python/metatomic_torch/setup.py +++ b/python/metatomic_torch/setup.py @@ -1,5 +1,6 @@ import glob import os +import pathlib import subprocess import sys @@ -12,7 +13,7 @@ from setuptools.command.sdist import sdist -ROOT = os.path.realpath(os.path.dirname(__file__)) +ROOT = pathlib.Path(__file__).parent.resolve() METATOMIC_BUILD_TYPE = os.environ.get("METATOMIC_BUILD_TYPE", "release") if METATOMIC_BUILD_TYPE not in ["debug", "release"]: @@ -21,11 +22,9 @@ "expected 'debug' or 'release'" ) -METATOMIC_TORCH_SRC = os.path.realpath( - os.path.join(ROOT, "..", "..", "metatomic-torch") -) -METATOMIC_CORE = os.path.realpath(os.path.join(ROOT, "..", "metatomic_core")) -METATOMIC_ASE = os.path.realpath(os.path.join(ROOT, "..", "metatomic_ase")) +METATOMIC_TORCH_SRC = (ROOT / ".." / ".." / "metatomic-torch").resolve() +METATOMIC_CORE = (ROOT / ".." / "metatomic_core").resolve() +METATOMIC_ASE = (ROOT / ".." / "metatomic_ase").resolve() class universal_wheel(bdist_wheel): @@ -50,10 +49,10 @@ def run(self): import torch source_dir = ROOT - build_dir = os.path.join(ROOT, "build", "cmake-build") - install_dir = os.path.join(os.path.realpath(self.build_lib), "metatomic_torch") + build_dir = ROOT / "build" / "cmake-build" + install_dir = pathlib.Path(self.build_lib).resolve() / "metatomic_torch" - os.makedirs(build_dir, exist_ok=True) + build_dir.mkdir(parents=True, exist_ok=True) # Tell CMake where to find metatensor, metatensor_torch, and torch cmake_prefix_path = [ @@ -66,9 +65,7 @@ def run(self): # compile the code. This allows having multiple version of this shared library # inside the wheel; and dynamically pick the right one. torch_major, torch_minor, *_ = torch.__version__.split(".") - cmake_install_prefix = os.path.join( - install_dir, f"torch-{torch_major}.{torch_minor}" - ) + cmake_install_prefix = install_dir / f"torch-{torch_major}.{torch_minor}" use_external_lib = os.environ.get( "METATOMIC_TORCH_PYTHON_USE_EXTERNAL_LIB", "OFF" @@ -142,8 +139,8 @@ def run(self): def generate_cxx_tar(): - script = os.path.join(ROOT, "..", "..", "scripts", "package-torch.sh") - assert os.path.exists(script) + script = (ROOT / ".." / ".." / "scripts" / "package-torch.sh").resolve() + assert script.exists() try: output = subprocess.run( @@ -180,15 +177,15 @@ def git_version_info(): """ TAG_PREFIX = "metatomic-torch-v" - if os.path.exists("git_version_info"): + if (ROOT / "git_version_info").exists(): # we are building from a sdist, without git available, but the git # version was recorded in the `git_version_info` file - with open("git_version_info") as fd: + with open(ROOT / "git_version_info") as fd: n_commits = int(fd.readline().strip()) git_hash = fd.readline().strip() else: - script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") - assert os.path.exists(script) + script = (ROOT / ".." / ".." / "scripts" / "git-version-info.py").resolve() + assert script.exists() output = subprocess.run( [sys.executable, script, TAG_PREFIX], @@ -275,10 +272,10 @@ def create_version_number(version): # End of Windows/MKL/PIP hack - if not os.path.exists(METATOMIC_TORCH_SRC): + if not METATOMIC_TORCH_SRC.exists(): # we are building from a sdist, which should include metatomic-torch C++ # sources as a tarball - tarballs = glob.glob(os.path.join(ROOT, "metatomic-torch-cxx-*.tar.gz")) + tarballs = glob.glob(ROOT / "metatomic-torch-cxx-*.tar.gz") if not len(tarballs) == 1: raise RuntimeError( @@ -286,7 +283,7 @@ def create_version_number(version): "metatomic-torch C++ sources" ) - METATOMIC_TORCH_SRC = os.path.realpath(tarballs[0]) + METATOMIC_TORCH_SRC = pathlib.Path(tarballs[0]).resolve() subprocess.run( ["cmake", "-E", "tar", "xf", METATOMIC_TORCH_SRC], cwd=ROOT, @@ -295,15 +292,15 @@ def create_version_number(version): METATOMIC_TORCH_SRC = ".".join(METATOMIC_TORCH_SRC.split(".")[:-2]) - with open(os.path.join(METATOMIC_TORCH_SRC, "VERSION")) as fd: + with open(METATOMIC_TORCH_SRC / "VERSION") as fd: METATOMIC_TORCH_VERSION = fd.read().strip() - with open(os.path.join(ROOT, "AUTHORS")) as fd: + with open(ROOT / "AUTHORS") as fd: authors = fd.read().splitlines() if authors[0].startswith(".."): # handle "raw" symlink files (on Windows or from full repo tarball) - with open(os.path.join(ROOT, authors[0])) as fd: + with open(ROOT / authors[0]) as fd: authors = fd.read().splitlines() try: @@ -326,11 +323,11 @@ def create_version_number(version): # when packaging a sdist for release, we should never use local dependencies METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_CORE): - assert os.path.exists(METATOMIC_ASE) + if not METATOMIC_NO_LOCAL_DEPS and METATOMIC_CORE.exists(): + assert METATOMIC_ASE.exists() # we are building from a git checkout or full repo archive - install_requires.append(f"metatomic-core @ file://{METATOMIC_CORE}") - install_requires.append(f"metatomic-ase @ file://{METATOMIC_ASE}") + install_requires.append(f"metatomic-core @ {METATOMIC_CORE.as_uri()}") + install_requires.append(f"metatomic-ase @ {METATOMIC_ASE.as_uri()}") else: # we are building from a sdist/installing from a wheel install_requires.append("metatomic-core >=0.1.0,<0.2.0") diff --git a/python/metatomic_torchsim/setup.py b/python/metatomic_torchsim/setup.py index f3d5d025..2e403a07 100644 --- a/python/metatomic_torchsim/setup.py +++ b/python/metatomic_torchsim/setup.py @@ -1,4 +1,5 @@ import os +import pathlib import subprocess import sys @@ -7,8 +8,8 @@ from setuptools.command.sdist import sdist -ROOT = os.path.realpath(os.path.dirname(__file__)) -METATOMIC_TORCH = os.path.realpath(os.path.join(ROOT, "..", "metatomic_torch")) +ROOT = pathlib.Path(__file__).parent.resolve() +METATOMIC_TORCH = (ROOT / ".." / "metatomic_torch").resolve() METATOMIC_TORCHSIM_VERSION = "0.1.3" @@ -38,15 +39,15 @@ def git_version_info(): """ TAG_PREFIX = "metatomic-torchsim-v" - if os.path.exists("git_version_info"): + if (ROOT / "git_version_info").exists(): # we are building from a sdist, without git available, but the git # version was recorded in the `git_version_info` file - with open("git_version_info") as fd: + with open(ROOT / "git_version_info") as fd: n_commits = int(fd.readline().strip()) git_hash = fd.readline().strip() else: - script = os.path.join(ROOT, "..", "..", "scripts", "git-version-info.py") - assert os.path.exists(script) + script = (ROOT / ".." / ".." / "scripts" / "git-version-info.py").resolve() + assert script.exists() output = subprocess.run( [sys.executable, script, TAG_PREFIX], @@ -102,7 +103,7 @@ def create_version_number(version): if __name__ == "__main__": - with open(os.path.join(ROOT, "AUTHORS")) as fd: + with open(ROOT / "AUTHORS") as fd: authors = fd.read().splitlines() install_requires = [ @@ -113,9 +114,9 @@ def create_version_number(version): # when packaging a sdist for release, we should never use local dependencies METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_TORCH): + if not METATOMIC_NO_LOCAL_DEPS and METATOMIC_TORCH.exists(): # we are building from a git checkout or full repo archive - install_requires.append(f"metatomic-torch @ file://{METATOMIC_TORCH}") + install_requires.append(f"metatomic-torch @ {METATOMIC_TORCH.as_uri()}") else: # we are building from a sdist/installing from a wheel install_requires.append("metatomic-torch >=0.1.12,<0.2") diff --git a/setup.py b/setup.py index 2124530b..69699d06 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,14 @@ import os +import pathlib from setuptools import setup -ROOT = os.path.realpath(os.path.dirname(__file__)) -METATOMIC_CORE = os.path.join(ROOT, "python", "metatomic_core") -METATOMIC_TORCH = os.path.join(ROOT, "python", "metatomic_torch") -METATOMIC_ASE = os.path.join(ROOT, "python", "metatomic_ase") -METATOMIC_TORCHSIM = os.path.join(ROOT, "python", "metatomic_torchsim") +ROOT = pathlib.Path(__file__).parent.resolve() +METATOMIC_CORE = (ROOT / "python" / "metatomic_core").resolve() +METATOMIC_TORCH = (ROOT / "python" / "metatomic_torch").resolve() +METATOMIC_ASE = (ROOT / "python" / "metatomic_ase").resolve() +METATOMIC_TORCHSIM = (ROOT / "python" / "metatomic_torchsim").resolve() if __name__ == "__main__": @@ -17,16 +18,18 @@ # when packaging a sdist for release, we should never use local dependencies METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" - if not METATOMIC_NO_LOCAL_DEPS and os.path.exists(METATOMIC_CORE): - assert os.path.exists(METATOMIC_TORCH) - assert os.path.exists(METATOMIC_ASE) - assert os.path.exists(METATOMIC_TORCHSIM) + if not METATOMIC_NO_LOCAL_DEPS and METATOMIC_CORE.exists(): + assert METATOMIC_TORCH.exists() + assert METATOMIC_ASE.exists() + assert METATOMIC_TORCHSIM.exists() # we are building from a git checkout - install_requires.append(f"metatomic-core @ file://{METATOMIC_CORE}") - extras_require["torch"] = f"metatomic-torch @ file://{METATOMIC_TORCH}" - extras_require["ase"] = f"metatomic-ase @ file://{METATOMIC_ASE}" - extras_require["torchsim"] = f"metatomic-torchsim @ file://{METATOMIC_TORCHSIM}" + install_requires.append(f"metatomic-core @ {METATOMIC_CORE.as_uri()}") + extras_require["torch"] = f"metatomic-torch @ {METATOMIC_TORCH.as_uri()}" + extras_require["ase"] = f"metatomic-ase @ {METATOMIC_ASE.as_uri()}" + extras_require["torchsim"] = ( + f"metatomic-torchsim @ {METATOMIC_TORCHSIM.as_uri()}" + ) else: # we are building from a sdist/installing from a wheel install_requires.append("metatomic-core") From 477bf489d0e83112b3a9c0bfe239fd1f1ae9a73f Mon Sep 17 00:00:00 2001 From: Guillaume Fraux Date: Tue, 12 May 2026 15:26:39 +0200 Subject: [PATCH 03/10] Switch main test runner from tox to cargo --- .github/workflows/python-tests.yml | 96 +++++ .github/workflows/torch-tests.yml | 96 ++--- .gitignore | 3 + CONTRIBUTING.rst | 80 +++- Cargo.toml | 7 + docs/src/devdoc/get-started.rst | 6 + docs/src/devdoc/index.rst | 26 ++ docs/src/index.rst | 1 + metatomic-torch/Cargo.toml | 13 + metatomic-torch/lib.rs | 1 + metatomic-torch/tests/CMakeLists.txt | 6 +- metatomic-torch/tests/check-torch-install.rs | 207 ++++++++++ metatomic-torch/tests/run-torch-tests.rs | 47 +++ metatomic-torch/tests/utils/mod.rs | 410 +++++++++++++++++++ python/Cargo.toml | 12 + python/lib.rs | 1 + python/tests/run-python-tests.rs | 23 ++ tox.ini | 71 ---- 18 files changed, 971 insertions(+), 135 deletions(-) create mode 100644 .github/workflows/python-tests.yml create mode 100644 Cargo.toml create mode 100644 docs/src/devdoc/get-started.rst create mode 100644 docs/src/devdoc/index.rst create mode 100644 metatomic-torch/Cargo.toml create mode 100644 metatomic-torch/lib.rs create mode 100644 metatomic-torch/tests/check-torch-install.rs create mode 100644 metatomic-torch/tests/run-torch-tests.rs create mode 100644 metatomic-torch/tests/utils/mod.rs create mode 100644 python/Cargo.toml create mode 100644 python/lib.rs create mode 100644 python/tests/run-python-tests.rs diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000..da3944fb --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,96 @@ +name: Python tests + +on: + push: + branches: [main] + pull_request: + # Check all PR + +concurrency: + group: python-tests-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + python-tests: + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / Python ${{ matrix.python-version }} / Torch ${{ matrix.torch-version }} + strategy: + matrix: + include: + - os: ubuntu-24.04 + python-version: "3.10" + torch-version: "2.3" + numpy-version-pin: "<2.0" + # Do not run docs-tests with python 3.10 since torch-sim-atomistic + # is not available for this version of python + tox-envs: lint,torch-tests + - os: ubuntu-24.04 + python-version: "3.10" + torch-version: "2.12" + # See above + tox-envs: lint,torch-tests + - os: ubuntu-24.04 + # TorchScript is no longer supported in Python 3.14 + # so we keep a test with 3.13 to make sure this doesn't break + python-version: "3.13" + torch-version: "2.12" + tox-envs: lint,torch-tests,docs-tests + - os: ubuntu-24.04 + python-version: "3.14" + torch-version: "2.12" + tox-envs: lint,torch-tests,docs-tests + - os: macos-15 + python-version: "3.14" + torch-version: "2.12" + tox-envs: lint,torch-tests,docs-tests + - os: windows-2022 + python-version: "3.14" + torch-version: "2.12" + tox-envs: lint,torch-tests,docs-tests + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: setup Python + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: setup rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Cache Rust dependencies + uses: Leafwing-Studios/cargo-cache@v2.6.1 + with: + sweep-cache: true + + - name: Setup sccache + if: ${{ !env.ACT }} + uses: mozilla-actions/sccache-action@v0.0.10 + with: + version: "v0.10.0" + + - name: setup MSVC command prompt + uses: ilammy/msvc-dev-cmd@v1 + + - name: Setup sccache environnement variables + if: ${{ !env.ACT }} + run: | + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + + - name: install tests dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox coverage + + - name: run tests + run: tox -e ${{ matrix.tox-envs }} + env: + PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu + METATOMIC_TESTS_TORCH_VERSION: ${{ matrix.torch-version }} diff --git a/.github/workflows/torch-tests.yml b/.github/workflows/torch-tests.yml index 1c549795..62ed6025 100644 --- a/.github/workflows/torch-tests.yml +++ b/.github/workflows/torch-tests.yml @@ -13,81 +13,85 @@ concurrency: jobs: tests: runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} / Python ${{ matrix.python-version }} / Torch ${{ matrix.torch-version }} + name: ${{ matrix.os }} / Torch ${{ matrix.torch-version }}${{ matrix.extra-name }} + container: ${{ matrix.container }} strategy: matrix: include: - os: ubuntu-24.04 - python-version: "3.10" - torch-version: "2.3" - - os: ubuntu-24.04 - python-version: "3.10" torch-version: "2.12" - - os: ubuntu-24.04 - # Keep a building with Python 3.13 since TorchScript is deprecated - # in Python 3.14 - python-version: "3.13" - torch-version: "2.12" - - os: ubuntu-24.04 python-version: "3.14" - torch-version: "2.12" + cargo-test-flags: --release + do-valgrind: true + + # check the build on a stock Ubuntu 22.04, which uses cmake 3.22 + - os: ubuntu-24.04 + container: ubuntu:22.04 + extra-name: ", cmake 3.22" + torch-version: "2.3" + cargo-test-flags: "" + - os: macos-15 - python-version: "3.14" torch-version: "2.12" - - os: windows-2022 python-version: "3.14" + cargo-test-flags: --release + + - os: windows-2022 torch-version: "2.12" + python-version: "3.14" + cargo-test-flags: --release steps: + - name: install dependencies in container + if: matrix.container == 'ubuntu:22.04' + run: | + apt update + apt install -y software-properties-common + add-apt-repository ppa:deadsnakes/ppa + apt install -y cmake make gcc g++ git curl python3.10 python3.10-venv + + update-alternatives --install /usr/local/bin/python python /usr/bin/python3.10 1 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - name: setup Python - uses: actions/setup-python@v6 + - name: Configure git safe directory + if: matrix.container == 'ubuntu:22.04' + run: git config --global --add safe.directory /__w/metatomic/metatomic + + - name: setup rust + uses: dtolnay/rust-toolchain@master with: - python-version: ${{ matrix.python-version }} + toolchain: stable + + - name: Cache Rust dependencies + uses: Leafwing-Studios/cargo-cache@v2.6.1 + with: + sweep-cache: true + + - name: install valgrind + if: matrix.do-valgrind + run: | + sudo apt-get install -y valgrind - name: Setup sccache + if: ${{ !env.ACT }} uses: mozilla-actions/sccache-action@v0.0.10 with: version: "v0.10.0" - - name: setup MSVC command prompt - uses: ilammy/msvc-dev-cmd@v1 - - name: Setup sccache environnement variables + if: ${{ !env.ACT }} run: | echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV - - name: install tests dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox coverage - - - name: run Python tests - run: tox -e lint,torch-tests,docs-tests + - name: run TorchScript C++ tests + run: cargo test --package metatomic-torch ${{ matrix.cargo-test-flags }} env: + # Use the CPU only version of torch when building/running the code PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu METATOMIC_TESTS_TORCH_VERSION: ${{ matrix.torch-version }} - - - name: run C++ tests - run: tox -e torch-tests-cxx,torch-install-tests-cxx - env: - PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu - METATOMIC_TESTS_TORCH_VERSION: ${{ matrix.torch-version }} - - - name: combine Python coverage files - shell: bash - run: | - coverage combine .tox/*/.coverage - coverage xml - - - name: upload to codecov.io - uses: codecov/codecov-action@v6 - with: - fail_ci_if_error: true - files: coverage.xml - token: ${{ secrets.CODECOV_TOKEN }} + CXXFLAGS: ${{ matrix.cxx-flags }} diff --git a/.gitignore b/.gitignore index ab865aa2..265263ff 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ build/ htmlcov/ .coverage* coverage.xml + +Cargo.lock +target/ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 50c8dc98..e62180b8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -16,6 +16,10 @@ on metatomic: - **git**: the software we use for version control of the source code. See https://git-scm.com/downloads for installation instructions. +- **the rust compiler**: you will need both ``rustc`` (the compiler) and + ``cargo`` (associated build tool). You can install both using `rustup`_, or + use a version provided by your operating system. We need at least Rust version + 1.74 to build metatomic. - **Python**: you can install ``Python`` and ``pip`` on your operating system. We require a Python version of at least 3.9. - **tox**: a Python test runner, see https://tox.readthedocs.io/en/latest/. You @@ -28,17 +32,21 @@ not have to interact with them directly: - **a C++ compiler** we need a compiler supporting C++11. GCC >= 7, clang >= 5 and MSVC >= 19 should all work, although MSVC is not yet tested continuously. +.. _rustup: https://rustup.rs +.. _`cargo` : https://doc.rust-lang.org/cargo/ +.. _tox: https://tox.readthedocs.io/en/latest + .. admonition:: Optional tools Depending on which part of the code you are working on, you might experience a - lot of time spent re-compiling code, even if you did not directly change them. - For faster builds (and in turn faster tests), you can use compiler cache, like - `sccache`_ or the classic `ccache`_ to reduce the recompilation of unchanged - source code. To do this, you should install and configure one of these tools - (we suggest ``sccache`` since it also supports Rust), and then configure - ``cmake`` and ``cargo`` to use them by setting environnement variables. On - Linux and macOS, you should set the following (look up how to do set - environment variable with your shell): + lot of time spend re-compiling Rust or C++ code, even if you did not change + them. If you'd like faster builds (and in turn faster tests), you can use + `sccache`_ or the classic `ccache`_ to only re-run the compiler if the + corresponding source code changed. To do this, you should install and configure + one of these tools (we suggest sccache since it also supports Rust), and then + configure cmake and cargo to use them by setting environnement variables. On + Linux and macOS, you should set the following (look up how to do set environment + variable with your shell): .. code-block:: bash @@ -88,32 +96,70 @@ changes: Running tests ------------- -The continuous integration pipeline is based on `tox`_. You can run all tests +The continuous integration pipeline is based on `cargo`_. You can run all tests with: .. code-block:: bash cd - tox + cargo test # or cargo test --release to run tests in release mode -These are exactly the same tests that will be performed online in our Github CI +These are exactly the same tests that will be performed online in our GitHub CI workflows. You can also run only a subset of tests with one of these commands: +- ``cargo test`` runs everything + +- ``cargo test --package=metatomic-torch`` to run the C++ TorchScript tests only; + + - ``cargo test --test=run-torch-tests`` will run the unit tests for the + TorchScript C++ extension; + - ``cargo test --test=check-cxx-install`` will build the C++ TorchScript + extension, install it and then try to build a basic project depending on + this extension with CMake; + +- ``cargo test --package=metatomic-python`` (or ``tox`` directly, see below) to + run Python tests only; +- ``cargo test --lib`` to run unit tests; +- ``cargo test --doc`` to run documentation tests; +- ``cargo bench --test`` compiles and run the benchmarks once, to quickly ensure + they still work. + +You can add some flags to any of above commands to further refine which tests +should run: + +- ``--release`` to run tests in release mode (default is to run tests in debug mode) +- ``-- `` to only run tests whose name contains filter, for example ``cargo test -- system`` + +Also, you can run individual Python tests using `tox`_ if you wish to run a +subset of Python tests, for example: + .. code-block:: bash tox -e lint # check files for formatting errors tox -e torch-tests # unit tests for metatomic-torch, in Python - tox -e torch-tests-cxx # unit tests for metatomic-torch, in C++ - tox -e torch-install-tests-cxx # testing that the C++ code is a valid CMake package + tox -e ase-tests # unit tests for metatomic-ase, in Python + tox -e torchsim-tests # unit tests for metatomic-torchsim, in Python tox -e docs-tests # doctests (checking inline examples) for all packages - tox -e lint # code style tox -e format # format all files -The last command ``tox -e format`` will use ``tox`` to do actual formatting -instead of just checking it, you can use this to automatically fix some of the -issues detected by ``tox -e lint``. +The last command ``tox -e format`` will use tox to do actual formatting instead +of just checking it, you can use to automatically fix some of the issues +detected by ``tox -e lint``. + +You can run only a subset of the tests with ``tox -e tests -- ``, +replacing ```` with the path to the files you want to test, e.g. +``tox -e tests -- python/tests/operations/abs.py``. + +To get the release build for ``tox`` runs, set the environment variable. + +.. code-block:: bash + + METATOMIC_BUILD_TYPE="release" tox -e torch-tests + +This corresponds to running ``cargo test --package-metatensor-python --release`` +but on the subset of interest. You can run only a subset of the tests with ``tox -e torch-tests -- ``, replacing ```` with the path to the files you diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..5256b960 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" + +members = [ + "metatomic-torch", + "python", +] diff --git a/docs/src/devdoc/get-started.rst b/docs/src/devdoc/get-started.rst new file mode 100644 index 00000000..4c19e4ef --- /dev/null +++ b/docs/src/devdoc/get-started.rst @@ -0,0 +1,6 @@ +.. _devdoc-get-started: + +Getting started +=============== + +.. include:: ../../../CONTRIBUTING.rst diff --git a/docs/src/devdoc/index.rst b/docs/src/devdoc/index.rst new file mode 100644 index 00000000..43755fdf --- /dev/null +++ b/docs/src/devdoc/index.rst @@ -0,0 +1,26 @@ +.. _devdoc: + +Developer documentation +####################### + +This developer documentation contains the following sections: + +1. :ref:`devdoc-get-started` explains how you can start developing code and + documentation; + +.. toctree:: + :maxdepth: 2 + + get-started + +Development team +---------------- + +Metatensor is developed in the `COSMO laboratory`_ at `EPFL`_, and made +available under the `BSD 3-clauses license `_. We welcome +contributions from anyone, feel free to contact us if you need some help working +with the code! + +.. _COSMO laboratory: https://www.epfl.ch/labs/cosmo/ +.. _EPFL: https://www.epfl.ch/ +.. _LICENSE: https://github.com/metatensor/metatensor/blob/main/LICENSE diff --git a/docs/src/index.rst b/docs/src/index.rst index d94c6ded..170c25c1 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -96,4 +96,5 @@ existing trained models, look into the metatrain_ project instead. quantities/index engines/index examples/index + devdoc/index cite diff --git a/metatomic-torch/Cargo.toml b/metatomic-torch/Cargo.toml new file mode 100644 index 00000000..3809a5a9 --- /dev/null +++ b/metatomic-torch/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "metatomic-torch" +version = "0.0.0" +edition = "2021" +publish = false +rust-version = "1.74" + +[lib] +path = "lib.rs" + +[dev-dependencies] +lazy_static = "1" +which = "8" diff --git a/metatomic-torch/lib.rs b/metatomic-torch/lib.rs new file mode 100644 index 00000000..59bc69bb --- /dev/null +++ b/metatomic-torch/lib.rs @@ -0,0 +1 @@ +// empty lib.rs, this crate only exists to run TorchScript C++ tests with cargo diff --git a/metatomic-torch/tests/CMakeLists.txt b/metatomic-torch/tests/CMakeLists.txt index 89a3db0f..8a64a4f3 100644 --- a/metatomic-torch/tests/CMakeLists.txt +++ b/metatomic-torch/tests/CMakeLists.txt @@ -14,9 +14,11 @@ if (VALGRIND) "--leak-check=full" "--show-leak-kinds=definite,indirect,possible" "--track-origins=yes" "--gen-suppressions=all" "--suppressions=${CMAKE_CURRENT_SOURCE_DIR}/valgrind.supp" ) + set(USING_VALGRIND ON) endif() else() set(TEST_COMMAND "") + set(USING_VALGRIND OFF) endif() @@ -46,7 +48,9 @@ foreach(_file_ ${ALL_TESTS}) ) # stop tests if they run for more than 30s - set_tests_properties(torch-${_name_} PROPERTIES TIMEOUT 30) + if (NOT USING_VALGRIND) + set_tests_properties(torch-${_name_} PROPERTIES TIMEOUT 30) + endif() if(WIN32) # We need to set the path to allow access to torch.dll diff --git a/metatomic-torch/tests/check-torch-install.rs b/metatomic-torch/tests/check-torch-install.rs new file mode 100644 index 00000000..8883d916 --- /dev/null +++ b/metatomic-torch/tests/check-torch-install.rs @@ -0,0 +1,207 @@ +use std::path::PathBuf; +use std::sync::Mutex; + +mod utils; + +lazy_static::lazy_static! { + // Make sure only one of the tests below run at the time, since they both + // try to modify the same files + static ref LOCK: Mutex<()> = Mutex::new(()); +} + +/// Check that metatomic-torch can be built and installed with cmake, and that +/// the installed version can be used from another cmake project with +/// `find_package` +#[test] +fn check_torch_install() { + let _guard = match LOCK.lock() { + Ok(guard) => guard, + Err(_) => { + panic!("another test failed, stopping") + } + }; + + const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); + let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + // ====================================================================== // + // build and install metatensor-torch with cmake + let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); + build_dir.push("torch-install"); + build_dir.push("cmake-find-package"); + std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + + + let deps_dir = build_dir.join("deps"); + + let torch_dep = deps_dir.join("virtualenv"); + std::fs::create_dir_all(&torch_dep).expect("failed to create virtualenv dir"); + let python = utils::create_python_venv(torch_dep); + let pytorch_cmake_prefix = utils::setup_torch_pip(&python); + let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python); + let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python); + + // configure cmake for metatomic-torch + let metatomic_torch_dep = deps_dir.join("metatomic-torch"); + + let cmake_options = vec![ + format!( + "-DCMAKE_PREFIX_PATH={};{};{}", + pytorch_cmake_prefix.display(), + metatensor_cmake_prefix.display(), + metatensor_torch_cmake_prefix.display() + ), + // The two properties below handle the RPATH for metatomic_torch, + // setting it in such a way that we can always load libmetatensor.so and + // libtorch.so from the location they are found at when compiling + // metatomic-torch. See + // https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling + // for more information on CMake RPATH handling + "-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON".into(), + "-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON".into(), + ]; + + let install_prefix = utils::setup_metatomic_torch_cmake( + &cargo_manifest_dir, + &metatomic_torch_dep, + cmake_options, + ); + + // ====================================================================== // + // // try to use the installed metatomic-torch from cmake + let mut source_dir = PathBuf::from(&cargo_manifest_dir); + source_dir.extend(["tests", "cmake-project"]); + + // configure cmake for the test cmake project + let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); + cmake_config.arg(format!( + "-DCMAKE_PREFIX_PATH={};{};{};{}", + metatensor_cmake_prefix.display(), + pytorch_cmake_prefix.display(), + metatensor_torch_cmake_prefix.display(), + install_prefix.display(), + )); + + utils::run_command(cmake_config, "cmake configuration"); + + // build the code, linking to metatomic-torch + let cmake_build = utils::cmake_build(&build_dir); + utils::run_command(cmake_build, "cmake build"); + + // run the executables + let ctest = utils::ctest(&build_dir); + utils::run_command(ctest, "ctest"); +} + +/// Same as above, but using pre-built metatensor-torch from the Python wheel, +/// instead of building it from source with cmake. +#[test] +fn check_python_install() { + let _guard = match LOCK.lock() { + Ok(guard) => guard, + Err(_) => { + panic!("another test failed, stopping") + } + }; + + const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); + + // ====================================================================== // + // build and install metatensor and metatensor-torch with pip + let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); + build_dir.push("torch-install"); + build_dir.push("python-wheels"); + std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + + let mut venv_dir = build_dir.clone(); + venv_dir.push("virtualenv"); + + let python_exe = utils::create_python_venv(venv_dir); + + let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let pytorch_cmake_prefix = utils::setup_torch_pip(&python_exe); + let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); + let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python_exe); + + let python_source_dir = cargo_manifest_dir.parent().unwrap().join("python").join("metatomic_torch"); + let metatomic_torch_cmake_prefix = utils::setup_metatomic_torch_pip(&python_exe, &python_source_dir); + + // ====================================================================== // + // try to use the installed metatensor-torch from cmake + let mut source_dir = PathBuf::from(&cargo_manifest_dir); + source_dir.extend(["tests", "cmake-project"]); + + // configure cmake for the test cmake project + let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); + cmake_config.arg(format!( + "-DCMAKE_PREFIX_PATH={};{};{};{}", + pytorch_cmake_prefix.display(), + metatensor_cmake_prefix.display(), + metatensor_torch_cmake_prefix.display(), + metatomic_torch_cmake_prefix.display(), + )); + + utils::run_command(cmake_config, "cmake configuration"); + + // build the code, linking to metatensor-torch + let cmake_build = utils::cmake_build(&build_dir); + utils::run_command(cmake_build, "cmake build"); + + // run the executables + let ctest = utils::ctest(&build_dir); + utils::run_command(ctest, "ctest"); +} + +/// Same test as above, but building metatomic-torch in the same +/// CMake project (i.e. using add_subdirectory instead of find_package) +#[test] +fn check_cmake_subdirectory() { + let _guard = match LOCK.lock() { + Ok(guard) => guard, + Err(_) => { + panic!("another test failed, stopping") + } + }; + + const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); + + // install torch + let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); + build_dir.push("torch-install"); + build_dir.push("cmake-subdirectory"); + std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + + let deps_dir = build_dir.join("deps"); + + let torch_dep = deps_dir.join("virtualenv"); + std::fs::create_dir_all(&torch_dep).expect("failed to create virtualenv dir"); + let python = utils::create_python_venv(torch_dep); + let pytorch_cmake_prefix = utils::setup_torch_pip(&python); + let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python); + let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python); + + // ====================================================================== // + let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let mut source_dir = PathBuf::from(&cargo_manifest_dir); + source_dir.extend(["tests", "cmake-project"]); + + // configure cmake for the test cmake project + let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); + cmake_config.arg(format!( + "-DCMAKE_PREFIX_PATH={};{};{}", + pytorch_cmake_prefix.display(), + metatensor_cmake_prefix.display(), + metatensor_torch_cmake_prefix.display() + )); + cmake_config.arg("-DUSE_CMAKE_SUBDIRECTORY=ON"); + + utils::run_command(cmake_config, "cmake configuration"); + + // build the code, linking to metatomic-torch + let cmake_build = utils::cmake_build(&build_dir); + utils::run_command(cmake_build, "cmake build"); + + // run the executables + let ctest = utils::ctest(&build_dir); + utils::run_command(ctest, "ctest"); +} diff --git a/metatomic-torch/tests/run-torch-tests.rs b/metatomic-torch/tests/run-torch-tests.rs new file mode 100644 index 00000000..93772f0a --- /dev/null +++ b/metatomic-torch/tests/run-torch-tests.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +mod utils; + +#[test] +fn run_torch_tests() { + const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); + let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + // ====================================================================== // + // setup dependencies for the torch tests + + let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); + build_dir.push("torch-tests"); + let deps_dir = build_dir.join("deps"); + + let torch_dep = deps_dir.join("virtualenv"); + std::fs::create_dir_all(&torch_dep).expect("failed to create virtualenv dir"); + let python_exe = utils::create_python_venv(torch_dep); + let pytorch_cmake_prefix = utils::setup_torch_pip(&python_exe); + let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); + let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python_exe); + + // ====================================================================== // + // build the metatomic-torch C++ tests and run them + let source_dir = cargo_manifest_dir; + + // configure cmake for the tests + let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); + cmake_config.arg("-DMETATOMIC_TORCH_TESTS=ON"); + cmake_config.arg(format!( + "-DCMAKE_PREFIX_PATH={};{};{}", + pytorch_cmake_prefix.display(), + metatensor_cmake_prefix.display(), + metatensor_torch_cmake_prefix.display() + )); + + utils::run_command(cmake_config, "cmake configuration"); + + // build the tests + let cmake_build = utils::cmake_build(&build_dir); + utils::run_command(cmake_build, "cmake build"); + + // run the tests + let ctest = utils::ctest(&build_dir); + utils::run_command(ctest, "ctest"); +} diff --git a/metatomic-torch/tests/utils/mod.rs b/metatomic-torch/tests/utils/mod.rs new file mode 100644 index 00000000..e223bcee --- /dev/null +++ b/metatomic-torch/tests/utils/mod.rs @@ -0,0 +1,410 @@ +#![allow(dead_code)] +#![allow(clippy::needless_return)] + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +fn build_type() -> &'static str { + // assume that debug assertion means that we are building the code in + // debug mode, even if that could be not true in some cases + if cfg!(debug_assertions) { + "debug" + } else { + "release" + } +} + +fn append_flags(existing: Option, extra: &str) -> String { + match existing { + Some(flags) if !flags.trim().is_empty() => format!("{flags} {extra}"), + _ => extra.into(), + } +} + +pub fn cmake_config(source_dir: &Path, build_dir: &Path) -> Command { + let cmake = which::which("cmake").expect("could not find cmake"); + + let mut cmake_config = Command::new(cmake); + cmake_config.current_dir(build_dir); + cmake_config.arg(source_dir); + cmake_config.arg("--no-warn-unused-cli"); + cmake_config.arg(format!("-DCMAKE_BUILD_TYPE={}", build_type())); + + // the cargo executable currently running + let cargo_exe = std::env::var("CARGO").expect("CARGO env var is not set"); + cmake_config.arg(format!("-DCARGO_EXE={}", cargo_exe)); + + if std::env::var_os("CARGO_LLVM_COV").is_some() { + let coverage_compile_flags = "-fprofile-instr-generate -fcoverage-mapping"; + let coverage_link_flags = "-fprofile-instr-generate"; + + let c_flags = append_flags(std::env::var("CFLAGS").ok(), coverage_compile_flags); + let cxx_flags = append_flags(std::env::var("CXXFLAGS").ok(), coverage_compile_flags); + let exe_linker_flags = + append_flags(std::env::var("LDFLAGS").ok(), coverage_link_flags); + + cmake_config.arg(format!("-DCMAKE_C_FLAGS={c_flags}")); + cmake_config.arg(format!("-DCMAKE_CXX_FLAGS={cxx_flags}")); + cmake_config.arg(format!("-DCMAKE_EXE_LINKER_FLAGS={exe_linker_flags}")); + cmake_config.arg(format!("-DCMAKE_SHARED_LINKER_FLAGS={exe_linker_flags}")); + } + + return cmake_config; +} + +pub fn cmake_build(build_dir: &Path) -> Command { + let cmake = which::which("cmake").expect("could not find cmake"); + + let mut cmake_build = Command::new(cmake); + cmake_build.current_dir(build_dir); + cmake_build.arg("--build"); + cmake_build.arg("."); + cmake_build.arg("--parallel"); + cmake_build.arg("--config"); + cmake_build.arg(build_type()); + + return cmake_build; +} + + +pub fn ctest(build_dir: &Path) -> Command { + let ctest = which::which("ctest").expect("could not find ctest"); + + let mut ctest = Command::new(ctest); + ctest.current_dir(build_dir); + ctest.arg("--output-on-failure"); + ctest.arg("--build-config"); + ctest.arg(build_type()); + + return ctest +} + +/// Find the path to the uv binary, or None if not present +fn find_uv() -> Option { + which::which("uv").ok() +} + +/// Find the path to the `python`or `python3` binary on the user system +fn find_python() -> PathBuf { + if let Ok(python) = which::which("python") { + let output = Command::new(&python) + .arg("-c") + .arg("import sys; print(sys.version_info.major)") + .output() + .expect("could not run python"); + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + + if stdout.trim() == "3" { + // we found Python 3 + return python; + } + } + } + + // try python3 + let python = which::which("python3").expect("failed to run `which python3`"); + let output = Command::new(&python) + .arg("-c") + .arg("import sys; print(sys.version_info.major)") + .output() + .expect("could not run python"); + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + if stdout.trim() == "3" { + // we found Python 3 + return python; + } + } + + panic!("could not find Python 3") +} + +/// Helper: get python executable path inside a venv +fn python_in_venv(venv_dir: &Path) -> PathBuf { + let mut python = venv_dir.to_path_buf(); + if cfg!(target_os = "windows") { + python.extend(["Scripts", "python.exe"]); + } else { + python.extend(["bin", "python"]); + } + python +} + +/// Create a fresh Python virtualenv using uv if available, else fallback to +/// `python -m venv`, and return the path to the python executable in the venv +pub fn create_python_venv(build_dir: PathBuf) -> PathBuf { + if let Some(uv_bin) = find_uv() { + let mut cmd = Command::new(&uv_bin); + cmd.arg("venv"); + cmd.arg("--clear"); + cmd.arg(&build_dir); + + run_command(cmd, "uv venv creation"); + } else { + let mut cmd = Command::new(find_python()); + cmd.arg("-m"); + cmd.arg("venv"); + cmd.arg(&build_dir); + + run_command(cmd, "python to create virtualenv with `venv`"); + + // update pip in case the system uses a very old one + let python = python_in_venv(&build_dir); + let mut cmd = Command::new(&python); + cmd.arg("-m"); + cmd.arg("pip"); + cmd.arg("install"); + cmd.arg("--upgrade"); + cmd.arg("pip"); + + run_command(cmd, "pip upgrade in virtualenv"); + } + + python_in_venv(&build_dir) +} + +#[derive(Default)] +pub struct PipInstallOptions { + pub upgrade: bool, + pub no_deps: bool, + pub no_build_isolation: bool, +} + +/// Install a package with pip (uses uv if present, else falls back to python) +fn pip_install( + python: &Path, + packages: &[&str], + options: PipInstallOptions, +) { + if let Some(uv_bin) = find_uv() { + let mut cmd = Command::new(&uv_bin); + cmd.arg("pip").arg("install").arg("--python").arg(python); + + // follow the same behavior as pip when there are multiple indexes + cmd.arg("--index-strategy"); + cmd.arg("unsafe-best-match"); + + if options.upgrade { + cmd.arg("--upgrade"); + } + if options.no_deps { + cmd.arg("--no-deps"); + } + if options.no_build_isolation { + cmd.arg("--no-build-isolation"); + // uv doesn't support --check-build-dependencies + } + + for package in packages { + cmd.arg(package); + } + + run_command(cmd, "uv pip install"); + } else { + let mut cmd = Command::new(python); + cmd.arg("-m").arg("pip").arg("install"); + if options.upgrade { + cmd.arg("--upgrade"); + } + if options.no_deps { + cmd.arg("--no-deps"); + } + if options.no_build_isolation { + // If pip, add both supported options + cmd.arg("--no-build-isolation"); + cmd.arg("--check-build-dependencies"); + } + + for package in packages { + cmd.arg(package); + } + + run_command(cmd, "pip install"); + } +} + +/// Download PyTorch in a Python virtualenv, and return the +/// CMAKE_PREFIX_PATH for the corresponding libtorch +pub fn setup_torch_pip(python: &Path) -> PathBuf { + let torch_version = std::env::var("METATOMIC_TESTS_TORCH_VERSION").unwrap_or("2.12".into()); + pip_install( + python, + &[&format!("torch=={}.*", torch_version)], + PipInstallOptions { upgrade: true, no_deps: false, no_build_isolation: false } + ); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import torch; print(torch.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get torch cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +/// Install metatensor in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatensor. +pub fn setup_metatensor_pip(python: &Path) -> PathBuf { + pip_install(python, &["metatensor-core >=0.2.0,<0.3"], PipInstallOptions::default()); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import metatensor; print(metatensor.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get metatensor cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'metatensor.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +/// Install metatensor-torch in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatensor_torch. +pub fn setup_metatensor_torch_pip(python: &Path) -> PathBuf { + pip_install(python, &["metatensor-torch >=0.9.0,<0.10"], PipInstallOptions::default()); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import metatensor.torch; print(metatensor.torch.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get metatensor_torch cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'metatensor.torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +/// Build metatomic-torch located in `source_dir` inside `build_dir`, and return +/// the installation prefix. +pub fn setup_metatomic_torch_cmake(source_dir: &Path, build_dir: &Path, cmake_args: Vec) -> PathBuf { + std::fs::create_dir_all(build_dir).expect("failed to create metatomic build dir"); + + // configure cmake for metatomic-torch + let mut cmake_config = cmake_config(source_dir, build_dir); + + let install_prefix = build_dir.join("usr"); + cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix.display())); + + // Add any additional cmake arguments + for arg in cmake_args { + cmake_config.arg(arg); + } + + run_command(cmake_config, "cmake configuration for metatomic_torch"); + + // build and install metatomic-torch + let mut cmake_build = cmake_build(build_dir); + cmake_build.arg("--target"); + cmake_build.arg("install"); + + run_command(cmake_build, "cmake build for metatomic_torch"); + + install_prefix +} + + +/// Install metatomic-torch in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatomic_torch. +pub fn setup_metatomic_torch_pip(python: &Path, source_dir: &Path) -> PathBuf { + pip_install(python, &["setuptools>=77", "packaging>=23", "cmake"], PipInstallOptions::default()); + + pip_install( + python, + &[&source_dir.display().to_string()], + PipInstallOptions { + upgrade: true, + no_deps: false, + no_build_isolation: true + } + ); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import metatomic.torch; print(metatomic.torch.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get metatomic_torch cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'metatomic.torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + + +pub fn run_command(mut command: Command, context: &str) -> std::process::Output { + write!(std::io::stdout().lock(), "\n\n[Running] {:?}\n\n", command).unwrap(); + + let mut child = command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn().unwrap_or_else(|_| panic!("failed to spawn {}", context)); + + let mut child_stdout = child.stdout.take().expect("missing stdout"); + let mut child_stderr = child.stderr.take().expect("missing stderr"); + + let out_handle = std::thread::spawn(move || -> std::io::Result> { + let mut buf = [0u8; 8192]; + let mut captured = Vec::new(); + let mut sink = std::io::stdout().lock(); + loop { + let n = child_stdout.read(&mut buf)?; + if n == 0 { + break; + } + sink.write_all(&buf[..n])?; + sink.flush()?; + captured.extend_from_slice(&buf[..n]); + } + Ok(captured) + }); + + let err_handle = std::thread::spawn(move || -> std::io::Result> { + let mut buf = [0u8; 8192]; + let mut captured = Vec::new(); + let mut sink = std::io::stderr().lock(); + loop { + let n = child_stderr.read(&mut buf)?; + if n == 0 { + break; + } + sink.write_all(&buf[..n])?; + sink.flush()?; + captured.extend_from_slice(&buf[..n]); + } + Ok(captured) + }); + + let status = child.wait().unwrap_or_else(|_| panic!("failed to run {}", context)); + let stdout = String::from_utf8_lossy(&out_handle.join().unwrap().unwrap()).into_owned(); + let stderr = String::from_utf8_lossy(&err_handle.join().unwrap().unwrap()).into_owned(); + + if !status.success() { + panic!( + "{} failed, status: {}\nstderr:\n\n{}\nstdout:\n\n{}\n", + context, status, stderr, stdout + ); + } + + return std::process::Output { status, stdout: stdout.into_bytes(), stderr: stderr.into_bytes() }; +} diff --git a/python/Cargo.toml b/python/Cargo.toml new file mode 100644 index 00000000..2ca54178 --- /dev/null +++ b/python/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "metatomic-python" +version = "0.0.0" +edition = "2021" +publish = false +rust-version = "1.74" + +[lib] +path = "lib.rs" + +[dev-dependencies] +which = "8" diff --git a/python/lib.rs b/python/lib.rs new file mode 100644 index 00000000..5ef74bad --- /dev/null +++ b/python/lib.rs @@ -0,0 +1 @@ +// empty lib.rs, this crate only exists to run Python tests with cargo diff --git a/python/tests/run-python-tests.rs b/python/tests/run-python-tests.rs new file mode 100644 index 00000000..8d52a6f8 --- /dev/null +++ b/python/tests/run-python-tests.rs @@ -0,0 +1,23 @@ +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn run_python_tests() { + let tox = which::which("tox").expect("could not find tox"); + + let mut root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + root.pop(); + + let mut tox = Command::new(tox); + tox.arg("--"); + if cfg!(debug_assertions) { + // assume that debug assertions means that we are building the code + // in debug mode, even if optimizations could be enabled + tox.env("METATOMIC_BUILD_TYPE", "debug"); + } else { + tox.env("METATOMIC_BUILD_TYPE", "release"); + } + tox.current_dir(&root); + let status = tox.status().expect("failed to run tox"); + assert!(status.success()); +} diff --git a/tox.ini b/tox.ini index 6eae4e86..8674eea7 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,6 @@ requires = tox >=4.39 # `tox` in the command-line without anything else envlist = lint - torch-tests-cxx - torch-install-tests-cxx torch-tests docs-tests ase-tests @@ -45,75 +43,6 @@ metatensor_deps = metatensor-operations >=0.5.0,<0.6 -################################################################################ -##### C++ tests setup ##### -################################################################################ - -[testenv:torch-tests-cxx] -description = Run the C++ tests for metatomic-torch -deps = - cmake - {[testenv]metatensor_deps} - torch=={env:METATOMIC_TESTS_TORCH_VERSION:2.12}.* - -commands = - # configure cmake - cmake -B {env_dir}/build metatomic-torch \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DCMAKE_PREFIX_PATH={env_site_packages_dir}/metatensor/;\ - {env_site_packages_dir}/torch/;\ - {env_site_packages_dir}/metatensor_torch/torch-{env:METATOMIC_TESTS_TORCH_VERSION:2.12}/ \ - -DMETATOMIC_TORCH_TESTS=ON - - # build code with cmake - cmake --build {env_dir}/build --config Debug --parallel - - # run all tests - ctest --test-dir {env_dir}/build --build-config Debug --output-on-failure - -[testenv:torch-install-tests-cxx] -description = Run the C++ tests for metatomic-torch -deps = - cmake - {[testenv]metatensor_deps} - torch=={env:METATOMIC_TESTS_TORCH_VERSION:2.12}.* - -commands = - # configure, build and install metatomic-torch - cmake -B {env_dir}/build-metatomic-torch metatomic-torch \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX={env_dir}/usr/ \ - -DCMAKE_PREFIX_PATH={env_site_packages_dir}/metatensor/;\ - {env_site_packages_dir}/torch/;\ - {env_site_packages_dir}/metatensor_torch/torch-{env:METATOMIC_TESTS_TORCH_VERSION:2.12}/ \ - -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ - -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON - cmake --build {env_dir}/build-metatomic-torch --config Debug --parallel --target install - - # try to use the installed metatomic-torch from another CMake project - cmake -B {env_dir}/build-find-package metatomic-torch/tests/cmake-project \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_PREFIX_PATH={env_site_packages_dir}/metatensor/;\ - {env_site_packages_dir}/torch/;\ - {env_site_packages_dir}/metatensor_torch/torch-{env:METATOMIC_TESTS_TORCH_VERSION:2.12}/;\ - {env_dir}/usr/ \ - -DUSE_CMAKE_SUBDIRECTORY=OFF - - cmake --build {env_dir}/build-find-package --config Debug --parallel - ctest --test-dir {env_dir}/build-find-package --build-config Debug --output-on-failure - - # Same, but using metatomic-torch as a CMake subdirectory - cmake -B {env_dir}/build-subdirectory metatomic-torch/tests/cmake-project \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_PREFIX_PATH={env_site_packages_dir}/metatensor/;\ - {env_site_packages_dir}/torch/;\ - {env_site_packages_dir}/metatensor_torch/torch-{env:METATOMIC_TESTS_TORCH_VERSION:2.12}/ \ - -DUSE_CMAKE_SUBDIRECTORY=ON - - cmake --build {env_dir}/build-subdirectory --config Debug --parallel - ctest --test-dir {env_dir}/build-subdirectory --build-config Debug --output-on-failure - ################################################################################ ##### Python tests setup ##### ################################################################################ From 094a56c93d186cf859f814c1bcfc12d3293dc125 Mon Sep 17 00:00:00 2001 From: Guillaume Fraux Date: Wed, 13 May 2026 14:45:08 +0200 Subject: [PATCH 04/10] Scaffold a new metatomic-core package --- .github/workflows/build-wheels.yml | 11 +- .github/workflows/torch-tests.yml | 7 + Cargo.toml | 1 + metatomic-core/CHANGELOG.md | 18 + metatomic-core/CMakeLists.txt | 506 ++++++++++++++++++ metatomic-core/Cargo.toml | 26 + metatomic-core/Clippy.toml | 1 + metatomic-core/build.rs | 48 ++ metatomic-core/cmake/dev-versions.cmake | 91 ++++ .../cmake/metatomic-config.in.cmake | 91 ++++ metatomic-core/cmake/tempdir.cmake | 51 ++ metatomic-core/include/metatomic.h | 32 ++ metatomic-core/include/metatomic.hpp | 2 + metatomic-core/include/metatomic/model.hpp | 7 + metatomic-core/include/metatomic/system.hpp | 7 + metatomic-core/src/c_api/mod.rs | 18 + metatomic-core/src/lib.rs | 13 + metatomic-core/tests/CMakeLists.txt | 86 +++ metatomic-core/tests/check-cxx-install.rs | 64 +++ .../tests/cmake-project/CMakeLists.txt | 84 +++ metatomic-core/tests/cmake-project/README.md | 3 + metatomic-core/tests/cmake-project/src/main.c | 8 + .../tests/cmake-project/src/main.cpp | 9 + .../tests/external/.gitattributes | 0 .../tests/external/CMakeLists.txt | 0 .../tests/external/catch/catch.cpp | 0 .../tests/external/catch/catch.hpp | 0 metatomic-core/tests/misc.cpp | 15 + metatomic-core/tests/run-cxx-tests.rs | 40 ++ metatomic-core/tests/utils/mod.rs | 470 ++++++++++++++++ metatomic-torch/tests/CMakeLists.txt | 3 +- metatomic-torch/tests/check-torch-install.rs | 10 +- metatomic-torch/tests/utils/mod.rs | 411 +------------- .../metatomic_torch/build-backend/backend.py | 17 +- 34 files changed, 1733 insertions(+), 417 deletions(-) create mode 100644 metatomic-core/CHANGELOG.md create mode 100644 metatomic-core/CMakeLists.txt create mode 100644 metatomic-core/Cargo.toml create mode 100644 metatomic-core/Clippy.toml create mode 100644 metatomic-core/build.rs create mode 100644 metatomic-core/cmake/dev-versions.cmake create mode 100644 metatomic-core/cmake/metatomic-config.in.cmake create mode 100644 metatomic-core/cmake/tempdir.cmake create mode 100644 metatomic-core/include/metatomic.h create mode 100644 metatomic-core/include/metatomic.hpp create mode 100644 metatomic-core/include/metatomic/model.hpp create mode 100644 metatomic-core/include/metatomic/system.hpp create mode 100644 metatomic-core/src/c_api/mod.rs create mode 100644 metatomic-core/src/lib.rs create mode 100644 metatomic-core/tests/CMakeLists.txt create mode 100644 metatomic-core/tests/check-cxx-install.rs create mode 100644 metatomic-core/tests/cmake-project/CMakeLists.txt create mode 100644 metatomic-core/tests/cmake-project/README.md create mode 100644 metatomic-core/tests/cmake-project/src/main.c create mode 100644 metatomic-core/tests/cmake-project/src/main.cpp rename {metatomic-torch => metatomic-core}/tests/external/.gitattributes (100%) rename {metatomic-torch => metatomic-core}/tests/external/CMakeLists.txt (100%) rename {metatomic-torch => metatomic-core}/tests/external/catch/catch.cpp (100%) rename {metatomic-torch => metatomic-core}/tests/external/catch/catch.hpp (100%) create mode 100644 metatomic-core/tests/misc.cpp create mode 100644 metatomic-core/tests/run-cxx-tests.rs create mode 100644 metatomic-core/tests/utils/mod.rs mode change 100644 => 120000 metatomic-torch/tests/utils/mod.rs diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index b12d709e..564e0440 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -100,8 +100,17 @@ jobs: CIBW_BUILD_VERBOSITY: 1 CIBW_MANYLINUX_X86_64_IMAGE: gcc11-manylinux_2_28_x86_64 CIBW_MANYLINUX_AARCH64_IMAGE: gcc11-manylinux_2_28_aarch64 + # METATOMIC_NO_LOCAL_DEPS is set to 1 when building a tag of + # metatomic-torch, which will force to use the version of + # metatomic-core already released on PyPI. Otherwise, this will use + # the version of metatomic-core from git checkout (in case there are + # unreleased breaking changes). + # + # This means that when releasing a breaking change in metatomic-core, + # the full release should be available on PyPI before pushing the new + # metatomic-torch tag. CIBW_ENVIRONMENT: > - METATOMIC_NO_LOCAL_DEPS=1 + METATOMIC_NO_LOCAL_DEPS=${{ startsWith(github.ref, 'refs/tags/metatomic-torch-v') && '1' || '0' }} METATOMIC_TORCH_BUILD_WITH_TORCH_VERSION=${{ matrix.torch-version }}.* PIP_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cpu MACOSX_DEPLOYMENT_TARGET=11 diff --git a/.github/workflows/torch-tests.yml b/.github/workflows/torch-tests.yml index 62ed6025..ce378170 100644 --- a/.github/workflows/torch-tests.yml +++ b/.github/workflows/torch-tests.yml @@ -55,6 +55,12 @@ jobs: with: fetch-depth: 0 + - name: setup Python + uses: actions/setup-python@v6 + if: matrix.container == null + with: + python-version: ${{ matrix.python-version }} + - name: Configure git safe directory if: matrix.container == 'ubuntu:22.04' run: git config --global --add safe.directory /__w/metatomic/metatomic @@ -95,3 +101,4 @@ jobs: PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu METATOMIC_TESTS_TORCH_VERSION: ${{ matrix.torch-version }} CXXFLAGS: ${{ matrix.cxx-flags }} + RUST_BACKTRACE: full diff --git a/Cargo.toml b/Cargo.toml index 5256b960..1a233774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "metatomic-core", "metatomic-torch", "python", ] diff --git a/metatomic-core/CHANGELOG.md b/metatomic-core/CHANGELOG.md new file mode 100644 index 00000000..160995db --- /dev/null +++ b/metatomic-core/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to metatomic-core are documented here, following the [keep +a changelog](https://keepachangelog.com/en/1.1.0/) format. This project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased](https://github.com/metatensor/metatensor/) + + diff --git a/metatomic-core/CMakeLists.txt b/metatomic-core/CMakeLists.txt new file mode 100644 index 00000000..717e52e8 --- /dev/null +++ b/metatomic-core/CMakeLists.txt @@ -0,0 +1,506 @@ +# This file defines the CMake build system for the C and C++ API of metatomic. +# +# This API is implemented in Rust, in the metatomic-core crate, but Rust users +# of the API should use the metatomic crate instead, wrapping metatomic-core in +# an easier to use, idiomatic Rust API. +cmake_minimum_required(VERSION 3.22) + +# Is metatomic the main project configured by the user? Or is this being used +# as a submodule/subdirectory? +if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR}) + set(METATOMIC_MAIN_PROJECT ON) +else() + set(METATOMIC_MAIN_PROJECT OFF) +endif() + +if(${METATOMIC_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_VERSION}" VERSION_EQUAL ${CMAKE_VERSION}) + # We use CACHED_LAST_CMAKE_VERSION to only print the cmake version + # once in the configuration log + set(CACHED_LAST_CMAKE_VERSION ${CMAKE_VERSION} CACHE INTERNAL "Last version of cmake used to configure") + message(STATUS "Running CMake version ${CMAKE_VERSION}") +endif() + +if (POLICY CMP0077) + # use variables to set OPTIONS + cmake_policy(SET CMP0077 NEW) +endif() + +file(STRINGS "Cargo.toml" CARGO_TOML_CONTENT) +foreach(line ${CARGO_TOML_CONTENT}) + string(REGEX REPLACE "^version = \"(.*)\"" "\\1" METATOMIC_VERSION ${line}) + if (NOT ${CMAKE_MATCH_COUNT} EQUAL 0) + # stop on the first regex match, this should be the right version + break() + endif() +endforeach() + +include(cmake/dev-versions.cmake) +create_development_version("${METATOMIC_VERSION}" METATOMIC_FULL_VERSION "metatomic-core-v") +message(STATUS "Building metatomic-core v${METATOMIC_FULL_VERSION}") + +# strip any -dev/-rc suffix on the version since project(VERSION) does not support it +string(REGEX REPLACE "([0-9]*)\\.([0-9]*)\\.([0-9]*).*" "\\1.\\2.\\3" METATOMIC_VERSION ${METATOMIC_FULL_VERSION}) +project(metatomic + VERSION ${METATOMIC_VERSION} + LANGUAGES C CXX # we need to declare a language to access CMAKE_SIZEOF_VOID_P later +) +set(PROJECT_VERSION ${METATOMIC_FULL_VERSION}) + + +# We follow the standard CMake convention of using BUILD_SHARED_LIBS to provide +# either a shared or static library as a default target. But since cargo always +# builds both versions by default, we also install both versions by default. +# `METATOMIC_INSTALL_BOTH_STATIC_SHARED=OFF` allow to disable this behavior, and +# only install the file corresponding to `BUILD_SHARED_LIBS=ON/OFF`. +# +# BUILD_SHARED_LIBS controls the `metatomic` cmake target, making it an alias of +# either `metatomic::static` or `metatomic::shared`. This is mainly relevant +# when using metatomic from another cmake project, either as a submodule or from +# an installed library (see cmake/metatomic-config.cmake) +option(BUILD_SHARED_LIBS "Use a shared library by default instead of a static one" ON) +option(METATOMIC_INSTALL_BOTH_STATIC_SHARED "Install both shared and static libraries" ON) + +set(RUST_BUILD_TARGET "${RUST_BUILD_TARGET}" CACHE STRING "Cross-compilation target for rust code. Leave empty to build for the host") +set(EXTRA_RUST_FLAGS "${EXTRA_RUST_FLAGS}" CACHE STRING "Flags used to build rust code") + +include(GNUInstallDirs) + +if("${CMAKE_BUILD_TYPE}" STREQUAL "" AND "${CMAKE_CONFIGURATION_TYPES}" STREQUAL "") + message(STATUS "Setting build type to 'release' as none was specified.") + set(CMAKE_BUILD_TYPE "release" + CACHE STRING + "Choose the type of build, options are: debug or release" + FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS release debug) +endif() + +if(${METATOMIC_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_BUILD_TYPE}" STREQUAL "${CMAKE_BUILD_TYPE}") + set(CACHED_LAST_CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE} CACHE INTERNAL "Last build type used in configuration") + message(STATUS "Building metatomic in ${CMAKE_BUILD_TYPE} mode") +endif() + + +function(check_compatible_versions _actual_ _requested_) + if(${_actual_} MATCHES "^([0-9]+)\\.([0-9]+)") + set(_actual_major_ "${CMAKE_MATCH_1}") + set(_actual_minor_ "${CMAKE_MATCH_2}") + else() + message(FATAL_ERROR "Failed to parse actual version: ${_actual_}") + endif() + + if(${_requested_} MATCHES "^([0-9]+)\\.([0-9]+)") + set(_requested_major_ "${CMAKE_MATCH_1}") + set(_requested_minor_ "${CMAKE_MATCH_2}") + else() + message(FATAL_ERROR "Failed to parse requested version: ${_requested_}") + endif() + + if (${_requested_major_} EQUAL 0 AND ${_actual_minor_} EQUAL ${_requested_minor_}) + # major version is 0 and same minor version, everything is fine + elseif (${_actual_major_} EQUAL ${_requested_major_}) + # same major version, everything is fine + else() + # not compatible + message(FATAL_ERROR "Incompatible versions: we need ${_requested_}, but we got ${_actual_}") + endif() +endfunction() + + +set(REQUIRED_METATENSOR_VERSION "0.2.0") +# Either metatensor is built as part of the same CMake project, or we try to +# find the corresponding CMake package +if (TARGET metatensor) + get_target_property(METATENSOR_BUILD_VERSION metatensor BUILD_VERSION) + check_compatible_versions(${METATENSOR_BUILD_VERSION} ${REQUIRED_METATENSOR_VERSION}) +else() + find_package(metatensor ${REQUIRED_METATENSOR_VERSION} CONFIG REQUIRED) +endif() + + +find_program(CARGO_EXE "cargo" DOC "path to cargo (Rust build system)") +if (NOT CARGO_EXE) + message(FATAL_ERROR + "could not find cargo, please make sure the Rust compiler is installed \ + (see https://www.rust-lang.org/tools/install) or set CARGO_EXE" + ) +endif() + +execute_process( + COMMAND ${CARGO_EXE} "--version" "--verbose" + RESULT_VARIABLE CARGO_STATUS + OUTPUT_VARIABLE CARGO_VERSION_RAW +) + +if(CARGO_STATUS AND NOT CARGO_STATUS EQUAL 0) + message(FATAL_ERROR + "could not run cargo, please make sure the Rust compiler is installed \ + (see https://www.rust-lang.org/tools/install)" + ) +endif() + +set(REQUIRED_RUST_VERSION "1.74.0") +if (CARGO_VERSION_RAW MATCHES "cargo ([0-9]+\\.[0-9]+\\.[0-9]+).*") + set(CARGO_VERSION "${CMAKE_MATCH_1}") +else() + message(FATAL_ERROR "failed to determine cargo version, output was: ${CARGO_VERSION_RAW}") +endif() + +if (${CARGO_VERSION} VERSION_LESS ${REQUIRED_RUST_VERSION}) + message(FATAL_ERROR + "your Rust installation is too old (you have version ${CARGO_VERSION}), \ + at least ${REQUIRED_RUST_VERSION} is required" + ) +else() + if(NOT "${CACHED_LAST_CARGO_VERSION}" STREQUAL ${CARGO_VERSION}) + set(CACHED_LAST_CARGO_VERSION ${CARGO_VERSION} CACHE INTERNAL "Last version of cargo used in configuration") + message(STATUS "Using cargo version ${CARGO_VERSION} at ${CARGO_EXE}") + set(CARGO_VERSION_CHANGED TRUE) + endif() +endif() + +# ============================================================================ # +# determine Cargo flags + +set(CARGO_BUILD_ARG "") + +if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.lock) + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--locked") +endif() + +# TODO: support multiple configuration generators (MSVC, ...) +string(TOLOWER ${CMAKE_BUILD_TYPE} BUILD_TYPE) +if ("${BUILD_TYPE}" STREQUAL "debug") + set(CARGO_BUILD_TYPE "debug") +elseif("${BUILD_TYPE}" STREQUAL "release") + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--release") + set(CARGO_BUILD_TYPE "release") +elseif("${BUILD_TYPE}" STREQUAL "relwithdebinfo") + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--release") + set(CARGO_BUILD_TYPE "release") +else() + message(FATAL_ERROR "unsuported build type: ${CMAKE_BUILD_TYPE}") +endif() + +set(CARGO_TARGET_DIR ${CMAKE_CURRENT_BINARY_DIR}/target) +set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--target-dir=${CARGO_TARGET_DIR}") + +if (CARGO_VERSION_RAW MATCHES "host: ([a-zA-Z0-9_\\-]*)\n") + set(RUST_HOST_TARGET "${CMAKE_MATCH_1}") + if (RUST_HOST_TARGET MATCHES "([a-zA-Z0-9_]*)\\-") + set(RUST_HOST_ARCH "${CMAKE_MATCH_1}") + else() + message(FATAL_ERROR "failed to determine host CPU arch, target was: ${RUST_HOST_TARGET}") + endif() +else() + message(FATAL_ERROR "failed to determine host target, output was: ${CARGO_VERSION_RAW}") +endif() + +if (WIN32) + # on Windows, we need to use the same ABI in both CMake and cargo. If the + # user did not explicitly request a target, we can try to set it ourself, + # otherwise we just check that it matches what we expect. + if (MSVC) + if ("${RUST_BUILD_TARGET}" STREQUAL "") + set(RUST_BUILD_TARGET "${RUST_HOST_ARCH}-pc-windows-msvc") + message(STATUS "Setting rust target to ${RUST_BUILD_TARGET}") + elseif(NOT "${RUST_BUILD_TARGET}" MATCHES "-pc-windows-msvc") + message(FATAL_ERROR "CMake is building with MSVC but the Rust target is ${RUST_BUILD_TARGET}") + endif() + endif() + + if (MINGW) + if ("${RUST_BUILD_TARGET}" STREQUAL "") + set(RUST_BUILD_TARGET "${RUST_HOST_ARCH}-pc-windows-gnu") + message(STATUS "Setting rust target to ${RUST_BUILD_TARGET}") + elseif(NOT "${RUST_BUILD_TARGET}" MATCHES "-pc-windows-gnu") + message(FATAL_ERROR "CMake is building with MinGW but the Rust target is ${RUST_BUILD_TARGET}") + endif() + endif() +endif() + +# Handle cross compilation with RUST_BUILD_TARGET +if ("${RUST_BUILD_TARGET}" STREQUAL "") + if (${METATOMIC_MAIN_PROJECT}) + message(STATUS "Compiling to host (${RUST_HOST_TARGET})") + endif() + + set(CARGO_OUTPUT_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}") + set(RUST_BUILD_TARGET ${RUST_HOST_TARGET}) +else() + if (${METATOMIC_MAIN_PROJECT}) + message(STATUS "Cross-compiling to ${RUST_BUILD_TARGET}") + endif() + + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--target=${RUST_BUILD_TARGET}") + set(CARGO_OUTPUT_DIR "${CARGO_TARGET_DIR}/${RUST_BUILD_TARGET}/${CARGO_BUILD_TYPE}") +endif() + +# Get the list of libraries linked by default by cargo/rustc to add when linking +# to metatomic::static +if (CARGO_VERSION_CHANGED) + include(cmake/tempdir.cmake) + get_tempdir(TMPDIR) + + # Adapted from https://github.com/corrosion-rs/corrosion/blob/dc1e4e5/cmake/FindRust.cmake + execute_process( + COMMAND "${CARGO_EXE}" new --lib _cargo_required_libs + WORKING_DIRECTORY "${TMPDIR}" + RESULT_VARIABLE cargo_new_result + ERROR_QUIET + ) + + if (cargo_new_result) + message(FATAL_ERROR "could not create empty project to find default static libs: ${cargo_new_result}") + endif() + + file(APPEND "${TMPDIR}/_cargo_required_libs/Cargo.toml" "[lib]\ncrate-type=[\"staticlib\"]") + + execute_process( + COMMAND ${CARGO_EXE} rustc --color never --target=${RUST_BUILD_TARGET} -- --print=native-static-libs + WORKING_DIRECTORY "${TMPDIR}/_cargo_required_libs" + RESULT_VARIABLE cargo_static_libs_result + ERROR_VARIABLE cargo_static_libs_stderr + ) + + # clean up the files + file(REMOVE_RECURSE "${TMPDIR}") + + if (cargo_static_libs_result) + message(FATAL_ERROR + "could not extract default static libs (status ${cargo_static_libs_result}), stderr:\n${cargo_static_libs_stderr}" + ) + endif() + + # The pattern starts with `native-static-libs:` and goes to the end of the line. + if (cargo_static_libs_stderr MATCHES "native-static-libs: ([^\r\n]+)\r?\n") + string(REPLACE " " ";" "libs_list" "${CMAKE_MATCH_1}") + set(stripped_lib_list "") + foreach(lib ${libs_list}) + # Strip leading `-l` (unix) and potential .lib suffix (windows) + string(REGEX REPLACE "^-l" "" "stripped_lib" "${lib}") + string(REGEX REPLACE "\.lib$" "" "stripped_lib" "${stripped_lib}") + list(APPEND stripped_lib_list "${stripped_lib}") + endforeach() + + # Special case `msvcrt` to link with the debug version in Debug mode. + list(TRANSFORM stripped_lib_list REPLACE "^msvcrt$" "\$<\$:msvcrtd>") + # Don't try to pass a linker *flag* where CMake expects libraries + list(REMOVE_ITEM stripped_lib_list "/defaultlib:msvcrt") + + if (APPLE) + # Prevent warnings about duplicated `System` in linked libraries + # from Apple's `ld` + list(REMOVE_ITEM stripped_lib_list "System") + endif() + + list(REMOVE_DUPLICATES stripped_lib_list) + set(CARGO_DEFAULT_LIBRARIES "${stripped_lib_list}" CACHE INTERNAL "list of implicitly linked libraries") + + if (${METATOMIC_MAIN_PROJECT}) + message(STATUS "Cargo default link libraries are: ${CARGO_DEFAULT_LIBRARIES}") + endif() + else() + message(FATAL_ERROR "could not find default static libs: `native-static-libs` not found in: `${cargo_static_libs_stderr}`") + endif() +endif() + +file(GLOB_RECURSE ALL_RUST_SOURCES + ${PROJECT_SOURCE_DIR}/Cargo.toml + ${PROJECT_SOURCE_DIR}/src/**.rs +) + +add_library(metatomic::shared SHARED IMPORTED GLOBAL) +set(METATOMIC_SHARED_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}metatomic${CMAKE_SHARED_LIBRARY_SUFFIX}") +set(METATOMIC_IMPLIB_LOCATION "${METATOMIC_SHARED_LOCATION}.lib") + +if (MINGW) + # `rustc` does not follow the usual naming scheme for DLL with mingw (it + # would typically be 'libmetatomic.dll') + set(METATOMIC_SHARED_LOCATION "${CARGO_OUTPUT_DIR}/metatomic.dll") + set(METATOMIC_IMPLIB_LOCATION "${CARGO_OUTPUT_DIR}/libmetatomic.dll.a") +endif() + +add_library(metatomic::static STATIC IMPORTED GLOBAL) +set(METATOMIC_STATIC_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}metatomic${CMAKE_STATIC_LIBRARY_SUFFIX}") + +get_filename_component(METATOMIC_SHARED_LIB_NAME ${METATOMIC_SHARED_LOCATION} NAME) +get_filename_component(METATOMIC_IMPLIB_NAME ${METATOMIC_IMPLIB_LOCATION} NAME) +get_filename_component(METATOMIC_STATIC_LIB_NAME ${METATOMIC_STATIC_LOCATION} NAME) + +# We need to add some metadata to the shared library to enable linking to it +# without using an absolute path. +if (UNIX) + if (APPLE) + # set the install name to `@rpath/libmetatomic.dylib` + set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-install_name,@rpath/${METATOMIC_SHARED_LIB_NAME}") + set_target_properties(metatomic::shared PROPERTIES + IMPORTED_SONAME @rpath/${METATOMIC_SHARED_LIB_NAME} + ) + else() # LINUX + # set the SONAME to libmetatomic.so + set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-soname,${METATOMIC_SHARED_LIB_NAME}") + set_target_properties(metatomic::shared PROPERTIES + IMPORTED_SONAME ${METATOMIC_SHARED_LIB_NAME} + ) + endif() +else() + set(CARGO_RUSTC_ARGS "") +endif() + +if (NOT "${EXTRA_RUST_FLAGS}" STREQUAL "") + set(CARGO_RUSTC_ARGS "${CARGO_RUSTC_ARGS};${EXTRA_RUST_FLAGS}") +endif() + +# Set environment variables for cargo build +set(CARGO_ENV "METATOMIC_FULL_VERSION=${METATOMIC_FULL_VERSION}") +if (NOT "${CMAKE_OSX_DEPLOYMENT_TARGET}" STREQUAL "") + list(APPEND CARGO_ENV "MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}") +endif() +if (NOT "$ENV{RUSTC_WRAPPER}" STREQUAL "") + list(APPEND CARGO_ENV "RUSTC_WRAPPER=$ENV{RUSTC_WRAPPER}") +endif() + +if (METATOMIC_INSTALL_BOTH_STATIC_SHARED) + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=cdylib;--crate-type=staticlib") + set(CARGO_OUTPUTS ${METATOMIC_SHARED_LOCATION} ${METATOMIC_STATIC_LOCATION}) + if (WIN32) + list(APPEND CARGO_OUTPUTS ${METATOMIC_IMPLIB_LOCATION}) + set(FILE_CREATED_MESSAGE "${METATOMIC_SHARED_LIB_NAME}, ${METATOMIC_STATIC_LIB_NAME}, and ${METATOMIC_IMPLIB_NAME}") + else() + set(FILE_CREATED_MESSAGE "${METATOMIC_SHARED_LIB_NAME} and ${METATOMIC_STATIC_LIB_NAME}") + endif() +else() + if (BUILD_SHARED_LIBS) + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=cdylib") + set(CARGO_OUTPUTS ${METATOMIC_SHARED_LOCATION}) + if (WIN32) + list(APPEND CARGO_OUTPUTS ${METATOMIC_IMPLIB_LOCATION}) + set(FILE_CREATED_MESSAGE "${METATOMIC_SHARED_LIB_NAME} and ${METATOMIC_IMPLIB_NAME}") + else() + set(FILE_CREATED_MESSAGE "${METATOMIC_SHARED_LIB_NAME}") + endif() + else() + set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=staticlib") + set(CARGO_OUTPUTS ${METATOMIC_STATIC_LOCATION}) + set(FILE_CREATED_MESSAGE "${METATOMIC_STATIC_LIB_NAME}") + endif() +endif() + +add_custom_command( + OUTPUT ${CARGO_OUTPUTS} + COMMAND ${CMAKE_COMMAND} -E env ${CARGO_ENV} + cargo rustc ${CARGO_BUILD_ARG} -- ${CARGO_RUSTC_ARGS} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${ALL_RUST_SOURCES} + COMMENT "Building ${FILE_CREATED_MESSAGE} with cargo" + VERBATIM +) +add_custom_target(cargo-build-metatomic ALL DEPENDS ${CARGO_OUTPUTS}) + +# Auto-generate a header containing the version number as #define +set(_path_ "${CMAKE_CURRENT_BINARY_DIR}/generated-version.h") +file(WRITE ${_path_} "#pragma once\n\n") +file(APPEND ${_path_} "/** Full version of metatomic as a string */\n") +file(APPEND ${_path_} "#define METATOMIC_VERSION \"${METATOMIC_FULL_VERSION}\"\n\n") +file(APPEND ${_path_} "/** Major version number of metatomic as an integer */\n") +file(APPEND ${_path_} "#define METATOMIC_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}\n\n") +file(APPEND ${_path_} "/** Minor version number of metatomic as an integer */\n") +file(APPEND ${_path_} "#define METATOMIC_VERSION_MINOR ${PROJECT_VERSION_MINOR}\n\n") +file(APPEND ${_path_} "/** Patch version number of metatomic as an integer */\n") +file(APPEND ${_path_} "#define METATOMIC_VERSION_PATCH ${PROJECT_VERSION_PATCH}\n") + +file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/include/metatomic) +set(_destination_ "${CMAKE_CURRENT_BINARY_DIR}/include/metatomic/version.h") +file(COPY_FILE ${_path_} ${_destination_} ONLY_IF_DIFFERENT) + +add_dependencies(metatomic::shared cargo-build-metatomic) +add_dependencies(metatomic::static cargo-build-metatomic) + +set_target_properties(metatomic::shared PROPERTIES + IMPORTED_LOCATION ${METATOMIC_SHARED_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include;${CMAKE_CURRENT_BINARY_DIR}/include" + BUILD_VERSION "${METATOMIC_FULL_VERSION}" +) +target_compile_features(metatomic::shared INTERFACE cxx_std_17) + +if (WIN32) + set_target_properties(metatomic::shared PROPERTIES + IMPORTED_IMPLIB ${METATOMIC_IMPLIB_LOCATION} + ) +endif() + +set_target_properties(metatomic::static PROPERTIES + IMPORTED_LOCATION ${METATOMIC_STATIC_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include;${CMAKE_CURRENT_BINARY_DIR}/include" + INTERFACE_LINK_LIBRARIES "${CARGO_DEFAULT_LIBRARIES}" + BUILD_VERSION "${METATOMIC_FULL_VERSION}" +) +target_compile_features(metatomic::static INTERFACE cxx_std_17) + +if (TARGET metatensor::static) + target_link_libraries(metatomic::static INTERFACE metatensor::static) +else() + target_link_libraries(metatomic::static INTERFACE metatensor) +endif() + +if (TARGET metatensor::shared) + target_link_libraries(metatomic::shared INTERFACE metatensor::shared) +else() + target_link_libraries(metatomic::shared INTERFACE metatensor) +endif() + + +if (BUILD_SHARED_LIBS) + add_library(metatomic ALIAS metatomic::shared) +else() + add_library(metatomic ALIAS metatomic::static) +endif() + +#------------------------------------------------------------------------------# +# Installation configuration +#------------------------------------------------------------------------------# +include(CMakePackageConfigHelpers) +configure_package_config_file( + ${PROJECT_SOURCE_DIR}/cmake/metatomic-config.in.cmake + ${PROJECT_BINARY_DIR}/metatomic-config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/metatomic +) +write_basic_package_version_file( + metatomic-config-version.cmake + VERSION ${METATOMIC_FULL_VERSION} + COMPATIBILITY SameMinorVersion +) + +install(FILES "include/metatomic.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES "include/metatomic.hpp" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(DIRECTORY "include/metatomic" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/metatomic/version.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/metatomic) + +if (METATOMIC_INSTALL_BOTH_STATIC_SHARED OR BUILD_SHARED_LIBS) + if (WIN32) + # DLL files should go in /bin + install( + FILES ${METATOMIC_SHARED_LOCATION} + DESTINATION ${CMAKE_INSTALL_BINDIR} + PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_READ WORLD_EXECUTE + ) + # .lib files should go in /lib + install(FILES ${METATOMIC_IMPLIB_LOCATION} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + else() + install( + FILES ${METATOMIC_SHARED_LOCATION} + DESTINATION ${CMAKE_INSTALL_LIBDIR} + PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_READ WORLD_EXECUTE + ) + endif() +endif() + +if (METATOMIC_INSTALL_BOTH_STATIC_SHARED OR NOT BUILD_SHARED_LIBS) + install(FILES ${METATOMIC_STATIC_LOCATION} DESTINATION ${CMAKE_INSTALL_LIBDIR}) +endif() + +install(FILES + ${PROJECT_BINARY_DIR}/metatomic-config-version.cmake + ${PROJECT_BINARY_DIR}/metatomic-config.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/metatomic +) diff --git a/metatomic-core/Cargo.toml b/metatomic-core/Cargo.toml new file mode 100644 index 00000000..2a32c1c0 --- /dev/null +++ b/metatomic-core/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "metatomic-core" +version = "0.1.0" +edition = "2021" +publish = false +rust-version = "1.74" +exclude = [ + "tests" +] + +[lib] +crate-type = ["cdylib", "staticlib"] +name = "metatomic" +bench = false + +[dependencies] +once_cell = "1" + + +[build-dependencies] +cbindgen = { version = "0.29", default-features = false } + + +[dev-dependencies] +lazy_static = "1" +which = "8" diff --git a/metatomic-core/Clippy.toml b/metatomic-core/Clippy.toml new file mode 100644 index 00000000..49c5aa7b --- /dev/null +++ b/metatomic-core/Clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["DLPack", "ROCm", ".."] diff --git a/metatomic-core/build.rs b/metatomic-core/build.rs new file mode 100644 index 00000000..edec71e6 --- /dev/null +++ b/metatomic-core/build.rs @@ -0,0 +1,48 @@ +#![allow(clippy::field_reassign_with_default)] + +use std::path::PathBuf; + +fn main() { + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + let generated_comment = "\ +/* ============ Automatically generated file, DO NOT EDIT. ============== * + * * + * This file is automatically generated from the metatomic sources, * + * using cbindgen. If you want to change this file (including documentation), * + * make the corresponding changes in the rust sources and regenerate it. * + * ============================================================================= */"; + + let mut config: cbindgen::Config = Default::default(); + config.language = cbindgen::Language::C; + config.cpp_compat = true; + config.include_guard = Some("METATOMIC_H".into()); + config.include_version = false; + config.documentation = true; + config.documentation_style = cbindgen::DocumentationStyle::Doxy; + config.line_endings = cbindgen::LineEndingStyle::LF; + config.autogen_warning = Some(generated_comment.into()); + config.includes.push("metatomic/version.h".into()); + + let result = cbindgen::Builder::new() + .with_crate(crate_dir) + .with_config(config) + .generate() + .map(|data| { + let mut path = PathBuf::from("include"); + path.push("metatomic.h"); + data.write_to_file(&path); + }); + + // if not ok, rerun the build script unconditionally + if result.is_ok() { + println!("cargo:rerun-if-changed=src"); + println!("cargo:rerun-if-changed=build.rs"); + } + + if std::env::var("METATOMIC_FULL_VERSION").is_err() { + let version = std::env::var("CARGO_PKG_VERSION").expect("missing CARGO_PKG_VERSION"); + println!("cargo:rustc-env=METATOMIC_FULL_VERSION={}+rust", version); + } + println!("cargo:rerun-if-env-changed=METATOMIC_FULL_VERSION"); +} diff --git a/metatomic-core/cmake/dev-versions.cmake b/metatomic-core/cmake/dev-versions.cmake new file mode 100644 index 00000000..54329649 --- /dev/null +++ b/metatomic-core/cmake/dev-versions.cmake @@ -0,0 +1,91 @@ +# Parse a `_version_` number, and store its components in `_major_` `_minor_` +# `_patch_` and `_rc_` +function(parse_version _version_ _major_ _minor_ _patch_ _rc_) + string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)(-rc)?([0-9]+)?" _ "${_version_}") + + if(${CMAKE_MATCH_COUNT} EQUAL 3) + set(${_rc_} "" PARENT_SCOPE) + elseif(${CMAKE_MATCH_COUNT} EQUAL 5) + set(${_rc_} ${CMAKE_MATCH_5} PARENT_SCOPE) + else() + message(FATAL_ERROR "invalid version string ${_version_}") + endif() + + set(${_major_} ${CMAKE_MATCH_1} PARENT_SCOPE) + set(${_minor_} ${CMAKE_MATCH_2} PARENT_SCOPE) + set(${_patch_} ${CMAKE_MATCH_3} PARENT_SCOPE) +endfunction() + +# Get the time of the last modification since the last tag/release, and a hash +# of the latest commit/full state of a dirty repository +function(git_version_info _tag_prefix_ _output_n_commits_ _output_git_hash_) + set(_script_ "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../../scripts/git-version-info.py") + + if (EXISTS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/git_version_info") + # When building from a tarball, the script is executed and the result + # put in this file + file(STRINGS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/git_version_info" _file_content_) + list(GET _file_content_ 0 _n_commits_) + list(GET _file_content_ 1 _git_hash_) + + elseif (EXISTS "${_script_}") + # When building from a checkout, we'll need to run the script + find_package(Python COMPONENTS Interpreter REQUIRED) + execute_process( + COMMAND "${Python_EXECUTABLE}" "${_script_}" "${_tag_prefix_}" + RESULT_VARIABLE _status_ + OUTPUT_VARIABLE _stdout_ + ERROR_VARIABLE _stderr_ + WORKING_DIRECTORY ${CMAKE_CURRENT_FUNCTION_LIST_DIR} + ) + + if (NOT ${_status_} EQUAL 0) + message(WARNING + "git-version-info.py failed, version number might be wrong:\nstdout: ${_stdout_}\nstderr: ${_stderr_}") + set(${_output_} 0 PARENT_SCOPE) + return() + endif() + + if (NOT "${_stderr_}" STREQUAL "") + message(WARNING "git-version-info.py gave some errors, version number might be wrong:\nstdout: ${_stdout_}\nstderr: ${_stderr_}") + endif() + + string(REPLACE "\n" ";" _lines_ ${_stdout_}) + list(GET _lines_ 0 _n_commits_) + list(GET _lines_ 1 _git_hash_) + else() + message(FATAL_ERROR "could not update git version information") + endif() + + string(STRIP ${_n_commits_} _n_commits_) + set(${_output_n_commits_} ${_n_commits_} PARENT_SCOPE) + + string(STRIP ${_git_hash_} _git_hash_) + set(${_output_git_hash_} ${_git_hash_} PARENT_SCOPE) +endfunction() + + +# Take the version declared in the package, and increase the right number if we +# are actually installing a developement version from after the latest git tag +function(create_development_version _version_ _output_ _tag_prefix_) + git_version_info("${_tag_prefix_}" _n_commits_ _git_hash_) + + parse_version(${_version_} _major_ _minor_ _patch_ _rc_) + if(${_n_commits_} STREQUAL "0") + # we are building a release, leave the version number as-is + if("${_rc_}" STREQUAL "") + set(${_output_} "${_major_}.${_minor_}.${_patch_}" PARENT_SCOPE) + else() + set(${_output_} "${_major_}.${_minor_}.${_patch_}-rc${_rc_}" PARENT_SCOPE) + endif() + else() + # we are building a development version, increase the right part of the version + if("${_rc_}" STREQUAL "") + math(EXPR _minor_ "${_minor_} + 1") + set(${_output_} "${_major_}.${_minor_}.0-dev${_n_commits_}+${_git_hash_}" PARENT_SCOPE) + else() + math(EXPR _rc_ "${_rc_} + 1") + set(${_output_} "${_major_}.${_minor_}.${_patch_}-rc${_rc_}-dev${_n_commits_}+${_git_hash_}" PARENT_SCOPE) + endif() + endif() +endfunction() diff --git a/metatomic-core/cmake/metatomic-config.in.cmake b/metatomic-core/cmake/metatomic-config.in.cmake new file mode 100644 index 00000000..310f5436 --- /dev/null +++ b/metatomic-core/cmake/metatomic-config.in.cmake @@ -0,0 +1,91 @@ +@PACKAGE_INIT@ + +cmake_minimum_required(VERSION 3.22) + +include(CMakeFindDependencyMacro) +include(FindPackageHandleStandardArgs) + +if(metatomic_FOUND) + return() +endif() + +enable_language(CXX) + +# use the same version for metatensor-core as the main CMakeLists.txt +set(REQUIRED_METATENSOR_VERSION @REQUIRED_METATENSOR_VERSION@) +find_package(metatensor ${REQUIRED_METATENSOR_VERSION} CONFIG REQUIRED) + +get_filename_component(METATOMIC_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_RELATIVE_PATH@" ABSOLUTE) + +if (WIN32) + set(METATOMIC_SHARED_LOCATION ${METATOMIC_PREFIX_DIR}/@CMAKE_INSTALL_BINDIR@/@METATOMIC_SHARED_LIB_NAME@) + set(METATOMIC_IMPLIB_LOCATION ${METATOMIC_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@/@METATOMIC_IMPLIB_NAME@) +else() + set(METATOMIC_SHARED_LOCATION ${METATOMIC_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@/@METATOMIC_SHARED_LIB_NAME@) +endif() + +set(METATOMIC_STATIC_LOCATION ${METATOMIC_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@/@METATOMIC_STATIC_LIB_NAME@) +set(METATOMIC_INCLUDE ${METATOMIC_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@/) + +if (NOT EXISTS ${METATOMIC_INCLUDE}/metatomic.h OR NOT EXISTS ${METATOMIC_INCLUDE}/metatomic.hpp) + message(FATAL_ERROR "could not find metatomic headers in '${METATOMIC_INCLUDE}', please re-install metatomic") +endif() + + +# Shared library target +if (@METATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR @BUILD_SHARED_LIBS@) + if (NOT EXISTS ${METATOMIC_SHARED_LOCATION}) + message(FATAL_ERROR "could not find metatomic library at '${METATOMIC_SHARED_LOCATION}', please re-install metatomic") + endif() + + add_library(metatomic::shared SHARED IMPORTED) + set_target_properties(metatomic::shared PROPERTIES + IMPORTED_LOCATION ${METATOMIC_SHARED_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES ${METATOMIC_INCLUDE} + BUILD_VERSION "@METATOMIC_FULL_VERSION@" + ) + + target_compile_features(metatomic::shared INTERFACE cxx_std_17) + + if (WIN32) + if (NOT EXISTS ${METATOMIC_IMPLIB_LOCATION}) + message(FATAL_ERROR "could not find metatomic library at '${METATOMIC_IMPLIB_LOCATION}', please re-install metatomic") + endif() + + set_target_properties(metatomic::shared PROPERTIES + IMPORTED_IMPLIB ${METATOMIC_IMPLIB_LOCATION} + ) + endif() +endif() + + +# Static library target +if (@METATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR NOT @BUILD_SHARED_LIBS@) + if (NOT EXISTS ${METATOMIC_STATIC_LOCATION}) + message(FATAL_ERROR "could not find metatomic library at '${METATOMIC_STATIC_LOCATION}', please re-install metatomic") + endif() + + add_library(metatomic::static STATIC IMPORTED) + set_target_properties(metatomic::static PROPERTIES + IMPORTED_LOCATION ${METATOMIC_STATIC_LOCATION} + INTERFACE_INCLUDE_DIRECTORIES ${METATOMIC_INCLUDE} + INTERFACE_LINK_LIBRARIES "@CARGO_DEFAULT_LIBRARIES@" + BUILD_VERSION "@METATOMIC_FULL_VERSION@" + ) + + target_compile_features(metatomic::static INTERFACE cxx_std_17) +endif() + +# Export either the shared or static library as the metatomic target +if (@BUILD_SHARED_LIBS@) + add_library(metatomic ALIAS metatomic::shared) +else() + add_library(metatomic ALIAS metatomic::static) +endif() + + +if (@BUILD_SHARED_LIBS@) + find_package_handle_standard_args(metatomic DEFAULT_MSG METATOMIC_SHARED_LOCATION METATOMIC_INCLUDE) +else() + find_package_handle_standard_args(metatomic DEFAULT_MSG METATOMIC_STATIC_LOCATION METATOMIC_INCLUDE) +endif() diff --git a/metatomic-core/cmake/tempdir.cmake b/metatomic-core/cmake/tempdir.cmake new file mode 100644 index 00000000..52e4805f --- /dev/null +++ b/metatomic-core/cmake/tempdir.cmake @@ -0,0 +1,51 @@ +# Create a temporary directory using mktemp on *nix and powershell on windows +function(get_tempdir _outvar_) + # special case for github actions, where $TEMP might + # exist but point to nowhere/a non writable location + # https://docs.github.com/en/actions/learn-github-actions/variables + if (DEFINED ENV{RUNNER_TEMP}) + string(RANDOM LENGTH 12 _dirname_) + set(_output_ $ENV{RUNNER_TEMP}/${_dirname_}) + file(TO_NATIVE_PATH "${_output_}" _output_) + file(MAKE_DIRECTORY ${_output_}) + set(${_outvar_} ${_output_} PARENT_SCOPE) + return() + endif() + + find_program(MKTEMP_EXE NAMES mktemp) + if(MKTEMP_EXE) + execute_process( + COMMAND ${MKTEMP_EXE} -d + OUTPUT_VARIABLE _output_ + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _status_ + ) + + if(_status_ EQUAL 0) + file(MAKE_DIRECTORY ${_output_}) + set(${_outvar_} ${_output_} PARENT_SCOPE) + return() + endif() + endif() + + + find_program(POWERSHELL_EXE NAMES pwsh powershell) + if(POWERSHELL_EXE) + execute_process( + COMMAND ${POWERSHELL_EXE} -c "[System.IO.Path]::GetTempPath()" + OUTPUT_VARIABLE _output_ + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _status_ + ) + + if(_status_ EQUAL 0) + string(RANDOM LENGTH 12 _dirname_) + set(_output_ ${_output_}${_dirname_}) + file(MAKE_DIRECTORY ${_output_}) + set(${_outvar_} ${_output_} PARENT_SCOPE) + return() + endif() + endif() + + message(FATAL_ERROR "Could not find mktemp or PowerShell to make temporary directory") +endfunction() diff --git a/metatomic-core/include/metatomic.h b/metatomic-core/include/metatomic.h new file mode 100644 index 00000000..83b6cb1f --- /dev/null +++ b/metatomic-core/include/metatomic.h @@ -0,0 +1,32 @@ +#ifndef METATOMIC_H +#define METATOMIC_H + +/* ============ Automatically generated file, DO NOT EDIT. ============== * + * * + * This file is automatically generated from the metatomic sources, * + * using cbindgen. If you want to change this file (including documentation), * + * make the corresponding changes in the rust sources and regenerate it. * + * ============================================================================= */ + +#include +#include +#include +#include +#include "metatomic/version.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Get the runtime version of the metatomic library as a string. + * + * This version follows the `..[-]` format. + */ +const char *mta_version(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* METATOMIC_H */ diff --git a/metatomic-core/include/metatomic.hpp b/metatomic-core/include/metatomic.hpp new file mode 100644 index 00000000..016f26bc --- /dev/null +++ b/metatomic-core/include/metatomic.hpp @@ -0,0 +1,2 @@ +#include "metatomic/system.hpp" // IWYU pragma: export +#include "metatomic/model.hpp" // IWYU pragma: export diff --git a/metatomic-core/include/metatomic/model.hpp b/metatomic-core/include/metatomic/model.hpp new file mode 100644 index 00000000..1cae91bd --- /dev/null +++ b/metatomic-core/include/metatomic/model.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace metatomic { + +} // namespace metatomic diff --git a/metatomic-core/include/metatomic/system.hpp b/metatomic-core/include/metatomic/system.hpp new file mode 100644 index 00000000..1cae91bd --- /dev/null +++ b/metatomic-core/include/metatomic/system.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace metatomic { + +} // namespace metatomic diff --git a/metatomic-core/src/c_api/mod.rs b/metatomic-core/src/c_api/mod.rs new file mode 100644 index 00000000..33e0786d --- /dev/null +++ b/metatomic-core/src/c_api/mod.rs @@ -0,0 +1,18 @@ +use std::ffi::CString; +use std::os::raw::c_char; + +use once_cell::sync::Lazy; + + +static VERSION: Lazy = Lazy::new(|| { + CString::new(env!("METATOMIC_FULL_VERSION")).expect("version contains NULL byte") +}); + + +/// Get the runtime version of the metatomic library as a string. +/// +/// This version follows the `..[-]` format. +#[no_mangle] +pub extern "C" fn mta_version() -> *const c_char { + return VERSION.as_ptr(); +} diff --git a/metatomic-core/src/lib.rs b/metatomic-core/src/lib.rs new file mode 100644 index 00000000..bc47948b --- /dev/null +++ b/metatomic-core/src/lib.rs @@ -0,0 +1,13 @@ +#![warn(clippy::all, clippy::pedantic)] + +// disable some style lints +#![allow(clippy::needless_return, clippy::must_use_candidate, clippy::comparison_chain)] +#![allow(clippy::redundant_field_names, clippy::redundant_closure_for_method_calls, clippy::redundant_else)] +#![allow(clippy::unreadable_literal, clippy::option_if_let_else, clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc, clippy::missing_safety_doc)] +#![allow(clippy::similar_names, clippy::borrow_as_ptr, clippy::uninlined_format_args)] +#![allow(clippy::let_underscore_untyped, clippy::manual_let_else, clippy::empty_line_after_doc_comments)] + + +#[doc(hidden)] +mod c_api; diff --git a/metatomic-core/tests/CMakeLists.txt b/metatomic-core/tests/CMakeLists.txt new file mode 100644 index 00000000..1a96108d --- /dev/null +++ b/metatomic-core/tests/CMakeLists.txt @@ -0,0 +1,86 @@ +cmake_minimum_required(VERSION 3.22) +project(metatomic-tests) + +if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR}) + if("${CMAKE_BUILD_TYPE}" STREQUAL "" AND "${CMAKE_CONFIGURATION_TYPES}" STREQUAL "") + message(STATUS "Setting build type to 'release' as none was specified.") + set(CMAKE_BUILD_TYPE "release" + CACHE STRING + "Choose the type of build, options are: debug or release" + FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS release debug) + endif() +endif() + +if (MINGW) + # CI can't find libsdc++, so we statically link it + set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") +endif() + +add_subdirectory(../ metatomic) +get_target_property(METATOMIC_IMPORTED_LOCATION metatomic::shared IMPORTED_LOCATION) +get_filename_component(METATOMIC_DIR ${METATOMIC_IMPORTED_LOCATION} DIRECTORY) + +add_subdirectory(external) + +find_program(VALGRIND valgrind) +if (VALGRIND) + if (NOT "$ENV{METATOMIC_DISABLE_VALGRIND}" EQUAL "1") + message(STATUS "Running tests using valgrind") + set(TEST_COMMAND + "${VALGRIND}" "--tool=memcheck" "--dsymutil=yes" "--error-exitcode=125" + "--leak-check=full" "--show-leak-kinds=definite,indirect,possible" "--track-origins=yes" + "--gen-suppressions=all" + ) + endif() +else() + set(TEST_COMMAND "") +endif() + +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Weverything") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-c++98-compat") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-c++98-compat-pedantic") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-weak-vtables") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-float-equal") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-missing-prototypes") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-shadow-uncaptured-local") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-padded") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unsafe-buffer-usage") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-poison-system-directories") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-allocator-wrappers") +endif() + + +enable_testing() + +file(GLOB ALL_TESTS *.cpp) +foreach(_file_ ${ALL_TESTS}) + get_filename_component(_name_ ${_file_} NAME_WE) + add_executable(${_name_} ${_file_}) + target_link_libraries(${_name_} metatomic catch) + + set_target_properties(${_name_} PROPERTIES + # Ensure that the binaries find the right shared library. + # + # Without this, when configuring with cmake before the library is built, + # cmake does not find the library on the filesystem and does not add the + # RPATH to executables linking to it + BUILD_RPATH ${METATOMIC_DIR} + NO_SYSTEM_FROM_IMPORTED ON + ) + + add_test( + NAME ${_name_} + COMMAND ${TEST_COMMAND} $ + ) + + if(WIN32) + # We need to set the path to allow access to metatomic.dll + # this does a similar job to the BUILD_RPATH above + STRING(REPLACE ";" "\\;" PATH_STRING "$ENV{PATH}") + set_tests_properties(${_name_} PROPERTIES + ENVIRONMENT "PATH=${PATH_STRING}\;$" + ) + endif() +endforeach() diff --git a/metatomic-core/tests/check-cxx-install.rs b/metatomic-core/tests/check-cxx-install.rs new file mode 100644 index 00000000..d66f4883 --- /dev/null +++ b/metatomic-core/tests/check-cxx-install.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; +use std::sync::Mutex; + +mod utils; + +lazy_static::lazy_static! { + // Make sure only one of the tests below run at the time, since they both + // try to modify the same files + static ref LOCK: Mutex<()> = Mutex::new(()); +} + + +/// Check that metatomic can be built and installed with cmake, and that the +/// installed version can be used from another cmake project with `find_package` +#[test] +fn check_cxx_install() { + let _guard = match LOCK.lock() { + Ok(guard) => guard, + Err(_) => { + panic!("another test failed, stopping") + } + }; + + const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); + + // ====================================================================== // + // build and install metatensor with cmake + let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); + build_dir.push("cxx-install"); + build_dir.push("cmake-find-package"); + std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + + let deps_dir = build_dir.join("deps"); + let virtualenv_dir = deps_dir.join("virtualenv"); + std::fs::create_dir_all(&virtualenv_dir).expect("failed to create virtualenv dir"); + let python_exe = utils::create_python_venv(virtualenv_dir); + let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); + + let metatomic_dep = deps_dir.join("metatomic-core"); + let source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + let cmake_args = vec![ + format!("-DCMAKE_PREFIX_PATH={}", metatensor_cmake_prefix.display()), + ]; + let metatomic_cmake_prefix = utils::setup_metatomic_cmake(&source_dir, &metatomic_dep, cmake_args); + + // ====================================================================== // + // try to use the installed metatomic from cmake + let mut tests_source_dir = source_dir; + tests_source_dir.extend(["tests", "cmake-project"]); + + // configure cmake for the test cmake project + let mut cmake_config = utils::cmake_config(&tests_source_dir, &build_dir); + cmake_config.arg(format!("-DCMAKE_PREFIX_PATH={};{}", metatensor_cmake_prefix.display(), metatomic_cmake_prefix.display())); + utils::run_command(cmake_config, "cmake configuration"); + + // build the code, linking to metatensor + let cmake_build = utils::cmake_build(&build_dir); + utils::run_command(cmake_build, "cmake build"); + + // run the executables + let ctest = utils::ctest(&build_dir); + utils::run_command(ctest, "ctest"); +} diff --git a/metatomic-core/tests/cmake-project/CMakeLists.txt b/metatomic-core/tests/cmake-project/CMakeLists.txt new file mode 100644 index 00000000..2b04acfa --- /dev/null +++ b/metatomic-core/tests/cmake-project/CMakeLists.txt @@ -0,0 +1,84 @@ +cmake_minimum_required(VERSION 3.22) + +message(STATUS "Running with CMake version ${CMAKE_VERSION}") + +project(metatomic-test-cmake-project C CXX) + +option(USE_CMAKE_SUBDIRECTORY OFF) + +if (MINGW) + # CI can't find libsdc++, so we statically link it + set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") +endif() + + +if (USE_CMAKE_SUBDIRECTORY) + message(STATUS "Using metatomic with add_subdirectory") + # build metatomic as part of this project + add_subdirectory(../../ metatomic) + + # load metatomic from the build path + set(CMAKE_BUILD_RPATH "$") +else() + message(STATUS "Using metatomic with find_package") + # If building a dev version, we also need to update the REQUIRED_METATOMIC_VERSION + # in the same way we update the metatomic-torch version + include(../../cmake/dev-versions.cmake) + set(REQUIRED_METATOMIC_VERSION "0.1.0") + create_development_version("${REQUIRED_METATOMIC_VERSION}" METATOMIC_CORE_FULL_VERSION "metatomic-core-v") + string(REGEX REPLACE "([0-9]*)\\.([0-9]*).*" "\\1.\\2" REQUIRED_METATOMIC_VERSION ${METATOMIC_CORE_FULL_VERSION}) + + find_package(metatomic ${REQUIRED_METATOMIC_VERSION} REQUIRED) + + if(TARGET metatomic::shared) + get_target_property(mta_build_version metatomic::shared BUILD_VERSION) + if (NOT ${mta_build_version} STREQUAL ${METATOMIC_CORE_FULL_VERSION}) + message(FATAL_ERROR "Invalid BUILD_VERSION for metatomic::shared, expected ${METATOMIC_CORE_FULL_VERSION} but got ${mta_build_version}") + endif() + endif() + + if(TARGET metatomic::static) + get_target_property(mta_build_version metatomic::static BUILD_VERSION) + if (NOT ${mta_build_version} STREQUAL ${METATOMIC_CORE_FULL_VERSION}) + message(FATAL_ERROR "Invalid BUILD_VERSION for metatomic::static, expected ${METATOMIC_CORE_FULL_VERSION} but got ${mta_build_version}") + endif() + endif() +endif() + +enable_testing() + + +if(TARGET metatomic::shared) + add_executable(c-main src/main.c) + target_link_libraries(c-main metatomic::shared) + + add_executable(cxx-main src/main.cpp) + target_link_libraries(cxx-main metatomic::shared) + + add_test(NAME c-main COMMAND c-main) + add_test(NAME cxx-main COMMAND cxx-main) + + if(WIN32) + # We need to set the path to allow access to metatomic.dll + STRING(REPLACE ";" "\\;" PATH_STRING "$ENV{PATH}") + set_tests_properties(c-main PROPERTIES + ENVIRONMENT "PATH=${PATH_STRING}\;$" + ) + + set_tests_properties(cxx-main PROPERTIES + ENVIRONMENT "PATH=${PATH_STRING}\;$" + ) + endif() +endif() + + +if(TARGET metatomic::static) + add_executable(c-main-static src/main.c) + target_link_libraries(c-main-static metatomic::static) + + add_executable(cxx-main-static src/main.cpp) + target_link_libraries(cxx-main-static metatomic::static) + + add_test(NAME c-main-static COMMAND c-main-static) + add_test(NAME cxx-main-static COMMAND cxx-main-static) +endif() diff --git a/metatomic-core/tests/cmake-project/README.md b/metatomic-core/tests/cmake-project/README.md new file mode 100644 index 00000000..70a687bf --- /dev/null +++ b/metatomic-core/tests/cmake-project/README.md @@ -0,0 +1,3 @@ +# Sample CMake project using metatomic + +This is a basic cmake project linking to metatomic from C and C++ code. diff --git a/metatomic-core/tests/cmake-project/src/main.c b/metatomic-core/tests/cmake-project/src/main.c new file mode 100644 index 00000000..dcad0f76 --- /dev/null +++ b/metatomic-core/tests/cmake-project/src/main.c @@ -0,0 +1,8 @@ +#include + +#include + +int main(void) { + printf("Metatomic version: %s\n", mta_version()); + return 0; +} diff --git a/metatomic-core/tests/cmake-project/src/main.cpp b/metatomic-core/tests/cmake-project/src/main.cpp new file mode 100644 index 00000000..04ec152b --- /dev/null +++ b/metatomic-core/tests/cmake-project/src/main.cpp @@ -0,0 +1,9 @@ +#include + +#include + + +int main() { + std::cout << "Metatomic version: " << mta_version() << std::endl; + return 0; +} diff --git a/metatomic-torch/tests/external/.gitattributes b/metatomic-core/tests/external/.gitattributes similarity index 100% rename from metatomic-torch/tests/external/.gitattributes rename to metatomic-core/tests/external/.gitattributes diff --git a/metatomic-torch/tests/external/CMakeLists.txt b/metatomic-core/tests/external/CMakeLists.txt similarity index 100% rename from metatomic-torch/tests/external/CMakeLists.txt rename to metatomic-core/tests/external/CMakeLists.txt diff --git a/metatomic-torch/tests/external/catch/catch.cpp b/metatomic-core/tests/external/catch/catch.cpp similarity index 100% rename from metatomic-torch/tests/external/catch/catch.cpp rename to metatomic-core/tests/external/catch/catch.cpp diff --git a/metatomic-torch/tests/external/catch/catch.hpp b/metatomic-core/tests/external/catch/catch.hpp similarity index 100% rename from metatomic-torch/tests/external/catch/catch.hpp rename to metatomic-core/tests/external/catch/catch.hpp diff --git a/metatomic-core/tests/misc.cpp b/metatomic-core/tests/misc.cpp new file mode 100644 index 00000000..bf0ce275 --- /dev/null +++ b/metatomic-core/tests/misc.cpp @@ -0,0 +1,15 @@ +#include + +#include "metatomic.h" + + +TEST_CASE("Version macros") { + CHECK(std::string(METATOMIC_VERSION) == mta_version()); + + auto version = std::to_string(METATOMIC_VERSION_MAJOR) + "." + + std::to_string(METATOMIC_VERSION_MINOR) + "." + + std::to_string(METATOMIC_VERSION_PATCH); + + // METATOMIC_VERSION should start with `x.y.z` + CHECK(std::string(METATOMIC_VERSION).find(version) == 0); +} diff --git a/metatomic-core/tests/run-cxx-tests.rs b/metatomic-core/tests/run-cxx-tests.rs new file mode 100644 index 00000000..0d3b48d9 --- /dev/null +++ b/metatomic-core/tests/run-cxx-tests.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +mod utils; + +#[test] +fn run_cxx_tests() { + const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); + + let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); + build_dir.push("cxx-tests"); + std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + + // ====================================================================== // + // setup dependencies for the torch tests + let deps_dir = build_dir.join("deps"); + let virtualenv_dir = deps_dir.join("virtualenv"); + std::fs::create_dir_all(&virtualenv_dir).expect("failed to create virtualenv dir"); + let python_exe = utils::create_python_venv(virtualenv_dir); + let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); + + // ====================================================================== // + // build the metatomic C++ tests and run them + + let mut source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + source_dir.push("tests"); + + // configure cmake for the tests + let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); + cmake_config.arg("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"); + cmake_config.arg(format!("-DCMAKE_PREFIX_PATH={}", metatensor_cmake_prefix.display())); + utils::run_command(cmake_config, "cmake configuration"); + + // build the tests + let cmake_build = utils::cmake_build(&build_dir); + utils::run_command(cmake_build, "cmake build"); + + // run the tests + let ctest = utils::ctest(&build_dir); + utils::run_command(ctest, "ctest"); +} diff --git a/metatomic-core/tests/utils/mod.rs b/metatomic-core/tests/utils/mod.rs new file mode 100644 index 00000000..e12e6897 --- /dev/null +++ b/metatomic-core/tests/utils/mod.rs @@ -0,0 +1,470 @@ +#![allow(dead_code)] +#![allow(clippy::needless_return)] + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +fn build_type() -> &'static str { + // assume that debug assertion means that we are building the code in + // debug mode, even if that could be not true in some cases + if cfg!(debug_assertions) { + "debug" + } else { + "release" + } +} + +fn append_flags(existing: Option, extra: &str) -> String { + match existing { + Some(flags) if !flags.trim().is_empty() => format!("{flags} {extra}"), + _ => extra.into(), + } +} + +pub fn cmake_config(source_dir: &Path, build_dir: &Path) -> Command { + let cmake = which::which("cmake").expect("could not find cmake"); + + let mut cmake_config = Command::new(cmake); + cmake_config.current_dir(build_dir); + cmake_config.arg(source_dir); + cmake_config.arg("--no-warn-unused-cli"); + cmake_config.arg(format!("-DCMAKE_BUILD_TYPE={}", build_type())); + + // the cargo executable currently running + let cargo_exe = std::env::var("CARGO").expect("CARGO env var is not set"); + cmake_config.arg(format!("-DCARGO_EXE={}", cargo_exe)); + + if std::env::var_os("CARGO_LLVM_COV").is_some() { + let coverage_compile_flags = "-fprofile-instr-generate -fcoverage-mapping"; + let coverage_link_flags = "-fprofile-instr-generate"; + + let c_flags = append_flags(std::env::var("CFLAGS").ok(), coverage_compile_flags); + let cxx_flags = append_flags(std::env::var("CXXFLAGS").ok(), coverage_compile_flags); + let exe_linker_flags = + append_flags(std::env::var("LDFLAGS").ok(), coverage_link_flags); + + cmake_config.arg(format!("-DCMAKE_C_FLAGS={c_flags}")); + cmake_config.arg(format!("-DCMAKE_CXX_FLAGS={cxx_flags}")); + cmake_config.arg(format!("-DCMAKE_EXE_LINKER_FLAGS={exe_linker_flags}")); + cmake_config.arg(format!("-DCMAKE_SHARED_LINKER_FLAGS={exe_linker_flags}")); + } + + return cmake_config; +} + +pub fn cmake_build(build_dir: &Path) -> Command { + let cmake = which::which("cmake").expect("could not find cmake"); + + let mut cmake_build = Command::new(cmake); + cmake_build.current_dir(build_dir); + cmake_build.arg("--build"); + cmake_build.arg("."); + cmake_build.arg("--parallel"); + cmake_build.arg("--config"); + cmake_build.arg(build_type()); + + return cmake_build; +} + + +pub fn ctest(build_dir: &Path) -> Command { + let ctest = which::which("ctest").expect("could not find ctest"); + + let mut ctest = Command::new(ctest); + ctest.current_dir(build_dir); + ctest.arg("--output-on-failure"); + ctest.arg("--build-config"); + ctest.arg(build_type()); + + return ctest +} + +/// Find the path to the uv binary, or None if not present +fn find_uv() -> Option { + which::which("uv").ok() +} + +/// Find the path to the `python`or `python3` binary on the user system +fn find_python() -> PathBuf { + if let Ok(python) = which::which("python") { + let output = Command::new(&python) + .arg("-c") + .arg("import sys; print(sys.version_info.major)") + .output() + .expect("could not run python"); + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + + if stdout.trim() == "3" { + // we found Python 3 + return python; + } + } + } + + // try python3 + let python = which::which("python3").expect("failed to run `which python3`"); + let output = Command::new(&python) + .arg("-c") + .arg("import sys; print(sys.version_info.major)") + .output() + .expect("could not run python"); + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + if stdout.trim() == "3" { + // we found Python 3 + return python; + } + } + + panic!("could not find Python 3") +} + +/// Helper: get python executable path inside a venv +fn python_in_venv(venv_dir: &Path) -> PathBuf { + let mut python = venv_dir.to_path_buf(); + if cfg!(target_os = "windows") { + python.extend(["Scripts", "python.exe"]); + } else { + python.extend(["bin", "python"]); + } + python +} + +/// Create a fresh Python virtualenv using uv if available, else fallback to +/// `python -m venv`, and return the path to the python executable in the venv +pub fn create_python_venv(build_dir: PathBuf) -> PathBuf { + if let Some(uv_bin) = find_uv() { + let mut cmd = Command::new(&uv_bin); + cmd.arg("venv"); + cmd.arg("--clear"); + cmd.arg(&build_dir); + + run_command(cmd, "uv venv creation"); + } else { + let mut cmd = Command::new(find_python()); + cmd.arg("-m"); + cmd.arg("venv"); + cmd.arg(&build_dir); + + run_command(cmd, "python to create virtualenv with `venv`"); + + // update pip in case the system uses a very old one + let python = python_in_venv(&build_dir); + let mut cmd = Command::new(&python); + cmd.arg("-m"); + cmd.arg("pip"); + cmd.arg("install"); + cmd.arg("--upgrade"); + cmd.arg("pip"); + + run_command(cmd, "pip upgrade in virtualenv"); + } + + python_in_venv(&build_dir) +} + +#[derive(Default)] +pub struct PipInstallOptions { + pub upgrade: bool, + pub no_deps: bool, + pub no_build_isolation: bool, +} + +/// Install a package with pip (uses uv if present, else falls back to python) +fn pip_install( + python: &Path, + packages: &[&str], + options: PipInstallOptions, +) { + if let Some(uv_bin) = find_uv() { + let mut cmd = Command::new(&uv_bin); + cmd.arg("pip").arg("install").arg("--python").arg(python); + + // follow the same behavior as pip when there are multiple indexes + cmd.arg("--index-strategy"); + cmd.arg("unsafe-best-match"); + + if options.upgrade { + cmd.arg("--upgrade"); + } + if options.no_deps { + cmd.arg("--no-deps"); + } + if options.no_build_isolation { + cmd.arg("--no-build-isolation"); + // uv doesn't support --check-build-dependencies + } + + for package in packages { + cmd.arg(package); + } + + run_command(cmd, "uv pip install"); + } else { + let mut cmd = Command::new(python); + cmd.arg("-m").arg("pip").arg("install"); + if options.upgrade { + cmd.arg("--upgrade"); + } + if options.no_deps { + cmd.arg("--no-deps"); + } + if options.no_build_isolation { + // If pip, add both supported options + cmd.arg("--no-build-isolation"); + cmd.arg("--check-build-dependencies"); + } + + for package in packages { + cmd.arg(package); + } + + run_command(cmd, "pip install"); + } +} + +/// Download PyTorch in a Python virtualenv, and return the +/// CMAKE_PREFIX_PATH for the corresponding libtorch +pub fn setup_torch_pip(python: &Path) -> PathBuf { + let torch_version = std::env::var("METATOMIC_TESTS_TORCH_VERSION").unwrap_or("2.12".into()); + pip_install( + python, + &[&format!("torch=={}.*", torch_version)], + PipInstallOptions { upgrade: true, no_deps: false, no_build_isolation: false } + ); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import torch; print(torch.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get torch cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +/// Install metatensor in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatensor. +pub fn setup_metatensor_pip(python: &Path) -> PathBuf { + pip_install(python, &["metatensor-core >=0.2.0,<0.3"], PipInstallOptions::default()); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import metatensor; print(metatensor.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get metatensor cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'metatensor.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +/// Install metatensor-torch in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatensor_torch. +pub fn setup_metatensor_torch_pip(python: &Path) -> PathBuf { + pip_install(python, &["metatensor-torch >=0.9.0,<0.10"], PipInstallOptions::default()); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import metatensor.torch; print(metatensor.torch.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get metatensor_torch cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'metatensor.torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +/// Build metatomic-torch located in `source_dir` inside `build_dir`, and return +/// the installation prefix. +pub fn setup_metatomic_torch_cmake(source_dir: &Path, build_dir: &Path, cmake_args: Vec) -> PathBuf { + std::fs::create_dir_all(build_dir).expect("failed to create metatomic build dir"); + + // configure cmake for metatomic-torch + let mut cmake_config = cmake_config(source_dir, build_dir); + + let install_prefix = build_dir.join("usr"); + cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix.display())); + + // Add any additional cmake arguments + for arg in cmake_args { + cmake_config.arg(arg); + } + + run_command(cmake_config, "cmake configuration for metatomic_torch"); + + // build and install metatomic-torch + let mut cmake_build = cmake_build(build_dir); + cmake_build.arg("--target"); + cmake_build.arg("install"); + + run_command(cmake_build, "cmake build for metatomic_torch"); + + install_prefix +} + +/// Build metatomic-core located in `source_dir` inside `build_dir`, and return +/// the installation prefix +pub fn setup_metatomic_cmake(source_dir: &Path, build_dir: &Path, cmake_args: Vec) -> PathBuf { + std::fs::create_dir_all(build_dir).expect("failed to create metatomic build dir"); + + // configure cmake for metatomic + let mut cmake_config = cmake_config(source_dir, build_dir); + + let install_prefix = build_dir.join("usr"); + cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix.display())); + + // Add any additional cmake arguments + for arg in cmake_args { + cmake_config.arg(arg); + } + + run_command(cmake_config, "cmake configuration for metatomic"); + + // build and install metatomic + let mut cmake_build = cmake_build(build_dir); + cmake_build.arg("--target"); + cmake_build.arg("install"); + + run_command(cmake_build, "cmake build for metatomic"); + + install_prefix +} + +/// Install metatomic-core in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatomic. +pub fn setup_metatomic_core_pip(python: &Path, source_dir: &Path) -> PathBuf { + pip_install( + python, + &["cmake", "packaging >=26", "setuptools >=77"], + PipInstallOptions::default() + ); + + pip_install( + python, + &[&source_dir.display().to_string()], + PipInstallOptions { + upgrade: true, + no_deps: true, + no_build_isolation: true + } + ); + + // let mut cmd = Command::new(python); + // cmd.arg("-c"); + // cmd.arg("import metatomic; print(metatomic.utils.cmake_prefix_path)"); + + // let output = run_command(cmd, "python to get metatomic cmake prefix"); + + // let stdout = String::from_utf8_lossy(&output.stdout); + // let prefix = PathBuf::from(stdout.trim()); + // if !prefix.exists() { + // panic!("'metatomic.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + // } + + // return prefix; + return PathBuf::new(); +} + + +/// Install metatomic-torch in a Python virtualenv with pip, and return the +/// CMAKE_PREFIX_PATH for the installed libmetatomic_torch. +pub fn setup_metatomic_torch_pip(python: &Path, source_dir: &Path) -> PathBuf { + pip_install( + python, + &[&source_dir.display().to_string()], + PipInstallOptions { + upgrade: true, + no_deps: true, + no_build_isolation: true + } + ); + + let mut cmd = Command::new(python); + cmd.arg("-c"); + cmd.arg("import metatomic.torch; print(metatomic.torch.utils.cmake_prefix_path)"); + + let output = run_command(cmd, "python to get metatomic_torch cmake prefix"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let prefix = PathBuf::from(stdout.trim()); + if !prefix.exists() { + panic!("'metatomic.torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); + } + + return prefix; +} + +pub fn run_command(mut command: Command, context: &str) -> std::process::Output { + write!(std::io::stdout().lock(), "\n\n[Running] {:?}\n\n", command).unwrap(); + + let mut child = command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn().unwrap_or_else(|_| panic!("failed to spawn {}", context)); + + let mut child_stdout = child.stdout.take().expect("missing stdout"); + let mut child_stderr = child.stderr.take().expect("missing stderr"); + + let out_handle = std::thread::spawn(move || -> std::io::Result> { + let mut buf = [0u8; 8192]; + let mut captured = Vec::new(); + let mut sink = std::io::stdout().lock(); + loop { + let n = child_stdout.read(&mut buf)?; + if n == 0 { + break; + } + sink.write_all(&buf[..n])?; + sink.flush()?; + captured.extend_from_slice(&buf[..n]); + } + Ok(captured) + }); + + let err_handle = std::thread::spawn(move || -> std::io::Result> { + let mut buf = [0u8; 8192]; + let mut captured = Vec::new(); + let mut sink = std::io::stderr().lock(); + loop { + let n = child_stderr.read(&mut buf)?; + if n == 0 { + break; + } + sink.write_all(&buf[..n])?; + sink.flush()?; + captured.extend_from_slice(&buf[..n]); + } + Ok(captured) + }); + + let status = child.wait().unwrap_or_else(|_| panic!("failed to run {}", context)); + let stdout = String::from_utf8_lossy(&out_handle.join().unwrap().unwrap()).into_owned(); + let stderr = String::from_utf8_lossy(&err_handle.join().unwrap().unwrap()).into_owned(); + + if !status.success() { + panic!( + "{} failed, status: {}\nstderr:\n\n{}\nstdout:\n\n{}\n", + context, status, stderr, stdout + ); + } + + return std::process::Output { status, stdout: stdout.into_bytes(), stderr: stderr.into_bytes() }; +} diff --git a/metatomic-torch/tests/CMakeLists.txt b/metatomic-torch/tests/CMakeLists.txt index 8a64a4f3..7d6257a0 100644 --- a/metatomic-torch/tests/CMakeLists.txt +++ b/metatomic-torch/tests/CMakeLists.txt @@ -1,4 +1,5 @@ -add_subdirectory(external) +# re-use catch from metatomic-core C++ tests +add_subdirectory(../../metatomic-core/tests/external external) # make sure we compile catch with the flags that torch requires. In particular, # torch sets -D_GLIBCXX_USE_CXX11_ABI=0 on Linux, which changes some of the diff --git a/metatomic-torch/tests/check-torch-install.rs b/metatomic-torch/tests/check-torch-install.rs index 8883d916..ad8cfb60 100644 --- a/metatomic-torch/tests/check-torch-install.rs +++ b/metatomic-torch/tests/check-torch-install.rs @@ -123,8 +123,11 @@ fn check_python_install() { let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python_exe); - let python_source_dir = cargo_manifest_dir.parent().unwrap().join("python").join("metatomic_torch"); - let metatomic_torch_cmake_prefix = utils::setup_metatomic_torch_pip(&python_exe, &python_source_dir); + let mta_core_source_dir = cargo_manifest_dir.parent().unwrap().join("python").join("metatomic_core"); + let metatomic_core_cmake_prefix = utils::setup_metatomic_core_pip(&python_exe, &mta_core_source_dir); + + let mta_torch_source_dir = cargo_manifest_dir.parent().unwrap().join("python").join("metatomic_torch"); + let metatomic_torch_cmake_prefix = utils::setup_metatomic_torch_pip(&python_exe, &mta_torch_source_dir); // ====================================================================== // // try to use the installed metatensor-torch from cmake @@ -134,10 +137,11 @@ fn check_python_install() { // configure cmake for the test cmake project let mut cmake_config = utils::cmake_config(&source_dir, &build_dir); cmake_config.arg(format!( - "-DCMAKE_PREFIX_PATH={};{};{};{}", + "-DCMAKE_PREFIX_PATH={};{};{};{};{}", pytorch_cmake_prefix.display(), metatensor_cmake_prefix.display(), metatensor_torch_cmake_prefix.display(), + metatomic_core_cmake_prefix.display(), metatomic_torch_cmake_prefix.display(), )); diff --git a/metatomic-torch/tests/utils/mod.rs b/metatomic-torch/tests/utils/mod.rs deleted file mode 100644 index e223bcee..00000000 --- a/metatomic-torch/tests/utils/mod.rs +++ /dev/null @@ -1,410 +0,0 @@ -#![allow(dead_code)] -#![allow(clippy::needless_return)] - -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -fn build_type() -> &'static str { - // assume that debug assertion means that we are building the code in - // debug mode, even if that could be not true in some cases - if cfg!(debug_assertions) { - "debug" - } else { - "release" - } -} - -fn append_flags(existing: Option, extra: &str) -> String { - match existing { - Some(flags) if !flags.trim().is_empty() => format!("{flags} {extra}"), - _ => extra.into(), - } -} - -pub fn cmake_config(source_dir: &Path, build_dir: &Path) -> Command { - let cmake = which::which("cmake").expect("could not find cmake"); - - let mut cmake_config = Command::new(cmake); - cmake_config.current_dir(build_dir); - cmake_config.arg(source_dir); - cmake_config.arg("--no-warn-unused-cli"); - cmake_config.arg(format!("-DCMAKE_BUILD_TYPE={}", build_type())); - - // the cargo executable currently running - let cargo_exe = std::env::var("CARGO").expect("CARGO env var is not set"); - cmake_config.arg(format!("-DCARGO_EXE={}", cargo_exe)); - - if std::env::var_os("CARGO_LLVM_COV").is_some() { - let coverage_compile_flags = "-fprofile-instr-generate -fcoverage-mapping"; - let coverage_link_flags = "-fprofile-instr-generate"; - - let c_flags = append_flags(std::env::var("CFLAGS").ok(), coverage_compile_flags); - let cxx_flags = append_flags(std::env::var("CXXFLAGS").ok(), coverage_compile_flags); - let exe_linker_flags = - append_flags(std::env::var("LDFLAGS").ok(), coverage_link_flags); - - cmake_config.arg(format!("-DCMAKE_C_FLAGS={c_flags}")); - cmake_config.arg(format!("-DCMAKE_CXX_FLAGS={cxx_flags}")); - cmake_config.arg(format!("-DCMAKE_EXE_LINKER_FLAGS={exe_linker_flags}")); - cmake_config.arg(format!("-DCMAKE_SHARED_LINKER_FLAGS={exe_linker_flags}")); - } - - return cmake_config; -} - -pub fn cmake_build(build_dir: &Path) -> Command { - let cmake = which::which("cmake").expect("could not find cmake"); - - let mut cmake_build = Command::new(cmake); - cmake_build.current_dir(build_dir); - cmake_build.arg("--build"); - cmake_build.arg("."); - cmake_build.arg("--parallel"); - cmake_build.arg("--config"); - cmake_build.arg(build_type()); - - return cmake_build; -} - - -pub fn ctest(build_dir: &Path) -> Command { - let ctest = which::which("ctest").expect("could not find ctest"); - - let mut ctest = Command::new(ctest); - ctest.current_dir(build_dir); - ctest.arg("--output-on-failure"); - ctest.arg("--build-config"); - ctest.arg(build_type()); - - return ctest -} - -/// Find the path to the uv binary, or None if not present -fn find_uv() -> Option { - which::which("uv").ok() -} - -/// Find the path to the `python`or `python3` binary on the user system -fn find_python() -> PathBuf { - if let Ok(python) = which::which("python") { - let output = Command::new(&python) - .arg("-c") - .arg("import sys; print(sys.version_info.major)") - .output() - .expect("could not run python"); - - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - - if stdout.trim() == "3" { - // we found Python 3 - return python; - } - } - } - - // try python3 - let python = which::which("python3").expect("failed to run `which python3`"); - let output = Command::new(&python) - .arg("-c") - .arg("import sys; print(sys.version_info.major)") - .output() - .expect("could not run python"); - - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - if stdout.trim() == "3" { - // we found Python 3 - return python; - } - } - - panic!("could not find Python 3") -} - -/// Helper: get python executable path inside a venv -fn python_in_venv(venv_dir: &Path) -> PathBuf { - let mut python = venv_dir.to_path_buf(); - if cfg!(target_os = "windows") { - python.extend(["Scripts", "python.exe"]); - } else { - python.extend(["bin", "python"]); - } - python -} - -/// Create a fresh Python virtualenv using uv if available, else fallback to -/// `python -m venv`, and return the path to the python executable in the venv -pub fn create_python_venv(build_dir: PathBuf) -> PathBuf { - if let Some(uv_bin) = find_uv() { - let mut cmd = Command::new(&uv_bin); - cmd.arg("venv"); - cmd.arg("--clear"); - cmd.arg(&build_dir); - - run_command(cmd, "uv venv creation"); - } else { - let mut cmd = Command::new(find_python()); - cmd.arg("-m"); - cmd.arg("venv"); - cmd.arg(&build_dir); - - run_command(cmd, "python to create virtualenv with `venv`"); - - // update pip in case the system uses a very old one - let python = python_in_venv(&build_dir); - let mut cmd = Command::new(&python); - cmd.arg("-m"); - cmd.arg("pip"); - cmd.arg("install"); - cmd.arg("--upgrade"); - cmd.arg("pip"); - - run_command(cmd, "pip upgrade in virtualenv"); - } - - python_in_venv(&build_dir) -} - -#[derive(Default)] -pub struct PipInstallOptions { - pub upgrade: bool, - pub no_deps: bool, - pub no_build_isolation: bool, -} - -/// Install a package with pip (uses uv if present, else falls back to python) -fn pip_install( - python: &Path, - packages: &[&str], - options: PipInstallOptions, -) { - if let Some(uv_bin) = find_uv() { - let mut cmd = Command::new(&uv_bin); - cmd.arg("pip").arg("install").arg("--python").arg(python); - - // follow the same behavior as pip when there are multiple indexes - cmd.arg("--index-strategy"); - cmd.arg("unsafe-best-match"); - - if options.upgrade { - cmd.arg("--upgrade"); - } - if options.no_deps { - cmd.arg("--no-deps"); - } - if options.no_build_isolation { - cmd.arg("--no-build-isolation"); - // uv doesn't support --check-build-dependencies - } - - for package in packages { - cmd.arg(package); - } - - run_command(cmd, "uv pip install"); - } else { - let mut cmd = Command::new(python); - cmd.arg("-m").arg("pip").arg("install"); - if options.upgrade { - cmd.arg("--upgrade"); - } - if options.no_deps { - cmd.arg("--no-deps"); - } - if options.no_build_isolation { - // If pip, add both supported options - cmd.arg("--no-build-isolation"); - cmd.arg("--check-build-dependencies"); - } - - for package in packages { - cmd.arg(package); - } - - run_command(cmd, "pip install"); - } -} - -/// Download PyTorch in a Python virtualenv, and return the -/// CMAKE_PREFIX_PATH for the corresponding libtorch -pub fn setup_torch_pip(python: &Path) -> PathBuf { - let torch_version = std::env::var("METATOMIC_TESTS_TORCH_VERSION").unwrap_or("2.12".into()); - pip_install( - python, - &[&format!("torch=={}.*", torch_version)], - PipInstallOptions { upgrade: true, no_deps: false, no_build_isolation: false } - ); - - let mut cmd = Command::new(python); - cmd.arg("-c"); - cmd.arg("import torch; print(torch.utils.cmake_prefix_path)"); - - let output = run_command(cmd, "python to get torch cmake prefix"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let prefix = PathBuf::from(stdout.trim()); - if !prefix.exists() { - panic!("'torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); - } - - return prefix; -} - -/// Install metatensor in a Python virtualenv with pip, and return the -/// CMAKE_PREFIX_PATH for the installed libmetatensor. -pub fn setup_metatensor_pip(python: &Path) -> PathBuf { - pip_install(python, &["metatensor-core >=0.2.0,<0.3"], PipInstallOptions::default()); - - let mut cmd = Command::new(python); - cmd.arg("-c"); - cmd.arg("import metatensor; print(metatensor.utils.cmake_prefix_path)"); - - let output = run_command(cmd, "python to get metatensor cmake prefix"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let prefix = PathBuf::from(stdout.trim()); - if !prefix.exists() { - panic!("'metatensor.utils.cmake_prefix' at '{}' does not exist", prefix.display()); - } - - return prefix; -} - -/// Install metatensor-torch in a Python virtualenv with pip, and return the -/// CMAKE_PREFIX_PATH for the installed libmetatensor_torch. -pub fn setup_metatensor_torch_pip(python: &Path) -> PathBuf { - pip_install(python, &["metatensor-torch >=0.9.0,<0.10"], PipInstallOptions::default()); - - let mut cmd = Command::new(python); - cmd.arg("-c"); - cmd.arg("import metatensor.torch; print(metatensor.torch.utils.cmake_prefix_path)"); - - let output = run_command(cmd, "python to get metatensor_torch cmake prefix"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let prefix = PathBuf::from(stdout.trim()); - if !prefix.exists() { - panic!("'metatensor.torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); - } - - return prefix; -} - -/// Build metatomic-torch located in `source_dir` inside `build_dir`, and return -/// the installation prefix. -pub fn setup_metatomic_torch_cmake(source_dir: &Path, build_dir: &Path, cmake_args: Vec) -> PathBuf { - std::fs::create_dir_all(build_dir).expect("failed to create metatomic build dir"); - - // configure cmake for metatomic-torch - let mut cmake_config = cmake_config(source_dir, build_dir); - - let install_prefix = build_dir.join("usr"); - cmake_config.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix.display())); - - // Add any additional cmake arguments - for arg in cmake_args { - cmake_config.arg(arg); - } - - run_command(cmake_config, "cmake configuration for metatomic_torch"); - - // build and install metatomic-torch - let mut cmake_build = cmake_build(build_dir); - cmake_build.arg("--target"); - cmake_build.arg("install"); - - run_command(cmake_build, "cmake build for metatomic_torch"); - - install_prefix -} - - -/// Install metatomic-torch in a Python virtualenv with pip, and return the -/// CMAKE_PREFIX_PATH for the installed libmetatomic_torch. -pub fn setup_metatomic_torch_pip(python: &Path, source_dir: &Path) -> PathBuf { - pip_install(python, &["setuptools>=77", "packaging>=23", "cmake"], PipInstallOptions::default()); - - pip_install( - python, - &[&source_dir.display().to_string()], - PipInstallOptions { - upgrade: true, - no_deps: false, - no_build_isolation: true - } - ); - - let mut cmd = Command::new(python); - cmd.arg("-c"); - cmd.arg("import metatomic.torch; print(metatomic.torch.utils.cmake_prefix_path)"); - - let output = run_command(cmd, "python to get metatomic_torch cmake prefix"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let prefix = PathBuf::from(stdout.trim()); - if !prefix.exists() { - panic!("'metatomic.torch.utils.cmake_prefix' at '{}' does not exist", prefix.display()); - } - - return prefix; -} - - -pub fn run_command(mut command: Command, context: &str) -> std::process::Output { - write!(std::io::stdout().lock(), "\n\n[Running] {:?}\n\n", command).unwrap(); - - let mut child = command - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn().unwrap_or_else(|_| panic!("failed to spawn {}", context)); - - let mut child_stdout = child.stdout.take().expect("missing stdout"); - let mut child_stderr = child.stderr.take().expect("missing stderr"); - - let out_handle = std::thread::spawn(move || -> std::io::Result> { - let mut buf = [0u8; 8192]; - let mut captured = Vec::new(); - let mut sink = std::io::stdout().lock(); - loop { - let n = child_stdout.read(&mut buf)?; - if n == 0 { - break; - } - sink.write_all(&buf[..n])?; - sink.flush()?; - captured.extend_from_slice(&buf[..n]); - } - Ok(captured) - }); - - let err_handle = std::thread::spawn(move || -> std::io::Result> { - let mut buf = [0u8; 8192]; - let mut captured = Vec::new(); - let mut sink = std::io::stderr().lock(); - loop { - let n = child_stderr.read(&mut buf)?; - if n == 0 { - break; - } - sink.write_all(&buf[..n])?; - sink.flush()?; - captured.extend_from_slice(&buf[..n]); - } - Ok(captured) - }); - - let status = child.wait().unwrap_or_else(|_| panic!("failed to run {}", context)); - let stdout = String::from_utf8_lossy(&out_handle.join().unwrap().unwrap()).into_owned(); - let stderr = String::from_utf8_lossy(&err_handle.join().unwrap().unwrap()).into_owned(); - - if !status.success() { - panic!( - "{} failed, status: {}\nstderr:\n\n{}\nstdout:\n\n{}\n", - context, status, stderr, stdout - ); - } - - return std::process::Output { status, stdout: stdout.into_bytes(), stderr: stderr.into_bytes() }; -} diff --git a/metatomic-torch/tests/utils/mod.rs b/metatomic-torch/tests/utils/mod.rs new file mode 120000 index 00000000..20b8b009 --- /dev/null +++ b/metatomic-torch/tests/utils/mod.rs @@ -0,0 +1 @@ +../../../metatomic-core/tests/utils/mod.rs \ No newline at end of file diff --git a/python/metatomic_torch/build-backend/backend.py b/python/metatomic_torch/build-backend/backend.py index c762d91e..be0389a2 100644 --- a/python/metatomic_torch/build-backend/backend.py +++ b/python/metatomic_torch/build-backend/backend.py @@ -1,11 +1,24 @@ # This is a custom Python build backend wrapping setuptool's to only depend on # torch/metatensor-torch when building the wheel and not the sdist import os +import pathlib from setuptools import build_meta -ROOT = os.path.realpath(os.path.dirname(__file__)) +ROOT = pathlib.Path(__file__).parent.resolve() + +METATOMIC_CORE = (ROOT / ".." / ".." / "metatomic_core").resolve() +METATOMIC_NO_LOCAL_DEPS = os.environ.get("METATOMIC_NO_LOCAL_DEPS", "0") == "1" + + +if not METATOMIC_NO_LOCAL_DEPS and METATOMIC_CORE.exists(): + # we are building from a git checkout + METATOMIC_CORE_DEP = f"metatomic-core @ {METATOMIC_CORE.as_uri()}" +else: + # we are building from a sdist + METATOMIC_CORE_DEP = "metatomic-core >=0.1.0,<0.2" + FORCED_TORCH_VERSION = os.environ.get("METATOMIC_TORCH_BUILD_WITH_TORCH_VERSION") if FORCED_TORCH_VERSION is not None: @@ -27,7 +40,7 @@ # Special dependencies to build the wheels def get_requires_for_build_wheel(config_settings=None): defaults = build_meta.get_requires_for_build_wheel(config_settings) - return defaults + [TORCH_DEP] + return defaults + [TORCH_DEP, METATOMIC_CORE_DEP] def build_editable(wheel_directory, config_settings=None, metadata_directory=None): From fb694424d357662777947fd476d77f028398d867 Mon Sep 17 00:00:00 2001 From: Guillaume Fraux Date: Wed, 27 May 2026 16:25:27 +0200 Subject: [PATCH 05/10] Draft the C API for metatomic-core --- .github/workflows/rust-tests.yml | 186 ++++++++++++++ .github/workflows/torch-tests.yml | 9 +- docs/Doxyfile | 4 +- docs/src/core/CHANGELOG.md | 1 + docs/src/core/index.rst | 17 ++ docs/src/core/reference/c/index.rst | 17 ++ docs/src/core/reference/c/misc.rst | 56 +++++ docs/src/core/reference/c/model.rst | 16 ++ docs/src/core/reference/c/plugin.rst | 16 ++ docs/src/core/reference/c/system.rst | 42 ++++ docs/src/index.rst | 1 + metatomic-core/Cargo.toml | 11 + metatomic-core/build.rs | 1 + .../cmake/metatomic-config.in.cmake | 2 + metatomic-core/include/metatomic.h | 237 ++++++++++++++++++ metatomic-core/include/metatomic.hpp | 4 +- metatomic-core/include/metatomic/plugin.hpp | 7 + metatomic-core/include/metatomic/utils.hpp | 7 + metatomic-core/src/c_api/mod.rs | 25 +- metatomic-core/src/c_api/model.rs | 76 ++++++ metatomic-core/src/c_api/plugin.rs | 41 +++ metatomic-core/src/c_api/status.rs | 42 ++++ metatomic-core/src/c_api/system.rs | 131 ++++++++++ metatomic-core/src/c_api/utils.rs | 102 ++++++++ metatomic-core/src/lib.rs | 43 +++- metatomic-core/src/metadata.rs | 132 ++++++++++ metatomic-core/src/model.rs | 20 ++ metatomic-core/src/plugin.rs | 37 +++ metatomic-core/src/system.rs | 53 ++++ metatomic-core/src/units.rs | 7 + metatomic-core/tests/check-cxx-install.rs | 8 +- metatomic-torch/tests/check-torch-install.rs | 31 ++- rustfmt.toml | 1 + scripts/check-c-api-docs.py | 101 ++++++++ scripts/include/README | 4 + scripts/include/metatensor.h | 8 + scripts/include/metatomic/version.h | 0 scripts/include/stdarg.h | 0 scripts/include/stdbool.h | 1 + scripts/include/stddef.h | 6 + scripts/include/stdint.h | 7 + scripts/include/stdlib.h | 1 + 42 files changed, 1475 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/rust-tests.yml create mode 120000 docs/src/core/CHANGELOG.md create mode 100644 docs/src/core/index.rst create mode 100644 docs/src/core/reference/c/index.rst create mode 100644 docs/src/core/reference/c/misc.rst create mode 100644 docs/src/core/reference/c/model.rst create mode 100644 docs/src/core/reference/c/plugin.rst create mode 100644 docs/src/core/reference/c/system.rst create mode 100644 metatomic-core/include/metatomic/plugin.hpp create mode 100644 metatomic-core/include/metatomic/utils.hpp create mode 100644 metatomic-core/src/c_api/model.rs create mode 100644 metatomic-core/src/c_api/plugin.rs create mode 100644 metatomic-core/src/c_api/status.rs create mode 100644 metatomic-core/src/c_api/system.rs create mode 100644 metatomic-core/src/c_api/utils.rs create mode 100644 metatomic-core/src/metadata.rs create mode 100644 metatomic-core/src/model.rs create mode 100644 metatomic-core/src/plugin.rs create mode 100644 metatomic-core/src/system.rs create mode 100644 metatomic-core/src/units.rs create mode 100644 rustfmt.toml create mode 100755 scripts/check-c-api-docs.py create mode 100644 scripts/include/README create mode 100644 scripts/include/metatensor.h create mode 100644 scripts/include/metatomic/version.h create mode 100644 scripts/include/stdarg.h create mode 100644 scripts/include/stdbool.h create mode 100644 scripts/include/stddef.h create mode 100644 scripts/include/stdint.h create mode 100644 scripts/include/stdlib.h diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml new file mode 100644 index 00000000..77ac7838 --- /dev/null +++ b/.github/workflows/rust-tests.yml @@ -0,0 +1,186 @@ +name: Rust tests + +on: + push: + branches: [main] + pull_request: + # Check all PR + +concurrency: + group: rust-tests-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + rust-tests: + name: ${{ matrix.os }} / Rust ${{ matrix.rust-version }}${{ matrix.extra-name }} + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + defaults: + run: + shell: "bash" + env: + CMAKE_CXX_COMPILER: ${{ matrix.cxx }} + CMAKE_C_COMPILER: ${{ matrix.cc }} + CMAKE_GENERATOR: ${{ matrix.cmake-generator }} + strategy: + matrix: + include: + - os: ubuntu-24.04 + rust-version: stable + rust-target: x86_64-unknown-linux-gnu + cxx: g++ + cc: gcc + cmake-generator: Unix Makefiles + + # check the build on a stock Ubuntu 22.04, which uses cmake 3.22, and + # with our minimal supported rust version + - os: ubuntu-24.04 + rust-version: 1.74 + container: ubuntu:22.04 + rust-target: x86_64-unknown-linux-gnu + extra-name: ", cmake 3.22" + cxx: g++ + cc: gcc + cmake-generator: Unix Makefiles + + - os: macos-15 + rust-version: stable + rust-target: aarch64-apple-darwin + extra-name: "" + cxx: clang++ + cc: clang + cmake-generator: Unix Makefiles + + # - os: windows-2022 + # rust-version: stable + # rust-target: x86_64-pc-windows-msvc + # extra-name: " / MSVC" + # cxx: cl.exe + # cc: cl.exe + # cmake-generator: Visual Studio 17 2022 + + # - os: windows-2022 + # rust-version: stable + # rust-target: x86_64-pc-windows-gnu + # extra-name: " / MinGW" + # cxx: g++.exe + # cc: gcc.exe + # cmake-generator: MinGW Makefiles + steps: + - name: install dependencies in container + if: matrix.container == 'ubuntu:22.04' + run: | + apt update + apt install -y software-properties-common + apt install -y cmake make gcc g++ git curl python3-venv + + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Configure git safe directory + if: matrix.container == 'ubuntu:22.04' + run: git config --global --add safe.directory /__w/metatomic/metatomic + + - name: setup rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust-version }} + target: ${{ matrix.rust-target }} + + - name: setup Python + uses: actions/setup-python@v6 + if: matrix.container == null + with: + # Python 3.14.5 fails with "No module named pip.__main__; 'pip' is a + # package and cannot be directly executed" when using a venv, so we + # use 3.14.4 for now + python-version: "3.14.4" + + - name: Cache Rust dependencies + uses: Leafwing-Studios/cargo-cache@v2.6.1 + with: + sweep-cache: true + + - name: install valgrind + if: matrix.do-valgrind + run: | + sudo apt-get install -y valgrind + + - name: Setup sccache + if: ${{ !env.ACT }} + uses: mozilla-actions/sccache-action@v0.0.10 + with: + version: "v0.15.0" + + - name: Setup sccache environnement variables + if: ${{ !env.ACT }} + run: | + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + + - name: run tests + run: | + cargo test --package metatomic-core --target ${{ matrix.rust-target }} + env: + RUST_BACKTRACE: full + + - name: check that the header was already up to date + run: | + git diff --exit-code + + # check that the C API declarations are correctly documented and used + prevent-bitrot: + runs-on: ubuntu-24.04 + name: check C API declarations + steps: + - uses: actions/checkout@v6 + + - name: setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: install python dependencies + run: | + pip install pycparser + + - name: check that C API functions are all documented + run: | + python scripts/check-c-api-docs.py + + # make sure no debug print stays in the code + check-debug-prints: + runs-on: ubuntu-24.04 + name: check leftover debug print + + steps: + - uses: actions/checkout@v6 + + - name: install ripgrep + run: | + wget https://github.com/BurntSushi/ripgrep/releases/download/13.0.0/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz + tar xf ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz + echo "$(pwd)/ripgrep-13.0.0-x86_64-unknown-linux-musl" >> $GITHUB_PATH + + - name: check for leftover dbg! + run: | + # use ripgrep (rg) to check for instances of `dbg!` in rust files. + # rg will return 1 if it fails to find a match, so we invert it again + # with the `!` builtin to get the error/success in CI + + ! rg "dbg!" --type=rust --quiet + + - name: check for leftover \#include + run: | + ! rg "" --iglob "\!metatomic-core/tests/cpp/external/catch/catch.hpp" --quiet + + - name: check for leftover std::cout + run: | + ! rg "cout" --iglob "\!metatomic-core/tests/cpp/external/catch/catch.hpp" --quiet + + - name: check for leftover std::cerr + run: | + ! rg "cerr" --iglob "\!metatomic-core/tests/cpp/external/catch/catch.hpp" --quiet diff --git a/.github/workflows/torch-tests.yml b/.github/workflows/torch-tests.yml index ce378170..b088f750 100644 --- a/.github/workflows/torch-tests.yml +++ b/.github/workflows/torch-tests.yml @@ -20,7 +20,10 @@ jobs: include: - os: ubuntu-24.04 torch-version: "2.12" - python-version: "3.14" + # Python 3.14.5 fails with "No module named pip.__main__; 'pip' is a + # package and cannot be directly executed" when using a venv, so we + # use 3.14.4 for now + python-version: "3.14.4" cargo-test-flags: --release do-valgrind: true @@ -33,12 +36,12 @@ jobs: - os: macos-15 torch-version: "2.12" - python-version: "3.14" + python-version: "3.14.4" cargo-test-flags: --release - os: windows-2022 torch-version: "2.12" - python-version: "3.14" + python-version: "3.14.4" cargo-test-flags: --release steps: - name: install dependencies in container diff --git a/docs/Doxyfile b/docs/Doxyfile index f48f15ed..5cf71fe6 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -991,7 +991,9 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = ../metatomic-torch/include/metatomic \ +INPUT = ../metatomic-core/include/ \ + ../metatomic-core/include/metatomic \ + ../metatomic-torch/include/metatomic \ ../metatomic-torch/include/metatomic/torch # This tag can be used to specify the character encoding of the source files diff --git a/docs/src/core/CHANGELOG.md b/docs/src/core/CHANGELOG.md new file mode 120000 index 00000000..a344bc46 --- /dev/null +++ b/docs/src/core/CHANGELOG.md @@ -0,0 +1 @@ +../../../metatomic-core/CHANGELOG.md \ No newline at end of file diff --git a/docs/src/core/index.rst b/docs/src/core/index.rst new file mode 100644 index 00000000..60512b35 --- /dev/null +++ b/docs/src/core/index.rst @@ -0,0 +1,17 @@ +Core Classes +============ + +WIP + + +.. toctree:: + :maxdepth: 2 + + reference/c/index + + +.. toctree:: + :maxdepth: 1 + :hidden: + + CHANGELOG.md diff --git a/docs/src/core/reference/c/index.rst b/docs/src/core/reference/c/index.rst new file mode 100644 index 00000000..f190a5e7 --- /dev/null +++ b/docs/src/core/reference/c/index.rst @@ -0,0 +1,17 @@ +.. _c-api-core: + +C API reference +=============== + +WIP + +The functions and types provided in ``metatomic.h`` can be grouped in four +main groups: + +.. toctree:: + :maxdepth: 1 + + system + model + plugin + misc diff --git a/docs/src/core/reference/c/misc.rst b/docs/src/core/reference/c/misc.rst new file mode 100644 index 00000000..6aec886b --- /dev/null +++ b/docs/src/core/reference/c/misc.rst @@ -0,0 +1,56 @@ +Miscellaneous +============= + +Version number +^^^^^^^^^^^^^^ + +.. doxygenfunction:: mta_version + +.. c:macro:: METATOMIC_VERSION + + Macro containing the compile-time version of metatomic, as a string + +.. c:macro:: METATOMIC_VERSION_MAJOR + + Macro containing the compile-time **major** version number of metatomic, as + an integer + +.. c:macro:: METATOMIC_VERSION_MINOR + + Macro containing the compile-time **minor** version number of metatomic, as + an integer + +.. c:macro:: METATOMIC_VERSION_PATCH + + Macro containing the compile-time **patch** version number of metatomic, as + an integer + + +Error handling +^^^^^^^^^^^^^^ + +.. doxygenfunction:: mta_last_error + +.. doxygenfunction:: mta_set_last_error + +.. doxygenenum:: mta_status_t + + +String manipulation +^^^^^^^^^^^^^^^^^^^ + +.. doxygentypedef:: mta_string_t + +.. doxygenfunction:: mta_string_create + +.. doxygenfunction:: mta_string_free + +.. doxygenfunction:: mta_string_view + +.. doxygenfunction:: mta_format_metadata + + +Unit conversion +^^^^^^^^^^^^^^^ + +.. doxygenfunction:: mta_unit_conversion_factor diff --git a/docs/src/core/reference/c/model.rst b/docs/src/core/reference/c/model.rst new file mode 100644 index 00000000..6a3d9ee3 --- /dev/null +++ b/docs/src/core/reference/c/model.rst @@ -0,0 +1,16 @@ +Model +===== + +.. doxygenstruct:: mta_model_t + :members: + +The following functions operate on :c:type:`mta_model_t`: + +- :c:func:`mta_load_model`: TODO summary +- :c:func:`mta_execute_model`: TODO summary + +-------------------------------------------------------------------------------- + +.. doxygenfunction:: mta_load_model + +.. doxygenfunction:: mta_execute_model diff --git a/docs/src/core/reference/c/plugin.rst b/docs/src/core/reference/c/plugin.rst new file mode 100644 index 00000000..952650f4 --- /dev/null +++ b/docs/src/core/reference/c/plugin.rst @@ -0,0 +1,16 @@ +Plugin system +============= + +.. doxygenstruct:: mta_plugin_t + :members: + +The following functions operate on :c:type:`mta_plugin_t`: + +- :c:func:`mta_register_plugin`: TODO summary +- :c:func:`mta_load_plugin`: TODO summary + +-------------------------------------------------------------------------------- + +.. doxygenfunction:: mta_register_plugin + +.. doxygenfunction:: mta_load_plugin diff --git a/docs/src/core/reference/c/system.rst b/docs/src/core/reference/c/system.rst new file mode 100644 index 00000000..15524525 --- /dev/null +++ b/docs/src/core/reference/c/system.rst @@ -0,0 +1,42 @@ +System +====== + +.. doxygentypedef:: mta_system_t + +The following functions operate on :c:type:`mta_system_t`: + +- :c:func:`mta_system_create`: TODO summary +- :c:func:`mta_system_free`: TODO summary +- :c:func:`mta_system_size`: TODO summary +- :c:func:`mta_system_get_data`: TODO summary +- :c:func:`mta_system_get_length_unit`: TODO summary +- :c:func:`mta_system_add_pairs`: TODO summary +- :c:func:`mta_system_get_pairs`: TODO summary +- :c:func:`mta_system_known_pairs`: TODO summary +- :c:func:`mta_system_add_custom_data`: TODO summary +- :c:func:`mta_system_get_custom_data`: TODO summary +- :c:func:`mta_system_known_custom_data`: TODO summary + +-------------------------------------------------------------------------------- + +.. doxygenfunction:: mta_system_create + +.. doxygenfunction:: mta_system_free + +.. doxygenfunction:: mta_system_size + +.. doxygenfunction:: mta_system_get_data + +.. doxygenfunction:: mta_system_get_length_unit + +.. doxygenfunction:: mta_system_add_pairs + +.. doxygenfunction:: mta_system_get_pairs + +.. doxygenfunction:: mta_system_known_pairs + +.. doxygenfunction:: mta_system_add_custom_data + +.. doxygenfunction:: mta_system_get_custom_data + +.. doxygenfunction:: mta_system_known_custom_data diff --git a/docs/src/index.rst b/docs/src/index.rst index 170c25c1..441356c2 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -92,6 +92,7 @@ existing trained models, look into the metatrain_ project instead. overview installation + core/index torch/index quantities/index engines/index diff --git a/metatomic-core/Cargo.toml b/metatomic-core/Cargo.toml index 2a32c1c0..2335505a 100644 --- a/metatomic-core/Cargo.toml +++ b/metatomic-core/Cargo.toml @@ -14,12 +14,23 @@ name = "metatomic" bench = false [dependencies] +metatensor = { version = "0.3.0" } once_cell = "1" +dlpk = "0.3" +json = "0.12" [build-dependencies] cbindgen = { version = "0.29", default-features = false } +# the last versions that supports Rust 1.74 +serde_spanned = "=1.0.1" +toml = "=0.9.6" +toml_datetime = "=0.7.1" +toml_parser = "=1.0.2" +toml_writer = "=1.0.2" +tempfile = "=3.24.0" +indexmap = "=2.11.4" [dev-dependencies] lazy_static = "1" diff --git a/metatomic-core/build.rs b/metatomic-core/build.rs index edec71e6..b92cc292 100644 --- a/metatomic-core/build.rs +++ b/metatomic-core/build.rs @@ -22,6 +22,7 @@ fn main() { config.documentation_style = cbindgen::DocumentationStyle::Doxy; config.line_endings = cbindgen::LineEndingStyle::LF; config.autogen_warning = Some(generated_comment.into()); + config.includes.push("metatensor.h".into()); config.includes.push("metatomic/version.h".into()); let result = cbindgen::Builder::new() diff --git a/metatomic-core/cmake/metatomic-config.in.cmake b/metatomic-core/cmake/metatomic-config.in.cmake index 310f5436..90fca167 100644 --- a/metatomic-core/cmake/metatomic-config.in.cmake +++ b/metatomic-core/cmake/metatomic-config.in.cmake @@ -46,6 +46,7 @@ if (@METATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR @BUILD_SHARED_LIBS@) ) target_compile_features(metatomic::shared INTERFACE cxx_std_17) + target_link_libraries(metatomic::shared INTERFACE metatensor) if (WIN32) if (NOT EXISTS ${METATOMIC_IMPLIB_LOCATION}) @@ -74,6 +75,7 @@ if (@METATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR NOT @BUILD_SHARED_LIBS@) ) target_compile_features(metatomic::static INTERFACE cxx_std_17) + target_link_libraries(metatomic::static INTERFACE metatensor) endif() # Export either the shared or static library as the metatomic target diff --git a/metatomic-core/include/metatomic.h b/metatomic-core/include/metatomic.h index 83b6cb1f..1e69263e 100644 --- a/metatomic-core/include/metatomic.h +++ b/metatomic-core/include/metatomic.h @@ -12,12 +12,118 @@ #include #include #include +#include "metatensor.h" #include "metatomic/version.h" +/** + * TODO + */ +#define MTA_ABI_VERSION 1 + +typedef enum mta_status_t { + MTA_SUCCESS = 0, + MTA_ERROR_OTHER = 255, +} mta_status_t; + +/** + * TODO + */ +typedef enum mta_system_data_kind { + MTA_SYSTEM_DATA_TYPES = 0, + MTA_SYSTEM_DATA_POSITIONS = 1, + MTA_SYSTEM_DATA_CELL = 2, + MTA_SYSTEM_DATA_PBC = 3, +} mta_system_data_kind; + +/** + * TODO + */ +typedef struct mta_opaque_string_t mta_opaque_string_t; + +/** + * TODO + */ +typedef struct mta_system_t mta_system_t; + +/** + * TODO + */ +typedef struct mta_opaque_string_t *mta_string_t; + +/** + * TODO + */ +typedef struct mta_model_t { + /** + * TODO + */ + void *data; + /** + * TODO + */ + enum mta_status_t (*unload)(void *model_data); + /** + * TODO + */ + enum mta_status_t (*metadata)(const void *model_data, mta_string_t *metadata_json); + /** + * TODO + */ + enum mta_status_t (*supported_outputs)(const void *model_data, mta_string_t *outputs_json); + /** + * TODO + */ + enum mta_status_t (*requested_pair_lists)(const void *model_data, mta_string_t *pair_options_json); + /** + * TODO + */ + enum mta_status_t (*requested_inputs)(const void *model_data, mta_string_t *inputs_json); + /** + * TODO + */ + enum mta_status_t (*execute_inner)(void *model_data, + const struct mta_system_t *const *systems, + uintptr_t systems_count, + const mts_labels_t *selected_atoms, + const char *const *requested_outputs_json, + uintptr_t requested_outputs_count, + mts_tensormap_t **outputs, + uintptr_t outputs_count); +} mta_model_t; + +/** + * TODO + */ +typedef struct mta_plugin_t { + /** + * TODO + */ + const char *name; + /** + * TODO + */ + enum mta_status_t (*load_model)(const char *load_from, + const char *options_json, + struct mta_model_t *model); +} mta_plugin_t; + #ifdef __cplusplus extern "C" { #endif // __cplusplus +/** + * TODO + */ +enum mta_status_t mta_last_error(const char **message, const char **origin, void **data); + +/** + * TODO + */ +enum mta_status_t mta_set_last_error(const char *message, + const char *origin, + void *data, + void (*data_deleter)(void*)); + /** * Get the runtime version of the metatomic library as a string. * @@ -25,6 +131,137 @@ extern "C" { */ const char *mta_version(void); +/** + * TODO + */ +mta_string_t mta_string_create(const char *raw); + +/** + * TODO + */ +void mta_string_free(mta_string_t string); + +/** + * TODO + */ +const char *mta_string_view(mta_string_t string); + +/** + * TODO + */ +enum mta_status_t mta_unit_conversion_factor(const char *from_unit, + const char *to_unit, + double *conversion); + +/** + * TODO + */ +enum mta_status_t mta_system_create(const char *length_unit, + DLManagedTensorVersioned *types, + DLManagedTensorVersioned *positions, + DLManagedTensorVersioned *cell, + DLManagedTensorVersioned *pbc, + struct mta_system_t **system); + +/** + * TODO + */ +enum mta_status_t mta_system_free(struct mta_system_t *system); + +/** + * TODO + */ +enum mta_status_t mta_system_size(const struct mta_system_t *system, uintptr_t *size); + +/** + * TODO + */ +enum mta_status_t mta_system_get_data(const struct mta_system_t *system, + enum mta_system_data_kind request, + DLManagedTensorVersioned **data); + +/** + * TODO + */ +enum mta_status_t mta_system_get_length_unit(const struct mta_system_t *system, + mta_string_t *length_unit); + +/** + * TODO + */ +enum mta_status_t mta_system_add_pairs(struct mta_system_t *system, + const char *options, + mts_block_t *pairs); + +/** + * TODO + */ +enum mta_status_t mta_system_get_pairs(const struct mta_system_t *system, + const char *options, + const mts_block_t **pairs); + +/** + * TODO + */ +enum mta_status_t mta_system_known_pairs(const struct mta_system_t *system, + mta_string_t *pairs_options); + +/** + * TODO + */ +enum mta_status_t mta_system_add_custom_data(struct mta_system_t *system, + const char *name, + mts_tensormap_t *data); + +/** + * TODO + */ +enum mta_status_t mta_system_get_custom_data(const struct mta_system_t *system, + const char *name, + const mts_tensormap_t **data); + +/** + * TODO + */ +enum mta_status_t mta_system_known_custom_data(const struct mta_system_t *system, + mta_string_t *names); + +/** + * TODO + */ +enum mta_status_t mta_execute_model(struct mta_model_t model, + const struct mta_system_t *const *systems, + uintptr_t systems_count, + const mts_labels_t *selected_atoms, + const char *const *requested_outputs_json, + uintptr_t requested_outputs_count, + bool check_consistency, + mts_tensormap_t **outputs, + uintptr_t outputs_count); + +/** + * TODO + */ +enum mta_status_t mta_format_metadata(const char *metadata, mta_string_t *printed); + +/** + * TODO + */ +void mta_register_plugin(struct mta_plugin_t plugin); + +/** + * TODO + */ +enum mta_status_t mta_load_plugin(const char *path); + +/** + * TODO + */ +enum mta_status_t mta_load_model(const char *plugin_name, + const char *load_from, + const char *options_json, + struct mta_model_t *model); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/metatomic-core/include/metatomic.hpp b/metatomic-core/include/metatomic.hpp index 016f26bc..3b5c8ac2 100644 --- a/metatomic-core/include/metatomic.hpp +++ b/metatomic-core/include/metatomic.hpp @@ -1,2 +1,4 @@ +#include "metatomic/utils.hpp" // IWYU pragma: export #include "metatomic/system.hpp" // IWYU pragma: export -#include "metatomic/model.hpp" // IWYU pragma: export +#include "metatomic/model.hpp" // IWYU pragma: export +#include "metatomic/plugin.hpp" // IWYU pragma: export diff --git a/metatomic-core/include/metatomic/plugin.hpp b/metatomic-core/include/metatomic/plugin.hpp new file mode 100644 index 00000000..1cae91bd --- /dev/null +++ b/metatomic-core/include/metatomic/plugin.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace metatomic { + +} // namespace metatomic diff --git a/metatomic-core/include/metatomic/utils.hpp b/metatomic-core/include/metatomic/utils.hpp new file mode 100644 index 00000000..1cae91bd --- /dev/null +++ b/metatomic-core/include/metatomic/utils.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace metatomic { + +} // namespace metatomic diff --git a/metatomic-core/src/c_api/mod.rs b/metatomic-core/src/c_api/mod.rs index 33e0786d..cf6c6176 100644 --- a/metatomic-core/src/c_api/mod.rs +++ b/metatomic-core/src/c_api/mod.rs @@ -1,18 +1,15 @@ -use std::ffi::CString; -use std::os::raw::c_char; +mod status; +pub use self::status::mta_status_t; -use once_cell::sync::Lazy; +mod utils; +pub use self::utils::mta_string_t; +pub use self::utils::{mta_string_create, mta_string_free, mta_string_view}; +mod system; +pub use self::system::mta_system_t; -static VERSION: Lazy = Lazy::new(|| { - CString::new(env!("METATOMIC_FULL_VERSION")).expect("version contains NULL byte") -}); +mod model; +pub use self::model::mta_model_t; - -/// Get the runtime version of the metatomic library as a string. -/// -/// This version follows the `..[-]` format. -#[no_mangle] -pub extern "C" fn mta_version() -> *const c_char { - return VERSION.as_ptr(); -} +mod plugin; +pub use self::plugin::{mta_plugin_t, mta_register_plugin, mta_load_model}; diff --git a/metatomic-core/src/c_api/model.rs b/metatomic-core/src/c_api/model.rs new file mode 100644 index 00000000..b586f73c --- /dev/null +++ b/metatomic-core/src/c_api/model.rs @@ -0,0 +1,76 @@ +use std::ffi::{c_void, c_char}; +use metatensor::c_api::{mts_labels_t, mts_tensormap_t}; + +use super::{mta_status_t, mta_string_t, mta_system_t}; + +/// TODO +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct mta_model_t { + /// TODO + pub data: *mut c_void, + + /// TODO + pub unload: Option mta_status_t>, + + /// TODO + pub metadata: Option mta_status_t>, + + /// TODO + pub supported_outputs: Option mta_status_t>, + + /// TODO + pub requested_pair_lists: Option mta_status_t>, + + /// TODO + pub requested_inputs: Option mta_status_t>, + + /// TODO + pub execute_inner: Option mta_status_t>, +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_execute_model( + model: mta_model_t, + systems: *const *const mta_system_t, + systems_count: usize, + selected_atoms: *const mts_labels_t, + requested_outputs_json: *const *const c_char, + requested_outputs_count: usize, + check_consistency: bool, + outputs: *mut *mut mts_tensormap_t, + outputs_count: usize, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_format_metadata( + metadata: *const c_char, + printed: *mut mta_string_t, +) -> mta_status_t { + todo!() +} diff --git a/metatomic-core/src/c_api/plugin.rs b/metatomic-core/src/c_api/plugin.rs new file mode 100644 index 00000000..6dbfc4ad --- /dev/null +++ b/metatomic-core/src/c_api/plugin.rs @@ -0,0 +1,41 @@ +use std::ffi::c_char; + +use super::{mta_model_t, mta_status_t}; + +/// TODO +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct mta_plugin_t { + /// TODO + pub name: *const c_char, + + /// TODO + pub load_model: Option mta_status_t>, +} + +/// TODO +#[no_mangle] +pub extern "C" fn mta_register_plugin(plugin: mta_plugin_t) { + todo!() +} + +/// TODO +#[no_mangle] +pub extern "C" fn mta_load_plugin(path: *const c_char) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub extern "C" fn mta_load_model( + plugin_name: *const c_char, + load_from: *const c_char, + options_json: *const c_char, + model: *mut mta_model_t, +) -> mta_status_t { + todo!() +} diff --git a/metatomic-core/src/c_api/status.rs b/metatomic-core/src/c_api/status.rs new file mode 100644 index 00000000..0c48707c --- /dev/null +++ b/metatomic-core/src/c_api/status.rs @@ -0,0 +1,42 @@ +use std::ffi::{c_char, c_void}; + +use crate::Error; + + +// TODO +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(PartialEq, Eq, Debug)] +pub enum mta_status_t { + MTA_SUCCESS = 0, + // ... + MTA_ERROR_OTHER = 255, +} + + +impl From for mta_status_t { + fn from(err: Error) -> Self { + todo!() + } +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_last_error( + message: *mut *const c_char, + origin: *mut *const c_char, + data: *mut *mut c_void, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_set_last_error( + message: *const c_char, + origin: *const c_char, + data: *mut c_void, + data_deleter: Option, +) -> mta_status_t { + todo!() +} diff --git a/metatomic-core/src/c_api/system.rs b/metatomic-core/src/c_api/system.rs new file mode 100644 index 00000000..1697e515 --- /dev/null +++ b/metatomic-core/src/c_api/system.rs @@ -0,0 +1,131 @@ +use std::ffi::c_char; + +use dlpk::sys::DLManagedTensorVersioned; +use metatensor::c_api::{mts_block_t, mts_tensormap_t}; + +use crate::System; +use super::{mta_status_t, mta_string_t}; + +/// TODO +#[allow(non_camel_case_types)] +pub struct mta_system_t(pub(crate) System); + + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_create( + length_unit: *const c_char, + types: *mut DLManagedTensorVersioned, + positions: *mut DLManagedTensorVersioned, + cell: *mut DLManagedTensorVersioned, + pbc: *mut DLManagedTensorVersioned, + system: *mut *mut mta_system_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_free(system: *mut mta_system_t) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_size( + system: *const mta_system_t, + size: *mut usize, +) -> mta_status_t { + todo!() +} + +/// TODO +#[allow(non_camel_case_types)] +#[repr(C)] +#[non_exhaustive] +pub enum mta_system_data_kind { + MTA_SYSTEM_DATA_TYPES = 0, + MTA_SYSTEM_DATA_POSITIONS = 1, + MTA_SYSTEM_DATA_CELL = 2, + MTA_SYSTEM_DATA_PBC = 3, +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_get_data( + system: *const mta_system_t, + request: mta_system_data_kind, + data: *mut *mut DLManagedTensorVersioned, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_get_length_unit( + system: *const mta_system_t, + length_unit: *mut mta_string_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_add_pairs( + system: *mut mta_system_t, + options: *const c_char, + pairs: *mut mts_block_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_get_pairs( + system: *const mta_system_t, + options: *const c_char, + pairs: *mut *const mts_block_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_known_pairs( + system: *const mta_system_t, + pairs_options: *mut mta_string_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_add_custom_data( + system: *mut mta_system_t, + name: *const c_char, + data: *mut mts_tensormap_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_get_custom_data( + system: *const mta_system_t, + name: *const c_char, + data: *mut *const mts_tensormap_t, +) -> mta_status_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_system_known_custom_data( + system: *const mta_system_t, + names: *mut mta_string_t, +) -> mta_status_t { + todo!() +} + + +// TODO: mta_system_to(device, dtype) diff --git a/metatomic-core/src/c_api/utils.rs b/metatomic-core/src/c_api/utils.rs new file mode 100644 index 00000000..350a9552 --- /dev/null +++ b/metatomic-core/src/c_api/utils.rs @@ -0,0 +1,102 @@ +use std::ffi::{CString, c_char}; + +use once_cell::sync::Lazy; + +use super::mta_status_t; + + +static VERSION: Lazy = Lazy::new(|| { + CString::new(env!("METATOMIC_FULL_VERSION")).expect("version contains NULL byte") +}); + + +/// Get the runtime version of the metatomic library as a string. +/// +/// This version follows the `..[-]` format. +#[no_mangle] +pub extern "C" fn mta_version() -> *const c_char { + return VERSION.as_ptr(); +} + +/// TODO +#[allow(non_camel_case_types)] +pub struct mta_opaque_string_t(CString); + +/// TODO +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct mta_string_t(*mut mta_opaque_string_t); + +impl std::fmt::Debug for mta_string_t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut builder = f.debug_tuple("mta_string_t"); + + if self.0.is_null() { + builder.field(&"NULL"); + } else { + builder.field(&self.as_str()); + } + builder.finish() + } +} + +impl mta_string_t { + /// TODO + pub fn new(value: impl Into) -> Self { + let cstring = CString::new(value.into()).unwrap(); + let boxed = Box::new(mta_opaque_string_t(cstring)); + mta_string_t(Box::into_raw(boxed)) + } + + /// TODO + pub fn null() -> Self { + mta_string_t(std::ptr::null_mut()) + } + + /// TODO + pub fn as_str(&self) -> &str { + if self.0.is_null() { + return ""; + } + unsafe { + return (*(self.0)).0.to_str().expect("mta_string_t is not valid UTF8") + } + } +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_string_create( + raw: *const c_char, +) -> mta_string_t { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_string_free(string: mta_string_t) { + todo!() +} + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_string_view( + string: mta_string_t, +) -> *const c_char { + todo!() +} + + +/// TODO +#[no_mangle] +pub unsafe extern "C" fn mta_unit_conversion_factor( + from_unit: *const c_char, + to_unit: *const c_char, + conversion: *mut f64, +) -> mta_status_t { + todo!() +} + + + +// TODO: logging & warnings? diff --git a/metatomic-core/src/lib.rs b/metatomic-core/src/lib.rs index bc47948b..8e4c828b 100644 --- a/metatomic-core/src/lib.rs +++ b/metatomic-core/src/lib.rs @@ -9,5 +9,46 @@ #![allow(clippy::let_underscore_untyped, clippy::manual_let_else, clippy::empty_line_after_doc_comments)] +// To be removed lated +#![allow(unused_variables, dead_code, clippy::needless_pass_by_value)] + + #[doc(hidden)] -mod c_api; +pub mod c_api; + +mod metadata; +pub use self::metadata::{ModelMetadata, Quantity, PairListOptions}; + +mod system; +pub use self::system::System; + +mod model; +pub use self::model::Model; + +mod plugin; +pub use self::plugin::{Plugin, load_plugin, load_model}; + +mod units; +pub use self::units::unit_conversion_factor; + +/// TODO +#[derive(Debug)] +pub enum Error { + // TODO +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + todo!() + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + self.source() + } +} diff --git a/metatomic-core/src/metadata.rs b/metatomic-core/src/metadata.rs new file mode 100644 index 00000000..d56a4674 --- /dev/null +++ b/metatomic-core/src/metadata.rs @@ -0,0 +1,132 @@ +use json::JsonValue; + +use crate::Error; + +/// TODO +pub struct PairListOptions { + /// TODO + cutoff: f64, + /// TODO + full_list: bool, + /// TODO + strict: bool, + /// TODO + requestors: Vec, +} + +impl std::cmp::PartialEq for PairListOptions { + fn eq(&self, other: &Self) -> bool { + self.cutoff == other.cutoff + && self.full_list == other.full_list + && self.strict == other.strict + } +} + +impl std::cmp::Eq for PairListOptions {} + +impl std::cmp::PartialOrd for PairListOptions { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::cmp::Ord for PairListOptions { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.cutoff.partial_cmp(&other.cutoff).expect("cutoff is NaN") + .then_with(|| self.full_list.cmp(&other.full_list)) + .then_with(|| self.strict.cmp(&other.strict)) + } +} + +// TODO +// { +// "type": "metatomic_pair_options", +// "cutoff": "0xaeabf23", <== hex of the int corresponding to the f64 bits to keep full precision +// "full_list": false, +// "strict": false, +// "requestors": ["..."] +// } +impl From for JsonValue { + fn from(value: PairListOptions) -> Self { + todo!() + } +} + +impl TryFrom for PairListOptions { + type Error = Error; + + fn try_from(value: JsonValue) -> Result { + todo!() + } +} + +// ========================================================================== // +// ========================================================================== // +// ========================================================================== // + +/// TODO +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ModelMetadata { + pub name: String, + // TODO +} + +// { +// "type": "metatomic_model_metadata", +// "name": "...", +// "authors": ["..."], +// "references": { +// "implementation": ["..."], +// "architecture": ["..."], +// "model": ["..."] +// }, +// "extra": { +// "key...": "value..." +// } +// }, +impl From for JsonValue { + fn from(value: ModelMetadata) -> Self { + todo!() + } +} + +impl TryFrom for ModelMetadata { + type Error = Error; + + fn try_from(value: JsonValue) -> Result { + todo!() + } +} + +// ========================================================================== // +// ========================================================================== // +// ========================================================================== // + +/// TODO, previously `ModelOutput` +#[derive(Debug)] +pub struct Quantity { + pub name: String, + // TODO +} + +// TODO: +// { +// "type": "metatomic_quantity", +// "name": "...", +// "unit": "...", +// "gradients": ["...", "..."], +// "sample_kind": "atom" | "system" | "atom-pair", +// }, +impl From for JsonValue { + fn from(value: Quantity) -> Self { + todo!() + } +} + +impl TryFrom for Quantity { + type Error = Error; + + fn try_from(value: JsonValue) -> Result { + todo!() + } +} diff --git a/metatomic-core/src/model.rs b/metatomic-core/src/model.rs new file mode 100644 index 00000000..16b208ac --- /dev/null +++ b/metatomic-core/src/model.rs @@ -0,0 +1,20 @@ +use metatensor::{Labels, TensorMap}; + +use crate::{Error, Quantity, System}; + +use crate::c_api::mta_model_t; + +/// TODO +pub struct Model(pub(crate) mta_model_t); + + +/// TODO +pub fn execute_model( + model: &Model, + systems: &[System], + selected_atoms: Option, + requested_outputs: &[Quantity], + check_consistency: bool, +) -> Result, Error> { + todo!() +} diff --git a/metatomic-core/src/plugin.rs b/metatomic-core/src/plugin.rs new file mode 100644 index 00000000..60d14580 --- /dev/null +++ b/metatomic-core/src/plugin.rs @@ -0,0 +1,37 @@ +use std::collections::BTreeMap; + +use crate::c_api::mta_plugin_t; +use crate::{Error, Model}; + +/// TODO +pub const MTA_ABI_VERSION: i32 = 1; + +/// TODO +pub struct Plugin(mta_plugin_t); + +impl Plugin { + /// TODO + pub fn new(c_plugin: mta_plugin_t) -> Self { + Self(c_plugin) + } + + /// TODO + pub fn name(&self) -> &str { + todo!() + } + + /// TODO + pub fn load_model(&self, load_from: &str, options: BTreeMap) -> Result { + todo!() + } +} + +/// TODO +pub fn load_plugin(path: &str) -> Result<(), Error> { + todo!() +} + +/// TODO +pub fn load_model(plugin: Option<&str>, load_from: &str, options: BTreeMap) -> Result { + todo!() +} diff --git a/metatomic-core/src/system.rs b/metatomic-core/src/system.rs new file mode 100644 index 00000000..30677f5f --- /dev/null +++ b/metatomic-core/src/system.rs @@ -0,0 +1,53 @@ +use std::collections::{BTreeMap, HashMap}; + +use dlpk::DLPackTensor; +use metatensor::{TensorBlock, TensorMap}; + +use crate::PairListOptions; + + +/// TODO +pub struct System { + length_unit: String, + types: DLPackTensor, + positions: DLPackTensor, + cell: DLPackTensor, + pbc: DLPackTensor, + + pairs: BTreeMap, + custom_data: HashMap, +} + + +impl System { + /// TODO + pub fn new( + length_unit: String, + types: DLPackTensor, + positions: DLPackTensor, + cell: DLPackTensor, + pbc: DLPackTensor + ) -> Self { + todo!() + } + + /// TODO + pub fn add_pairs(&mut self, options: PairListOptions, pairs: TensorBlock, check_consistency: bool) { + todo!() + } + + /// TODO + pub fn get_pairs(&mut self, options: PairListOptions) -> Option<&TensorBlock> { + todo!() + } + + /// TODO + pub fn set_custom_data(&mut self, name: String, data: TensorMap) { + todo!() + } + + /// TODO + pub fn get_custom_data(&self, name: &str) -> Option<&TensorMap> { + todo!() + } +} diff --git a/metatomic-core/src/units.rs b/metatomic-core/src/units.rs new file mode 100644 index 00000000..d06eab41 --- /dev/null +++ b/metatomic-core/src/units.rs @@ -0,0 +1,7 @@ +use crate::Error; + + +/// TODO +pub fn unit_conversion_factor(from_unit: &str, to_unit: &str) -> Result { + todo!() +} diff --git a/metatomic-core/tests/check-cxx-install.rs b/metatomic-core/tests/check-cxx-install.rs index d66f4883..6baa5b4e 100644 --- a/metatomic-core/tests/check-cxx-install.rs +++ b/metatomic-core/tests/check-cxx-install.rs @@ -23,19 +23,21 @@ fn check_cxx_install() { const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); - // ====================================================================== // - // build and install metatensor with cmake let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); build_dir.push("cxx-install"); build_dir.push("cmake-find-package"); std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + // ====================================================================== // + // install dependencies with pip let deps_dir = build_dir.join("deps"); let virtualenv_dir = deps_dir.join("virtualenv"); std::fs::create_dir_all(&virtualenv_dir).expect("failed to create virtualenv dir"); let python_exe = utils::create_python_venv(virtualenv_dir); let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); + // ====================================================================== // + // build and install metatomic with cmake let metatomic_dep = deps_dir.join("metatomic-core"); let source_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -54,7 +56,7 @@ fn check_cxx_install() { cmake_config.arg(format!("-DCMAKE_PREFIX_PATH={};{}", metatensor_cmake_prefix.display(), metatomic_cmake_prefix.display())); utils::run_command(cmake_config, "cmake configuration"); - // build the code, linking to metatensor + // build the code, linking to metatomic let cmake_build = utils::cmake_build(&build_dir); utils::run_command(cmake_build, "cmake build"); diff --git a/metatomic-torch/tests/check-torch-install.rs b/metatomic-torch/tests/check-torch-install.rs index ad8cfb60..14e85628 100644 --- a/metatomic-torch/tests/check-torch-install.rs +++ b/metatomic-torch/tests/check-torch-install.rs @@ -24,14 +24,13 @@ fn check_torch_install() { const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - // ====================================================================== // - // build and install metatensor-torch with cmake let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); build_dir.push("torch-install"); build_dir.push("cmake-find-package"); std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); - + // ====================================================================== // + // install dependencies with pip let deps_dir = build_dir.join("deps"); let torch_dep = deps_dir.join("virtualenv"); @@ -41,7 +40,8 @@ fn check_torch_install() { let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python); let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python); - // configure cmake for metatomic-torch + // ====================================================================== // + // build and install metatomic-torch with cmake let metatomic_torch_dep = deps_dir.join("metatomic-torch"); let cmake_options = vec![ @@ -68,7 +68,7 @@ fn check_torch_install() { ); // ====================================================================== // - // // try to use the installed metatomic-torch from cmake + // try to use the installed metatomic-torch from cmake let mut source_dir = PathBuf::from(&cargo_manifest_dir); source_dir.extend(["tests", "cmake-project"]); @@ -93,7 +93,7 @@ fn check_torch_install() { utils::run_command(ctest, "ctest"); } -/// Same as above, but using pre-built metatensor-torch from the Python wheel, +/// Same as above, but using metatomic-torch from the Python wheel, /// instead of building it from source with cmake. #[test] fn check_python_install() { @@ -106,13 +106,13 @@ fn check_python_install() { const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); - // ====================================================================== // - // build and install metatensor and metatensor-torch with pip let mut build_dir = PathBuf::from(CARGO_TARGET_TMPDIR); build_dir.push("torch-install"); build_dir.push("python-wheels"); std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + // ====================================================================== // + // install dependencies with pip let mut venv_dir = build_dir.clone(); venv_dir.push("virtualenv"); @@ -123,6 +123,8 @@ fn check_python_install() { let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python_exe); let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python_exe); + // ====================================================================== // + // build and install metatomic and metatomic-torch with pip let mta_core_source_dir = cargo_manifest_dir.parent().unwrap().join("python").join("metatomic_core"); let metatomic_core_cmake_prefix = utils::setup_metatomic_core_pip(&python_exe, &mta_core_source_dir); @@ -130,7 +132,7 @@ fn check_python_install() { let metatomic_torch_cmake_prefix = utils::setup_metatomic_torch_pip(&python_exe, &mta_torch_source_dir); // ====================================================================== // - // try to use the installed metatensor-torch from cmake + // try to use the installed metatomic-torch from cmake let mut source_dir = PathBuf::from(&cargo_manifest_dir); source_dir.extend(["tests", "cmake-project"]); @@ -147,7 +149,7 @@ fn check_python_install() { utils::run_command(cmake_config, "cmake configuration"); - // build the code, linking to metatensor-torch + // build the code, linking to metatomic-torch let cmake_build = utils::cmake_build(&build_dir); utils::run_command(cmake_build, "cmake build"); @@ -175,16 +177,19 @@ fn check_cmake_subdirectory() { build_dir.push("cmake-subdirectory"); std::fs::create_dir_all(&build_dir).expect("failed to create build dir"); + // ====================================================================== // + // install dependencies with pip let deps_dir = build_dir.join("deps"); - let torch_dep = deps_dir.join("virtualenv"); - std::fs::create_dir_all(&torch_dep).expect("failed to create virtualenv dir"); - let python = utils::create_python_venv(torch_dep); + let virtualenv_dir = deps_dir.join("virtualenv"); + std::fs::create_dir_all(&virtualenv_dir).expect("failed to create virtualenv dir"); + let python = utils::create_python_venv(virtualenv_dir); let pytorch_cmake_prefix = utils::setup_torch_pip(&python); let metatensor_cmake_prefix = utils::setup_metatensor_pip(&python); let metatensor_torch_cmake_prefix = utils::setup_metatensor_torch_pip(&python); // ====================================================================== // + // build metatomic-torch with cmake, using add_subdirectory let cargo_manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let mut source_dir = PathBuf::from(&cargo_manifest_dir); source_dir.extend(["tests", "cmake-project"]); diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..c7ad93ba --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +disable_all_formatting = true diff --git a/scripts/check-c-api-docs.py b/scripts/check-c-api-docs.py new file mode 100755 index 00000000..73ee7d92 --- /dev/null +++ b/scripts/check-c-api-docs.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +""" +A small script checking that all the C API functions are documented +""" + +import os +import sys + +from pycparser import c_ast, parse_file + + +ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) +C_API_DOCS = os.path.join(ROOT, "docs", "src", "core", "reference", "c") +FAKE_INCLUDES = [os.path.join(ROOT, "scripts", "include")] +METATOMIC_HEADER = os.path.relpath( + os.path.join(ROOT, "metatomic-core", "include", "metatomic.h") +) + + +ERRORS = 0 + + +def error(message): + global ERRORS + ERRORS += 1 + print(message) + + +def documented_functions(): + functions = [] + + for root, _, paths in os.walk(C_API_DOCS): + for path in paths: + with open(os.path.join(root, path), encoding="utf8") as fd: + for line in fd: + if line.startswith(".. doxygenfunction::"): + name = line.split()[2] + functions.append(name) + + return functions + + +def functions_in_outline(): + # function from the "miscellaneous" section of the docs don't require an outline + # (since they are not related to a specific struct type) + functions = [ + "mta_version", + "mta_last_error", + "mta_set_last_error", + "mta_string_create", + "mta_string_free", + "mta_string_view", + "mta_format_metadata", + "mta_unit_conversion_factor", + ] + + for root, _, paths in os.walk(C_API_DOCS): + for path in paths: + with open(os.path.join(root, path), encoding="utf8") as fd: + for line in fd: + if ":c:func:" in line: + name = line.split("`")[1] + functions.append(name) + return functions + + +def all_functions(): + cpp_args = ["-E"] + for path in FAKE_INCLUDES: + cpp_args += ["-I", path] + ast = parse_file(METATOMIC_HEADER, use_cpp=True, cpp_path="gcc", cpp_args=cpp_args) + + functions = [] + + class AstVisitor(c_ast.NodeVisitor): + def visit_Decl(self, node): + if not isinstance(node.type, c_ast.FuncDecl): + return + + if not node.name.startswith("mta_"): + return + + functions.append(node.name) + + visitor = AstVisitor() + visitor.visit(ast) + + return functions + + +if __name__ == "__main__": + docs = documented_functions() + outline = functions_in_outline() + for function in all_functions(): + if function not in docs: + error("Missing documentation for {}".format(function)) + if function not in outline: + error("Missing outline for {}".format(function)) + + if ERRORS != 0: + sys.exit(1) diff --git a/scripts/include/README b/scripts/include/README new file mode 100644 index 00000000..d56dd078 --- /dev/null +++ b/scripts/include/README @@ -0,0 +1,4 @@ +This directory contains fake headers used to allow pycparser to parse the code +without having to deal with all the complexity of actual stdlib implementations + +See https://eli.thegreenplace.net/2015/on-parsing-c-type-declarations-and-fake-headers for more information diff --git a/scripts/include/metatensor.h b/scripts/include/metatensor.h new file mode 100644 index 00000000..fb8e88f0 --- /dev/null +++ b/scripts/include/metatensor.h @@ -0,0 +1,8 @@ +// empty header with minimal content, to be used to parse metatomic.h + +typedef struct mts_labels_t mts_labels_t; +typedef struct mts_block_t mts_block_t; +typedef struct mts_tensormap_t mts_tensormap_t; + + +typedef struct DLManagedTensorVersioned DLManagedTensorVersioned; diff --git a/scripts/include/metatomic/version.h b/scripts/include/metatomic/version.h new file mode 100644 index 00000000..e69de29b diff --git a/scripts/include/stdarg.h b/scripts/include/stdarg.h new file mode 100644 index 00000000..e69de29b diff --git a/scripts/include/stdbool.h b/scripts/include/stdbool.h new file mode 100644 index 00000000..3bd41ef2 --- /dev/null +++ b/scripts/include/stdbool.h @@ -0,0 +1 @@ +typedef _Bool bool; \ No newline at end of file diff --git a/scripts/include/stddef.h b/scripts/include/stddef.h new file mode 100644 index 00000000..48b3db66 --- /dev/null +++ b/scripts/include/stddef.h @@ -0,0 +1,6 @@ +#ifndef FAKE_STDDEF_H +#define FAKE_STDDEF_H + +typedef void nullptr_t; + +#endif /* FAKE_STDDEF_H */ diff --git a/scripts/include/stdint.h b/scripts/include/stdint.h new file mode 100644 index 00000000..43ccc01d --- /dev/null +++ b/scripts/include/stdint.h @@ -0,0 +1,7 @@ +typedef int uint64_t; +typedef int int64_t; +typedef int int32_t; +typedef int uint32_t; +typedef int uint16_t; +typedef int uint8_t; +typedef int uintptr_t; diff --git a/scripts/include/stdlib.h b/scripts/include/stdlib.h new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/scripts/include/stdlib.h @@ -0,0 +1 @@ + From b020133dc4fd9a2e83010a2c34e2f57564f5d779 Mon Sep 17 00:00:00 2001 From: Sofiia Chorna Date: Thu, 28 May 2026 16:58:17 +0200 Subject: [PATCH 06/10] Implement PairListOptions json serialization --- docs/src/core/index.rst | 1 + docs/src/core/reference/json-formats.rst | 52 ++++++ metatomic-core/src/lib.rs | 11 +- metatomic-core/src/metadata.rs | 204 +++++++++++++++++++++-- 4 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 docs/src/core/reference/json-formats.rst diff --git a/docs/src/core/index.rst b/docs/src/core/index.rst index 60512b35..4d0cf24a 100644 --- a/docs/src/core/index.rst +++ b/docs/src/core/index.rst @@ -8,6 +8,7 @@ WIP :maxdepth: 2 reference/c/index + reference/json-formats .. toctree:: diff --git a/docs/src/core/reference/json-formats.rst b/docs/src/core/reference/json-formats.rst new file mode 100644 index 00000000..d1589fa2 --- /dev/null +++ b/docs/src/core/reference/json-formats.rst @@ -0,0 +1,52 @@ +.. _core-json-formats: + +JSON data formats +================= + +Some metatomic data structures are exchanged across the C API as JSON-encoded +strings rather than dedicated C types. This page documents the exact JSON +representation of each such structure, so that engines and models written in any +language can produce and consume them. + +Pair list options +----------------- + +Options describing a requested pair list (also known as a neighbor list). This +is the JSON representation of ``PairListOptions``, used for example by +:c:func:`mta_system_set_pairs`, :c:func:`mta_system_get_pairs` and +:c:func:`mta_system_pairs_options`. + +.. code-block:: json + + { + "type": "metatomic_pair_options", + "cutoff": "0x400c000000000000", + "full_list": false, + "strict": false, + "requestors": ["my-model"] + } + +``type`` + Must be the string ``"metatomic_pair_options"``. + +``cutoff`` + Cutoff radius for the pair list in the length unit of the model. Must be a + positive finite number. + + It is stored as a string containing the hexadecimal representation of the + 64-bit integer with the same bit pattern as the ``cutoff`` floating-point + value (i.e. reinterpreting the ``double`` as a ``uint64_t``). + +``full_list`` + Boolean. If ``true``, the list is a full list containing both ``i -> j`` + and ``j -> i`` for each pair, if ``false``, it is a half list containing + only ``i -> j``. + +``strict`` + Boolean. If ``true``, the list is guaranteed to contain only atoms within + the cutoff, if ``false``, it may also include some pairs slightly beyond the + cutoff. + +``requestors`` + Optional array of strings identifying who requested this pair list. May be + omitted, in which case it is treated as an empty list. diff --git a/metatomic-core/src/lib.rs b/metatomic-core/src/lib.rs index 8e4c828b..894e70e3 100644 --- a/metatomic-core/src/lib.rs +++ b/metatomic-core/src/lib.rs @@ -34,18 +34,23 @@ pub use self::units::unit_conversion_factor; /// TODO #[derive(Debug)] pub enum Error { - // TODO + /// Error while serializing data to or deserializing data from JSON + Serialization(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - todo!() + match self { + Error::Serialization(message) => write!(f, "{}", message), + } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - todo!() + match self { + Error::Serialization(_) => None, + } } fn cause(&self) -> Option<&dyn std::error::Error> { diff --git a/metatomic-core/src/metadata.rs b/metatomic-core/src/metadata.rs index d56a4674..913edcdf 100644 --- a/metatomic-core/src/metadata.rs +++ b/metatomic-core/src/metadata.rs @@ -2,15 +2,19 @@ use json::JsonValue; use crate::Error; -/// TODO +/// Options for the calculation of a pair list (neighbor list) +#[derive(Debug, Clone)] pub struct PairListOptions { - /// TODO + /// Cutoff radius for this pair list in the length unit of the model cutoff: f64, - /// TODO + /// Whether the list is a full list (contains both the pair `i -> j` and `j -> i`) + /// or a half list (contains only `i -> j`) full_list: bool, - /// TODO + /// Whether the list guarantees that only atoms within the cutoff are + /// included (strict) or may also include pairs slightly beyond the cutoff + /// (non-strict) strict: bool, - /// TODO + /// List of strings describing who requested this pair list requestors: Vec, } @@ -38,17 +42,16 @@ impl std::cmp::Ord for PairListOptions { } } -// TODO -// { -// "type": "metatomic_pair_options", -// "cutoff": "0xaeabf23", <== hex of the int corresponding to the f64 bits to keep full precision -// "full_list": false, -// "strict": false, -// "requestors": ["..."] -// } impl From for JsonValue { fn from(value: PairListOptions) -> Self { - todo!() + let mut result = JsonValue::new_object(); + result["type"] = "metatomic_pair_options".into(); + // store the bit pattern so the float round-trips exactly + result["cutoff"] = format!("{:#x}", value.cutoff.to_bits()).into(); + result["full_list"] = value.full_list.into(); + result["strict"] = value.strict.into(); + result["requestors"] = value.requestors.into(); + return result; } } @@ -56,7 +59,61 @@ impl TryFrom for PairListOptions { type Error = Error; fn try_from(value: JsonValue) -> Result { - todo!() + if !value.is_object() { + return Err(Error::Serialization( + "invalid JSON data for PairListOptions, expected an object".into() + )); + } + + if value["type"].as_str() != Some("metatomic_pair_options") { + return Err(Error::Serialization( + "'type' in JSON for PairListOptions must be 'metatomic_pair_options'".into() + )); + } + + let cutoff = value["cutoff"].as_str().ok_or_else(|| Error::Serialization( + "'cutoff' in JSON for PairListOptions must be a hex-encoded string".into() + ))?; + let bits = u64::from_str_radix(cutoff.strip_prefix("0x").unwrap_or(cutoff), 16) + .map_err(|_| Error::Serialization( + "'cutoff' in JSON for PairListOptions must be a hex-encoded string".into() + ))?; + let cutoff = f64::from_bits(bits); + + if !cutoff.is_finite() || cutoff <= 0.0 { + return Err(Error::Serialization( + "'cutoff' in JSON for PairListOptions must be a finite positive number".into() + )); + } + + let full_list = value["full_list"].as_bool().ok_or_else(|| Error::Serialization( + "'full_list' in JSON for PairListOptions must be a boolean".into() + ))?; + + let strict = value["strict"].as_bool().ok_or_else(|| Error::Serialization( + "'strict' in JSON for PairListOptions must be a boolean".into() + ))?; + + let mut requestors = Vec::new(); + if value.has_key("requestors") { + if !value["requestors"].is_array() { + return Err(Error::Serialization( + "'requestors' in JSON for PairListOptions must be an array".into() + )); + } + + for requestor in value["requestors"].members() { + let requestor = requestor.as_str().ok_or_else(|| Error::Serialization( + "'requestors' in JSON for PairListOptions must be an array of strings".into() + ))?; + // ignore empty strings and duplicates, keeping first-seen order + if !requestor.is_empty() && !requestors.iter().any(|r| r == requestor) { + requestors.push(requestor.to_string()); + } + } + } + + return Ok(PairListOptions { cutoff, full_list, strict, requestors }); } } @@ -130,3 +187,120 @@ impl TryFrom for Quantity { todo!() } } + + +#[cfg(test)] +mod tests { + mod pair_list_options { + use super::super::*; + + fn example() -> PairListOptions { + PairListOptions { + cutoff: 3.5, + full_list: true, + strict: false, + requestors: vec!["nl-1".to_string(), "nl-2".to_string()], + } + } + + #[test] + fn roundtrip() { + let options = example(); + let json: JsonValue = options.clone().into(); + + assert_eq!(json["type"].as_str(), Some("metatomic_pair_options")); + assert_eq!(json["cutoff"].as_str(), Some(format!("{:#x}", 3.5_f64.to_bits()).as_str())); + assert_eq!(json["full_list"].as_bool(), Some(true)); + assert_eq!(json["strict"].as_bool(), Some(false)); + + let parsed = PairListOptions::try_from(json).unwrap(); + assert_eq!(parsed.cutoff.to_bits(), options.cutoff.to_bits()); + assert_eq!(parsed.full_list, options.full_list); + assert_eq!(parsed.strict, options.strict); + assert_eq!(parsed.requestors, options.requestors); + } + + #[test] + fn cutoff_keeps_full_precision() { + let mut options = example(); + options.cutoff = 1.0 / 3.0; + let parsed = PairListOptions::try_from(JsonValue::from(options.clone())).unwrap(); + assert_eq!(parsed.cutoff.to_bits(), options.cutoff.to_bits()); + } + + #[test] + fn requestors_are_optional() { + let mut json: JsonValue = example().into(); + json.remove("requestors"); + let parsed = PairListOptions::try_from(json).unwrap(); + assert!(parsed.requestors.is_empty()); + } + + #[test] + fn rejects_invalid_json() { + // each case corrupts exactly one field of an otherwise valid object + let with_cutoff = |value: f64| { + let mut json = JsonValue::from(example()); + json["cutoff"] = format!("{:#x}", value.to_bits()).into(); + json + }; + + let mut wrong_type = JsonValue::from(example()); + wrong_type["type"] = "something-else".into(); + + let mut missing_cutoff = JsonValue::from(example()); + missing_cutoff.remove("cutoff"); + + let mut non_hex_cutoff = JsonValue::from(example()); + non_hex_cutoff["cutoff"] = "not-hex".into(); + + let mut non_boolean_flag = JsonValue::from(example()); + non_boolean_flag["full_list"] = "yes".into(); + + let mut non_array_requestors = JsonValue::from(example()); + non_array_requestors["requestors"] = "nl-1".into(); + + let mut non_string_requestor = JsonValue::from(example()); + non_string_requestor["requestors"] = json::array![ "nl-1", 42 ]; + + let cases = [ + (JsonValue::from("not an object"), + "invalid JSON data for PairListOptions, expected an object"), + (wrong_type, + "'type' in JSON for PairListOptions must be 'metatomic_pair_options'"), + (missing_cutoff, + "'cutoff' in JSON for PairListOptions must be a hex-encoded string"), + (non_hex_cutoff, + "'cutoff' in JSON for PairListOptions must be a hex-encoded string"), + (with_cutoff(f64::NAN), + "'cutoff' in JSON for PairListOptions must be a finite positive number"), + (with_cutoff(f64::INFINITY), + "'cutoff' in JSON for PairListOptions must be a finite positive number"), + (with_cutoff(-1.0), + "'cutoff' in JSON for PairListOptions must be a finite positive number"), + (with_cutoff(0.0), + "'cutoff' in JSON for PairListOptions must be a finite positive number"), + (non_boolean_flag, + "'full_list' in JSON for PairListOptions must be a boolean"), + (non_array_requestors, + "'requestors' in JSON for PairListOptions must be an array"), + (non_string_requestor, + "'requestors' in JSON for PairListOptions must be an array of strings"), + ]; + + for (json, expected) in cases { + let error = PairListOptions::try_from(json).expect_err("expected an error"); + assert_eq!(error.to_string(), expected); + } + } + + #[test] + fn requestors_skip_empty_and_duplicates() { + let mut json: JsonValue = example().into(); + json["requestors"] = json::array![ "a", "", "b", "a" ]; + + let parsed = PairListOptions::try_from(json).unwrap(); + assert_eq!(parsed.requestors, vec!["a".to_string(), "b".to_string()]); + } + } +} From 08d04c5b0f998c9511ce7ae54bd137e76f3fc3ae Mon Sep 17 00:00:00 2001 From: frostedoyster Date: Thu, 28 May 2026 16:38:52 +0200 Subject: [PATCH 07/10] Implement functions for C++ interface --- metatomic-core/include/metatomic/model.hpp | 225 ++++++++++++++++ metatomic-core/include/metatomic/plugin.hpp | 133 ++++++++++ metatomic-core/include/metatomic/system.hpp | 199 ++++++++++++++ metatomic-core/include/metatomic/utils.hpp | 280 ++++++++++++++++++++ 4 files changed, 837 insertions(+) diff --git a/metatomic-core/include/metatomic/model.hpp b/metatomic-core/include/metatomic/model.hpp index 1cae91bd..5a4d6bce 100644 --- a/metatomic-core/include/metatomic/model.hpp +++ b/metatomic-core/include/metatomic/model.hpp @@ -1,7 +1,232 @@ #pragma once +#include +#include +#include + #include +#include + +#include "./system.hpp" +#include "./utils.hpp" namespace metatomic { +/// RAII wrapper around a `mta_model_t`. +class Model final { +public: + /// Create an empty, invalid model. + Model() { + model_ = empty_model(); + } + + /// Take ownership of a raw `mta_model_t`. + explicit Model(mta_model_t model): model_(model) {} + + ~Model() { + this->reset_noexcept(); + } + + Model(const Model&) = delete; + Model& operator=(const Model&) = delete; + + Model(Model&& other) noexcept: Model() { + *this = std::move(other); + } + + Model& operator=(Model&& other) noexcept { + if (this != &other) { + this->reset_noexcept(); + model_ = other.model_; + other.model_ = empty_model(); + } + return *this; + } + + /// Does this wrapper contain a model? + bool is_valid() const { + return model_.data != nullptr; + } + + /// Unload the model. + void unload() { + if (model_.data != nullptr && model_.unload != nullptr) { + details::check_status(model_.unload(model_.data)); + } + model_ = empty_model(); + } + + /// Get model metadata as a JSON string. + std::string metadata() const { + this->check_callback(model_.metadata, "metadata"); + + mta_string_t metadata = nullptr; + details::check_status(model_.metadata(model_.data, &metadata)); + return String(metadata).str(); + } + + /// Get supported outputs as a JSON string. + std::string supported_outputs() const { + this->check_callback(model_.supported_outputs, "supported_outputs"); + + mta_string_t outputs = nullptr; + details::check_status(model_.supported_outputs(model_.data, &outputs)); + return String(outputs).str(); + } + + /// Get all pair lists requested by this model, each one serialized as JSON. + std::vector requested_pair_lists() const { + this->check_callback(model_.requested_pair_lists_count, "requested_pair_lists_count"); + this->check_callback(model_.requested_pair_list, "requested_pair_list"); + + uintptr_t count = 0; + details::check_status(model_.requested_pair_lists_count(model_.data, &count)); + + auto result = std::vector(); + result.reserve(count); + for (uintptr_t i=0; i requested_inputs() const { + this->check_callback(model_.requested_inputs_count, "requested_inputs_count"); + this->check_callback(model_.requested_input, "requested_input"); + + uintptr_t count = 0; + details::check_status(model_.requested_inputs_count(model_.data, &count)); + + auto result = std::vector(); + result.reserve(count); + for (uintptr_t i=0; i execute( + const std::vector& systems, + const metatensor::Labels* selected_atoms, + const std::vector& requested_outputs + ) { + this->check_valid(); + + auto c_systems = std::vector(); + c_systems.reserve(systems.size()); + for (const auto* system: systems) { + details::check_pointer(system); + c_systems.push_back(system->as_mta_system_t()); + } + + auto c_requested_outputs = std::vector(); + c_requested_outputs.reserve(requested_outputs.size()); + for (const auto& output: requested_outputs) { + c_requested_outputs.push_back(output.c_str()); + } + + auto raw_outputs = std::vector(requested_outputs.size(), nullptr); + details::check_status(mta_execute_model( + model_, + c_systems.data(), + c_systems.size(), + selected_atoms == nullptr ? nullptr : selected_atoms->as_mts_labels_t(), + c_requested_outputs.data(), + c_requested_outputs.size(), + raw_outputs.data(), + raw_outputs.size() + )); + + auto outputs = std::vector(); + outputs.reserve(raw_outputs.size()); + + try { + for (auto*& output: raw_outputs) { + details::check_pointer(output); + outputs.emplace_back(output); + output = nullptr; + } + } catch (...) { + for (auto* output: raw_outputs) { + if (output != nullptr) { + (void)mts_tensormap_free(output); + } + } + throw; + } + + return outputs; + } + + /// Execute this model on all atoms. + std::vector execute( + const std::vector& systems, + const std::vector& requested_outputs + ) { + return this->execute(systems, nullptr, requested_outputs); + } + + /// Get the underlying `mta_model_t`. + const mta_model_t& as_mta_model_t() const & { + return model_; + } + + const mta_model_t& as_mta_model_t() && = delete; + + /// Release the underlying `mta_model_t` without unloading it. + mta_model_t release() { + auto model = model_; + model_ = empty_model(); + return model; + } + +private: + static mta_model_t empty_model() { + mta_model_t model; + model.data = nullptr; + model.unload = nullptr; + model.metadata = nullptr; + model.supported_outputs = nullptr; + model.requested_pair_lists_count = nullptr; + model.requested_pair_list = nullptr; + model.requested_inputs_count = nullptr; + model.requested_input = nullptr; + model.execute_inner = nullptr; + return model; + } + + void reset_noexcept() noexcept { + if (model_.data != nullptr && model_.unload != nullptr) { + (void)model_.unload(model_.data); + } + model_ = empty_model(); + } + + void check_valid() const { + if (model_.data == nullptr) { + throw Error("can not use an empty metatomic::Model"); + } + } + + template + void check_callback(Callback callback, const char* name) const { + this->check_valid(); + if (callback == nullptr) { + throw Error("metatomic::Model does not implement " + std::string(name)); + } + } + + mta_model_t model_; +}; + } // namespace metatomic diff --git a/metatomic-core/include/metatomic/plugin.hpp b/metatomic-core/include/metatomic/plugin.hpp index 1cae91bd..1b0e7ce8 100644 --- a/metatomic-core/include/metatomic/plugin.hpp +++ b/metatomic-core/include/metatomic/plugin.hpp @@ -1,7 +1,140 @@ #pragma once +#include + +#include +#include +#include +#include + #include +#include "./model.hpp" +#include "./utils.hpp" + namespace metatomic { +/// Abstract base class for metatomic plugins implemented in C++. +class Plugin { +public: + virtual ~Plugin() = default; + + /// Name used to identify this plugin. + virtual std::string name() const = 0; + + /// Load a model from `load_from`, using the provided key/value options. + virtual Model load_model( + const std::string& load_from, + const std::vector& options + ) = 0; +}; + +namespace details { + template + struct PluginRegistration { + static PluginT* plugin; + static const char* name; + + static mta_status_t load_model( + const char* load_from, + const mta_kv_pair_t* options, + uintptr_t options_count, + mta_model_t* model + ) { + return details::catch_exceptions([&]() { + details::check_pointer(plugin); + details::check_pointer(model); + + auto loaded = plugin->load_model( + load_from == nullptr ? "" : load_from, + details::from_c_options(options, options_count) + ); + + *model = loaded.release(); + }); + } + }; + + template + PluginT* PluginRegistration::plugin = nullptr; + + template + const char* PluginRegistration::name = nullptr; +} // namespace details + +/// Register a C++ plugin. +/// +/// Due to the current C plugin ABI, this stores one plugin instance per concrete +/// C++ plugin type. The registered object must outlive all model-loading calls. +template +void register_plugin(PluginT& plugin) { + static_assert( + std::is_base_of::value, + "register_plugin expects a class derived from metatomic::Plugin" + ); + + details::PluginRegistration::plugin = &plugin; + const auto name = plugin.name(); + // The C plugin registry keeps this pointer; allocate stable process-lifetime storage. + auto* name_storage = new char[name.size() + 1]; + std::memcpy(name_storage, name.c_str(), name.size() + 1); + details::PluginRegistration::name = name_storage; + + auto c_plugin = mta_plugin_t{ + details::PluginRegistration::name, + &details::PluginRegistration::load_model, + }; + + mta_register_plugin(c_plugin); +} + +/// Register a raw C plugin. +inline void register_plugin(mta_plugin_t plugin) { + mta_register_plugin(plugin); +} + +/// Load a plugin dynamic library from the given path. +inline void load_plugin(const std::string& path) { + details::check_status(mta_load_plugin(path.c_str())); +} + +/// Load a model using the given plugin. +inline Model load_model( + const std::string& plugin_name, + const std::string& load_from, + const std::vector& options = {} +) { + auto c_options = details::to_c_options(options); + + auto model = mta_model_t{}; + details::check_status(mta_load_model( + plugin_name.c_str(), + load_from.c_str(), + c_options.data(), + c_options.size(), + &model + )); + + return Model(model); +} + +/// Load a model, letting metatomic pick the plugin. +inline Model load_model( + const std::string& load_from, + const std::vector& options = {} +) { + auto c_options = details::to_c_options(options); + + auto model = mta_model_t{}; + details::check_status(mta_load_model( + nullptr, + load_from.c_str(), + c_options.data(), + c_options.size(), + &model + )); + + return Model(model); +} + } // namespace metatomic diff --git a/metatomic-core/include/metatomic/system.hpp b/metatomic-core/include/metatomic/system.hpp index 1cae91bd..5fde00f3 100644 --- a/metatomic-core/include/metatomic/system.hpp +++ b/metatomic-core/include/metatomic/system.hpp @@ -1,7 +1,206 @@ #pragma once +#include +#include + #include +#include + +#include "./utils.hpp" namespace metatomic { +/// A System contains all the information about an atomistic system, and should +/// be used as input of atomistic models. +class System final { +public: + /// Create a new `System` from DLPack tensors. + /// + /// Ownership of all DLPack tensors is transferred to the C API. + System( + const std::string& length_unit, + DLManagedTensorVersioned* types, + DLManagedTensorVersioned* positions, + DLManagedTensorVersioned* cell, + DLManagedTensorVersioned* pbc + ): system_(nullptr) { + details::check_status(mta_system_create( + length_unit.c_str(), + types, + positions, + cell, + pbc, + &system_ + )); + details::check_pointer(system_); + } + + ~System() { + if (system_ != nullptr) { + (void)mta_system_free(system_); + } + } + + System(const System&) = delete; + System& operator=(const System&) = delete; + + System(System&& other) noexcept: system_(nullptr) { + *this = std::move(other); + } + + System& operator=(System&& other) noexcept { + if (system_ != nullptr) { + (void)mta_system_free(system_); + } + + system_ = other.system_; + other.system_ = nullptr; + return *this; + } + + /// Get the number of particles in this system. + size_t size() const { + uintptr_t result = 0; + details::check_status(mta_system_size(system_, &result)); + return static_cast(result); + } + + /// Get the length unit used by positions and cell. + std::string length_unit() const { + mta_string_t length_unit = nullptr; + details::check_status(mta_system_get_length_unit(system_, &length_unit)); + return String(length_unit).str(); + } + + /// Get particle types for all particles in the system. + DLPackTensor types() const { + return this->data(MTA_SYSTEM_DATA_TYPES); + } + + /// Get the positions for all particles in the system. + DLPackTensor positions() const { + return this->data(MTA_SYSTEM_DATA_POSITIONS); + } + + /// Get the unit cell/bounding box of the system. + DLPackTensor cell() const { + return this->data(MTA_SYSTEM_DATA_CELL); + } + + /// Get the periodic boundary conditions for the system. + DLPackTensor pbc() const { + return this->data(MTA_SYSTEM_DATA_PBC); + } + + /// Add a new pair list in this system. + /// + /// Ownership of `pairs` is transferred to the C API. + void set_pairs(const std::string& options, mts_block_t* pairs) { + details::check_status(mta_system_set_pairs(system_, options.c_str(), pairs)); + } + + /// Retrieve a previously stored pair list with the given options. + const mts_block_t* pairs_raw(const std::string& options) const { + const mts_block_t* pairs = nullptr; + details::check_status(mta_system_get_pairs(system_, options.c_str(), &pairs)); + details::check_pointer(pairs); + return pairs; + } + + /// Retrieve a previously stored pair list with the given options as a + /// non-owning metatensor view. + metatensor::TensorBlock pairs(const std::string& options) const { + return metatensor::TensorBlock::unsafe_view_from_ptr( + const_cast(this->pairs_raw(options)) + ); + } + + /// Get the options for all pair lists registered with this `System`. + std::vector pairs_options() const { + uintptr_t count = 0; + details::check_status(mta_system_pairs_count(system_, &count)); + + auto result = std::vector(); + result.reserve(count); + for (uintptr_t i=0; i data_names() const { + uintptr_t count = 0; + details::check_status(mta_system_data_count(system_, &count)); + + auto result = std::vector(); + result.reserve(count); + for (uintptr_t i=0; i +#include + +#include +#include +#include +#include +#include + #include namespace metatomic { +/// Exception class used for all errors in metatomic. +class Error: public std::runtime_error { +public: + /// Create a new Error with the given `message`. + explicit Error(const std::string& message): std::runtime_error(message) {} +}; + +/// Key/value pair used when loading models from plugins. +struct KeyValuePair { + std::string key; + std::string value; +}; + +/// RAII wrapper around a `DLManagedTensorVersioned*`. +/// +/// This owns the DLPack managed tensor object, and calls its deleter when the +/// wrapper is destroyed. +class DLPackTensor final { +public: + /// Create an empty wrapper. + DLPackTensor(): tensor_(nullptr) {} + + /// Take ownership of an existing DLPack managed tensor. + explicit DLPackTensor(DLManagedTensorVersioned* tensor): tensor_(tensor) {} + + ~DLPackTensor() { + if (tensor_ != nullptr && tensor_->deleter != nullptr) { + tensor_->deleter(tensor_); + } + } + + DLPackTensor(const DLPackTensor&) = delete; + DLPackTensor& operator=(const DLPackTensor&) = delete; + + DLPackTensor(DLPackTensor&& other) noexcept: DLPackTensor() { + *this = std::move(other); + } + + DLPackTensor& operator=(DLPackTensor&& other) noexcept { + if (tensor_ != nullptr && tensor_->deleter != nullptr) { + tensor_->deleter(tensor_); + } + + tensor_ = other.tensor_; + other.tensor_ = nullptr; + return *this; + } + + /// Check if this wrapper contains a tensor. + explicit operator bool() const { + return tensor_ != nullptr; + } + + /// Get the underlying DLPack managed tensor. + DLManagedTensorVersioned* get() const { + return tensor_; + } + + /// Get the underlying DLPack managed tensor. + DLManagedTensorVersioned* as_dlpack() const { + return tensor_; + } + + /// Release the DLPack managed tensor without calling its deleter. + DLManagedTensorVersioned* release() { + auto* tensor = tensor_; + tensor_ = nullptr; + return tensor; + } + +private: + DLManagedTensorVersioned* tensor_; +}; + +namespace details { + /// Check if a return status from the C API indicates an error, and throw a + /// `metatomic::Error` with the last error message if this is the case. + inline void check_status(mta_status_t status) { + if (status == MTA_SUCCESS) { + return; + } + + const char* message = nullptr; + const char* origin = nullptr; + void* data = nullptr; + (void)mta_last_error(&message, &origin, &data); + + if (origin != nullptr && std::strcmp(origin, "C++ exception") == 0 && data != nullptr) { + std::rethrow_exception(*static_cast(data)); + } + + throw Error(message == nullptr ? "unknown error" : message); + } + + /// Call the given `function`, catching any C++ exception and translating it + /// to a metatomic error code. + /// + /// This is required to prevent callbacks unwinding through the C API. + template + inline mta_status_t catch_exceptions(Function function, Args ...args) { + try { + function(std::move(args)...); + return MTA_SUCCESS; + } catch (...) { + auto* exception_ptr = new std::exception_ptr(std::current_exception()); + + const char* message = nullptr; + try { + std::rethrow_exception(*exception_ptr); + } catch (const std::exception& e) { + message = e.what(); + } catch (...) { + message = "C++ code threw an exception that was not a std::exception"; + } + + auto status = mta_set_last_error( + message, + "C++ exception", + exception_ptr, + [](void* ptr) { delete static_cast(ptr); } + ); + + if (status != MTA_SUCCESS) { + std::fprintf( + stderr, + "INTERNAL ERROR: unable to set last error after C++ callback failure (status: %d). ", + static_cast(status) + ); + if (message != nullptr) { + std::fprintf(stderr, "C++ error was: %s\n", message); + } else { + std::fprintf(stderr, "Unknown C++ error\n"); + } + delete exception_ptr; + } + + return MTA_ERROR_OTHER; + } + } + + /// Check if a pointer allocated by the C API is null. + inline void check_pointer(const void* pointer) { + if (pointer != nullptr) { + return; + } + + const char* message = nullptr; + const char* origin = nullptr; + void* data = nullptr; + (void)mta_last_error(&message, &origin, &data); + + if (origin != nullptr && std::strcmp(origin, "C++ exception") == 0 && data != nullptr) { + std::rethrow_exception(*static_cast(data)); + } + + throw Error(message == nullptr ? "received a null pointer from the metatomic C API" : message); + } + + inline std::vector to_c_options(const std::vector& options) { + auto c_options = std::vector(); + c_options.reserve(options.size()); + + for (const auto& option: options) { + c_options.push_back(mta_kv_pair_t{option.key.c_str(), option.value.c_str()}); + } + + return c_options; + } + + inline std::vector from_c_options(const mta_kv_pair_t* options, uintptr_t count) { + auto result = std::vector(); + result.reserve(count); + + if (count != 0) { + check_pointer(options); + } + + for (uintptr_t i=0; ic_str()); + } + +private: + mta_string_t string_; +}; + +/// Get the runtime version of metatomic as a string. +inline std::string version() { + auto* raw = mta_version(); + details::check_pointer(raw); + return std::string(raw); +} + +/// Get the conversion factor from `from_unit` to `to_unit`. +inline double unit_conversion_factor(const std::string& from_unit, const std::string& to_unit) { + double conversion = 0.0; + details::check_status(mta_unit_conversion_factor(from_unit.c_str(), to_unit.c_str(), &conversion)); + return conversion; +} + +/// Format model metadata JSON for display. +inline std::string format_metadata(const std::string& metadata) { + mta_string_t printed = nullptr; + details::check_status(mta_format_metadata(metadata.c_str(), &printed)); + return String(printed).str(); +} + } // namespace metatomic From d87def32bbba5ae804b1ed2fdf98a7f74a8c595f Mon Sep 17 00:00:00 2001 From: frostedoyster Date: Fri, 29 May 2026 07:41:03 +0200 Subject: [PATCH 08/10] Implement review suggestions --- metatomic-core/CMakeLists.txt | 16 ++ .../cmake/metatomic-config.in.cmake | 14 +- metatomic-core/include/metatomic.hpp | 1 + metatomic-core/include/metatomic/metadata.hpp | 192 +++++++++++++++ metatomic-core/include/metatomic/model.hpp | 227 +++++++++++++++++- metatomic-core/include/metatomic/plugin.hpp | 70 +++--- metatomic-core/include/metatomic/system.hpp | 20 +- 7 files changed, 494 insertions(+), 46 deletions(-) create mode 100644 metatomic-core/include/metatomic/metadata.hpp diff --git a/metatomic-core/CMakeLists.txt b/metatomic-core/CMakeLists.txt index 717e52e8..5cb30446 100644 --- a/metatomic-core/CMakeLists.txt +++ b/metatomic-core/CMakeLists.txt @@ -5,6 +5,11 @@ # an easier to use, idiomatic Rust API. cmake_minimum_required(VERSION 3.22) +if (POLICY CMP0135) + # Use download time as timestamp when extracting files from archives. + cmake_policy(SET CMP0135 NEW) +endif() + # Is metatomic the main project configured by the user? Or is this being used # as a submodule/subdirectory? if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR}) @@ -449,6 +454,17 @@ else() target_link_libraries(metatomic::shared INTERFACE metatensor) endif() +include(FetchContent) + +# JSON library from https://github.com/nlohmann/json +FetchContent_Declare(nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH SHA256=d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d +) +FetchContent_MakeAvailable(nlohmann_json) + +target_link_libraries(metatomic::shared INTERFACE nlohmann_json::nlohmann_json) +target_link_libraries(metatomic::static INTERFACE nlohmann_json::nlohmann_json) if (BUILD_SHARED_LIBS) add_library(metatomic ALIAS metatomic::shared) diff --git a/metatomic-core/cmake/metatomic-config.in.cmake b/metatomic-core/cmake/metatomic-config.in.cmake index 90fca167..8f2fc544 100644 --- a/metatomic-core/cmake/metatomic-config.in.cmake +++ b/metatomic-core/cmake/metatomic-config.in.cmake @@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 3.22) include(CMakeFindDependencyMacro) include(FindPackageHandleStandardArgs) +include(FetchContent) if(metatomic_FOUND) return() @@ -15,6 +16,15 @@ enable_language(CXX) set(REQUIRED_METATENSOR_VERSION @REQUIRED_METATENSOR_VERSION@) find_package(metatensor ${REQUIRED_METATENSOR_VERSION} CONFIG REQUIRED) +if (NOT TARGET nlohmann_json::nlohmann_json) + # JSON library from https://github.com/nlohmann/json + FetchContent_Declare(nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH SHA256=d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d + ) + FetchContent_MakeAvailable(nlohmann_json) +endif() + get_filename_component(METATOMIC_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_RELATIVE_PATH@" ABSOLUTE) if (WIN32) @@ -46,7 +56,7 @@ if (@METATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR @BUILD_SHARED_LIBS@) ) target_compile_features(metatomic::shared INTERFACE cxx_std_17) - target_link_libraries(metatomic::shared INTERFACE metatensor) + target_link_libraries(metatomic::shared INTERFACE metatensor nlohmann_json::nlohmann_json) if (WIN32) if (NOT EXISTS ${METATOMIC_IMPLIB_LOCATION}) @@ -75,7 +85,7 @@ if (@METATOMIC_INSTALL_BOTH_STATIC_SHARED@ OR NOT @BUILD_SHARED_LIBS@) ) target_compile_features(metatomic::static INTERFACE cxx_std_17) - target_link_libraries(metatomic::static INTERFACE metatensor) + target_link_libraries(metatomic::static INTERFACE metatensor nlohmann_json::nlohmann_json) endif() # Export either the shared or static library as the metatomic target diff --git a/metatomic-core/include/metatomic.hpp b/metatomic-core/include/metatomic.hpp index 3b5c8ac2..9eaad1e5 100644 --- a/metatomic-core/include/metatomic.hpp +++ b/metatomic-core/include/metatomic.hpp @@ -1,4 +1,5 @@ #include "metatomic/utils.hpp" // IWYU pragma: export +#include "metatomic/metadata.hpp" // IWYU pragma: export #include "metatomic/system.hpp" // IWYU pragma: export #include "metatomic/model.hpp" // IWYU pragma: export #include "metatomic/plugin.hpp" // IWYU pragma: export diff --git a/metatomic-core/include/metatomic/metadata.hpp b/metatomic-core/include/metatomic/metadata.hpp new file mode 100644 index 00000000..35a92c75 --- /dev/null +++ b/metatomic-core/include/metatomic/metadata.hpp @@ -0,0 +1,192 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace metatomic { + +/// Options for the calculation of a pair list. +class PairListOptions final { +public: + PairListOptions() = default; + + PairListOptions(double cutoff_value, bool full_list_value, bool strict_value, std::vector requestors_list = {}): + cutoff(cutoff_value), + full_list(full_list_value), + strict(strict_value), + requestors(std::move(requestors_list)) + {} + + double cutoff = 0.0; + bool full_list = false; + bool strict = false; + std::vector requestors; + + std::string to_json() const; + static PairListOptions from_json(const std::string& json); +}; + +/// Metadata about a specific exported model. +class ModelMetadata final { +public: + ModelMetadata() = default; + + ModelMetadata( + std::string model_name, + std::string model_description, + std::vector model_authors, + std::map> model_references = {}, + std::map extra_metadata = {} + ): + name(std::move(model_name)), + description(std::move(model_description)), + authors(std::move(model_authors)), + references(std::move(model_references)), + extra(std::move(extra_metadata)) + {} + + std::string name; + std::string description; + std::vector authors; + std::map> references; + std::map extra; + + std::string to_json() const; + static ModelMetadata from_json(const std::string& json); +}; + +/// Description of a quantity used as model input or output. +class Quantity final { +public: + Quantity() = default; + + Quantity( + std::string quantity_name, + std::string quantity_unit, + std::vector quantity_gradients, + std::string quantity_sample_kind + ): + name(std::move(quantity_name)), + unit(std::move(quantity_unit)), + gradients(std::move(quantity_gradients)), + sample_kind(std::move(quantity_sample_kind)) + {} + + std::string name; + std::string unit; + std::vector gradients; + std::string sample_kind; + + std::string to_json() const; + static Quantity from_json(const std::string& json); +}; + +namespace details { + inline std::string double_to_hex(double value) { + uint64_t bits = 0; + static_assert(sizeof(bits) == sizeof(value), "unexpected double size"); + std::memcpy(&bits, &value, sizeof(bits)); + + auto stream = std::ostringstream(); + stream << "0x" << std::hex << bits; + return stream.str(); + } + + inline double hex_to_double(const std::string& value) { + auto bits = uint64_t(0); + auto stream = std::istringstream(value); + if (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0) { + stream.seekg(2); + } + stream >> std::hex >> bits; + + auto result = 0.0; + static_assert(sizeof(bits) == sizeof(result), "unexpected double size"); + std::memcpy(&result, &bits, sizeof(result)); + return result; + } + +} // namespace details + +inline std::string PairListOptions::to_json() const { + return nlohmann::json{ + {"type", "metatomic_pair_options"}, + {"cutoff", details::double_to_hex(cutoff)}, + {"full_list", full_list}, + {"strict", strict}, + {"requestors", requestors}, + }.dump(); +} + +inline PairListOptions PairListOptions::from_json(const std::string& string) { + auto json = nlohmann::json::parse(string); + auto options = PairListOptions(); + + auto cutoff = std::string("0x0"); + if (json.contains("cutoff")) { + cutoff = json.at("cutoff").get(); + } + + options.cutoff = details::hex_to_double(cutoff); + options.full_list = json.value("full_list", false); + options.strict = json.value("strict", false); + options.requestors = json.value("requestors", std::vector{}); + + return options; +} + +inline std::string ModelMetadata::to_json() const { + return nlohmann::json{ + {"type", "metatomic_model_metadata"}, + {"name", name}, + {"description", description}, + {"authors", authors}, + {"references", references}, + {"extra", extra}, + }.dump(); +} + +inline ModelMetadata ModelMetadata::from_json(const std::string& string) { + auto json = nlohmann::json::parse(string); + auto metadata = ModelMetadata(); + + metadata.name = json.value("name", ""); + metadata.description = json.value("description", ""); + metadata.authors = json.value("authors", std::vector{}); + metadata.references = json.value("references", std::map>{}); + metadata.extra = json.value("extra", std::map{}); + + return metadata; +} + +inline std::string Quantity::to_json() const { + return nlohmann::json{ + {"type", "metatomic_quantity"}, + {"name", name}, + {"unit", unit}, + {"gradients", gradients}, + {"sample_kind", sample_kind}, + }.dump(); +} + +inline Quantity Quantity::from_json(const std::string& string) { + auto json = nlohmann::json::parse(string); + auto quantity = Quantity(); + + quantity.name = json.value("name", ""); + quantity.unit = json.value("unit", ""); + quantity.gradients = json.value("gradients", std::vector{}); + quantity.sample_kind = json.value("sample_kind", ""); + + return quantity; +} + +} // namespace metatomic diff --git a/metatomic-core/include/metatomic/model.hpp b/metatomic-core/include/metatomic/model.hpp index 5a4d6bce..ea0592dc 100644 --- a/metatomic-core/include/metatomic/model.hpp +++ b/metatomic-core/include/metatomic/model.hpp @@ -1,17 +1,48 @@ #pragma once #include +#include #include #include #include #include +#include "./metadata.hpp" #include "./system.hpp" #include "./utils.hpp" namespace metatomic { +/// Abstract base class for atomistic models implemented in C++. +class ModelBase { +public: + virtual ~ModelBase() = default; + + /// Get metadata about this model. + virtual ModelMetadata metadata() const = 0; + + /// Get all quantities this model can compute. + virtual std::vector supported_outputs() const = 0; + + /// Get all pair lists this model requires. + virtual std::vector requested_pair_lists() const { + return {}; + } + + /// Get all custom inputs this model requires. + virtual std::vector requested_inputs() const { + return {}; + } + + /// Execute this model. + virtual std::vector execute( + const std::vector& systems, + const mts_labels_t* selected_atoms, + const std::vector& requested_outputs + ) = 0; +}; + /// RAII wrapper around a `mta_model_t`. class Model final { public: @@ -23,6 +54,24 @@ class Model final { /// Take ownership of a raw `mta_model_t`. explicit Model(mta_model_t model): model_(model) {} + /// Create a C API model wrapping a C++ model implementation. + explicit Model(std::unique_ptr model) { + if (model == nullptr) { + throw Error("can not create a metatomic::Model from a null ModelBase"); + } + + model_ = empty_model(); + model_.data = model.release(); + model_.unload = &Model::unload_callback; + model_.metadata = &Model::metadata_callback; + model_.supported_outputs = &Model::supported_outputs_callback; + model_.requested_pair_lists_count = &Model::requested_pair_lists_count_callback; + model_.requested_pair_list = &Model::requested_pair_list_callback; + model_.requested_inputs_count = &Model::requested_inputs_count_callback; + model_.requested_input = &Model::requested_input_callback; + model_.execute_inner = &Model::execute_callback; + } + ~Model() { this->reset_noexcept(); } @@ -56,8 +105,8 @@ class Model final { model_ = empty_model(); } - /// Get model metadata as a JSON string. - std::string metadata() const { + /// Get model metadata serialized as JSON. + std::string metadata_json() const { this->check_callback(model_.metadata, "metadata"); mta_string_t metadata = nullptr; @@ -65,8 +114,13 @@ class Model final { return String(metadata).str(); } - /// Get supported outputs as a JSON string. - std::string supported_outputs() const { + /// Get model metadata. + ModelMetadata metadata() const { + return ModelMetadata::from_json(this->metadata_json()); + } + + /// Get supported outputs serialized as JSON. + std::string supported_outputs_json() const { this->check_callback(model_.supported_outputs, "supported_outputs"); mta_string_t outputs = nullptr; @@ -74,8 +128,17 @@ class Model final { return String(outputs).str(); } + /// Get all quantities this model can compute. + std::vector supported_outputs() const { + auto outputs = std::vector(); + for (const auto& output: nlohmann::json::parse(this->supported_outputs_json())) { + outputs.push_back(Quantity::from_json(output.dump())); + } + return outputs; + } + /// Get all pair lists requested by this model, each one serialized as JSON. - std::vector requested_pair_lists() const { + std::vector requested_pair_lists_json() const { this->check_callback(model_.requested_pair_lists_count, "requested_pair_lists_count"); this->check_callback(model_.requested_pair_list, "requested_pair_list"); @@ -93,8 +156,17 @@ class Model final { return result; } + /// Get all pair lists requested by this model. + std::vector requested_pair_lists() const { + auto result = std::vector(); + for (const auto& options: this->requested_pair_lists_json()) { + result.push_back(PairListOptions::from_json(options)); + } + return result; + } + /// Get all custom inputs requested by this model, each one serialized as JSON. - std::vector requested_inputs() const { + std::vector requested_inputs_json() const { this->check_callback(model_.requested_inputs_count, "requested_inputs_count"); this->check_callback(model_.requested_input, "requested_input"); @@ -112,13 +184,22 @@ class Model final { return result; } + /// Get all custom inputs requested by this model. + std::vector requested_inputs() const { + auto result = std::vector(); + for (const auto& input: this->requested_inputs_json()) { + result.push_back(Quantity::from_json(input)); + } + return result; + } + /// Execute this model. /// /// The number of returned tensor maps is `requested_outputs.size()`. std::vector execute( const std::vector& systems, const metatensor::Labels* selected_atoms, - const std::vector& requested_outputs + const std::vector& requested_outputs ) { this->check_valid(); @@ -130,9 +211,12 @@ class Model final { } auto c_requested_outputs = std::vector(); + auto requested_outputs_json = std::vector(); + requested_outputs_json.reserve(requested_outputs.size()); c_requested_outputs.reserve(requested_outputs.size()); for (const auto& output: requested_outputs) { - c_requested_outputs.push_back(output.c_str()); + requested_outputs_json.push_back(output.to_json()); + c_requested_outputs.push_back(requested_outputs_json.back().c_str()); } auto raw_outputs = std::vector(requested_outputs.size(), nullptr); @@ -171,7 +255,7 @@ class Model final { /// Execute this model on all atoms. std::vector execute( const std::vector& systems, - const std::vector& requested_outputs + const std::vector& requested_outputs ) { return this->execute(systems, nullptr, requested_outputs); } @@ -205,6 +289,131 @@ class Model final { return model; } + static ModelBase* model_base(const void* data) { + details::check_pointer(data); + return static_cast(const_cast(data)); + } + + static mta_status_t unload_callback(void* data) { + return details::catch_exceptions([&]() { + delete model_base(data); + }); + } + + static mta_status_t metadata_callback(const void* data, mta_string_t* metadata_json) { + return details::catch_exceptions([&]() { + details::check_pointer(metadata_json); + *metadata_json = mta_string_create(model_base(data)->metadata().to_json().c_str()); + details::check_pointer(*metadata_json); + }); + } + + static mta_status_t supported_outputs_callback(const void* data, mta_string_t* outputs_json) { + return details::catch_exceptions([&]() { + details::check_pointer(outputs_json); + auto outputs = nlohmann::json::array(); + for (const auto& output: model_base(data)->supported_outputs()) { + outputs.push_back(nlohmann::json::parse(output.to_json())); + } + + *outputs_json = mta_string_create(outputs.dump().c_str()); + details::check_pointer(*outputs_json); + }); + } + + static mta_status_t requested_pair_lists_count_callback(const void* data, uintptr_t* count) { + return details::catch_exceptions([&]() { + details::check_pointer(count); + *count = model_base(data)->requested_pair_lists().size(); + }); + } + + static mta_status_t requested_pair_list_callback(const void* data, uintptr_t index, mta_string_t* pair_options_json) { + return details::catch_exceptions([&]() { + details::check_pointer(pair_options_json); + auto options = model_base(data)->requested_pair_lists(); + if (index >= options.size()) { + throw Error("pair list request index out of bounds"); + } + *pair_options_json = mta_string_create(options[index].to_json().c_str()); + details::check_pointer(*pair_options_json); + }); + } + + static mta_status_t requested_inputs_count_callback(const void* data, uintptr_t* count) { + return details::catch_exceptions([&]() { + details::check_pointer(count); + *count = model_base(data)->requested_inputs().size(); + }); + } + + static mta_status_t requested_input_callback(const void* data, uintptr_t index, mta_string_t* input_json) { + return details::catch_exceptions([&]() { + details::check_pointer(input_json); + auto inputs = model_base(data)->requested_inputs(); + if (index >= inputs.size()) { + throw Error("input request index out of bounds"); + } + *input_json = mta_string_create(inputs[index].to_json().c_str()); + details::check_pointer(*input_json); + }); + } + + static mta_status_t execute_callback( + void* data, + const mta_system_t* const* systems, + uintptr_t systems_count, + const mts_labels_t* selected_atoms, + const char* const* requested_outputs_json, + uintptr_t requested_outputs_count, + mts_tensormap_t** outputs, + uintptr_t outputs_count + ) { + return details::catch_exceptions([&]() { + if (systems_count != 0) { + details::check_pointer(systems); + } + if (requested_outputs_count != 0) { + details::check_pointer(requested_outputs_json); + } + if (outputs_count != 0) { + details::check_pointer(outputs); + } + if (requested_outputs_count != outputs_count) { + throw Error("expected one output storage slot for each requested output"); + } + + auto system_views = std::vector(); + system_views.reserve(systems_count); + for (uintptr_t i=0; i(); + cxx_systems.reserve(system_views.size()); + for (const auto& system: system_views) { + cxx_systems.push_back(&system); + } + + auto requested_outputs = std::vector(); + requested_outputs.reserve(requested_outputs_count); + for (uintptr_t i=0; iexecute(cxx_systems, selected_atoms, requested_outputs); + if (result.size() != outputs_count) { + throw Error("model returned the wrong number of outputs"); + } + + for (uintptr_t i=0; i - #include #include #include @@ -25,15 +23,47 @@ class Plugin { /// Load a model from `load_from`, using the provided key/value options. virtual Model load_model( const std::string& load_from, - const std::vector& options + const std::vector& options = {} ) = 0; }; +/// Handle to a plugin registered in metatomic's global plugin registry. +class PluginHandle final { +public: + explicit PluginHandle(std::string name): name_(std::move(name)) {} + + /// Name used to identify this plugin. + const std::string& name() const { + return name_; + } + + /// Load a model from `load_from`, using the provided key/value options. + Model load_model( + const std::string& load_from, + const std::vector& options = {} + ) const { + auto c_options = details::to_c_options(options); + + auto model = mta_model_t{}; + details::check_status(mta_load_model( + name_.c_str(), + load_from.c_str(), + c_options.data(), + c_options.size(), + &model + )); + + return Model(model); + } + +private: + std::string name_; +}; + namespace details { template struct PluginRegistration { static PluginT* plugin; - static const char* name; static mta_status_t load_model( const char* load_from, @@ -57,9 +87,6 @@ namespace details { template PluginT* PluginRegistration::plugin = nullptr; - - template - const char* PluginRegistration::name = nullptr; } // namespace details /// Register a C++ plugin. @@ -75,47 +102,32 @@ void register_plugin(PluginT& plugin) { details::PluginRegistration::plugin = &plugin; const auto name = plugin.name(); - // The C plugin registry keeps this pointer; allocate stable process-lifetime storage. - auto* name_storage = new char[name.size() + 1]; - std::memcpy(name_storage, name.c_str(), name.size() + 1); - details::PluginRegistration::name = name_storage; auto c_plugin = mta_plugin_t{ - details::PluginRegistration::name, + name.c_str(), &details::PluginRegistration::load_model, }; mta_register_plugin(c_plugin); } -/// Register a raw C plugin. -inline void register_plugin(mta_plugin_t plugin) { - mta_register_plugin(plugin); -} - /// Load a plugin dynamic library from the given path. inline void load_plugin(const std::string& path) { details::check_status(mta_load_plugin(path.c_str())); } +/// Get a handle to a plugin in metatomic's global plugin registry. +inline PluginHandle plugin(const std::string& name) { + return PluginHandle(name); +} + /// Load a model using the given plugin. inline Model load_model( const std::string& plugin_name, const std::string& load_from, const std::vector& options = {} ) { - auto c_options = details::to_c_options(options); - - auto model = mta_model_t{}; - details::check_status(mta_load_model( - plugin_name.c_str(), - load_from.c_str(), - c_options.data(), - c_options.size(), - &model - )); - - return Model(model); + return plugin(plugin_name).load_model(load_from, options); } /// Load a model, letting metatomic pick the plugin. diff --git a/metatomic-core/include/metatomic/system.hpp b/metatomic-core/include/metatomic/system.hpp index 5fde00f3..55724d52 100644 --- a/metatomic-core/include/metatomic/system.hpp +++ b/metatomic-core/include/metatomic/system.hpp @@ -23,7 +23,7 @@ class System final { DLManagedTensorVersioned* positions, DLManagedTensorVersioned* cell, DLManagedTensorVersioned* pbc - ): system_(nullptr) { + ): system_(nullptr), is_view_(false) { details::check_status(mta_system_create( length_unit.c_str(), types, @@ -36,7 +36,7 @@ class System final { } ~System() { - if (system_ != nullptr) { + if (system_ != nullptr && !is_view_) { (void)mta_system_free(system_); } } @@ -44,17 +44,19 @@ class System final { System(const System&) = delete; System& operator=(const System&) = delete; - System(System&& other) noexcept: system_(nullptr) { + System(System&& other) noexcept: system_(nullptr), is_view_(true) { *this = std::move(other); } System& operator=(System&& other) noexcept { - if (system_ != nullptr) { + if (system_ != nullptr && !is_view_) { (void)mta_system_free(system_); } system_ = other.system_; + is_view_ = other.is_view_; other.system_ = nullptr; + other.is_view_ = true; return *this; } @@ -180,7 +182,12 @@ class System final { /// Take ownership of a raw `mta_system_t*`. static System unsafe_from_ptr(mta_system_t* system) { - return System(system); + return System(system, false); + } + + /// Create a non-owning view of a raw `mta_system_t*`. + static System unsafe_view_from_ptr(const mta_system_t* system) { + return System(const_cast(system), true); } /// Release the raw `mta_system_t*` without freeing it. @@ -191,7 +198,7 @@ class System final { } private: - explicit System(mta_system_t* system): system_(system) {} + explicit System(mta_system_t* system, bool is_view): system_(system), is_view_(is_view) {} DLPackTensor data(mta_system_data_kind request) const { DLManagedTensorVersioned* data = nullptr; @@ -201,6 +208,7 @@ class System final { } mta_system_t* system_; + bool is_view_; }; } // namespace metatomic From f8c10b95d3566e339a9572fc8a17d60098a815f2 Mon Sep 17 00:00:00 2001 From: frostedoyster Date: Sun, 31 May 2026 08:48:44 +0200 Subject: [PATCH 09/10] Update to new C API --- metatomic-core/include/metatomic/model.hpp | 115 +++++++------------- metatomic-core/include/metatomic/plugin.hpp | 34 +++--- metatomic-core/include/metatomic/system.hpp | 107 +++++++++++++----- metatomic-core/include/metatomic/utils.hpp | 36 ------ 4 files changed, 137 insertions(+), 155 deletions(-) diff --git a/metatomic-core/include/metatomic/model.hpp b/metatomic-core/include/metatomic/model.hpp index ea0592dc..608f232b 100644 --- a/metatomic-core/include/metatomic/model.hpp +++ b/metatomic-core/include/metatomic/model.hpp @@ -7,6 +7,7 @@ #include #include +#include #include "./metadata.hpp" #include "./system.hpp" @@ -65,10 +66,8 @@ class Model final { model_.unload = &Model::unload_callback; model_.metadata = &Model::metadata_callback; model_.supported_outputs = &Model::supported_outputs_callback; - model_.requested_pair_lists_count = &Model::requested_pair_lists_count_callback; - model_.requested_pair_list = &Model::requested_pair_list_callback; - model_.requested_inputs_count = &Model::requested_inputs_count_callback; - model_.requested_input = &Model::requested_input_callback; + model_.requested_pair_lists = &Model::requested_pair_lists_callback; + model_.requested_inputs = &Model::requested_inputs_callback; model_.execute_inner = &Model::execute_callback; } @@ -137,58 +136,38 @@ class Model final { return outputs; } - /// Get all pair lists requested by this model, each one serialized as JSON. - std::vector requested_pair_lists_json() const { - this->check_callback(model_.requested_pair_lists_count, "requested_pair_lists_count"); - this->check_callback(model_.requested_pair_list, "requested_pair_list"); + /// Get all pair lists requested by this model serialized as a JSON array. + std::string requested_pair_lists_json() const { + this->check_callback(model_.requested_pair_lists, "requested_pair_lists"); - uintptr_t count = 0; - details::check_status(model_.requested_pair_lists_count(model_.data, &count)); - - auto result = std::vector(); - result.reserve(count); - for (uintptr_t i=0; i requested_pair_lists() const { auto result = std::vector(); - for (const auto& options: this->requested_pair_lists_json()) { - result.push_back(PairListOptions::from_json(options)); + for (const auto& options: nlohmann::json::parse(this->requested_pair_lists_json())) { + result.push_back(PairListOptions::from_json(options.dump())); } return result; } - /// Get all custom inputs requested by this model, each one serialized as JSON. - std::vector requested_inputs_json() const { - this->check_callback(model_.requested_inputs_count, "requested_inputs_count"); - this->check_callback(model_.requested_input, "requested_input"); - - uintptr_t count = 0; - details::check_status(model_.requested_inputs_count(model_.data, &count)); + /// Get all custom inputs requested by this model serialized as a JSON array. + std::string requested_inputs_json() const { + this->check_callback(model_.requested_inputs, "requested_inputs"); - auto result = std::vector(); - result.reserve(count); - for (uintptr_t i=0; i requested_inputs() const { auto result = std::vector(); - for (const auto& input: this->requested_inputs_json()) { - result.push_back(Quantity::from_json(input)); + for (const auto& input: nlohmann::json::parse(this->requested_inputs_json())) { + result.push_back(Quantity::from_json(input.dump())); } return result; } @@ -199,7 +178,8 @@ class Model final { std::vector execute( const std::vector& systems, const metatensor::Labels* selected_atoms, - const std::vector& requested_outputs + const std::vector& requested_outputs, + bool check_consistency = true ) { this->check_valid(); @@ -227,6 +207,7 @@ class Model final { selected_atoms == nullptr ? nullptr : selected_atoms->as_mts_labels_t(), c_requested_outputs.data(), c_requested_outputs.size(), + check_consistency, raw_outputs.data(), raw_outputs.size() )); @@ -255,9 +236,10 @@ class Model final { /// Execute this model on all atoms. std::vector execute( const std::vector& systems, - const std::vector& requested_outputs + const std::vector& requested_outputs, + bool check_consistency = true ) { - return this->execute(systems, nullptr, requested_outputs); + return this->execute(systems, nullptr, requested_outputs, check_consistency); } /// Get the underlying `mta_model_t`. @@ -281,10 +263,8 @@ class Model final { model.unload = nullptr; model.metadata = nullptr; model.supported_outputs = nullptr; - model.requested_pair_lists_count = nullptr; - model.requested_pair_list = nullptr; - model.requested_inputs_count = nullptr; - model.requested_input = nullptr; + model.requested_pair_lists = nullptr; + model.requested_inputs = nullptr; model.execute_inner = nullptr; return model; } @@ -321,41 +301,29 @@ class Model final { }); } - static mta_status_t requested_pair_lists_count_callback(const void* data, uintptr_t* count) { - return details::catch_exceptions([&]() { - details::check_pointer(count); - *count = model_base(data)->requested_pair_lists().size(); - }); - } - - static mta_status_t requested_pair_list_callback(const void* data, uintptr_t index, mta_string_t* pair_options_json) { + static mta_status_t requested_pair_lists_callback(const void* data, mta_string_t* pair_options_json) { return details::catch_exceptions([&]() { details::check_pointer(pair_options_json); - auto options = model_base(data)->requested_pair_lists(); - if (index >= options.size()) { - throw Error("pair list request index out of bounds"); + auto options = nlohmann::json::array(); + for (const auto& option: model_base(data)->requested_pair_lists()) { + options.push_back(nlohmann::json::parse(option.to_json())); } - *pair_options_json = mta_string_create(options[index].to_json().c_str()); - details::check_pointer(*pair_options_json); - }); - } - static mta_status_t requested_inputs_count_callback(const void* data, uintptr_t* count) { - return details::catch_exceptions([&]() { - details::check_pointer(count); - *count = model_base(data)->requested_inputs().size(); + *pair_options_json = mta_string_create(options.dump().c_str()); + details::check_pointer(*pair_options_json); }); } - static mta_status_t requested_input_callback(const void* data, uintptr_t index, mta_string_t* input_json) { + static mta_status_t requested_inputs_callback(const void* data, mta_string_t* inputs_json) { return details::catch_exceptions([&]() { - details::check_pointer(input_json); - auto inputs = model_base(data)->requested_inputs(); - if (index >= inputs.size()) { - throw Error("input request index out of bounds"); + details::check_pointer(inputs_json); + auto inputs = nlohmann::json::array(); + for (const auto& input: model_base(data)->requested_inputs()) { + inputs.push_back(nlohmann::json::parse(input.to_json())); } - *input_json = mta_string_create(inputs[index].to_json().c_str()); - details::check_pointer(*input_json); + + *inputs_json = mta_string_create(inputs.dump().c_str()); + details::check_pointer(*inputs_json); }); } @@ -399,6 +367,7 @@ class Model final { auto requested_outputs = std::vector(); requested_outputs.reserve(requested_outputs_count); for (uintptr_t i=0; i #include #include -#include #include @@ -23,7 +22,7 @@ class Plugin { /// Load a model from `load_from`, using the provided key/value options. virtual Model load_model( const std::string& load_from, - const std::vector& options = {} + const std::string& options_json = "{}" ) = 0; }; @@ -40,16 +39,13 @@ class PluginHandle final { /// Load a model from `load_from`, using the provided key/value options. Model load_model( const std::string& load_from, - const std::vector& options = {} + const std::string& options_json = "{}" ) const { - auto c_options = details::to_c_options(options); - auto model = mta_model_t{}; details::check_status(mta_load_model( name_.c_str(), load_from.c_str(), - c_options.data(), - c_options.size(), + options_json.c_str(), &model )); @@ -64,11 +60,11 @@ namespace details { template struct PluginRegistration { static PluginT* plugin; + static std::string name; static mta_status_t load_model( const char* load_from, - const mta_kv_pair_t* options, - uintptr_t options_count, + const char* options_json, mta_model_t* model ) { return details::catch_exceptions([&]() { @@ -77,7 +73,7 @@ namespace details { auto loaded = plugin->load_model( load_from == nullptr ? "" : load_from, - details::from_c_options(options, options_count) + options_json == nullptr ? "{}" : options_json ); *model = loaded.release(); @@ -87,6 +83,9 @@ namespace details { template PluginT* PluginRegistration::plugin = nullptr; + + template + std::string PluginRegistration::name; } // namespace details /// Register a C++ plugin. @@ -101,10 +100,10 @@ void register_plugin(PluginT& plugin) { ); details::PluginRegistration::plugin = &plugin; - const auto name = plugin.name(); + details::PluginRegistration::name = plugin.name(); auto c_plugin = mta_plugin_t{ - name.c_str(), + details::PluginRegistration::name.c_str(), &details::PluginRegistration::load_model, }; @@ -125,24 +124,21 @@ inline PluginHandle plugin(const std::string& name) { inline Model load_model( const std::string& plugin_name, const std::string& load_from, - const std::vector& options = {} + const std::string& options_json = "{}" ) { - return plugin(plugin_name).load_model(load_from, options); + return plugin(plugin_name).load_model(load_from, options_json); } /// Load a model, letting metatomic pick the plugin. inline Model load_model( const std::string& load_from, - const std::vector& options = {} + const std::string& options_json = "{}" ) { - auto c_options = details::to_c_options(options); - auto model = mta_model_t{}; details::check_status(mta_load_model( nullptr, load_from.c_str(), - c_options.data(), - c_options.size(), + options_json.c_str(), &model )); diff --git a/metatomic-core/include/metatomic/system.hpp b/metatomic-core/include/metatomic/system.hpp index 55724d52..15d136b8 100644 --- a/metatomic-core/include/metatomic/system.hpp +++ b/metatomic-core/include/metatomic/system.hpp @@ -5,7 +5,9 @@ #include #include +#include +#include "./metadata.hpp" #include "./utils.hpp" namespace metatomic { @@ -97,47 +99,97 @@ class System final { /// Add a new pair list in this system. /// /// Ownership of `pairs` is transferred to the C API. - void set_pairs(const std::string& options, mts_block_t* pairs) { - details::check_status(mta_system_set_pairs(system_, options.c_str(), pairs)); + void add_pairs(const PairListOptions& options, mts_block_t* pairs) { + this->add_pairs(options.to_json(), pairs); + } + + /// Add a new pair list in this system. + /// + /// Ownership of `pairs` is transferred to the C API. + void add_pairs(const std::string& options_json, mts_block_t* pairs) { + details::check_status(mta_system_add_pairs(system_, options_json.c_str(), pairs)); + } + + /// Add a new pair list in this system. + /// + /// Ownership of `pairs` is transferred to the C API. + void set_pairs(const PairListOptions& options, mts_block_t* pairs) { + this->add_pairs(options, pairs); + } + + /// Add a new pair list in this system. + /// + /// Ownership of `pairs` is transferred to the C API. + void set_pairs(const std::string& options_json, mts_block_t* pairs) { + this->add_pairs(options_json, pairs); } /// Retrieve a previously stored pair list with the given options. - const mts_block_t* pairs_raw(const std::string& options) const { + const mts_block_t* pairs_raw(const PairListOptions& options) const { + return this->pairs_raw(options.to_json()); + } + + /// Retrieve a previously stored pair list with the given options. + const mts_block_t* pairs_raw(const std::string& options_json) const { const mts_block_t* pairs = nullptr; - details::check_status(mta_system_get_pairs(system_, options.c_str(), &pairs)); + details::check_status(mta_system_get_pairs(system_, options_json.c_str(), &pairs)); details::check_pointer(pairs); return pairs; } /// Retrieve a previously stored pair list with the given options as a /// non-owning metatensor view. - metatensor::TensorBlock pairs(const std::string& options) const { + metatensor::TensorBlock pairs(const PairListOptions& options) const { + return this->pairs(options.to_json()); + } + + /// Retrieve a previously stored pair list with the given options as a + /// non-owning metatensor view. + metatensor::TensorBlock pairs(const std::string& options_json) const { return metatensor::TensorBlock::unsafe_view_from_ptr( - const_cast(this->pairs_raw(options)) + const_cast(this->pairs_raw(options_json)) ); } + /// Get the options for all pair lists registered with this `System`, + /// serialized as a JSON array. + std::string known_pairs_json() const { + mta_string_t pairs_options = nullptr; + details::check_status(mta_system_known_pairs(system_, &pairs_options)); + return String(pairs_options).str(); + } + /// Get the options for all pair lists registered with this `System`. - std::vector pairs_options() const { - uintptr_t count = 0; - details::check_status(mta_system_pairs_count(system_, &count)); + std::vector known_pairs() const { + auto result = std::vector(); + for (const auto& options: nlohmann::json::parse(this->known_pairs_json())) { + result.push_back(PairListOptions::from_json(options.dump())); + } + return result; + } + /// Get the options for all pair lists registered with this `System`, + /// each one serialized as JSON. + std::vector pairs_options() const { auto result = std::vector(); - result.reserve(count); - for (uintptr_t i=0; iknown_pairs_json())) { + result.push_back(options.dump()); } - return result; } + /// Add custom data to this system. + /// + /// Ownership of `data` is transferred to the C API. + void add_data(const std::string& name, mts_tensormap_t* data) { + details::check_status(mta_system_add_custom_data(system_, name.c_str(), data)); + } + /// Add custom data to this system. /// /// Ownership of `data` is transferred to the C API. void set_data(const std::string& name, mts_tensormap_t* data) { - details::check_status(mta_system_set_custom_data(system_, name.c_str(), data)); + this->add_data(name, data); } /// Retrieve custom data stored in this system. @@ -151,19 +203,20 @@ class System final { } /// Get the names of all custom data registered with this `System`. - std::vector data_names() const { - uintptr_t count = 0; - details::check_status(mta_system_data_count(system_, &count)); + std::string known_data_json() const { + mta_string_t names = nullptr; + details::check_status(mta_system_known_custom_data(system_, &names)); + return String(names).str(); + } - auto result = std::vector(); - result.reserve(count); - for (uintptr_t i=0; i known_data() const { + return nlohmann::json::parse(this->known_data_json()).get>(); + } - return result; + /// Get the names of all custom data registered with this `System`. + std::vector data_names() const { + return this->known_data(); } /// Get the underlying `mta_system_t` pointer. diff --git a/metatomic-core/include/metatomic/utils.hpp b/metatomic-core/include/metatomic/utils.hpp index 9ff181d1..056e1822 100644 --- a/metatomic-core/include/metatomic/utils.hpp +++ b/metatomic-core/include/metatomic/utils.hpp @@ -7,7 +7,6 @@ #include #include #include -#include #include @@ -20,12 +19,6 @@ class Error: public std::runtime_error { explicit Error(const std::string& message): std::runtime_error(message) {} }; -/// Key/value pair used when loading models from plugins. -struct KeyValuePair { - std::string key; - std::string value; -}; - /// RAII wrapper around a `DLManagedTensorVersioned*`. /// /// This owns the DLPack managed tensor object, and calls its deleter when the @@ -170,35 +163,6 @@ namespace details { throw Error(message == nullptr ? "received a null pointer from the metatomic C API" : message); } - - inline std::vector to_c_options(const std::vector& options) { - auto c_options = std::vector(); - c_options.reserve(options.size()); - - for (const auto& option: options) { - c_options.push_back(mta_kv_pair_t{option.key.c_str(), option.value.c_str()}); - } - - return c_options; - } - - inline std::vector from_c_options(const mta_kv_pair_t* options, uintptr_t count) { - auto result = std::vector(); - result.reserve(count); - - if (count != 0) { - check_pointer(options); - } - - for (uintptr_t i=0; i Date: Sun, 31 May 2026 09:21:19 +0200 Subject: [PATCH 10/10] Rename model classes for consistency with current `metatomic.torch` --- metatomic-core/include/metatomic/model.hpp | 44 ++++++++++----------- metatomic-core/include/metatomic/plugin.hpp | 16 ++++---- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/metatomic-core/include/metatomic/model.hpp b/metatomic-core/include/metatomic/model.hpp index 608f232b..ee140760 100644 --- a/metatomic-core/include/metatomic/model.hpp +++ b/metatomic-core/include/metatomic/model.hpp @@ -16,9 +16,9 @@ namespace metatomic { /// Abstract base class for atomistic models implemented in C++. -class ModelBase { +class ModelInterface { public: - virtual ~ModelBase() = default; + virtual ~ModelInterface() = default; /// Get metadata about this model. virtual ModelMetadata metadata() const = 0; @@ -45,44 +45,44 @@ class ModelBase { }; /// RAII wrapper around a `mta_model_t`. -class Model final { +class AtomisticModel final { public: /// Create an empty, invalid model. - Model() { + AtomisticModel() { model_ = empty_model(); } /// Take ownership of a raw `mta_model_t`. - explicit Model(mta_model_t model): model_(model) {} + explicit AtomisticModel(mta_model_t model): model_(model) {} /// Create a C API model wrapping a C++ model implementation. - explicit Model(std::unique_ptr model) { + explicit AtomisticModel(std::unique_ptr model) { if (model == nullptr) { - throw Error("can not create a metatomic::Model from a null ModelBase"); + throw Error("can not create a metatomic::AtomisticModel from a null ModelInterface"); } model_ = empty_model(); model_.data = model.release(); - model_.unload = &Model::unload_callback; - model_.metadata = &Model::metadata_callback; - model_.supported_outputs = &Model::supported_outputs_callback; - model_.requested_pair_lists = &Model::requested_pair_lists_callback; - model_.requested_inputs = &Model::requested_inputs_callback; - model_.execute_inner = &Model::execute_callback; + model_.unload = &AtomisticModel::unload_callback; + model_.metadata = &AtomisticModel::metadata_callback; + model_.supported_outputs = &AtomisticModel::supported_outputs_callback; + model_.requested_pair_lists = &AtomisticModel::requested_pair_lists_callback; + model_.requested_inputs = &AtomisticModel::requested_inputs_callback; + model_.execute_inner = &AtomisticModel::execute_callback; } - ~Model() { + ~AtomisticModel() { this->reset_noexcept(); } - Model(const Model&) = delete; - Model& operator=(const Model&) = delete; + AtomisticModel(const AtomisticModel&) = delete; + AtomisticModel& operator=(const AtomisticModel&) = delete; - Model(Model&& other) noexcept: Model() { + AtomisticModel(AtomisticModel&& other) noexcept: AtomisticModel() { *this = std::move(other); } - Model& operator=(Model&& other) noexcept { + AtomisticModel& operator=(AtomisticModel&& other) noexcept { if (this != &other) { this->reset_noexcept(); model_ = other.model_; @@ -269,9 +269,9 @@ class Model final { return model; } - static ModelBase* model_base(const void* data) { + static ModelInterface* model_base(const void* data) { details::check_pointer(data); - return static_cast(const_cast(data)); + return static_cast(const_cast(data)); } static mta_status_t unload_callback(void* data) { @@ -392,7 +392,7 @@ class Model final { void check_valid() const { if (model_.data == nullptr) { - throw Error("can not use an empty metatomic::Model"); + throw Error("can not use an empty metatomic::AtomisticModel"); } } @@ -400,7 +400,7 @@ class Model final { void check_callback(Callback callback, const char* name) const { this->check_valid(); if (callback == nullptr) { - throw Error("metatomic::Model does not implement " + std::string(name)); + throw Error("metatomic::AtomisticModel does not implement " + std::string(name)); } } diff --git a/metatomic-core/include/metatomic/plugin.hpp b/metatomic-core/include/metatomic/plugin.hpp index a806e71d..f5aee069 100644 --- a/metatomic-core/include/metatomic/plugin.hpp +++ b/metatomic-core/include/metatomic/plugin.hpp @@ -19,8 +19,8 @@ class Plugin { /// Name used to identify this plugin. virtual std::string name() const = 0; - /// Load a model from `load_from`, using the provided key/value options. - virtual Model load_model( + /// Load a model from `load_from`, using the provided JSON options. + virtual AtomisticModel load_model( const std::string& load_from, const std::string& options_json = "{}" ) = 0; @@ -36,8 +36,8 @@ class PluginHandle final { return name_; } - /// Load a model from `load_from`, using the provided key/value options. - Model load_model( + /// Load a model from `load_from`, using the provided JSON options. + AtomisticModel load_model( const std::string& load_from, const std::string& options_json = "{}" ) const { @@ -49,7 +49,7 @@ class PluginHandle final { &model )); - return Model(model); + return AtomisticModel(model); } private: @@ -121,7 +121,7 @@ inline PluginHandle plugin(const std::string& name) { } /// Load a model using the given plugin. -inline Model load_model( +inline AtomisticModel load_model( const std::string& plugin_name, const std::string& load_from, const std::string& options_json = "{}" @@ -130,7 +130,7 @@ inline Model load_model( } /// Load a model, letting metatomic pick the plugin. -inline Model load_model( +inline AtomisticModel load_model( const std::string& load_from, const std::string& options_json = "{}" ) { @@ -142,7 +142,7 @@ inline Model load_model( &model )); - return Model(model); + return AtomisticModel(model); } } // namespace metatomic