diff --git a/other/materials_designer/specific_examples/Introduction.ipynb b/other/materials_designer/specific_examples/Introduction.ipynb index 4795f3c8..e7d65470 100644 --- a/other/materials_designer/specific_examples/Introduction.ipynb +++ b/other/materials_designer/specific_examples/Introduction.ipynb @@ -27,7 +27,7 @@ "| `C-2D-INT-Z` | Interface ZSL | [BN/Graphene 2D–2D Interface](interface_2d_2d_boron_nitride_graphene.ipynb) | *To be added* | [[4]](#ref4) |\n", "| `C-2D-INT-Z` | Interface ZSL | [Graphene/SiO₂ 2D–3D Interface](interface_2d_3d_graphene_silicon_dioxide.ipynb) | *To be added* | [[5]](#ref5) |\n", "| `C-2D-INT-Z` | Interface ZSL | [Cu/Cristobalite 3D–3D Interface](interface_3d_3d_copper_cristobalite.ipynb) | *To be added* | [[6]](#ref6) |\n", - "| `C-2D-INT-Z` | Interface ZSL | [Graphene/Ni Interface Film XY Position Optimization](optimization_interface_film_xy_position_graphene_nickel.ipynb) | *To be added* | [[7]](#ref7) |\n", + "| `C-2D-INT-Z` | Interface ZSL | [Graphene/Ni Interface Film XY Position Optimization](optimization_interface_film_xy_position_graphene_nickel.ipynb) | [Gr/Ni(111) Registry and Separation](optimization_interface_film_xy_position_graphene_nickel_SIMULATION.ipynb) | [[7]](#ref7) |\n", "| `C-2D-INT-T` | Interface Twisted | *To be added* | — | — |\n", "| `C-2D-INT-C` | Interface Commensurate Lattice | [Twisted Commensurate MoS₂ Bilayer](interface_bilayer_twisted_commensurate_lattices_molybdenum_disulfide.ipynb) | [Twisted MoS₂ Bilayer Band Structure](interface_bilayer_twisted_commensurate_lattices_molybdenum_disulfide_SIMULATION.ipynb) | [[8]](#ref8) |\n", "| `C-2D-MLT` | Multi-Layer | *To be added* | — | — |\n", diff --git a/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel.ipynb b/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel.ipynb index f8ad43b8..6aa1b241 100644 --- a/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel.ipynb +++ b/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel.ipynb @@ -317,7 +317,7 @@ "id": "16", "metadata": {}, "source": [ - "# 4. Save optimized material" + "# 4. Save the base and optimized materials" ] }, { @@ -330,6 +330,9 @@ "from mat3ra.notebooks_utils.io import download_content_to_file\n", "from mat3ra.notebooks_utils.material import set_materials\n", "\n", + "set_materials(interface_material)\n", + "download_content_to_file(interface_material.to_json(), f\"{interface_material.name}.json\")\n", + "\n", "optimized_material.name = f\"{interface_material.name}_optimized_xy\"\n", "set_materials(optimized_material)\n", "download_content_to_file(optimized_material.to_json(), f\"{interface_material.name}_optimized_xy.json\")" diff --git a/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel_SIMULATION.ipynb b/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel_SIMULATION.ipynb new file mode 100644 index 00000000..45eb44fa --- /dev/null +++ b/other/materials_designer/specific_examples/optimization_interface_film_xy_position_graphene_nickel_SIMULATION.ipynb @@ -0,0 +1,1027 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Graphene/Ni(111) Interface: Registry, Separation and Work of Adhesion\n", + "\n", + "## 0. Introduction\n", + "\n", + "This notebook reproduces the structure and energetics of graphene on Ni(111) following the review:\n", + "\n", + "> **Arjun Dahal, Matthias Batzill**\n", + "> \"Graphene–nickel interfaces: a review\"\n", + "> Nanoscale, 6(5), 2548. (2014)\n", + "> [DOI: 10.1039/c3nr05279f](https://doi.org/10.1039/c3nr05279f)\n", + "\n", + "The review's structural facts (its section 2.1): graphene locks into a 1×1 registry on Ni(111);\n", + "LEED I–V and ion scattering identify the adsorbed structure as one carbon **atop** a first-layer Ni\n", + "and the other in the **fcc hollow**, 0.211 nm above the surface with a 0.005 nm buckling in which\n", + "the atop carbon sits further out. Its computed numbers come from\n", + "[Lahiri et al., New J. Phys. 13, 025001 (2011)](https://doi.org/10.1088/1367-2630/13/2/025001)\n", + "(open access), whose Table 1 is the quantitative target here:\n", + "\n", + "| interface | work of adhesion (J/m²) | separation (Å) |\n", + "|---|---|---|\n", + "| fcc (atop + fcc hollow) | 0.81 | 2.16 |\n", + "| hcp (atop + hcp hollow) | 0.77 | 2.17 |\n", + "| hollow (fcc + hcp hollows) | 0.31 | 3.26 |\n", + "\n", + "(The review's text quotes the hollow as 0.38 J/m²; the source paper's Table 1 says 0.31 — this\n", + "notebook targets the source.) The four candidate registries, in the review's own Fig. 1:\n", + "\n", + "\"The\n", + "\n", + "The bridge registry (d) is not quantified in either paper — it is included here as an extra point\n", + "beyond the published set.\n", + "\n", + "The published calculation (Lahiri et al., section 2.2) used **LDA, spin-polarized, with geometry\n", + "relaxation** — five Ni layers with the bottom two fixed — because \"GGA does not provide an adequate\n", + "description of Ni–graphene bonding\". This notebook follows that recipe in two tiers:\n", + "\n", + "- **Fast (here, in minutes):** each registry relaxed with the\n", + " [MACE-MP](https://github.com/ACEsuit/mace) machine-learned force field (+D3), with the bottom\n", + " substrate layers fixed as in the paper; same-cell references give the work of adhesion. MACE is\n", + " PBE-trained, and PBE is exactly the functional the paper rejects for this system — so its\n", + " chemisorption values are expected to underbind, and the notebook prints them **against** the\n", + " paper's rather than pretending. The structure side (registry, separation trend, buckling sign,\n", + " the hollow's dispersion-bound minimum) is where the fast tier earns its keep.\n", + "- **Precise (platform jobs):** the paper's functional — **LDA** (pz, ultrasoft), spin-polarized,\n", + " **with relaxation**, no dispersion correction (LDA binds this interface unaided, which is why the\n", + " paper chose it) — for each registry plus the two same-cell references the work of adhesion needs.\n", + "\n", + "**Prerequisite:** run\n", + "[optimization_interface_film_xy_position_graphene_nickel.ipynb](optimization_interface_film_xy_position_graphene_nickel.ipynb)\n", + "first — it creates and saves the base interface material this notebook loads.\n", + "\n", + "## 1. Prepare the Environment\n", + "### 1.1. Install Packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.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": "2", + "metadata": {}, + "source": [ + "### 1.2. Set Parameters\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "from mat3ra.ide.compute import QueueName\n", + "\n", + "# 2. Auth and organization parameters\n", + "ORGANIZATION_NAME = None\n", + "\n", + "# 3. Material parameters\n", + "FOLDER = \"./uploads\"\n", + "BASE_MATERIAL_NAME = \"Graphene_Nickel_interface\" # created by the companion structure notebook\n", + "\n", + "# 4. MLFF parameters. MACE-MP-0 is trained on inorganic crystals and surfaces. The large model at\n", + "# float64 is not a preference: the medium model at float32 finds no chemisorbed minimum at all.\n", + "MACE_MODEL_FAMILY = \"MACE-MP-0\"\n", + "MACE_MODEL = \"large\" # \"small\", \"medium\", \"large\"\n", + "MACE_DISPERSION = True # D3; the hollow registry is dispersion-bound\n", + "MACE_DEFAULT_DTYPE = \"float64\"\n", + "MACE_DEVICE = \"cpu\"\n", + "\n", + "# 5. Separation scan, in Angstrom — brackets the minima before relaxing. The window has to cover\n", + "# both published distances (2.16 A chemisorbed, 3.26 A for the hollow) with room on either side.\n", + "Z_SCAN_START = 1.8\n", + "Z_SCAN_STOP = 4.3\n", + "Z_SCAN_STEP = 0.25\n", + "\n", + "# A chemisorbing registry has two minima: one where graphene bonds to the surface and one held\n", + "# only by dispersion, further out. Anything below 2.6 A is the chemisorbed branch by a wide\n", + "# margin either way (2.16 vs 3.26 A in the paper).\n", + "CHEMISORBED_BELOW = 2.6 # Angstrom\n", + "\n", + "# 6. Relaxation — the paper's scheme: geometry optimization with the bottom substrate layers\n", + "# fixed. Relaxation is what produces the buckling, which is one of the published numbers.\n", + "FMAX = 0.02 # eV/A\n", + "FROZEN_SUBSTRATE_LAYERS = 2 # the paper fixes the bottom two of its five Ni layers\n", + "\n", + "# 7. Workflow parameters\n", + "WORKFLOW_SEARCH_TERM = \"total_energy.json\"\n", + "APPLICATION_NAME = \"espresso\"\n", + "MY_WORKFLOW_NAME = \"Total Energy (Gr/Ni registry)\"\n", + "\n", + "# Method parameters — the published setup where the platform can express it. Lahiri et al. used\n", + "# LDA, spin-polarized, with relaxation, and no dispersion correction: LDA binds this interface\n", + "# unaided, and that is the stated reason they chose it over GGA.\n", + "PSEUDOPOTENTIAL_TYPE = \"us\" # GBRV ultrasoft; the platform carries the lda/pz set for Ni and C\n", + "FUNCTIONAL = \"pz\" # LDA\n", + "MODEL_SUBTYPE = \"lda\"\n", + "ECUTWFC = 40 # GBRV publishes its ultrasoft set as a 40 / 200 Ry pair\n", + "ECUTRHO = 200\n", + "\n", + "# K is at (1/3, 1/3), so in-plane divisions must be a multiple of three for the mesh to contain\n", + "# it, and a metal needs a dense mesh to resolve its Fermi surface.\n", + "SCF_KGRID = [12, 12, 1]\n", + "\n", + "# Nickel is ferromagnetic — spin-polarized, started near its bulk moment (the paper's LDA value\n", + "# is 0.56 uB).\n", + "STARTING_MAGNETIZATION = {\"Ni\": 0.7}\n", + "\n", + "# A spin-polarized metal slab is the hard case for SCF, and the platform defaults do not converge\n", + "# it: a first run stopped at \"convergence NOT achieved after 100 iterations\" with the total energy\n", + "# oscillating in its fourth decimal — charge sloshing, not divergence. Cold smearing, local-TF\n", + "# mixing and a smaller mixing fraction address exactly that.\n", + "SMEARING = \"mv\" # Marzari-Vanderbilt cold smearing\n", + "DEGAUSS = 0.01 # Ry\n", + "ADDITIONAL_PARAMETERS = {\n", + " \"electrons\": {\n", + " \"mixing_mode\": \"local-TF\",\n", + " \"mixing_beta\": 0.2,\n", + " \"electron_maxstep\": 200,\n", + " },\n", + "}\n", + "\n", + "# 8. Compute parameters\n", + "CLUSTER_NAME = None\n", + "QUEUE_NAME = QueueName.D\n", + "PPN = 1\n", + "\n", + "# 9. Job parameters\n", + "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n", + "POLL_INTERVAL = 30\n" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2. Load the Base Interface\n", + "\n", + "The base interface is created by the companion structure notebook and saved into `uploads/`.\n", + "It is required — this notebook does not substitute another material.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.made.tools.modify import interface_get_part\n", + "from mat3ra.made.tools.convert.interface_parts_enum import InterfacePartsEnum\n", + "from mat3ra.notebooks_utils.material import load_material_from_folder\n", + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", + "\n", + "base_interface = load_material_from_folder(FOLDER, BASE_MATERIAL_NAME)\n", + "if base_interface is None:\n", + " raise RuntimeError(\n", + " f\"'{BASE_MATERIAL_NAME}' not found in {FOLDER} — run \"\n", + " \"optimization_interface_film_xy_position_graphene_nickel.ipynb first.\"\n", + " )\n", + "\n", + "film_part = interface_get_part(base_interface, part=InterfacePartsEnum.FILM)\n", + "substrate_part = interface_get_part(base_interface, part=InterfacePartsEnum.SUBSTRATE)\n", + "\n", + "_cart = base_interface.clone()\n", + "_cart.to_cartesian()\n", + "film_cart = film_part.clone(); film_cart.to_cartesian()\n", + "substrate_cart = substrate_part.clone(); substrate_cart.to_cartesian()\n", + "\n", + "film_z = [c[2] for c in film_cart.basis.coordinates.values]\n", + "substrate_z = [c[2] for c in substrate_cart.basis.coordinates.values]\n", + "measured_gap = min(film_z) - max(substrate_z)\n", + "\n", + "print(f\"Material: {base_interface.name}\")\n", + "from collections import Counter\n", + "composition = dict(Counter(base_interface.basis.elements.values))\n", + "print(f\"Composition: {composition}\")\n", + "print(f\"Atoms: {len(base_interface.basis.elements.values)} \"\n", + " f\"({len(film_cart.basis.elements.values)} film C, {len(substrate_cart.basis.elements.values)} substrate Ni)\")\n", + "print(f\"Film-substrate plane distance as built: {measured_gap:.3f} A\")\n", + "\n", + "visualize([{\"material\": base_interface, \"title\": base_interface.name}], repetitions=[3, 3, 1], rotation=\"-90x\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3. Place the Film at the High-Symmetry Registries\n", + "\n", + "The registries are defined by where carbon atoms sit relative to the Ni(111) surface sites:\n", + "**top** (above a first-layer Ni), **hcp hollow** (above a second-layer Ni), **fcc hollow**\n", + "(above a third-layer Ni), and **bridge** (midpoint of two neighboring first-layer Ni).\n", + "The sites are measured from the structure itself — the top three Ni layers — and the film is\n", + "translated so one carbon sublattice lands on each site in turn.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "cell_2d = np.array(_cart.lattice.vector_arrays)[:2, :2]\n", + "\n", + "ni_xyz = np.array(substrate_cart.basis.coordinates.values)\n", + "z_values = sorted(set(np.round(ni_xyz[:, 2], 2)), reverse=True)\n", + "layer_tol = 0.5\n", + "layers = []\n", + "for z in z_values:\n", + " if layers and abs(z - layers[-1][0]) < layer_tol:\n", + " continue\n", + " layers.append((z, ni_xyz[np.abs(ni_xyz[:, 2] - z) < layer_tol]))\n", + "if len(layers) < 3:\n", + " raise RuntimeError(f\"Need >= 3 Ni layers to locate the fcc and hcp sites, found {len(layers)}\")\n", + "\n", + "c_xyz = np.array(film_cart.basis.coordinates.values)\n", + "if len(c_xyz) != 2:\n", + " raise RuntimeError(f\"Expected a 1x1 graphene film (2 carbons), found {len(c_xyz)}\")\n", + "c_a, c_b = c_xyz[0], c_xyz[1]\n", + "\n", + "def nearest_image(site_xy, point_xy):\n", + " \"\"\"The periodic image of site_xy closest to point_xy.\"\"\"\n", + " images = [site_xy + i * cell_2d[0] + j * cell_2d[1] for i in (-1, 0, 1) for j in (-1, 0, 1)]\n", + " return min(images, key=lambda s: np.linalg.norm(s - point_xy))\n", + "\n", + "# Surface sites read off the structure itself: a first-layer Ni marks an atop site, a second-layer\n", + "# Ni projects onto the hcp hollow and a third-layer Ni onto the fcc hollow.\n", + "site_xy = {\n", + " \"atop\": nearest_image(layers[0][1][0][:2], c_a[:2]),\n", + " \"hcp\": nearest_image(layers[1][1][0][:2], c_a[:2]),\n", + " \"fcc\": nearest_image(layers[2][1][0][:2], c_a[:2]),\n", + "}\n", + "def site_of(point_xy):\n", + " \"\"\"Which named site a carbon lands on. Refuses to guess when two are equidistant.\"\"\"\n", + " distances = {name: np.linalg.norm(nearest_image(site, point_xy) - point_xy)\n", + " for name, site in site_xy.items()}\n", + " ordered = sorted(distances.items(), key=lambda kv: kv[1])\n", + " if len(ordered) > 1 and abs(ordered[0][1] - ordered[1][1]) < 0.05:\n", + " return None\n", + " return ordered[0][0]\n", + "\n", + "# The manuscript's Fig. 1: (a) hollow, (b) atop/fcc, (c) atop/hcp, (d) bridge. In a 1x1 cell the two\n", + "# carbon sublattices sit on two of the three named sites, which gives the first three. In the bridge\n", + "# registry neither carbon is on a site: the C-C bond straddles a first-layer Ni, which sits under the\n", + "# bond midpoint (Fig. 1d shows the vertical bonds running through the centres of the surface atoms).\n", + "bond_midpoint = (c_a[:2] + c_b[:2]) / 2\n", + "displacements = {\"bridge\": np.array([*(site_xy[\"atop\"] - bond_midpoint), 0.0])}\n", + "for a_site in (\"fcc\", \"atop\", \"hcp\"):\n", + " shift = np.array([*(site_xy[a_site] - c_a[:2]), 0.0])\n", + " b_site = site_of(c_b[:2] + shift[:2])\n", + " if b_site is None:\n", + " raise RuntimeError(f\"Carbon B is equidistant from two sites for the {a_site} placement\")\n", + " pair = {a_site, b_site}\n", + " label = f\"atop_{(pair - {'atop'}).pop()}\" if \"atop\" in pair else \"hollow\"\n", + " displacements[label] = shift\n", + "\n", + "expected = {\"hollow\", \"atop_fcc\", \"atop_hcp\", \"bridge\"}\n", + "if set(displacements) != expected:\n", + " raise RuntimeError(f\"Registry derivation produced {set(displacements)}, expected {expected}\")\n", + "\n", + "bridge_offset = np.linalg.norm(nearest_image(site_xy[\"atop\"], bond_midpoint + displacements[\"bridge\"][:2])\n", + " - (bond_midpoint + displacements[\"bridge\"][:2]))\n", + "if bridge_offset > 1e-6:\n", + " raise RuntimeError(f\"Bridge registry is off by {bridge_offset:.3f} A — no Ni under the bond midpoint\")\n", + "\n", + "print(f\"{'registry':<12}{'manuscript Fig. 1':<22}{'film shift (A)'}\")\n", + "for label, panel in ((\"hollow\", \"(a) hollow site\"), (\"atop_fcc\", \"(b) atop/'fcc' site\"),\n", + " (\"atop_hcp\", \"(c) atop/'hcp' site\"), (\"bridge\", \"(d) bridge site\")):\n", + " print(f\"{label:<12}{panel:<22}{np.round(displacements[label][:2], 3)}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.tools.modify import interface_displace_part\n", + "\n", + "def film_at(registry_label, plane_distance):\n", + " displacement = displacements[registry_label] + np.array([0.0, 0.0, plane_distance - measured_gap])\n", + " return interface_displace_part(base_interface, displacement=list(displacement))\n", + "\n", + "preview = []\n", + "for label in displacements:\n", + " m = film_at(label, measured_gap)\n", + " m.name = f\"{BASE_MATERIAL_NAME} {label}\"\n", + " preview.append({\"material\": m, \"title\": label})\n", + "\n", + "visualize(preview, repetitions=[2, 2, 1])" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## 4. Fast Tier: Relax Each Registry with MACE\n", + "\n", + "Each registry is bracketed by a rigid scan, then **relaxed** — all atoms free, the bottom\n", + "substrate layers fixed, the paper's scheme — and the same-cell references (bare Ni slab,\n", + "free-standing graphene) are relaxed the same way, which turns total energies into a work of\n", + "adhesion: W = (E_slab + E_graphene − E_interface) / A. After each relaxation the registry is\n", + "re-measured from the final positions, so a structure that slid into a neighbouring registry\n", + "cannot be reported under the wrong name. Distances follow the paper's convention: the averaged\n", + "carbon height above the averaged top-Ni height; buckling is the height difference between the\n", + "two carbons, positive when the atop carbon sits further out.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "import importlib.util\n", + "\n", + "from ase.constraints import FixAtoms\n", + "from ase.optimize import BFGS\n", + "from mat3ra.made.tools.convert import from_ase, to_ase\n", + "from mat3ra.notebooks_utils.mlff import create_mlff_calculator\n", + "\n", + "# D3 needs the torch-dftd package. Where it is unavailable (the in-browser environment does not\n", + "# bundle it), MACE runs at plain PBE level — which is exactly the description the review rejects\n", + "# for this interface: chemisorption comes out unbound and the hollow registry loses its\n", + "# dispersion-bound minimum. The notebook states which picture it is computing.\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: the fast tier runs WITHOUT dispersion — the\")\n", + " print(\"GGA-level picture the manuscript describes as inadequate for this interface.\")\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", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "distances = np.arange(Z_SCAN_START, Z_SCAN_STOP + 1e-9, Z_SCAN_STEP)\n", + "n_carbon = len(film_cart.basis.elements.values)\n", + "film_elements = set(film_cart.basis.elements.values)\n", + "substrate_elements = set(substrate_cart.basis.elements.values)\n", + "EV_PER_A2_TO_J_PER_M2 = 16.0217663\n", + "\n", + "def refine_minimum(x, y, i):\n", + " if 0 < i < len(x) - 1:\n", + " coefficients = np.polyfit(x[i - 1:i + 2], y[i - 1:i + 2], 2)\n", + " d = float(-coefficients[1] / (2 * coefficients[0]))\n", + " return d, float(np.polyval(coefficients, d))\n", + " return float(x[i]), float(y[i])\n", + "\n", + "def relax(atoms):\n", + " \"\"\"The paper's relaxation scheme: everything free except the bottom substrate layers.\"\"\"\n", + " symbols, z = atoms.get_chemical_symbols(), atoms.positions[:, 2]\n", + " substrate_z = sorted({round(z[i], 1) for i, s in enumerate(symbols) if s in substrate_elements})\n", + " held = [i for i, s in enumerate(symbols)\n", + " if s in substrate_elements and round(z[i], 1) in substrate_z[:FROZEN_SUBSTRATE_LAYERS]]\n", + " if held:\n", + " atoms.set_constraint(FixAtoms(indices=held))\n", + " atoms.calc = calculator\n", + " BFGS(atoms).run(fmax=FMAX, steps=300)\n", + " return atoms\n", + "\n", + "def interface_geometry(atoms):\n", + " \"\"\"Distances per the paper's convention: averaged heights; buckling signed by the atop carbon.\"\"\"\n", + " symbols, pos = atoms.get_chemical_symbols(), atoms.positions\n", + " carbon = [i for i, s in enumerate(symbols) if s in film_elements]\n", + " nickel_z = [pos[i, 2] for i, s in enumerate(symbols) if s in substrate_elements]\n", + " top_layer = [z for z in nickel_z if z > max(nickel_z) - 0.5]\n", + " carbon_by_site = {site_of(pos[i, :2]): i for i in carbon}\n", + " atop_index = carbon_by_site.get(\"atop\")\n", + " separation = float(np.mean([pos[i, 2] for i in carbon]) - np.mean(top_layer))\n", + " if atop_index is not None:\n", + " other = next(i for i in carbon if i != atop_index)\n", + " buckling = float(pos[atop_index, 2] - pos[other, 2])\n", + " else:\n", + " buckling = float(abs(pos[carbon[0], 2] - pos[carbon[1], 2]))\n", + " registry_now = frozenset(site_of(pos[i, :2]) for i in carbon)\n", + " return separation, buckling, registry_now\n", + "\n", + "# Same-cell references, relaxed under the same scheme\n", + "slab_atoms = relax(to_ase(substrate_part))\n", + "sheet_atoms = to_ase(film_part)\n", + "sheet_atoms.calc = calculator\n", + "BFGS(sheet_atoms).run(fmax=FMAX, steps=300)\n", + "E_slab, E_sheet = float(slab_atoms.get_potential_energy()), float(sheet_atoms.get_potential_energy())\n", + "cell = np.array(to_ase(base_interface).cell)\n", + "area = float(np.linalg.norm(np.cross(cell[0], cell[1])))\n", + "\n", + "scan_results = {}\n", + "for label in displacements:\n", + " energies = []\n", + " for d in distances:\n", + " atoms = to_ase(film_at(label, float(d)))\n", + " atoms.calc = calculator\n", + " energies.append(float(atoms.get_potential_energy()))\n", + " energies = np.array(energies)\n", + " minima = [refine_minimum(distances, energies, i)\n", + " for i in range(1, len(energies) - 1)\n", + " if energies[i] < energies[i - 1] and energies[i] < energies[i + 1]]\n", + " chem = min((m for m in minima if m[0] < CHEMISORBED_BELOW), key=lambda m: m[1], default=None)\n", + " phys = min((m for m in minima if m[0] >= CHEMISORBED_BELOW), key=lambda m: m[1], default=None)\n", + " start = chem or phys\n", + " if start is None:\n", + " # A monotonic curve has no minimum to relax from — the expected outcome for the\n", + " # dispersion-bound hollow registry when D3 is unavailable.\n", + " scan_results[label] = {\"distances\": distances, \"energies\": energies,\n", + " \"chem\": None, \"phys\": None, \"relaxed\": None}\n", + " print(f\"{label:<10} unbound in this window — no minimum to relax from\"\n", + " + (\"\" if dispersion_active else \" (dispersion inactive)\"))\n", + " continue\n", + " relaxed_atoms = relax(to_ase(film_at(label, start[0])))\n", + " separation, buckling, registry_now = interface_geometry(relaxed_atoms)\n", + " expected_sites = {\"atop_fcc\": frozenset((\"atop\", \"fcc\")), \"atop_hcp\": frozenset((\"atop\", \"hcp\")),\n", + " \"hollow\": frozenset((\"fcc\", \"hcp\"))}.get(label)\n", + " if expected_sites is not None and registry_now != expected_sites and None not in registry_now:\n", + " print(f\"! {label}: relaxed into {set(registry_now)} — treat its row with suspicion\")\n", + " energy = float(relaxed_atoms.get_potential_energy())\n", + " scan_results[label] = {\n", + " \"distances\": distances, \"energies\": energies, \"chem\": chem, \"phys\": phys,\n", + " \"relaxed\": {\"energy\": energy, \"separation\": separation, \"buckling\": buckling,\n", + " \"w_adh\": (E_slab + E_sheet - energy) / area * EV_PER_A2_TO_J_PER_M2,\n", + " \"material\": Material.create(from_ase(relaxed_atoms))},\n", + " }\n", + " print(f\"{label:<10} relaxed: d = {separation:5.2f} A buckling = {buckling:+.3f} A \"\n", + " f\"W_adh = {scan_results[label]['relaxed']['w_adh']:.2f} J/m^2\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.graph_objects as go\n", + "\n", + "reference = min(min(m[1] for m in (r[\"chem\"], r[\"phys\"]) if m) for r in scan_results.values())\n", + "fig = go.Figure()\n", + "for label, r in scan_results.items():\n", + " fig.add_trace(go.Scatter(x=r[\"distances\"], y=(r[\"energies\"] - reference) * 1000 / n_carbon,\n", + " mode=\"lines+markers\", name=label))\n", + "fig.update_layout(\n", + " title=\"Rigid-scan energy vs. separation (MACE-MP + D3) — bracketing only; the table below is relaxed\",\n", + " xaxis_title=\"plane distance (A)\",\n", + " yaxis_title=\"energy relative to the deepest minimum (meV / C atom)\",\n", + " yaxis_range=[-20, 300],\n", + ")\n", + "fig.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# Lahiri et al. (2011), Table 1 — the published targets (the review quotes the hollow as 0.38)\n", + "PAPER = {\n", + " \"atop_fcc\": {\"w_adh\": 0.81, \"separation\": 2.16},\n", + " \"atop_hcp\": {\"w_adh\": 0.77, \"separation\": 2.17},\n", + " \"hollow\": {\"w_adh\": 0.31, \"separation\": 3.26},\n", + "}\n", + "PAPER_BUCKLING = 0.03 # A, computed (the review, from ref. 35); LEED I-V measures 0.05 A\n", + "TOL_W = 0.15 # J/m^2\n", + "TOL_D = 0.10 # A\n", + "\n", + "relaxed_rows = {k: v[\"relaxed\"] for k, v in scan_results.items() if v[\"relaxed\"] is not None}\n", + "print(f\"{'registry':<10}{'W_adh J/m^2':<14}{'paper':<8}{'d (A)':<8}{'paper':<8}{'buckling (A)'}\")\n", + "for label, r in sorted(relaxed_rows.items(), key=lambda kv: -kv[1][\"w_adh\"]):\n", + " t = PAPER.get(label, {})\n", + " print(f\"{label:<10}{r['w_adh']:<14.2f}{t.get('w_adh', '—'):<8}\"\n", + " f\"{r['separation']:<8.2f}{t.get('separation', '—'):<8}{r['buckling']:+.3f}\")\n", + "for label, v in scan_results.items():\n", + " if v[\"relaxed\"] is None:\n", + " print(f\"{label:<10}unbound in this environment — paper: \"\n", + " f\"{PAPER.get(label, {}).get('w_adh', '—')} J/m^2 at {PAPER.get(label, {}).get('separation', '—')} A\")\n", + "\n", + "def within(label, key, target, tol):\n", + " row = relaxed_rows.get(label)\n", + " return row is not None and abs(row[key] - target) <= tol\n", + "\n", + "checks_mace = {\n", + " \"ordering fcc > hcp > hollow (W_adh)\": (\n", + " all(k in relaxed_rows for k in PAPER)\n", + " and relaxed_rows[\"atop_fcc\"][\"w_adh\"] > relaxed_rows[\"atop_hcp\"][\"w_adh\"] > relaxed_rows[\"hollow\"][\"w_adh\"]),\n", + " \"fcc W_adh within 0.15 J/m^2 of 0.81\": within(\"atop_fcc\", \"w_adh\", 0.81, TOL_W),\n", + " \"fcc separation within 0.10 A of 2.16\": within(\"atop_fcc\", \"separation\", 2.16, TOL_D),\n", + " \"hollow separation within 0.10 A of 3.26\": within(\"hollow\", \"separation\", 3.26, TOL_D),\n", + " \"atop carbon buckles outward\": \"atop_fcc\" in relaxed_rows and relaxed_rows[\"atop_fcc\"][\"buckling\"] > 0,\n", + "}\n", + "for name, ok in checks_mace.items():\n", + " print(f\" {'ok ' if ok else 'FAIL'} {name}\")\n", + "print(f\"\\nReproduces Lahiri et al. Table 1 [MACE tier]: {'yes' if all(checks_mace.values()) else 'no'}\")\n", + "reason = (\"dispersion is inactive here, so this is the GGA-level picture the manuscript rejects\"\n", + " if not dispersion_active else\n", + " \"MACE-MP is PBE-trained, and PBE is the functional the manuscript rejects for this interface\")\n", + "print(f\"({reason} — the DFT tier below runs the paper's LDA and carries the reproduction claim)\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## 5. Precise Tier: the Paper's LDA, Relaxed, on the Platform\n", + "\n", + "One relaxation + total-energy job per selected registry, starting from the MACE-relaxed geometry,\n", + "plus the two same-cell references the work of adhesion needs — the paper's functional (LDA),\n", + "spin-polarized, no dispersion correction. A default run selects one registry (three jobs). An\n", + "**empty** list skips the platform tier entirely, which is what the automated test does: with\n", + "relaxation these jobs take longer than a browser test may wait.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "DFT_REGISTRY_NAMES = [\n", + " \"atop_fcc\",\n", + " # \"atop_hcp\",\n", + " # \"hollow\",\n", + " # \"bridge\",\n", + "]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "await authenticate()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.api_client import APIClient\n", + "\n", + "client = APIClient.authenticate()\n", + "client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "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": "code", + "execution_count": null, + "id": "19", + "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": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "\n", + "def submitted_copy(material, name):\n", + " \"\"\"QE needs ATOMIC_SPECIES and ATOMIC_POSITIONS to agree, and the film/substrate labels only\n", + " served the displacement, so they are dropped from anything submitted.\"\"\"\n", + " m = material.clone()\n", + " m.basis.labels.values = []\n", + " m.name = name\n", + " return Material.create(get_or_create_material(client, m, ACCOUNT_ID))\n", + "\n", + "dft_materials, reference_materials = {}, {}\n", + "if DFT_REGISTRY_NAMES:\n", + " for label in DFT_REGISTRY_NAMES:\n", + " relaxed = scan_results[label][\"relaxed\"]\n", + " saved = submitted_copy(relaxed[\"material\"],\n", + " f\"{BASE_MATERIAL_NAME} {label} d{relaxed['separation']:.2f} relaxed\")\n", + " dft_materials[label] = saved\n", + " print(f\"{label:<16} -> '{saved.name}' ({len(saved.basis.elements.values)} atoms)\")\n", + " # The references live in the same cell and run with the same settings, so the cell- and\n", + " # sampling-dependent part of the error drops out of the work-of-adhesion difference.\n", + " for name, part in ((\"substrate\", substrate_part), (\"film\", film_part)):\n", + " saved = submitted_copy(part, f\"{BASE_MATERIAL_NAME} {name} reference\")\n", + " reference_materials[name] = saved\n", + " print(f\"{name + ' ref':<16} -> '{saved.name}' ({len(saved.basis.elements.values)} atoms)\")\n", + "else:\n", + " print(\"DFT tier skipped: no registries selected.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.applications import ApplicationStandata\n", + "from mat3ra.ade.application import Application\n", + "\n", + "app_config = ApplicationStandata.get_by_name_first_match(APPLICATION_NAME)\n", + "app = Application(**app_config)\n", + "print(f\"Using application: {app.name}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.workflows import WorkflowStandata\n", + "from mat3ra.wode.workflows import Workflow\n", + "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", + "\n", + "workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(WORKFLOW_SEARCH_TERM)\n", + "workflow = Workflow.create(workflow_config)\n", + "workflow.name = MY_WORKFLOW_NAME\n", + "\n", + "visualize_workflow(workflow)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.mode import ModelFactory\n", + "from mat3ra.standata.model_tree import ModelTreeStandata\n", + "\n", + "# The paper's functional. LDA describes this interface's geometry in agreement with experiment,\n", + "# which is the stated reason Lahiri et al. chose it over GGA; no dispersion correction is added\n", + "# on top, matching the paper.\n", + "model_config = ModelTreeStandata.get_model_by_parameters(\n", + " type=\"dft\",\n", + " subtype=MODEL_SUBTYPE,\n", + " functional=FUNCTIONAL,\n", + ")\n", + "model_config[\"method\"] = {\"type\": \"pseudopotential\", \"subtype\": PSEUDOPOTENTIAL_TYPE}\n", + "model = ModelFactory.create(model_config)\n", + "\n", + "for subworkflow in workflow.subworkflows:\n", + " subworkflow.model = model\n", + "\n", + "# Relaxation is the point: the buckling is one of the published numbers, and a single point at the\n", + "# MACE geometry would inherit MACE's PBE-grade structure.\n", + "workflow.add_relaxation()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.wode.context.providers import PlanewaveCutoffsContextProvider, PointsGridDataProvider\n", + "from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input\n", + "\n", + "QE_UNIT_NAMES = [\"pw_relax\", \"pw_scf\"]\n", + "\n", + "def system_patch_for(material):\n", + " \"\"\"&SYSTEM settings for one material. starting_magnetization is indexed by position in\n", + " ATOMIC_SPECIES, so the index is looked up per material — a free-standing graphene reference\n", + " contains no Ni and must not inherit its moment.\"\"\"\n", + " species_names = []\n", + " for element in material.basis.elements.values:\n", + " if element not in species_names:\n", + " species_names.append(element)\n", + " patch = {\"nspin\": 2, \"degauss\": DEGAUSS, \"smearing\": SMEARING}\n", + " for atomic_species, value in STARTING_MAGNETIZATION.items():\n", + " for index, name in enumerate(species_names):\n", + " if name == atomic_species:\n", + " patch[f\"starting_magnetization({index + 1})\"] = value\n", + " return species_names, patch\n", + "\n", + "def apply_calculation_settings(built, material):\n", + " for unit_name in QE_UNIT_NAMES:\n", + " for subworkflow in built.subworkflows:\n", + " unit = subworkflow.get_unit_by_name(name=unit_name)\n", + " if unit:\n", + " unit.add_context(PointsGridDataProvider(material=material, dimensions=SCF_KGRID,\n", + " isEdited=True).get_context_item_data())\n", + " unit.add_context(PlanewaveCutoffsContextProvider(wavefunction=ECUTWFC, density=ECUTRHO,\n", + " isEdited=True).get_context_item_data())\n", + " subworkflow.set_unit(unit)\n", + " _, patch = system_patch_for(material)\n", + " patch_workflow_qe_input(built, {\"system\": patch}, unit_names=QE_UNIT_NAMES)\n", + " if ADDITIONAL_PARAMETERS:\n", + " patch_workflow_qe_input(built, ADDITIONAL_PARAMETERS, unit_names=QE_UNIT_NAMES)\n", + " return built\n", + "\n", + "if dft_materials:\n", + " reference_material = dft_materials[DFT_REGISTRY_NAMES[0]]\n", + " apply_calculation_settings(workflow, reference_material)\n", + " species_names, system_patch = system_patch_for(reference_material)\n", + " print(f\"ATOMIC_SPECIES order: {species_names}\")\n", + " print(f\"&SYSTEM patch: {system_patch}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", + "\n", + "saved_workflows = {}\n", + "if dft_materials:\n", + " def configured_workflow(material, name):\n", + " built = Workflow.create(WorkflowStandata.filter_by_application(app.name)\n", + " .get_by_name_first_match(WORKFLOW_SEARCH_TERM))\n", + " built.name = name\n", + " for subworkflow in built.subworkflows:\n", + " subworkflow.model = model\n", + " built.add_relaxation()\n", + " return apply_calculation_settings(built, material)\n", + "\n", + " # One workflow per distinct element set, so a reference never inherits another material's\n", + " # magnetization indices.\n", + " workflows = {\"interface\": workflow}\n", + " for name, material in reference_materials.items():\n", + " if set(material.basis.elements.values) != set(reference_material.basis.elements.values):\n", + " workflows[name] = configured_workflow(material, f\"{MY_WORKFLOW_NAME} {name}\")\n", + " else:\n", + " workflows[name] = workflow\n", + "\n", + " seen = {}\n", + " for key, wf in workflows.items():\n", + " if id(wf) not in seen:\n", + " seen[id(wf)] = Workflow.create(get_or_create_workflow(client, wf, ACCOUNT_ID))\n", + " saved_workflows[key] = seen[id(wf)]\n", + " print(f\"{key:<12} -> workflow {saved_workflows[key].id}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "clusters = client.clusters.list() if dft_materials else []\n", + "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.ide.compute import Compute\n", + "\n", + "compute = None\n", + "if dft_materials:\n", + " if CLUSTER_NAME:\n", + " cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), None)\n", + " else:\n", + " cluster = clusters[0]\n", + " compute = Compute(cluster=cluster, queue=QUEUE_NAME, ppn=PPN)\n", + " print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.utils.namespace import dict_to_namespace_recursive\n", + "from mat3ra.notebooks_utils.job import create_job\n", + "\n", + "def submit_job_for(label, saved_material, which=\"interface\"):\n", + " job_response = create_job(\n", + " api_client=client,\n", + " materials=[saved_material],\n", + " workflow=workflows[which],\n", + " project_id=project_id,\n", + " owner_id=ACCOUNT_ID,\n", + " prefix=f\"{MY_WORKFLOW_NAME} {label} {timestamp}\",\n", + " compute=compute.to_dict(),\n", + " )\n", + " job_id = dict_to_namespace_recursive(job_response)._id\n", + " print(f\"{label:<16} -> job {job_id}\")\n", + " return job_id\n", + "\n", + "jobs, reference_jobs = {}, {}\n", + "if dft_materials:\n", + " jobs = {label: submit_job_for(label, m) for label, m in dft_materials.items()}\n", + " reference_jobs = {name: submit_job_for(f\"{name} reference\", m, which=name)\n", + " for name, m in reference_materials.items()}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "for label, job_id in {**jobs, **reference_jobs}.items():\n", + " client.jobs.submit(job_id)\n", + " print(f\"Submitted {label}: {job_id}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.api.job import wait_for_jobs_to_finish_async\n", + "\n", + "all_job_ids = list(jobs.values()) + list(reference_jobs.values())\n", + "if all_job_ids:\n", + " await wait_for_jobs_to_finish_async(client.jobs, all_job_ids, poll_interval=POLL_INTERVAL)\n", + "else:\n", + " print(\"Nothing to wait for — the DFT tier was skipped.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.prode import PropertyName\n", + "\n", + "dft_energies, reference_energies, dft_w_adh = {}, {}, {}\n", + "if jobs:\n", + " def total_energy_of(job_id):\n", + " property_data = client.properties.get_for_job(job_id, property_name=PropertyName.scalar.total_energy.value)\n", + " return float(property_data[0][\"data\"][\"value\"])\n", + "\n", + " dft_energies = {label: total_energy_of(job_id) for label, job_id in jobs.items()}\n", + " reference_energies = {name: total_energy_of(job_id) for name, job_id in reference_jobs.items()}\n", + " separated = reference_energies[\"substrate\"] + reference_energies[\"film\"]\n", + " dft_w_adh = {label: (separated - e) / area * EV_PER_A2_TO_J_PER_M2 for label, e in dft_energies.items()}\n", + "\n", + " print(f\"{'registry':<12}{'E_DFT (eV)':<16}{'W_adh (J/m^2)':<15}{'paper (J/m^2)'}\")\n", + " for label, e in sorted(dft_energies.items(), key=lambda kv: kv[1]):\n", + " t = PAPER.get(label, {})\n", + " print(f\"{label:<12}{e:<16.4f}{dft_w_adh[label]:<15.2f}{t.get('w_adh', '—')}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "## 6. Compare with the Article\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "# The verdict, per tier, against Lahiri et al. (2011) Table 1 — reached through the review.\n", + "print(\"Targets: fcc 0.81 J/m^2 @ 2.16 A · hcp 0.77 @ 2.17 · hollow 0.31 @ 3.26 · \"\n", + " f\"buckling ~{PAPER_BUCKLING} A, atop carbon out\\n\")\n", + "\n", + "print(f\"Reproduces Lahiri et al. Table 1 [MACE tier]: {'yes' if all(checks_mace.values()) else 'no'}\"\n", + " f\" ({sum(checks_mace.values())}/{len(checks_mace)} checks)\")\n", + "\n", + "if dft_w_adh:\n", + " evaluated = {label: dft_w_adh[label] for label in PAPER if label in dft_w_adh}\n", + " checks_dft = {f\"{label} W_adh within {TOL_W} J/m^2 of {PAPER[label]['w_adh']}\":\n", + " abs(w - PAPER[label][\"w_adh\"]) <= TOL_W for label, w in evaluated.items()}\n", + " if len(evaluated) == len(PAPER):\n", + " checks_dft[\"ordering fcc > hcp > hollow\"] = (\n", + " dft_w_adh[\"atop_fcc\"] > dft_w_adh[\"atop_hcp\"] > dft_w_adh[\"hollow\"])\n", + " for name, ok in checks_dft.items():\n", + " print(f\" {'ok ' if ok else 'FAIL'} {name}\")\n", + " partial = \"\" if len(evaluated) == len(PAPER) else f\" ({len(evaluated)} of {len(PAPER)} registries)\"\n", + " print(f\"Reproduces Lahiri et al. Table 1 [DFT tier]: \"\n", + " f\"{'yes' if checks_dft and all(checks_dft.values()) else 'no'}{partial}\")\n", + "else:\n", + " print(\"DFT tier: not run — select registries in DFT_REGISTRY_NAMES for the paper's-functional verdict.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] Arjun Dahal, Matthias Batzill, \"Graphene-nickel interfaces: a review\",\n", + "Nanoscale 6(5), 2548 (2014). [DOI: 10.1039/c3nr05279f](https://doi.org/10.1039/c3nr05279f)\n", + "\n", + "[2] Jayeeta Lahiri, Travis S. Miller, Andrew J. Ross, Lyudmyla Adamska, Ivan I. Oleynik,\n", + "Matthias Batzill, \"Graphene growth and stability at nickel surfaces\", New J. Phys. 13, 025001\n", + "(2011). [DOI: 10.1088/1367-2630/13/2/025001](https://doi.org/10.1088/1367-2630/13/2/025001)\n", + "\n", + "[3] mat3ra-made: https://github.com/Exabyte-io/made\n", + "\n", + "[4] MACE-MP-0 foundation models: https://github.com/ACEsuit/mace\n" + ] + } + ], + "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 +}