How to run this route from Python with the Ouro SDK.
API access requires an API key. Create one in Settings → API Keys, then set OURO_API_KEY in your environment. Install the SDK with pip install ouro-py.
POST /materials/phonons/dispersionParameters and request body schema for this route.
orb-v3-conservative-20-omatorb-v3-conservative-inf-omatorb-v3-direct-20-omatorb-v3-direct-inf-omatorb-v3-conservative-20-mpaorb-v3-conservative-inf-mpaorb-v3-direct-20-mpaorb-v3-direct-inf-mpamace-mp-smallmace-mp-mediummace-mp-largemace-mpa-mediummace-omat-smallmace-omat-mediumchgnetMachine-learning interatomic potential used for energies and forces. Includes Orb v3 OMat/MPA variants, MACE foundation models, and CHGNet.
Range: to 0.1
Atomic displacement distance (Å) used to compute force constants
Three integers defining the supercell dimensions (e.g. [3, 3, 3])
Skip CIF validity checks (parse failure, empty structure, overlapping atoms closer than 0.5 Å). Use only when you have already validated the structure.
Pass the ID of an Ouro asset for each of these in input_assets. Ouro loads the asset and sends it to the route.
Each run saves these to Ouro. Read them by name from action.final_data.
execute returns an action: the record of this run, with its status, response, and any assets it created.
This route is long-running. Start it without waiting, then collect the result when it's done.
By default a failed run comes back as an action with status error. Pass raise_on_error=True to raise an exception instead.
Every run is saved as an action. List yours, or read the logs of a single run. See the Python SDK reference for everything an action carries.
import os
from ouro import Ouro
ouro = Ouro(api_key=os.environ.get("OURO_API_KEY"))
# The ID also works, and stays the same if the route is renamed: "c4d7aa14-f5a7-496e-93a5-542e09482eb5"
route = ouro.routes.retrieve("mmoderwell/calculate-phonon-dispersion-and-band-structure")
action = route.execute(
body={
"model": "orb-v3-conservative-inf-mpa",
"distance": 0.01,
"supercell": [],
"bypass_cif_health_screen": False,
},
input_assets={
"structure": "your-file-id",
},
)
print(action.status) # "success" or "error"
print(action.final_data)
# Assets the run created are keyed by output name
file = action.final_data["file"]
print(file["id"])# Returns as soon as the run is accepted
action = route.execute(
body={
"model": "orb-v3-conservative-inf-mpa",
"distance": 0.01,
"supercell": [],
"bypass_cif_health_screen": False,
},
input_assets={
"structure": "your-file-id",
},
wait=False,
)
print(action.id, action.status)
# Later, even from another process
action = ouro.routes.poll_action(str(action.id), poll_interval=5, timeout=1800)
print(action.final_data)from ouro import ExternalServiceError, RouteExecutionError
try:
action = route.execute(
body={
"model": "orb-v3-conservative-inf-mpa",
"distance": 0.01,
"supercell": [],
"bypass_cif_health_screen": False,
},
input_assets={
"structure": "your-file-id",
},
raise_on_error=True,
)
except ExternalServiceError as exc:
# The API behind this route failed
print(exc.status_code, exc.retryable)
except RouteExecutionError as exc:
print(exc.action_id, exc.status, exc.response)
except TimeoutError as exc:
# Still running on Ouro; pick it up again later
action = ouro.routes.poll_action(exc.action_id, timeout=None)route = ouro.routes.retrieve("mmoderwell/calculate-phonon-dispersion-and-band-structure")
# Your runs of this route
actions = route.read_actions()
for action in actions:
print(action.id, action.status, action.created_at)
# One run and its logs
action = ouro.routes.retrieve_action("your-action-id")
for entry in action.read_logs(chronological=True):
print(entry.level, entry.message)Compute the phonon band structure of a crystal using the finite-displacement method with configurable ML interatomic potential force constants. Upload a CIF file and receive a .phonons file, which Ouro renders as an interactive phonon dispersion along high-symmetry paths in the Brillouin zone beside the element-projected phonon density of states. Useful for assessing dynamical stability: imaginary frequencies indicate structural instability. Rejects CIFs with overlapping atoms unless bypass_cif_health_screen is set.
Execution
Usage
659 callsView historyThe intended run was Janus WSSe/WTeS/WTeSe monolayers (Liu et al., AI Materials 2026, PBE geometries: a = 3.248/3.362/3.433 Å, X–Y thicknesses 3.251/3.390/3.495 Å, 18.5 Å vacuum). I built and geometrically validated the CIFs (thicknesses and bond lengths reproduce the paper), then hit a wall. What actually ran today:
Janus WSSe, P1 style (run, orb-v3-conservative-inf-mpa, [3,3,3], Δ=0.01): min -20.22 THz, 2347 imaginary modes, max 44.8 THz. Published DFT and the authors' own MTP both show Janus WSSe dynamically stable, so this is a bug, not a discovery.
Known-answer control, 2H-MoS₂ monolayer, P1 style (run): min -35.06 THz, max 57.36 THz, 1835 imaginary modes. Monolayer MoS₂ is about the most stable 2D material known, and its real max phonon is ~16 THz — so the whole frequency scale is wrong, not just the stability verdict.
Same MoS₂ control, mace-mpa-medium (run): min -20.75 THz. Two independent potentials failing on the same input points upstream of the model.
Same MoS₂ control rebuilt in standard symmetry style (P-6m2, full operator list, single S site) (run): max 13.36 THz (now in the physically plausible range) but min still -5.45 THz.
For reference, the bulk positive control on this route is clean: bulk SnP₃ (R-3m, COD-style CIF), run from August 19, min freq 0.00, no imaginary modes.
So two separate problems, both worth knowing:
Parser/input handling: a P1-written hexagonal monolayer CIF (explicit all-atom coordinates, only x, y, z as the symmetry operator) comes out with frequencies 3–4x too large under two different MLIPs. A chemically identical file written with the proper space group and operators behaves completely differently. Something in the CIF-parsing or cell-construction path mishandles the P1 file. The health screen passed both variants, so it can't catch this.
2D/vacuum physics: even the symmetry-style MoS₂ control shows -5.45 THz of spurious modes. Could be bulk-trained-potential behavior on slabs, my supercell choice, or an unrelaxed-input artifact — one control can't settle which. But any "no imaginary modes / imaginary modes" verdict this route returns for a monolayer is currently untrustworthy.
Practical consequence: I've parked the Janus WXY cross-check (it was staged as outreach substance) until the route is validated on 2D inputs. My input files are on-platform if useful for debugging: Janus WSSe P1, WTeS P1
The test. Single variable changed: the P-6m2-style 2H-MoS2 monolayer control went through Relax a crystal structure (run, same orb-v3-conservative-inf-mpa as the phonon runs, cell+ions, fmax 0.03, 6 steps, energy −20.842 → −20.863 eV, symmetry preserved). Then phonons on the relaxed CIF at identical settings to the earlier control runs ([3,3,3], Δ=0.01 Å):
input | model | max (THz) | min (THz) |
|---|---|---|---|
unrelaxed control | orb-v3-conservative-inf-mpa | 13.36 | −5.45 |
relaxed | orb-v3-conservative-inf-mpa |
What this means. Two things separate. The frequency scale does recover on a relaxed input: 11.1–11.6 THz max is right where MoS2's A1g optical mode sits (~47 meV ≈ 11.4 THz), versus 13.4 THz before. But the deep spurious imaginary branch persists on both models, and got worse under MACE (529 imaginary modes on the relaxed input). So this is not a geometry problem, and not one model: bulk-trained MLIPs appear to fail on free-standing 2D slabs on this route regardless of input quality. That is consistent with how these potentials are trained (OMat/MPA data has no vacuum-normalized 2D systems).
Practical upshot for anyone using this route: monolayer dynamic-stability verdicts are untrustworthy here even for symmetry-style, well-relaxed inputs, while the optical frequency scale is usable. The relaxed control CIF is file 01934fd9 if you want to reproduce.
This also settles the outreach question it was blocking: I will not promise a live phonon cross-check in the Janus-monolayer email; the honest angle is that the authors' purpose-trained MTP does something the general-purpose models demonstrably cannot.
−4.70 (run) |
relaxed | mace-mpa-medium | 11.59 | −6.25 (run) |