From 0c9bf4d36bf238b21d223e9451c481b10344b6b3 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 12 Jul 2026 13:43:24 -0700 Subject: [PATCH 01/10] update: use made --- .../workflows/interfacial_energy.ipynb | 766 ++++++++++++++++++ .../core/entity/material/analysis.py | 70 +- 2 files changed, 806 insertions(+), 30 deletions(-) create mode 100644 other/materials_designer/workflows/interfacial_energy.ipynb diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb new file mode 100644 index 000000000..a3629f61e --- /dev/null +++ b/other/materials_designer/workflows/interfacial_energy.ipynb @@ -0,0 +1,766 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Interfacial Energy\n", + "\n", + "Calculate the interfacial energy γ (eV/Ų) of a heterostructure using a DFT workflow on the Mat3ra platform.\n", + "\n", + "This notebook loads an interface from `../uploads`, strips atom labels (required for Quantum ESPRESSO), resolves substrate and film bulk materials from `metadata.build` crystal hashes, and checks that both bulk materials and their `total_energy` properties already exist on the platform.\n", + "\n", + "If substrate bulk, film bulk, or either bulk total energy is missing, the notebook stops and points to the [Total Energy](total_energy.ipynb) notebook. It does not create or run bulk pre-calculations automatically.\n", + "\n", + "The job uses a single interface material. The workflow resolves bulks and fetches their precomputed total energies automatically.\n", + "\n", + "

Usage

\n", + "\n", + "1. Build an interface (for example via `create_interface_with_min_strain_zsl.ipynb`) and export it to `../uploads`, or set `INTERFACE_NAME` to match an existing interface on the platform.\n", + "2. Run [Total Energy](total_energy.ipynb) for each resolved substrate and film bulk material.\n", + "3. Set parameters in cells 1.2 and 1.3 below.\n", + "4. Click \"Run\" > \"Run All\".\n", + "5. Inspect resolved bulks, bulk total energies, and the final interfacial energy.\n", + "\n", + "## Summary\n", + "\n", + "1. Set up the environment and parameters.\n", + "2. Authenticate and initialize API client.\n", + "3. Load interface, strip labels, resolve substrate/film bulks, and verify they exist.\n", + "4. Verify bulk total energies exist.\n", + "5. Configure and save the Interfacial Energy workflow.\n", + "6. Configure compute.\n", + "7. Create, submit, and monitor the job.\n", + "8. Retrieve results.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Set up the environment and parameters\n", + "### 1.1. Install packages (JupyterLite)\n" + ] + }, + { + "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\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Set parameters\n" + ] + }, + { + "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", + "INTERFACE_NAME = \"Interface\" # Name to search in uploads or on the platform\n", + "\n", + "# 4. Workflow parameters\n", + "WORKFLOW_SEARCH_TERM = \"interfacial_energy.json\"\n", + "APPLICATION_NAME = \"espresso\"\n", + "MY_WORKFLOW_NAME = \"Interfacial 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\n" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "### 1.3. Set specific interfacial energy parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# K-grid for interface SCF (if not set, KPPRA is used by default)\n", + "SCF_KGRID = None # e.g., [4, 4, 1]" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## 2. Authenticate and initialize API client\n", + "### 2.1. Authenticate\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "await authenticate()\n" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "### 2.2. Initialize API client\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.api_client import APIClient\n", + "\n", + "client = APIClient.authenticate()\n", + "client\n" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### 2.3. Select account to work under\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "client.list_accounts()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "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": "14", + "metadata": {}, + "source": [ + "### 2.4. Select project\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "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": "16", + "metadata": {}, + "source": [ + "## 3. Load interface and resolve substrate/film bulks\n", + "### 3.1. Load interface from local file or platform\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "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", + "interface = load_material_from_folder(FOLDER, INTERFACE_NAME)\n", + "\n", + "if interface is None:\n", + " interface_matches = client.materials.list({\n", + " \"name\": {\"$regex\": re.escape(INTERFACE_NAME), \"$options\": \"i\"},\n", + " })\n", + " if not interface_matches:\n", + " raise ValueError(\n", + " f\"No interface containing '{INTERFACE_NAME}' was found in '{FOLDER}' or on the platform.\"\n", + " )\n", + " interface = Material.create(interface_matches[0])\n", + " print(f\"♻️ Loaded interface from platform: {interface_matches[0]['_id']}\")\n", + "else:\n", + " 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" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "### 3.2. Prepare interface for Quantum ESPRESSO\n", + "Strip atom labels. Labels are useful for analysis notebooks but are not compatible with QE input generation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "interface_material = interface.clone()\n", + "interface_material.basis.set_labels_from_list([])\n", + "print(f\"Prepared interface material: {interface_material.name}\")\n", + "visualize(interface_material, repetitions=[1, 1, 1], rotation=\"-90x\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### 3.3. Load workflow from Standata\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.workflows import WorkflowStandata\n", + "from mat3ra.wode.workflows import Workflow\n", + "\n", + "interfacial_workflow_config = WorkflowStandata.filter_by_application(APPLICATION_NAME).get_by_name_first_match(\n", + " WORKFLOW_SEARCH_TERM\n", + ")\n", + "interfacial_workflow = Workflow.create(interfacial_workflow_config)\n", + "resolve_refs_subworkflow = interfacial_workflow.subworkflows[0]\n", + "fetch_substrate_te_subworkflow = interfacial_workflow.subworkflows[1]\n", + "fetch_film_te_subworkflow = interfacial_workflow.subworkflows[2]\n", + "print(f\"Loaded workflow: {interfacial_workflow.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "### 3.4. Resolve substrate bulk from interface build metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.material.analysis import get_interface_bulk_crystal\n", + "from mat3ra.notebooks_utils.ui import display_JSON\n", + "\n", + "CRYSTAL_SUBSTRATE = get_interface_bulk_crystal(interface_material, part=\"substrate\")\n", + "io_unit = resolve_refs_subworkflow.get_unit_by_name(name=\"io-bulk-substrate\")\n", + "query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "projection_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "substrate_query = eval(query_template, {\"__builtins__\": {}}, {\"CRYSTAL_SUBSTRATE\": CRYSTAL_SUBSTRATE})\n", + "projection = eval(projection_template, {\"__builtins__\": {}})\n", + "substrate_matches = client.materials.list(query=substrate_query, projection=projection)\n", + "if not substrate_matches:\n", + " raise ValueError(\n", + " \"The substrate bulk material resolved from interface metadata is not on the platform. \"\n", + " \"Run total_energy.ipynb for that bulk material first, then rerun this notebook.\"\n", + " )\n", + "\n", + "substrate_bulk_response = substrate_matches[0]\n", + "substrate_bulk = Material.create(substrate_bulk_response)\n", + "BULK_SUBSTRATE_MATERIAL_IDS = [material[\"_id\"] for material in substrate_matches]\n", + "print(f\"Found substrate bulk material: {substrate_bulk_response['_id']}\")\n", + "print(f\"Resolved substrate bulk query: {substrate_query}\")\n", + "display_JSON({\"substrate_query\": substrate_query}, level=2)\n", + "visualize(substrate_bulk)\n" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "### 3.5. Resolve film bulk from interface build metadata\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "CRYSTAL_FILM = get_interface_bulk_crystal(interface_material, part=\"film\")\n", + "io_unit = resolve_refs_subworkflow.get_unit_by_name(name=\"io-bulk-film\")\n", + "query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "projection_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "film_query = eval(query_template, {\"__builtins__\": {}}, {\"CRYSTAL_FILM\": CRYSTAL_FILM})\n", + "projection = eval(projection_template, {\"__builtins__\": {}})\n", + "film_matches = client.materials.list(query=film_query, projection=projection)\n", + "if not film_matches:\n", + " raise ValueError(\n", + " \"The film bulk material resolved from interface metadata is not on the platform. \"\n", + " \"Run total_energy.ipynb for that bulk material first, then rerun this notebook.\"\n", + " )\n", + "\n", + "film_bulk_response = film_matches[0]\n", + "film_bulk = Material.create(film_bulk_response)\n", + "BULK_FILM_MATERIAL_IDS = [material[\"_id\"] for material in film_matches]\n", + "print(f\"Found film bulk material: {film_bulk_response['_id']}\")\n", + "print(f\"Resolved film bulk query: {film_query}\")\n", + "display_JSON({\"film_query\": film_query}, level=2)\n", + "visualize(film_bulk)\n" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "### 3.6. Save the interface material for the workflow\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "\n", + "saved_interface_response = get_or_create_material(client, interface_material, ACCOUNT_ID)\n", + "saved_interface = Material.create(saved_interface_response)\n", + "print(f\"✅ Saved interface material: {saved_interface_response['_id']}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "## 4. Check bulk total energies\n", + "### 4.1. Check substrate bulk total energy\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract the substrate bulk TE queries and projections from the workflow definition\n", + "# so they stay in sync with what the workflow actually uses at runtime.\n", + "io_job_unit = fetch_substrate_te_subworkflow.get_unit_by_name(name=\"io-bulk-te-job-substrate\")\n", + "job_query_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "job_projection_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "BULK_MATERIAL_IDS = BULK_SUBSTRATE_MATERIAL_IDS\n", + "job_query = eval(job_query_template, {\"__builtins__\": {}}, {\"BULK_MATERIAL_IDS\": BULK_MATERIAL_IDS})\n", + "job_projection = eval(job_projection_template, {\"__builtins__\": {}})\n", + "jobs = client.jobs.list(query=job_query, projection=job_projection)\n", + "if not jobs:\n", + " raise RuntimeError(\n", + " \"Substrate bulk total energy not found. Run total_energy.ipynb for the substrate bulk first.\"\n", + " )\n", + "\n", + "BULK_TE_JOB_ID = jobs[0][\"_id\"]\n", + "io_te_unit = fetch_substrate_te_subworkflow.get_unit_by_name(name=\"io-te-bulk-substrate\")\n", + "te_query_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "te_projection_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "te_query = eval(te_query_template, {\"__builtins__\": {}}, {\"BULK_TE_JOB_ID\": BULK_TE_JOB_ID})\n", + "te_projection = eval(te_projection_template, {\"__builtins__\": {}})\n", + "props = client.properties.list(query=te_query, projection=te_projection)\n", + "substrate_te_property = props[0] if props else None\n", + "if substrate_te_property is None:\n", + " raise RuntimeError(\n", + " \"Substrate bulk total energy not found. Run total_energy.ipynb for the substrate bulk first.\"\n", + " )\n", + "\n", + "print(\n", + " f\"♻️ Found substrate bulk total energy: {substrate_te_property['_id']} -> \"\n", + " f\"{substrate_te_property['data']['value']}\"\n", + ")\n", + "display_JSON(substrate_te_property, level=2)\n" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "### 4.2. Check film bulk total energy\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "io_job_unit = fetch_film_te_subworkflow.get_unit_by_name(name=\"io-bulk-te-job-film\")\n", + "job_query_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "job_projection_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "BULK_MATERIAL_IDS = BULK_FILM_MATERIAL_IDS\n", + "job_query = eval(job_query_template, {\"__builtins__\": {}}, {\"BULK_MATERIAL_IDS\": BULK_MATERIAL_IDS})\n", + "job_projection = eval(job_projection_template, {\"__builtins__\": {}})\n", + "jobs = client.jobs.list(query=job_query, projection=job_projection)\n", + "if not jobs:\n", + " raise RuntimeError(\n", + " \"Film bulk total energy not found. Run total_energy.ipynb for the film bulk first.\"\n", + " )\n", + "\n", + "BULK_TE_JOB_ID = jobs[0][\"_id\"]\n", + "io_te_unit = fetch_film_te_subworkflow.get_unit_by_name(name=\"io-te-bulk-film\")\n", + "te_query_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "te_projection_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "te_query = eval(te_query_template, {\"__builtins__\": {}}, {\"BULK_TE_JOB_ID\": BULK_TE_JOB_ID})\n", + "te_projection = eval(te_projection_template, {\"__builtins__\": {}})\n", + "props = client.properties.list(query=te_query, projection=te_projection)\n", + "film_te_property = props[0] if props else None\n", + "if film_te_property is None:\n", + " raise RuntimeError(\n", + " \"Film bulk total energy not found. Run total_energy.ipynb for the film bulk first.\"\n", + " )\n", + "\n", + "print(\n", + " f\"♻️ Found film bulk total energy: {film_te_property['_id']} -> \"\n", + " f\"{film_te_property['data']['value']}\"\n", + ")\n", + "display_JSON(film_te_property, level=2)\n" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "## 5. Configure the Interfacial Energy workflow\n", + "### 5.1. Select application\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "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}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "### 5.2. Configure workflow and preview it\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.wode.context.providers import PointsGridDataProvider\n", + "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", + "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", + "\n", + "\n", + "def apply_workflow_kgrids(workflow: Workflow, scf_kgrid=None) -> Workflow:\n", + " if scf_kgrid is not None:\n", + " new_context_scf = PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data()\n", + " for subworkflow in workflow.subworkflows:\n", + " unit_names = [unit.name for unit in subworkflow.units]\n", + " if \"pw_scf\" not in unit_names:\n", + " continue\n", + " unit_to_modify_scf = subworkflow.get_unit_by_name(name=\"pw_scf\")\n", + " unit_to_modify_scf.add_context(new_context_scf)\n", + " subworkflow.set_unit(unit_to_modify_scf)\n", + " break\n", + " return workflow\n", + "\n", + "\n", + "interfacial_workflow.name = MY_WORKFLOW_NAME\n", + "interfacial_workflow = apply_workflow_kgrids(interfacial_workflow, scf_kgrid=SCF_KGRID)\n", + "\n", + "visualize_workflow(interfacial_workflow)\n" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### 5.3. Save workflow to collection\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "saved_interfacial_workflow_response = get_or_create_workflow(client, interfacial_workflow, ACCOUNT_ID)\n", + "saved_interfacial_workflow = Workflow.create(saved_interfacial_workflow_response)\n", + "print(f\"Interfacial Energy workflow ID: {saved_interfacial_workflow.id}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "## 6. Create the compute configuration\n", + "### 6.1. Select cluster\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "clusters = client.clusters.list()\n", + "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "### 6.2. Create compute configuration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "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}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "## 7. Create the Interfacial Energy job\n", + "### 7.1. Create job with interface and workflow configuration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.job import create_job, wait_for_jobs_to_finish_async\n", + "from mat3ra.utils.namespace import dict_to_namespace_recursive\n", + "\n", + "print(f\"Interface material: {saved_interface.id}\")\n", + "print(f\"Substrate bulk material: {substrate_bulk_response['_id']}\")\n", + "print(f\"Film bulk material: {film_bulk_response['_id']}\")\n", + "print(f\"Interfacial workflow: {saved_interfacial_workflow.id}\")\n", + "print(f\"Project: {project_id}\")\n", + "\n", + "interfacial_job_name = f\"{MY_WORKFLOW_NAME} {saved_interface.formula} {timestamp}\"\n", + "interfacial_job_response = create_job(\n", + " api_client=client,\n", + " materials=[saved_interface],\n", + " workflow=saved_interfacial_workflow,\n", + " project_id=project_id,\n", + " owner_id=ACCOUNT_ID,\n", + " prefix=interfacial_job_name,\n", + " compute=compute.to_dict(),\n", + ")\n", + "\n", + "interfacial_job = dict_to_namespace_recursive(interfacial_job_response)\n", + "interfacial_job_id = interfacial_job._id\n", + "print(f\"✅ Interfacial Energy job created successfully: {interfacial_job_id}\")\n", + "display_JSON(interfacial_job_response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "### 7.2. Submit the Interfacial Energy job and monitor the status\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "client.jobs.submit(interfacial_job_id)\n", + "print(f\"✅ Job {interfacial_job_id} submitted successfully!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "await wait_for_jobs_to_finish_async(client.jobs, [interfacial_job_id], poll_interval=POLL_INTERVAL)\n" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "## 8. Retrieve results\n", + "### 8.1. Retrieve and visualize interfacial energy\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.prode import PropertyName\n", + "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n", + "\n", + "interfacial_energy_data = client.properties.get_for_job(\n", + " interfacial_job_id, property_name=PropertyName.scalar.interfacial_energy.value\n", + ")\n", + "interface_te_data = client.properties.get_for_job(\n", + " interfacial_job_id, property_name=PropertyName.scalar.total_energy.value\n", + ")\n", + "\n", + "if interfacial_energy_data:\n", + " visualize_properties(interfacial_energy_data, title=\"Interfacial Energy\")\n", + "else:\n", + " print(\"No 'interfacial_energy' property was returned for the job.\")\n", + "\n", + "if interface_te_data:\n", + " visualize_properties(interface_te_data, title=\"Interface Total Energy\")\n", + "else:\n", + " print(\"No interface total energy property was returned for the job.\")\n", + "\n", + "print(f\"Resolved substrate bulk query: {substrate_query}\")\n", + "print(f\"Substrate bulk material: {substrate_bulk_response['_id']}\")\n", + "print(f\"Substrate bulk total energy: {substrate_te_property['data']['value']}\")\n", + "print(f\"Resolved film bulk query: {film_query}\")\n", + "print(f\"Film bulk material: {film_bulk_response['_id']}\")\n", + "print(f\"Film bulk total energy: {film_te_property['data']['value']}\")\n", + "print(f\"Saved interface material: {saved_interface_response['_id']}\")\n" + ] + } + ], + "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/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py b/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py index d7156261c..68eee6b77 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union import ase.build import ase.constraints @@ -7,6 +7,11 @@ import pymatgen.io.ase import pymatgen.symmetry.analyzer from mat3ra.made.material import Material +from mat3ra.made.tools.analyze.interface_material import ( + get_interface_bulk_crystal as get_interface_bulk_crystal_from_build_metadata, +) +from mat3ra.made.tools.analyze.slab import SlabMaterialAnalyzer +from mat3ra.made.tools.build import MaterialWithBuildMetadata def is_symmetric(slab: pymatgen.core.structure.Structure) -> bool: @@ -107,43 +112,29 @@ def get_slab_area(a_vector: np.ndarray, b_vector: np.ndarray) -> float: return np.linalg.norm(crossprod) +def get_slab_bulk_crystal(slab_material: Material) -> dict: + slab_with_metadata = MaterialWithBuildMetadata.create(slab_material.to_dict()) + crystal = SlabMaterialAnalyzer(material=slab_with_metadata).build_configuration.atomic_layers.crystal + if crystal is None: + raise ValueError("No bulk crystal for slab in build metadata.") + return crystal if isinstance(crystal, dict) else crystal.to_dict() + + +def get_interface_bulk_crystal(interface_material: Material, part: str = "substrate") -> dict: + interface_with_metadata = MaterialWithBuildMetadata.create(interface_material.to_dict()) + return get_interface_bulk_crystal_from_build_metadata(interface_with_metadata, part=part) + + def get_bulk_material(api_client: Any, slab_material: Material, owner_id: str): slab_dict = slab_material.to_dict() metadata = slab_dict.get("metadata") or {} - bulk_crystal = None if metadata.get("bulkId") is not None: bulk_query = {"_id": metadata["bulkId"]} else: - for build_step in reversed(metadata.get("build") or []): - try: - bulk_crystal = build_step["configuration"]["stack_components"][0]["crystal"] - break - except (KeyError, IndexError, TypeError): - continue - - if bulk_crystal is None: - raise ValueError( - "No metadata.build[*].configuration.stack_components[0].crystal entry was found on the slab." - ) - - if bulk_crystal.get("_id") is not None: - bulk_query = {"_id": bulk_crystal["_id"]} - elif bulk_crystal.get("scaledHash") is not None: - bulk_query = {"scaledHash": bulk_crystal["scaledHash"]} - elif bulk_crystal.get("hash") is not None: - bulk_query = {"hash": bulk_crystal["hash"]} - else: - try: - bulk_query = {"hash": Material.create(bulk_crystal).hash} - except Exception as exc: - raise ValueError("Could not resolve a bulk query from the slab metadata.") from exc + bulk_query = resolve_bulk_query_from_crystal(get_slab_bulk_crystal(slab_material)) - matches = api_client.materials.list(bulk_query) - bulk_material_response = next( - (item for item in matches if item.get("owner", {}).get("_id") == owner_id), - None, - ) or (matches[0] if matches else None) + bulk_material_response = find_owned_material(api_client, bulk_query, owner_id) if bulk_material_response is None: raise ValueError( @@ -153,3 +144,22 @@ def get_bulk_material(api_client: Any, slab_material: Material, owner_id: str): print(f"Found exact bulk material: {bulk_material_response['_id']}") return bulk_query, bulk_material_response, Material.create(bulk_material_response) + + +def resolve_bulk_query_from_crystal(bulk_crystal: dict) -> dict: + if bulk_crystal.get("_id") is not None: + return {"_id": bulk_crystal["_id"]} + if bulk_crystal.get("scaledHash") is not None: + return {"scaledHash": bulk_crystal["scaledHash"]} + if bulk_crystal.get("hash") is not None: + return {"hash": bulk_crystal["hash"]} + try: + return {"hash": Material.create(bulk_crystal).hash} + except Exception as exc: + raise ValueError("Could not resolve a bulk query from crystal metadata.") from exc + + +def find_owned_material(api_client: Any, bulk_query: dict, owner_id: str) -> Optional[Any]: + matches = api_client.materials.list(bulk_query) + owned = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None) + return owned or (matches[0] if matches else None) From 2f5d67dc8eda621723c0d7e88b9e17b71a32d0da Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 13 Jul 2026 13:32:30 -0700 Subject: [PATCH 02/10] update: intro nb --- README.md | 1 + other/materials_designer/workflows/Introduction.ipynb | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be63c0d8d..3efca1142 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Workflow notebooks that run simulations. | [Band Structure (HSE)](other/materials_designer/workflows/band_structure_hse.ipynb) | Band structure with the HSE hybrid functional. | | [Convergence](other/materials_designer/workflows/convergence.ipynb) | General convergence study. | | [Surface Energy](other/materials_designer/workflows/surface_energy.ipynb) | Calculate surface formation energy. | +| [Interfacial Energy](other/materials_designer/workflows/interfacial_energy.ipynb) | Calculate interfacial energy γ at a heterostructure. | | [Valence Band Offset](other/materials_designer/workflows/valence_band_offset.ipynb) | Determine the valence band offset at an interface. | ### `notebooks_utils` — Python Utility Package diff --git a/other/materials_designer/workflows/Introduction.ipynb b/other/materials_designer/workflows/Introduction.ipynb index 2f78cebea..d57822d23 100644 --- a/other/materials_designer/workflows/Introduction.ipynb +++ b/other/materials_designer/workflows/Introduction.ipynb @@ -60,10 +60,10 @@ "#### [6.1.1. Equation of State](equation_of_state.ipynb)\n", "\n", "### 6.2. Surface Energy\n", - "#### 6.2.1. Surface energy calculation. *(to be added)*\n", + "#### [6.2.1. Surface energy calculation.](surface_energy.ipynb)\n", "\n", - "### 6.3. Interface Energy\n", - "#### 6.3.1. Interface energy calculation. *(to be added)*\n", + "### 6.3. Interfacial Energy\n", + "#### [6.3.1. Interfacial energy calculation.](interfacial_energy.ipynb)\n", "\n", "### 6.4. Zero-Point Energy\n", "#### 6.4.1. Zero-point energy calculation. *(to be added)*\n", From 31a2f8bfa200e3988842e093e8091ac41ac6bbcc Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 13 Jul 2026 13:33:08 -0700 Subject: [PATCH 03/10] update: use helper --- .../workflows/surface_energy.ipynb | 219 ++++++++++++------ 1 file changed, 143 insertions(+), 76 deletions(-) diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb index 10f20e754..2821513d0 100644 --- a/other/materials_designer/workflows/surface_energy.ipynb +++ b/other/materials_designer/workflows/surface_energy.ipynb @@ -59,7 +59,7 @@ "id": "3", "metadata": {}, "source": [ - "### 1.2. Set parameters and configurations for the workflow and job\n" + "### 1.2. Set parameters\n" ] }, { @@ -73,7 +73,6 @@ "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", @@ -81,9 +80,8 @@ "SLAB_NAME = \"Slab\" # Name of the slab to load from uploads folder or from the platform\n", "\n", "# 4. Workflow parameters\n", - "WORKFLOW_SEARCH_TERM = \"surface_energy.json\" # Search term for Workflows Standata\n", - "APPLICATION_NAME = \"espresso\" # Specify application name (e.g., \"espresso\", \"vasp\", \"nwchem\")\n", - "SCF_KGRID = None # e.g., [4, 4, 1]\n", + "WORKFLOW_SEARCH_TERM = \"surface_energy.json\"\n", + "APPLICATION_NAME = \"espresso\"\n", "MY_WORKFLOW_NAME = \"Surface Energy\"\n", "\n", "# 5. Compute parameters\n", @@ -100,6 +98,25 @@ "cell_type": "markdown", "id": "5", "metadata": {}, + "source": [ + "### 1.3. Set specific surface energy parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# K-grid for slab SCF (if not set, KPPRA is used by default)\n", + "SCF_KGRID = None # e.g., [4, 4, 1]" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, "source": [ "## 2. Authenticate and initialize API client\n", "### 2.1. Authenticate\n", @@ -109,7 +126,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -120,7 +137,7 @@ }, { "cell_type": "markdown", - "id": "7", + "id": "9", "metadata": {}, "source": [ "### 2.2. Initialize API client\n" @@ -129,7 +146,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -141,7 +158,7 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "11", "metadata": {}, "source": [ "### 2.3. Select account to work under\n" @@ -150,7 +167,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -160,7 +177,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -175,7 +192,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "### 2.4. Select project\n" @@ -184,7 +201,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -195,7 +212,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "## 3. Load slab and resolve the exact bulk candidate\n", @@ -205,7 +222,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -234,43 +251,101 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ - "### 3.2. Check for the exact bulk material from slab metadata\n" + "### 3.2. Load workflow from Standata\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.core.entity.material.analysis import get_bulk_material\n", - "from mat3ra.notebooks_utils.ui import display_JSON\n", + "from mat3ra.standata.workflows import WorkflowStandata\n", + "from mat3ra.wode.workflows import Workflow\n", "\n", - "bulk_query, bulk_material_response, bulk_material = get_bulk_material(\n", - " client,\n", - " slab,\n", - " ACCOUNT_ID,\n", + "surface_workflow_config = WorkflowStandata.filter_by_application(APPLICATION_NAME).get_by_name_first_match(\n", + " WORKFLOW_SEARCH_TERM\n", ")\n", + "surface_workflow = Workflow.create(surface_workflow_config)\n", + "resolve_bulk_by_id_subworkflow = surface_workflow.subworkflows[1]\n", + "resolve_bulk_by_build_subworkflow = surface_workflow.subworkflows[2]\n", + "get_bulk_energy_subworkflow = surface_workflow.subworkflows[3]\n", + "print(f\"Loaded workflow: {surface_workflow.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### 3.3. Check for the exact bulk material from slab metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract bulk lookup queries from the workflow definition so they stay in sync\n", + "# with what the workflow actually uses at runtime.\n", + "from mat3ra.notebooks_utils.ui import display_JSON\n", + "\n", + "SLAB = slab.to_dict()\n", + "bulk_id_template = resolve_bulk_by_id_subworkflow.get_unit_by_name(name=\"assign-bulk-id\").value\n", + "BULK_ID = eval(bulk_id_template, {\"__builtins__\": {}}, {\"SLAB\": SLAB})\n", + "\n", + "if BULK_ID is not None:\n", + " io_unit = resolve_bulk_by_id_subworkflow.get_unit_by_name(name=\"io-bulk\")\n", + " query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + " bulk_query = eval(query_template, {\"__builtins__\": {}}, {\"BULK_ID\": BULK_ID})\n", + "else:\n", + " assign_crystal_template = resolve_bulk_by_build_subworkflow.get_unit_by_name(name=\"assign-crystal\").value\n", + " LAST_BUILD_CRYSTAL = eval(assign_crystal_template, {\"__builtins__\": {}}, {\"SLAB\": SLAB})\n", + " bulk_query_template = resolve_bulk_by_build_subworkflow.get_unit_by_name(name=\"assign-bulk-query\").value\n", + " bulk_query = eval(bulk_query_template, {\"__builtins__\": {}}, {\"LAST_BUILD_CRYSTAL\": LAST_BUILD_CRYSTAL})\n", + "\n", + "if bulk_query is None:\n", + " raise ValueError(\n", + " \"Surface slab build metadata does not carry a crystal scaledHash or hash. \"\n", + " \"Upload the bulk material and run total_energy.ipynb first.\"\n", + " )\n", + "\n", + "projection_template = resolve_bulk_by_build_subworkflow.get_unit_by_name(name=\"io-bulk\").input[0][\n", + " \"endpoint_options\"\n", + "][\"params\"][\"projection\"]\n", + "projection = eval(projection_template, {\"__builtins__\": {}})\n", + "bulk_matches = client.materials.list(query=bulk_query, projection=projection)\n", + "if not bulk_matches:\n", + " raise ValueError(\n", + " \"The bulk material resolved from slab metadata is not on the platform. \"\n", + " \"Run total_energy.ipynb for that bulk material first, then rerun this notebook.\"\n", + " )\n", + "\n", + "bulk_material_response = bulk_matches[0]\n", + "bulk_material = Material.create(bulk_material_response)\n", + "print(f\"Found exact bulk material: {bulk_material_response['_id']}\")\n", "print(f\"Resolved bulk query: {bulk_query}\")\n", "display_JSON({\"bulk_query\": bulk_query}, level=2)" ] }, { "cell_type": "markdown", - "id": "18", + "id": "22", "metadata": {}, "source": [ - "### 3.3. Inspect the exact bulk material found on the platform\n" + "### 3.4. Inspect the exact bulk material found on the platform\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -280,16 +355,16 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "24", "metadata": {}, "source": [ - "### 3.4. Save the slab material for the workflow\n" + "### 3.5. Save the slab material for the workflow\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -303,7 +378,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "26", "metadata": {}, "source": [ "## 4. Check bulk total energy\n", @@ -313,32 +388,30 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "27", "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", - "\n", - "def get_refined_bulk_total_energy(api_client: APIClient, bulk_material_dict: dict):\n", - " query = {\n", - " \"exabyteId\": bulk_material_dict[\"exabyteId\"],\n", - " \"data.name\": \"total_energy\",\n", - " \"group\": {\"$regex\": \"qe:\"},\n", - " }\n", - " projection = {\"sort\": {\"precision.value\": -1}, \"limit\": 1}\n", - " results = api_client.properties.request(\n", - " \"GET\",\n", - " \"refined-properties\",\n", - " params={\n", - " \"query\": json.dumps(query),\n", - " \"projection\": json.dumps(projection),\n", - " },\n", - " )\n", - " return results[0] if results else None\n", - "\n", - "\n", - "bulk_total_energy_property = get_refined_bulk_total_energy(client, bulk_material_response)\n", + "# Extract the bulk TE query and projection from the workflow definition\n", + "# so they stay in sync with what the workflow actually uses at runtime.\n", + "io_unit = get_bulk_energy_subworkflow.get_unit_by_name(name=\"io-e-bulk\")\n", + "query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", + "projection_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "\n", + "BULK = bulk_material_response\n", + "query = eval(query_template, {\"__builtins__\": {}}, {\"BULK\": BULK})\n", + "projection = eval(projection_template, {\"__builtins__\": {}})\n", + "results = client.properties.request(\n", + " \"GET\",\n", + " \"refined-properties\",\n", + " params={\n", + " \"query\": json.dumps(query),\n", + " \"projection\": json.dumps(projection),\n", + " },\n", + ")\n", + "bulk_total_energy_property = results[0] if results else None\n", "if bulk_total_energy_property is None:\n", " raise RuntimeError(\n", " \"Bulk total energy does not exist for the resolved exact bulk material. \"\n", @@ -354,7 +427,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "28", "metadata": {}, "source": [ "## 5. Configure the Surface Energy workflow\n", @@ -364,7 +437,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -378,22 +451,20 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "30", "metadata": {}, "source": [ - "### 5.2. Load workflow from Standata and preview it\n" + "### 5.2. Configure workflow and preview it\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "31", "metadata": {}, "outputs": [], "source": [ - "from mat3ra.standata.workflows import WorkflowStandata\n", "from mat3ra.wode.context.providers import PointsGridDataProvider\n", - "from mat3ra.wode.workflows import Workflow\n", "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", "\n", @@ -412,10 +483,6 @@ " return workflow\n", "\n", "\n", - "surface_workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(\n", - " WORKFLOW_SEARCH_TERM\n", - ")\n", - "surface_workflow = Workflow.create(surface_workflow_config)\n", "surface_workflow.name = MY_WORKFLOW_NAME\n", "surface_workflow = apply_workflow_kgrids(surface_workflow, scf_kgrid=SCF_KGRID)\n", "\n", @@ -424,7 +491,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "32", "metadata": {}, "source": [ "### 5.3. Save workflow to collection\n" @@ -433,7 +500,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -444,7 +511,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "34", "metadata": {}, "source": [ "## 6. Create the compute configuration\n", @@ -454,7 +521,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -464,7 +531,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "36", "metadata": {}, "source": [ "### 6.2. Create compute configuration\n" @@ -473,7 +540,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -490,7 +557,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "38", "metadata": {}, "source": [ "## 7. Create the Surface Energy job\n", @@ -500,7 +567,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -532,7 +599,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "40", "metadata": {}, "source": [ "### 7.2. Submit the Surface Energy job and monitor the status\n" @@ -541,7 +608,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -552,7 +619,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -561,7 +628,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "43", "metadata": {}, "source": [ "## 8. Retrieve results\n", @@ -571,7 +638,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "44", "metadata": {}, "outputs": [], "source": [ From 7f6e6fdf28fc8b0d5e6b02f2e0c3eda7ed7ab0c4 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jul 2026 15:35:46 -0700 Subject: [PATCH 04/10] update: NBs celanup --- .../workflows/interfacial_energy.ipynb | 397 +++++++----------- .../workflows/surface_energy.ipynb | 230 +++------- 2 files changed, 213 insertions(+), 414 deletions(-) diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb index a3629f61e..e7fab8d12 100644 --- a/other/materials_designer/workflows/interfacial_energy.ipynb +++ b/other/materials_designer/workflows/interfacial_energy.ipynb @@ -7,32 +7,35 @@ "source": [ "# Interfacial Energy\n", "\n", - "Calculate the interfacial energy γ (eV/Ų) of a heterostructure using a DFT workflow on the Mat3ra platform.\n", + "Calculate the interfacial energy γ (eV/Ų) of a heterostructure using a multi-material DFT workflow on the Mat3ra platform.\n", "\n", - "This notebook loads an interface from `../uploads`, strips atom labels (required for Quantum ESPRESSO), resolves substrate and film bulk materials from `metadata.build` crystal hashes, and checks that both bulk materials and their `total_energy` properties already exist on the platform.\n", + "The workflow takes **three materials, in order**:\n", "\n", - "If substrate bulk, film bulk, or either bulk total energy is missing, the notebook stops and points to the [Total Energy](total_energy.ipynb) notebook. It does not create or run bulk pre-calculations automatically.\n", + "- **[0] Interface** — the heterostructure (e.g. Gr/Ni(111)).\n", + "- **[1] Substrate bulk** — the substrate reference crystal.\n", + "- **[2] Film bulk** — the film reference crystal.\n", "\n", - "The job uses a single interface material. The workflow resolves bulks and fetches their precomputed total energies automatically.\n", + "Only the interface is computed here (`pw_scf`). The two bulk reference energies are fetched from previously-finished **Total Energy** jobs, so each bulk must already be converged/relaxed and have such a job on the platform. The substrate/film multiplicities are derived automatically from the structures; `N_INTERFACES` (1 with vacuum, 2 for a periodic stack) is provided by this notebook.\n", + "\n", + "This notebook loads an interface from `../uploads`, resolves the substrate and film bulk crystals from the interface `metadata.build`, verifies each bulk has a total energy on the platform, and submits the three materials to the multi-material workflow.\n", "\n", "

Usage

\n", "\n", - "1. Build an interface (for example via `create_interface_with_min_strain_zsl.ipynb`) and export it to `../uploads`, or set `INTERFACE_NAME` to match an existing interface on the platform.\n", - "2. Run [Total Energy](total_energy.ipynb) for each resolved substrate and film bulk material.\n", + "1. Build an interface (for example via `create_interface_with_min_strain_zsl.ipynb`) and export it to `../uploads`, or set `INTERFACE_NAME` to match an interface on the platform.\n", + "2. Run [Total Energy](total_energy.ipynb) for each resolved substrate and film bulk crystal.\n", "3. Set parameters in cells 1.2 and 1.3 below.\n", "4. Click \"Run\" > \"Run All\".\n", - "5. Inspect resolved bulks, bulk total energies, and the final interfacial energy.\n", + "5. Inspect the resolved bulks, their total energies, and the final interfacial energy.\n", "\n", "## Summary\n", "\n", "1. Set up the environment and parameters.\n", "2. Authenticate and initialize API client.\n", - "3. Load interface, strip labels, resolve substrate/film bulks, and verify they exist.\n", - "4. Verify bulk total energies exist.\n", - "5. Configure and save the Interfacial Energy workflow.\n", - "6. Configure compute.\n", - "7. Create, submit, and monitor the job.\n", - "8. Retrieve results.\n" + "3. Load the interface, resolve substrate/film bulks with their total energies, and assemble the three job materials.\n", + "4. Configure the Interfacial Energy workflow.\n", + "5. Configure compute.\n", + "6. Create, submit, and monitor the multi-material job.\n", + "7. Retrieve results.\n" ] }, { @@ -112,7 +115,12 @@ "outputs": [], "source": [ "# K-grid for interface 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", + "# 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" ] }, { @@ -295,10 +303,8 @@ " WORKFLOW_SEARCH_TERM\n", ")\n", "interfacial_workflow = Workflow.create(interfacial_workflow_config)\n", - "resolve_refs_subworkflow = interfacial_workflow.subworkflows[0]\n", - "fetch_substrate_te_subworkflow = interfacial_workflow.subworkflows[1]\n", - "fetch_film_te_subworkflow = interfacial_workflow.subworkflows[2]\n", - "print(f\"Loaded workflow: {interfacial_workflow.name}\")" + "print(f\"Loaded workflow: {interfacial_workflow.name}\")\n", + "print(f\"Multi-material: {getattr(interfacial_workflow, 'isMultiMaterial', False)}\")" ] }, { @@ -306,7 +312,8 @@ "id": "22", "metadata": {}, "source": [ - "### 3.4. Resolve substrate bulk from interface build metadata" + "### 3.4. Resolve substrate/film crystals from interface build metadata\n", + "Uses `made`'s `get_interface_bulk_crystal` helper for typed access to the interface build configuration, instead of hand-parsing the raw `metadata.build` dict." ] }, { @@ -316,206 +323,132 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.core.entity.material.analysis import get_interface_bulk_crystal\n", - "from mat3ra.notebooks_utils.ui import display_JSON\n", + "from mat3ra.made.tools.analyze.interface_material import get_interface_bulk_crystal\n", + "from mat3ra.made.tools.build_components.metadata import MaterialWithBuildMetadata\n", "\n", - "CRYSTAL_SUBSTRATE = get_interface_bulk_crystal(interface_material, part=\"substrate\")\n", - "io_unit = resolve_refs_subworkflow.get_unit_by_name(name=\"io-bulk-substrate\")\n", - "query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "projection_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", + "interface_with_metadata = MaterialWithBuildMetadata.create(interface_material.to_dict())\n", "\n", - "substrate_query = eval(query_template, {\"__builtins__\": {}}, {\"CRYSTAL_SUBSTRATE\": CRYSTAL_SUBSTRATE})\n", - "projection = eval(projection_template, {\"__builtins__\": {}})\n", - "substrate_matches = client.materials.list(query=substrate_query, projection=projection)\n", - "if not substrate_matches:\n", - " raise ValueError(\n", - " \"The substrate bulk material resolved from interface metadata is not on the platform. \"\n", - " \"Run total_energy.ipynb for that bulk material first, then rerun this notebook.\"\n", - " )\n", + "# Bulk crystals as recorded when the interface was built.\n", + "CRYSTAL_SUBSTRATE = Material.create(get_interface_bulk_crystal(interface_with_metadata, part=\"substrate\"))\n", + "CRYSTAL_FILM = Material.create(get_interface_bulk_crystal(interface_with_metadata, part=\"film\"))\n", "\n", - "substrate_bulk_response = substrate_matches[0]\n", - "substrate_bulk = Material.create(substrate_bulk_response)\n", - "BULK_SUBSTRATE_MATERIAL_IDS = [material[\"_id\"] for material in substrate_matches]\n", - "print(f\"Found substrate bulk material: {substrate_bulk_response['_id']}\")\n", - "print(f\"Resolved substrate bulk query: {substrate_query}\")\n", - "display_JSON({\"substrate_query\": substrate_query}, level=2)\n", - "visualize(substrate_bulk)\n" - ] - }, - { - "cell_type": "markdown", - "id": "24", - "metadata": {}, - "source": [ - "### 3.5. Resolve film bulk from interface build metadata\n" + "print(f\"Substrate crystal: {CRYSTAL_SUBSTRATE.name}\")\n", + "print(f\"Film crystal: {CRYSTAL_FILM.name}\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "24", "metadata": {}, "outputs": [], "source": [ - "CRYSTAL_FILM = get_interface_bulk_crystal(interface_material, part=\"film\")\n", - "io_unit = resolve_refs_subworkflow.get_unit_by_name(name=\"io-bulk-film\")\n", - "query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "projection_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", - "\n", - "film_query = eval(query_template, {\"__builtins__\": {}}, {\"CRYSTAL_FILM\": CRYSTAL_FILM})\n", - "projection = eval(projection_template, {\"__builtins__\": {}})\n", - "film_matches = client.materials.list(query=film_query, projection=projection)\n", - "if not film_matches:\n", - " raise ValueError(\n", - " \"The film bulk material resolved from interface metadata is not on the platform. \"\n", - " \"Run total_energy.ipynb for that bulk material first, then rerun this notebook.\"\n", - " )\n", + "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n", + "\n", + "# Multiple platform materials can share the same crystal hash (e.g. uploaded\n", + "# independently by different accounts). Only some of them may have a finished\n", + "# Total Energy job. Prefer a hash-twin that already has one, so the bulk we\n", + "# attach to the job is one whose energy is actually available.\n", + "def resolve_bulk_material(crystal_hash):\n", + " matches = client.materials.list(query={\"hash\": crystal_hash}, projection={})\n", + " if not matches:\n", + " return None, None, None\n", + " for candidate in matches:\n", + " job, prop = find_total_energy_for_material(client, candidate[\"_id\"])\n", + " if prop is not None:\n", + " return candidate, job, prop\n", + " return matches[0], None, None\n", + "\n", + "\n", + "def find_bulk_with_total_energy(crystal, role, material_index):\n", + " \"\"\"Find the platform material for `crystal` and its precomputed total energy.\n", + "\n", + " Raises if the crystal isn't on the platform at all, or if none of its\n", + " hash-twins has a finished Total Energy job yet.\n", + " \"\"\"\n", + " match, _, te_property = resolve_bulk_material(crystal.hash)\n", + " if match is None:\n", + " raise ValueError(\n", + " f\"The {role} bulk material resolved from the interface metadata is not on the \"\n", + " \"platform. Run total_energy.ipynb for that bulk crystal first, then rerun this notebook.\"\n", + " )\n", + " if te_property is None:\n", + " raise RuntimeError(\n", + " f\"{role.capitalize()} bulk total energy not found. Run total_energy.ipynb for the {role} bulk first.\"\n", + " )\n", "\n", - "film_bulk_response = film_matches[0]\n", - "film_bulk = Material.create(film_bulk_response)\n", - "BULK_FILM_MATERIAL_IDS = [material[\"_id\"] for material in film_matches]\n", - "print(f\"Found film bulk material: {film_bulk_response['_id']}\")\n", - "print(f\"Resolved film bulk query: {film_query}\")\n", - "display_JSON({\"film_query\": film_query}, level=2)\n", - "visualize(film_bulk)\n" + " bulk = Material.create(match)\n", + " print(f\"✅ {role.capitalize()} bulk (material {material_index}): {bulk.name} ({bulk.id}), \"\n", + " f\"total energy: {te_property['data']['value']} eV\")\n", + " return bulk, te_property" ] }, { "cell_type": "markdown", - "id": "26", + "id": "25", "metadata": {}, "source": [ - "### 3.6. Save the interface material for the workflow\n" + "### 3.5. Find substrate and film bulk materials on the platform\n", + "Each bulk must already exist on the platform with a finished Total Energy job (see [Total Energy](total_energy.ipynb)); the workflow will fetch that energy rather than recomputing it." ] }, { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "26", "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "substrate_bulk, substrate_te_property = find_bulk_with_total_energy(CRYSTAL_SUBSTRATE, \"substrate\", 1)\n", + "film_bulk, film_te_property = find_bulk_with_total_energy(CRYSTAL_FILM, \"film\", 2)\n", "\n", - "saved_interface_response = get_or_create_material(client, interface_material, ACCOUNT_ID)\n", - "saved_interface = Material.create(saved_interface_response)\n", - "print(f\"✅ Saved interface material: {saved_interface_response['_id']}\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "28", - "metadata": {}, - "source": [ - "## 4. Check bulk total energies\n", - "### 4.1. Check substrate bulk total energy\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "29", - "metadata": {}, - "outputs": [], - "source": [ - "# Extract the substrate bulk TE queries and projections from the workflow definition\n", - "# so they stay in sync with what the workflow actually uses at runtime.\n", - "io_job_unit = fetch_substrate_te_subworkflow.get_unit_by_name(name=\"io-bulk-te-job-substrate\")\n", - "job_query_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "job_projection_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", - "\n", - "BULK_MATERIAL_IDS = BULK_SUBSTRATE_MATERIAL_IDS\n", - "job_query = eval(job_query_template, {\"__builtins__\": {}}, {\"BULK_MATERIAL_IDS\": BULK_MATERIAL_IDS})\n", - "job_projection = eval(job_projection_template, {\"__builtins__\": {}})\n", - "jobs = client.jobs.list(query=job_query, projection=job_projection)\n", - "if not jobs:\n", - " raise RuntimeError(\n", - " \"Substrate bulk total energy not found. Run total_energy.ipynb for the substrate bulk first.\"\n", - " )\n", - "\n", - "BULK_TE_JOB_ID = jobs[0][\"_id\"]\n", - "io_te_unit = fetch_substrate_te_subworkflow.get_unit_by_name(name=\"io-te-bulk-substrate\")\n", - "te_query_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "te_projection_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", - "\n", - "te_query = eval(te_query_template, {\"__builtins__\": {}}, {\"BULK_TE_JOB_ID\": BULK_TE_JOB_ID})\n", - "te_projection = eval(te_projection_template, {\"__builtins__\": {}})\n", - "props = client.properties.list(query=te_query, projection=te_projection)\n", - "substrate_te_property = props[0] if props else None\n", - "if substrate_te_property is None:\n", - " raise RuntimeError(\n", - " \"Substrate bulk total energy not found. Run total_energy.ipynb for the substrate bulk first.\"\n", - " )\n", - "\n", - "print(\n", - " f\"♻️ Found substrate bulk total energy: {substrate_te_property['_id']} -> \"\n", - " f\"{substrate_te_property['data']['value']}\"\n", - ")\n", - "display_JSON(substrate_te_property, level=2)\n" + "visualize([substrate_bulk, film_bulk], repetitions=[1, 1, 1])" ] }, { "cell_type": "markdown", - "id": "30", + "id": "27", "metadata": {}, "source": [ - "### 4.2. Check film bulk total energy\n" + "### 3.6. Save the interface and assemble the job materials\n", + "Save the (label-stripped) interface, then assemble the three materials the multi-material workflow expects, in order: `[0]` interface, `[1]` substrate bulk, `[2]` film bulk." ] }, { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "28", "metadata": {}, "outputs": [], "source": [ - "io_job_unit = fetch_film_te_subworkflow.get_unit_by_name(name=\"io-bulk-te-job-film\")\n", - "job_query_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "job_projection_template = io_job_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", - "\n", - "BULK_MATERIAL_IDS = BULK_FILM_MATERIAL_IDS\n", - "job_query = eval(job_query_template, {\"__builtins__\": {}}, {\"BULK_MATERIAL_IDS\": BULK_MATERIAL_IDS})\n", - "job_projection = eval(job_projection_template, {\"__builtins__\": {}})\n", - "jobs = client.jobs.list(query=job_query, projection=job_projection)\n", - "if not jobs:\n", - " raise RuntimeError(\n", - " \"Film bulk total energy not found. Run total_energy.ipynb for the film bulk first.\"\n", - " )\n", - "\n", - "BULK_TE_JOB_ID = jobs[0][\"_id\"]\n", - "io_te_unit = fetch_film_te_subworkflow.get_unit_by_name(name=\"io-te-bulk-film\")\n", - "te_query_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "te_projection_template = io_te_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", - "\n", - "te_query = eval(te_query_template, {\"__builtins__\": {}}, {\"BULK_TE_JOB_ID\": BULK_TE_JOB_ID})\n", - "te_projection = eval(te_projection_template, {\"__builtins__\": {}})\n", - "props = client.properties.list(query=te_query, projection=te_projection)\n", - "film_te_property = props[0] if props else None\n", - "if film_te_property is None:\n", - " raise RuntimeError(\n", - " \"Film bulk total energy not found. Run total_energy.ipynb for the film bulk first.\"\n", - " )\n", - "\n", - "print(\n", - " f\"♻️ Found film bulk total energy: {film_te_property['_id']} -> \"\n", - " f\"{film_te_property['data']['value']}\"\n", - ")\n", - "display_JSON(film_te_property, level=2)\n" + "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "\n", + "saved_interface_response = get_or_create_material(client, interface_material, ACCOUNT_ID)\n", + "saved_interface = Material.create(saved_interface_response)\n", + "\n", + "# Order matters: the workflow reads material 0 as the interface, 1 as the\n", + "# substrate bulk, and 2 as the film bulk.\n", + "materials = [saved_interface, substrate_bulk, film_bulk]\n", + "print(\"✅ Materials for the multi-material job (index → material):\")\n", + "for index, material in enumerate(materials):\n", + " print(f\" [{index}] {material.name} ({material.id})\")\n", + "\n", + "visualize(materials, repetitions=[1, 1, 1], rotation=\"-90x\")" ] }, { "cell_type": "markdown", - "id": "32", + "id": "29", "metadata": {}, "source": [ - "## 5. Configure the Interfacial Energy workflow\n", - "### 5.1. Select application\n" + "## 4. Configure the Interfacial Energy workflow\n", + "### 4.1. Select application" ] }, { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -529,77 +462,62 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "31", "metadata": {}, "source": [ - "### 5.2. Configure workflow and preview it\n" + "### 4.2. Configure workflow and preview it" ] }, { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "32", "metadata": {}, "outputs": [], "source": [ "from mat3ra.wode.context.providers import PointsGridDataProvider\n", - "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", "\n", "\n", - "def apply_workflow_kgrids(workflow: Workflow, scf_kgrid=None) -> Workflow:\n", - " if scf_kgrid is not None:\n", - " new_context_scf = PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data()\n", - " for subworkflow in workflow.subworkflows:\n", - " unit_names = [unit.name for unit in subworkflow.units]\n", - " if \"pw_scf\" not in unit_names:\n", - " continue\n", - " unit_to_modify_scf = subworkflow.get_unit_by_name(name=\"pw_scf\")\n", - " unit_to_modify_scf.add_context(new_context_scf)\n", - " subworkflow.set_unit(unit_to_modify_scf)\n", - " break\n", + "def apply_workflow_parameters(workflow: Workflow, scf_kgrid=None, n_interfaces=None) -> Workflow:\n", + " for subworkflow in workflow.subworkflows:\n", + " unit_names = [unit.name for unit in subworkflow.units]\n", + "\n", + " # K-grid for the interface SCF.\n", + " if scf_kgrid is not None and \"pw_scf\" in unit_names:\n", + " unit = subworkflow.get_unit_by_name(name=\"pw_scf\")\n", + " unit.add_context(PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data())\n", + " subworkflow.set_unit(unit)\n", + "\n", + " # Number of interfaces in the supercell (γ normalization), provided here.\n", + " if n_interfaces is not None and \"set-n-interfaces\" in unit_names:\n", + " unit = subworkflow.get_unit_by_name(name=\"set-n-interfaces\")\n", + " unit.value = str(n_interfaces)\n", + " subworkflow.set_unit(unit)\n", " return workflow\n", "\n", "\n", "interfacial_workflow.name = MY_WORKFLOW_NAME\n", - "interfacial_workflow = apply_workflow_kgrids(interfacial_workflow, scf_kgrid=SCF_KGRID)\n", + "interfacial_workflow = apply_workflow_parameters(\n", + " interfacial_workflow, scf_kgrid=SCF_KGRID, n_interfaces=N_INTERFACES\n", + ")\n", "\n", - "visualize_workflow(interfacial_workflow)\n" - ] - }, - { - "cell_type": "markdown", - "id": "36", - "metadata": {}, - "source": [ - "### 5.3. Save workflow to collection\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "37", - "metadata": {}, - "outputs": [], - "source": [ - "saved_interfacial_workflow_response = get_or_create_workflow(client, interfacial_workflow, ACCOUNT_ID)\n", - "saved_interfacial_workflow = Workflow.create(saved_interfacial_workflow_response)\n", - "print(f\"Interfacial Energy workflow ID: {saved_interfacial_workflow.id}\")\n" + "visualize_workflow(interfacial_workflow)" ] }, { "cell_type": "markdown", - "id": "38", + "id": "33", "metadata": {}, "source": [ - "## 6. Create the compute configuration\n", - "### 6.1. Select cluster\n" + "## 5. Create the compute configuration\n", + "### 5.1. Select cluster" ] }, { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -609,16 +527,16 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "35", "metadata": {}, "source": [ - "### 6.2. Create compute configuration\n" + "### 5.2. Create compute configuration" ] }, { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -635,34 +553,35 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "37", "metadata": {}, "source": [ - "## 7. Create the Interfacial Energy job\n", - "### 7.1. Create job with interface and workflow configuration\n" + "## 6. Create the Interfacial Energy job\n", + "### 6.1. Create the multi-material job (interface + substrate bulk + film bulk)" ] }, { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "38", "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.job import create_job, wait_for_jobs_to_finish_async\n", + "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", - "print(f\"Interface material: {saved_interface.id}\")\n", - "print(f\"Substrate bulk material: {substrate_bulk_response['_id']}\")\n", - "print(f\"Film bulk material: {film_bulk_response['_id']}\")\n", - "print(f\"Interfacial workflow: {saved_interfacial_workflow.id}\")\n", + "print(\"Materials (job material order):\")\n", + "for index, material in enumerate(materials):\n", + " print(f\" [{index}] {material.name} ({material.id})\")\n", "print(f\"Project: {project_id}\")\n", "\n", "interfacial_job_name = f\"{MY_WORKFLOW_NAME} {saved_interface.formula} {timestamp}\"\n", "interfacial_job_response = create_job(\n", " api_client=client,\n", - " materials=[saved_interface],\n", - " workflow=saved_interfacial_workflow,\n", + " materials=materials,\n", + " workflow=interfacial_workflow,\n", " project_id=project_id,\n", " owner_id=ACCOUNT_ID,\n", " prefix=interfacial_job_name,\n", @@ -672,21 +591,21 @@ "interfacial_job = dict_to_namespace_recursive(interfacial_job_response)\n", "interfacial_job_id = interfacial_job._id\n", "print(f\"✅ Interfacial Energy job created successfully: {interfacial_job_id}\")\n", - "display_JSON(interfacial_job_response)\n" + "display_JSON(interfacial_job_response)" ] }, { "cell_type": "markdown", - "id": "44", + "id": "39", "metadata": {}, "source": [ - "### 7.2. Submit the Interfacial Energy job and monitor the status\n" + "### 6.2. Submit the Interfacial Energy job and monitor the status" ] }, { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -697,7 +616,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -706,25 +625,27 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "42", "metadata": {}, "source": [ - "## 8. Retrieve results\n", - "### 8.1. Retrieve and visualize interfacial energy\n" + "## 7. Retrieve results\n", + "### 7.1. Retrieve and visualize interfacial energy" ] }, { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "43", "metadata": {}, "outputs": [], "source": [ "from mat3ra.prode import PropertyName\n", "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n", "\n", + "# NOTE: \"interfacial_energy\" is not yet registered in prode/esse's ScalarPropertyEnum,\n", + "# so it is passed as a raw string here (property_name is a plain str filter server-side).\n", "interfacial_energy_data = client.properties.get_for_job(\n", - " interfacial_job_id, property_name=PropertyName.scalar.interfacial_energy.value\n", + " interfacial_job_id, property_name=\"interfacial_energy\"\n", ")\n", "interface_te_data = client.properties.get_for_job(\n", " interfacial_job_id, property_name=PropertyName.scalar.total_energy.value\n", @@ -740,13 +661,11 @@ "else:\n", " print(\"No interface total energy property was returned for the job.\")\n", "\n", - "print(f\"Resolved substrate bulk query: {substrate_query}\")\n", - "print(f\"Substrate bulk material: {substrate_bulk_response['_id']}\")\n", - "print(f\"Substrate bulk total energy: {substrate_te_property['data']['value']}\")\n", - "print(f\"Resolved film bulk query: {film_query}\")\n", - "print(f\"Film bulk material: {film_bulk_response['_id']}\")\n", - "print(f\"Film bulk total energy: {film_te_property['data']['value']}\")\n", - "print(f\"Saved interface material: {saved_interface_response['_id']}\")\n" + "print(f\"Interface (material 0): {saved_interface.name} ({saved_interface.id})\")\n", + "print(f\"Substrate bulk (material 1): {substrate_bulk.name} ({substrate_bulk.id}), \"\n", + " f\"total energy: {substrate_te_property['data']['value']} eV\")\n", + "print(f\"Film bulk (material 2): {film_bulk.name} ({film_bulk.id}), \"\n", + " f\"total energy: {film_te_property['data']['value']} eV\")" ] } ], diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb index 2821513d0..7ce07d230 100644 --- a/other/materials_designer/workflows/surface_energy.ipynb +++ b/other/materials_designer/workflows/surface_energy.ipynb @@ -9,9 +9,9 @@ "\n", "Calculate the surface energy of a slab using a DFT workflow on the Mat3ra platform.\n", "\n", - "This notebook resolves the bulk material from slab metadata and checks that both the bulk material and its refined `total_energy` property already exist on the platform.\n", + "This notebook resolves the bulk material from slab metadata and checks that it already has a finished Total Energy job on the platform.\n", "\n", - "If either the bulk material or bulk total energy is missing, the notebook stops and points to the Total Energy notebook. It does not create or run a bulk pre-calculation automatically.\n", + "If either the bulk material or its total energy is missing, the notebook stops and points to the Total Energy notebook. It does not create or run a bulk pre-calculation automatically.\n", "\n", "

Usage

\n", "\n", @@ -19,18 +19,17 @@ "2. Set the slab and workflow parameters in cells 1.2 and 1.3 below.\n", "3. Click \"Run\" > \"Run All\".\n", "4. If the bulk material or bulk total energy is missing, run the Total Energy notebook for the exact bulk material first, then rerun this notebook.\n", - "5. Inspect the resolved bulk, the bulk total energy used by the workflow, and the final surface energy.\n", + "5. Inspect the resolved bulk, its total energy, and the final surface energy.\n", "\n", "## Summary\n", "\n", "1. Set up the environment and parameters.\n", "2. Authenticate and initialize API client.\n", - "3. Load slab, resolve the exact bulk candidate, and check that exact bulk exists on the platform.\n", - "4. Check that refined bulk total energy already exists.\n", - "5. Configure and save the Surface Energy workflow.\n", - "6. Configure compute.\n", - "7. Create, submit, and monitor the Surface Energy job.\n", - "8. Retrieve results.\n" + "3. Load the slab, resolve the exact bulk material with its total energy, and save the slab.\n", + "4. Configure the Surface Energy workflow.\n", + "5. Configure compute.\n", + "6. Create, submit, and monitor the Surface Energy job.\n", + "7. Retrieve results." ] }, { @@ -215,8 +214,8 @@ "id": "16", "metadata": {}, "source": [ - "## 3. Load slab and resolve the exact bulk candidate\n", - "### 3.1. Load slab from local file or platform\n" + "## 3. Load slab, resolve bulk material, and save\n", + "### 3.1. Load slab from local file or platform" ] }, { @@ -271,9 +270,6 @@ " WORKFLOW_SEARCH_TERM\n", ")\n", "surface_workflow = Workflow.create(surface_workflow_config)\n", - "resolve_bulk_by_id_subworkflow = surface_workflow.subworkflows[1]\n", - "resolve_bulk_by_build_subworkflow = surface_workflow.subworkflows[2]\n", - "get_bulk_energy_subworkflow = surface_workflow.subworkflows[3]\n", "print(f\"Loaded workflow: {surface_workflow.name}\")" ] }, @@ -282,7 +278,8 @@ "id": "20", "metadata": {}, "source": [ - "### 3.3. Check for the exact bulk material from slab metadata" + "### 3.3. Find the bulk material and its total energy on the platform\n", + "Uses `notebooks_utils`' `get_bulk_material` to resolve the exact bulk from the slab's build metadata, then checks it already has a finished Total Energy job — the workflow fetches that energy rather than recomputing it." ] }, { @@ -292,46 +289,20 @@ "metadata": {}, "outputs": [], "source": [ - "# Extract bulk lookup queries from the workflow definition so they stay in sync\n", - "# with what the workflow actually uses at runtime.\n", - "from mat3ra.notebooks_utils.ui import display_JSON\n", - "\n", - "SLAB = slab.to_dict()\n", - "bulk_id_template = resolve_bulk_by_id_subworkflow.get_unit_by_name(name=\"assign-bulk-id\").value\n", - "BULK_ID = eval(bulk_id_template, {\"__builtins__\": {}}, {\"SLAB\": SLAB})\n", + "from mat3ra.notebooks_utils.core.entity.material.analysis import get_bulk_material\n", + "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n", "\n", - "if BULK_ID is not None:\n", - " io_unit = resolve_bulk_by_id_subworkflow.get_unit_by_name(name=\"io-bulk\")\n", - " query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - " bulk_query = eval(query_template, {\"__builtins__\": {}}, {\"BULK_ID\": BULK_ID})\n", - "else:\n", - " assign_crystal_template = resolve_bulk_by_build_subworkflow.get_unit_by_name(name=\"assign-crystal\").value\n", - " LAST_BUILD_CRYSTAL = eval(assign_crystal_template, {\"__builtins__\": {}}, {\"SLAB\": SLAB})\n", - " bulk_query_template = resolve_bulk_by_build_subworkflow.get_unit_by_name(name=\"assign-bulk-query\").value\n", - " bulk_query = eval(bulk_query_template, {\"__builtins__\": {}}, {\"LAST_BUILD_CRYSTAL\": LAST_BUILD_CRYSTAL})\n", - "\n", - "if bulk_query is None:\n", - " raise ValueError(\n", - " \"Surface slab build metadata does not carry a crystal scaledHash or hash. \"\n", - " \"Upload the bulk material and run total_energy.ipynb first.\"\n", - " )\n", + "bulk_query, bulk_material_response, bulk_material = get_bulk_material(client, slab, ACCOUNT_ID)\n", "\n", - "projection_template = resolve_bulk_by_build_subworkflow.get_unit_by_name(name=\"io-bulk\").input[0][\n", - " \"endpoint_options\"\n", - "][\"params\"][\"projection\"]\n", - "projection = eval(projection_template, {\"__builtins__\": {}})\n", - "bulk_matches = client.materials.list(query=bulk_query, projection=projection)\n", - "if not bulk_matches:\n", - " raise ValueError(\n", - " \"The bulk material resolved from slab metadata is not on the platform. \"\n", - " \"Run total_energy.ipynb for that bulk material first, then rerun this notebook.\"\n", + "_, bulk_total_energy_property = find_total_energy_for_material(client, bulk_material.id)\n", + "if bulk_total_energy_property is None:\n", + " raise RuntimeError(\n", + " \"Bulk total energy not found for the resolved bulk material. Run total_energy.ipynb first.\"\n", " )\n", "\n", - "bulk_material_response = bulk_matches[0]\n", - "bulk_material = Material.create(bulk_material_response)\n", - "print(f\"Found exact bulk material: {bulk_material_response['_id']}\")\n", - "print(f\"Resolved bulk query: {bulk_query}\")\n", - "display_JSON({\"bulk_query\": bulk_query}, level=2)" + "print(f\"✅ Bulk material: {bulk_material.name} ({bulk_material.id}), \"\n", + " f\"total energy: {bulk_total_energy_property['data']['value']} eV\")\n", + "visualize(bulk_material)" ] }, { @@ -339,7 +310,7 @@ "id": "22", "metadata": {}, "source": [ - "### 3.4. Inspect the exact bulk material found on the platform\n" + "### 3.4. Save the slab material for the workflow" ] }, { @@ -348,25 +319,6 @@ "id": "23", "metadata": {}, "outputs": [], - "source": [ - "display_JSON(bulk_material_response, level=2)\n", - "visualize(bulk_material)\n" - ] - }, - { - "cell_type": "markdown", - "id": "24", - "metadata": {}, - "source": [ - "### 3.5. Save the slab material for the workflow\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25", - "metadata": {}, - "outputs": [], "source": [ "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", "\n", @@ -378,66 +330,17 @@ }, { "cell_type": "markdown", - "id": "26", - "metadata": {}, - "source": [ - "## 4. Check bulk total energy\n", - "### 4.1. Check refined bulk total energy for the exact bulk material\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27", - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "\n", - "# Extract the bulk TE query and projection from the workflow definition\n", - "# so they stay in sync with what the workflow actually uses at runtime.\n", - "io_unit = get_bulk_energy_subworkflow.get_unit_by_name(name=\"io-e-bulk\")\n", - "query_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"query\"]\n", - "projection_template = io_unit.input[0][\"endpoint_options\"][\"params\"][\"projection\"]\n", - "\n", - "BULK = bulk_material_response\n", - "query = eval(query_template, {\"__builtins__\": {}}, {\"BULK\": BULK})\n", - "projection = eval(projection_template, {\"__builtins__\": {}})\n", - "results = client.properties.request(\n", - " \"GET\",\n", - " \"refined-properties\",\n", - " params={\n", - " \"query\": json.dumps(query),\n", - " \"projection\": json.dumps(projection),\n", - " },\n", - ")\n", - "bulk_total_energy_property = results[0] if results else None\n", - "if bulk_total_energy_property is None:\n", - " raise RuntimeError(\n", - " \"Bulk total energy does not exist for the resolved exact bulk material. \"\n", - " \"Run total_energy.ipynb first.\"\n", - " )\n", - "\n", - "print(\n", - " f\"♻️ Found refined bulk total energy: {bulk_total_energy_property['_id']} -> \"\n", - " f\"{bulk_total_energy_property['data']['value']}\"\n", - ")\n", - "display_JSON(bulk_total_energy_property, level=2)\n" - ] - }, - { - "cell_type": "markdown", - "id": "28", + "id": "24", "metadata": {}, "source": [ - "## 5. Configure the Surface Energy workflow\n", - "### 5.1. Select application\n" + "## 4. Configure the Surface Energy workflow\n", + "### 4.1. Select application" ] }, { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -451,21 +354,20 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "26", "metadata": {}, "source": [ - "### 5.2. Configure workflow and preview it\n" + "### 4.2. Configure workflow and preview it" ] }, { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "27", "metadata": {}, "outputs": [], "source": [ "from mat3ra.wode.context.providers import PointsGridDataProvider\n", - "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", "\n", "\n", @@ -491,37 +393,17 @@ }, { "cell_type": "markdown", - "id": "32", - "metadata": {}, - "source": [ - "### 5.3. Save workflow to collection\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33", - "metadata": {}, - "outputs": [], - "source": [ - "saved_surface_workflow_response = get_or_create_workflow(client, surface_workflow, ACCOUNT_ID)\n", - "saved_surface_workflow = Workflow.create(saved_surface_workflow_response)\n", - "print(f\"Surface workflow ID: {saved_surface_workflow.id}\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "34", + "id": "28", "metadata": {}, "source": [ - "## 6. Create the compute configuration\n", - "### 6.1. Select cluster\n" + "## 5. Create the compute configuration\n", + "### 5.1. Select cluster" ] }, { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -531,16 +413,16 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "30", "metadata": {}, "source": [ - "### 6.2. Create compute configuration\n" + "### 5.2. Create compute configuration" ] }, { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -557,27 +439,27 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "32", "metadata": {}, "source": [ - "## 7. Create the Surface Energy job\n", - "### 7.1. Create job with slab and workflow configuration\n" + "## 6. Create the Surface Energy job\n", + "### 6.1. Create job with slab and workflow configuration" ] }, { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "33", "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.job import create_job, wait_for_jobs_to_finish_async\n", + "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.utils.namespace import dict_to_namespace_recursive\n", "from mat3ra.notebooks_utils.ui import display_JSON\n", "\n", "print(f\"Slab material: {saved_slab.id}\")\n", - "print(f\"Resolved exact bulk material: {bulk_material.id}\")\n", - "print(f\"Surface workflow: {saved_surface_workflow.id}\")\n", + "print(f\"Resolved bulk material: {bulk_material.id}\")\n", "print(f\"Project: {project_id}\")\n", "\n", "surface_job_name = f\"{MY_WORKFLOW_NAME} {saved_slab.formula} {timestamp}\"\n", @@ -599,16 +481,16 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "34", "metadata": {}, "source": [ - "### 7.2. Submit the Surface Energy job and monitor the status\n" + "### 6.2. Submit the Surface Energy job and monitor the status" ] }, { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -619,7 +501,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -628,17 +510,17 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "37", "metadata": {}, "source": [ - "## 8. Retrieve results\n", - "### 8.1. Retrieve and visualize Surface Energy results\n" + "## 7. Retrieve results\n", + "### 7.1. Retrieve and visualize Surface Energy results" ] }, { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -660,11 +542,9 @@ "else:\n", " print(\"No slab total energy property was returned for the job.\")\n", "\n", - "print(f\"Resolved bulk query from slab metadata: {bulk_query}\")\n", - "print(f\"Exact bulk material used by the workflow: {bulk_material_response['_id']}\")\n", - "print(f\"Exact bulk exabyteId: {bulk_material_response['exabyteId']}\")\n", - "print(f\"Refined bulk total energy used by the workflow: {bulk_total_energy_property['data']['value']}\")\n", - "print(f\"Saved slab material used by the workflow: {saved_slab_response['_id']}\")" + "print(f\"Slab: {saved_slab.name} ({saved_slab.id})\")\n", + "print(f\"Bulk: {bulk_material.name} ({bulk_material.id}), \"\n", + " f\"total energy: {bulk_total_energy_property['data']['value']} eV\")" ] } ], From 7e460b5a2b5a1a8493331a16c1f5b30e08b17904 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jul 2026 18:13:37 -0700 Subject: [PATCH 05/10] chore: remove useless --- .../workflows/interfacial_energy.ipynb | 20 +++++++++---------- .../workflows/surface_energy.ipynb | 10 +--------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb index e7fab8d12..9fdef3dfb 100644 --- a/other/materials_designer/workflows/interfacial_energy.ipynb +++ b/other/materials_designer/workflows/interfacial_energy.ipynb @@ -642,8 +642,6 @@ "from mat3ra.prode import PropertyName\n", "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n", "\n", - "# NOTE: \"interfacial_energy\" is not yet registered in prode/esse's ScalarPropertyEnum,\n", - "# so it is passed as a raw string here (property_name is a plain str filter server-side).\n", "interfacial_energy_data = client.properties.get_for_job(\n", " interfacial_job_id, property_name=\"interfacial_energy\"\n", ")\n", @@ -651,15 +649,7 @@ " interfacial_job_id, property_name=PropertyName.scalar.total_energy.value\n", ")\n", "\n", - "if interfacial_energy_data:\n", - " visualize_properties(interfacial_energy_data, title=\"Interfacial Energy\")\n", - "else:\n", - " print(\"No 'interfacial_energy' property was returned for the job.\")\n", - "\n", - "if interface_te_data:\n", - " visualize_properties(interface_te_data, title=\"Interface Total Energy\")\n", - "else:\n", - " print(\"No interface total energy property was returned for the job.\")\n", + "visualize_properties(interfacial_energy_data, title=\"Interfacial Energy\")\n", "\n", "print(f\"Interface (material 0): {saved_interface.name} ({saved_interface.id})\")\n", "print(f\"Substrate bulk (material 1): {substrate_bulk.name} ({substrate_bulk.id}), \"\n", @@ -667,6 +657,14 @@ "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": "44", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb index 7ce07d230..100505b7a 100644 --- a/other/materials_designer/workflows/surface_energy.ipynb +++ b/other/materials_designer/workflows/surface_energy.ipynb @@ -532,15 +532,7 @@ " surface_job_id, property_name=PropertyName.scalar.total_energy.value\n", ")\n", "\n", - "if surface_energy_data:\n", - " visualize_properties(surface_energy_data, title=\"Surface Energy\")\n", - "else:\n", - " print(\"No 'surface_energy' property was returned for the job.\")\n", - "\n", - "if slab_total_energy_data:\n", - " visualize_properties(slab_total_energy_data, title=\"Slab Total Energy\")\n", - "else:\n", - " print(\"No slab total energy property was returned for the job.\")\n", + "visualize_properties(surface_energy_data, title=\"Surface Energy\")\n", "\n", "print(f\"Slab: {saved_slab.name} ({saved_slab.id})\")\n", "print(f\"Bulk: {bulk_material.name} ({bulk_material.id}), \"\n", From 2504dc7ab66f437ec950b7bb7fdb37c408b16ae0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 21 Jul 2026 12:59:51 -0700 Subject: [PATCH 06/10] update: reuse helper --- .../workflows/interfacial_energy.ipynb | 130 ++++++------------ .../workflows/surface_energy.ipynb | 37 +---- src/py/mat3ra/notebooks_utils/workflow.py | 17 +++ 3 files changed, 63 insertions(+), 121 deletions(-) diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb index 9fdef3dfb..2dd0cecc4 100644 --- a/other/materials_designer/workflows/interfacial_energy.ipynb +++ b/other/materials_designer/workflows/interfacial_energy.ipynb @@ -336,56 +336,9 @@ "print(f\"Film crystal: {CRYSTAL_FILM.name}\")" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n", - "\n", - "# Multiple platform materials can share the same crystal hash (e.g. uploaded\n", - "# independently by different accounts). Only some of them may have a finished\n", - "# Total Energy job. Prefer a hash-twin that already has one, so the bulk we\n", - "# attach to the job is one whose energy is actually available.\n", - "def resolve_bulk_material(crystal_hash):\n", - " matches = client.materials.list(query={\"hash\": crystal_hash}, projection={})\n", - " if not matches:\n", - " return None, None, None\n", - " for candidate in matches:\n", - " job, prop = find_total_energy_for_material(client, candidate[\"_id\"])\n", - " if prop is not None:\n", - " return candidate, job, prop\n", - " return matches[0], None, None\n", - "\n", - "\n", - "def find_bulk_with_total_energy(crystal, role, material_index):\n", - " \"\"\"Find the platform material for `crystal` and its precomputed total energy.\n", - "\n", - " Raises if the crystal isn't on the platform at all, or if none of its\n", - " hash-twins has a finished Total Energy job yet.\n", - " \"\"\"\n", - " match, _, te_property = resolve_bulk_material(crystal.hash)\n", - " if match is None:\n", - " raise ValueError(\n", - " f\"The {role} bulk material resolved from the interface metadata is not on the \"\n", - " \"platform. Run total_energy.ipynb for that bulk crystal first, then rerun this notebook.\"\n", - " )\n", - " if te_property is None:\n", - " raise RuntimeError(\n", - " f\"{role.capitalize()} bulk total energy not found. Run total_energy.ipynb for the {role} bulk first.\"\n", - " )\n", - "\n", - " bulk = Material.create(match)\n", - " print(f\"✅ {role.capitalize()} bulk (material {material_index}): {bulk.name} ({bulk.id}), \"\n", - " f\"total energy: {te_property['data']['value']} eV\")\n", - " return bulk, te_property" - ] - }, { "cell_type": "markdown", - "id": "25", + "id": "24", "metadata": {}, "source": [ "### 3.5. Find substrate and film bulk materials on the platform\n", @@ -395,19 +348,25 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "25", "metadata": {}, "outputs": [], "source": [ - "substrate_bulk, substrate_te_property = find_bulk_with_total_energy(CRYSTAL_SUBSTRATE, \"substrate\", 1)\n", - "film_bulk, film_te_property = find_bulk_with_total_energy(CRYSTAL_FILM, \"film\", 2)\n", + "from mat3ra.notebooks_utils.core.entity.material.analysis import find_bulk_with_total_energy\n", + "\n", + "substrate_bulk, substrate_te_property = find_bulk_with_total_energy(\n", + " client, ACCOUNT_ID, bulk_crystal=CRYSTAL_SUBSTRATE, role=\"substrate\", material_index=1\n", + ")\n", + "film_bulk, film_te_property = find_bulk_with_total_energy(\n", + " client, ACCOUNT_ID, bulk_crystal=CRYSTAL_FILM, role=\"film\", material_index=2\n", + ")\n", "\n", "visualize([substrate_bulk, film_bulk], repetitions=[1, 1, 1])" ] }, { "cell_type": "markdown", - "id": "27", + "id": "26", "metadata": {}, "source": [ "### 3.6. Save the interface and assemble the job materials\n", @@ -417,7 +376,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -438,7 +397,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "28", "metadata": {}, "source": [ "## 4. Configure the Interfacial Energy workflow\n", @@ -448,7 +407,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -462,7 +421,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "30", "metadata": {}, "source": [ "### 4.2. Configure workflow and preview it" @@ -471,43 +430,32 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "31", "metadata": {}, "outputs": [], "source": [ - "from mat3ra.wode.context.providers import PointsGridDataProvider\n", + "from mat3ra.notebooks_utils.workflow import apply_scf_kgrid\n", "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", "\n", - "\n", - "def apply_workflow_parameters(workflow: Workflow, scf_kgrid=None, n_interfaces=None) -> Workflow:\n", - " for subworkflow in workflow.subworkflows:\n", - " unit_names = [unit.name for unit in subworkflow.units]\n", - "\n", - " # K-grid for the interface SCF.\n", - " if scf_kgrid is not None and \"pw_scf\" in unit_names:\n", - " unit = subworkflow.get_unit_by_name(name=\"pw_scf\")\n", - " unit.add_context(PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data())\n", - " subworkflow.set_unit(unit)\n", - "\n", - " # Number of interfaces in the supercell (γ normalization), provided here.\n", - " if n_interfaces is not None and \"set-n-interfaces\" in unit_names:\n", - " unit = subworkflow.get_unit_by_name(name=\"set-n-interfaces\")\n", - " unit.value = str(n_interfaces)\n", - " subworkflow.set_unit(unit)\n", - " return workflow\n", - "\n", + "if type(N_INTERFACES) is not int or N_INTERFACES not in (1, 2):\n", + " raise ValueError(\"N_INTERFACES must be either 1 (vacuum) or 2 (periodic stack)\")\n", "\n", "interfacial_workflow.name = MY_WORKFLOW_NAME\n", - "interfacial_workflow = apply_workflow_parameters(\n", - " interfacial_workflow, scf_kgrid=SCF_KGRID, n_interfaces=N_INTERFACES\n", - ")\n", + "interfacial_workflow = apply_scf_kgrid(interfacial_workflow, scf_kgrid=SCF_KGRID)\n", + "\n", + "for subworkflow in interfacial_workflow.subworkflows:\n", + " if \"set-n-interfaces\" not in [unit.name for unit in subworkflow.units]:\n", + " continue\n", + " unit = subworkflow.get_unit_by_name(name=\"set-n-interfaces\")\n", + " unit.value = str(N_INTERFACES)\n", + " subworkflow.set_unit(unit)\n", "\n", "visualize_workflow(interfacial_workflow)" ] }, { "cell_type": "markdown", - "id": "33", + "id": "32", "metadata": {}, "source": [ "## 5. Create the compute configuration\n", @@ -517,7 +465,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -527,7 +475,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "34", "metadata": {}, "source": [ "### 5.2. Create compute configuration" @@ -536,7 +484,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -553,7 +501,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "36", "metadata": {}, "source": [ "## 6. Create the Interfacial Energy job\n", @@ -563,7 +511,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -596,7 +544,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "38", "metadata": {}, "source": [ "### 6.2. Submit the Interfacial Energy job and monitor the status" @@ -605,7 +553,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -616,7 +564,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -625,7 +573,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "41", "metadata": {}, "source": [ "## 7. Retrieve results\n", @@ -635,7 +583,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -661,7 +609,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "43", "metadata": {}, "outputs": [], "source": [] diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb index 100505b7a..c7436385f 100644 --- a/other/materials_designer/workflows/surface_energy.ipynb +++ b/other/materials_designer/workflows/surface_energy.ipynb @@ -279,7 +279,7 @@ "metadata": {}, "source": [ "### 3.3. Find the bulk material and its total energy on the platform\n", - "Uses `notebooks_utils`' `get_bulk_material` to resolve the exact bulk from the slab's build metadata, then checks it already has a finished Total Energy job — the workflow fetches that energy rather than recomputing it." + "Uses `notebooks_utils`' `find_bulk_with_total_energy` to resolve the exact bulk from the slab's build metadata and require a finished Total Energy job — the workflow fetches that energy rather than recomputing it." ] }, { @@ -289,19 +289,11 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.core.entity.material.analysis import get_bulk_material\n", - "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n", + "from mat3ra.notebooks_utils.core.entity.material.analysis import find_bulk_with_total_energy\n", "\n", - "bulk_query, bulk_material_response, 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", - "if bulk_total_energy_property is None:\n", - " raise RuntimeError(\n", - " \"Bulk total energy not found for the resolved bulk material. Run total_energy.ipynb first.\"\n", - " )\n", - "\n", - "print(f\"✅ Bulk material: {bulk_material.name} ({bulk_material.id}), \"\n", - " f\"total energy: {bulk_total_energy_property['data']['value']} eV\")\n", + "bulk_material, bulk_total_energy_property = find_bulk_with_total_energy(\n", + " client, ACCOUNT_ID, slab_material=slab, role=\"bulk\"\n", + ")\n", "visualize(bulk_material)" ] }, @@ -367,26 +359,11 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.wode.context.providers import PointsGridDataProvider\n", + "from mat3ra.notebooks_utils.workflow import apply_scf_kgrid\n", "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", "\n", - "\n", - "def apply_workflow_kgrids(workflow: Workflow, scf_kgrid=None) -> Workflow:\n", - " if scf_kgrid is not None:\n", - " new_context_scf = PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data()\n", - " for subworkflow in workflow.subworkflows:\n", - " unit_names = [unit.name for unit in subworkflow.units]\n", - " if \"pw_scf\" not in unit_names:\n", - " continue\n", - " unit_to_modify_scf = subworkflow.get_unit_by_name(name=\"pw_scf\")\n", - " unit_to_modify_scf.add_context(new_context_scf)\n", - " subworkflow.set_unit(unit_to_modify_scf)\n", - " break\n", - " return workflow\n", - "\n", - "\n", "surface_workflow.name = MY_WORKFLOW_NAME\n", - "surface_workflow = apply_workflow_kgrids(surface_workflow, scf_kgrid=SCF_KGRID)\n", + "surface_workflow = apply_scf_kgrid(surface_workflow, scf_kgrid=SCF_KGRID, first_only=True)\n", "\n", "visualize_workflow(surface_workflow)" ] diff --git a/src/py/mat3ra/notebooks_utils/workflow.py b/src/py/mat3ra/notebooks_utils/workflow.py index 594332866..8ff0f62da 100644 --- a/src/py/mat3ra/notebooks_utils/workflow.py +++ b/src/py/mat3ra/notebooks_utils/workflow.py @@ -2,6 +2,7 @@ from typing import Dict, List, Optional from mat3ra.wode import Workflow +from mat3ra.wode.context.providers import PointsGridDataProvider FORTRAN_NUMBER_PATTERN = re.compile(r"^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[de][+-]?\d+)?$", re.IGNORECASE) @@ -59,3 +60,19 @@ def patch_workflow_qe_input( template.set_content(content) subworkflow.set_unit(unit) return workflow + + +def apply_scf_kgrid(workflow: Workflow, scf_kgrid=None, *, first_only: bool = False) -> Workflow: + """Attach an edited SCF k-grid context to pw_scf units.""" + if scf_kgrid is None: + return workflow + context = PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data() + for subworkflow in workflow.subworkflows: + if "pw_scf" not in [unit.name for unit in subworkflow.units]: + continue + unit = subworkflow.get_unit_by_name(name="pw_scf") + unit.add_context(context) + subworkflow.set_unit(unit) + if first_only: + break + return workflow From 7b1a68edb2c22e2465b66cdf7337460d85d40989 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 21 Jul 2026 13:00:17 -0700 Subject: [PATCH 07/10] update: test --- tests/py/unit/test_workflow_utils.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/py/unit/test_workflow_utils.py b/tests/py/unit/test_workflow_utils.py index 69aa2f5b1..ceaaebcb9 100644 --- a/tests/py/unit/test_workflow_utils.py +++ b/tests/py/unit/test_workflow_utils.py @@ -1,10 +1,12 @@ import pytest -from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input +from mat3ra.notebooks_utils.workflow import apply_scf_kgrid, patch_workflow_qe_input from mat3ra.standata.workflows import WorkflowStandata from mat3ra.wode.workflows import Workflow FIXED_CELL_RELAXATION = "fixed_cell_relaxation.json" RELAX_UNIT_NAMES = ["pw_relax"] +SURFACE_ENERGY_WORKFLOW = "surface_energy.json" +SCF_KGRID = [4, 4, 1] def _relax_workflow(): @@ -40,3 +42,19 @@ def test_patch_workflow_qe_input(parameters, present, absent, error): assert text in content for text in absent: assert text not in content + + +def _surface_workflow(): + config = WorkflowStandata.filter_by_application("espresso").get_by_name_first_match(SURFACE_ENERGY_WORKFLOW) + return Workflow.create(config) + + +def test_apply_scf_kgrid_updates_pw_scf_context(): + workflow = _surface_workflow() + apply_scf_kgrid(workflow, scf_kgrid=SCF_KGRID, first_only=True) + unit = next( + subworkflow.get_unit_by_name(name="pw_scf") + for subworkflow in workflow.subworkflows + if subworkflow.get_unit_by_name(name="pw_scf") + ) + assert any(item.get("name") == "kgrid" or "dimensions" in item for item in unit.context) From 45af412f8af7a2aa1e8cb19579bc8457890eb978 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 22 Jul 2026 18:14:57 -0700 Subject: [PATCH 08/10] update: simplify and reuse --- .../core/entity/material/analysis.py | 48 +++-------------- .../core/entity/material/api.py | 51 +++++++++++++++++++ .../core/entity/property/api.py | 31 +++++++++++ src/py/mat3ra/notebooks_utils/workflow.py | 18 +++++-- 4 files changed, 102 insertions(+), 46 deletions(-) diff --git a/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py b/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py index 68eee6b77..78424dd0c 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, Tuple, Union import ase.build import ase.constraints @@ -7,9 +7,6 @@ import pymatgen.io.ase import pymatgen.symmetry.analyzer from mat3ra.made.material import Material -from mat3ra.made.tools.analyze.interface_material import ( - get_interface_bulk_crystal as get_interface_bulk_crystal_from_build_metadata, -) from mat3ra.made.tools.analyze.slab import SlabMaterialAnalyzer from mat3ra.made.tools.build import MaterialWithBuildMetadata @@ -113,6 +110,7 @@ def get_slab_area(a_vector: np.ndarray, b_vector: np.ndarray) -> float: def get_slab_bulk_crystal(slab_material: Material) -> dict: + """Gets the bulk crystal a slab was built from, as recorded in its build metadata.""" slab_with_metadata = MaterialWithBuildMetadata.create(slab_material.to_dict()) crystal = SlabMaterialAnalyzer(material=slab_with_metadata).build_configuration.atomic_layers.crystal if crystal is None: @@ -120,46 +118,12 @@ def get_slab_bulk_crystal(slab_material: Material) -> dict: return crystal if isinstance(crystal, dict) else crystal.to_dict() -def get_interface_bulk_crystal(interface_material: Material, part: str = "substrate") -> dict: - interface_with_metadata = MaterialWithBuildMetadata.create(interface_material.to_dict()) - return get_interface_bulk_crystal_from_build_metadata(interface_with_metadata, part=part) - - -def get_bulk_material(api_client: Any, slab_material: Material, owner_id: str): - slab_dict = slab_material.to_dict() - metadata = slab_dict.get("metadata") or {} - - if metadata.get("bulkId") is not None: - bulk_query = {"_id": metadata["bulkId"]} - else: - bulk_query = resolve_bulk_query_from_crystal(get_slab_bulk_crystal(slab_material)) - - bulk_material_response = find_owned_material(api_client, bulk_query, owner_id) - - if bulk_material_response is None: - raise ValueError( - "The bulk material resolved from slab metadata is not present on the platform. " - "Run the Total Energy notebook for that bulk material first, then rerun this notebook." - ) - - print(f"Found exact bulk material: {bulk_material_response['_id']}") - return bulk_query, bulk_material_response, Material.create(bulk_material_response) - - def resolve_bulk_query_from_crystal(bulk_crystal: dict) -> dict: - if bulk_crystal.get("_id") is not None: - return {"_id": bulk_crystal["_id"]} - if bulk_crystal.get("scaledHash") is not None: - return {"scaledHash": bulk_crystal["scaledHash"]} - if bulk_crystal.get("hash") is not None: - return {"hash": bulk_crystal["hash"]} + """Builds a materials.list query that resolves a bulk crystal to a platform material.""" + for key in ("scaledHash", "hash", "_id"): + if bulk_crystal.get(key) is not None: + return {key: bulk_crystal[key]} try: return {"hash": Material.create(bulk_crystal).hash} except Exception as exc: raise ValueError("Could not resolve a bulk query from crystal metadata.") from exc - - -def find_owned_material(api_client: Any, bulk_query: dict, owner_id: str) -> Optional[Any]: - matches = api_client.materials.list(bulk_query) - owned = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None) - return owned or (matches[0] if matches else None) diff --git a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py index a82127b1b..3051b42ad 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py @@ -1,4 +1,7 @@ from mat3ra.api_client import APIClient +from mat3ra.made.material import Material + +from .analysis import get_slab_bulk_crystal, resolve_bulk_query_from_crystal def get_or_create_material(api_client: APIClient, material, owner_id: str) -> dict: @@ -21,3 +24,51 @@ def get_or_create_material(api_client: APIClient, material, owner_id: str) -> di created = api_client.materials.create(material.to_dict(), owner_id=owner_id) print(f"✅ Material created: {created['_id']}") return created + + +def get_bulk_material(api_client: APIClient, slab_material: Material, owner_id: str) -> Material: + """ + Resolves the platform bulk material a slab was built from, owned by the given account. + + Args: + api_client (APIClient): API client instance. + slab_material (Material): The slab whose bulk material should be resolved. + owner_id (str): Account ID the resolved bulk material must belong to. + + Returns: + Material: The resolved bulk material. + """ + metadata = slab_material.to_dict().get("metadata") or {} + bulk_query = ( + {"_id": metadata["bulkId"]} + if metadata.get("bulkId") is not None + else resolve_bulk_query_from_crystal(get_slab_bulk_crystal(slab_material)) + ) + return _require_material_for_owner(api_client, bulk_query, owner_id) + + +def get_bulk_material_by_crystal(api_client: APIClient, bulk_crystal: Material, owner_id: str) -> Material: + """ + Resolves the platform bulk material matching a given bulk crystal, owned by the given account. + + Args: + api_client (APIClient): API client instance. + bulk_crystal (Material): The bulk crystal to resolve on the platform. + owner_id (str): Account ID the resolved bulk material must belong to. + + Returns: + Material: The resolved bulk material. + """ + bulk_query = resolve_bulk_query_from_crystal(bulk_crystal.to_dict()) + return _require_material_for_owner(api_client, bulk_query, owner_id) + + +def _require_material_for_owner(api_client: APIClient, query: dict, owner_id: str) -> Material: + matches = api_client.materials.list(query) + material_response = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None) + if material_response is None: + raise ValueError( + "The bulk material resolved from metadata is not present on the platform for this account. " + "Run the Total Energy notebook for that bulk material first, then rerun this notebook." + ) + return Material.create(material_response) 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 090373fc4..e0189e0ac 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/property/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/property/api.py @@ -71,6 +71,37 @@ 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]: + """ + Find the best-precision total_energy property for a material. Mirrors the + platform's "Resolve Total Energies for Elemental Materials" subworkflow, + which queries properties directly by material and selects by precision -- + no job lookup involved. + + Properties are keyed by the material's `exabyteId`, not its platform `_id`, + so the material is fetched first to resolve that field. + + Args: + client (APIClient): API client instance. + material_id (str): Material _id to look up the total_energy property for. + + Returns: + The best-precision total_energy property, or None if none exists. + """ + material = client.materials.get(material_id) + exabyte_id = material.get("exabyteId") + if not exabyte_id: + return None + properties = client.properties.list( + query={ + "exabyteId": exabyte_id, + "slug": "total_energy", + }, + projection={"sort": {"precision.value": -1}, "limit": 1}, + ) + return properties[0] if properties else None + + def get_property_by_subworkflow_and_unit_indicies( endpoint: PropertiesEndpoints, property_name: str, job: dict, subworkflow_index: int, unit_index: int ) -> dict: diff --git a/src/py/mat3ra/notebooks_utils/workflow.py b/src/py/mat3ra/notebooks_utils/workflow.py index 8ff0f62da..3e324813a 100644 --- a/src/py/mat3ra/notebooks_utils/workflow.py +++ b/src/py/mat3ra/notebooks_utils/workflow.py @@ -62,15 +62,25 @@ def patch_workflow_qe_input( return workflow -def apply_scf_kgrid(workflow: Workflow, scf_kgrid=None, *, first_only: bool = False) -> Workflow: - """Attach an edited SCF k-grid context to pw_scf units.""" +def apply_scf_kgrid( + workflow: Workflow, scf_kgrid=None, *, unit_name: str = "pw_scf", first_only: bool = False +) -> Workflow: + """ + Attaches an edited SCF k-grid context to units named `unit_name`. + + Args: + workflow: Workflow with subworkflows. + scf_kgrid: K-grid dimensions, e.g. [4, 4, 1]. If None, the workflow is returned unchanged. + unit_name: Name of the unit to attach the k-grid context to. + first_only: If True, only patch the first matching subworkflow. + """ if scf_kgrid is None: return workflow context = PointsGridDataProvider(dimensions=scf_kgrid, isEdited=True).get_context_item_data() for subworkflow in workflow.subworkflows: - if "pw_scf" not in [unit.name for unit in subworkflow.units]: + if unit_name not in [unit.name for unit in subworkflow.units]: continue - unit = subworkflow.get_unit_by_name(name="pw_scf") + unit = subworkflow.get_unit_by_name(name=unit_name) unit.add_context(context) subworkflow.set_unit(unit) if first_only: From 5d099c5c3214ea478c0108657621db26438fe1d6 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 22 Jul 2026 18:15:35 -0700 Subject: [PATCH 09/10] update: adjust tests --- .../core/entity/test_material_analysis.py | 28 +++++++++++++++++++ tests/py/unit/test_workflow_utils.py | 3 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/py/unit/core/entity/test_material_analysis.py diff --git a/tests/py/unit/core/entity/test_material_analysis.py b/tests/py/unit/core/entity/test_material_analysis.py new file mode 100644 index 000000000..28e433114 --- /dev/null +++ b/tests/py/unit/core/entity/test_material_analysis.py @@ -0,0 +1,28 @@ +"""Unit tests for bulk-crystal query resolution.""" + +import pytest +from mat3ra.notebooks_utils.core.entity.material.analysis import resolve_bulk_query_from_crystal +from mat3ra.standata.materials import Materials + +SILICON = Materials.get_by_name_first_match("Silicon") + + +@pytest.mark.parametrize( + "extra_keys,expected", + [ + ( + {"scaledHash": "scaled-hash-value", "hash": "hash-value", "_id": "material-id"}, + {"scaledHash": "scaled-hash-value"}, + ), + ({"hash": "hash-value", "_id": "material-id"}, {"hash": "hash-value"}), + ({"_id": "material-id"}, {"_id": "material-id"}), + ], +) +def test_resolve_bulk_query_prefers_scaled_hash_then_hash_then_id(extra_keys, expected): + assert resolve_bulk_query_from_crystal({**SILICON, **extra_keys}) == expected + + +def test_resolve_bulk_query_computes_hash_when_none_present(): + query = resolve_bulk_query_from_crystal(SILICON) + assert set(query) == {"hash"} + assert query["hash"] diff --git a/tests/py/unit/test_workflow_utils.py b/tests/py/unit/test_workflow_utils.py index ceaaebcb9..ad4323be7 100644 --- a/tests/py/unit/test_workflow_utils.py +++ b/tests/py/unit/test_workflow_utils.py @@ -57,4 +57,5 @@ def test_apply_scf_kgrid_updates_pw_scf_context(): for subworkflow in workflow.subworkflows if subworkflow.get_unit_by_name(name="pw_scf") ) - assert any(item.get("name") == "kgrid" or "dimensions" in item for item in unit.context) + kgrid_item = next(item for item in unit.context if item.get("name") == "kgrid") + assert kgrid_item["data"]["dimensions"] == SCF_KGRID From 7fe685d7f5b3fc73df5f114e3b9e21125d96c705 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 22 Jul 2026 18:16:02 -0700 Subject: [PATCH 10/10] update: use new helpers --- .../workflows/interfacial_energy.ipynb | 25 +++++++++++++------ .../workflows/surface_energy.ipynb | 19 +++++++++----- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb index 2dd0cecc4..18fa5c611 100644 --- a/other/materials_designer/workflows/interfacial_energy.ipynb +++ b/other/materials_designer/workflows/interfacial_energy.ipynb @@ -352,14 +352,22 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.core.entity.material.analysis import find_bulk_with_total_energy\n", + "from mat3ra.notebooks_utils.core.entity.material.api import get_bulk_material_by_crystal\n", + "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", + "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", - "substrate_bulk, substrate_te_property = find_bulk_with_total_energy(\n", - " client, ACCOUNT_ID, bulk_crystal=CRYSTAL_SUBSTRATE, role=\"substrate\", material_index=1\n", - ")\n", - "film_bulk, film_te_property = find_bulk_with_total_energy(\n", - " client, ACCOUNT_ID, bulk_crystal=CRYSTAL_FILM, role=\"film\", material_index=2\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", + "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", + " f\"total energy: {film_te_property['data']['value']} eV\")\n", "\n", "visualize([substrate_bulk, film_bulk], repetitions=[1, 1, 1])" ] @@ -599,7 +607,8 @@ "\n", "visualize_properties(interfacial_energy_data, title=\"Interfacial Energy\")\n", "\n", - "print(f\"Interface (material 0): {saved_interface.name} ({saved_interface.id})\")\n", + "print(f\"Interface (material 0): {saved_interface.name} ({saved_interface.id}), \"\n", + " f\"total energy: {interface_te_data[0]['data']['value']} eV\")\n", "print(f\"Substrate bulk (material 1): {substrate_bulk.name} ({substrate_bulk.id}), \"\n", " f\"total energy: {substrate_te_property['data']['value']} eV\")\n", "print(f\"Film bulk (material 2): {film_bulk.name} ({film_bulk.id}), \"\n", diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb index c7436385f..cef5240bf 100644 --- a/other/materials_designer/workflows/surface_energy.ipynb +++ b/other/materials_designer/workflows/surface_energy.ipynb @@ -279,7 +279,7 @@ "metadata": {}, "source": [ "### 3.3. Find the bulk material and its total energy on the platform\n", - "Uses `notebooks_utils`' `find_bulk_with_total_energy` to resolve the exact bulk from the slab's build metadata and require a finished Total Energy job — the workflow fetches that energy rather than recomputing it." + "Resolves the exact bulk from the slab's build metadata and requires a finished Total Energy job — the workflow fetches that energy rather than recomputing it." ] }, { @@ -289,11 +289,17 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.notebooks_utils.core.entity.material.analysis import find_bulk_with_total_energy\n", + "from mat3ra.notebooks_utils.core.entity.material.api import get_bulk_material\n", + "from mat3ra.notebooks_utils.core.entity.property.api import find_total_energy_for_material\n", "\n", - "bulk_material, bulk_total_energy_property = find_bulk_with_total_energy(\n", - " client, ACCOUNT_ID, slab_material=slab, role=\"bulk\"\n", - ")\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", + "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", + "print(f\"✅ Bulk material: {bulk_material.name} ({bulk_material.id}), \"\n", + " f\"total energy: {bulk_total_energy_property['data']['value']} eV\")\n", "visualize(bulk_material)" ] }, @@ -511,7 +517,8 @@ "\n", "visualize_properties(surface_energy_data, title=\"Surface Energy\")\n", "\n", - "print(f\"Slab: {saved_slab.name} ({saved_slab.id})\")\n", + "print(f\"Slab: {saved_slab.name} ({saved_slab.id}), \"\n", + " f\"total energy: {slab_total_energy_data[0]['data']['value']} eV\")\n", "print(f\"Bulk: {bulk_material.name} ({bulk_material.id}), \"\n", " f\"total energy: {bulk_total_energy_property['data']['value']} eV\")" ]