diff --git a/.github/workflows/check_links.yml b/.github/workflows/check_links.yml
index 526ec827b..f15cac997 100644
--- a/.github/workflows/check_links.yml
+++ b/.github/workflows/check_links.yml
@@ -13,4 +13,6 @@ jobs:
- name: Link Checker
uses: lycheeverse/lychee-action@v1.8.0
with:
+ # materialsproject.org blocks automated requests (Cloudflare 403)
+ args: --verbose --no-progress --exclude 'materialsproject\.org' './**/*.md' './**/*.html'
fail: true
diff --git a/README.md b/README.md
index 3efca1142..f7bfea407 100644
--- a/README.md
+++ b/README.md
@@ -160,7 +160,7 @@ In order to run or edit the examples:
3. Optionally, for local Jupyter without OIDC, set legacy API token values in [settings.json](src/py/mat3ra/notebooks_utils/core/api/settings.json). See [Get Authentication Params](examples/system/get_authentication_params.ipynb) for details. API tokens can also be generated in [Account Preferences](https://docs.mat3ra.com/accounts/ui/preferences/api/) on the platform.
-NOTE: The Materials Project API key should be set in `settings.json` and obtained from [https://legacy.materialsproject.org/open](https://legacy.materialsproject.org/open).
+NOTE: The Materials Project API key should be set in `settings.json` and obtained from [https://next-gen.materialsproject.org/api](https://next-gen.materialsproject.org/api).
## Contribute
diff --git a/examples/assets/README.md b/examples/assets/README.md
index 69303e2d5..53fba23c8 100644
--- a/examples/assets/README.md
+++ b/examples/assets/README.md
@@ -2,6 +2,6 @@
This directory contains various files for the example notebooks.
-- `mp-978534.poscar` contains a SiGe structure from [https://legacy.materialsproject.org/materials/mp-978534](https://legacy.materialsproject.org/materials/mp-978534).
+- `mp-978534.poscar` contains a SiGe structure from [https://next-gen.materialsproject.org/materials/mp-978534](https://next-gen.materialsproject.org/materials/mp-978534).
- `bash_workflow_template.json` contains the template JSON schema of Mat3ra workflow.
- `Si.pz-vbc.UPF` is the pseudo potential file of silicon used in the Quantum Espresso workflow example.
diff --git a/other/materials_designer/uploads/H2O.json b/other/materials_designer/uploads/H2O.json
new file mode 100644
index 000000000..af0fb0555
--- /dev/null
+++ b/other/materials_designer/uploads/H2O.json
@@ -0,0 +1,62 @@
+{
+ "name": "H2O",
+ "basis": {
+ "elements": [
+ {
+ "id": 0,
+ "value": "O"
+ },
+ {
+ "id": 1,
+ "value": "H"
+ },
+ {
+ "id": 2,
+ "value": "H"
+ }
+ ],
+ "coordinates": [
+ {
+ "id": 0,
+ "value": [
+ 0.0,
+ 0.5,
+ 0.0
+ ]
+ },
+ {
+ "id": 1,
+ "value": [
+ 0.043139,
+ 0.431029,
+ 0.043078
+ ]
+ },
+ {
+ "id": 2,
+ "value": [
+ 0.036635,
+ 0.576406,
+ 0.035949
+ ]
+ }
+ ],
+ "units": "crystal",
+ "labels": [],
+ "constraints": []
+ },
+ "lattice": {
+ "a": 10.58354,
+ "b": 10.58354,
+ "c": 12.70025,
+ "alpha": 90.0,
+ "beta": 90.0,
+ "gamma": 90.0,
+ "units": {
+ "length": "angstrom",
+ "angle": "degree"
+ },
+ "type": "TRI"
+ },
+ "isNonPeriodic": true
+}
diff --git a/other/materials_designer/workflows/Introduction.ipynb b/other/materials_designer/workflows/Introduction.ipynb
index 592d0a641..d1a91ac91 100644
--- a/other/materials_designer/workflows/Introduction.ipynb
+++ b/other/materials_designer/workflows/Introduction.ipynb
@@ -69,7 +69,7 @@
"#### [6.4.1. Zero-point energy calculation.](zero_point_energy.ipynb)\n",
"\n",
"### 6.5. Defect Energy\n",
- "#### 6.5.1. Defect formation energy. *(to be added)*\n",
+ "#### [6.5.1. Defect formation energy.](defect_formation_energy.ipynb)\n",
"\n",
"### 6.6. Formation Energy\n",
"#### [6.6.1. Compound formation energy.](formation_energy.ipynb)\n",
@@ -84,10 +84,10 @@
"#### 7.1.1. NEB reaction pathway calculation. *(to be added)*\n",
"\n",
"### 7.2. HOMO-LUMO (NWChem)\n",
- "#### 7.2.1. HOMO-LUMO gap calculation. *(to be added)*\n",
+ "#### [7.2.1. HOMO-LUMO gap calculation.](homo_lumo_frequency.ipynb)\n",
"\n",
"### 7.3. Vibrational Frequency (NWChem)\n",
- "#### 7.3.1. Vibrational frequency calculation. *(to be added)*\n",
+ "#### [7.3.1. Vibrational frequency calculation.](homo_lumo_frequency.ipynb)\n",
"\n",
"\n",
"## 8. Electronics\n",
diff --git a/other/materials_designer/workflows/defect_formation_energy.ipynb b/other/materials_designer/workflows/defect_formation_energy.ipynb
new file mode 100644
index 000000000..fa92265c2
--- /dev/null
+++ b/other/materials_designer/workflows/defect_formation_energy.ipynb
@@ -0,0 +1,623 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# Defect Formation Energy\n",
+ "\n",
+ "Calculate the neutral defect formation energy (eV) of a defective supercell using a multi-material DFT workflow on the Mat3ra platform.\n",
+ "\n",
+ "The workflow takes **two materials, in order**:\n",
+ "\n",
+ "- **[0] Defective supercell** — the structure containing the defect(s) (vacancy, substitution, interstitial, or a mix).\n",
+ "- **[1] Pristine supercell** — the defect-free version of the *same* supercell.\n",
+ "\n",
+ "Only the defective cell is computed here (`pw_scf`). The pristine reference energy is fetched from a previously-finished **Total Energy** job, so the pristine must already be converged/relaxed and have such a job on the platform. Elemental chemical potentials are taken from Standata elemental reference materials (each needs a total energy too), as in [Formation Energy](formation_energy.ipynb).\n",
+ "\n",
+ "Formula:\n",
+ "\n",
+ "$$E_{\\text{defect}} = E_{\\text{defective}}[q] - E_{\\text{pristine}} - \\sum_i \\Delta N_i\\, \\mu_i + q\\,(E_{\\text{VBM}} + E_F) \\quad [\\text{eV}]$$\n",
+ "\n",
+ "Set `CHARGE` (cell 1.3) to a non-zero value to charge the defective supercell (`tot_charge` in the QE `&SYSTEM` namelist, compensated by a uniform jellium background). **This notebook does not compute a charged-defect finite-size correction** (e.g. Freysoldt-Neugebauer-Van de Walle) or track the Fermi-level term $q(E_{\\text{VBM}}+E_F)$ -- for `CHARGE = 0` (the default) neither is needed and the value below is directly physical; for `CHARGE != 0` the reported value is the raw, uncorrected total-energy difference only.\n",
+ "\n",
+ "where $\\Delta N_i = \\text{count}_i(\\text{defective}) - \\text{count}_i(\\text{pristine})$ is the per-species atom-count change and $\\mu_i = E_{\\text{elemental},i} / n_{\\text{atoms},i}$.\n",
+ "\n",
+ "The reported value is the **total** formation energy of the whole defective configuration (per cell), not per defect: for a single isolated defect it equals the point-defect formation energy; for multiple/interacting defects it is the aggregate for that configuration.\n",
+ "\n",
+ "
Usage
\n",
+ "\n",
+ "1. Build a pristine supercell and a defective version of it (for example via `create_point_defect.ipynb`, which produces both), and export both to `../uploads` — or set the names below to match materials on the platform.\n",
+ "2. Run [Total Energy](total_energy.ipynb) for the pristine supercell, and for each Standata elemental reference material.\n",
+ "3. Set parameters in cells 1.2 and 1.3 below.\n",
+ "4. Click \"Run\" > \"Run All\".\n",
+ "5. Inspect the resolved references, their total energies, and the final defect formation energy.\n",
+ "\n",
+ "## Summary\n",
+ "\n",
+ "1. Set up the environment and parameters.\n",
+ "2. Authenticate and initialize API client.\n",
+ "3. Load defective and pristine supercells, resolve elemental references, verify the pristine total energy, and assemble the job materials.\n",
+ "4. Configure the Defect Formation Energy workflow.\n",
+ "5. Configure compute.\n",
+ "6. Create, submit, and monitor the multi-material job.\n",
+ "7. Retrieve results."
+ ]
+ },
+ {
+ "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|api_examples\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3",
+ "metadata": {},
+ "source": [
+ "### 1.2. Set parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from datetime import datetime\n",
+ "from mat3ra.ide.compute import QueueName\n",
+ "\n",
+ "# 2. Auth and organization parameters\n",
+ "ORGANIZATION_NAME = None\n",
+ "\n",
+ "# 3. Material parameters\n",
+ "FOLDER = \"../uploads\"\n",
+ "DEFECTIVE_NAME = \"Si\" # defective supercell: name in uploads or on the platform\n",
+ "PRISTINE_NAME = \"Si\" # defect-free version of the SAME supercell\n",
+ "\n",
+ "# 4. Workflow parameters\n",
+ "WORKFLOW_SEARCH_TERM = \"defect_formation_energy.json\"\n",
+ "APPLICATION_NAME = \"espresso\"\n",
+ "MY_WORKFLOW_NAME = \"Defect Formation Energy\"\n",
+ "\n",
+ "# 5. Compute parameters\n",
+ "CLUSTER_NAME = None # specify full or partial name i.e. \"cluster-001\" to select\n",
+ "QUEUE_NAME = QueueName.D\n",
+ "PPN = 1\n",
+ "\n",
+ "# 6. Job parameters\n",
+ "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n",
+ "POLL_INTERVAL = 30 # seconds"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "### 1.3. Set specific defect formation energy parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# K-grid for the defective-cell SCF (if not set, KPPRA is used by default)\n",
+ "SCF_KGRID = None # e.g. [4, 4, 4]\n",
+ "\n",
+ "# Net charge on the defective supercell (e.g. -3 for a triply negatively charged\n",
+ "# defect, +1 for a singly positively charged one). 0 = neutral defect (default).\n",
+ "# NOTE: a non-zero charge requires a charged-defect finite-size correction (e.g.\n",
+ "# Freysoldt-Neugebauer-Van de Walle) to remove the spurious electrostatic\n",
+ "# interaction between the charged defect and its periodic images -- this\n",
+ "# notebook does NOT compute that correction, so CHARGE != 0 results are raw,\n",
+ "# uncorrected values only.\n",
+ "CHARGE = 0 # e.g. -3, +1\n",
+ "\n",
+ "# Whose total_energy properties to consider for the pristine reference:\n",
+ "# \"public\" (any owner, highest precision wins), \"curators\" (only curators'),\n",
+ "# or \"my_account\" (curators' or your own).\n",
+ "PRISTINE_TOTAL_ENERGY_SOURCE = \"my_account\"\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "## 2. Authenticate and initialize API client\n",
+ "### 2.1. Configure API endpoint\n",
+ "Local development: point the API client at a local platform instance. Remove or adjust these for the hosted platform."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8",
+ "metadata": {},
+ "source": [
+ "### 2.2. Authenticate\n",
+ "Authenticate in the browser and have credentials stored in environment variable `OIDC_ACCESS_TOKEN`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.auth import authenticate\n",
+ "\n",
+ "await authenticate()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "10",
+ "metadata": {},
+ "source": [
+ "### 2.3. Initialize API client"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "11",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.api_client import APIClient\n",
+ "\n",
+ "client = APIClient.authenticate()\n",
+ "client"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "12",
+ "metadata": {},
+ "source": [
+ "### 2.4. Select account to work under"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "13",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "client.list_accounts()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "selected_account = client.my_account\n",
+ "\n",
+ "if ORGANIZATION_NAME:\n",
+ " selected_account = client.get_account(name=ORGANIZATION_NAME)\n",
+ "\n",
+ "ACCOUNT_ID = selected_account.id\n",
+ "print(f\"✅ Selected account ID: {ACCOUNT_ID}, name: {selected_account.name}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "15",
+ "metadata": {},
+ "source": [
+ "### 2.5. Select project"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "16",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "projects = client.projects.list({\"isDefault\": True, \"owner._id\": ACCOUNT_ID})\n",
+ "project_id = projects[0][\"_id\"]\n",
+ "print(f\"✅ Using project: {projects[0]['name']} ({project_id})\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "17",
+ "metadata": {},
+ "source": [
+ "## 3. Load defective and pristine supercells\n",
+ "### 3.1. Load materials from local files or platform"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "18",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import re\n",
+ "from mat3ra.made.material import Material\n",
+ "from mat3ra.notebooks_utils.material import load_material_from_folder\n",
+ "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n",
+ "\n",
+ "\n",
+ "def load_material(name):\n",
+ " loaded = load_material_from_folder(FOLDER, name)\n",
+ " if loaded is not None:\n",
+ " print(f\"✅ Loaded '{name}' from folder: {loaded.name}\")\n",
+ " return loaded\n",
+ " matches = client.materials.list({\n",
+ " \"name\": {\"$regex\": re.escape(name), \"$options\": \"i\"},\n",
+ " \"owner._id\": ACCOUNT_ID,\n",
+ " })\n",
+ " if not matches:\n",
+ " raise ValueError(f\"No material containing '{name}' was found in '{FOLDER}' or on the platform.\")\n",
+ " material = Material.create(matches[0])\n",
+ " print(f\"♻️ Loaded '{name}' from platform: {matches[0]['_id']}\")\n",
+ " return material\n",
+ "\n",
+ "\n",
+ "defective = load_material(DEFECTIVE_NAME)\n",
+ "pristine = load_material(PRISTINE_NAME)\n",
+ "\n",
+ "visualize([{\"material\": defective, \"title\": \"Defective\"},\n",
+ " {\"material\": pristine, \"title\": \"Pristine\"}], repetitions=[1, 1, 1], rotation=\"-90x\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "19",
+ "metadata": {},
+ "source": [
+ "### 3.2. Prepare materials for Quantum ESPRESSO\n",
+ "Strip atom labels. Labels are useful for analysis notebooks but are not compatible with QE input generation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "20",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "defective_material = defective.clone()\n",
+ "defective_material.basis.set_labels_from_list([])\n",
+ "pristine_material = pristine.clone()\n",
+ "pristine_material.basis.set_labels_from_list([])\n",
+ "print(f\"Prepared defective: {defective_material.name}\")\n",
+ "print(f\"Prepared pristine: {pristine_material.name}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "21",
+ "metadata": {},
+ "source": [
+ "### 3.3. Resolve unique elements (union of defective and pristine)\n",
+ "The chemical-potential term covers every species whose count changes, so elements are taken from the union of the two structures (e.g. a species fully removed by a vacancy still appears via the pristine)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "22",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "elements = sorted(\n",
+ " set(defective_material.basis.elements.values) | set(pristine_material.basis.elements.values)\n",
+ ")\n",
+ "print(f\"Elements (defective ∪ pristine): {elements}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "23",
+ "metadata": {},
+ "source": [
+ "### 3.4. Resolve Standata elemental reference materials\n",
+ "Elemental chemical potentials come from Standata materials tagged `elemental` with `metadata.element`. Each must also have a refined `total_energy` — this is enforced by the workflow at runtime; run [Total Energy](total_energy.ipynb) for any elemental reference that is missing one."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "elemental_materials_data = client.materials.list(\n",
+ " {\"tags\": \"elemental\", \"metadata.element\": {\"$in\": elements}},\n",
+ ")\n",
+ "element_materials_by_symbol = {\n",
+ " element: [metadata for metadata in elemental_materials_data if\n",
+ " metadata.get(\"metadata\", {}).get(\"element\") == element]\n",
+ " for element in elements\n",
+ "}\n",
+ "missing = [element for element in elements if not element_materials_by_symbol.get(element)]\n",
+ "if missing:\n",
+ " raise RuntimeError(\n",
+ " f\"Missing elemental reference material(s) for {missing}. \"\n",
+ " \"Add elemental material(s) from Standata, or add tag 'elemental' with metadata.element = {element}\"\n",
+ " )\n",
+ "print(f\"Resolved elemental reference materials for: {', '.join(elements)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "25",
+ "metadata": {},
+ "source": [
+ "### 3.5. Save materials and check the pristine total energy\n",
+ "Save both materials and confirm the pristine already has a finished Total Energy job (its energy is fetched, not recomputed), then assemble the job materials in order: `[0]` defective, `[1]` pristine."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "26",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n",
+ "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n",
+ "\n",
+ "saved_defective = Material.create(get_or_create_material(client, defective_material, ACCOUNT_ID))\n",
+ "saved_pristine = Material.create(get_or_create_material(client, pristine_material, ACCOUNT_ID))\n",
+ "\n",
+ "pristine_te_property = find_total_energy_for_material(\n",
+ " client, saved_pristine.id, source=PRISTINE_TOTAL_ENERGY_SOURCE\n",
+ ")\n",
+ "if pristine_te_property is None:\n",
+ " raise RuntimeError(\"Run total_energy.ipynb for the pristine material first.\")\n",
+ "\n",
+ "# Order matters: [0] defective (computed), [1] pristine (reference).\n",
+ "materials = [saved_defective, saved_pristine]\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "27",
+ "metadata": {},
+ "source": [
+ "## 4. Configure the Defect Formation Energy workflow\n",
+ "### 4.1. Select application"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "28",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.ade.application import Application\n",
+ "from mat3ra.standata.applications import ApplicationStandata\n",
+ "\n",
+ "app_config = ApplicationStandata.get_by_name_first_match(APPLICATION_NAME)\n",
+ "app = Application(**app_config)\n",
+ "print(f\"Using application: {app.name}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "29",
+ "metadata": {},
+ "source": [
+ "### 4.2. Load workflow from Standata, apply parameters, and preview"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "30",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.standata.workflows import WorkflowStandata\n",
+ "from mat3ra.wode.workflows import Workflow\n",
+ "from mat3ra.wode.context.providers import PointsGridDataProvider\n",
+ "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n",
+ "from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input\n",
+ "\n",
+ "defect_workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(\n",
+ " WORKFLOW_SEARCH_TERM\n",
+ ")\n",
+ "defect_workflow = Workflow.create(defect_workflow_config)\n",
+ "defect_workflow.name = MY_WORKFLOW_NAME\n",
+ "print(f\"Loaded workflow: {defect_workflow.name}\")\n",
+ "print(f\"Multi-material: {getattr(defect_workflow, 'isMultiMaterial', False)}\")\n",
+ "\n",
+ "# K-grid for the defective-cell SCF.\n",
+ "if SCF_KGRID is not None:\n",
+ " new_context = PointsGridDataProvider(dimensions=SCF_KGRID, isEdited=True).get_context_item_data()\n",
+ " for subworkflow in defect_workflow.subworkflows:\n",
+ " unit = subworkflow.get_unit_by_name(name=\"pw_scf\")\n",
+ " if unit:\n",
+ " unit.add_context(new_context)\n",
+ " subworkflow.set_unit(unit)\n",
+ "\n",
+ "# Net charge on the defective-cell SCF: adds `tot_charge` to the &SYSTEM namelist\n",
+ "if CHARGE:\n",
+ " patch_workflow_qe_input(defect_workflow, {\"system\": {\"tot_charge\": CHARGE}}, unit_names=[\"pw_scf\"])\n",
+ "\n",
+ "visualize_workflow(defect_workflow)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "31",
+ "metadata": {},
+ "source": [
+ "## 5. Create the compute configuration\n",
+ "### 5.1. Select cluster"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "32",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "clusters = client.clusters.list()\n",
+ "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "33",
+ "metadata": {},
+ "source": [
+ "### 5.2. Create compute configuration"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "34",
+ "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",
+ "else:\n",
+ " cluster = clusters[0]\n",
+ "\n",
+ "compute = Compute(cluster=cluster, queue=QUEUE_NAME, ppn=PPN)\n",
+ "print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "35",
+ "metadata": {},
+ "source": [
+ "## 6. Create the Defect Formation Energy job\n",
+ "### 6.1. Create the multi-material job (defective + pristine)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "36",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.api.job import wait_for_jobs_to_finish_async\n",
+ "from mat3ra.notebooks_utils.job import create_job\n",
+ "from mat3ra.notebooks_utils.ui import display_JSON\n",
+ "from mat3ra.utils.namespace import dict_to_namespace_recursive\n",
+ "\n",
+ "defect_job_name = f\"{MY_WORKFLOW_NAME} {saved_defective.formula} {timestamp}\"\n",
+ "defect_job_response = create_job(\n",
+ " api_client=client,\n",
+ " materials=materials,\n",
+ " workflow=defect_workflow,\n",
+ " project_id=project_id,\n",
+ " owner_id=ACCOUNT_ID,\n",
+ " prefix=defect_job_name,\n",
+ " compute=compute.to_dict(),\n",
+ ")\n",
+ "\n",
+ "defect_job = dict_to_namespace_recursive(defect_job_response)\n",
+ "defect_job_id = defect_job._id\n",
+ "print(f\"✅ Defect Formation Energy job created: {defect_job_id}\")\n",
+ "display_JSON(defect_job_response)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "37",
+ "metadata": {},
+ "source": [
+ "### 6.2. Submit the Defect Formation Energy job and monitor the status"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "38",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "client.jobs.submit(defect_job_id)\n",
+ "print(f\"✅ Job {defect_job_id} submitted successfully!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "39",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "await wait_for_jobs_to_finish_async(client.jobs, [defect_job_id], poll_interval=POLL_INTERVAL)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "40",
+ "metadata": {},
+ "source": [
+ "## 7. Retrieve results\n",
+ "### 7.1. Retrieve and visualize defect formation energy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "41",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n",
+ "\n",
+ "defect_energy_data = client.properties.get_for_job(defect_job_id)\n",
+ "visualize_properties(defect_energy_data, title=\"Defect Formation Energy\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python",
+ "pygments_lexer": "ipython3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/other/materials_designer/workflows/dielectric_tensor.ipynb b/other/materials_designer/workflows/dielectric_tensor.ipynb
index 1355b1be1..e5ef233cd 100644
--- a/other/materials_designer/workflows/dielectric_tensor.ipynb
+++ b/other/materials_designer/workflows/dielectric_tensor.ipynb
@@ -437,7 +437,7 @@
" unit.add_context(cutoffs_context)\n",
" swf.set_unit(unit)\n",
"\n",
- "# epsilon.x energy grid -- this unit has no context provider, so it is patched directly\n",
+ "# epsilon.x energy grid\n",
"patch_workflow_qe_input(\n",
" workflow,\n",
" {\n",
@@ -631,12 +631,7 @@
"dielectric_tensor_data = get_properties_for_job(\n",
" client, job_id, property_name=PropertyName.non_scalar.dielectric_tensor.value\n",
")\n",
- "visualize_properties(dielectric_tensor_data, title=\"Dielectric Tensor\", extra_config={\"material\": material.to_dict()})\n",
- "\n",
- "# Sanity check: confirm real, non-trivial values were computed (not an empty/failed result)\n",
- "_real_entry = next(v for v in dielectric_tensor_data[0][\"values\"] if v[\"part\"] == \"real\")\n",
- "_eps_static = _real_entry[\"components\"][0][0]\n",
- "print(f\"Dielectric tensor: eps1(0) = {_eps_static:.2f}\")"
+ "visualize_properties(dielectric_tensor_data, title=\"Dielectric Tensor\", extra_config={\"material\": material.to_dict()})"
]
}
],
diff --git a/other/materials_designer/workflows/homo_lumo_frequency.ipynb b/other/materials_designer/workflows/homo_lumo_frequency.ipynb
new file mode 100644
index 000000000..ea38c2727
--- /dev/null
+++ b/other/materials_designer/workflows/homo_lumo_frequency.ipynb
@@ -0,0 +1,514 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# HOMO-LUMO and Vibrational Frequency (NWChem)\n",
+ "\n",
+ "Calculate frontier-orbital energies (HOMO, LUMO, and the HOMO-LUMO gap) and vibrational-frequency-derived thermochemistry (zero-point energy and thermal corrections) for a molecule using NWChem on the Mat3ra platform.\n",
+ "\n",
+ "Choose which calculations to run with the `CALCULATE_HOMO_LUMO` and `CALCULATE_FREQUENCY` toggles below. The frequency calculation returns thermochemical quantities (zero-point energy and thermal corrections); per-mode frequencies and IR spectra are not produced."
+ ]
+ },
+ {
+ "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|api_examples\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3",
+ "metadata": {},
+ "source": [
+ "### 1.2. Set parameters and configurations for the workflow and job"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from datetime import datetime\n",
+ "from mat3ra.ide.compute import QueueName\n",
+ "\n",
+ "# 2. Auth and organization parameters\n",
+ "# Set organization name to use it as the owner, otherwise your personal account is used\n",
+ "ORGANIZATION_NAME = None\n",
+ "\n",
+ "# 3. Material parameters\n",
+ "FOLDER = \"../uploads\"\n",
+ "MATERIAL_NAME = \"H2O\" # Molecule to load from the uploads folder (e.g. \"H2O\", \"CH4\", \"NH3\", \"O2\", \"N2\")\n",
+ "\n",
+ "# 4. Workflow parameters\n",
+ "APPLICATION_NAME = \"nwchem\"\n",
+ "\n",
+ "# Choose which calculations to run\n",
+ "CALCULATE_HOMO_LUMO = True # HOMO energy, LUMO energy, and HOMO-LUMO gap\n",
+ "CALCULATE_FREQUENCY = True # zero-point energy and thermal corrections\n",
+ "assert CALCULATE_HOMO_LUMO or CALCULATE_FREQUENCY, \"Enable at least one of CALCULATE_HOMO_LUMO / CALCULATE_FREQUENCY\"\n",
+ "\n",
+ "calculation_labels = []\n",
+ "if CALCULATE_HOMO_LUMO:\n",
+ " calculation_labels.append(\"HOMO-LUMO\")\n",
+ "if CALCULATE_FREQUENCY:\n",
+ " calculation_labels.append(\"Frequency\")\n",
+ "MY_WORKFLOW_NAME = \" + \".join(calculation_labels) + \" (nwchem)\"\n",
+ "\n",
+ "# 5. Compute parameters\n",
+ "CLUSTER_NAME = None # specify full or partial name i.e. \"cluster-001\" to select\n",
+ "QUEUE_NAME = QueueName.D\n",
+ "PPN = 1\n",
+ "\n",
+ "# 6. Job parameters\n",
+ "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n",
+ "POLL_INTERVAL = 30 # seconds\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "## 2. Authenticate and initialize API client\n",
+ "### 2.1. Authenticate\n",
+ "Authenticate in the browser and have credentials stored in environment variable \"OIDC_ACCESS_TOKEN\".\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.auth import authenticate\n",
+ "\n",
+ "\n",
+ "await authenticate()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "### 2.2. Initialize API Client\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.api_client import APIClient\n",
+ "\n",
+ "client = APIClient.authenticate()\n",
+ "client"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9",
+ "metadata": {},
+ "source": [
+ "### 2.3. Select account to work under"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "client.list_accounts()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "11",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "selected_account = client.my_account\n",
+ "\n",
+ "if ORGANIZATION_NAME:\n",
+ " selected_account = client.get_account(name=ORGANIZATION_NAME)\n",
+ "\n",
+ "ACCOUNT_ID = selected_account.id\n",
+ "print(f\"✅ Selected account ID: {ACCOUNT_ID}, name: {selected_account.name}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "12",
+ "metadata": {},
+ "source": [
+ "### 2.4. Select project"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "13",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "projects = client.projects.list({\"isDefault\": True, \"owner._id\": ACCOUNT_ID})\n",
+ "project_id = projects[0][\"_id\"]\n",
+ "print(f\"✅ Using project: {projects[0]['name']} ({project_id})\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "14",
+ "metadata": {},
+ "source": [
+ "## 3. Create material\n",
+ "### 3.1. Load molecule from local file"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "15",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.made.material import Material\n",
+ "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n",
+ "from mat3ra.notebooks_utils.material import load_material_from_folder\n",
+ "\n",
+ "material = load_material_from_folder(FOLDER, MATERIAL_NAME)\n",
+ "\n",
+ "visualize(material)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "16",
+ "metadata": {},
+ "source": [
+ "### 3.2. Save material to the platform"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n",
+ "\n",
+ "saved_material_response = get_or_create_material(client, material, ACCOUNT_ID)\n",
+ "saved_material = Material.create(saved_material_response)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18",
+ "metadata": {},
+ "source": [
+ "## 4. Create workflow and set its parameters\n",
+ "### 4.1. Get list of applications and select one"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "19",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.standata.applications import ApplicationStandata\n",
+ "from mat3ra.ade.application import Application\n",
+ "\n",
+ "app_config = ApplicationStandata.get_by_name_first_match(APPLICATION_NAME)\n",
+ "app = Application(**app_config)\n",
+ "print(f\"Using application: {app.name}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20",
+ "metadata": {},
+ "source": [
+ "### 4.2. Create workflow from standard workflows and preview it"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.standata.workflows import WorkflowStandata\n",
+ "from mat3ra.wode import Workflow, Subworkflow\n",
+ "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n",
+ "\n",
+ "nwchem_workflows = WorkflowStandata.filter_by_application(app.name)\n",
+ "\n",
+ "base_search_term = \"total_energy.json\" if CALCULATE_HOMO_LUMO else \"frequency.json\"\n",
+ "workflow = Workflow.create(nwchem_workflows.get_by_name_first_match(base_search_term))\n",
+ "\n",
+ "if CALCULATE_HOMO_LUMO and CALCULATE_FREQUENCY:\n",
+ " frequency_config = nwchem_workflows.get_by_name_first_match(\"frequency.json\")\n",
+ " workflow.add_subworkflow(Subworkflow(**frequency_config[\"subworkflows\"][0]))\n",
+ "\n",
+ "workflow.name = MY_WORKFLOW_NAME\n",
+ "\n",
+ "visualize_workflow(workflow)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "22",
+ "metadata": {},
+ "source": [
+ "### 4.4. Save workflow to collection"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "23",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.utils.namespace import dict_to_namespace_recursive\n",
+ "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n",
+ "\n",
+ "workflow_id_or_dict = None\n",
+ "\n",
+ "saved_workflow_response = get_or_create_workflow(client, workflow, ACCOUNT_ID)\n",
+ "saved_workflow = Workflow.create(saved_workflow_response)\n",
+ "print(f\"Workflow ID: {saved_workflow.id}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "24",
+ "metadata": {},
+ "source": [
+ "## 5. Create the compute configuration\n",
+ "### 5.1. Get list of clusters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "25",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "clusters = client.clusters.list()\n",
+ "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "26",
+ "metadata": {},
+ "source": [
+ "### 5.2. Create compute configuration for the job\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.ide.compute import Compute\n",
+ "\n",
+ "# Select cluster: use specified name if provided, otherwise use first available\n",
+ "if CLUSTER_NAME:\n",
+ " cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), None)\n",
+ "else:\n",
+ " cluster = clusters[0]\n",
+ "\n",
+ "compute = Compute(\n",
+ " cluster=cluster,\n",
+ " queue=QUEUE_NAME,\n",
+ " ppn=PPN\n",
+ ")\n",
+ "print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "28",
+ "metadata": {},
+ "source": [
+ "## 6. Create the job with material and workflow configuration\n",
+ "### 6.1. Create job"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.job import create_job\n",
+ "from mat3ra.notebooks_utils.ui import display_JSON\n",
+ "\n",
+ "print(f\"Material: {saved_material.id}\")\n",
+ "print(f\"Workflow: {saved_workflow.id}\")\n",
+ "print(f\"Project: {project_id}\")\n",
+ "\n",
+ "formula = saved_material.formula or material.formula or MATERIAL_NAME\n",
+ "job_name = MY_WORKFLOW_NAME + \" \" + formula + \" \" + timestamp\n",
+ "job_response = create_job(\n",
+ " api_client=client,\n",
+ " materials=[saved_material],\n",
+ " workflow=workflow,\n",
+ " project_id=project_id,\n",
+ " owner_id=ACCOUNT_ID,\n",
+ " prefix=job_name,\n",
+ " compute=compute.to_dict()\n",
+ ")\n",
+ "\n",
+ "job = dict_to_namespace_recursive(job_response)\n",
+ "job_id = job._id\n",
+ "print(\"✅ Job created successfully!\")\n",
+ "print(f\"Job ID: {job_id}\")\n",
+ "display_JSON(job_response)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "30",
+ "metadata": {},
+ "source": [
+ "## 7. Submit the job and monitor the status"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "client.jobs.submit(job_id)\n",
+ "print(f\"✅ Job {job_id} submitted successfully!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "32",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.api.job import wait_for_jobs_to_finish_async\n",
+ "\n",
+ "await wait_for_jobs_to_finish_async(client.jobs, [job_id], poll_interval=POLL_INTERVAL)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "33",
+ "metadata": {},
+ "source": [
+ "## 8. Retrieve results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "34",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n",
+ "\n",
+ "properties_data = client.properties.get_for_job(job_id)\n",
+ "visualize_properties(properties_data)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "35",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n",
+ "\n",
+ "if CALCULATE_HOMO_LUMO:\n",
+ " homo_data = client.properties.get_for_job(job_id, property_name=\"homo_energy\")\n",
+ " lumo_data = client.properties.get_for_job(job_id, property_name=\"lumo_energy\")\n",
+ " visualize_properties(homo_data, title=\"HOMO Energy\")\n",
+ " visualize_properties(lumo_data, title=\"LUMO Energy\")\n",
+ " homo_lumo_gap = lumo_data[0][\"value\"] - homo_data[0][\"value\"]\n",
+ " print(f\"HOMO-LUMO gap: {homo_lumo_gap:.4f} eV\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "36",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if CALCULATE_FREQUENCY:\n",
+ " zero_point_energy_data = client.properties.get_for_job(job_id, property_name=\"zero_point_energy\")\n",
+ " thermal_correction_to_energy_data = client.properties.get_for_job(job_id, property_name=\"thermal_correction_to_energy\")\n",
+ " thermal_correction_to_enthalpy_data = client.properties.get_for_job(job_id, property_name=\"thermal_correction_to_enthalpy\")\n",
+ " visualize_properties(zero_point_energy_data, title=\"Zero Point Energy\")\n",
+ " visualize_properties(thermal_correction_to_energy_data, title=\"Thermal Correction to Energy\")\n",
+ " visualize_properties(thermal_correction_to_enthalpy_data, title=\"Thermal Correction to Enthalpy\")\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",
+ "nbformat_minor": 5,
+ "pygments_lexer": "ipython3",
+ "version": "3.11.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb
index 18fa5c611..f265de0d7 100644
--- a/other/materials_designer/workflows/interfacial_energy.ipynb
+++ b/other/materials_designer/workflows/interfacial_energy.ipynb
@@ -120,7 +120,12 @@
"# Number of interfaces in the supercell: 1 if the slab has vacuum (one physical\n",
"# interface), 2 for a periodic stack with no vacuum. Substrate and film\n",
"# multiplicities are derived automatically from the structures.\n",
- "N_INTERFACES = 1"
+ "N_INTERFACES = 1\n",
+ "\n",
+ "# Whose total_energy properties to consider for the substrate/film bulk\n",
+ "# references: \"public\" (any owner, highest precision wins), \"curators\" (only\n",
+ "# curators'), or \"my_account\" (curators' or your own).\n",
+ "BULK_TOTAL_ENERGY_SOURCE = \"my_account\""
]
},
{
@@ -245,6 +250,7 @@
"if interface is None:\n",
" interface_matches = client.materials.list({\n",
" \"name\": {\"$regex\": re.escape(INTERFACE_NAME), \"$options\": \"i\"},\n",
+ " \"owner._id\": ACCOUNT_ID,\n",
" })\n",
" if not interface_matches:\n",
" raise ValueError(\n",
@@ -256,7 +262,7 @@
" print(f\"✅ Loaded interface from folder: {interface.name}\")\n",
"\n",
"visualize(interface, repetitions=[1, 1, 1])\n",
- "visualize(interface, repetitions=[1, 1, 1], rotation=\"-90x\")\n"
+ "visualize(interface, repetitions=[1, 1, 1], rotation=\"-90x\")"
]
},
{
@@ -356,14 +362,18 @@
"from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n",
"\n",
"substrate_bulk = get_bulk_material_by_crystal(client, CRYSTAL_SUBSTRATE, ACCOUNT_ID)\n",
- "substrate_te_property = find_total_energy_for_material(client, substrate_bulk.id)\n",
+ "substrate_te_property = find_total_energy_for_material(\n",
+ " client, substrate_bulk.id, source=BULK_TOTAL_ENERGY_SOURCE\n",
+ ")\n",
"if substrate_te_property is None:\n",
" raise RuntimeError(\"Substrate bulk total energy not found. Run total_energy.ipynb for the substrate bulk first.\")\n",
"print(f\"✅ Substrate bulk (material 1): {substrate_bulk.name} ({substrate_bulk.id}), \"\n",
" f\"total energy: {substrate_te_property['data']['value']} eV\")\n",
"\n",
"film_bulk = get_bulk_material_by_crystal(client, CRYSTAL_FILM, ACCOUNT_ID)\n",
- "film_te_property = find_total_energy_for_material(client, film_bulk.id)\n",
+ "film_te_property = find_total_energy_for_material(\n",
+ " client, film_bulk.id, source=BULK_TOTAL_ENERGY_SOURCE\n",
+ ")\n",
"if film_te_property is None:\n",
" raise RuntimeError(\"Film bulk total energy not found. Run total_energy.ipynb for the film bulk first.\")\n",
"print(f\"✅ Film bulk (material 2): {film_bulk.name} ({film_bulk.id}), \"\n",
@@ -614,14 +624,6 @@
"print(f\"Film bulk (material 2): {film_bulk.name} ({film_bulk.id}), \"\n",
" f\"total energy: {film_te_property['data']['value']} eV\")"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "43",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
diff --git a/other/materials_designer/workflows/phonon_dos_dispersion.ipynb b/other/materials_designer/workflows/phonon_dos_dispersion.ipynb
index 47b96ebf7..ac2cc4210 100644
--- a/other/materials_designer/workflows/phonon_dos_dispersion.ipynb
+++ b/other/materials_designer/workflows/phonon_dos_dispersion.ipynb
@@ -635,11 +635,7 @@
"phonon_dispersions_data = get_properties_for_job(\n",
" client, job_id, property_name=PropertyName.non_scalar.phonon_dispersions.value\n",
")\n",
- "visualize_properties(phonon_dispersions_data, title=\"Phonon Dispersion\", extra_config={\"material\": material.to_dict()})\n",
- "\n",
- "# Sanity check: confirm real, non-trivial values were computed (not an empty/failed result)\n",
- "_freqs = [f for band in phonon_dispersions_data[0][\"yDataSeries\"] for f in band]\n",
- "print(f\"Phonon dispersion: {len(phonon_dispersions_data[0]['yDataSeries'])} bands, frequency range {min(_freqs):.1f} to {max(_freqs):.1f} cm^-1\")"
+ "visualize_properties(phonon_dispersions_data, title=\"Phonon Dispersion\", extra_config={\"material\": material.to_dict()})"
]
},
{
@@ -660,12 +656,7 @@
"phonon_dos_data = get_properties_for_job(\n",
" client, job_id, property_name=PropertyName.non_scalar.phonon_dos.value\n",
")\n",
- "visualize_properties(phonon_dos_data, title=\"Phonon DOS\", extra_config={\"material\": material.to_dict()})\n",
- "\n",
- "# Sanity check: confirm real, non-trivial values were computed (not an empty/failed result)\n",
- "_dos_freqs = phonon_dos_data[0][\"xDataArray\"][0]\n",
- "_dos_vals = phonon_dos_data[0][\"yDataSeries\"][0]\n",
- "print(f\"Phonon DOS: frequency range {min(_dos_freqs):.1f} to {max(_dos_freqs):.1f} cm^-1, peak DOS {max(_dos_vals):.4f} states/cm^-1\")"
+ "visualize_properties(phonon_dos_data, title=\"Phonon DOS\", extra_config={\"material\": material.to_dict()})"
]
}
],
diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb
index cef5240bf..e98e13fc4 100644
--- a/other/materials_designer/workflows/surface_energy.ipynb
+++ b/other/materials_designer/workflows/surface_energy.ipynb
@@ -109,7 +109,12 @@
"outputs": [],
"source": [
"# K-grid for slab SCF (if not set, KPPRA is used by default)\n",
- "SCF_KGRID = None # e.g., [4, 4, 1]"
+ "SCF_KGRID = None # e.g., [4, 4, 1]\n",
+ "\n",
+ "# Whose total_energy properties to consider for the bulk reference: \"public\"\n",
+ "# (any owner, highest precision wins), \"curators\" (only curators'), or\n",
+ "# \"my_account\" (curators' or your own).\n",
+ "BULK_TOTAL_ENERGY_SOURCE = \"my_account\""
]
},
{
@@ -235,6 +240,7 @@
"if slab is None:\n",
" slab_matches = client.materials.list({\n",
" \"name\": {\"$regex\": re.escape(SLAB_NAME), \"$options\": \"i\"},\n",
+ " \"owner._id\": ACCOUNT_ID,\n",
" })\n",
" if not slab_matches:\n",
" raise ValueError(\n",
@@ -294,7 +300,9 @@
"\n",
"bulk_material = get_bulk_material(client, slab, ACCOUNT_ID)\n",
"\n",
- "bulk_total_energy_property = find_total_energy_for_material(client, bulk_material.id)\n",
+ "bulk_total_energy_property = find_total_energy_for_material(\n",
+ " client, bulk_material.id, source=BULK_TOTAL_ENERGY_SOURCE\n",
+ ")\n",
"if bulk_total_energy_property is None:\n",
" raise RuntimeError(\"Bulk total energy not found. Run total_energy.ipynb for the bulk material first.\")\n",
"\n",
diff --git a/src/py/mat3ra/notebooks_utils/core/entity/property/api.py b/src/py/mat3ra/notebooks_utils/core/entity/property/api.py
index e0189e0ac..4e3376a3b 100644
--- a/src/py/mat3ra/notebooks_utils/core/entity/property/api.py
+++ b/src/py/mat3ra/notebooks_utils/core/entity/property/api.py
@@ -71,7 +71,7 @@ def update_property_holder_value(client: APIClient, property_holder_id: str, val
return client.properties.update(property_holder_id, {"$set": {"data.value": value}})
-def find_total_energy_for_material(client: APIClient, material_id: str) -> Optional[dict]:
+def find_total_energy_for_material(client: APIClient, material_id: str, source: str = "my_account") -> Optional[dict]:
"""
Find the best-precision total_energy property for a material. Mirrors the
platform's "Resolve Total Energies for Elemental Materials" subworkflow,
@@ -84,6 +84,7 @@ def find_total_energy_for_material(client: APIClient, material_id: str) -> Optio
Args:
client (APIClient): API client instance.
material_id (str): Material _id to look up the total_energy property for.
+ source (str): Source of the total energy property: `public`, `my_account` or `curators`.
Returns:
The best-precision total_energy property, or None if none exists.
@@ -92,11 +93,15 @@ def find_total_energy_for_material(client: APIClient, material_id: str) -> Optio
exabyte_id = material.get("exabyteId")
if not exabyte_id:
return None
+ query = {"exabyteId": exabyte_id, "slug": "total_energy"}
+ if source == "curators":
+ query["owner.slug"] = "curators"
+ elif source == "my_account":
+ query["owner._id"] = client.my_account.id
+ elif source != "public":
+ raise ValueError(f"Invalid source: {source!r}. Expected 'public', 'curators', or 'my_account'.")
properties = client.properties.list(
- query={
- "exabyteId": exabyte_id,
- "slug": "total_energy",
- },
+ query=query,
projection={"sort": {"precision.value": -1}, "limit": 1},
)
return properties[0] if properties else None