diff --git a/other/materials_designer/workflows/Introduction.ipynb b/other/materials_designer/workflows/Introduction.ipynb index c5987b4d..7ec1564e 100644 --- a/other/materials_designer/workflows/Introduction.ipynb +++ b/other/materials_designer/workflows/Introduction.ipynb @@ -96,7 +96,7 @@ "#### [8.1.1. Valence band offset at an interface.](valence_band_offset.ipynb)\n", "\n", "### 8.2. Dielectric Tensor\n", - "#### 8.2.1. Dielectric tensor calculation. *(to be added)*\n", + "#### [8.2.1. Dielectric tensor calculation.](dielectric_tensor.ipynb)\n", "\n", "\n", "## 9. Custom\n", diff --git a/other/materials_designer/workflows/dielectric_tensor.ipynb b/other/materials_designer/workflows/dielectric_tensor.ipynb new file mode 100644 index 00000000..1355b1be --- /dev/null +++ b/other/materials_designer/workflows/dielectric_tensor.ipynb @@ -0,0 +1,656 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Dielectric Tensor (Optical Dielectric Function)\n", + "\n", + "Calculate the frequency-dependent optical dielectric function ε(ω) of a material using a DFT workflow on the Mat3ra platform (Quantum ESPRESSO: `pw.x` SCF/NSCF + `epsilon.x` independent-particle dielectric response).\n", + "\n", + "

Usage

\n", + "\n", + "1. Set material and calculation parameters in cells 1.2 and 1.3 below (or use the default values).\n", + "1. Click \"Run\" > \"Run All\" to run all cells.\n", + "1. Wait for the job to complete.\n", + "1. Scroll down to view the result.\n", + "\n", + "## Summary\n", + "\n", + "1. Set up the environment and parameters: install packages (JupyterLite only) and configure parameters for material, workflow, compute resources, and job.\n", + "1. Authenticate and initialize API client: authenticate via browser, initialize the client, then select account and project.\n", + "1. Create material: materials are read from the `../uploads` folder — place files there manually or run a material creation notebook first. If the material is not found by name, Standata is used as a fallback. The material is then saved to the platform.\n", + "1. Configure workflow: select application, load dielectric tensor workflow from Standata, optionally add relaxation, set model and computational parameters, and save the workflow.\n", + "1. Configure compute: get list of clusters and create compute configuration with selected cluster, queue, and number of processors.\n", + "1. Create the job with material and workflow configuration: assemble the job from material, workflow, project, and compute configuration.\n", + "1. Submit the job and monitor the status: submit the job and wait for completion.\n", + "1. Retrieve results: get and display the dielectric tensor (real and imaginary parts of ε(ω))." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Set up the environment and parameters\n", + "### 1.1. Install packages (JupyterLite)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.packages import install_packages\n", + "\n", + "await install_packages(\"made|api_examples\")" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Set parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "from mat3ra.ide.compute import QueueName\n", + "\n", + "# 2. Auth and organization parameters\n", + "ORGANIZATION_NAME = None\n", + "\n", + "# 3. Material parameters\n", + "FOLDER = \"../uploads\"\n", + "MATERIAL_NAME = \"Silicon\" # small primitive cell (FCC, mp-149); NC pseudopotential available\n", + "\n", + "# 4. Workflow parameters\n", + "WORKFLOW_SEARCH_TERM = \"dielectric_tensor.json\"\n", + "APPLICATION_NAME = \"espresso\"\n", + "MY_WORKFLOW_NAME = \"Dielectric Function\"\n", + "\n", + "# Model parameters\n", + "MODEL_SUBTYPE = \"gga\" # \"gga\" or \"lda\"\n", + "\n", + "# 5. Compute parameters\n", + "CLUSTER_NAME = None # specify full or partial name i.e. \"cluster-001\" to select\n", + "QUEUE_NAME = QueueName.D\n", + "PPN = 1\n", + "\n", + "# 6. Job parameters\n", + "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n", + "POLL_INTERVAL = 30 # seconds" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "### 1.3. Set specific dielectric tensor parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Method parameters\n", + "# NOTE: epsilon.x (used to compute the dielectric function) does NOT support\n", + "# ultrasoft or PAW pseudopotentials -- norm-conserving (\"nc\") is required.\n", + "PSEUDOPOTENTIAL_TYPE = \"nc\" # \"nc\" (norm-conserving) -- required; \"us\"/\"paw\" will fail\n", + "FUNCTIONAL = \"pbe\" # for gga: \"pbe\", \"pbesol\"; for lda: \"pz\"\n", + "\n", + "# Relax the structure before computing the dielectric response. Recommended for\n", + "# any material not already at its equilibrium geometry (e.g. Graphene / 2D).\n", + "ADD_RELAXATION = False\n", + "\n", + "# Electronic k-grids; if None, the workflow default is used\n", + "RELAXATION_KGRID = None # e.g. [8, 8, 8] (used only when ADD_RELAXATION)\n", + "SCF_KGRID = None # e.g. [8, 8, 8] (3D) or [12, 12, 1] (2D)\n", + "# The NSCF step recomputes the full (non-symmetry-reduced) k-mesh that epsilon.x\n", + "# integrates over; for a converged dielectric function it should be at least as\n", + "# dense as, and typically denser than, the SCF grid (e.g. [16, 16, 16]).\n", + "NSCF_KGRID = None # e.g. [16, 16, 16] (3D) or [24, 24, 1] (2D)\n", + "\n", + "# Energy cutoffs (raise for harder cases -- e.g. 60 / 480 for Graphene)\n", + "ECUTWFC = 40\n", + "ECUTRHO = 200\n", + "\n", + "# epsilon.x energy grid (all values in eV)\n", + "ENERGY_GRID_MIN = 0.0 # wmin\n", + "ENERGY_GRID_MAX = 15.0 # wmax\n", + "ENERGY_GRID_POINTS = 500 # nw\n", + "INTERSMEAR = 0.2 # Lorentzian broadening between bands (eV)\n", + "INTRASMEAR = 0.0 # Drude-like intraband broadening (eV); set > 0 for metals\n", + "ENERGY_GRID_SHIFT = 0.0 # rigid shift of the energy grid (eV)" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## 2. Authenticate and initialize API client\n", + "### 2.1. Authenticate\n", + "Authenticate in the browser and have credentials stored in environment variable \"OIDC_ACCESS_TOKEN\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "await authenticate()" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "### 2.2. Initialize API client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.api_client import APIClient\n", + "\n", + "client = APIClient.authenticate()\n", + "client" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### 2.3. Select account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "client.list_accounts()" + ] + }, + { + "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}\")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "### 2.4. Select project" + ] + }, + { + "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})\")" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## 3. Create material\n", + "### 3.1. Load material from local file (or Standata)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.standata.materials import Materials\n", + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", + "from mat3ra.notebooks_utils.material import load_material_from_folder\n", + "\n", + "material = load_material_from_folder(FOLDER, MATERIAL_NAME) or Material.create(\n", + " Materials.get_by_name_first_match(MATERIAL_NAME))\n", + "\n", + "visualize(material)" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "### 3.2. Save material to the platform" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "\n", + "material.basis.set_labels_from_list([])\n", + "saved_material_response = get_or_create_material(client, material, ACCOUNT_ID)\n", + "saved_material = Material.create(saved_material_response)" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## 4. Configure workflow\n", + "### 4.1. Select application" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.applications import ApplicationStandata\n", + "from mat3ra.ade.application import Application\n", + "\n", + "app_config = ApplicationStandata.get_by_name_first_match(APPLICATION_NAME)\n", + "app = Application(**app_config)\n", + "print(f\"Using application: {app.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "### 4.2. Load workflow from Standata and preview it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.workflows import WorkflowStandata\n", + "from mat3ra.wode.workflows import Workflow\n", + "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", + "\n", + "workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(WORKFLOW_SEARCH_TERM)\n", + "workflow = Workflow.create(workflow_config)\n", + "workflow.name = MY_WORKFLOW_NAME\n", + "\n", + "visualize_workflow(workflow)" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "### 4.3. Add relaxation (optional)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "if ADD_RELAXATION:\n", + " workflow.add_relaxation()\n", + " visualize_workflow(workflow)" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "### 4.4. Set Model and its parameters (physics)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.model_tree import ModelTreeStandata\n", + "from mat3ra.mode import ModelFactory\n", + "\n", + "model_config = ModelTreeStandata.get_model_by_parameters(\n", + " type=\"dft\", subtype=MODEL_SUBTYPE, functional=FUNCTIONAL\n", + ")\n", + "model_config[\"method\"] = {\"type\": \"pseudopotential\", \"subtype\": PSEUDOPOTENTIAL_TYPE}\n", + "model = ModelFactory.create(model_config)\n", + "\n", + "for subworkflow in workflow.subworkflows:\n", + " subworkflow.model = model\n", + "\n", + "visualize_workflow(workflow)" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "### 4.5. Modify Method (computational parameters): k-grids, cutoffs, and energy grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.wode.context.providers import PlanewaveCutoffsContextProvider, PointsGridDataProvider\n", + "from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input\n", + "\n", + "dielectric_subworkflow = workflow.subworkflows[1 if ADD_RELAXATION else 0]\n", + "\n", + "# Relaxation electronic k-grid\n", + "if RELAXATION_KGRID is not None and ADD_RELAXATION:\n", + " unit = workflow.subworkflows[0].get_unit_by_name(name_regex=\"relax\")\n", + " if unit:\n", + " unit.add_context(PointsGridDataProvider(dimensions=RELAXATION_KGRID, isEdited=True).get_context_item_data())\n", + " workflow.subworkflows[0].set_unit(unit)\n", + "\n", + "# SCF electronic k-grid (pw.x)\n", + "if SCF_KGRID is not None:\n", + " unit = dielectric_subworkflow.get_unit_by_name(name=\"pw_scf\")\n", + " if unit:\n", + " unit.add_context(PointsGridDataProvider(dimensions=SCF_KGRID, isEdited=True).get_context_item_data())\n", + " dielectric_subworkflow.set_unit(unit)\n", + "\n", + "# NSCF electronic k-grid (pw.x) -- the grid epsilon.x integrates over\n", + "if NSCF_KGRID is not None:\n", + " unit = dielectric_subworkflow.get_unit_by_name(name=\"pw_nscf\")\n", + " if unit:\n", + " unit.add_context(PointsGridDataProvider(dimensions=NSCF_KGRID, isEdited=True).get_context_item_data())\n", + " dielectric_subworkflow.set_unit(unit)\n", + "\n", + "# Energy cutoffs on every pw.x unit\n", + "if ECUTWFC is not None:\n", + " cutoffs_context = PlanewaveCutoffsContextProvider(\n", + " wavefunction=ECUTWFC, density=ECUTRHO, isEdited=True\n", + " ).get_context_item_data()\n", + " for unit_name in [\"pw_relax\", \"pw_vc-relax\", \"pw_scf\", \"pw_nscf\"]:\n", + " for swf in workflow.subworkflows:\n", + " unit = swf.get_unit_by_name(name=unit_name)\n", + " if unit:\n", + " unit.add_context(cutoffs_context)\n", + " swf.set_unit(unit)\n", + "\n", + "# epsilon.x energy grid -- this unit has no context provider, so it is patched directly\n", + "patch_workflow_qe_input(\n", + " workflow,\n", + " {\n", + " \"energy_grid\": {\n", + " \"wmin\": ENERGY_GRID_MIN,\n", + " \"wmax\": ENERGY_GRID_MAX,\n", + " \"nw\": ENERGY_GRID_POINTS,\n", + " \"intersmear\": INTERSMEAR,\n", + " \"intrasmear\": INTRASMEAR,\n", + " \"shift\": ENERGY_GRID_SHIFT,\n", + " }\n", + " },\n", + " unit_names=[\"Compute dielectric function\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "### 4.6. Preview final workflow" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "visualize_workflow(workflow)" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "### 4.7. Save workflow to collection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", + "\n", + "saved_workflow_response = get_or_create_workflow(client, workflow, ACCOUNT_ID)\n", + "saved_workflow = Workflow.create(saved_workflow_response)\n", + "print(f\"Workflow ID: {saved_workflow.id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## 5. Create the compute configuration\n", + "### 5.1. Get list of clusters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "clusters = client.clusters.list()\n", + "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### 5.2. Create compute configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.ide.compute import Compute\n", + "\n", + "if CLUSTER_NAME:\n", + " cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), None)\n", + "else:\n", + " cluster = clusters[0]\n", + "\n", + "compute = Compute(cluster=cluster, queue=QUEUE_NAME, ppn=PPN)\n", + "print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}\")" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "## 6. Create the job\n", + "### 6.1. Create job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "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", + "job_name = f\"{MY_WORKFLOW_NAME} {saved_material.formula} {timestamp}\"\n", + "job_response = create_job(\n", + " api_client=client,\n", + " materials=[saved_material],\n", + " workflow=workflow,\n", + " project_id=project_id,\n", + " owner_id=ACCOUNT_ID,\n", + " prefix=job_name,\n", + " compute=compute.to_dict(),\n", + ")\n", + "\n", + "job = dict_to_namespace_recursive(job_response)\n", + "job_id = job._id\n", + "print(f\"✅ Job created: {job_id}\")\n", + "display_JSON(job_response)" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "## 7. Submit the job and monitor the status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "client.jobs.submit(job_id)\n", + "print(f\"✅ Job {job_id} submitted successfully!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.api.job import wait_for_jobs_to_finish_async\n", + "\n", + "await wait_for_jobs_to_finish_async(client.jobs, [job_id], poll_interval=POLL_INTERVAL)" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "## 8. Retrieve and visualize results\n", + "### 8.1. Dielectric Tensor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.prode import PropertyName\n", + "from mat3ra.notebooks_utils.core.entity.property.api import get_properties_for_job\n", + "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n", + "\n", + "dielectric_tensor_data = get_properties_for_job(\n", + " client, job_id, property_name=PropertyName.non_scalar.dielectric_tensor.value\n", + ")\n", + "visualize_properties(dielectric_tensor_data, title=\"Dielectric Tensor\", extra_config={\"material\": material.to_dict()})\n", + "\n", + "# Sanity check: confirm real, non-trivial values were computed (not an empty/failed result)\n", + "_real_entry = next(v for v in dielectric_tensor_data[0][\"values\"] if v[\"part\"] == \"real\")\n", + "_eps_static = _real_entry[\"components\"][0][0]\n", + "print(f\"Dielectric tensor: eps1(0) = {_eps_static:.2f}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}