diff --git a/other/materials_designer/specific_examples/Introduction.ipynb b/other/materials_designer/specific_examples/Introduction.ipynb
index 4795f3c8..cb27fa08 100644
--- a/other/materials_designer/specific_examples/Introduction.ipynb
+++ b/other/materials_designer/specific_examples/Introduction.ipynb
@@ -42,7 +42,7 @@
"| `D-0D-DFP` | Defect Pair | [Defect Pair in GaN](defect_point_pair_gallium_nitride.ipynb) | *To be added* | [[16]](#ref16) |\n",
"| `D-0D-INT` | Interstitial | [Interstitial in SnO₂](defect_point_interstitial_tin_oxide.ipynb) | *To be added* | [[17]](#ref17) |\n",
"| `D-0D-SUB` | Substitution | [N-doped Graphene](defect_point_substitution_graphene.ipynb) | [N-doped Graphene Band Structure](defect_point_substitution_graphene_SIMULATION.ipynb) | [[18]](#ref18) |\n",
- "| `D-0D-VAC` | Vacancy | [Vacancy in BN](defect_point_vacancy_boron_nitride.ipynb) | *To be added* | [[19]](#ref19) |\n",
+ "| `D-0D-VAC` | Vacancy | [Vacancy in BN](defect_point_vacancy_boron_nitride.ipynb) | [B Vacancy Formation Energy in h-BN](defect_point_vacancy_boron_nitride_SIMULATION.ipynb) | [[19]](#ref19) |\n",
"| `X-3D-PER` | Perturbation | *To be added* | — | — |\n",
"| `X-3D-ANL` | Annealed Crystal | *To be added* | — | — |\n",
"| `X-2D-PER` | Perturbation | [Ripple Perturbation in Graphene](perturbation_ripple_graphene.ipynb) | *To be added* | [[20]](#ref20) |\n",
diff --git a/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride.ipynb b/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride.ipynb
index 9d1977f5..b1f58e76 100644
--- a/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride.ipynb
+++ b/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride.ipynb
@@ -12,7 +12,7 @@
"This tutorial demonstrates the process of creating materials with vacancy defects, based on the work presented in the following manuscript:\n",
"\n",
"> **Fabian Bertoldo, Sajid Ali, Simone Manti & Kristian S. Thygesen**,\n",
- "> \"Quantum point defects in 2D materials - the QPOD database\", Nature, 2022. \n",
+ "> \"Quantum point defects in 2D materials - the QPOD database\", npj Computational Materials, 2022. \n",
"> [DOI:10.1038/s41524-022-00730-w](https://doi.org/10.1038/s41524-022-00730-w)\n",
"\n",
"Below is the figure 1 from the manuscript demonstrating the boron vacancy defects in hexagonal boron nitride (h-BN).\n",
@@ -189,8 +189,9 @@
"from mat3ra.notebooks_utils.io import download_content_to_file\n",
"from mat3ra.notebooks_utils.material import set_materials\n",
"\n",
+ "nanoribbon.name = \"h-BN supercell\"\n",
"material_with_defect.name = \"B-vacancy h-BN\"\n",
- "set_materials(material_with_defect)\n",
+ "set_materials([nanoribbon, material_with_defect])\n",
"download_content_to_file(material_with_defect.to_json(), \"B-vacancy_hexagonal_boron_nitride.json\")"
]
}
diff --git a/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride_SIMULATION.ipynb b/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride_SIMULATION.ipynb
new file mode 100644
index 00000000..ee7e0fde
--- /dev/null
+++ b/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride_SIMULATION.ipynb
@@ -0,0 +1,730 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# Defect Formation Energy of a Boron Vacancy in h-BN\n",
+ "\n",
+ "> **Fabian Bertoldo, Sajid Ali, Simone Manti & Kristian S. Thygesen**,\n",
+ "> \"Quantum point defects in 2D materials - the QPOD database\", npj Computational Materials, 2022.\n",
+ "> [DOI:10.1038/s41524-022-00730-w](https://doi.org/10.1038/s41524-022-00730-w)\n",
+ "\n",
+ "Computes the neutral formation energy of the vacancy created in the\n",
+ "[structure notebook](defect_point_vacancy_boron_nitride.ipynb) and compares it with QPOD's\n",
+ "`v_B in BN (charge 0)` entry, using the generic\n",
+ "[Defect Formation Energy](../workflows/defect_formation_energy.ipynb) workflow.\n",
+ "\n",
+ "$$E_f = E_{\\text{defective}} - E_{\\text{pristine}} - \\Delta N_B\\, \\mu_B \\quad [\\text{eV}]$$\n",
+ "\n",
+ "$\\mu_B$ is the total energy of boron's standard state per atom (QPOD's convention, $q = 0$). For\n",
+ "V_B, $\\Delta N_B = -1$ (one boron removed), so the last term adds $\\mu_B$.\n",
+ "\n",
+ "QPOD's value is for a relaxed cell (84 atoms pristine, 83 defective; 15.06 Å defect spacing); this\n",
+ "notebook's 48/47-atom cell is unrelaxed -- each effect is below 0.05 eV (measured with MACE).\n",
+ "\n",
+ "
Usage
\n",
+ "\n",
+ "1. Create the materials in the [structure notebook](defect_point_vacancy_boron_nitride.ipynb) first.\n",
+ "1. Set the parameters in cells 1.2-1.4 (or use the defaults), then \"Run\" > \"Run All\".\n",
+ "1. The default run submits up to four Quantum ESPRESSO jobs (pristine, B, N, defect) and waits for\n",
+ " them, which can take hours; reference jobs are reused on a rerun.\n",
+ "1. The last line reports whether E_f reproduces Bertoldo et al. (2022), and flags it if the\n",
+ " workflow's references were not this notebook's own jobs.\n",
+ "\n",
+ "## Summary\n",
+ "\n",
+ "Loads the materials, resolves the elemental references, configures one model and per-material\n",
+ "k-grids, creates and waits for the prerequisite and defect jobs, then compares the result with QPOD.\n"
+ ]
+ },
+ {
+ "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.packages import install_packages\n",
+ "\n",
+ "await install_packages(\"made|specific_examples|api_examples\")\n"
+ ]
+ },
+ {
+ "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_vacancy_boron_nitride.ipynb.\n",
+ "PRISTINE_NAME = \"h-BN supercell\"\n",
+ "DEFECTIVE_NAME = \"B-vacancy h-BN\"\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "### 1.3. Parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from datetime import datetime\n",
+ "from mat3ra.ide.compute import QueueName\n",
+ "\n",
+ "ORGANIZATION_NAME = None # set to your organization name (full or partial); otherwise, your default one is used\n",
+ "FOLDER = \"./uploads\"\n",
+ "\n",
+ "TOTAL_ENERGY_SEARCH_TERM = \"total_energy.json\"\n",
+ "DEFECT_WORKFLOW_SEARCH_TERM = \"defect_formation_energy.json\"\n",
+ "MY_WORKFLOW_NAME = \"Defect Formation Energy\"\n",
+ "APPLICATION_NAME = \"espresso\"\n",
+ "\n",
+ "CLUSTER_NAME = \"cluster-001\"\n",
+ "QUEUE_NAME = QueueName.OF\n",
+ "PPN = 40\n",
+ "TIME_LIMIT = \"04:00:00\" # one hour is not enough for a spin-polarized SCF on this cell\n",
+ "\n",
+ "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n",
+ "POLL_INTERVAL = 60 # seconds\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "### 1.4. DFT model parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "FUNCTIONAL = \"pbe\"\n",
+ "PSEUDOPOTENTIAL_TYPE = \"us\" # GBRV ultrasoft, the platform's default PBE family for B and N\n",
+ "ECUTWFC = 40 # Ry, GBRV's recommended wavefunction cutoff\n",
+ "ECUTRHO = 200 # Ry, GBRV's recommended charge-density cutoff\n",
+ "\n",
+ "# QPOD: 6 Å⁻¹ for relaxations, 12 for ground states; 6 used here for cost -- the neutral E_f\n",
+ "# does not need the denser grid.\n",
+ "KPOINT_DENSITY = 6\n",
+ "MODEL_TAG = f\"{FUNCTIONAL}-{PSEUDOPOTENTIAL_TYPE} {ECUTWFC}-{ECUTRHO}Ry\"\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9",
+ "metadata": {},
+ "source": [
+ "## 2. Authenticate and initialize API client\n",
+ "### 2.1. Authenticate"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.auth import authenticate\n",
+ "\n",
+ "await authenticate()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11",
+ "metadata": {},
+ "source": [
+ "### 2.2. Initialize API client"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.api_client import APIClient\n",
+ "\n",
+ "client = APIClient.authenticate()\n",
+ "client\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13",
+ "metadata": {},
+ "source": [
+ "### 2.3. Select account"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "client.list_accounts()\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "15",
+ "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}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "16",
+ "metadata": {},
+ "source": [
+ "### 2.4. Select project"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17",
+ "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})\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18",
+ "metadata": {},
+ "source": [
+ "## 3. Load the materials\n",
+ "### 3.1. Load from the uploads folder, and print provenance"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "19",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from collections import Counter\n",
+ "from mat3ra.notebooks_utils.material import load_material_from_folder\n",
+ "\n",
+ "def formula(material):\n",
+ " counts = Counter(material.basis.elements.values)\n",
+ " return \"\".join(f\"{element}{counts[element]}\" for element in sorted(counts))\n",
+ "\n",
+ "materials_by_name = {}\n",
+ "for name in (PRISTINE_NAME, DEFECTIVE_NAME):\n",
+ " material = load_material_from_folder(FOLDER, name, verbose=False)\n",
+ " if material is None:\n",
+ " raise ValueError(f\"No material named '{name}' in '{FOLDER}'. Run \"\n",
+ " \"defect_point_vacancy_boron_nitride.ipynb first, or correct the name above.\")\n",
+ " materials_by_name[name] = material\n",
+ "\n",
+ "pristine, defective = materials_by_name[PRISTINE_NAME], materials_by_name[DEFECTIVE_NAME]\n",
+ "for name, material in materials_by_name.items():\n",
+ " a, b = material.lattice.a, material.lattice.b\n",
+ " print(f\"{name}: {formula(material)}, {material.basis.number_of_atoms} atoms, \"\n",
+ " f\"cell {a:.2f} x {b:.2f} Å, min in-plane vector {min(a, b):.2f} Å\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20",
+ "metadata": {},
+ "source": [
+ "### 3.2. Resolve elemental reference materials\n",
+ "\n",
+ "Elemental references are platform materials, not Standata entries: the workflow itself resolves\n",
+ "`{'tags': 'elemental', 'metadata.element': ...}` and then `total_energy` properties by that\n",
+ "material's `exabyteId`, which an uploaded copy never shares with the platform's own seed."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.made.material import Material\n",
+ "\n",
+ "elements = sorted(set(pristine.basis.elements.values) | set(defective.basis.elements.values))\n",
+ "\n",
+ "elemental_materials_data = client.materials.list({\"tags\": \"elemental\", \"metadata.element\": {\"$in\": elements}})\n",
+ "elemental_materials = {}\n",
+ "for element in elements:\n",
+ " matches = [m for m in elemental_materials_data if m.get(\"metadata\", {}).get(\"element\") == element]\n",
+ " if not matches:\n",
+ " raise ValueError(f\"No platform elemental reference material tagged for {element}; \"\n",
+ " \"seed one (tags=['elemental'], metadata.element) first.\")\n",
+ " elemental_materials[element] = matches[0]\n",
+ " atoms = Material.create(matches[0]).basis.number_of_atoms\n",
+ " print(f\"{element}: {matches[0]['name']} ({matches[0]['_id']}, owner {matches[0]['owner']['slug']}, \"\n",
+ " f\"{atoms} atoms)\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "22",
+ "metadata": {},
+ "source": [
+ "### 3.3. Save the materials to the platform"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "23",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n",
+ "\n",
+ "saved_pristine = get_or_create_material(client, pristine, ACCOUNT_ID)\n",
+ "saved_defective = get_or_create_material(client, defective, ACCOUNT_ID)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "24",
+ "metadata": {},
+ "source": [
+ "## 4. Configure the shared DFT model and k-grid\n",
+ "### 4.1. DFT model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "25",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.standata.applications import ApplicationStandata\n",
+ "from mat3ra.standata.model_tree import ModelTreeStandata\n",
+ "from mat3ra.ade.application import Application\n",
+ "from mat3ra.mode import ModelFactory\n",
+ "from mat3ra.wode.context.providers import PlanewaveCutoffsContextProvider\n",
+ "\n",
+ "app_config = ApplicationStandata.get_by_name_first_match(APPLICATION_NAME)\n",
+ "app = Application(**app_config)\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)\n",
+ "\n",
+ "cutoffs_context = PlanewaveCutoffsContextProvider(\n",
+ " wavefunction=ECUTWFC, density=ECUTRHO, isEdited=True).get_context_item_data()\n",
+ "print(f\"Using application: {app.name}, model: {MODEL_TAG}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "26",
+ "metadata": {},
+ "source": [
+ "### 4.2. k-grid per material"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import math\n",
+ "\n",
+ "# made.Material objects, purely for lattice/basis access -- kept alongside the platform dicts\n",
+ "# in elemental_materials, which are used for ids and job creation.\n",
+ "material_objects = {PRISTINE_NAME: pristine, DEFECTIVE_NAME: defective,\n",
+ " **{element: Material.create(data) for element, data in elemental_materials.items()}}\n",
+ "\n",
+ "def kgrid_for_density(material, periodic_dims=(0, 1, 2)):\n",
+ " # |b_i| = 2*pi*reciprocal_vector_norms[i]; dims outside periodic_dims stay at 1 (vacuum).\n",
+ " norms = material.lattice.reciprocal_vector_norms\n",
+ " grid = [1, 1, 1]\n",
+ " for dim in periodic_dims:\n",
+ " grid[dim] = max(1, math.ceil(KPOINT_DENSITY * 2 * math.pi * norms[dim]))\n",
+ " return grid\n",
+ "\n",
+ "kgrid = {\n",
+ " PRISTINE_NAME: kgrid_for_density(pristine, periodic_dims=(0, 1)),\n",
+ " DEFECTIVE_NAME: kgrid_for_density(defective, periodic_dims=(0, 1)),\n",
+ " **{element: kgrid_for_density(material_objects[element]) for element in elemental_materials},\n",
+ "}\n",
+ "for name, grid in kgrid.items():\n",
+ " print(f\"{name}: k-grid {grid}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "28",
+ "metadata": {},
+ "source": [
+ "## 5. Configure compute\n",
+ "### 5.1. Select cluster"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "clusters = client.clusters.list()\n",
+ "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "30",
+ "metadata": {},
+ "source": [
+ "### 5.2. Create compute configuration"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.ide.compute import Compute\n",
+ "\n",
+ "if CLUSTER_NAME:\n",
+ " cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), None)\n",
+ " if cluster is None:\n",
+ " raise ValueError(f\"Cluster '{CLUSTER_NAME}' not found. Available: {[c['hostname'] for c in clusters]}\")\n",
+ "else:\n",
+ " cluster = clusters[0]\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}, \"\n",
+ " f\"time limit: {TIME_LIMIT}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32",
+ "metadata": {},
+ "source": [
+ "## 6. Prerequisite Total Energy jobs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "33",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.standata.workflows import WorkflowStandata\n",
+ "from mat3ra.wode.workflows import Workflow\n",
+ "from mat3ra.notebooks_utils.workflow import apply_scf_kgrid\n",
+ "from mat3ra.notebooks_utils.job import create_job\n",
+ "\n",
+ "total_energy_workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(\n",
+ " TOTAL_ENERGY_SEARCH_TERM\n",
+ ")\n",
+ "\n",
+ "# Reuse is keyed on this exact workflow name, which carries the model, so a finished job at other\n",
+ "# cutoffs is not a hit -- the defect workflow resolves the pristine energy separately, by highest\n",
+ "# precision among this material's own qe: total_energy properties, and the results cell's\n",
+ "# consistency check catches it if that resolves to a job other than this one. Jobs on\n",
+ "# curators-owned materials (the elementals) are allowed.\n",
+ "prerequisite_materials = {PRISTINE_NAME: saved_pristine, **elemental_materials}\n",
+ "prerequisite_job_ids = {}\n",
+ "new_job_ids = []\n",
+ "for name, saved_material in prerequisite_materials.items():\n",
+ " workflow_name = f\"Total Energy {name} {MODEL_TAG}\"\n",
+ " existing = client.jobs.list(\n",
+ " {\"_material._id\": saved_material[\"_id\"], \"owner._id\": ACCOUNT_ID, \"status\": \"finished\",\n",
+ " \"workflow.name\": workflow_name},\n",
+ " {\"sort\": {\"updatedAt\": -1}, \"limit\": 1},\n",
+ " )\n",
+ " if existing:\n",
+ " print(f\"♻️ {name}: reusing existing Total Energy job {existing[0]['_id']}\")\n",
+ " prerequisite_job_ids[name] = existing[0][\"_id\"]\n",
+ " continue\n",
+ " workflow = Workflow.create(total_energy_workflow_config)\n",
+ " workflow.name = workflow_name\n",
+ " subworkflow = workflow.subworkflows[0]\n",
+ " subworkflow.model = model\n",
+ " unit = subworkflow.get_unit_by_name(name=\"pw_scf\")\n",
+ " unit.add_context(cutoffs_context)\n",
+ " subworkflow.set_unit(unit)\n",
+ " apply_scf_kgrid(workflow, kgrid[name], material=material_objects[name])\n",
+ "\n",
+ " job_response = create_job(\n",
+ " api_client=client, materials=[saved_material], workflow=workflow, project_id=project_id,\n",
+ " owner_id=ACCOUNT_ID, prefix=f\"{workflow.name} {timestamp}\", compute=compute.to_dict(),\n",
+ " )\n",
+ " prerequisite_job_ids[name] = job_response[\"_id\"]\n",
+ " new_job_ids.append(job_response[\"_id\"])\n",
+ " print(f\"✅ {name}: created Total Energy job {job_response['_id']}\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "34",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.api.job import submit_jobs, wait_for_jobs_to_finish_async\n",
+ "\n",
+ "if new_job_ids:\n",
+ " submit_jobs(client.jobs, new_job_ids)\n",
+ " print(f\"✅ Submitted {len(new_job_ids)} prerequisite job(s).\")\n",
+ " await wait_for_jobs_to_finish_async(client.jobs, new_job_ids, poll_interval=POLL_INTERVAL)\n",
+ "\n",
+ "job_statuses = {name: client.jobs.get(job_id)[\"status\"] for name, job_id in prerequisite_job_ids.items()}\n",
+ "unfinished = {name: status for name, status in job_statuses.items() if status != \"finished\"}\n",
+ "if unfinished:\n",
+ " raise RuntimeError(f\"Prerequisite job(s) not finished: {unfinished}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "35",
+ "metadata": {},
+ "source": [
+ "## 7. Configure the Defect Formation Energy workflow"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "36",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input\n",
+ "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n",
+ "\n",
+ "defect_workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(\n",
+ " DEFECT_WORKFLOW_SEARCH_TERM\n",
+ ")\n",
+ "defect_workflow = Workflow.create(defect_workflow_config)\n",
+ "defect_workflow.name = f\"{MY_WORKFLOW_NAME} {MODEL_TAG}\"\n",
+ "\n",
+ "for subworkflow in defect_workflow.subworkflows:\n",
+ " if subworkflow.name == \"Compute Total Energy for Defective Material\":\n",
+ " subworkflow.model = model\n",
+ " unit = subworkflow.get_unit_by_name(name=\"pw_scf\")\n",
+ " unit.add_context(cutoffs_context)\n",
+ " subworkflow.set_unit(unit)\n",
+ " elif subworkflow.name == \"Resolve Total Energies for Elemental Materials\":\n",
+ " # References resolve from this account (falling back to curators), not the workflow's\n",
+ " # own default 'public' (any owner, highest precision wins).\n",
+ " source_unit = subworkflow.get_unit_by_name(name=\"assign-source-of-te-for-an-element\")\n",
+ " source_unit.value = \"'my_account'\"\n",
+ " subworkflow.set_unit(source_unit)\n",
+ "apply_scf_kgrid(defect_workflow, kgrid[DEFECTIVE_NAME], material=defective)\n",
+ "\n",
+ "# nspin/magnetization on the defective cell's SCF only -- species order is B, N, and the V_B\n",
+ "# moment sits on the N neighbours, so both are given an initial moment.\n",
+ "patch_workflow_qe_input(defect_workflow, {\"system\": {\"nspin\": 2, \"starting_magnetization(1)\": 0.5,\n",
+ " \"starting_magnetization(2)\": 0.5}},\n",
+ " unit_names=[\"pw_scf\"])\n",
+ "\n",
+ "visualize_workflow(defect_workflow)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "37",
+ "metadata": {},
+ "source": [
+ "## 8. Create and run the Defect Formation Energy job\n",
+ "### 8.1. Create the job (defective + pristine)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "38",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "defect_job_response = create_job(\n",
+ " api_client=client, materials=[saved_defective, saved_pristine], workflow=defect_workflow,\n",
+ " project_id=project_id, owner_id=ACCOUNT_ID, compute=compute.to_dict(),\n",
+ " prefix=f\"{MY_WORKFLOW_NAME} {formula(defective)} {timestamp}\",\n",
+ ")\n",
+ "defect_job_id = defect_job_response[\"_id\"]\n",
+ "print(f\"✅ Defect Formation Energy job created: {defect_job_id}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "39",
+ "metadata": {},
+ "source": [
+ "### 8.2. Submit and monitor the job"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "40",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "client.jobs.submit(defect_job_id)\n",
+ "print(f\"✅ Job {defect_job_id} submitted successfully!\")\n",
+ "await wait_for_jobs_to_finish_async(client.jobs, [defect_job_id], poll_interval=POLL_INTERVAL)\n",
+ "\n",
+ "status = client.jobs.get(defect_job_id)[\"status\"]\n",
+ "if status != \"finished\":\n",
+ " raise RuntimeError(f\"Defect Formation Energy job {defect_job_id} did not finish (status: {status}).\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "41",
+ "metadata": {},
+ "source": [
+ "## 9. Retrieve the results\n",
+ "### 9.1. Defect formation energy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "42",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.core.entity.property.api import get_properties_for_job\n",
+ "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n",
+ "\n",
+ "defect_energy_data = get_properties_for_job(client, defect_job_id, property_name=\"defect_formation_energy\")\n",
+ "if not defect_energy_data:\n",
+ " raise RuntimeError(f\"Job {defect_job_id} produced no defect_formation_energy -- check it finished.\")\n",
+ "visualize_properties(defect_energy_data, title=\"Defect Formation Energy\")\n",
+ "e_formation = defect_energy_data[0][\"value\"]\n",
+ "\n",
+ "def total_energy_for(job_id, label):\n",
+ " data = get_properties_for_job(client, job_id, property_name=\"total_energy\")\n",
+ " if not data:\n",
+ " raise RuntimeError(f\"Job {job_id} ({label}) has no total_energy property.\")\n",
+ " return data[0][\"value\"]\n",
+ "\n",
+ "# Consistency check: recompute E_f from the three total energies this notebook owns, and flag\n",
+ "# it if the workflow resolved a different reference (e.g. a curators' energy outranking ours).\n",
+ "e_def = total_energy_for(defect_job_id, \"defective\")\n",
+ "e_pris = total_energy_for(prerequisite_job_ids[PRISTINE_NAME], \"pristine\")\n",
+ "b_atoms = material_objects[\"B\"].basis.number_of_atoms\n",
+ "e_b_per_atom = total_energy_for(prerequisite_job_ids[\"B\"], \"B reference\") / b_atoms\n",
+ "e_formation_check = e_def - e_pris + e_b_per_atom\n",
+ "reference_matches = abs(e_formation - e_formation_check) <= 0.001\n",
+ "print(f\"μ_B = {e_b_per_atom:.4f} eV/atom ({b_atoms} atoms)\")\n",
+ "print(f\"E_f (workflow): {e_formation:.3f} eV, E_f (recomputed): {e_formation_check:.3f} eV\")\n",
+ "if not reference_matches:\n",
+ " print(\"⚠️ The workflow resolved a different reference energy than this notebook's own jobs.\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "43",
+ "metadata": {},
+ "source": [
+ "### 9.2. Compare with QPOD\n",
+ "\n",
+ "Only the neutral (q = 0) defect is compared: QPOD's charged states need a finite-size correction\n",
+ "this workflow does not compute. QPOD's 10.18 eV is itself PBE/PAW (GPAW) theory, not experiment."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "44",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "QPOD = {\"standard_states\": 10.18, \"B_poor\": 8.89} # eV, QPOD v_B in BN (charge 0)\n",
+ "\n",
+ "difference = e_formation - QPOD[\"standard_states\"]\n",
+ "verdict = \"yes\" if abs(difference) <= 0.5 else \"no\"\n",
+ "note = \"\" if reference_matches else \" -- reference mismatch, see warning above\"\n",
+ "print(f\"E_f (this notebook): {e_formation:.3f} eV\")\n",
+ "print(f\"E_f (QPOD, standard states): {QPOD['standard_states']:.3f} eV\")\n",
+ "print(f\"E_f (QPOD, B-poor): {QPOD['B_poor']:.3f} eV\")\n",
+ "print(f\"Difference from standard states: {difference:+.3f} eV\")\n",
+ "print(f\"Reproduces Bertoldo et al. (2022): {verdict}{note}\")\n"
+ ]
+ }
+ ],
+ "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.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}