{ "cells": [ { "cell_type": "markdown", "id": "intro-md", "metadata": {}, "source": "# Relaxation Settings\n\nASSYST provides several relaxation modes that differ in which degrees of freedom are minimized. They all share the same interface (see {class}`assyst.relaxations.Relax`) but apply different ASE filters and constraints to control whether positions, cell shape, volume, or symmetry are allowed to vary.\n\nThis notebook walks through each option on a small Cu structure using ASE's EMT calculator so the examples run quickly.\n\n| Class | Positions | Cell shape | Volume | Symmetry |\n|---|---|---|---|---|\n| `Relax` | yes | fixed | fixed | free |\n| `CellRelax` | fixed | yes | fixed | free |\n| `VolumeRelax` | fixed | fixed | yes | free |\n| `SymmetryRelax` | yes | yes | yes | preserved |\n| `FullRelax` | yes | yes | yes | free |" }, { "cell_type": "code", "execution_count": 1, "id": "imports", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:06.488206Z", "iopub.status.busy": "2026-05-04T23:56:06.487913Z", "iopub.status.idle": "2026-05-04T23:56:06.974292Z", "shell.execute_reply": "2026-05-04T23:56:06.973188Z" } }, "outputs": [], "source": [ "from assyst.relaxations import Relax, CellRelax, VolumeRelax, SymmetryRelax, FullRelax, relax\n", "\n", "from ase.build import bulk\n", "from ase.calculators.emt import EMT\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "starting-structure-md", "metadata": {}, "source": [ "## Starting structure\n", "\n", "We start from an FCC Cu cell that is slightly distorted away from its equilibrium so that all relaxation modes have something to do." ] }, { "cell_type": "code", "execution_count": 2, "id": "starting-structure", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:06.976856Z", "iopub.status.busy": "2026-05-04T23:56:06.976518Z", "iopub.status.idle": "2026-05-04T23:56:06.984017Z", "shell.execute_reply": "2026-05-04T23:56:06.982978Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "volume = 52.679 A^3\n", "cell =\n", "[[3.774 0.037 0. ]\n", " [0.037 3.663 0. ]\n", " [0. 0. 3.811]]\n" ] } ], "source": [ "def make_structure():\n", " s = bulk('Cu', 'fcc', a=3.7, cubic=True)\n", " # Shear and stretch the cell a little\n", " strain = np.eye(3) + np.array([[0.02, 0.01, 0.0], [0.01, -0.01, 0.0], [0.0, 0.0, 0.03]])\n", " s.set_cell(s.cell.array @ strain, scale_atoms=True)\n", " # Displace one atom\n", " s.positions[0] += [0.05, 0.0, 0.0]\n", " return s\n", "\n", "s0 = make_structure()\n", "print(f'volume = {s0.get_volume():.3f} A^3')\n", "print(f'cell =\\n{s0.cell.array}')" ] }, { "cell_type": "markdown", "id": "helper-md", "metadata": {}, "source": "Helper to run a single relaxation through {func}`assyst.relaxations.relax` and print a summary." }, { "cell_type": "code", "execution_count": 3, "id": "helper", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:06.986259Z", "iopub.status.busy": "2026-05-04T23:56:06.985976Z", "iopub.status.idle": "2026-05-04T23:56:06.990817Z", "shell.execute_reply": "2026-05-04T23:56:06.989516Z" } }, "outputs": [], "source": [ "def show(settings, label):\n", " [out] = list(relax([make_structure()], settings, EMT()))\n", " print(f'--- {label} ---')\n", " print(f'energy = {out.get_potential_energy():.4f} eV')\n", " print(f'volume = {out.get_volume():.3f} A^3')\n", " print(f'cell =\\n{out.cell.array}')\n", " print(f'pos[0] = {out.positions[0]}')\n", " print()\n", " return out" ] }, { "cell_type": "markdown", "id": "relax-md", "metadata": {}, "source": [ "## `Relax` -- positions only\n", "\n", "The base class only relaxes internal positions. Cell shape and volume are kept fixed. This is the right choice if you want to study atomic relaxations at a fixed lattice (e.g. defect cores at a given volume)." ] }, { "cell_type": "code", "execution_count": 4, "id": "relax-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:06.992861Z", "iopub.status.busy": "2026-05-04T23:56:06.992678Z", "iopub.status.idle": "2026-05-04T23:56:07.024825Z", "shell.execute_reply": "2026-05-04T23:56:07.023603Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- Relax (positions) ---\n", "energy = 0.2782 eV\n", "volume = 52.679 A^3\n", "cell =\n", "[[3.774 0.037 0. ]\n", " [0.037 3.663 0. ]\n", " [0. 0. 3.811]]\n", "pos[0] = [1.24991806e-02 4.56971337e-05 2.44048787e-16]\n", "\n" ] }, { "data": { "text/plain": [ "Atoms(symbols='Cu4', pbc=True, cell=[[3.7740000000000005, 0.037000000000000005, 0.0], [0.037000000000000005, 3.6630000000000003, 0.0], [0.0, 0.0, 3.8110000000000004]], calculator=SinglePointCalculator(...))" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show(Relax(force_tolerance=1e-3), 'Relax (positions)')" ] }, { "cell_type": "markdown", "id": "volrelax-md", "metadata": {}, "source": [ "## `VolumeRelax` -- isotropic volume only\n", "\n", "`VolumeRelax` keeps relative atomic positions and the cell shape fixed and only scales the cell isotropically. An optional `pressure` (in eV/A^3) can be applied. Use this to find the equilibrium volume at fixed shape, e.g. for an EOS scan." ] }, { "cell_type": "code", "execution_count": 5, "id": "volrelax-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:07.027218Z", "iopub.status.busy": "2026-05-04T23:56:07.026992Z", "iopub.status.idle": "2026-05-04T23:56:07.109855Z", "shell.execute_reply": "2026-05-04T23:56:07.109030Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- VolumeRelax ---\n", "energy = -0.0066 eV\n", "volume = 46.349 A^3\n", "cell =\n", "[[3.61636196 0.03545453 0. ]\n", " [0.03545453 3.50999837 0. ]\n", " [0. 0. 3.65181649]]\n", "pos[0] = [ 4.79115257e-02 -4.76068290e-20 0.00000000e+00]\n", "\n" ] }, { "data": { "text/plain": [ "Atoms(symbols='Cu4', pbc=True, cell=[[3.616361961945911, 0.03545452903868547, 0.0], [0.03545452903868556, 3.5099983748298618, 0.0], [0.0, 0.0, 3.6518164909846065]], calculator=SinglePointCalculator(...))" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show(VolumeRelax(force_tolerance=1e-3), 'VolumeRelax')" ] }, { "cell_type": "markdown", "id": "cellrelax-md", "metadata": {}, "source": [ "## `CellRelax` -- cell shape at fixed volume\n", "\n", "`CellRelax` allows the cell shape to relax while the volume and the relative atomic positions are constrained. This is useful to determine an equilibrium cell shape independent of volume." ] }, { "cell_type": "code", "execution_count": 6, "id": "cellrelax-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:07.112004Z", "iopub.status.busy": "2026-05-04T23:56:07.111824Z", "iopub.status.idle": "2026-05-04T23:56:07.360880Z", "shell.execute_reply": "2026-05-04T23:56:07.359414Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- CellRelax ---\n", "energy = 0.2749 eV\n", "volume = 52.679 A^3\n", "cell =\n", "[[ 3.74999627e+00 4.22598103e-04 -2.74415677e-16]\n", " [-5.58519886e-04 3.74761880e+00 1.05826558e-15]\n", " [-2.89973337e-16 -1.41779516e-16 3.74842220e+00]]\n", "pos[0] = [ 4.96869808e-02 -4.95970383e-04 -3.77760288e-18]\n", "\n" ] }, { "data": { "text/plain": [ "Atoms(symbols='Cu4', pbc=True, cell=[[3.749996272558185, 0.00042259810312605706, -2.7441567748951744e-16], [-0.0005585198857762368, 3.7476187968640704, 1.0582655815858194e-15], [-2.8997333667320494e-16, -1.4177951621226878e-16, 3.7484221994809563]], calculator=SinglePointCalculator(...))" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show(CellRelax(force_tolerance=1e-3), 'CellRelax')" ] }, { "cell_type": "markdown", "id": "symrelax-md", "metadata": {}, "source": [ "## `SymmetryRelax` -- full relaxation, preserving space group\n", "\n", "`SymmetryRelax` relaxes positions and cell, but uses ASE's `FixSymmetry` constraint to keep the initial space group of the structure. This is useful when you want to reach the energy minimum of a given symmetry-constrained crystal without accidentally hopping into a different polymorph." ] }, { "cell_type": "code", "execution_count": 7, "id": "symrelax-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:07.363147Z", "iopub.status.busy": "2026-05-04T23:56:07.362966Z", "iopub.status.idle": "2026-05-04T23:56:07.928600Z", "shell.execute_reply": "2026-05-04T23:56:07.927165Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- SymmetryRelax ---\n", "energy = -0.0281 eV\n", "volume = 46.260 A^3\n", "cell =\n", "[[ 3.58982183e+00 4.43425047e-04 0.00000000e+00]\n", " [-4.86369808e-04 3.58975972e+00 0.00000000e+00]\n", " [ 0.00000000e+00 0.00000000e+00 3.58979071e+00]]\n", "pos[0] = [ 0.01193187 -0.00010575 0. ]\n", "\n" ] }, { "data": { "text/plain": [ "Atoms(symbols='Cu4', pbc=True, cell=[[3.5898218289953747, 0.0004434250465215887, 0.0], [-0.0004863698075401491, 3.5897597186116306, 0.0], [0.0, 0.0, 3.5897907114688534]], calculator=SinglePointCalculator(...))" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show(SymmetryRelax(force_tolerance=1e-3), 'SymmetryRelax')" ] }, { "cell_type": "markdown", "id": "fullrelax-md", "metadata": {}, "source": [ "## `FullRelax` -- everything free\n", "\n", "`FullRelax` relaxes positions and the full cell with no constraints. This may break the original symmetry of the structure but yields the true local minimum for the given calculator." ] }, { "cell_type": "code", "execution_count": 8, "id": "fullrelax-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:07.931026Z", "iopub.status.busy": "2026-05-04T23:56:07.930819Z", "iopub.status.idle": "2026-05-04T23:56:08.447796Z", "shell.execute_reply": "2026-05-04T23:56:08.446179Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- FullRelax ---\n", "energy = -0.0281 eV\n", "volume = 46.260 A^3\n", "cell =\n", "[[ 3.58982183e+00 4.43425047e-04 -3.17074887e-17]\n", " [-4.86369808e-04 3.58975972e+00 3.47731333e-16]\n", " [-1.64591795e-17 -1.69308377e-15 3.58979071e+00]]\n", "pos[0] = [ 1.19318730e-02 -1.05747012e-04 -6.55088215e-15]\n", "\n" ] }, { "data": { "text/plain": [ "Atoms(symbols='Cu4', pbc=True, cell=[[3.589821828995376, 0.00044342504652072906, -3.170748871808039e-17], [-0.0004863698075419537, 3.5897597186116235, 3.477313332380583e-16], [-1.6459179464011644e-17, -1.6930837749666468e-15, 3.589790711468833]], calculator=SinglePointCalculator(...))" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show(FullRelax(force_tolerance=1e-3), 'FullRelax')" ] }, { "cell_type": "markdown", "id": "pressure-md", "metadata": {}, "source": [ "## Applying pressure\n", "\n", "`VolumeRelax`, `SymmetryRelax`, and `FullRelax` accept a `pressure` argument (units of eV/A^3, the ASE convention). A positive pressure compresses the cell, a negative pressure expands it. This is convenient to generate structures off the equilibrium volume for the training set." ] }, { "cell_type": "code", "execution_count": 9, "id": "pressure-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:08.450561Z", "iopub.status.busy": "2026-05-04T23:56:08.450317Z", "iopub.status.idle": "2026-05-04T23:56:10.069514Z", "shell.execute_reply": "2026-05-04T23:56:10.068326Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- SymmetryRelax @ p=-0.05 eV/A^3 ---\n", "energy = 0.0594 eV\n", "volume = 49.562 A^3\n", "cell =\n", "[[ 3.67334413e+00 4.53060207e-04 0.00000000e+00]\n", " [-4.72114196e-04 3.67326515e+00 0.00000000e+00]\n", " [ 0.00000000e+00 0.00000000e+00 3.67309008e+00]]\n", "pos[0] = [ 0.01213643 -0.00011955 0. ]\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "--- SymmetryRelax @ p=0.0 eV/A^3 ---\n", "energy = -0.0281 eV\n", "volume = 46.260 A^3\n", "cell =\n", "[[ 3.58982183e+00 4.43425047e-04 0.00000000e+00]\n", " [-4.86369808e-04 3.58975972e+00 0.00000000e+00]\n", " [ 0.00000000e+00 0.00000000e+00 3.58979071e+00]]\n", "pos[0] = [ 0.01193187 -0.00010575 0. ]\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "--- SymmetryRelax @ p=0.05 eV/A^3 ---\n", "energy = 0.0292 eV\n", "volume = 43.860 A^3\n", "cell =\n", "[[ 3.52662319e+00 4.61513352e-04 0.00000000e+00]\n", " [-4.82133340e-04 3.52658468e+00 0.00000000e+00]\n", " [ 0.00000000e+00 0.00000000e+00 3.52660237e+00]]\n", "pos[0] = [ 0.01172025 -0.00010472 0. ]\n", "\n" ] } ], "source": [ "for p in (-0.05, 0.0, 0.05):\n", " show(SymmetryRelax(force_tolerance=1e-3, pressure=p), f'SymmetryRelax @ p={p} eV/A^3')" ] }, { "cell_type": "markdown", "id": "algorithm-md", "metadata": {}, "source": [ "## Optimizer and convergence\n", "\n", "All relaxation classes share three knobs:\n", "\n", "* `algorithm` -- `'LBFGS'` (default), `'BFGS'`, or `'FIRE'`. LBFGS is usually fastest for well-behaved systems; FIRE is more robust for noisy or stiff potentials.\n", "* `force_tolerance` -- maximum residual force (eV/A) at which the optimizer stops.\n", "* `max_steps` -- hard cap on the number of optimizer steps.\n" ] }, { "cell_type": "code", "execution_count": 10, "id": "algorithm-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:10.071808Z", "iopub.status.busy": "2026-05-04T23:56:10.071631Z", "iopub.status.idle": "2026-05-04T23:56:12.062955Z", "shell.execute_reply": "2026-05-04T23:56:12.061446Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- FullRelax / LBFGS ---\n", "energy = -0.0281 eV\n", "volume = 46.260 A^3\n", "cell =\n", "[[ 3.58982183e+00 4.43425047e-04 -3.17074887e-17]\n", " [-4.86369808e-04 3.58975972e+00 3.47731333e-16]\n", " [-1.64591795e-17 -1.69308377e-15 3.58979071e+00]]\n", "pos[0] = [ 1.19318730e-02 -1.05747012e-04 -6.55088215e-15]\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "--- FullRelax / BFGS ---\n", "energy = -0.0281 eV\n", "volume = 46.260 A^3\n", "cell =\n", "[[ 3.58982183e+00 4.43425047e-04 -4.10397021e-16]\n", " [-4.86369808e-04 3.58975972e+00 1.84800982e-15]\n", " [-4.51102644e-16 1.81496158e-15 3.58979071e+00]]\n", "pos[0] = [ 1.19318730e-02 -1.05747012e-04 -1.31728955e-15]\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "--- FullRelax / FIRE ---\n", "energy = -0.0281 eV\n", "volume = 46.262 A^3\n", "cell =\n", "[[ 3.59016243e+00 4.25469374e-04 8.57522550e-18]\n", " [-4.74899174e-04 3.58954000e+00 5.16658685e-16]\n", " [ 1.27749967e-17 -9.82100490e-16 3.58981764e+00]]\n", "pos[0] = [ 1.18927460e-02 -1.19549147e-04 -4.20777233e-16]\n", "\n" ] } ], "source": [ "for algo in ('LBFGS', 'BFGS', 'FIRE'):\n", " show(FullRelax(algorithm=algo, force_tolerance=1e-3, max_steps=200), f'FullRelax / {algo}')" ] }, { "cell_type": "markdown", "id": "batch-md", "metadata": {}, "source": [ "## Relaxing many structures\n", "\n", "`relax` is a generator: pass it any iterable of `Atoms` and it yields the relaxed copies one by one. Each output structure has a `SinglePointCalculator` attached with the final energy, forces, and stress, ready to be written to a training file.\n", "\n", "To illustrate this we relax four different fcc metals supported by EMT (Cu, Ag, Au, Ni). Each settles at its own equilibrium lattice parameter rather than collapsing to a single common value." ] }, { "cell_type": "code", "execution_count": 11, "id": "batch-cell", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T23:56:12.065216Z", "iopub.status.busy": "2026-05-04T23:56:12.065027Z", "iopub.status.idle": "2026-05-04T23:56:12.703472Z", "shell.execute_reply": "2026-05-04T23:56:12.701670Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cu a = 3.590 A E = -0.0281 eV\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Ag a = 4.064 A E = -0.0015 eV\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Au a = 4.056 A E = -0.0005 eV\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Ni a = 3.487 A E = -0.0532 eV\n" ] } ], "source": [ "structures = [bulk(el, 'fcc', cubic=True) for el in ('Cu', 'Ag', 'Au', 'Ni')]\n", "settings = SymmetryRelax(force_tolerance=1e-3)\n", "for s in relax(structures, settings, EMT()):\n", " a = s.cell.array.diagonal().mean() # cubic, so a = side length\n", " print(f'{s.symbols[0]:2s} a = {a:.3f} A E = {s.get_potential_energy():.4f} eV')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.11.15" } }, "nbformat": 4, "nbformat_minor": 5 }