---
myst:
html_meta:
"description": "How eOn handles parallel force evaluation with thread-safe and per-image potential instances."
"keywords": "eOn, parallel, threading, NEB, potential, MetatomicPotential, XTB"
---
# Parallel Force Evaluation
eOn supports parallel force evaluation in NEB, Dimer/ImprovedDimer, and
ProcessSearchJob. The threading model uses `std::thread` with per-image
potential ownership.
## Threading Model
Two virtual methods on `Potential` control the behavior:
### `isThreadSafe()`
Returns whether the *same* potential instance can be called from multiple
threads concurrently. Most empirical potentials (LJ, Morse, SW, etc.)
return `true` (default). Python-based potentials (ASE, CatLearn) return
`false` because CPython has the GIL.
When `true`, NEB spawns one thread per image and all threads call
`force()` on the shared potential instance.
### `needsPerImageInstance()`
Returns whether NEB should create a *separate* `Potential` instance per
image via `makePotential()`. This is needed for potentials where:
- The same instance cannot be called concurrently (internal state, caches)
- But separate instances CAN run in parallel (each has its own state)
Examples:
- **MetatomicPotential**: PyTorch model has internal caches. Same instance
needs a mutex; separate instances run truly in parallel. Returns
`needsPerImageInstance() = true`.
- **XTBPot**: Fortran library has per-instance state (`xtb_TEnvironment`,
`xtb_TCalculator`). Same instance is not thread-safe; separate instances
are independent. Returns `needsPerImageInstance() = true`.
When `needsPerImageInstance()` is `true`, NEB creates N+2 potential
instances (one per image) at construction time. The parallel force
evaluation then proceeds lock-free.
## Decision Table
| `isThreadSafe()` | `needsPerImageInstance()` | Behavior | Examples |
|:-:|:-:|:--|:--|
| `true` | `false` | Shared instance, parallel threads | LJ, Morse, SW, EMT |
| `false` | `true` | Per-image instances, parallel threads | XTB, ASE, metatomic |
| `true` | `true` | Per-image instances, parallel threads | MetatomicPotential (mutex fallback) |
| `false` | `false` | Sequential evaluation | (none currently) |
The parallel check in NEB is:
```cpp
bool canParallel = pot->isThreadSafe() || perImagePotentials_;
if (numImages > 1 && params.main_options.parallel && canParallel) { ... }
```
## Affected Code Paths
| Component | Parallel Units | Per-Image Potential |
|:--|:--|:--|
| NEB | N images | Each `path[i]` Matter |
| Dimer | center + forward | `matterDimer` |
| ImprovedDimer | x0 + x1 | `x1` |
| ProcessSearchJob | min1 + min2 | `min2` |
## Performance
With the Morse empirical potential (337-atom Pt, 5 NEB images), parallel
force evaluation gives a **2.3x speedup** over SVN sequential.
With PET-MAD-S ML potential (14-atom Claisen, 10 NEB images):
- Mutex-serialized (shared instance): 192 seconds
- Per-image instances (true parallel): 69 seconds (**2.8x speedup**)
## Adding a New Potential
If your potential has internal state that prevents concurrent calls on the
same instance but supports independent instances:
1. Override `isThreadSafe()` to return `true` (with internal mutex as
fallback) or `false`
2. Override `needsPerImageInstance()` to return `true`
3. Ensure the constructor (called by `makePotential()`) creates an
independent instance (no shared static state)
The `[Main] parallel = true` config option (default) enables threading.
Set `parallel = false` to force sequential evaluation.