---
title: "Phonon format"
description: "The .phonons file format Ouro renders as an interactive phonon dispersion and density of states"
date: "2026-09-25"
last_updated: "2026-09-25"
---

Ouro renders any file ending in `.phonons` as an interactive phonon
dispersion, with the phonon density of states beside it. The format is open:
any service, script, or agent that writes a file following this page gets the
same viewer that Ouro's phonon route uses.

The file carries frequencies you already computed. The viewer plots them as
given and derives only what follows directly from them, such as whether the
structure is dynamically stable.

## What the viewer shows

The viewer draws each branch along the high-symmetry path, with labeled
q-points. Imaginary modes, written as negative frequencies, are drawn below
zero in a second color. When the file carries a density of states, the DOS is
drawn beside the branches on the same frequency axis.

The viewer reports whether the structure is dynamically stable. If it isn't,
it gives the lowest frequency, where it occurs, and every part of the path
where modes are imaginary. It treats frequencies below −0.3 THz as imaginary;
shallower dips are usually numerical noise on the acoustic branches near Γ.

Readers can hover a branch to read its frequency and, with
[mode projections](#mode-projections), which elements move in that mode.
Projections also let readers choose an element and see the branches thicken
where its atoms move. Readers can switch between THz, meV, and cm⁻¹.

Embedded in a post, the file shows with a compact hover card.

## Encoding

The file is a single UTF-8 JSON object. Upload it with the `.phonons`
extension; the content type is `application/json`.

Frequencies are in THz, with imaginary frequencies written as negative
numbers, the convention phonopy and most phonon codes follow.

| Field         | Type       | Required | Meaning                                                                                                  |
| ------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `format`      | string     | Yes      | Always `"ouro.phonons"`                                                                                  |
| `version`     | integer    | Yes      | Always `1` for the format on this page                                                                   |
| `qpoints`     | object     | Yes      | The path, described under [Q-points](#q-points)                                                          |
| `frequencies` | number[][] | Yes      | Frequencies in THz, indexed `[branch][q-point]`                                                          |
| `projections` | object[]   | No       | Which elements move in each mode, described under [Mode projections](#mode-projections)                  |
| `dos`         | object     | No       | A density of states to draw beside the branches, described under [Density of states](#density-of-states) |
| `force_model` | string     | No       | Label for the method behind the force constants, such as `"PBE"` or an interatomic potential             |

Each branch is a list with one frequency per q-point, in path order. A
structure with _n_ atoms in its primitive cell has 3*n* branches. The viewer
draws each branch index as one line and doesn't reorder them, so branches
sorted by frequency at each q-point, as phonopy writes them, are fine.

### Q-points

The path is encoded exactly as the k-point path of a
[band structure file](/docs/developers/electronic-structure-format#k-points).

| Field       | Type     | Required | Meaning                                                          |
| ----------- | -------- | -------- | ---------------------------------------------------------------- |
| `distances` | number[] | Yes      | Cumulative distance along the path, one per q-point              |
| `labels`    | object[] | Yes      | High-symmetry points, each `{"index": integer, "label": string}` |

`distances` starts at 0 and never decreases. Its units only set the relative
widths of the path's segments. Where the path jumps between points that
aren't connected, the last q-point before the jump and the first after it
share the same distance, and each carries its own label; the viewer breaks the
lines there and labels the tick `U|K`. Write labels as readers should see
them, such as `"Γ"` or `"X₁"`, rather than as LaTeX.

### Mode projections

Each projection gives one element's share of every mode: the squared
amplitude of the mode's eigenvector on that element's atoms.

| Field     | Type       | Required | Meaning                                                             |
| --------- | ---------- | -------- | ------------------------------------------------------------------- |
| `element` | string     | Yes      | Element symbol, such as `"Na"`                                      |
| `weights` | number[][] | Yes      | Shares from 0 to 1, indexed like `frequencies`: `[branch][q-point]` |

A mode's shares across all elements sum to 1. Two decimals are enough and keep
the file small. The viewer ignores projections for a structure with one
element, where every share is the whole.

### Density of states

| Field         | Type     | Required | Meaning                                                                                  |
| ------------- | -------- | -------- | ---------------------------------------------------------------------------------------- |
| `frequencies` | number[] | Yes      | Frequency grid in THz, ascending                                                         |
| `total`       | number[] | Yes      | Total DOS in states/THz, one value per grid frequency                                    |
| `projections` | object[] | No       | The DOS on each element, each `{"element": string, "values": number[]}` on the same grid |

Sample the DOS on a mesh through the whole Brillouin zone, not along the
path.

## Example

A two-atom structure along a path with one jump, with a small imaginary mode
at X. A real file has many more q-points and DOS values:

```json
{
  "format": "ouro.phonons",
  "version": 1,
  "force_model": "PBE",
  "qpoints": {
    "distances": [0, 0.5, 1.0, 1.0, 1.4],
    "labels": [
      { "index": 0, "label": "Γ" },
      { "index": 2, "label": "X" },
      { "index": 3, "label": "K" },
      { "index": 4, "label": "Γ" }
    ]
  },
  "frequencies": [
    [0, -0.4, -0.9, 2.1, 0],
    [0, 2.2, 3.0, 3.1, 0],
    [0, 2.4, 3.2, 3.3, 0],
    [7.8, 7.5, 7.1, 7.2, 7.8],
    [7.8, 7.6, 7.3, 7.4, 7.8],
    [8.4, 8.1, 7.9, 8.0, 8.4]
  ],
  "projections": [
    {
      "element": "Na",
      "weights": [
        [0.3, 0.6, 0.9, 0.8, 0.3],
        [0.3, 0.6, 0.8, 0.7, 0.3],
        [0.3, 0.6, 0.8, 0.7, 0.3],
        [0.7, 0.4, 0.2, 0.3, 0.7],
        [0.7, 0.4, 0.2, 0.3, 0.7],
        [0.7, 0.4, 0.1, 0.2, 0.7]
      ]
    },
    {
      "element": "Cl",
      "weights": [
        [0.7, 0.4, 0.1, 0.2, 0.7],
        [0.7, 0.4, 0.2, 0.3, 0.7],
        [0.7, 0.4, 0.2, 0.3, 0.7],
        [0.3, 0.6, 0.8, 0.7, 0.3],
        [0.3, 0.6, 0.8, 0.7, 0.3],
        [0.3, 0.6, 0.9, 0.8, 0.3]
      ]
    }
  ],
  "dos": {
    "frequencies": [-1, 0, 2, 4, 6, 8, 10],
    "total": [0.05, 0.1, 0.8, 0.6, 0.2, 1.4, 0],
    "projections": [
      { "element": "Na", "values": [0.04, 0.06, 0.6, 0.4, 0.1, 0.4, 0] },
      { "element": "Cl", "values": [0.01, 0.04, 0.2, 0.2, 0.1, 1.0, 0] }
    ]
  }
}
```

The lowest branch drops to −0.9 THz at X, below the −0.3 THz threshold, so the
viewer reports the structure as dynamically unstable, with imaginary modes
along Γ–X and at X. Na moves in 90% of that mode.

## Writing files from phonopy

If you compute phonons with [phonopy](https://phonopy.github.io/phonopy/), the
[Python SDK](/docs/developers/libraries) serializes a `Phonopy` object whose
force constants are produced, adding the element projections and a DOS on a
full-zone mesh:

```python showLineNumbers
import json

from ouro import Ouro
from ouro.utils.phonons import PHONONS_EXTENSION, phonons_to_dict

ouro = Ouro()

phonon.produce_force_constants()
data = phonons_to_dict(phonon, force_model="Orb v3")
ouro.files.create(
    name="NaCl phonons",
    visibility="public",
    file_content=json.dumps(data).encode(),
    file_name=f"NaCl.{PHONONS_EXTENSION}",
)
```

`phonons_to_dict` runs phonopy's high-symmetry band structure and DOS mesh on
the object you pass, replacing any band structure or mesh results it held.

## Returning files from a route

A [route](/docs/concepts/routes) that computes phonons declares its output as
a file with the `phonons` extension, and the result renders with the viewer
wherever the route's output appears. See
[route input and output assets](/guides/route-input-output-assets) for how to
declare outputs.

## Versioning

The `version` field changes only for changes that would break existing readers.
Readers should reject versions they don't know, as Ouro's viewer does, and
ignore fields they don't recognize, so optional fields can be added within a
version.
