Architecture Overview

rgpot follows a metatensor-style layered architecture: a Rust core defines the fundamental types and RPC infrastructure, exposed via a C ABI (generated by cbindgen), with hand-written C++ RAII wrappers on top. Existing C++ potentials remain callable through an FFI callback mechanism.

Layer Diagram

+---------------------------------------------+
|  C++ Consumer Layer (RAII wrappers)          |
|  include/rgpot/*.hpp                         |
+---------------------------------------------+
|  C ABI (generated by cbindgen)               |
|  rgpot-core/include/rgpot.h                  |
+---------------------------------------------+
|  Rust Core                                   |
|  rgpot-core/src/                             |
|  - Types (ForceInput, ForceOut)              |
|  - Potential trait + callback dispatch        |
|  - Cap'n Proto RPC client/server             |
|  - Error handling (status codes + TLS msg)   |
+---------------------------------------------+
        | FFI callbacks
+---------------------------------------------+
|  C++ Potentials                              |
|  LJ, CuH2, XTBPot, TBLitePot, MetatomicPot |
+---------------------------------------------+

Layers in Detail

Rust Core (rgpot-core/)

The Rust crate is the single source of truth for the public API. It defines:

rgpot_force_input_t and rgpot_force_out_t

#[repr(C)] structs mirroring

the legacy C++ ForceInput / ForceOut from ForceStructs.hpp. These are the canonical data exchange types across the FFI boundary.

rgpot_status_t

An enum of status codes (RGPOT_SUCCESS,

RGPOT_INVALID_PARAMETER, RGPOT_INTERNAL_ERROR, RGPOT_RPC_ERROR, RGPOT_BUFFER_SIZE_ERROR). Every extern "C" function returns one of these.

rgpot_potential_t

An opaque handle wrapping a C function pointer callback

plus a void* user_data. This is how C++ potentials are registered into the Rust core without the core knowing the concrete type.

rgpot_last_error()

Thread-local last error message.

On any non-success return, the caller can retrieve a human-readable string describing the failure.

catch_unwind wrapper

Every extern "C" function boundary catches Rust

panics and converts them to RGPOT_INTERNAL_ERROR plus an error message. This prevents undefined behaviour from unwinding across the FFI boundary.

Feature Gates

The crate uses Cargo feature flags to keep the core minimal:

rpc

Enables Cap’n Proto RPC client and server.

Adds dependencies on capnp, capnp-rpc, and tokio. The RPC module reuses the existing Potentials.capnp schema shared with the C+

  • server.

cache

Reserved for future caching integration.

gen-header

Enables C header generation via cbindgen.

Used only during development; run pixi r gen-header to regenerate include/ rgpot.h.

Cap’n Proto Schema Sharing

The Cap’n Proto schema (Potentials.capnp) is bundled inside the Rust crate at rgpot-core/schema/Potentials.capnp. This is the canonical copy used when building from crates.io. In the monorepo, build.rs also checks CppCore/rgpot/rpc/Potentials.capnp as a fallback. The Cap’n Proto wire format is stable across language implementations, so Rust clients and C++ servers (or vice versa) are fully interoperable.

C ABI (rgpot-core/include/rgpot.h)

The C header is auto-generated by cbindgen from the Rust source. Run pixi r gen-header (which invokes cargo build --features gen-header) to regenerate it. This header is the contract between the Rust core and all C/C++ consumers. It contains:

  • Type definitions for all #[repr(C)] structs and enums.

  • Forward declarations for opaque handles (rgpot_potential_t,

rgpot_rpc_client_t).

  • Function declarations for the entire public API.

  • Doxygen-style documentation extracted from Rust doc comments.

  • #ifdef RGPOT_HAS_RPC guards around RPC-specific declarations.

This header is never edited by hand; any changes to the public API are made in the Rust source and regenerated.

C++ RAII Wrappers (include/rgpot/)

Hand-written C++ headers provide idiomatic wrappers around the C API:

errors.hpp

Maps rgpot_status_t codes to rgpot::Error exceptions via

details::check_status(). Internally calls rgpot_last_error() to populate the exception message.

types.hpp

Provides rgpot::InputSpec (a lightweight view over borrowed

input data) and rgpot::CalcResult (owns the force buffer and stores energy/ variance). These are distinct from the legacy rgpot::ForceInput / rgpot::ForceOut types in ForceStructs.hpp to avoid name collisions.

potential.hpp

The rgpot::PotentialHandle class is a move-only RAII

wrapper around rgpot_potential_t*. Key methods:

calculate(const InputSpec&)

Invokes the potential and returns a

CalcResult. Throws rgpot::Error on failure.

from_impl<Impl>(impl)

Template factory that wraps any C++ potential object

with a compatible forceImpl method. Generates a type-safe trampoline function automatically.

from_callback(fn, user_data, free_fn)

Low-level factory taking an explicit

C function pointer.

rpc_client.hpp

RAII wrapper around rgpot_rpc_client_t*.

Connects to a remote server on construction and provides calculate() with the same InputSpec / CalcResult interface. Guarded by #ifdef RGPOT_HAS_RPC.

rgpot.hpp

Aggregate header that includes everything.

C++ Potentials

The CppCore/rgpot/ directory contains all potential implementations using the CRTP-based Potential<Derived> template. Each potential provides void forceImpl(const ForceInput&, ForceOut*) const. To use them through the Rust core, pass them to PotentialHandle::from_impl<>().

Built-in Potentials

LJPot

Standard 12-6 Lennard-Jones (always available).

LJClusterPot

12-6 Lennard-Jones on free boundaries, for isolated

clusters; the cell is ignored. Configurable via LJClusterConfig (u0, cutoff, psi).

MorsePot

Pairwise Morse with a shifted cutoff, platinum parameters by

default. Configurable via MorseConfig (De, a, re, cutoff).

ZBLPot

Screened nuclear repulsion (Ziegler-Biersack-Littmark) with the

LAMMPS switching function, on free boundaries. Configurable via ZBLConfig (cut_inner, cut_global); the constructor throws std::invalid_argument unless 0 < cut_inner < cut_global.

rgpot::fortranpots::CuH2Pot

Copper-Hydrogen EAM.

One of the Fortran 2018 kernels under CppCore/rgpot/fortran/, declared in FortranPots.hpp and built when with_fortran_pots finds a Fortran compiler.

Feature-gated Potentials

XTBPot (-Dwith_xtb=true)

GFN tight-binding methods (GFNFF, GFN0-xTB,

GFN1-xTB, GFN2-xTB) via the xtb C API. Configurable via XTBConfig struct (method, accuracy, electronic temperature, charge, spin). Not thread-safe on a single instance; create separate instances for parallel use.

TBLitePot (-Dwith_tblite=true)

GFN tight-binding methods (GFN1, GFN2,

IPEA1) via the tblite C API. Same config pattern as XTBPot. Lighter weight than xtb but fewer methods.

MetatomicPot (-Dwith_metatomic=true)

Pre-trained ML atomistic models

via metatomic/PyTorch, loaded directly as TorchScript (.pt) in C++ with no Python at evaluation time. Uses vesin 0.5+ for neighbor lists and PyTorch autograd for forces. Pass CPU as VesinDevice{VesinCPU, 0} (struct { VesinDeviceKind type; int device_id; }; VesinCPU alone is only the kind enum). Thread-safe via internal mutex; caches the atomic-types tensor across geometry steps. Configurable device (CPU/CUDA), dtype override, length unit, consistency checks, and torch_determinism via MetatomicConfig. TorchDeterminismPolicy::Fast (default) does not touch process-global LibTorch state. Strict calls apply_torch_determinism_policy, which sets at::globalContext() deterministic algorithms (no warn-only fallback), pins scaled-dot-product attention to the math SDP backend (flash, memory-efficient, and cuDNN SDP off), disables TF32 for cuBLAS and cuDNN (full fp32 mantissas on Ampere+), forces deterministic cuDNN algorithms with benchmarking off, and fills uninitialized memory deterministically. Those flags are process-global: they affect every Torch user in the process, and Fast does not restore a previous Strict setting. CUDA host processes that need bit-stable cuBLAS must also export CUBLAS_WORKSPACE_CONFIG:4096:8= (or :16:8) before the first cuBLAS call — that is process environment, not at::globalContext() (see NVIDIA cuBLAS reproducibility and alertCuBLASConfigNotDeterministic). On the RPC server pass Metatomic:/path/to/model.pt. Build with the metatomicbld pixi env (linux-64; pulls torch, metatomic-torch, and vesin >=0.5.2,<0.6 from PyPI). CI job potentials_metatomic in ci- orchestrator.yml.

The pip product (rgpot on PyPI) does not link Metatomic into the Python extension. It dlopen=s a portable =libmetatomic_engine.so chosen from rgpot/lib/torch- X.Y/ for the installed torch major. Supported torch majors for bundled engines start at 2.7 (wheels ship 2.7– 2.13). See:

Python package (PyPI)

All feature-gated potentials are detected via pkg-config / cmake and controlled by Meson options.

NWChemPot (rgpot/NWChemPot/)

Always-built frontend (NWChemPot.cc) that serializes NWChemParams and dlopen=s =libnwchemc (via DynLib.hpp). Stable consumer C-ABI contract in nwchem_c_abi.h uses nwchemc_* symbols and flat Cap’n Proto message bytes; nwchem_c_abi_stub.c is a no-op ABI linked by the conformance test only. rgpot is a pure consumer: the engine implementation (NWChem embed + Fortran) lives in the split https://github.com/OmniPotentRPC/ nwchemc project, not in rgpot. RPC schema adds NWChemParams, PotentialConfig, and configure @1 on Potential; potserv <port> NWChem. Runtime: NWCHEMC_LIBRARY, RGPOT_NWCHEMC_ENGINE, NWCHEM_TOP. Details: CppCore/rgpot/NWChemPot/README.md.

Units (rgpot/units.hpp)

rgpot’s internal unit system is energy = eV, length = Angstrom, force = eV/Angstrom.

  • Compile-time constants (CODATA 2018): BOHR_TO_ANGSTROM, HARTREE_TO_EV,

HARTREE_BOHR_TO_EV_ANGSTROM, NEG_GRAD_TO_FORCE, Boltzmann constants, etc. Used by XTBPot/TBLitePot (Bohr/Hartree backends) and any caller that wants zero- overhead conversion.

  • Runtime expression parser (unit_conversion_factor(from, to)): Shunting-Yard

algorithm with SI dimensional analysis, derived from metatomic-torch (PR #173). Accepts compound expressions such as "kJ/mol", "eV/angstrom^3", or "(eV*u)^(1/2)". Throws std::invalid_argument on dimension mismatch or unknown tokens. validate_unit(quantity, unit) checks a unit against a known quantity (length, energy, force, pressure, momentum, mass, velocity, charge).

  • RPC negotiation: Cap’n Proto ForceInput carries optional lengthUnit and

energyUnit (default "angstrom" / "eV"). The server converts inbound positions/box into Angstrom before forceImpl, then converts energy and forces into the caller’s requested units on the response.

Error Handling Conventions

The project uses a two-tier error strategy:

  1. Across the FFI boundary :: Status codes (rgpot_status_t) plus a thread-local

error message (rgpot_last_error()). No exceptions cross FFI. Every Rust extern "C" function wraps its body in catch_unwind to convert panics to RGPOT_INTERNAL_ERROR.

  1. Within C++ :: The RAII wrappers call details::check_status() which maps non-

success codes to rgpot::Error exceptions. C++ consumers work with exceptions as usual.

  1. Within Rust :: Standard Result types internally, converted to status codes

at the extern "C" boundary.

Adding a New Potential

To register a new C++ potential with the Rust core:

  1. Implement the potential class inheriting from Potential<MyPot> as usual.

The class must provide void forceImpl(const ForceInput&, ForceOut*) const.

  1. Use the RAII wrapper to create a handle:

#include "rgpot/potential.hpp"
#include "my_pot/MyPot.hpp"

rgpot::MyPot my_pot;
auto handle = rgpot::PotentialHandle::from_impl(my_pot);
auto result = handle.calculate(input_spec);
double energy = result.energy();
  1. Alternatively, write a standalone extern "C" trampoline and call

rgpot_potential_new() directly from C:

rgpot_status_t my_trampoline(void *ud, const rgpot_force_input_t *in,
                             rgpot_force_out_t *out) {
    /* ... forward to your implementation ... */
    return RGPOT_SUCCESS;
}

rgpot_potential_t *pot = rgpot_potential_new(my_trampoline, my_data, NULL);

Build System Integration

Meson

The Rust crate is built via a custom_target in meson.build, gated behind the with_rust_core option:

meson setup bbdir -Dwith_rust_core=true
meson compile -C bbdir

Meson invokes cargo build and links the resulting librgpot_core.a into the project. The feature flags (rpc, cache) are forwarded from the corresponding Meson options.

Standalone Rust

The crate can also be built and tested independently:

cd rgpot-core
cargo build
cargo test
cargo build --features rpc  # with RPC support

Directory Reference

Path

Purpose

rgpot-core/Cargo.toml

Rust crate configuration.

rgpot-core/build.rs

capnp compilation (+ cbindgen when gen-header enabled).

rgpot-core/cbindgen.toml

cbindgen configuration.

rgpot-core/src/

Rust source (types, status, potential, capi, rpc).

rgpot-core/include/rgpot.h

Auto-generated C header.

include/rgpot/

Hand-written C++ RAII wrapper headers.

CppCore/rgpot/

Original C++ potentials and helpers.

rgpot-core/schema/Potentials.capnp

Bundled Cap’n Proto schema (canonical for crates.io).

CppCore/rgpot/rpc/Potentials.capnp

Shared Cap’n Proto schema (monorepo fallback).