From 1bc9f55ef0f4ef65fdbd242b9890525179764afe Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Apr 2026 12:59:22 -0700 Subject: [PATCH 1/5] update: option for polar --- .../workflows/valence_band_offset.ipynb | 125 ++++++++++++------ 1 file changed, 87 insertions(+), 38 deletions(-) diff --git a/other/materials_designer/workflows/valence_band_offset.ipynb b/other/materials_designer/workflows/valence_band_offset.ipynb index 09858eeb..c2b63fa0 100644 --- a/other/materials_designer/workflows/valence_band_offset.ipynb +++ b/other/materials_designer/workflows/valence_band_offset.ipynb @@ -82,11 +82,13 @@ "\n", "# 3. Material parameters\n", "FOLDER = \"../uploads\"\n", - "INTERFACE_NAME = \"Interface\" # To search for in \"uploads\" folder\n", + "INTERFACE_NAME = \"CTa\" # To search for in \"uploads\" folder\n", "LEFT_SIDE_PART = InterfacePartsEnum.SUBSTRATE\n", "RIGHT_SIDE_PART = InterfacePartsEnum.FILM\n", "INTERFACE_SYSTEM_NAME = None # Used as tag to group the materials. Defaults to shorthand from the loaded interface name\n", "\n", + "IS_POLAR = True # Whether the interface is polar, to adjust the VBO calculation method accordingly.\n", + "\n", "# 4. Workflow parameters\n", "APPLICATION_NAME = \"espresso\"\n", "WORKFLOW_SEARCH_TERM = \"valence_band_offset.json\"\n", @@ -151,6 +153,21 @@ "id": "8", "metadata": {}, "outputs": [], + "source": [ + "import os\n", + "\n", + "os.environ[\"API_PORT\"] = \"3000\"\n", + "os.environ[\"API_SECURE\"] = \"false\"\n", + "os.environ[\"API_HOST\"] = \"localhost\"\n", + "os.environ[\"OIDC_ACCESS_TOKEN\"] = \"XF1mzKWpm9Wqc9sDzXRK0f7fJT44pkYg9sGAiVbs83b\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], "source": [ "from utils.auth import authenticate\n", "\n", @@ -159,7 +176,7 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "10", "metadata": {}, "source": [ "### 2.2. Initialize API client\n" @@ -168,7 +185,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -180,7 +197,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "12", "metadata": {}, "source": [ "### 2.3. Select account\n" @@ -189,7 +206,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -199,7 +216,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -214,7 +231,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### 2.4. Select project\n" @@ -223,7 +240,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -234,7 +251,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## 3. Create materials\n", @@ -244,7 +261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -259,7 +276,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "19", "metadata": {}, "source": [ "### 3.2. Create materials from interface parts\n", @@ -269,7 +286,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -299,7 +316,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "21", "metadata": {}, "source": [ "### 3.3. Save materials\n" @@ -308,7 +325,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -327,7 +344,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "23", "metadata": {}, "source": [ "## 4. Configure workflow\n", @@ -337,7 +354,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -351,7 +368,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "### 4.2. Load workflow from Standata and preview it\n" @@ -360,16 +377,19 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ "from mat3ra.standata.workflows import WorkflowStandata\n", "from mat3ra.wode.workflows import Workflow\n", + "from utils.polar_vbo import add_polar_vbo_postprocess\n", "from utils.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", + "if IS_POLAR:\n", + " workflow = add_polar_vbo_postprocess(workflow)\n", "workflow.name = MY_WORKFLOW_NAME\n", "\n", "visualize_workflow(workflow)\n" @@ -377,7 +397,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "27", "metadata": {}, "source": [ "### 4.3. Set model and its parameters (physics)\n" @@ -386,7 +406,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -406,7 +426,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "29", "metadata": {}, "source": [ "### 4.4. Modify method (computational parameters): k-grid, k-path, cutoffs, diagonalization, and mixing\n" @@ -415,7 +435,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -465,7 +485,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "31", "metadata": {}, "source": [ "### 4.5. Preview final workflow\n" @@ -474,7 +494,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -483,7 +503,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "33", "metadata": {}, "source": [ "## 5. Create the compute configuration\n", @@ -493,7 +513,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -503,7 +523,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "35", "metadata": {}, "source": [ "### 5.2. Create compute configuration\n" @@ -512,7 +532,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -533,7 +553,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "37", "metadata": {}, "source": [ "## 6. Create the job\n", @@ -543,7 +563,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -579,7 +599,7 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "39", "metadata": {}, "source": [ "## 7. Submit the job and monitor the status\n" @@ -588,7 +608,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -599,7 +619,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -610,7 +630,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "42", "metadata": {}, "source": [ "## 8. Retrieve and visualize results\n", @@ -620,7 +640,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "43", "metadata": {}, "outputs": [], "source": [ @@ -637,13 +657,15 @@ "print(f\"Valence Band Offset (VBO) value: {vbo_value['value']:.3f} eV\")\n", "\n", "avg_esp_unit_ids = {}\n", + "polar_file_unit_id = None\n", "for subworkflow in workflow[\"subworkflows\"]:\n", " subworkflow_name = subworkflow[\"name\"]\n", " for unit in subworkflow[\"units\"]:\n", " result_names = [result[\"name\"] for result in unit.get(\"results\", [])]\n", " if \"average_potential_profile\" in result_names:\n", " avg_esp_unit_ids[subworkflow_name] = unit[\"flowchartId\"]\n", - " break\n", + " if IS_POLAR and \"file_content\" in result_names:\n", + " polar_file_unit_id = unit[\"flowchartId\"]\n", "\n", "ordered_names = [\n", " \"BS + Avg ESP (Interface)\",\n", @@ -658,13 +680,40 @@ " property_name=\"average_potential_profile\",\n", " unit_id=unit_id,\n", " )[0]\n", - " visualize_properties(avg_esp_data, title=subworkflow_name)\n" + " visualize_properties(avg_esp_data, title=subworkflow_name)\n", + "\n", + "if IS_POLAR and polar_file_unit_id is not None:\n", + " polar_file_results = client.properties.get_for_job(\n", + " job_id,\n", + " property_name=\"file_content\",\n", + " unit_id=polar_file_unit_id,\n", + " )\n", + " if polar_file_results:\n", + " polar_file_data = dict(polar_file_results[0])\n", + " polar_file_name = polar_file_data.get(\"objectData\", {}).get(\"NAME\")\n", + " polar_file_basename = polar_file_data.get(\"basename\")\n", + " job_files = client.jobs.list_files(job_id)\n", + " matching_job_file = next(\n", + " (\n", + " file\n", + " for file in job_files\n", + " if file.get(\"key\") == polar_file_name\n", + " or file.get(\"name\") == polar_file_basename\n", + " or file.get(\"key\", \"\").endswith(f\"/{polar_file_basename}\")\n", + " or file.get(\"key\", \"\") == polar_file_basename\n", + " ),\n", + " None,\n", + " )\n", + " if matching_job_file:\n", + " polar_file_data[\"signedUrl\"] = matching_job_file.get(\"signedUrl\") or matching_job_file.get(\"signedURL\")\n", + " visualize_properties(polar_file_data, title=\"Polar VBO Fit\")\n", + "\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": {}, "outputs": [], "source": [] From 741c4ea68b358fefd244c1498616a33152606c3a Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Apr 2026 14:08:25 -0700 Subject: [PATCH 2/5] update: display file content --- .../workflows/valence_band_offset.ipynb | 106 +++++++----------- utils/api.py | 34 ++++++ 2 files changed, 72 insertions(+), 68 deletions(-) diff --git a/other/materials_designer/workflows/valence_band_offset.ipynb b/other/materials_designer/workflows/valence_band_offset.ipynb index c2b63fa0..f9177e56 100644 --- a/other/materials_designer/workflows/valence_band_offset.ipynb +++ b/other/materials_designer/workflows/valence_band_offset.ipynb @@ -87,7 +87,7 @@ "RIGHT_SIDE_PART = InterfacePartsEnum.FILM\n", "INTERFACE_SYSTEM_NAME = None # Used as tag to group the materials. Defaults to shorthand from the loaded interface name\n", "\n", - "IS_POLAR = True # Whether the interface is polar, to adjust the VBO calculation method accordingly.\n", + "IS_POLAR = False # Whether the interface is polar, to adjust the VBO calculation method accordingly.\n", "\n", "# 4. Workflow parameters\n", "APPLICATION_NAME = \"espresso\"\n", @@ -153,21 +153,6 @@ "id": "8", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"API_PORT\"] = \"3000\"\n", - "os.environ[\"API_SECURE\"] = \"false\"\n", - "os.environ[\"API_HOST\"] = \"localhost\"\n", - "os.environ[\"OIDC_ACCESS_TOKEN\"] = \"XF1mzKWpm9Wqc9sDzXRK0f7fJT44pkYg9sGAiVbs83b\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9", - "metadata": {}, - "outputs": [], "source": [ "from utils.auth import authenticate\n", "\n", @@ -176,7 +161,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "9", "metadata": {}, "source": [ "### 2.2. Initialize API client\n" @@ -185,7 +170,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -197,7 +182,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "11", "metadata": {}, "source": [ "### 2.3. Select account\n" @@ -206,7 +191,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -216,7 +201,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -231,7 +216,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "14", "metadata": {}, "source": [ "### 2.4. Select project\n" @@ -240,7 +225,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -251,7 +236,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "16", "metadata": {}, "source": [ "## 3. Create materials\n", @@ -261,7 +246,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -276,7 +261,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "18", "metadata": {}, "source": [ "### 3.2. Create materials from interface parts\n", @@ -286,7 +271,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -316,7 +301,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "20", "metadata": {}, "source": [ "### 3.3. Save materials\n" @@ -325,7 +310,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -344,7 +329,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "22", "metadata": {}, "source": [ "## 4. Configure workflow\n", @@ -354,7 +339,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -368,7 +353,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "24", "metadata": {}, "source": [ "### 4.2. Load workflow from Standata and preview it\n" @@ -377,7 +362,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -397,7 +382,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "26", "metadata": {}, "source": [ "### 4.3. Set model and its parameters (physics)\n" @@ -406,7 +391,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -426,7 +411,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "28", "metadata": {}, "source": [ "### 4.4. Modify method (computational parameters): k-grid, k-path, cutoffs, diagonalization, and mixing\n" @@ -435,7 +420,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -485,7 +470,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "30", "metadata": {}, "source": [ "### 4.5. Preview final workflow\n" @@ -494,7 +479,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -503,7 +488,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "32", "metadata": {}, "source": [ "## 5. Create the compute configuration\n", @@ -513,7 +498,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -523,7 +508,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "34", "metadata": {}, "source": [ "### 5.2. Create compute configuration\n" @@ -532,7 +517,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -553,7 +538,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "36", "metadata": {}, "source": [ "## 6. Create the job\n", @@ -563,7 +548,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -599,7 +584,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "38", "metadata": {}, "source": [ "## 7. Submit the job and monitor the status\n" @@ -608,7 +593,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -619,7 +604,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -630,7 +615,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "41", "metadata": {}, "source": [ "## 8. Retrieve and visualize results\n", @@ -640,11 +625,12 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "42", "metadata": {}, "outputs": [], "source": [ "from mat3ra.prode import PropertyName\n", + "from utils.api import attach_signed_url_to_file_property\n", "from utils.visualize import visualize_properties\n", "\n", "job_data = client.jobs.get(job_id)\n", @@ -689,23 +675,7 @@ " unit_id=polar_file_unit_id,\n", " )\n", " if polar_file_results:\n", - " polar_file_data = dict(polar_file_results[0])\n", - " polar_file_name = polar_file_data.get(\"objectData\", {}).get(\"NAME\")\n", - " polar_file_basename = polar_file_data.get(\"basename\")\n", - " job_files = client.jobs.list_files(job_id)\n", - " matching_job_file = next(\n", - " (\n", - " file\n", - " for file in job_files\n", - " if file.get(\"key\") == polar_file_name\n", - " or file.get(\"name\") == polar_file_basename\n", - " or file.get(\"key\", \"\").endswith(f\"/{polar_file_basename}\")\n", - " or file.get(\"key\", \"\") == polar_file_basename\n", - " ),\n", - " None,\n", - " )\n", - " if matching_job_file:\n", - " polar_file_data[\"signedUrl\"] = matching_job_file.get(\"signedUrl\") or matching_job_file.get(\"signedURL\")\n", + " polar_file_data = attach_signed_url_to_file_property(client, job_id, polar_file_results[0])\n", " visualize_properties(polar_file_data, title=\"Polar VBO Fit\")\n", "\n" ] @@ -713,7 +683,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "43", "metadata": {}, "outputs": [], "source": [] diff --git a/utils/api.py b/utils/api.py index b2959beb..75a96cff 100644 --- a/utils/api.py +++ b/utils/api.py @@ -200,6 +200,40 @@ def get_properties_for_job(client: APIClient, job_id: str, property_name: Option return [{**prop, "fermiEnergy": fermi_energy} for prop in properties] +def attach_signed_url_to_file_property(client: APIClient, job_id: str, file_property: dict) -> dict: + """ + Enrich a file_content property with a signed URL from the job file listing. + + Args: + client (APIClient): API client instance. + job_id (str): Job ID. + file_property (dict): Property record, typically a file_content result. + + Returns: + dict: Property dict with signedUrl added when a matching job file is found. + """ + property_with_url = dict(file_property) + object_name = property_with_url.get("objectData", {}).get("NAME") + basename = property_with_url.get("basename") + job_files = client.jobs.list_files(job_id) + + matching_job_file = next( + ( + file + for file in job_files + if file.get("key") == object_name + or file.get("name") == basename + or file.get("key", "").endswith(f"/{basename}") + or file.get("key", "") == basename + ), + None, + ) + if matching_job_file: + property_with_url["signedUrl"] = matching_job_file.get("signedUrl") or matching_job_file.get("signedURL") + + return property_with_url + + def create_job( api_client: APIClient, materials: List[Union[dict, Material]], From eb24c6d0127dcfc35ec055e1ed55baa1507e85b0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Apr 2026 14:14:44 -0700 Subject: [PATCH 3/5] update: description about polar --- .../workflows/valence_band_offset.ipynb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/other/materials_designer/workflows/valence_band_offset.ipynb b/other/materials_designer/workflows/valence_band_offset.ipynb index f9177e56..03dac64e 100644 --- a/other/materials_designer/workflows/valence_band_offset.ipynb +++ b/other/materials_designer/workflows/valence_band_offset.ipynb @@ -9,10 +9,17 @@ "\n", "Calculate the valence band offset for a labeled interface using a DFT workflow on the Mat3ra platform.\n", "\n", + "The notebook supports two modes controlled by `IS_POLAR` in cell 1.2:\n", + "\n", + "- `IS_POLAR = False`: use the standard VBO workflow based on extrema extracted from the macroscopically averaged electrostatic potential.\n", + "- `IS_POLAR = True`: append a final Python post-process that fits the electrostatic potential in the left and right slab regions, then uses those fitted averages for the polar-interface VBO evaluation.\n", + "\n", + "When the polar option is enabled, the final results section shows both the scalar VBO value and an additional plot, `Polar VBO Fit`, highlighting the fitted slab regions and linear fits used in the polar correction.\n", + "\n", "

Usage

\n", "\n", "1. Create and save an interface with labels (for example via `create_interface_with_min_strain_zsl.ipynb`).\n", - "1. Set the interface and calculation parameters in cells 1.2 and 1.3 below.\n", + "1. Set the interface and calculation parameters in cells 1.2 and 1.3 below, including `IS_POLAR` if the interface is polar.\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 VBO result.\n", @@ -26,7 +33,7 @@ "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 materials and workflow configuration: assemble the job from materials, 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 valence band offset.\n" + "1. Retrieve results: get and display the valence band offset, average ESP profiles, and the polar fit plot when `IS_POLAR = True`.\n" ] }, { @@ -82,7 +89,7 @@ "\n", "# 3. Material parameters\n", "FOLDER = \"../uploads\"\n", - "INTERFACE_NAME = \"CTa\" # To search for in \"uploads\" folder\n", + "INTERFACE_NAME = \"Interface\" # To search for in \"uploads\" folder\n", "LEFT_SIDE_PART = InterfacePartsEnum.SUBSTRATE\n", "RIGHT_SIDE_PART = InterfacePartsEnum.FILM\n", "INTERFACE_SYSTEM_NAME = None # Used as tag to group the materials. Defaults to shorthand from the loaded interface name\n", From 05f8789ad464db7122e4b14b2a529d65f445e945 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Apr 2026 14:52:15 -0700 Subject: [PATCH 4/5] update: polar VBO utils --- utils/polar_vbo.py | 261 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 utils/polar_vbo.py diff --git a/utils/polar_vbo.py b/utils/polar_vbo.py new file mode 100644 index 00000000..f6d43940 --- /dev/null +++ b/utils/polar_vbo.py @@ -0,0 +1,261 @@ +from copy import deepcopy + +from mat3ra.wode.workflows import Workflow + +POLAR_POSTPROCESS_SUBWORKFLOW_ID = "df9ddd647d8540ef6fd947fa" +POLAR_POSTPROCESS_FLOWCHART_ID = "87b5f54edde75c917c0d9389" +CALCULATE_VBO_SUBWORKFLOW_NAME = "Calculate VBO" +POLAR_POSTPROCESS_SUBWORKFLOW_NAME = "Python VBO Polar PostProcess" + +POLAR_VBO_SCRIPT = """# ------------------------------------------------------------------ # +# Linear Fit of ESP for Polar Interface VBO Calculation # +# ------------------------------------------------------------------ # +# +# Reference: Choudhary & Garrity, arXiv:2401.02021 (InterMat) # +# +# For polar interfaces, ESP shows linear gradient in bulk regions +# due to internal electric field. We fit each slab region and use +# the average value of the fit as the ESP reference. +# +# VBO Calculation: +# 1. Fit interface profile over slab 1 region -> Va_interface +# 2. Fit interface profile over slab 2 region -> Vb_interface +# 3. Fit bulk left profile over slab 1 region -> Va_bulk_left +# 4. Fit bulk right profile over slab 2 region -> Vb_bulk_right +# 5. VBO = (delta V_interface) - (delta V_bulk) +# where delta V_interface = Vb_interface - Va_interface +# delta V_bulk = Vb_bulk_right - Va_bulk_left +# +# Input: +# - profile_left, profile_right: ESP profiles for bulk materials +# - profile_interface: ESP profile for interface structure +# +# Output: VBO (Valence Band Offset) +# ------------------------------------------------------------------ # +import json +from types import SimpleNamespace + +import ase.io +import matplotlib +import numpy as np +from mat3ra.made.material import Material +from mat3ra.made.tools.convert import from_ase +from scipy.stats import linregress + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +pw_scf_output = "./pw_scf.out" +pw_scf_output_1 = "./pw_scf.out-1" +pw_scf_output_2 = "./pw_scf.out-2" + +atoms = ase.io.read(pw_scf_output, format="espresso-out") +atoms_1 = ase.io.read(pw_scf_output_1, format="espresso-out") +atoms_2 = ase.io.read(pw_scf_output_2, format="espresso-out") + +material = Material.create(from_ase(atoms)) +material_1 = Material.create(from_ase(atoms_1)) +material_2 = Material.create(from_ase(atoms_2)) + +material.to_cartesian() +material_1.to_cartesian() +material_2.to_cartesian() + +coords = material.basis.coordinates.values +elements = material.basis.elements.values +z_elements = sorted(zip([c[2] for c in coords], elements)) +n_left = len(material_1.basis.elements.values) + +z_max_1 = z_elements[n_left - 1][0] +z_min_2 = z_elements[n_left][0] +z_min_1 = z_elements[0][0] +z_max_2 = z_elements[-1][0] + +print(f"Detected Slab 1 (left) boundaries: z = {z_min_1:.3f} to {z_max_1:.3f} A") +print(f"Detected Slab 2 (right) boundaries: z = {z_min_2:.3f} to {z_max_2:.3f} A") + +checkpoint_file = "./.mat3ra/checkpoint" +with open(checkpoint_file, "r") as f: + checkpoint_data = json.load(f) + profile_interface = SimpleNamespace( + **checkpoint_data["scope"]["local"]["average-electrostatic-potential"]["average_potential_profile"] + ) + profile_left = SimpleNamespace( + **checkpoint_data["scope"]["local"]["average-electrostatic-potential-left"]["average_potential_profile"] + ) + profile_right = SimpleNamespace( + **checkpoint_data["scope"]["local"]["average-electrostatic-potential-right"]["average_potential_profile"] + ) + +X = np.array(profile_interface.xDataArray) +Y = np.array(profile_interface.yDataSeries[1]) +X_left = np.array(profile_left.xDataArray) +Y_left = np.array(profile_left.yDataSeries[1]) +X_right = np.array(profile_right.xDataArray) +Y_right = np.array(profile_right.yDataSeries[1]) + + +def get_region_indices(x_data, x_min, x_max): + mask = (x_data >= x_min) & (x_data <= x_max) + indices = np.where(mask)[0] + if len(indices) == 0: + return 0, len(x_data) + return indices[0], indices[-1] + 1 + + +def fit_and_average(x_data, y_data, start_idx, end_idx): + x_region = x_data[start_idx:end_idx] + y_region = y_data[start_idx:end_idx] + + if len(x_region) < 2: + avg = float(np.mean(y_region)) if len(y_region) > 0 else 0.0 + return avg, 0.0, avg + + slope, intercept, _, _, _ = linregress(x_region, y_region) + x_mid = (x_region[0] + x_region[-1]) / 2.0 + avg_value = slope * x_mid + intercept + return float(avg_value), float(slope), float(intercept) + + +slab1_start, slab1_end = get_region_indices(X, z_min_1, z_max_1) +slab2_start, slab2_end = get_region_indices(X, z_min_2, z_max_2) + +Va_interface, slope_a, intercept_a = fit_and_average(X, Y, slab1_start, slab1_end) +Vb_interface, slope_b, intercept_b = fit_and_average(X, Y, slab2_start, slab2_end) + +slab1_start_left, slab1_end_left = get_region_indices(X_left, z_min_1, z_max_1) +slab2_start_right, slab2_end_right = get_region_indices(X_right, z_min_2, z_max_2) + +Va_bulk_left, _, _ = fit_and_average(X_left, Y_left, slab1_start_left, slab1_end_left) +Vb_bulk_right, _, _ = fit_and_average(X_right, Y_right, slab2_start_right, slab2_end_right) + +VBO = (Vb_interface - Va_interface) - (Vb_bulk_right - Va_bulk_left) + +print(f"Interface ESP Slab 1 (Va_interface): {Va_interface:.3f} eV") +print(f"Interface ESP Slab 2 (Vb_interface): {Vb_interface:.3f} eV") +print(f"Bulk ESP Left (Va_bulk): {Va_bulk_left:.3f} eV") +print(f"Bulk ESP Right (Vb_bulk): {Vb_bulk_right:.3f} eV") +print(f"Interface delta V: {Vb_interface - Va_interface:.3f} eV") +print(f"Bulk delta V: {Vb_bulk_right - Va_bulk_left:.3f} eV") +print(f"Valence Band Offset (VBO): {VBO:.3f} eV") + +plt.figure(figsize=(10, 6)) +plt.plot(X, Y, label="Macroscopic Average Potential", linewidth=2) +plt.axvspan(z_min_1, z_max_1, color="red", alpha=0.2, label="Slab 1 Region") +plt.axvspan(z_min_2, z_max_2, color="blue", alpha=0.2, label="Slab 2 Region") + +if slab1_end > slab1_start: + x_fit1 = X[slab1_start:slab1_end] + y_fit1 = slope_a * x_fit1 + intercept_a + plt.plot(x_fit1, y_fit1, color="darkred", linestyle="--", linewidth=2, label="Fit Slab 1") + +if slab2_end > slab2_start: + x_fit2 = X[slab2_start:slab2_end] + y_fit2 = slope_b * x_fit2 + intercept_b + plt.plot(x_fit2, y_fit2, color="darkblue", linestyle="--", linewidth=2, label="Fit Slab 2") + +plt.axhline(Va_interface, color="red", linestyle=":", linewidth=2, label=f"Avg ESP Slab 1 = {Va_interface:.3f} eV") +plt.axhline(Vb_interface, color="blue", linestyle=":", linewidth=2, label=f"Avg ESP Slab 2 = {Vb_interface:.3f} eV") + +plt.xlabel("z-coordinate (A)", fontsize=12) +plt.ylabel("Macroscopic Average Potential (eV)", fontsize=12) +plt.title(f"Polar Interface VBO = {VBO:.3f} eV", fontsize=14, fontweight="bold") +plt.legend(loc="best", fontsize=10) +plt.grid(True, alpha=0.3) +plt.tight_layout() +plt.savefig("polar_vbo_fit_interface.png", dpi=150, bbox_inches="tight") +plt.close() +""" + +POLAR_VBO_REQUIREMENTS = """munch==2.5.0 +numpy>=1.19.5 +scipy>=1.5.4 +matplotlib>=3.0.0 +ase>=3.22.1 +mat3ra-made[tools]>=2024.11.12.post0 +""" + + +def _polar_postprocess_subworkflow(source_python_subworkflow): + source_unit = next(unit for unit in source_python_subworkflow["units"] if unit["type"] == "execution") + source_flavor_inputs = source_unit.get("flavor", {}).get("input", []) + script_input_name = source_flavor_inputs[0].get("name", "script.py") if source_flavor_inputs else "script.py" + script_template_name = ( + source_flavor_inputs[0].get("templateName", script_input_name) if source_flavor_inputs else script_input_name + ) + requirements_input_name = ( + source_flavor_inputs[1].get("name", "requirements.txt") if len(source_flavor_inputs) > 1 else "requirements.txt" + ) + requirements_template_name = ( + source_flavor_inputs[1].get("templateName", requirements_input_name) + if len(source_flavor_inputs) > 1 + else requirements_input_name + ) + + polar_unit = deepcopy(source_unit) + polar_unit["flowchartId"] = "20ea6fb714ad8529fae09fd3" + polar_unit["name"] = "Get VBO for Polar material" + polar_unit["head"] = True + polar_unit.pop("next", None) + polar_unit["context"] = {"subworkflowContext": {}} + polar_unit["results"] = [{"name": "file_content", "filetype": "image", "basename": "polar_vbo_fit_interface.png"}] + polar_unit["input"] = [ + { + "content": POLAR_VBO_SCRIPT, + "_id": "cKsDNiq3STXgGX9hP", + "applicationName": "python", + "executableName": "python", + "name": script_input_name, + "templateName": script_template_name, + "contextProviders": [], + "rendered": POLAR_VBO_SCRIPT, + "schemaVersion": "2022.8.16", + }, + { + "content": POLAR_VBO_REQUIREMENTS, + "_id": "yTM9FqBpxmFf2iigk", + "applicationName": "python", + "executableName": "python", + "name": requirements_input_name, + "templateName": requirements_template_name, + "contextProviders": [], + "rendered": POLAR_VBO_REQUIREMENTS, + "schemaVersion": "2022.8.16", + }, + ] + + polar_subworkflow = deepcopy(source_python_subworkflow) + polar_subworkflow["_id"] = POLAR_POSTPROCESS_SUBWORKFLOW_ID + polar_subworkflow["name"] = POLAR_POSTPROCESS_SUBWORKFLOW_NAME + polar_subworkflow["properties"] = [] + polar_subworkflow["units"] = [polar_unit] + return polar_subworkflow + + +def add_polar_vbo_postprocess(workflow: Workflow) -> Workflow: + workflow_dict = deepcopy(workflow.to_dict()) + + if any(unit["name"] == POLAR_POSTPROCESS_SUBWORKFLOW_NAME for unit in workflow_dict["units"]): + return workflow + + calculate_vbo_unit = next(unit for unit in workflow_dict["units"] if unit["name"] == CALCULATE_VBO_SUBWORKFLOW_NAME) + calculate_vbo_unit["next"] = POLAR_POSTPROCESS_FLOWCHART_ID + + source_python_subworkflow = next( + subworkflow for subworkflow in workflow_dict["subworkflows"] if subworkflow["application"]["name"] == "python" + ) + + workflow_dict["units"].append( + { + "name": POLAR_POSTPROCESS_SUBWORKFLOW_NAME, + "type": "subworkflow", + "_id": POLAR_POSTPROCESS_SUBWORKFLOW_ID, + "flowchartId": POLAR_POSTPROCESS_FLOWCHART_ID, + "status": "idle", + "statusTrack": [], + "tags": [], + "head": False, + } + ) + workflow_dict["subworkflows"].append(_polar_postprocess_subworkflow(source_python_subworkflow)) + return Workflow.create(workflow_dict) From 78a00f8d1dcbba0d0bf2e1fd8c09b35aa80640e1 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Apr 2026 14:52:51 -0700 Subject: [PATCH 5/5] update: display content --- utils/visualize.py | 48 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/utils/visualize.py b/utils/visualize.py index 5d791e94..0867dfde 100644 --- a/utils/visualize.py +++ b/utils/visualize.py @@ -390,6 +390,45 @@ def visualize_workflow(workflow, level: int = 2) -> None: display_JSON(workflow_config, level=level) +def _build_file_content_extra_config(results): + files = [] + for result in results: + if result.get("name") != "file_content": + continue + signed_url = result.get("signedUrl") or result.get("signedURL") or result.get("url") + basename = result.get("basename") + key = result.get("objectData", {}).get("NAME") or basename + if signed_url and key: + files.append({"basename": basename, "key": key, "signedUrl": signed_url}) + + if not files: + return None + + files_json = json.dumps(files) + return f"""{{ + getFileContent: function(data, onSuccess) {{ + const files = {files_json}; + const file = files.find((item) => + (data && data.objectData && data.objectData.NAME && item.key === data.objectData.NAME) || + (data && data.basename && item.basename === data.basename) + ); + if (!file) {{ + console.warn("No signed URL found for file_content result", data); + return; + }} + const fileMetadata = {{ key: file.key, signedUrl: file.signedUrl }}; + if (data && data.filetype === "image") {{ + onSuccess("", fileMetadata); + return; + }} + fetch(file.signedUrl) + .then((response) => response.text()) + .then((content) => onSuccess(content, fileMetadata)) + .catch((error) => console.error("Failed to load file content", error)); + }} + }}""" + + def visualize_properties(results, width=900, title="Properties", extra_config=None): """ Visualize properties using a Prove viewer. @@ -403,13 +442,18 @@ def visualize_properties(results, width=900, title="Properties", extra_config=No if isinstance(results, dict): results = [results] - DATA_KEYS = {"value", "values", "xDataArray"} + DATA_KEYS = {"value", "values", "xDataArray", "objectData"} results = [r for r in results if DATA_KEYS & r.keys()] timestamp = time.time() div_id = f"prove-{timestamp}" results_json = json.dumps(results) - extra_config_json = json.dumps(extra_config) if extra_config else "undefined" + if isinstance(extra_config, str): + extra_config_json = extra_config + elif extra_config: + extra_config_json = json.dumps(extra_config) + else: + extra_config_json = _build_file_content_extra_config(results) or "undefined" html = get_viewer_html( div_id=div_id,