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
44 changes: 42 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,15 @@ jobs:

docs:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned this is being done on the docs workflow.

Copy link
Copy Markdown
Member

@ElliottKasoar ElliottKasoar May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case it's not clear, @junwen94, the docs that are built during ci.yml (which runs on every push, and for every PR) do not run the notebook tutorials (make all), they just check the docs build from the .rst files (make html).

The tutorials will be run as part of the publish-on-pypi.yml workflow, which is triggered on new tagged releases, so you should still test locally that both make commands work.

You will need installations in ci.yml, but only for the tests section of the workflow, as we have for PLUMED (assuming, as I suggest elsewhere, we actually run tdep, and not just test the input generation).

runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
defaults:
run:
shell: bash -el {0}
steps:
- uses: actions/checkout@v4

- name: Check all sources documented
shell: bash
run: |
diff -y --suppress-common-lines \
<(git ls-files janus_core/** | sed '/.py$/!d; s/\/__init__.py//; s#/#.#g; s/.py$//' | sort) \
Expand All @@ -101,7 +105,43 @@ jobs:
- name: Install pandoc
uses: pandoc/actions/setup@v1

- name: Install dependencies
- name: Install poppler-utils and gnuplot
shell: bash
run: sudo apt-get update && sudo apt-get install -y poppler-utils gnuplot-nox
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add gnuplot? Poppler utils? Are these needed for users? none of this is documented anywhere.


- name: Set up Miniconda for TDEP
uses: conda-incubator/setup-miniconda@v3
with:
miniconda-version: latest
auto-update-conda: true
activate-environment: tdep
python-version: "3.10"

- name: Install TDEP build deps via conda-forge
run: |
conda install -c conda-forge -y \
gfortran openmpi-mpifort scalapack fftw hdf5

- name: Cache TDEP build
id: cache-tdep
uses: actions/cache@v4
with:
path: ~/tdep
key: tdep-${{ runner.os }}-conda-v1

- name: Clone & build TDEP
if: steps.cache-tdep.outputs.cache-hit != 'true'
run: |
git clone --depth 1 https://github.com/tdep-developers/tdep.git ~/tdep
cd ~/tdep
cp examples/build/important_settings.conda important_settings
sed -i "s|^PREFIX=.*|PREFIX=$CONDA_PREFIX|" important_settings
./build_things.sh --nthreads_make 2

- name: Add TDEP to PATH
run: echo "$HOME/tdep/bin" >> $GITHUB_PATH

- name: Install Python dependencies
run: |
uv sync --extra mace
uv pip install --reinstall pynvml
Expand Down
10 changes: 10 additions & 0 deletions docs/source/apidoc/janus_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,16 @@ janus\_core.processing.post\_process module
:undoc-members:
:show-inheritance:

janus\_core.processing.tdep module
----------------------------------

.. automodule:: janus_core.processing.tdep
:members:
:special-members:
:private-members:
:undoc-members:
:show-inheritance:

janus\_core.processing.symmetry module
--------------------------------------

Expand Down
5 changes: 5 additions & 0 deletions docs/source/examples/full/nvt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ rescale_every: 50

post_process_kwargs:
rdf_compute: True
tdep_compute: True
tdep_unit_cell_file: "tests/data/NaCl.cif"
tdep_supercell_file: "tests/data/NaCl.cif"
tdep_output_dir: "tdep-inputs"

correlation_kwargs:
vaf:
a: Velocity
Expand Down
231 changes: 231 additions & 0 deletions docs/source/tutorials/python/anharmonic_phonons.ipynb
Copy link
Copy Markdown
Member

@ElliottKasoar ElliottKasoar Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally these are run automatically via nbsphinx, so we don't usually save the output files (e.g. the png) or any of the notebook cell outputs, and it means we can test this all works when we build for the docs these on release.

Is there any reason not to do the same for this?

Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "acfd5c8f",
"metadata": {},
"source": [
"# Anharmonic Phonons From NVT Molecular Dynamics\n",
"\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/stfc/janus-core/blob/main/docs/source/tutorials/python/anharmonic_phonons.ipynb)\n",
"\n",
"This tutorial shows how to generate standard [TDEP](https://tdep-developers.github.io/tdep/) input files through `post_process_kwargs` while running an NVT molecular dynamics simulation with `janus-core`, and then use them to compute anharmonic phonon properties with TDEP.\n",
"\n",
"The workflow is:\n",
"\n",
"1. Build a unit cell and supercell.\n",
"2. Run a small NVT molecular dynamics simulation with `janus-core`.\n",
"3. Automatically generate standard TDEP input files.\n",
"4. Run TDEP separately to extract force constants and compute anharmonic phonon properties.\n",
"\n",
"> [!NOTE]\n",
"> The `janus-core` part of this tutorial can be run directly in Python, but the TDEP commands require a local environment where TDEP is installed.\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9751041e",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"\n",
"from ase.build import bulk\n",
"from ase.io import write\n",
"from IPython.display import Image\n",
"\n",
"from janus_core.calculations.md import NVT\n",
"from janus_core.calculations.single_point import SinglePoint\n"
]
},
{
"cell_type": "markdown",
"id": "5b840b9c",
"metadata": {},
"source": [
"## Build The Unit Cell And Supercell\n",
"\n",
"We first construct a rocksalt NaCl unit cell and expand it to a `2 x 2 x 2` supercell.\n",
"\n",
"The unit-cell structure is used to generate `infile.ucposcar`, while the supercell structure is used both for the molecular dynamics simulation and to generate `infile.ssposcar`.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9cfcc50b",
"metadata": {},
"outputs": [],
"source": [
"unit_cell = bulk(\"NaCl\", \"rocksalt\", a=5.63, cubic=True)\n",
"supercell = unit_cell * (2, 2, 2)\n",
"\n",
"write(\"unit_cell.cif\", unit_cell)\n",
"write(\"supercell.cif\", supercell)\n",
"\n",
"Path(\"unit_cell.cif\"), Path(\"supercell.cif\")"
]
},
{
"cell_type": "markdown",
"id": "036f06e1",
"metadata": {},
"source": [
"## Run NVT Molecular Dynamics And Generate TDEP Inputs\n",
"\n",
"We now run a small NVT molecular dynamics simulation using the supercell structure.\n",
"\n",
"TDEP input generation is enabled through `post_process_kwargs`. This automatically writes the following files into `tdep-inputs/` after the MD run finishes:\n",
"\n",
"- `infile.ucposcar`\n",
"- `infile.ssposcar`\n",
"- `infile.positions`\n",
"- `infile.forces`\n",
"- `infile.stat`\n",
"- `infile.meta`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b3c201e8",
"metadata": {},
"outputs": [],
"source": [
"single_point = SinglePoint(\n",
" struct=\"supercell.cif\",\n",
" arch=\"mace_mp\",\n",
" model=\"small\",\n",
")\n",
"\n",
"nvt = NVT(\n",
" struct=single_point.struct,\n",
" temp=300.0,\n",
" steps=200,\n",
" timestep=1.0,\n",
" traj_every=20,\n",
" stats_every=20,\n",
" file_prefix=\"janus_results/NaCl-nvt-T300\",\n",
" post_process_kwargs={\n",
" \"tdep_compute\": True,\n",
" \"tdep_unit_cell_file\": \"unit_cell.cif\",\n",
" \"tdep_supercell_file\": \"supercell.cif\",\n",
" \"tdep_output_dir\": \"janus_results/tdep-inputs\",\n",
" },\n",
")\n",
"\n",
"nvt.run()"
]
},
{
"cell_type": "markdown",
"id": "28caa974",
"metadata": {},
"source": [
"## Check The Generated TDEP Input Files\n",
"\n",
"After the molecular dynamics simulation completes, the TDEP input files should be available in the `tdep-inputs/` directory.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c69c025",
"metadata": {},
"outputs": [],
"source": [
"required_files = [\n",
" \"infile.ucposcar\",\n",
" \"infile.ssposcar\",\n",
" \"infile.positions\",\n",
" \"infile.forces\",\n",
" \"infile.stat\",\n",
" \"infile.meta\",\n",
"]\n",
"\n",
"for name in required_files:\n",
" path = Path(\"janus_results/tdep-inputs\") / name\n",
" print(name, path.exists(), path)"
]
},
{
"cell_type": "markdown",
"id": "54941085",
"metadata": {},
"source": [
"## Run TDEP And Plot The Phonon Dispersion\n",
"\n",
"With the input files in place, we run the [TDEP](https://tdep-developers.github.io/tdep/) executables directly from the notebook. The `-p` flag asks TDEP to emit a gnuplot script that targets a PDF; we then convert that PDF to a PNG with `pdftoppm` so the figure can be embedded inline.\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d2fbdb8",
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"set -e\n",
"cd janus_results/tdep-inputs\n",
"\n",
"extract_forceconstants -rc2 100\n",
"cp outfile.forceconstant infile.forceconstant\n",
"phonon_dispersion_relations --dos -p\n",
"gnuplot outfile.dispersion_relations.gnuplot_pdf\n",
"pdftoppm -png -r 150 outfile.dispersion_relations.pdf outfile.dispersion_relations\n"
]
},
{
"cell_type": "markdown",
"id": "4246dbef",
"metadata": {},
"source": [
"## Inspect The Phonon Dispersion\n",
"\n",
"The figure below is the TDEP-generated phonon dispersion rendered by gnuplot.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cba90ea0",
"metadata": {},
"outputs": [],
"source": [
"Image(filename=\"janus_results/tdep-inputs/outfile.dispersion_relations-1.png\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "janus-core",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
},
"nbsphinx": {
"execute": "always",
"timeout": 600
}
},
"nbformat": 4,
"nbformat_minor": 5
}
1 change: 1 addition & 0 deletions docs/source/tutorials/python/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Python Tutorials
phonons
neb
elasticity
anharmonic_phonons
20 changes: 18 additions & 2 deletions janus_core/calculations/md.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
)
from janus_core.processing.correlator import Correlation
from janus_core.processing.post_process import compute_rdf, compute_vaf
from janus_core.processing.tdep import build_tdep_inputs_from_nvt

units = create_units("2014")
DENS_FACT = (units.m / 1.0e2) ** 3 / units.mol
Expand Down Expand Up @@ -1282,11 +1283,11 @@ def _post_process(self) -> None:
# Nothing to do
if not any(
self.post_process_kwargs.get(kwarg, None)
for kwarg in ("rdf_compute", "vaf_compute")
for kwarg in ("rdf_compute", "vaf_compute", "tdep_compute")
):
warn(
"Post-processing arguments present, but no computation requested. "
"Please set either 'rdf_compute' or 'vaf_compute' "
"Please set 'rdf_compute' or 'vaf_compute' or 'tdep_compute' "
"to do post-processing.",
stacklevel=2,
)
Expand Down Expand Up @@ -1331,6 +1332,21 @@ def _post_process(self) -> None:
atoms_filter=self.post_process_kwargs.get("vaf_atoms", None),
)

if self.post_process_kwargs.get("tdep_compute", False):
tdep_output_dir = self.post_process_kwargs.get("tdep_output_dir", None)
if tdep_output_dir is None:
tdep_output_dir = self._build_filename("tdep-inputs", filename=None)

build_tdep_inputs_from_nvt(
unit_cell_file=self.post_process_kwargs["tdep_unit_cell_file"],
supercell=self.post_process_kwargs["tdep_supercell_file"],
traj_file=self.traj_file,
stats_file=self.stats_file,
temperature=self.temp,
output_dir=tdep_output_dir,
timestep=0.0,
)

def _write_restart(self) -> None:
"""Write restart file and (optionally) rotate files saved."""
step = self.offset + self.dyn.nsteps
Expand Down
5 changes: 5 additions & 0 deletions janus_core/helpers/janus_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ class PostProcessKwargs(TypedDict, total=False):
vaf_stop: int | None
vaf_step: int
vaf_output_files: Sequence[PathLike] | None
# TDEP
tdep_compute: bool
tdep_output_dir: PathLike | None
tdep_unit_cell_file: PathLike
tdep_supercell_file: PathLike


class Correlation(TypedDict, total=True):
Expand Down
Loading