From 26441ddc253d2eb17335a0cbc7288b7423f029a0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 12 Sep 2026 21:58:45 -0700 Subject: [PATCH 1/6] feat: save the pristine h-BN supercell for the vacancy defect-formation-energy SE The defect_formation_energy workflow needs the pristine and defective supercells as a pair; the structure notebook only named and saved the defective one. Name and save the pristine supercell too, and link the new simulation notebook from Introduction.ipynb's D-0D-VAC row (SOF-8044). --- other/materials_designer/specific_examples/Introduction.ipynb | 2 +- .../specific_examples/defect_point_vacancy_boron_nitride.ipynb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/other/materials_designer/specific_examples/Introduction.ipynb b/other/materials_designer/specific_examples/Introduction.ipynb index 4795f3c84..cb27fa08d 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 9d1977f57..36ff98aca 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 @@ -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\")" ] } From d54f084110b4f949a4dd043501269d97713840c5 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 12 Sep 2026 21:58:54 -0700 Subject: [PATCH 2/6] feat: add the h-BN boron vacancy formation energy simulation notebook Computes the neutral formation energy of the boron vacancy created in defect_point_vacancy_boron_nitride.ipynb with the defect_formation_energy workflow and compares it against QPOD's v_B in BN (charge 0) entry (Bertoldo et al. 2022). Elemental and pristine Total Energy references are computed under this account with the same PBE/PAW model as the defect job (D2) rather than trusted from curators, whose seeded values may use a different pseudopotential; the workflow's own elemental-total- energy resolution otherwise defaults to any owner ('public'), so its source is patched to this account. B and N have no ultrasoft PBE pseudopotential on the platform, so the model uses PAW throughout, and the defective cell's SCF is patched to nspin=2 with an initial magnetization for the spin-polarized vacancy. k-grids are derived per material from the paper's own k-point density (SOF-8044). --- ...int_vacancy_boron_nitride_SIMULATION.ipynb | 668 ++++++++++++++++++ 1 file changed, 668 insertions(+) create mode 100644 other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride_SIMULATION.ipynb 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 000000000..e5f662368 --- /dev/null +++ b/other/materials_designer/specific_examples/defect_point_vacancy_boron_nitride_SIMULATION.ipynb @@ -0,0 +1,668 @@ +{ + "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\", Nature, 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$).\n", + "\n", + "QPOD's value is for a relaxed 84-atom cell (15.06 Å defect spacing); this notebook's 48-atom\n", + "cell is unrelaxed -- both effects together shift E_f by about 0.06 eV (measured with MACE).\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\"\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 = \"paw\" # B, N: paw or nc only -- no ultrasoft PBE pseudopotential exists\n", + "ECUTWFC = 40 # higher of B/N PseudoDojo \"high\" hints (15, 20 Ha), in Ry\n", + "ECUTRHO = 8 * ECUTWFC # PAW needs a much denser augmentation charge\n", + "\n", + "KPOINT_DENSITY = 6 # paper's ground-state k-point density (Bertoldo et al. 2022, 6.1), Å⁻¹\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)}, {len(material.basis.elements.ids)} atoms, \"\n", + " f\"cell {a:.2f} x {b:.2f} Å, defect-defect distance {min(a, b):.2f} Å\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### 3.2. Resolve elemental reference materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.standata.materials import Materials\n", + "\n", + "elements = sorted(set(pristine.basis.elements.values) | set(defective.basis.elements.values))\n", + "\n", + "# Materials.get_by_categories(\"elemental\") returns nothing -- \"elemental\" is a per-entry tag,\n", + "# not a registered category.\n", + "elemental_by_symbol = {}\n", + "for entry in Materials.get_as_list():\n", + " symbols = {atom[\"value\"] for atom in entry[\"basis\"][\"elements\"]}\n", + " if len(symbols) == 1 and symbols <= set(elements):\n", + " elemental_by_symbol[symbols.pop()] = entry\n", + "\n", + "missing = set(elements) - elemental_by_symbol.keys()\n", + "if missing:\n", + " raise ValueError(f\"No Standata elemental reference material for {missing}.\")\n", + "\n", + "elemental_materials = {element: Material.create(elemental_by_symbol[element]) for element in elements}\n", + "for element, material in elemental_materials.items():\n", + " print(f\"{element}: {material.name}\")\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 = Material.create(get_or_create_material(client, pristine, ACCOUNT_ID))\n", + "saved_defective = Material.create(get_or_create_material(client, defective, ACCOUNT_ID))\n", + "saved_elemental = {\n", + " element: Material.create(get_or_create_material(client, material, ACCOUNT_ID))\n", + " for element, material in elemental_materials.items()\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "## 4. Configure the shared 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_config['subtype']}/{model_config['functional']}, \"\n", + " f\"pseudopotential: {PSEUDOPOTENTIAL_TYPE}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "### 4.1. k-grid per material" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "import math\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", + "\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) for element, material in elemental_materials.items()},\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", + "cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), 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.core.entity.property.api import find_total_energy_for_material\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", + "# Reuses a finished Total Energy job per material if one exists; creates one otherwise.\n", + "prerequisite_materials = {PRISTINE_NAME: saved_pristine, **saved_elemental}\n", + "prerequisite_job_ids = []\n", + "for name, saved_material in prerequisite_materials.items():\n", + " if find_total_energy_for_material(client, saved_material.id, source=\"my_account\") is not None:\n", + " print(f\"♻️ {name}: reusing existing Total Energy\")\n", + " continue\n", + " workflow = Workflow.create(total_energy_workflow_config)\n", + " workflow.name = f\"Total Energy {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=saved_material)\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.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 prerequisite_job_ids:\n", + " submit_jobs(client.jobs, prerequisite_job_ids)\n", + " print(f\"✅ Submitted {len(prerequisite_job_ids)} prerequisite job(s).\")\n", + " await wait_for_jobs_to_finish_async(client.jobs, prerequisite_job_ids, poll_interval=POLL_INTERVAL)\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.wode.context.providers import PointsGridDataProvider\n", + "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 = MY_WORKFLOW_NAME\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", + " for context in (cutoffs_context,\n", + " PointsGridDataProvider(material=defective, dimensions=kgrid[DEFECTIVE_NAME],\n", + " isEdited=True).get_context_item_data()):\n", + " unit.add_context(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", + "\n", + "# nspin/magnetization on the defective cell's SCF only; the references are non-magnetic.\n", + "patch_workflow_qe_input(defect_workflow, {\"system\": {\"nspin\": 2, \"starting_magnetization(1)\": 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} {saved_defective.formula} {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": [ + "if defect_job_id:\n", + " 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" + ] + }, + { + "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" + ] + }, + { + "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." + ] + }, + { + "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", + "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): {'yes' if abs(difference) <= 0.5 else 'no'}\")\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 +} From 6990c6e823b3a0d76407afac36119b0a81346f5c Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 12 Sep 2026 22:35:59 -0700 Subject: [PATCH 3/6] fix: resolve elemental references from the platform, not Standata uploads The workflow selects elemental materials by tags/metadata.element and then total_energy properties by those materials' exabyteId; an uploaded Standata copy gets a fresh exabyteId and can never match, so the notebook's own B/N Total Energy jobs were invisible to it (review blocker). Elemental references now come from client.materials.list, the same platform documents the workflow itself resolves; the Total Energy prerequisite jobs run directly on them. Also, per review: - reuse check now mirrors the workflow's own precondition (a finished job named Total Energy on the material, owned by this account) instead of a property lookup that could match a different workflow or a stale model; - errored/unfinished prerequisite or defect jobs raise before anything downstream is built or read; - starting_magnetization is set on both species (B, N) -- the V_B moment sits on the N neighbours, not on B; - model reverted to GBRV ultrasoft (40/200 Ry), which exists for B and N; PAW's only justification was matching QPOD's GPAW setup; k-point density comment corrected (QPOD: 6 for relaxations, 12 for ground states -- 6 used here for all jobs); - results cell recomputes E_f from the notebook's own three total energies and warns if it disagrees with the workflow's value by more than 1 meV. --- ...int_vacancy_boron_nitride_SIMULATION.ipynb | 139 +++++++++++------- 1 file changed, 82 insertions(+), 57 deletions(-) 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 index e5f662368..e2e99ef00 100644 --- 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 @@ -20,8 +20,8 @@ "\n", "$\\mu_B$ is the total energy of boron's standard state per atom (QPOD's convention, $q = 0$).\n", "\n", - "QPOD's value is for a relaxed 84-atom cell (15.06 Å defect spacing); this notebook's 48-atom\n", - "cell is unrelaxed -- both effects together shift E_f by about 0.06 eV (measured with MACE).\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" ] }, { @@ -116,11 +116,11 @@ "outputs": [], "source": [ "FUNCTIONAL = \"pbe\"\n", - "PSEUDOPOTENTIAL_TYPE = \"paw\" # B, N: paw or nc only -- no ultrasoft PBE pseudopotential exists\n", - "ECUTWFC = 40 # higher of B/N PseudoDojo \"high\" hints (15, 20 Ha), in Ry\n", - "ECUTRHO = 8 * ECUTWFC # PAW needs a much denser augmentation charge\n", + "PSEUDOPOTENTIAL_TYPE = \"us\" # GBRV ultrasoft, the platform's default PBE family for B and N\n", + "ECUTWFC = 40 # GBRV's recommended cutoffs\n", + "ECUTRHO = 200\n", "\n", - "KPOINT_DENSITY = 6 # paper's ground-state k-point density (Bertoldo et al. 2022, 6.1), Å⁻¹\n" + "KPOINT_DENSITY = 6 # QPOD: 6 Å⁻¹ for relaxations, 12 for ground states; 6 used here for all jobs\n" ] }, { @@ -254,7 +254,7 @@ "for name, material in materials_by_name.items():\n", " a, b = material.lattice.a, material.lattice.b\n", " print(f\"{name}: {formula(material)}, {len(material.basis.elements.ids)} atoms, \"\n", - " f\"cell {a:.2f} x {b:.2f} Å, defect-defect distance {min(a, b):.2f} Å\")\n" + " f\"cell {a:.2f} x {b:.2f} Å, min in-plane vector {min(a, b):.2f} Å\")\n" ] }, { @@ -262,7 +262,11 @@ "id": "20", "metadata": {}, "source": [ - "### 3.2. Resolve elemental reference materials" + "### 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." ] }, { @@ -272,26 +276,19 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.made.material import Material\n", - "from mat3ra.standata.materials import Materials\n", - "\n", "elements = sorted(set(pristine.basis.elements.values) | set(defective.basis.elements.values))\n", "\n", - "# Materials.get_by_categories(\"elemental\") returns nothing -- \"elemental\" is a per-entry tag,\n", - "# not a registered category.\n", - "elemental_by_symbol = {}\n", - "for entry in Materials.get_as_list():\n", - " symbols = {atom[\"value\"] for atom in entry[\"basis\"][\"elements\"]}\n", - " if len(symbols) == 1 and symbols <= set(elements):\n", - " elemental_by_symbol[symbols.pop()] = entry\n", - "\n", - "missing = set(elements) - elemental_by_symbol.keys()\n", - "if missing:\n", - " raise ValueError(f\"No Standata elemental reference material for {missing}.\")\n", - "\n", - "elemental_materials = {element: Material.create(elemental_by_symbol[element]) for element in elements}\n", - "for element, material in elemental_materials.items():\n", - " print(f\"{element}: {material.name}\")\n" + "elemental_materials_data = client.materials.list(\n", + " {\"tags\": \"elemental\", \"metadata.element\": {\"$in\": sorted(elements)}}\n", + ")\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", + " print(f\"{element}: {matches[0]['name']} ({matches[0]['_id']}, owner {matches[0]['owner']['slug']})\")\n" ] }, { @@ -311,12 +308,8 @@ "source": [ "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", "\n", - "saved_pristine = Material.create(get_or_create_material(client, pristine, ACCOUNT_ID))\n", - "saved_defective = Material.create(get_or_create_material(client, defective, ACCOUNT_ID))\n", - "saved_elemental = {\n", - " element: Material.create(get_or_create_material(client, material, ACCOUNT_ID))\n", - " for element, material in elemental_materials.items()\n", - "}\n" + "saved_pristine = get_or_create_material(client, pristine, ACCOUNT_ID)\n", + "saved_defective = get_or_create_material(client, defective, ACCOUNT_ID)\n" ] }, { @@ -324,7 +317,8 @@ "id": "24", "metadata": {}, "source": [ - "## 4. Configure the shared DFT model" + "## 4. Configure the shared DFT model and k-grid\n", + "### 4.1. DFT model" ] }, { @@ -358,7 +352,7 @@ "id": "26", "metadata": {}, "source": [ - "### 4.1. k-grid per material" + "### 4.2. k-grid per material" ] }, { @@ -369,6 +363,12 @@ "outputs": [], "source": [ "import math\n", + "from mat3ra.made.material import Material\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", @@ -378,11 +378,10 @@ " grid[dim] = max(1, math.ceil(KPOINT_DENSITY * 2 * math.pi * norms[dim]))\n", " return grid\n", "\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) for element, material in elemental_materials.items()},\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" @@ -448,7 +447,6 @@ "source": [ "from mat3ra.standata.workflows import WorkflowStandata\n", "from mat3ra.wode.workflows import Workflow\n", - "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n", "from mat3ra.notebooks_utils.workflow import apply_scf_kgrid\n", "from mat3ra.notebooks_utils.job import create_job\n", "\n", @@ -456,12 +454,17 @@ " TOTAL_ENERGY_SEARCH_TERM\n", ")\n", "\n", - "# Reuses a finished Total Energy job per material if one exists; creates one otherwise.\n", - "prerequisite_materials = {PRISTINE_NAME: saved_pristine, **saved_elemental}\n", - "prerequisite_job_ids = []\n", + "# Reuses a job the workflow's own precondition would accept: finished, named Total Energy, on\n", + "# this material, owned by this account -- jobs on 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", - " if find_total_energy_for_material(client, saved_material.id, source=\"my_account\") is not None:\n", - " print(f\"♻️ {name}: reusing existing Total Energy\")\n", + " existing = client.jobs.list({\"_material._id\": saved_material[\"_id\"], \"owner._id\": ACCOUNT_ID,\n", + " \"status\": \"finished\", \"workflow.name\": {\"$regex\": \"Total Energy\"}})\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 = f\"Total Energy {name}\"\n", @@ -470,13 +473,14 @@ " 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=saved_material)\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.append(job_response[\"_id\"])\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" ] }, @@ -489,10 +493,15 @@ "source": [ "from mat3ra.notebooks_utils.api.job import submit_jobs, wait_for_jobs_to_finish_async\n", "\n", - "if prerequisite_job_ids:\n", - " submit_jobs(client.jobs, prerequisite_job_ids)\n", - " print(f\"✅ Submitted {len(prerequisite_job_ids)} prerequisite job(s).\")\n", - " await wait_for_jobs_to_finish_async(client.jobs, prerequisite_job_ids, poll_interval=POLL_INTERVAL)\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" ] }, { @@ -536,8 +545,10 @@ " source_unit.value = \"'my_account'\"\n", " subworkflow.set_unit(source_unit)\n", "\n", - "# nspin/magnetization on the defective cell's SCF only; the references are non-magnetic.\n", - "patch_workflow_qe_input(defect_workflow, {\"system\": {\"nspin\": 2, \"starting_magnetization(1)\": 0.5}},\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" @@ -562,7 +573,7 @@ "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} {saved_defective.formula} {timestamp}\",\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" @@ -583,10 +594,13 @@ "metadata": {}, "outputs": [], "source": [ - "if defect_job_id:\n", - " 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" + "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" ] }, { @@ -612,7 +626,18 @@ "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" + "e_formation = defect_energy_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 = get_properties_for_job(client, defect_job_id, property_name=\"total_energy\")[0][\"value\"]\n", + "e_pris = get_properties_for_job(client, prerequisite_job_ids[PRISTINE_NAME], property_name=\"total_energy\")[0][\"value\"]\n", + "e_b_per_atom = (get_properties_for_job(client, prerequisite_job_ids[\"B\"], property_name=\"total_energy\")[0][\"value\"]\n", + " / material_objects[\"B\"].basis.number_of_atoms)\n", + "e_formation_check = e_def - e_pris + e_b_per_atom\n", + "print(f\"E_f (workflow): {e_formation:.3f} eV, E_f (recomputed): {e_formation_check:.3f} eV\")\n", + "if abs(e_formation - e_formation_check) > 0.001:\n", + " print(\"⚠️ The workflow resolved a different reference energy than this notebook's own jobs.\")\n" ] }, { @@ -623,7 +648,7 @@ "### 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." + "this workflow does not compute. QPOD's 10.18 eV is itself PBE/PAW (GPAW) theory, not experiment." ] }, { From 64b562aa68fb7453f08ba3ca0de5dda514b5a13c Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 12 Sep 2026 22:59:36 -0700 Subject: [PATCH 4/6] fix: encode the model in the reference workflow name so reuse can't cross cutoffs Round-2 review blocker: the reuse query for the elemental prerequisite jobs matched any finished job named "Total Energy" on the material, regardless of what cutoffs or pseudopotential it used -- a curators' total_energy at a different model would be silently reused and pass the consistency check (which recomputes from that same job). Fix: the reference workflow name now carries the exact model ("Total Energy pbe-us 40/200 Ry"), and reuse matches that name exactly (no regex), sorted by updatedAt desc, limit 1 -- still a superstring "Total Energy", so fetch_bulk_total_energy's own regex still finds the pristine's job. Also, per review: - mu_B (per atom) and the B reference's atom count are printed, both where the elemental references are resolved and in the results cell; - the reference-mismatch warning now carries into the verdict line instead of sitting two cells above it; - the three total_energy property lookups in the results cell raise a named RuntimeError instead of a bare IndexError on an empty list; - an unmatched CLUSTER_NAME raises, listing the available cluster hostnames, instead of silently falling back to an arbitrary cluster; - the defect workflow's k-grid is applied with apply_scf_kgrid, like every other workflow in this notebook, instead of a second hand-built context; - atom count is read via basis.number_of_atoms everywhere (was mixed with len(...elements.ids)); the redundant second sorted() on elements is dropped; the k-point density comment states why 6, not 12 (cost; the neutral E_f does not need it). --- ...int_vacancy_boron_nitride_SIMULATION.ipynb | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) 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 index e2e99ef00..90fb47fcd 100644 --- 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 @@ -120,7 +120,10 @@ "ECUTWFC = 40 # GBRV's recommended cutoffs\n", "ECUTRHO = 200\n", "\n", - "KPOINT_DENSITY = 6 # QPOD: 6 Å⁻¹ for relaxations, 12 for ground states; 6 used here for all jobs\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" ] }, { @@ -253,7 +256,7 @@ "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)}, {len(material.basis.elements.ids)} atoms, \"\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" ] }, @@ -276,11 +279,11 @@ "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(\n", - " {\"tags\": \"elemental\", \"metadata.element\": {\"$in\": sorted(elements)}}\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", @@ -288,7 +291,9 @@ " 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", - " print(f\"{element}: {matches[0]['name']} ({matches[0]['_id']}, owner {matches[0]['owner']['slug']})\")\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" ] }, { @@ -343,8 +348,7 @@ "\n", "cutoffs_context = PlanewaveCutoffsContextProvider(\n", " wavefunction=ECUTWFC, density=ECUTRHO, isEdited=True).get_context_item_data()\n", - "print(f\"Using application: {app.name}, model: {model_config['subtype']}/{model_config['functional']}, \"\n", - " f\"pseudopotential: {PSEUDOPOTENTIAL_TYPE}\")\n" + "print(f\"Using application: {app.name}, model: {MODEL_TAG}\")\n" ] }, { @@ -363,7 +367,6 @@ "outputs": [], "source": [ "import math\n", - "from mat3ra.made.material import Material\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", @@ -424,7 +427,9 @@ "source": [ "from mat3ra.ide.compute import Compute\n", "\n", - "cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), clusters[0])\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", "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" @@ -454,20 +459,25 @@ " TOTAL_ENERGY_SEARCH_TERM\n", ")\n", "\n", - "# Reuses a job the workflow's own precondition would accept: finished, named Total Energy, on\n", - "# this material, owned by this account -- jobs on curators-owned materials (the elementals) are allowed.\n", + "# Reuses a job with this exact model, named Total Energy (still matched by fetch_bulk_total_energy's\n", + "# regex for the pristine), finished, owned by this account -- jobs on curators-owned materials (the\n", + "# 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", - " existing = client.jobs.list({\"_material._id\": saved_material[\"_id\"], \"owner._id\": ACCOUNT_ID,\n", - " \"status\": \"finished\", \"workflow.name\": {\"$regex\": \"Total Energy\"}})\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 = f\"Total Energy {name}\"\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", @@ -519,7 +529,6 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.wode.context.providers import PointsGridDataProvider\n", "from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input\n", "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", "\n", @@ -533,10 +542,7 @@ " if subworkflow.name == \"Compute Total Energy for Defective Material\":\n", " subworkflow.model = model\n", " unit = subworkflow.get_unit_by_name(name=\"pw_scf\")\n", - " for context in (cutoffs_context,\n", - " PointsGridDataProvider(material=defective, dimensions=kgrid[DEFECTIVE_NAME],\n", - " isEdited=True).get_context_item_data()):\n", - " unit.add_context(context)\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", @@ -544,6 +550,7 @@ " 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", @@ -628,15 +635,23 @@ "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 = get_properties_for_job(client, defect_job_id, property_name=\"total_energy\")[0][\"value\"]\n", - "e_pris = get_properties_for_job(client, prerequisite_job_ids[PRISTINE_NAME], property_name=\"total_energy\")[0][\"value\"]\n", - "e_b_per_atom = (get_properties_for_job(client, prerequisite_job_ids[\"B\"], property_name=\"total_energy\")[0][\"value\"]\n", - " / material_objects[\"B\"].basis.number_of_atoms)\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 abs(e_formation - e_formation_check) > 0.001:\n", + "if not reference_matches:\n", " print(\"⚠️ The workflow resolved a different reference energy than this notebook's own jobs.\")\n" ] }, @@ -661,11 +676,13 @@ "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): {'yes' if abs(difference) <= 0.5 else 'no'}\")\n" + "print(f\"Reproduces Bertoldo et al. (2022): {verdict}{note}\")\n" ] } ], From 144f2c2fee72b7d0295b3c40485e8ef2e96a7fc0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 12 Sep 2026 23:23:56 -0700 Subject: [PATCH 5/6] fix: let CLUSTER_NAME be injected, and correct the reuse-naming rationale Round-3 review blocker: cell 32's cluster lookup did `CLUSTER_NAME in c["hostname"]` unconditionally, which TypeErrors on None -- so a harness (or a user) could not point the notebook at "whatever cluster is available" the way every other SE in this directory allows. Restored the sibling's shape: raise (listing hostnames) when CLUSTER_NAME is set and not found, fall back to the first listed cluster when it is None. The prerequisite-reuse comment cited "fetch_bulk_total_energy's regex" for the pristine; the installed workflow (and standata main) resolves E_PRISTINE by a properties query with no job-name filter at all -- that regex form only exists in a local, behind-main checkout. Corrected the comment: the workflow name change is justified by the notebook's own reuse being model-aware, nothing else; if the pristine path still resolves a different reference, the results cell's consistency check catches it. Also: journal corrected to npj Computational Materials; the header states Delta N_B = -1 next to Eq. (3); ECUTWFC/ECUTRHO comments carry their units (Ry); Usage/Summary sections added, matching the other two SIMULATION notebooks; MODEL_TAG dropped its "/" (now "pbe-us 40-200Ry", which slugifies cleanly) and the defect workflow's name carries it too, for provenance; TIME_LIMIT is commented. --- ...int_vacancy_boron_nitride_SIMULATION.ipynb | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) 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 index 90fb47fcd..e6fc76389 100644 --- 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 @@ -8,7 +8,7 @@ "# 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\", 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", "Computes the neutral formation energy of the vacancy created in the\n", @@ -18,10 +18,24 @@ "\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$).\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" + "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 the recomputed E_f reproduces Bertoldo et al. (2022).\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" ] }, { @@ -94,7 +108,7 @@ "CLUSTER_NAME = \"cluster-001\"\n", "QUEUE_NAME = QueueName.OF\n", "PPN = 40\n", - "TIME_LIMIT = \"04:00:00\"\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" @@ -117,13 +131,13 @@ "source": [ "FUNCTIONAL = \"pbe\"\n", "PSEUDOPOTENTIAL_TYPE = \"us\" # GBRV ultrasoft, the platform's default PBE family for B and N\n", - "ECUTWFC = 40 # GBRV's recommended cutoffs\n", - "ECUTRHO = 200\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" + "MODEL_TAG = f\"{FUNCTIONAL}-{PSEUDOPOTENTIAL_TYPE} {ECUTWFC}-{ECUTRHO}Ry\"\n" ] }, { @@ -427,9 +441,12 @@ "source": [ "from mat3ra.ide.compute import Compute\n", "\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", + "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" @@ -459,9 +476,11 @@ " TOTAL_ENERGY_SEARCH_TERM\n", ")\n", "\n", - "# Reuses a job with this exact model, named Total Energy (still matched by fetch_bulk_total_energy's\n", - "# regex for the pristine), finished, owned by this account -- jobs on curators-owned materials (the\n", - "# elementals) are allowed.\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", @@ -536,7 +555,7 @@ " DEFECT_WORKFLOW_SEARCH_TERM\n", ")\n", "defect_workflow = Workflow.create(defect_workflow_config)\n", - "defect_workflow.name = MY_WORKFLOW_NAME\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", From 0b109f2a6d966b11c3e2ba3352cbf85c01f1bde0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 12 Sep 2026 23:37:50 -0700 Subject: [PATCH 6/6] fix: correct the manuscript journal and the Usage block's verdict claim Round-4 review: the structure notebook's header still cited "Nature" for Bertoldo et al. (2022), which is npj Computational Materials -- the documentation page already says npj, so the two disagreed on one screen. Same fix already applied to the simulation notebook's header in an earlier round. The simulation notebook's new Usage block said the verdict reports "the recomputed E_f" -- it reports the workflow's own defect_formation_energy; the recomputed value is only the mismatch flag. Fixed the sentence. --- .../specific_examples/defect_point_vacancy_boron_nitride.ipynb | 2 +- .../defect_point_vacancy_boron_nitride_SIMULATION.ipynb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 36ff98aca..b1f58e76a 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", 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 index e6fc76389..ee7e0fdef 100644 --- 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 @@ -30,7 +30,8 @@ "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 the recomputed E_f reproduces Bertoldo et al. (2022).\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",