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 +}