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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ build/
htmlcov/
.coverage*
coverage.xml
.test-venv/
233 changes: 233 additions & 0 deletions python/examples/orca/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
ORCA external optimizer example
================================

This directory contains wrappers that let `ORCA`_ drive geometry optimization
(and other workflows using the external-tool interface) with a metatomic
machine-learning potential.

The wrappers implement the file protocol documented in `orca-external-tools`_.
On each ORCA step they read ``*.extinp.tmp`` and the accompanying XYZ geometry,
evaluate energy and gradient with :py:class:`metatomic_ase.MetatomicCalculator`,
and write ``*.engrad`` back for ORCA.

.. _ORCA: https://www.faccts.de/orca/
.. _orca-external-tools: https://github.com/faccts/orca-external-tools#interface

Prerequisites
-------------

- ORCA 6 or newer (``ProgExt`` / ``Ext_Params`` in the input file)
- Python packages ``metatomic``, ``metatomic-ase``, and their dependencies
- An exported metatomic model (``.pt``), plus an ``extensions/`` directory if the
model requires compiled extensions

Files
-----

``orca_common.py``
Shared protocol parsing, unit conversion, and job evaluation logic.

``metatomic-orca-external``
Standalone script invoked by ORCA via ``%method ProgExt``. Reloads the model
on every ORCA call.

``metatomic-orca-server``
Persistent HTTP server that keeps the model resident in memory.

``metatomic-orca-client``
Thin ORCA-facing client that forwards jobs to ``metatomic-orca-server``.

``water_opt/water.xyz``
Starting water geometry for a test optimization.

``water_opt/water_opt.inp``
ORCA input template using the server/client setup. Edit paths before running.

Recommended setup (server/client)
---------------------------------

For production workflows (geometry optimization, NEB, GOAT), start a persistent
server so ORCA does not reload the PyTorch model on every energy/gradient call.

1. Install metatomic and metatomic-ase in the Python environment ORCA will use.

2. Start the server in one terminal (use ``--warmup`` to load the model
immediately)::

metatomic-orca-server \
--model /path/to/model-md.pt \
--extensions-directory /path/to/extensions \
--device cuda \
--warmup

You can also set ``METATOMIC_MODEL``, ``METATOMIC_EXTENSIONS``, and
``METATOMIC_DEVICE`` instead of passing flags.

3. Edit ``water_opt/water_opt.inp``:

- ``ProgExt`` must point to ``metatomic-orca-client`` (absolute path)
- ``Ext_Params`` should pass ``-b hostname:port`` if not using the default
``127.0.0.1:8888``

Example::

%method
ProgExt "/home/user/metatomic/python/examples/orca/metatomic-orca-client"
Ext_Params "-b 127.0.0.1:8888"
end

Model paths are configured on the server. To override per job, add
``--model`` / ``--extensions-directory`` to ``Ext_Params``.

4. Run ORCA from the example directory::

cd water_opt
orca water_opt.inp > job.out

Standalone mode
---------------

For quick tests, ORCA can call ``metatomic-orca-external`` directly::

%method
ProgExt "/home/user/metatomic/python/examples/orca/metatomic-orca-external"
Ext_Params "--model /home/user/models/model-md.pt --extensions-directory /home/user/models/extensions"
end

Each ORCA step starts a new Python process and reloads the model, which is
simple but slow for long optimizations.

Parallelism: ORCA PAL, ``NCores``, and PyTorch threading
--------------------------------------------------------

ORCA does **not** parallelize the external program for you. It only reports how
many cores were allocated in each ``*.extinp.tmp`` file as ``NCores``. The
wrapper reads that value and configures CPU threading before each evaluation:

- ``torch.set_num_threads(NCores)``
- ``OMP_NUM_THREADS``, ``MKL_NUM_THREADS``, ``OPENBLAS_NUM_THREADS``, and related
variables set to ``NCores``

Set ``METATOMIC_DISABLE_THREADING_CONFIG=1`` if you prefer to manage these
variables yourself (for example in your job scheduler script).

Matching ORCA and metatomic resources
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For a single geometry optimization (one external call at a time), choose PAL so
that ``NCores`` matches the CPU cores you want PyTorch to use:

.. code-block:: text

! ExtOpt Opt PAL4

%pal
nprocs 4
end

%method
ProgExt "/path/to/metatomic-orca-client"
Ext_Params "-b 127.0.0.1:8888"
end

For multi-image workflows (NEB, GOAT, numerical frequencies), ORCA can run
several external calls in parallel. Use ``nprocs_group`` to set how many cores
each external call receives (see the `ORCA parallel manual`_):

.. code-block:: text

! ExtOpt NEB-CI PAL8

%pal
nprocs 8
nprocs_group 4
end

Here ORCA may launch two external evaluations at once, each with ``NCores = 4``.
Start **one** ``metatomic-orca-server`` per distinct ``NCores``/GPU combination,
or run standalone wrappers and let the scheduler place one process per core
group. Avoid oversubscribing: if ``NCores = 4``, do not let multiple concurrent
wrapper processes each spawn 4 OpenMP threads on the same 4 physical cores.

GPU evaluation
~~~~~~~~~~~~~~

For GPU inference, start the server with ``--device cuda`` (or set
``METATOMIC_DEVICE=cuda``). ORCA ``NCores`` then mainly controls CPU-side work
such as neighbor-list construction; the model forward pass runs on the GPU
selected by ``CUDA_VISIBLE_DEVICES`` on the server host.

Example server startup on a GPU node:

.. code-block:: bash

export CUDA_VISIBLE_DEVICES=0
metatomic-orca-server \
--model /path/to/model-md.pt \
--extensions-directory /path/to/extensions \
--device cuda \
--warmup

Keep ORCA ``PAL``/``NCores`` modest on GPU nodes unless CPU neighbor builds are
the bottleneck. A practical starting point is ``PAL1`` or ``PAL2`` for the
external call when using GPU inference.

.. _ORCA parallel manual: https://www.faccts.de/docs/orca/6.1/manual/contents/essentialelements/parallel.html

Expected outputs
----------------

- ``water_opt.engrad`` — energy and gradient written each step
- ``water_opt.xyz`` — final optimized geometry
- ``water_opt_trj.xyz`` — optimization trajectory (if ORCA writes it)

Standalone test (without ORCA)
------------------------------

Smoke-test the standalone wrapper if ORCA has already created an
``*.extinp.tmp`` file, or craft one following the `interface specification`_::

./metatomic-orca-external water_opt_EXT.extinp.tmp \
--model /path/to/model-md.pt \
--extensions-directory /path/to/extensions

Test the client against a running server::

metatomic-orca-server --model /path/to/model-md.pt --warmup
metatomic-orca-client -b 127.0.0.1:8888 water_opt_EXT.extinp.tmp

.. _interface specification: https://github.com/faccts/orca-external-tools#interface

Troubleshooting
---------------

**ORCA cannot find the script**
Use an absolute path in ``ProgExt``. ORCA's working directory may differ
from where you launch the job.

**Connection error from the client**
Ensure ``metatomic-orca-server`` is running and that ``-b`` matches the
server bind address.

**Model or extensions not found**
Pass absolute paths to ``metatomic-orca-server``, or set ``METATOMIC_MODEL``
and ``METATOMIC_EXTENSIONS``.

**Point charges**
ORCA point-charge files (``pointcharges.pc``) are not supported in this
version.

**CPU oversubscription / slow runs**
Check that ORCA ``PAL``/``NCores`` matches the threading configured by the
wrapper. Use ``METATOMIC_DISABLE_THREADING_CONFIG=1`` only when setting
``OMP_NUM_THREADS``/``MKL_NUM_THREADS`` manually.

Related
-------

- `metatomic issue #228`_
- `ORCA external optimizer tutorial`_
- `orca-external-tools`_

.. _metatomic issue #228: https://github.com/metatensor/metatomic/issues/228
.. _ORCA external optimizer tutorial: https://www.faccts.de/docs/orca/6.1/tutorials/workflows/extopt.html
82 changes: 82 additions & 0 deletions python/examples/orca/metatomic-orca-client
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""ORCA client that forwards external-tool jobs to a persistent Metatomic server."""

from __future__ import annotations

import json
import os
import sys
import traceback
import urllib.error
import urllib.request
from argparse import ArgumentParser

DEFAULT_BIND = "127.0.0.1:8888"


def send_to_server(host_port: str, arguments: list[str], *, working_directory: str) -> None:
"""Forward a calculation request to ``metatomic-orca-server``."""
host, port = host_port.split(":", 1)
url = f"http://{host}:{port}/calculate"
payload = {"arguments": arguments, "directory": working_directory}
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)

try:
with urllib.request.urlopen(request, timeout=None) as response:
data = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
print(f"HTTP error {exc.code}: {body}", file=sys.stderr)
raise SystemExit(1) from exc
except urllib.error.URLError as exc:
print(f"Connection error: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
except Exception as exc:
print(f"Unexpected error: {type(exc).__name__}: {exc}", file=sys.stderr)
traceback.print_exc()
raise SystemExit(1) from exc

print(data.get("stdout", ""), end="")
if data.get("status") != "Success":
print(
f"Server error {data.get('error_type')}: {data.get('error_message')}.",
file=sys.stderr,
)
if data.get("traceback"):
print(data["traceback"], file=sys.stderr)
raise SystemExit(1)


def build_client_parser() -> ArgumentParser:
parser = ArgumentParser(
prog="metatomic-orca-client",
description="Forward ORCA external-tool jobs to a running metatomic-orca-server.",
)
parser.add_argument(
"-b",
"--bind",
metavar="hostname:port",
default=DEFAULT_BIND,
dest="host_port",
help=f"Server bind address and port. Default: {DEFAULT_BIND}.",
)
return parser


def main(argv: list[str] | None = None) -> None:
parser = build_client_parser()
args, remaining_args = parser.parse_known_args(argv)
send_to_server(
args.host_port,
remaining_args,
working_directory=os.getcwd(),
)


if __name__ == "__main__":
main()
34 changes: 34 additions & 0 deletions python/examples/orca/metatomic-orca-external
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Metatomic ML potential wrapper for ORCA's external-tool interface."""

from __future__ import annotations

import sys
from pathlib import Path

EXAMPLE_DIR = Path(__file__).resolve().parent
if str(EXAMPLE_DIR) not in sys.path:
sys.path.insert(0, str(EXAMPLE_DIR))

from orca_common import ( # noqa: E402
build_runner_parser,
run_orca_job,
settings_from_namespace,
)


def main(argv: list[str] | None = None) -> None:
parser = build_runner_parser(
prog="metatomic-orca-external",
description="Metatomic ML potential wrapper for ORCA's external-tool interface.",
)
args = parser.parse_args(argv)
try:
settings = settings_from_namespace(args)
run_orca_job(args.inputfile, settings)
except (ValueError, FileNotFoundError) as exc:
raise SystemExit(str(exc)) from exc


if __name__ == "__main__":
main()
Loading