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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,9 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
*.bas4
*.bas2
*.bas3
*.bas5
*.bas0
*.bas1
19 changes: 19 additions & 0 deletions mace.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

SCRIPT_PATH="$(dirname -- "${BASH_SOURCE[0]}")"
src="${SCRIPT_PATH}/mace"
venv="$src/.venv"

need_setup=0
if [ ! -d "$venv" ]; then
need_setup=1
elif [ ! -x "$venv/bin/maceexttool" ]; then
need_setup=1
fi

if [ $need_setup -eq 1 ]; then
"$src"/install.sh
fi

source "$venv"/bin/activate
maceexttool "$@"
39 changes: 39 additions & 0 deletions mace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# MACE ExtTool for ORCA

Wrapper around MACE calculators to use ORCA's `otool_external` interface. Mirrors the UMA tool interface with standalone and server–client modes.

- Suites: `mace-mp` (Materials Project) and `mace-omol` (OMOL foundation model)
- Extras: `dispersion` (MP only), `default_dtype` (`float32` or `float64`), optional `device`, and `head` for advanced MP heads.

## Quick start

- Standalone (single call per ORCA task):
- `./mace.sh -s mp -m medium-mpa-0 your_job_EXT.extinp.tmp`
- `./mace.sh -s omol your_job_EXT.extinp.tmp`

- Server–client (faster for many calls in one ORCA job):
- Start: `./maceserver.sh -s mp -m medium-mpa-0 -n 2 -b 127.0.0.1:8888`
- Use in ORCA input: `progext ".../maceclient.sh"` (ORCA passes the temp input filepath)

## Arguments

Common:
- `-s, --suite`: `mp` or `omol` (default: `omol`)
- `-m, --model`: model spec or local path (MP: e.g. `medium-mpa-0`, `medium`, `small`; OMOL: `extra_large` or path)
- `--default-dtype`: `float32` (MD speed) or `float64` (opt accuracy)
- `--device`: `cpu`, `cuda`, etc.

MP only:
- `--dispersion`: enable D3 dispersion (off by default)
- `--damping`, `--dispersion-xc`, `--dispersion-cutoff` advanced dispersion controls
- `--head`: MACE head selection for multi-head variants

Server:
- `-b, --bind`: `host:port` (default: `127.0.0.1:8888`)
- `-n, --nthreads`: number of threads per server

## Install

- `cd mace && ./install.sh`
- This installs a venv, the wrapper, and tries to `pip install -e ../../mace` if the local MACE repo exists. Otherwise it relies on `mace-torch`.

49 changes: 49 additions & 0 deletions mace/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# MACE GOAT Examples (Custom Model)

This folder shows how to run ORCA's global optimization tool (GOAT) using the MACE wrapper with a custom model file.

Two variants are provided:
- Server–client (recommended for many external calls): `goat_water_mace_omol_server.inp`
- Standalone (simpler, slower for many calls): `goat_water_mace_omol_standalone.inp`

Replace `/abs/path/to/your/MACE-omol-custom.model` with your actual model file path before running.

## Server–Client (recommended)

1) Start the MACE server in another terminal with your custom model:

```
../../maceserver.sh \
-s omol \
-m /abs/path/to/your/MACE-omol-custom.model \
--default-dtype float64 \
--device cpu \
-n 4 \
-b 127.0.0.1:8888
```

2) Run ORCA on the provided input:

```
orca goat_water_mace_omol_server.inp > goat_water_mace_omol_server.out
```

- The input references `../../maceclient.sh` and points it to the server via `Ext_Params "-b 127.0.0.1:8888"`.
- This example uses `pal1` to avoid requiring MPI. If you have MPI, you can change to `pal4`.

## Standalone (simple, no server)

Run ORCA on the standalone input:

```
orca goat_water_mace_omol_standalone.inp > goat_water_mace_omol_standalone.out
```

- The input references `../../mace.sh` and passes the suite and custom model via `Ext_Params`.
- Recommended flags for optimization: `--default-dtype float64` and `--device cpu`.

Notes
- Ensure ORCA is available on your PATH (or call it via absolute path).
- Use absolute paths for the custom model so ORCA can find it regardless of working directory.
- For Materials Project models (mp), change `-s omol` to `-s mp` and add flags like `--dispersion` or `--head mh0` as needed.

11 changes: 11 additions & 0 deletions mace/examples/goat_water_mace_omol_server.inp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
! extopt goat pal1
%method
progext "../../maceclient.sh"
ext_params "-b 127.0.0.1:8888"
end
*xyz 0 1
O 0.000000 0.000000 0.000000
H 0.000000 0.000000 0.960000
H 0.000000 0.750000 -0.240000
*

11 changes: 11 additions & 0 deletions mace/examples/goat_water_mace_omol_standalone.inp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
! extopt goat pal1
%method
progext "../../mace.sh"
ext_params "-s omol -m /abs/path/to/your/MACE-omol-custom.model --default-dtype float64 --device cpu"
end
*xyz 0 1
O 0.000000 0.000000 0.000000
H 0.000000 0.000000 0.960000
H 0.000000 0.750000 -0.240000
*

49 changes: 49 additions & 0 deletions mace/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
#!/usr/bin/env bash
# Get the location of this script
SCRIPT_PATH="$(dirname -- "${BASH_SOURCE[0]}")"
# Name of the virtual environment
VENV=.venv

set -e

cd "$SCRIPT_PATH"

choose_python() {
for exe in python3.12 python3.11 python3.10 python3 python; do
if command -v "$exe" >/dev/null 2>&1; then
if "$exe" -c 'import sys; raise SystemExit(int(sys.version_info< (3,10)))'; then
echo "$exe"; return 0
fi
fi
done
echo ""; return 1
}

PYEXE="$(choose_python)" || {
echo "No suitable Python >=3.10 found to create venv. Please install Python 3.10+." >&2
exit 1
}
echo "Using Python interpreter: $PYEXE"

# Reset venv
rm -rf "$VENV" 2>/dev/null || true
"$PYEXE" -m venv "$VENV"
source "$VENV/bin/activate"

# Ensure modern pip/setuptools for editable installs with pyproject
python -m pip install --upgrade pip setuptools wheel

# Install wrapper
pip install -e .

# Try to install local MACE if available; otherwise rely on dependency
LOCAL_MACE_DIR="$(cd "$SCRIPT_PATH/../.." && pwd)/mace"
if [ -d "$LOCAL_MACE_DIR" ]; then
echo "Installing local MACE from $LOCAL_MACE_DIR"
pip install -e "$LOCAL_MACE_DIR" || echo "Warning: failed to install local MACE; ensure mace-torch is available."
else
echo "Local MACE repo not found. Using mace-torch from PyPI if available."
fi

echo "Setup complete. Activate venv with: source $SCRIPT_PATH/$VENV/bin/activate"
30 changes: 30 additions & 0 deletions mace/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[project]
name = "maceexttool"
version = "0.1.0"
description = "MACE wrapper for ORCA's ExtTool interface (standalone/server-client)"
requires-python = ">=3.10"
dependencies = [
"ase>=3.22.1",
"Flask>=3,<4",
"numpy<3.0.0,>=1.24",
"requests>=2,<3",
"waitress>=3,<4",
# Prefer local install, but allow PyPI fallback if available
"mace-torch>=0.3.14",
]

[project.scripts]
maceexttool = "maceexttool.standalone:main"
maceserver = "maceexttool.server:main"
maceclient = "maceexttool.client:main"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

4 changes: 4 additions & 0 deletions mace/src/maceexttool/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__all__ = [
"common",
]

59 changes: 59 additions & 0 deletions mace/src/maceexttool/calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from typing import Optional

from ase import Atoms


def init(
suite: str,
model: Optional[str] = None,
*,
device: str = "",
default_dtype: Optional[str] = None,
dispersion: bool = False,
damping: str = "bj",
dispersion_xc: str = "pbe",
dispersion_cutoff: Optional[float] = None,
head: Optional[str] = None,
):
"""Initialize a MACE calculator based on suite and options.

Returns an ASE calculator compatible object which can be attached to Atoms.
"""
# Lazy import to avoid heavy deps at import-time
from mace.calculators.foundations_models import mace_mp, mace_omol
from ase import units

if suite == "mp":
kwargs = dict(
model=model,
device=device or ("cuda" if _torch_cuda_available() else "cpu"),
default_dtype=default_dtype or "float32",
dispersion=dispersion,
damping=damping,
dispersion_xc=dispersion_xc,
dispersion_cutoff=(dispersion_cutoff if dispersion_cutoff is not None else 40.0 * units.Bohr),
)
if head:
kwargs["head"] = head
calc = mace_mp(**kwargs)
return calc
elif suite == "omol":
calc = mace_omol(
model=model,
device=device or ("cuda" if _torch_cuda_available() else "cpu"),
default_dtype=default_dtype or "float64",
)
return calc
else:
raise ValueError(f"Unknown suite: {suite}. Expected 'mp' or 'omol'.")


def _torch_cuda_available() -> bool:
try:
import torch # type: ignore

return torch.cuda.is_available()
except Exception:
return False
69 changes: 69 additions & 0 deletions mace/src/maceexttool/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

import json
import sys
from argparse import Namespace
from typing import Tuple

import requests

from maceexttool import common


def submit_mace(server_url: str,
atom_types: list[str],
coordinates: list[tuple[float, float, float]],
charge: int,
mult: int,
nthreads: int) -> Tuple[float, list[float]]:
payload = {
"atom_types": atom_types,
"coordinates": coordinates,
"charge": charge,
"mult": mult,
"nthreads": nthreads,
}

try:
response = requests.post('http://' + server_url + "/calculate", json=payload)
response.raise_for_status()
except requests.exceptions.ConnectionError:
print("The server is probably not running.")
print("Please start the server with the maceserver.sh script.")
raise
except requests.exceptions.Timeout as timeout_err:
print("Request to MACE server timed out:", timeout_err)
raise

data = response.json()
return data['energy'], data['gradient']


def run(arglist: list[str]):
args: Namespace = common.cli_parse(arglist, mode=common.RunMode.Client)

# read ORCA-generated input
xyzname, charge, mult, ncores, dograd = common.read_input(args.inputfile)
basename = xyzname.removesuffix(".xyz")
orca_engrad = basename + ".engrad"

atom_types, coordinates = common.read_xyzfile(xyzname)
natoms = len(atom_types)

energy, gradient = submit_mace(server_url=args.bind,
atom_types=atom_types,
coordinates=coordinates,
charge=charge,
mult=mult,
nthreads=ncores)

common.write_engrad(orca_engrad, natoms, energy, dograd, gradient)


def main():
run(sys.argv[1:])


if __name__ == '__main__':
main()

Loading