{ "cells": [ { "cell_type": "markdown", "id": "b0c63677", "metadata": {}, "source": [ "# Deploying a trained model as an MDI engine: liquid argon over the MolSSI Driver Interface\n", "\n", "The [MolSSI Driver Interface](https://github.com/MolSSI-MDI/MDI_Library) (MDI) couples\n", "simulation codes through a small, standardized command protocol: a **driver** (LAMMPS,\n", "SEAMM, or a plain Python script) steers one or more **engines** (quantum-chemistry codes,\n", "ML potentials, ...) by sending commands like `>COORDS` and ` **Note:** the MDI library can only be initialized once per process. Restart the kernel\n", "> before re-running the notebook.\n" ] }, { "cell_type": "markdown", "id": "36a1483b", "metadata": {}, "source": [ "## 0. Setup\n", "\n", "`float32` on the GPU for training speed; we switch to `float64` for deployment and\n", "dynamics (same convention as the `*_argon_density_md` notebooks).\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "61444740", "metadata": { "execution": { "iopub.execute_input": "2026-08-04T20:52:15.730835Z", "iopub.status.busy": "2026-08-04T20:52:15.730714Z", "iopub.status.idle": "2026-08-04T20:52:17.604409Z", "shell.execute_reply": "2026-08-04T20:52:17.603538Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "xnn: 0.1.0 | device: cuda | NVIDIA A100 80GB PCIe\n" ] } ], "source": [ "# silence the expected warnings (TorchScript scripting + e3nn torch.load)\n", "import logging, warnings\n", "logging.getLogger(\"cuequivariance\").setLevel(logging.ERROR)\n", "warnings.filterwarnings(\"ignore\", category=UserWarning,\n", " message=\"The TorchScript type system doesn't support\")\n", "warnings.filterwarnings(\"ignore\", category=FutureWarning,\n", " message=\"You are using `torch.load` with `weights_only=False`\")\n", "warnings.filterwarnings(\"ignore\", message=\"Use thermalize_momenta\")\n", "\n", "import sys, time, subprocess\n", "import numpy as np\n", "import torch\n", "import matplotlib.pyplot as plt\n", "\n", "torch.set_default_dtype(torch.float32)\n", "torch.manual_seed(0)\n", "\n", "DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", "import xnn\n", "print(\"xnn:\", xnn.__version__, \"| device:\", DEVICE,\n", " \"|\", torch.cuda.get_device_name(0) if DEVICE == \"cuda\" else \"\")" ] }, { "cell_type": "markdown", "id": "dc7ab482", "metadata": {}, "source": [ "## 1. Load the `argon_md` hub dataset\n", "\n", "Periodic liquid-argon MD frames (400 atoms each) with reference energies, forces and\n", "stress in eV / angstrom, bundled with the repository so this loads offline. The\n", "`config_type=IsolatedAtom` reference frame is dropped by the hub builder; for argon the\n", "isolated-atom energy $E_0$ is zero by construction.\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "3caeb7f1", "metadata": { "execution": { "iopub.execute_input": "2026-08-04T20:52:17.607674Z", "iopub.status.busy": "2026-08-04T20:52:17.607347Z", "iopub.status.idle": "2026-08-04T20:52:18.658362Z", "shell.execute_reply": "2026-08-04T20:52:18.657484Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "train: 200 frames | test: 50 frames | 400 atoms/frame | species: [18]\n" ] } ], "source": [ "from xnn.common.data import load_dataset\n", "\n", "train_structs = load_dataset(\"argon_md\", split=\"train\")\n", "test_structs = load_dataset(\"argon_md\", split=\"test\")\n", "\n", "CUTOFF = 6.0\n", "SPECIES = sorted({int(z) for s in train_structs for z in s[\"atomic_numbers\"]})\n", "E0 = {18: 0.0}\n", "n_at = len(train_structs[0][\"atomic_numbers\"])\n", "print(f\"train: {len(train_structs)} frames | test: {len(test_structs)} frames | \"\n", " f\"{n_at} atoms/frame | species: {SPECIES}\")" ] }, { "cell_type": "markdown", "id": "770251d7", "metadata": {}, "source": [ "## 2. Train a small MACE\n", "\n", "Same architecture and optimizer settings as `../gnn/mace/mace_argon_train_test.ipynb`\n", "(2 interactions, 32 channels, $\\ell_\\mathrm{max}=3$, $L_\\mathrm{max}=1$, per-atom\n", "energy + force loss), but only 30 epochs: deployment, not accuracy, is the point here.\n", "The `Trainer` writes the best-validation checkpoint to `runs/mdi_argon/best.pt`; that\n", "single file (weights **and** config) is everything the MDI engine needs.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "b63c092d", "metadata": { "execution": { "iopub.execute_input": "2026-08-04T20:52:18.660774Z", "iopub.status.busy": "2026-08-04T20:52:18.660453Z", "iopub.status.idle": "2026-08-04T20:53:52.803090Z", "shell.execute_reply": "2026-08-04T20:53:52.802109Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "avg neighbours = 16.96\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 0 | train loss 1.6979e-01 | val loss 1.1223e-01\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 1 | train loss 4.2205e-02 | val loss 7.2734e-02\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 2 | train loss 2.1054e-02 | val loss 3.4279e-02\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 3 | train loss 9.7247e-03 | val loss 1.0851e-02\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 4 | train loss 4.1363e-03 | val loss 7.2410e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 5 | train loss 2.8743e-03 | val loss 7.0642e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 6 | train loss 2.2595e-03 | val loss 3.8075e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 7 | train loss 1.9452e-03 | val loss 3.3339e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 8 | train loss 1.4030e-03 | val loss 2.5998e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 9 | train loss 1.2531e-03 | val loss 2.1951e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 10 | train loss 1.0680e-03 | val loss 2.0312e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 11 | train loss 1.0462e-03 | val loss 2.1602e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 12 | train loss 1.1194e-03 | val loss 1.7872e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 13 | train loss 9.2078e-04 | val loss 1.5490e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 14 | train loss 1.3386e-03 | val loss 2.6256e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 15 | train loss 1.1220e-03 | val loss 1.8298e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 16 | train loss 1.1244e-03 | val loss 1.3552e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 17 | train loss 1.0588e-03 | val loss 1.3890e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 18 | train loss 1.0304e-03 | val loss 1.3742e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 19 | train loss 1.0797e-03 | val loss 1.7713e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 20 | train loss 1.5142e-03 | val loss 2.6627e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 21 | train loss 1.0494e-03 | val loss 1.4176e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 22 | train loss 1.2966e-03 | val loss 1.6298e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 23 | train loss 8.5647e-04 | val loss 1.1694e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 24 | train loss 8.8634e-04 | val loss 1.1175e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 25 | train loss 6.7053e-04 | val loss 8.2944e-04\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 26 | train loss 6.8954e-04 | val loss 8.7361e-04\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 27 | train loss 9.5956e-04 | val loss 8.0108e-04\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 28 | train loss 1.3346e-03 | val loss 3.1894e-03\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "epoch 29 | train loss 1.4704e-03 | val loss 1.0907e-03\n", "\n", "trained 30 epochs in 94 s -> runs/mdi_argon/best.pt\n" ] } ], "source": [ "from xnn.common.config import from_dict\n", "from xnn.common.data import AtomicDataset\n", "from xnn.common.train import Trainer\n", "\n", "# average number of neighbours (MACE message normalization), from a few frames\n", "probe = AtomicDataset(train_structs[:10], CUTOFF)\n", "graphs = [probe[i] for i in range(len(probe))]\n", "LAMBDA = float(sum(g.num_edges for g in graphs) / sum(g.num_nodes for g in graphs))\n", "print(f\"avg neighbours = {LAMBDA:.2f}\")\n", "\n", "core = from_dict({\n", " \"model\": {\"name\": \"mace\", \"cutoff\": CUTOFF, \"n_features\": 32, \"n_interactions\": 2,\n", " \"n_rbf\": 8, \"species\": SPECIES, \"max_ell\": 3, \"max_L\": 1, \"correlation\": 3,\n", " \"hidden_irreps\": \"32x0e+32x1o\", \"MLP_irreps\": \"16x0e\",\n", " \"avg_num_neighbors\": LAMBDA, \"atomic_energies\": [E0[z] for z in SPECIES]},\n", " \"data\": {\"batch_size\": 10},\n", " \"optim\": {\"lr\": 0.01, \"weight_decay\": 5e-7, \"epochs\": 30,\n", " \"energy_weight\": 1.0, \"force_weight\": 100.0, \"scheduler\": \"plateau\"},\n", " \"device\": DEVICE, \"seed\": 0, \"output_dir\": \"runs/mdi_argon\",\n", "})\n", "\n", "rng = np.random.default_rng(0)\n", "idx = rng.permutation(len(train_structs))\n", "train_set = AtomicDataset([train_structs[i] for i in idx[:180]], CUTOFF)\n", "val_set = AtomicDataset([train_structs[i] for i in idx[180:]], CUTOFF)\n", "\n", "t0 = time.time()\n", "Trainer(core, train_set, val_set).fit()\n", "print(f\"\\ntrained {core.optim.epochs} epochs in {time.time()-t0:.0f} s \"\n", " f\"-> {core.output_dir}/best.pt\")" ] }, { "cell_type": "markdown", "id": "b320db56", "metadata": {}, "source": [ "## 3. Reload the checkpoint and sanity-check on the test set\n", "\n", "We reload `best.pt` exactly the way any deployment consumer does (and the way\n", "`MDIEngine.from_checkpoint` does internally): rebuild with `build_model` from the stored\n", "config, wrap in `ForceStressOutput`, load the state dict. A quick pass over ten test\n", "frames confirms the 30-epoch model is a physically usable argon potential before we\n", "serve it.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "bb46d648", "metadata": { "execution": { "iopub.execute_input": "2026-08-04T20:53:52.805695Z", "iopub.status.busy": "2026-08-04T20:53:52.805571Z", "iopub.status.idle": "2026-08-04T20:53:56.811623Z", "shell.execute_reply": "2026-08-04T20:53:56.810721Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10 test frames: energy MAE 13.34 meV/atom | force RMSE 2.1 meV/A\n" ] }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "torch.set_default_dtype(torch.float64) # deployment and dynamics in float64\n", "\n", "from ase import Atoms\n", "from xnn.common.models import build_model, ForceStressOutput\n", "from xnn.common.deploy import XNNCalculator\n", "\n", "ckpt = torch.load(\"runs/mdi_argon/best.pt\", map_location=\"cpu\", weights_only=False)\n", "model = ForceStressOutput(build_model(ckpt[\"cfg\"].model), compute_stress=True).double()\n", "model.load_state_dict(ckpt[\"model\"])\n", "calc = XNNCalculator(model, cutoff=ckpt[\"cfg\"].model.cutoff, device=DEVICE)\n", "\n", "de, df = [], []\n", "for s in test_structs[:10]:\n", " at = Atoms(numbers=s[\"atomic_numbers\"], positions=s[\"pos\"], cell=s[\"cell\"], pbc=True)\n", " at.calc = calc\n", " de.append((at.get_potential_energy() - s[\"energy\"]) / len(at))\n", " df.append((at.get_forces() - s[\"forces\"]).ravel())\n", "de, df = np.array(de), np.concatenate(df)\n", "print(f\"10 test frames: energy MAE {np.abs(de).mean()*1000:.2f} meV/atom | \"\n", " f\"force RMSE {np.sqrt((df**2).mean())*1000:.1f} meV/A\")\n", "\n", "f_ref = np.concatenate([s[\"forces\"].ravel() for s in test_structs[:10]])\n", "f_mod = f_ref + df\n", "pick = np.random.default_rng(0).choice(f_ref.size, 2000, replace=False)\n", "lim = np.abs(f_ref[pick]).max() * 1.1\n", "fig, ax = plt.subplots(figsize=(3.4, 3.4))\n", "ax.plot([-lim, lim], [-lim, lim], \"--\", color=\"0.6\", lw=1)\n", "ax.plot(f_ref[pick], f_mod[pick], \".\", ms=3, alpha=0.35, color=\"#1f77b4\")\n", "ax.set_xlabel(\"reference force [eV/A]\"); ax.set_ylabel(\"model force [eV/A]\")\n", "ax.set_title(\"force parity, 10 test frames\"); ax.set_aspect(\"equal\")\n", "plt.tight_layout(); plt.show()" ] }, { "cell_type": "markdown", "id": "6f53a958", "metadata": {}, "source": [ "## 4. Serve the checkpoint as an MDI engine\n", "\n", "The engine runs as a **separate process**, exactly as it would next to LAMMPS. The\n", "subprocess below is literally the `xnn mdi` console command:\n", "\n", "```bash\n", "xnn mdi --ckpt runs/mdi_argon/best.pt --device cuda --dtype float64 \\\n", " -mdi \"-role ENGINE -name xnn -method TCP -port 8021 -hostname localhost\"\n", "```\n", "\n", "`--dtype float64` upcasts the float32-trained weights so the NVE integration below is\n", "not limited by single precision. Order matters for TCP: the **driver initializes first**\n", "(it owns the listening socket), then the engine is launched and connects.\n", "\n", "The driver side is the ~40-line class below. It works in ASE units (angstrom / eV) and\n", "converts to MDI atomic units (Bohr / Hartree) at the wire, mirroring what the engine\n", "does on its side. We convert with the engine's own public constants\n", "(`xnn.common.deploy.mdi_engine.BOHR_TO_ANGSTROM` / `HARTREE_TO_EV`, CODATA 2018) so the\n", "round trip is bit-clean; a driver with a different CODATA vintage (e.g. `ase.units`,\n", "CODATA 2014) would differ at the physically irrelevant 1e-8 relative level.\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "e52ff71b", "metadata": { "execution": { "iopub.execute_input": "2026-08-04T20:53:56.813294Z", "iopub.status.busy": "2026-08-04T20:53:56.813170Z", "iopub.status.idle": "2026-08-04T20:53:56.819062Z", "shell.execute_reply": "2026-08-04T20:53:56.818267Z" } }, "outputs": [], "source": [ "import mdi\n", "from xnn.common.deploy.mdi_engine import ( # the engine's exact wire constants\n", " BOHR_TO_ANGSTROM as Bohr, HARTREE_TO_EV as Hartree)\n", "\n", "\n", "class MDIDriver:\n", " \"\"\"Minimal MDI driver speaking ASE units (angstrom / eV) to any MDI engine.\"\"\"\n", "\n", " def __init__(self, port):\n", " mdi.MDI_Init(f\"-role DRIVER -name driver -method TCP -port {port}\")\n", " self.comm = None\n", " self.natoms = 0\n", "\n", " def accept(self):\n", " \"\"\"Block until an engine connects.\"\"\"\n", " self.comm = mdi.MDI_Accept_Communicator()\n", "\n", " def send_system(self, atomic_numbers, cell):\n", " n = self.natoms = len(atomic_numbers)\n", " mdi.MDI_Send_Command(\">NATOMS\", self.comm)\n", " mdi.MDI_Send(n, 1, mdi.MDI_INT, self.comm)\n", " mdi.MDI_Send_Command(\">ELEMENTS\", self.comm)\n", " mdi.MDI_Send([int(z) for z in atomic_numbers], n, mdi.MDI_INT, self.comm)\n", " mdi.MDI_Send_Command(\">CELL\", self.comm)\n", " mdi.MDI_Send((np.asarray(cell) / Bohr).flatten(), 9, mdi.MDI_DOUBLE, self.comm)\n", "\n", " def send_positions(self, pos):\n", " mdi.MDI_Send_Command(\">COORDS\", self.comm)\n", " mdi.MDI_Send((np.asarray(pos) / Bohr).flatten(), 3 * self.natoms,\n", " mdi.MDI_DOUBLE, self.comm)\n", "\n", " def energy(self):\n", " mdi.MDI_Send_Command(\"COORDS` is re-sent; the engine keeps elements and cell from\n", "section 5 and lazily re-evaluates once per geometry, so the paired `" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "max |E_tot(t) - E_tot(0)| = 0.009 meV/atom over 1.2 ps | (last half) = 87.3 K\n" ] } ], "source": [ "t_ps = np.arange(N_STEPS + 1) * DT / u.fs / 1000\n", "etot = epot + ekin\n", "temp = 2 * ekin / (3 * N * u.kB)\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(9, 3.1))\n", "ax[0].plot(t_ps, (etot - etot[0]) / N * 1000, color=\"#1f77b4\", lw=1.2)\n", "ax[0].axhline(0, color=\"0.75\", lw=0.8, ls=\"--\")\n", "ax[0].set_xlabel(\"time [ps]\")\n", "ax[0].set_ylabel(\"$\\\\Delta E_\\\\mathrm{tot}$ [meV/atom]\")\n", "ax[0].set_title(\"NVE energy conservation\")\n", "ax[1].plot(t_ps, temp, color=\"#1f77b4\", lw=1.2)\n", "ax[1].axhline(T_K, color=\"0.75\", lw=0.8, ls=\"--\")\n", "ax[1].set_xlabel(\"time [ps]\"); ax[1].set_ylabel(\"T [K]\")\n", "ax[1].set_title(\"instantaneous temperature\")\n", "plt.tight_layout(); plt.show()\n", "\n", "drift = np.abs(etot - etot[0]).max() / N * 1000\n", "print(f\"max |E_tot(t) - E_tot(0)| = {drift:.3f} meV/atom over {t_ps[-1]:.1f} ps | \"\n", " f\" (last half) = {temp[N_STEPS // 2:].mean():.1f} K\")" ] }, { "cell_type": "markdown", "id": "85bd7876", "metadata": {}, "source": [ "## 7. Shut the engine down\n" ] }, { "cell_type": "code", "execution_count": 10, "id": "e1247ece", "metadata": { "execution": { "iopub.execute_input": "2026-08-04T20:54:56.600083Z", "iopub.status.busy": "2026-08-04T20:54:56.599917Z", "iopub.status.idle": "2026-08-04T20:54:57.118680Z", "shell.execute_reply": "2026-08-04T20:54:57.117959Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "engine exited with code 0\n", "\n", "--- runs/mdi_argon/engine.log (tail) ---\n", "INFO:__main__:MDI connection established\n", "INFO:__main__:received 400 atoms, elements [18]\n", "INFO:__main__:step 100: avg 141.9 ms/step\n", "INFO:__main__:step 200: avg 108.5 ms/step\n", "INFO:__main__:engine finished: 242 calculations, avg 102.4 ms/step\n" ] } ], "source": [ "driver.exit()\n", "engine.wait(timeout=60)\n", "engine_log.close()\n", "print(f\"engine exited with code {engine.returncode}\\n\")\n", "print(\"--- runs/mdi_argon/engine.log (tail) ---\")\n", "print(\"\\n\".join(open(\"runs/mdi_argon/engine.log\").read().splitlines()[-5:]))" ] }, { "cell_type": "markdown", "id": "1f44eee3", "metadata": {}, "source": [ "## Where to go from here\n", "\n", "**Drive it from LAMMPS instead of Python.** The engine side does not change; only the\n", "launch method does (MPI instead of TCP, `mpi4py` required):\n", "\n", "```bash\n", "mpirun -np 1 xnn mdi --ckpt runs/mdi_argon/best.pt --device cuda:0 --dtype float64 \\\n", " -mdi \"-role ENGINE -name xnn -method MPI\" \\\n", " : -np 4 lmp -mdi \"-role DRIVER -name LAMMPS -method MPI\" -in in.argon\n", "```\n", "\n", "with `fix mdi/qm` (or the `mdi` fix family) on the LAMMPS side. The same applies to any\n", "other MDI driver, e.g. SEAMM's LAMMPS step.\n", "\n", "**Serve a different model.** Nothing here is MACE specific: point `--ckpt` at any\n", "`best.pt` produced by the xnn `Trainer` (NequIP, Allegro, CACE, SchNet, ANI, PhysNet,\n", "BAMBOO, with or without the LES long-range wrapper) and the engine rebuilds it from the\n", "stored config and serves it through the identical protocol.\n", "\n", "**Embed it.** `MDIEngine` is a plain class\n", "(`from xnn.common.deploy import MDIEngine`); `MDIEngine.from_checkpoint(path)` plus\n", "`engine.run(mdi_options)` reproduces the CLI inside your own launcher, and\n", "`MDIEngine(model, cutoff)` accepts any in-memory model that follows the `AtomicGraph`\n", "contract.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.12" } }, "nbformat": 4, "nbformat_minor": 5 }