diff --git a/config.yml b/config.yml
index 7b88d0f05..5c9cca38b 100644
--- a/config.yml
+++ b/config.yml
@@ -66,7 +66,7 @@ notebooks:
- ipywidgets
- tabulate==0.9.0
- mat3ra-utils
- - mat3ra-api-client>=2026.10.8.post0
+ - emfs:/drive/packages/mat3ra_api_client-2026.11.4.post1.dev7+gb5f25f7-py3-none-any.whl
- name: dataframe
packages_pyodide:
- jinja2
@@ -79,7 +79,7 @@ notebooks:
- emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl
- mat3ra-standata
- mat3ra-ide
- - mat3ra-api-client>=2026.10.8.post0
+ - emfs:/drive/packages/mat3ra_api_client-2026.11.4.post1.dev7+gb5f25f7-py3-none-any.whl
- requests
- mat3ra-ade
- mat3ra-mode
diff --git a/other/materials_designer/specific_examples/Introduction.ipynb b/other/materials_designer/specific_examples/Introduction.ipynb
index 898458bb1..1582835c0 100644
--- a/other/materials_designer/specific_examples/Introduction.ipynb
+++ b/other/materials_designer/specific_examples/Introduction.ipynb
@@ -35,6 +35,7 @@
"| `D-3D-AMO` | Amorphous | *To be added* | — | — |\n",
"| `D-2D-ADA` | Adatom | [Pt Adatom/Island on MoS₂](defect_point_adatom_island_molybdenum_disulfide_platinum.ipynb) | *To be added* | [[10]](#ref10) |\n",
"| `D-2D-ADA` | Adatom | [Adatom on Graphene](defect_surface_adatom_graphene.ipynb) | *To be added* | [[11]](#ref11) |\n",
+ "| `D-2D-ADA` | Adatom | [Ti Adatom on a 5×5 TiN Island](defect_point_adatom_island_titanium_nitride.ipynb) | [Ti Adatom Descent on a 5×5 TiN Island (MACE and DFT)](defect_point_adatom_island_titanium_nitride_SIMULATION.ipynb) | [[14]](#ref14) |\n",
"| `D-2D-GBP` | Grain Boundary Planar | [Grain Boundary in 3D FCC Cu](defect_planar_grain_boundary_3d_fcc_metals_copper.ipynb) | *To be added* | [[13]](#ref13) |\n",
"| `D-2D-ISL` | Island | [TiN Island on Surface](defect_surface_island_titanium_nitride.ipynb) | *To be added* | [[14]](#ref14) |\n",
"| `D-2D-TER` | Terrace | [Step/Terrace on Pt Surface](defect_surface_step_platinum.ipynb) | *To be added* | [[15]](#ref15) |\n",
diff --git a/other/materials_designer/specific_examples/defect_point_adatom_island_titanium_nitride.ipynb b/other/materials_designer/specific_examples/defect_point_adatom_island_titanium_nitride.ipynb
new file mode 100644
index 000000000..02fb97193
--- /dev/null
+++ b/other/materials_designer/specific_examples/defect_point_adatom_island_titanium_nitride.ipynb
@@ -0,0 +1,325 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# Ti Adatom on a 5×5 TiN Island on TiN(001)\n",
+ "\n",
+ "## 0. Introduction\n",
+ "\n",
+ "This tutorial builds the cell of the Ti adatom descent study in the publication:\n",
+ "\n",
+ "> **D. G. Sangiovanni, A. B. Mei, D. Edström, L. Hultman, V. Chirita, I. Petrov, and J. E. Greene**, \"Effects of surface vibrations on interlayer mass transport: Ab initio molecular dynamics investigation of Ti adatom descent pathways and rates from TiN/TiN(001) islands\", Physical Review B 97, 035406 (2018). [DOI: 10.1103/PhysRevB.97.035406](https://doi.org/10.1103/PhysRevB.97.035406)\n",
+ "\n",
+ "The cell (Sec. II.A, Fig. 1b) is a 5×5-atom TiN island, one plane high with Ti corners, on a 12×12×3-atom\n",
+ "TiN(001) slab with 15.3 Å of vacuum: 457 atoms. A Ti adatom is added at each of the sites a, c and i of Fig. 6.\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {},
+ "source": [
+ "## 1. Prepare the Environment\n",
+ "### 1.1. Set up defect parameters\n",
+ "\n",
+ "Each entry of `ADATOM_SITES` places one adatom relative to the N atom with the smaller x in the island's\n",
+ "edge row at the largest y, in units of the Ti–N site spacing d = a/2, with Δz = 0 the island plane; to add a\n",
+ "site, add an entry under a new letter to both `ADATOM_SITES` and `ADATOM_MATERIAL_NAMES`.\n",
+ "\n",
+ "`CELL_SIZE` picks the cell: `\"12x12x3\"`, the manuscript's (12×12 sites, 3 slab planes, 457 atoms), or `\"10x10x2\"`\n",
+ "(10×10 sites, 2 slab planes, 225 atoms). With the bottom slab plane frozen, MACE gives E(c) − E(a) = 0.315 and 0.311 eV,\n",
+ "E(i) − E(a) = −2.247 and −2.109 eV, and E_ads(a) = −3.015 and −3.067 eV on the two."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "MATERIAL_NAME = \"Titanium Nitride\"\n",
+ "MILLER_INDICES = (0, 0, 1)\n",
+ "# Thickness counts conventional layers of two (001) planes; bottom planes are removed after the island is added.\n",
+ "SLAB_THICKNESS = 2\n",
+ "VACUUM = 15.3 # Angstrom\n",
+ "NUMBER_OF_ADDED_LAYERS = 0.5\n",
+ "\n",
+ "# Cell sizes as (xy supercell, island box minimum and maximum in crystal coordinates, bottom planes removed);\n",
+ "# x and y minima one site apart put Ti atoms at the island corners\n",
+ "CELL_SIZES = {\n",
+ " \"12x12x3\": ([[6, 0], [0, 6]], [0.3233, 0.24, 0], [0.7233, 0.64, 1], 1),\n",
+ " \"10x10x2\": ([[5, 0], [0, 5]], [0.29, 0.19, 0], [0.75, 0.65, 1], 2),\n",
+ "}\n",
+ "CELL_SIZE = \"12x12x3\"\n",
+ "XY_SUPERCELL_MATRIX, ISLAND_MIN_COORDINATE, ISLAND_MAX_COORDINATE, PLANES_REMOVED = CELL_SIZES[CELL_SIZE]\n",
+ "\n",
+ "ADATOM_ELEMENT = \"Ti\"\n",
+ "# Adatom sites as (Δx, Δy, Δz) from the island's edge N atom, in units of the Ti–N site spacing;\n",
+ "# Δz = 0 is the island plane. Fig. 6's b and d–h are NEB images between these sites, not positions.\n",
+ "ADATOM_SITES = {\n",
+ " \"a\": (0.5, -0.5, 1), # fourfold hollow on the island beside the edge\n",
+ " \"c\": (0, 0, 1), # atop the edge N\n",
+ " \"i\": (0, 1, 0), # atop the terrace N in front of the edge\n",
+ "}\n",
+ "\n",
+ "ISLAND_MATERIAL_NAME = f\"TiN(001) {CELL_SIZE} island 5x5\"\n",
+ "ADATOM_MATERIAL_NAMES = {\n",
+ " \"a\": f\"{ISLAND_MATERIAL_NAME} + Ti a (FFH island)\",\n",
+ " \"c\": f\"{ISLAND_MATERIAL_NAME} + Ti c (atop-N edge)\",\n",
+ " \"i\": f\"{ISLAND_MATERIAL_NAME} + Ti i (atop-N terrace)\",\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3",
+ "metadata": {},
+ "source": [
+ "### 1.2. Install Packages\n",
+ "The step executes only in Pyodide environment. For other environments, the packages should be installed via `pip install` (see [README](../../README.ipynb))."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.packages import install_packages\n",
+ "\n",
+ "await install_packages(\"made|specific_examples\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "### 1.3. Load input material"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.made.material import Material\n",
+ "from mat3ra.standata.materials import Materials\n",
+ "\n",
+ "material = Material.create(Materials.get_by_name_first_match(MATERIAL_NAME))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "### 1.4. Create and preview Slab"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.made.tools.helpers import create_slab\n",
+ "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials\n",
+ "\n",
+ "slab = create_slab(\n",
+ " crystal=material,\n",
+ " termination_top=None, # Use default termination\n",
+ " miller_indices=MILLER_INDICES,\n",
+ " number_of_layers=SLAB_THICKNESS,\n",
+ " vacuum=VACUUM,\n",
+ " use_orthogonal_c=True,\n",
+ " xy_supercell_matrix=XY_SUPERCELL_MATRIX\n",
+ ")\n",
+ "\n",
+ "visualize_materials(slab)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9",
+ "metadata": {},
+ "source": [
+ "## 2. Create the Island"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from mat3ra.made.tools.analyze.other import get_atom_indices_by_layer\n",
+ "from mat3ra.made.tools.convert import to_ase\n",
+ "from mat3ra.made.tools.entities.coordinate import BoxCoordinateCondition\n",
+ "from mat3ra.made.tools.helpers import create_island\n",
+ "from mat3ra.made.tools.modify import filter_by_ids, remove_vacuum\n",
+ "\n",
+ "coordinate_condition = BoxCoordinateCondition(min_coordinate=ISLAND_MIN_COORDINATE, max_coordinate=ISLAND_MAX_COORDINATE)\n",
+ "\n",
+ "slab_with_island = create_island(\n",
+ " slab=slab,\n",
+ " condition=coordinate_condition,\n",
+ " number_of_added_layers=NUMBER_OF_ADDED_LAYERS,\n",
+ " use_cartesian_coordinates=False,\n",
+ ")\n",
+ "for _ in range(PLANES_REMOVED):\n",
+ " bottom_plane = get_atom_indices_by_layer(slab_with_island)[0]\n",
+ " slab_with_island = filter_by_ids(slab_with_island, ids=bottom_plane, invert=True, reset_ids=True)\n",
+ "slab_with_island = remove_vacuum(slab_with_island, fixed_padding=VACUUM)\n",
+ "slab_with_island.name = ISLAND_MATERIAL_NAME\n",
+ "\n",
+ "atoms = to_ase(slab_with_island)\n",
+ "symbols = np.array(atoms.get_chemical_symbols())\n",
+ "planes = get_atom_indices_by_layer(slab_with_island)\n",
+ "island = planes[-1]\n",
+ "island_x, island_y = atoms.positions[island, 0], atoms.positions[island, 1]\n",
+ "is_corner = ((np.isclose(island_x, island_x.min()) | np.isclose(island_x, island_x.max()))\n",
+ " & (np.isclose(island_y, island_y.min()) | np.isclose(island_y, island_y.max())))\n",
+ "print(f\"{slab_with_island.name}: {len(atoms)} atoms ({sum(symbols == 'Ti')} Ti, {sum(symbols == 'N')} N) \"\n",
+ " f\"in {len(planes)} planes, c = {atoms.cell[2, 2]:.3f} Å\")\n",
+ "print(f\"Island: {len(island)} atoms, corners {symbols[island][is_corner].tolist()}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11",
+ "metadata": {},
+ "source": [
+ "## 3. Add the Adatom at the Sites of Fig. 6\n",
+ "\n",
+ "The paper gives no coordinates for the sites; they follow from the lattice and are measured on the island\n",
+ "built above, from the offsets in `ADATOM_SITES`. Measured from the edge N with the smaller x, a lies between\n",
+ "that N and the edge-centre Ti, the paper's FFH_edge; i is the epitaxial site that extends the island."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.made.tools.helpers import create_defect_point_interstitial\n",
+ "\n",
+ "site_spacing = material.lattice.a / 2\n",
+ "edge_y = island_y.max()\n",
+ "edge_nitrogen_x = min(atoms.positions[index, 0] for index in island\n",
+ " if symbols[index] == \"N\" and np.isclose(atoms.positions[index, 1], edge_y))\n",
+ "island_z = atoms.positions[island, 2].mean()\n",
+ "\n",
+ "adatom_sites = {\n",
+ " site: [edge_nitrogen_x + offset_x * site_spacing, edge_y + offset_y * site_spacing, island_z + offset_z * site_spacing]\n",
+ " for site, (offset_x, offset_y, offset_z) in ADATOM_SITES.items()\n",
+ "}\n",
+ "\n",
+ "materials_with_adatom = []\n",
+ "for site, coordinate in adatom_sites.items():\n",
+ " material_with_adatom = create_defect_point_interstitial(\n",
+ " slab_with_island, coordinate, ADATOM_ELEMENT, \"exact_coordinate\", use_cartesian_coordinates=True\n",
+ " )\n",
+ " material_with_adatom.name = ADATOM_MATERIAL_NAMES[site]\n",
+ " materials_with_adatom.append(material_with_adatom)\n",
+ "\n",
+ " atoms_with_adatom = to_ase(material_with_adatom)\n",
+ " adatom = len(atoms_with_adatom) - 1\n",
+ " distances = np.sort(atoms_with_adatom.get_distances(adatom, range(adatom), mic=True))[:3]\n",
+ " print(f\"{material_with_adatom.name}: {len(atoms_with_adatom)} atoms, \"\n",
+ " f\"adatom's three shortest distances {np.round(distances, 3)} Å\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13",
+ "metadata": {},
+ "source": [
+ "## 4. Visualize the Result(s)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "visualize_materials([{\"material\": material_to_show, \"title\": material_to_show.name, \"rotation\": \"-45x\"}\n",
+ " for material_to_show in [slab_with_island, *materials_with_adatom]])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "15",
+ "metadata": {},
+ "source": [
+ "## 5. Pass data to the outside runtime"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "16",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.io import download_content_to_file\n",
+ "from mat3ra.notebooks_utils.material import set_materials\n",
+ "\n",
+ "materials = [slab_with_island, *materials_with_adatom]\n",
+ "set_materials(materials)\n",
+ "for material_to_save in materials:\n",
+ " download_content_to_file(material_to_save.to_json(), f\"{material_to_save.name}.json\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "17",
+ "metadata": {},
+ "source": [
+ "## References\n",
+ "\n",
+ "[1] D. G. Sangiovanni, A. B. Mei, D. Edström, L. Hultman, V. Chirita, I. Petrov, J. E. Greene, \"Effects of surface\n",
+ "vibrations on interlayer mass transport: Ab initio molecular dynamics investigation of Ti adatom descent pathways\n",
+ "and rates from TiN/TiN(001) islands\", Phys. Rev. B 97, 035406 (2018).\n",
+ "[DOI: 10.1103/PhysRevB.97.035406](https://doi.org/10.1103/PhysRevB.97.035406)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/other/materials_designer/specific_examples/defect_point_adatom_island_titanium_nitride_SIMULATION.ipynb b/other/materials_designer/specific_examples/defect_point_adatom_island_titanium_nitride_SIMULATION.ipynb
new file mode 100644
index 000000000..2a49c229c
--- /dev/null
+++ b/other/materials_designer/specific_examples/defect_point_adatom_island_titanium_nitride_SIMULATION.ipynb
@@ -0,0 +1,808 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# Ti Adatom Descent on a 5×5 TiN Island (MACE and DFT)\n",
+ "\n",
+ "> **D. G. Sangiovanni, A. B. Mei, D. Edström, L. Hultman, V. Chirita, I. Petrov, and J. E. Greene**,\n",
+ "> \"Effects of surface vibrations on interlayer mass transport: Ab initio molecular dynamics investigation of Ti\n",
+ "> adatom descent pathways and rates from TiN/TiN(001) islands\", Physical Review B 97, 035406 (2018).\n",
+ "> [DOI: 10.1103/PhysRevB.97.035406](https://doi.org/10.1103/PhysRevB.97.035406)\n",
+ "\n",
+ "Relaxes a Ti adatom at three sites of the paper's Fig. 6 on the cell created in the\n",
+ "[structure notebook](defect_point_adatom_island_titanium_nitride.ipynb): **a**, the fourfold hollow on the island\n",
+ "next to its edge; **c**, atop the edge N atom; **i**, atop the terrace N atom in front of the edge, the site\n",
+ "that extends the island. The energies of c and i relative to a, and the adsorption energy at a, are compared\n",
+ "with the paper's. The paper computed them with DFT (GGA); here every relaxation uses the\n",
+ "[MACE-MP-0](https://github.com/ACEsuit/mace) machine-learned force field with the D3 dispersion correction.\n",
+ "DFT (PBE) relaxations on the platform, started from the MACE-relaxed structures, give the energies printed beside MACE's.\n",
+ "\n",
+ "The four relaxations are MACE runs on 457 and 458 atoms in the manuscript's cell, meant to be executed natively.\n",
+ "\n",
+ "
Usage
\n",
+ "\n",
+ "1. Create the materials in the [structure notebook](defect_point_adatom_island_titanium_nitride.ipynb) with the same `CELL_SIZE`, which saves the island cell and the three adatom cells to the `uploads` folder.\n",
+ "1. Set the material names and parameters in cells 1.2-1.5 (or use the defaults).\n",
+ "1. Run natively: `pip install \".[all]\" mace-torch torch-dftd`. In JupyterLite the install and the calculator run, but the first force call on the manuscript's cell exceeds the browser's memory (a 0.76 GB tensor), so the relaxations run natively; the browser also has no torch-dftd, so D3 is off there.\n",
+ "1. Click \"Run\" > \"Run All\" to run all cells.\n",
+ "1. Wait for the DFT jobs to finish.\n",
+ "1. Scroll down to view the results.\n",
+ "\n",
+ "## Summary\n",
+ "\n",
+ "1. Set up the environment and parameters: install packages (JupyterLite only), material names, force-field, relaxation, DFT and compute parameters.\n",
+ "1. Load the materials by name from the `uploads` folder.\n",
+ "1. Build the MACE calculator.\n",
+ "1. Relax the island cell; measure the island contraction and the surface ripple.\n",
+ "1. Relax the adatom at a, c and i.\n",
+ "1. Compute the energy of an isolated Ti atom.\n",
+ "1. Run DFT on the platform: a fixed-cell relaxation on the four slab structures, and a total energy on the isolated Ti atom, which has nothing to relax.\n",
+ "1. Compare with the paper: energies relative to a, the adsorption energy at a, and Fig. 6."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {},
+ "source": [
+ "## 1. Set up the environment and parameters\n",
+ "### 1.1. Install packages (JupyterLite)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.calculators.mlff import get_mlff_install_profiles\n",
+ "from mat3ra.notebooks_utils.packages import install_packages\n",
+ "\n",
+ "await install_packages(get_mlff_install_profiles(\"mace\"))\n",
+ "\n",
+ "from mat3ra.notebooks_utils.pyodide.packages.patches import apply_all_patches\n",
+ "\n",
+ "apply_all_patches(\"mace\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3",
+ "metadata": {},
+ "source": [
+ "### 1.2. Material names"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Names saved by defect_point_adatom_island_titanium_nitride.ipynb, with the CELL_SIZE set there.\n",
+ "CELL_SIZE = \"12x12x3\"\n",
+ "ISLAND_MATERIAL_NAME = f\"TiN(001) {CELL_SIZE} island 5x5\"\n",
+ "ADATOM_MATERIAL_NAMES = {\n",
+ " \"a\": f\"{ISLAND_MATERIAL_NAME} + Ti a (FFH island)\",\n",
+ " \"c\": f\"{ISLAND_MATERIAL_NAME} + Ti c (atop-N edge)\",\n",
+ " \"i\": f\"{ISLAND_MATERIAL_NAME} + Ti i (atop-N terrace)\",\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "### 1.3. Parameters\n",
+ "\n",
+ "The DFT settings below — `MAXIMUM_IONIC_STEPS`, `TIME_LIMIT` and the queue — are sized for the `\"10x10x2\"`\n",
+ "cell: to run them, set `CELL_SIZE = \"10x10x2\"` in both notebooks and list the structures to run in\n",
+ "`DFT_MATERIAL_LABELS`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ORGANIZATION_NAME = None\n",
+ "FOLDER = \"./uploads\"\n",
+ "\n",
+ "MACE_MODEL_FAMILY = \"MACE-MP-0\"\n",
+ "MACE_MODEL = \"large\"\n",
+ "MACE_DISPERSION = True\n",
+ "MACE_DEFAULT_DTYPE = \"float64\"\n",
+ "MACE_DEVICE = \"cpu\"\n",
+ "\n",
+ "FMAX = 0.02 # eV/Å\n",
+ "MAX_STEPS = 1000\n",
+ "FROZEN_SLAB_PLANES = 1 # bottom slab planes held fixed in the MACE and DFT relaxations\n",
+ "\n",
+ "# One platform job per structure: a fixed-cell relaxation on the MACE-relaxed slab structures, and a total\n",
+ "# energy on the isolated Ti atom, which has nothing to relax; an empty list skips the platform jobs.\n",
+ "# The structures can be added one at a time, each after the previous job has finished.\n",
+ "DFT_MATERIAL_LABELS = [\n",
+ " # \"slab_with_island\",\n",
+ " # \"a\",\n",
+ " # \"c\",\n",
+ " # \"i\",\n",
+ " # \"isolated_adatom\",\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "### 1.4. DFT model parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# manuscript: VASP, every structure relaxed with DFT; here the DFT relaxation starts from the MACE-relaxed structure\n",
+ "APPLICATION_NAME = \"espresso\"\n",
+ "RELAXATION_SEARCH_TERM = \"fixed_cell_relaxation.json\"\n",
+ "TOTAL_ENERGY_SEARCH_TERM = \"total_energy.json\" # the isolated atom: one SCF, nothing to relax\n",
+ "RELAX_UNIT = \"pw_relax\"\n",
+ "SCF_UNIT = \"pw_scf\"\n",
+ "# manuscript: GGA of Perdew, Burke and Wang (PW91); for PW91 add \"input_dft\": \"pw91\" to DFT_SYSTEM_SETTINGS\n",
+ "FUNCTIONAL = \"pbe\"\n",
+ "PSEUDOPOTENTIAL_TYPE = \"us\" # manuscript: PAW; ours GBRV ultrasoft\n",
+ "ECUTWFC = 40 # Ry; manuscript: 400 eV for the static calculations, Sec. II.B\n",
+ "ECUTRHO = 200 # Ry\n",
+ "DFT_KGRID = [1, 1, 1] # manuscript: Γ only\n",
+ "DFT_SYSTEM_SETTINGS = {\"smearing\": \"mv\", \"degauss\": 0.01} # Ry\n",
+ "MAXIMUM_IONIC_STEPS = 5 # per job, so that it ends within TIME_LIMIT\n",
+ "DAVIDSON_SUBSPACE_DIMENSION = 2 # half the default 4: less memory per process\n",
+ "DFT_CONTROL_SETTINGS = {\"nstep\": MAXIMUM_IONIC_STEPS}\n",
+ "# conv_thr 1e-4 Ry ≈ 1.4 meV on the total energy; local-TF mixing for a slab with vacuum\n",
+ "DFT_ELECTRONS_SETTINGS = {\"conv_thr\": 1e-4, \"mixing_mode\": \"local-TF\", \"mixing_beta\": 0.2,\n",
+ " \"diago_david_ndim\": DAVIDSON_SUBSPACE_DIMENSION}\n",
+ "DFT_SPIN_SETTINGS_BY_LABEL = {\"isolated_adatom\": {\"nspin\": 2, \"tot_magnetization\": 2}} # two unpaired Ti d electrons"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9",
+ "metadata": {},
+ "source": [
+ "### 1.5. Compute parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.ide.compute import QueueName\n",
+ "\n",
+ "CLUSTER_NAME = \"001\"\n",
+ "QUEUE_NAME = QueueName.OF\n",
+ "PPN = 60 # OF's maxPPN: the job is billed for the whole node\n",
+ "TIME_LIMIT = \"08:00:00\"\n",
+ "POLL_INTERVAL = 600 # seconds"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11",
+ "metadata": {},
+ "source": [
+ "## 2. Load the materials"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.material import load_materials_from_folder\n",
+ "\n",
+ "materials_by_name = {material.name: material for material in load_materials_from_folder(FOLDER)}\n",
+ "slab_with_island = materials_by_name[ISLAND_MATERIAL_NAME]\n",
+ "materials_with_adatom = {site: materials_by_name[name] for site, name in ADATOM_MATERIAL_NAMES.items()}\n",
+ "\n",
+ "for material in [slab_with_island, *materials_with_adatom.values()]:\n",
+ " print(f\"{material.name}: {material.basis.number_of_atoms} atoms\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13",
+ "metadata": {},
+ "source": [
+ "## 3. Build the MACE calculator"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import importlib.util\n",
+ "\n",
+ "from mat3ra.notebooks_utils.calculators.mlff import create_mlff_calculator\n",
+ "\n",
+ "dispersion_available = importlib.util.find_spec(\"torch_dftd\") is not None\n",
+ "dispersion_active = MACE_DISPERSION and dispersion_available\n",
+ "if MACE_DISPERSION and not dispersion_available:\n",
+ " print(\"torch-dftd is not available here: MACE runs WITHOUT the D3 dispersion correction.\")\n",
+ "\n",
+ "calculator = create_mlff_calculator(\n",
+ " \"mace\",\n",
+ " {\n",
+ " \"family\": MACE_MODEL_FAMILY,\n",
+ " \"model\": MACE_MODEL,\n",
+ " \"dispersion\": dispersion_active,\n",
+ " \"default_dtype\": MACE_DEFAULT_DTYPE,\n",
+ " \"device\": MACE_DEVICE,\n",
+ " },\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "15",
+ "metadata": {},
+ "source": [
+ "## 4. Relax the island cell\n",
+ "### 4.1. Relax\n",
+ "\n",
+ "The bottom slab plane is held fixed, as in the paper's AIMD runs (Sec. II.A); every other atom is free."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "16",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from mat3ra.made.tools.analyze.other import get_atom_indices_by_layer\n",
+ "from mat3ra.made.tools.calculate import calculate_total_energy\n",
+ "from mat3ra.made.tools.convert import to_ase\n",
+ "from mat3ra.notebooks_utils.workflows.relaxation import relax_material\n",
+ "\n",
+ "frozen_atom_indices = [index for plane in get_atom_indices_by_layer(slab_with_island)[:FROZEN_SLAB_PLANES]\n",
+ " for index in plane]\n",
+ "slab_with_island_relaxed = relax_material(slab_with_island, calculator, fmax=FMAX, max_steps=MAX_STEPS,\n",
+ " fixed_atom_indices=frozen_atom_indices)\n",
+ "energy_slab_with_island = calculate_total_energy(slab_with_island_relaxed, calculator)\n",
+ "print(f\"E(slab + island) = {energy_slab_with_island:.3f} eV\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "17",
+ "metadata": {},
+ "source": [
+ "### 4.2. Island contraction and surface ripple\n",
+ "\n",
+ "The island's Ti–Ti spacing along its diagonals and Ti–N spacing along its medians, against the same spacings as\n",
+ "built on the bulk lattice; the ripple is the height of the N atoms above the Ti atoms in the exposed top plane of\n",
+ "the terrace (Fig. 2 of the paper)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "18",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "built, relaxed = to_ase(slab_with_island), to_ase(slab_with_island_relaxed)\n",
+ "symbols = np.array(built.get_chemical_symbols())\n",
+ "planes = get_atom_indices_by_layer(slab_with_island)\n",
+ "island, terrace = np.array(planes[-1]), np.array(planes[-2])\n",
+ "\n",
+ "island_x, island_y = built.positions[island, 0], built.positions[island, 1]\n",
+ "x_values = (island_x.min(), (island_x.min() + island_x.max()) / 2, island_x.max())\n",
+ "y_values = (island_y.min(), (island_y.min() + island_y.max()) / 2, island_y.max())\n",
+ "grid = {(column, row): island[np.argmin(np.hypot(island_x - x, island_y - y))]\n",
+ " for column, x in enumerate(x_values) for row, y in enumerate(y_values)}\n",
+ "island_rows = {\n",
+ " \"Ti–Ti along diagonals\": [(grid[0, 0], grid[2, 2]), (grid[0, 2], grid[2, 0])],\n",
+ " \"Ti–N along medians\": [(grid[0, 1], grid[2, 1]), (grid[1, 0], grid[1, 2])],\n",
+ "}\n",
+ "spacings_per_row = np.sqrt(len(island)) - 1\n",
+ "\n",
+ "island_contraction = {}\n",
+ "for label, pairs in island_rows.items():\n",
+ " as_built, after = (np.mean([atoms.get_distance(*pair) for pair in pairs]) / spacings_per_row\n",
+ " for atoms in (built, relaxed))\n",
+ " island_contraction[label] = 100 * (after / as_built - 1)\n",
+ " print(f\"{label}: {after:.3f} Å, {island_contraction[label]:+.1f} % against {as_built:.3f} Å as built\")\n",
+ "\n",
+ "island_xy = built.positions[island, :2]\n",
+ "exposed_terrace = [index for index in terrace\n",
+ " if not np.isclose(np.linalg.norm(island_xy - built.positions[index, :2], axis=1), 0).any()]\n",
+ "ripple = (np.mean([relaxed.positions[index, 2] for index in exposed_terrace if symbols[index] == \"N\"])\n",
+ " - np.mean([relaxed.positions[index, 2] for index in exposed_terrace if symbols[index] == \"Ti\"]))\n",
+ "print(f\"Ripple of the exposed terrace ({len(exposed_terrace)} atoms): {ripple:.3f} Å\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "19",
+ "metadata": {},
+ "source": [
+ "## 5. Relax the adatom at a, c and i\n",
+ "\n",
+ "Each relaxation starts from the site placed in the structure notebook; the adatom's displacement from that site\n",
+ "shows whether it stayed there."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "20",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "energies = {}\n",
+ "relaxed_materials_with_adatom = {}\n",
+ "for site, material in materials_with_adatom.items():\n",
+ " material_relaxed = relax_material(material, calculator, fmax=FMAX, max_steps=MAX_STEPS,\n",
+ " fixed_atom_indices=frozen_atom_indices)\n",
+ " relaxed_materials_with_adatom[site] = material_relaxed\n",
+ " energies[site] = calculate_total_energy(material_relaxed, calculator)\n",
+ "\n",
+ " placed, final = to_ase(material), to_ase(material_relaxed)\n",
+ " adatom = len(final) - 1\n",
+ " displacement = np.linalg.norm(final.positions[adatom] - placed.positions[adatom])\n",
+ " distances = np.sort(final.get_distances(adatom, range(adatom), mic=True))[:3]\n",
+ " print(f\"{site}: E = {energies[site]:.3f} eV, adatom moved {displacement:.2f} Å from its site, \"\n",
+ " f\"three shortest distances {np.round(distances, 3)} Å\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "21",
+ "metadata": {},
+ "source": [
+ "### 5.1. Visualize each site as built and relaxed\n",
+ "\n",
+ "Side view along the island edge: the island is on the left, the terrace on the right."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "22",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials\n",
+ "\n",
+ "visualize_materials([{\"material\": materials[site], \"title\": f\"{site} — {state}\", \"rotation\": \"-90x,-90y\"}\n",
+ " for state, materials in ((\"as built\", materials_with_adatom), (\"relaxed\", relaxed_materials_with_adatom))\n",
+ " for site in materials])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "23",
+ "metadata": {},
+ "source": [
+ "## 6. Isolated Ti atom\n",
+ "\n",
+ "The reference of the adsorption energy, Eq. (4) of the paper: one Ti atom at the centre of an otherwise\n",
+ "empty cell the size of the island cell, same calculator. This energy is the model's prediction for a single\n",
+ "atom, outside its training data (periodic crystals); the comparison below prints the paper's −2.275 eV beside it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.esse.models.materials_category_components.entities.core.zero_dimensional.atom import AtomSchema\n",
+ "from mat3ra.made.tools.build_components.entities.core.zero_dimensional.atom.builder import AtomAtCoordinateBuilder\n",
+ "from mat3ra.made.tools.build_components.entities.core.zero_dimensional.atom.configuration import AtomAtCoordinateConfiguration\n",
+ "\n",
+ "adatom_element = materials_with_adatom[\"a\"].basis.elements.values[-1]\n",
+ "isolated_adatom = AtomAtCoordinateBuilder().get_material(AtomAtCoordinateConfiguration(\n",
+ " crystal=slab_with_island, element=AtomSchema(chemical_element=adatom_element), coordinate=[0.5, 0.5, 0.5]))\n",
+ "energy_isolated_adatom = calculate_total_energy(isolated_adatom, calculator)\n",
+ "print(f\"E({adatom_element}, isolated) = {energy_isolated_adatom:.3f} eV\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "25",
+ "metadata": {},
+ "source": [
+ "## 7. DFT on the platform\n",
+ "\n",
+ "One job per structure: a fixed-cell relaxation on the four slab structures, starting from the island cell and the\n",
+ "adatom at a, c and i as MACE relaxed them, with the same bottom slab plane fixed; and a total energy on the\n",
+ "isolated Ti atom, which has nothing to relax, spin-polarized. A finished or running job of the same workflow on\n",
+ "the same structure is reused.\n",
+ "\n",
+ "### 7.1. Authenticate"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "26",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.auth import authenticate\n",
+ "\n",
+ "await authenticate()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "27",
+ "metadata": {},
+ "source": [
+ "### 7.2. Initialize the API client"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "28",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.api_client import APIClient\n",
+ "\n",
+ "client = APIClient.authenticate()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "29",
+ "metadata": {},
+ "source": [
+ "### 7.3. Select the account"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "30",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "selected_account = client.my_account\n",
+ "\n",
+ "if ORGANIZATION_NAME:\n",
+ " selected_account = client.get_account(name=ORGANIZATION_NAME)\n",
+ "\n",
+ "ACCOUNT_ID = selected_account.id\n",
+ "print(f\"Selected account ID: {ACCOUNT_ID}, name: {selected_account.name}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "31",
+ "metadata": {},
+ "source": [
+ "### 7.4. Select the project"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "32",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "projects = client.projects.list({\"isDefault\": True, \"owner._id\": ACCOUNT_ID})\n",
+ "project_id = projects[0][\"_id\"]\n",
+ "print(f\"Using project: {projects[0]['name']} ({project_id})\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "33",
+ "metadata": {},
+ "source": [
+ "### 7.5. Save the structures to the platform"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "34",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.code.array_with_ids import ArrayWithIds\n",
+ "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n",
+ "\n",
+ "dft_materials = {label: material.clone() for label, material in\n",
+ " {\"slab_with_island\": slab_with_island_relaxed, **relaxed_materials_with_adatom}.items()}\n",
+ "for material in dft_materials.values():\n",
+ " material.name = f\"{material.name} (MACE-relaxed)\"\n",
+ " material.basis.constraints = ArrayWithIds.from_values(\n",
+ " [[index not in frozen_atom_indices] * 3 for index in range(material.basis.number_of_atoms)])\n",
+ "dft_materials[\"isolated_adatom\"] = isolated_adatom.clone()\n",
+ "dft_materials[\"isolated_adatom\"].name = f\"{adatom_element} atom {CELL_SIZE}\"\n",
+ "saved_materials = {label: get_or_create_material(client, dft_materials[label], ACCOUNT_ID) for label in DFT_MATERIAL_LABELS}\n",
+ "saved_materials = {label: client.materials.update(saved[\"_id\"], dft_materials[label].to_dict())\n",
+ " for label, saved in saved_materials.items()}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "35",
+ "metadata": {},
+ "source": [
+ "### 7.6. Set the DFT model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "36",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.mode import ModelFactory\n",
+ "from mat3ra.standata.model_tree import ModelTreeStandata\n",
+ "\n",
+ "model_config = ModelTreeStandata.get_model_by_parameters(type=\"dft\", subtype=\"gga\", functional=FUNCTIONAL)\n",
+ "model_config[\"method\"] = {\"type\": \"pseudopotential\", \"subtype\": PSEUDOPOTENTIAL_TYPE}\n",
+ "model = ModelFactory.create(model_config)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "37",
+ "metadata": {},
+ "source": [
+ "### 7.7. Create the compute configuration"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "38",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.ide.compute import Compute\n",
+ "\n",
+ "cluster = next(cluster for cluster in client.clusters.list() if not CLUSTER_NAME or CLUSTER_NAME in cluster[\"hostname\"])\n",
+ "compute = Compute(cluster=cluster, queue=QUEUE_NAME, ppn=PPN, timeLimit=TIME_LIMIT)\n",
+ "print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}, time limit: {TIME_LIMIT}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "39",
+ "metadata": {},
+ "source": [
+ "### 7.8. Create and submit one job per structure"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "40",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.api.job import submit_jobs\n",
+ "from mat3ra.notebooks_utils.core.entity.job.api import find_job_for_material\n",
+ "from mat3ra.notebooks_utils.job import create_job\n",
+ "from mat3ra.notebooks_utils.workflow import apply_planewave_cutoffs, apply_scf_kgrid, patch_workflow_qe_input\n",
+ "from mat3ra.standata.workflows import WorkflowStandata\n",
+ "from mat3ra.wode.workflows import Workflow\n",
+ "\n",
+ "RELAXATION_WORKFLOW = (RELAXATION_SEARCH_TERM, RELAX_UNIT, DFT_CONTROL_SETTINGS)\n",
+ "WORKFLOW_BY_LABEL = {\"isolated_adatom\": (TOTAL_ENERGY_SEARCH_TERM, SCF_UNIT, {})}\n",
+ "dft_job_ids, new_job_ids = {}, []\n",
+ "for label, material in saved_materials.items():\n",
+ " search_term, unit_name, control = WORKFLOW_BY_LABEL.get(label, RELAXATION_WORKFLOW)\n",
+ " workflow = Workflow.create(\n",
+ " WorkflowStandata.filter_by_application(APPLICATION_NAME).get_by_name_first_match(search_term))\n",
+ " workflow.subworkflows[0].model = model\n",
+ " apply_planewave_cutoffs(workflow, ECUTWFC, ECUTRHO, unit_name=unit_name)\n",
+ " apply_scf_kgrid(workflow, DFT_KGRID, material=dft_materials[label], unit_name=unit_name)\n",
+ " system = {**DFT_SYSTEM_SETTINGS, **DFT_SPIN_SETTINGS_BY_LABEL.get(label, {})}\n",
+ " patch_workflow_qe_input(\n",
+ " workflow, {\"control\": control, \"system\": system, \"electrons\": DFT_ELECTRONS_SETTINGS}, [unit_name]\n",
+ " )\n",
+ " job = find_job_for_material(client, material[\"_id\"], workflow.name, ACCOUNT_ID,\n",
+ " statuses=(\"submitted\", \"queued\", \"active\", \"finished\"), kgrid=DFT_KGRID,\n",
+ " unit_name=unit_name)\n",
+ " if job is None:\n",
+ " job = create_job(api_client=client, materials=[material], workflow=workflow, project_id=project_id,\n",
+ " owner_id=ACCOUNT_ID, compute=compute.to_dict(), prefix=f\"{workflow.name} {material['name']}\")\n",
+ " new_job_ids.append(job[\"_id\"])\n",
+ " dft_job_ids[label] = job[\"_id\"]\n",
+ " print(f\"{label}: job {job['_id']}, {job['status']}\")\n",
+ "submit_jobs(client.jobs, new_job_ids)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "41",
+ "metadata": {},
+ "source": [
+ "### 7.9. Wait for the jobs to finish"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "42",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.api.job import wait_for_jobs_to_finish_async\n",
+ "\n",
+ "if dft_job_ids:\n",
+ " await wait_for_jobs_to_finish_async(client.jobs, list(dft_job_ids.values()), poll_interval=POLL_INTERVAL)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "43",
+ "metadata": {},
+ "source": [
+ "### 7.10. Read the final energies"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "44",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.core.entity.property.api import get_properties_for_job\n",
+ "\n",
+ "dft_energies = {label: get_properties_for_job(client, job_id, \"total_energy\")[0][\"value\"]\n",
+ " for label, job_id in dft_job_ids.items()}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "45",
+ "metadata": {},
+ "source": [
+ "## 8. Compare with the paper\n",
+ "### 8.1. Energies relative to a, adsorption energy at a, island relaxation\n",
+ "\n",
+ "The paper's energies of c and i are read off Fig. 6 (±0.02 and ±0.05 eV); the adsorption energy at a, the\n",
+ "island contraction and the ripple are from Sec. III.A, the isolated Ti atom energy it uses from Sec. II.B.\n",
+ "Quantum ESPRESSO's absolute energy for the free Ti atom is not on the manuscript's scale, so the DFT column\n",
+ "leaves E_Ti out; it cancels in E_ads(a), where it is used.\n",
+ "The island contraction and the ripple are measured on the MACE relaxation only."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "46",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Fig. 6 upper panel, read off; b = 0.10 eV also Sec. III.C.1\n",
+ "PAPER_FIG6 = {\"a\": 0.0, \"b\": 0.10, \"c\": -0.15, \"d\": -0.20, \"e\": -0.25, \"f\": -0.50, \"g\": -1.13, \"h\": -2.25, \"i\": -2.65}\n",
+ "PAPER_ADSORPTION_ENERGY = -2.81 # Sec. III.A, FFH_edge on the 5×5 island\n",
+ "PAPER_ISOLATED_ADATOM_ENERGY = -2.275 # Sec. II.B, Eq. (4), from ref. [68]\n",
+ "PAPER_ISLAND_CONTRACTION = {\"Ti–Ti along diagonals\": -6.8, \"Ti–N along medians\": -5.2} # Sec. III.A, p. 10\n",
+ "PAPER_RIPPLE = 0.19 # Sec. III.A, Fig. 2b\n",
+ "\n",
+ "relative_energies = {site: energy - energies[\"a\"] for site, energy in energies.items()}\n",
+ "mace_energies = {\"slab_with_island\": energy_slab_with_island, **energies, \"isolated_adatom\": energy_isolated_adatom}\n",
+ "dft_energies_or_nan = {label: dft_energies.get(label, np.nan) for label in mace_energies}\n",
+ "energies_by_method = {\"MACE\": mace_energies, \"DFT\": dft_energies_or_nan}\n",
+ "comparison = {\n",
+ " \"E(c) - E(a), eV\": [energy[\"c\"] - energy[\"a\"] for energy in energies_by_method.values()] + [PAPER_FIG6[\"c\"]],\n",
+ " \"E(i) - E(a), eV\": [energy[\"i\"] - energy[\"a\"] for energy in energies_by_method.values()] + [PAPER_FIG6[\"i\"]],\n",
+ " \"E_ads(a), eV\": [energy[\"a\"] - energy[\"slab_with_island\"] - energy[\"isolated_adatom\"]\n",
+ " for energy in energies_by_method.values()] + [PAPER_ADSORPTION_ENERGY],\n",
+ " \"E_Ti, isolated, eV\": [energy_isolated_adatom, np.nan, PAPER_ISOLATED_ADATOM_ENERGY],\n",
+ " **{f\"{label}, %\": [island_contraction[label], np.nan, PAPER_ISLAND_CONTRACTION[label]] for label in island_contraction},\n",
+ " \"Ripple, Å\": [ripple, np.nan, PAPER_RIPPLE],\n",
+ "}\n",
+ "print(f\"{'':<28}{'MACE':>10}{'DFT':>10}{'manuscript':>12}\")\n",
+ "for label, (mace, dft, manuscript) in comparison.items():\n",
+ " dft_cell = \"—\" if np.isnan(dft) else f\"{dft:.3f}\"\n",
+ " print(f\"{label:<28}{mace:>10.3f}{dft_cell:>10}{manuscript:>12}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "47",
+ "metadata": {},
+ "source": [
+ "### 8.2. Fig. 6"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "48",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import plotly.graph_objects as go\n",
+ "\n",
+ "dft_sites = [site for site in relative_energies if site in dft_energies] if \"a\" in dft_energies else []\n",
+ "\n",
+ "figure = go.Figure()\n",
+ "figure.add_trace(go.Scatter(x=list(PAPER_FIG6), y=list(PAPER_FIG6.values()), mode=\"lines+markers\",\n",
+ " name=\"Sangiovanni et al. (2018)\"))\n",
+ "figure.add_trace(go.Scatter(x=list(relative_energies), y=list(relative_energies.values()), mode=\"markers\",\n",
+ " marker={\"size\": 12}, name=\"this notebook (MACE-MP-0)\"))\n",
+ "if dft_sites:\n",
+ " figure.add_trace(go.Scatter(x=dft_sites, y=[dft_energies[site] - dft_energies[\"a\"] for site in dft_sites],\n",
+ " mode=\"markers\", marker={\"size\": 14, \"symbol\": \"diamond-open\"},\n",
+ " name=\"this notebook (DFT)\"))\n",
+ "figure.update_layout(title=\"Ti adatom descent from the 5×5 island edge (Fig. 6)\", xaxis_title=\"state\",\n",
+ " yaxis_title=\"energy relative to a (eV)\")\n",
+ "figure.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "49",
+ "metadata": {},
+ "source": [
+ "## References\n",
+ "\n",
+ "[1] D. G. Sangiovanni, A. B. Mei, D. Edström, L. Hultman, V. Chirita, I. Petrov, J. E. Greene, \"Effects of surface\n",
+ "vibrations on interlayer mass transport: Ab initio molecular dynamics investigation of Ti adatom descent pathways\n",
+ "and rates from TiN/TiN(001) islands\", Phys. Rev. B 97, 035406 (2018).\n",
+ "[DOI: 10.1103/PhysRevB.97.035406](https://doi.org/10.1103/PhysRevB.97.035406)\n",
+ "\n",
+ "[2] MACE-MP-0 foundation models: https://github.com/ACEsuit/mace"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/packages/mat3ra_api_client-2026.11.4.post1.dev7+gb5f25f7-py3-none-any.whl b/packages/mat3ra_api_client-2026.11.4.post1.dev7+gb5f25f7-py3-none-any.whl
new file mode 100644
index 000000000..e655f953a
--- /dev/null
+++ b/packages/mat3ra_api_client-2026.11.4.post1.dev7+gb5f25f7-py3-none-any.whl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e0999d8a1f3a41775a85d8c1d86ba509c00ab0f7663fc3f890927cafa779728
+size 28984
diff --git a/pyproject.toml b/pyproject.toml
index 2840c8d43..a501d7e6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ utils_standata = [
"mat3ra-standata",
]
api = [
- "mat3ra-api-client>=2026.10.8.post0",
+ "mat3ra-api-client @ git+https://github.com/mat3ra/api-client@b5f25f755db5c40d7580662affa16ac642a8d67c",
]
materials = [
# ase >=3.25.0 is required for supercell to be generated,