diff --git a/other/materials_designer/create_solid_solution.ipynb b/other/materials_designer/create_solid_solution.ipynb new file mode 100644 index 000000000..243f8b8d0 --- /dev/null +++ b/other/materials_designer/create_solid_solution.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Create Solid Solution (Mixed Stoichiometry)\n", + "\n", + "Create a solid solution by partially substituting one element with another\n", + "(e.g., Hf₁₋ₓZrₓO₂). Automatically determines optimal supercell size to achieve\n", + "the target concentration.\n", + "\n", + "

Usage

\n", + "\n", + "1. Make sure to select Input Materials (in the outer runtime) before running the notebook.\n", + "1. Set parameters in cell 1.1. below.\n", + "1. Click \"Run\" > \"Run All\" to run all cells.\n", + "1. Scroll down to view results.\n", + "\n", + "## Notes\n", + "\n", + "1. For more information, see [Introduction](Introduction.ipynb)" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Prepare the Environment\n", + "### 1.1. Set solid solution parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "SOURCE_ELEMENT = \"Ni\" # Element to partially replace\n", + "TARGET_ELEMENT = \"Co\" # Replacement element\n", + "CONCENTRATION = 0.5 # Fraction of SOURCE_ELEMENT sites to replace (0–1)\n", + "\n", + "# Site selection: \"random\" or \"uniform\" (Farthest Point Sampling for even spacing)\n", + "SITE_SELECTION_METHOD = \"uniform\"\n", + "\n", + "# Random seed for reproducible site selection\n", + "SEED = 0\n", + "\n", + "# Tolerance for matching target concentration (smaller = larger supercell)\n", + "TOLERANCE = 0.01" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Install Packages\n", + "The step executes only in Pyodide environment. For other environments, the packages should be installed via `pip install` (see [README](../../README.ipynb))." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.packages import install_packages\n", + "\n", + "await install_packages(\"made\")" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "### 1.3. Get input materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.material import get_materials\n", + "\n", + "materials = get_materials(globals())" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## 2. Create Solid Solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.tools.helpers import create_solid_solution\n", + "\n", + "unit_cell = materials[0]\n", + "solid_solution = create_solid_solution(\n", + " material=unit_cell,\n", + " source_element=SOURCE_ELEMENT,\n", + " target_element=TARGET_ELEMENT,\n", + " concentration=CONCENTRATION,\n", + " seed=SEED,\n", + " tolerance=TOLERANCE,\n", + " site_selection_method=SITE_SELECTION_METHOD,\n", + ")\n", + "\n", + "from collections import Counter\n", + "composition = Counter(solid_solution.basis.elements.values)\n", + "n_cation = composition.get(SOURCE_ELEMENT, 0) + composition.get(TARGET_ELEMENT, 0)\n", + "actual_fraction = composition.get(TARGET_ELEMENT, 0) / n_cation if n_cation else 0\n", + "\n", + "print(f\"Unit cell: {unit_cell.name}\")\n", + "print(f\"Composition: {dict(composition)}\")\n", + "print(f\"Total atoms: {sum(composition.values())}\")\n", + "print(f\"Requested concentration: {CONCENTRATION}\")\n", + "print(f\"Actual concentration: {actual_fraction:.4f}\")\n", + "print(f\"Site selection method: {SITE_SELECTION_METHOD}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## 3. Visualize Result(s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", + "\n", + "visualize(\n", + " {\"material\": solid_solution, \"title\": f\"Solid Solution: {dict(composition)}\"}, viewer=\"wave\")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## 4. Pass data to the outside runtime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.material import set_materials\n", + "\n", + "set_materials([solid_solution])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/other/materials_designer/workflows/Introduction.ipynb b/other/materials_designer/workflows/Introduction.ipynb index 2f78cebea..7ae73f254 100644 --- a/other/materials_designer/workflows/Introduction.ipynb +++ b/other/materials_designer/workflows/Introduction.ipynb @@ -44,7 +44,7 @@ "### 4.2. Band Structure\n", "#### [4.2.1. Band Structure / Band Structure + DOS.](band_structure.ipynb)\n", "#### [4.2.2. Band Structure (HSE).](band_structure_hse.ipynb)\n", - "#### 4.2.3. Band Structure (Magnetic). *(to be added)*\n", + "#### [4.2.3. Band Structure (Magnetic).](band_structure_magn.ipynb)\n", "#### 4.2.4. Band Structure (Spin-Orbit Coupling). *(to be added)*\n", "\n", "\n", @@ -60,19 +60,23 @@ "#### [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", "\n", "### 6.4. Zero-Point Energy\n", - "#### 6.4.1. Zero-point energy calculation. *(to be added)*\n", + "#### [6.4.1. Zero-point energy calculation.](zero_point_energy.ipynb)\n", "\n", "### 6.5. Defect Energy\n", "#### 6.5.1. Defect formation energy. *(to be added)*\n", "\n", - "### 6.6. Formation Energy\n", - "#### 6.6.1. Compound formation energy. *(to be added)*\n", + "### 6.6. Phase Stability\n", + "#### [6.6.1. Formation Energies and Convex Hull.](analyze_convex_hull.ipynb)\n", + "#### [6.6.2. Phase Diagram.](generate_phase_diagram.ipynb)\n", + "\n", + "### 6.7. Formation Energy\n", + "#### 6.7.1. Compound formation energy. *(to be added)*\n", "\n", "\n", "## 7. Chemistry\n", diff --git a/other/materials_designer/workflows/analyze_convex_hull.ipynb b/other/materials_designer/workflows/analyze_convex_hull.ipynb new file mode 100644 index 000000000..1f99e6ca2 --- /dev/null +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -0,0 +1,389 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Formation Energies and Convex Hull (Analyzer)\n", + "\n", + "Analyze existing total energy results to compute formation energies and build the convex hull.\n", + "\n", + "

Usage

\n", + "\n", + "1. Set the material set and formulas in section 1 below.\n", + "2. Click \"Run\" > \"Run All\".\n", + "3. The notebook finds materials, retrieves total energies, and builds the convex hull.\n", + "\n", + "**Prerequisites:**\n", + "1. Save materials to a material set (for simpler search).\n", + "2. Relax if needed and store relaxed structures there.\n", + "3. Calculate total energies for the materials with the same formalism (e.g. DFT functional) and needed precision.\n", + "\n", + "## How it works\n", + "\n", + "1. **Find materials** in a material set (or all materials) matching the given formulas\n", + "2. **Preview** the materials (formula, structure type, space group)\n", + "3. **Retrieve total energies** from completed jobs\n", + "4. **Build convex hull** using pymatgen PhaseDiagram\n", + "5. **Analyze stability** — formation energies, energy above hull, decomposition products" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Set up the environment and parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.packages import install_packages\n", + "await install_packages(\"made|api_examples\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Set parameters and configurations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Formulas to search for materials\n", + "FORMULAS = [\"Hf\", \"Zr\", \"O2\", \"HfO2\", \"ZrO2\"]\n", + "\n", + "# Material set name to find materials and properties that belong to them. None = all materials.\n", + "MATERIAL_SET = None\n", + "\n", + "# Property group — encodes application:model:subtype:functional (e.g. \"qe:dft:gga:pbe\").\n", + "GROUP = \"qe:dft:gga:pbe\"\n", + "\n", + "# Precision value. None = use the best (highest) available. Specific value = use exactly that.\n", + "PRECISION = None\n", + "\n", + "# Organization to select.\n", + "ORGANIZATION_NAME = None\n" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## 2. Authenticate and initialize API client" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "### 2.2. Initialize API client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "await authenticate()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.api_client import APIClient\n", + "\n", + "client = APIClient.authenticate()\n", + "\n", + "selected_account = client.my_account\n", + "if ORGANIZATION_NAME:\n", + " selected_account = client.get_account(name=ORGANIZATION_NAME)\n", + "ACCOUNT_ID = selected_account.id\n", + "print(f\"✅ Account: {selected_account.name} ({ACCOUNT_ID})\")\n", + "client" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## 3. Find materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "# Resolve material set (if specified)\n", + "set_id = None\n", + "if MATERIAL_SET:\n", + " set_query = {\"owner._id\": ACCOUNT_ID, \"isEntitySet\": True,\n", + " \"name\": {\"$regex\": MATERIAL_SET, \"$options\": \"i\"}}\n", + " sets = client.materials.list(set_query)\n", + " if not sets:\n", + " raise ValueError(f\"No material set matching '{MATERIAL_SET}'\")\n", + " set_id = sets[0][\"_id\"]\n", + " print(f\"✅ Using set: {sets[0]['name']} ({set_id})\")\n", + "else:\n", + " print(\"ℹ️ No material set specified — searching all materials in account.\")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### 3.2. Search materials by formula" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "# Search materials by formula\n", + "all_materials = []\n", + "\n", + "for formula in FORMULAS:\n", + " query = {\"formula\": formula, \"owner._id\": ACCOUNT_ID}\n", + " if set_id:\n", + " query[\"inSet._id\"] = set_id\n", + " matches = client.materials.list(query)\n", + " # Filter out entity sets\n", + " matches = [m for m in matches if not m.get(\"isEntitySet\")]\n", + " for material in matches:\n", + " material[\"_search_formula\"] = formula # track which formula query found this\n", + " all_materials.extend(matches)\n", + "\n", + " print(f\"{formula}: {len(matches)} material(s) found\")\n", + "\n", + "print(f\"\\nTotal: {len(all_materials)} materials\")" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "### 3.3. Preview materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "preview = []\n", + "for material in all_materials:\n", + " n_atoms = len(material.get(\"basis\", {}).get(\"coordinates\", []))\n", + " lattice_type = material.get(\"lattice\", {}).get(\"type\", \"?\")\n", + " preview.append({\n", + " \"ID\": material[\"_id\"],\n", + " \"Name\": material.get(\"name\", \"?\"),\n", + " \"Formula\": material.get(\"formula\", \"?\"),\n", + " \"Lattice\": lattice_type,\n", + " \"Atoms\": n_atoms,\n", + " })\n", + "\n", + "df_preview = pd.DataFrame(preview)\n", + "df_preview" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## 4. Retrieve total energies\n", + "\n", + "For each material, find completed jobs and extract total energy properties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import Counter\n", + "\n", + "exabyte_ids = list(set(material[\"exabyteId\"] for material in all_materials if material.get(\"exabyteId\")))\n", + "query = {\n", + " \"exabyteId\": {\"$in\": exabyte_ids},\n", + " \"data.name\": \"total_energy\",\n", + "}\n", + "if GROUP:\n", + " query[\"group\"] = {\"$regex\": GROUP}\n", + "if PRECISION is not None:\n", + " query[\"precision.value\"] = PRECISION\n", + "all_properties = client.properties.list(query=query)\n", + "\n", + "# Keep one property per exabyteId: highest precision when PRECISION is None\n", + "properties_by_exabyte_id = {}\n", + "for property_holder in all_properties:\n", + " exabyte_id = property_holder.get(\"exabyteId\")\n", + " exabyte_id = exabyte_id if isinstance(exabyte_id, str) else (exabyte_id[0] if exabyte_id else None)\n", + " if not exabyte_id:\n", + " continue\n", + " existing = properties_by_exabyte_id.get(exabyte_id)\n", + " if existing is None or property_holder.get(\"precision\", {}).get(\"value\", 0) > existing.get(\"precision\", {}).get(\"value\", 0):\n", + " properties_by_exabyte_id[exabyte_id] = property_holder\n", + "\n", + "seen_exabyte_ids = set()\n", + "entries_data = []\n", + "for material in all_materials:\n", + " exabyte_id = material.get(\"exabyteId\")\n", + " if not exabyte_id or exabyte_id in seen_exabyte_ids:\n", + " continue\n", + " seen_exabyte_ids.add(exabyte_id)\n", + "\n", + " property_holder = properties_by_exabyte_id.get(exabyte_id)\n", + " if not property_holder:\n", + " print(f\"⚠️ {material['formula']}: no total energy\")\n", + " continue\n", + "\n", + " elements = material[\"basis\"][\"elements\"]\n", + " number_of_atoms = len(material[\"basis\"][\"coordinates\"])\n", + " composition = dict(Counter(element[\"value\"] for element in elements))\n", + " energy = property_holder[\"data\"][\"value\"]\n", + " precision = property_holder.get(\"precision\", {}).get(\"value\", \"?\")\n", + "\n", + " entries_data.append({\"material_id\": material[\"_id\"], \"formula\": material[\"formula\"], \"composition\": composition, \"n_atoms\": number_of_atoms, \"total_energy\": energy})\n", + " print(f\"✅ {material['formula']} (precision={precision}): {energy:.4f} eV ({energy / number_of_atoms:.4f} eV/atom)\")\n", + "\n", + "print(f\"\\n{len(entries_data)} entries.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## 5. Build convex hull" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "from mat3ra.notebooks_utils.core.entity.property.analysis import (\n", + " build_convex_hull, get_results_table\n", + ")\n", + "from mat3ra.notebooks_utils.ipython.entity.property.plot import (\n", + " plot_convex_hull\n", + ")\n", + "\n", + "phase_diagram = build_convex_hull(entries_data)\n", + "print(f\"✅ Convex hull built with {len(entries_data)} entries.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## 6. Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "df_results = get_results_table(phase_diagram, entries_data)\n", + "df_results\n" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "## 7. Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.plot._plotly import render_figure\n", + "\n", + "fig = plot_convex_hull(phase_diagram)\n", + "render_figure(fig)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/other/materials_designer/workflows/zero_point_energy.ipynb b/other/materials_designer/workflows/zero_point_energy.ipynb new file mode 100644 index 000000000..0827dcf19 --- /dev/null +++ b/other/materials_designer/workflows/zero_point_energy.ipynb @@ -0,0 +1,639 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Zero Point Energy\n", + "\n", + "Calculate the zero-point energy (ZPE) of a material using a DFT phonon workflow on the Mat3ra platform.\n", + "\n", + "The ZPE is computed from a gamma-point phonon calculation:\n", + "$$\\text{ZPE} = \\frac{1}{2} \\sum_i \\hbar\\omega_i$$\n", + "where $\\omega_i$ are the phonon frequencies at the Γ-point.\n", + "\n", + "The workflow runs two steps:\n", + "1. **SCF** (`pw.x`): Self-consistent field calculation to obtain the ground-state charge density.\n", + "2. **Phonon at Γ** (`ph.x`): Density-functional perturbation theory (DFPT) calculation at the gamma point to obtain phonon frequencies and ZPE.\n", + "\n", + "

Usage

\n", + "\n", + "1. Set material and calculation parameters in cell 1.2. 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. Create workflow and set its parameters: select application, load zero-point energy workflow from Standata, optionally add relaxation and adjust model/method parameters, and save the workflow to the platform.\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 zero-point energy." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Set up the environment and parameters\n", + "### 1.1. Install packages (JupyterLite)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.packages import install_packages\n", + "\n", + "await install_packages(\"made|api_examples\")" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Set parameters and configurations for the workflow and job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "from mat3ra.ide.compute import QueueName\n", + "\n", + "# 2. Auth and organization parameters\n", + "# Set organization name to use it as the owner, otherwise your personal account is used\n", + "ORGANIZATION_NAME = None\n", + "\n", + "# 3. Material parameters\n", + "FOLDER = \"../uploads\"\n", + "MATERIAL_NAME = \"Silicon\" # Name of the material to load from local file or Standata\n", + "\n", + "# 4. Workflow parameters\n", + "WORKFLOW_SEARCH_TERM = \"zero_point_energy.json\" # Search term for Workflows Standata\n", + "APPLICATION_NAME = \"espresso\" # Specify application name (e.g., \"espresso\", \"vasp\")\n", + "ADD_RELAXATION = False # Whether to add relaxation subworkflow as first unit\n", + "\n", + "MY_WORKFLOW_NAME = \"Zero Point Energy\" + (\" (relax)\" if ADD_RELAXATION else \"\")\n", + "\n", + "# Model parameters\n", + "MODEL_SUBTYPE = \"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\n" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "### 1.3. Set specific zero-point energy parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Method parameters\n", + "PSEUDOPOTENTIAL_TYPE = \"us\" # \"us\" (ultrasoft), \"nc\" (norm-conserving), \"paw\"\n", + "FUNCTIONAL = \"pbe\" # for lda: \"pz\"; for gga: \"pbe\", \"pbesol\"\n", + "\n", + "# K-grids\n", + "RELAXATION_KGRID = None # e.g. [4, 4, 4]\n", + "SCF_KGRID = None # e.g. [4, 4, 4]\n", + "\n", + "# Energy cutoffs\n", + "ECUTWFC = 40\n", + "ECUTRHO = 200" + ] + }, + { + "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\".\n" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "\n", + "await authenticate()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "### 2.2. Initialize API Client\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.api_client import APIClient\n", + "\n", + "client = APIClient.authenticate()\n", + "client" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "### 2.3. Select account to work under" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "client.list_accounts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "selected_account = client.my_account\n", + "\n", + "if ORGANIZATION_NAME:\n", + " selected_account = client.get_account(name=ORGANIZATION_NAME)\n", + "\n", + "ACCOUNT_ID = selected_account.id\n", + "print(f\"✅ Selected account ID: {ACCOUNT_ID}, name: {selected_account.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "### 2.4. Select project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "projects = client.projects.list({\"isDefault\": True, \"owner._id\": ACCOUNT_ID})\n", + "project_id = projects[0][\"_id\"]\n", + "print(f\"✅ Using project: {projects[0]['name']} ({project_id})\")" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## 3. Create material\n", + "### 3.1. Load material from local file (or Standata)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "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": "19", + "metadata": {}, + "source": [ + "### 3.2. Save material to the platform" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "\n", + "saved_material_response = get_or_create_material(client, material, ACCOUNT_ID)\n", + "saved_material = Material.create(saved_material_response)" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "## 4. Create workflow and set its parameters\n", + "### 4.1. Get list of applications and select one" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "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": "23", + "metadata": {}, + "source": [ + "### 4.2. Create workflow from standard workflows and preview it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "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": "25", + "metadata": {}, + "source": [ + "### 4.3. Modify workflow (Optional)\n", + "#### 4.3.1. Add relaxation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "if ADD_RELAXATION:\n", + " workflow.add_relaxation()\n" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### 4.3.2. Set model and method parameters (physics)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.model_tree import ModelTreeStandata\n", + "from mat3ra.mode.model import Model\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 = Model.create(model_config)\n", + "\n", + "for subworkflow in workflow.subworkflows:\n", + " subworkflow.model = model\n", + "\n", + "if ADD_RELAXATION:\n", + " workflow.relaxation_subworkflow.model = model\n", + "\n", + "# Preview modified workflow\n", + "visualize_workflow(workflow)\n" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "#### 4.3.3. Modify important settings\n", + "Set k-grid and cutoffs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.wode.context.providers import PlanewaveCutoffsContextProvider, PointsGridDataProvider\n", + "\n", + "if RELAXATION_KGRID is not None and ADD_RELAXATION:\n", + " new_context_relax = PointsGridDataProvider(dimensions=RELAXATION_KGRID,\n", + " isEdited=True).get_context_item_data() if ADD_RELAXATION else None\n", + " relaxation_subworkflow = workflow.subworkflows[0]\n", + " unit_to_modify_relax = relaxation_subworkflow.get_unit_by_name(name_regex=\"relax\")\n", + " unit_to_modify_relax.add_context(new_context_relax)\n", + " relaxation_subworkflow.set_unit(unit_to_modify_relax)\n", + "\n", + "if SCF_KGRID is not None:\n", + " new_context_scf = PointsGridDataProvider(dimensions=SCF_KGRID, isEdited=True).get_context_item_data()\n", + " zpe_subworkflow = workflow.subworkflows[1 if ADD_RELAXATION else 0]\n", + " unit_to_modify_scf = zpe_subworkflow.get_unit_by_name(name=\"pw_scf\")\n", + " unit_to_modify_scf.add_context(new_context_scf)\n", + " zpe_subworkflow.set_unit(unit_to_modify_scf)\n", + "\n", + "if ECUTWFC is not None:\n", + " cutoffs_context = PlanewaveCutoffsContextProvider(wavefunction=ECUTWFC, density=ECUTRHO, isEdited=True).get_context_item_data()\n", + " for unit_name in [\"pw_relax\", \"pw_vc-relax\", \"pw_scf\"]:\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", + "# Preview modified workflow\n", + "visualize_workflow(workflow)\n" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "### 4.4. Save workflow to collection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.utils.namespace import dict_to_namespace_recursive\n", + "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", + "\n", + "workflow_id_or_dict = None\n", + "\n", + "saved_workflow_response = get_or_create_workflow(client, workflow, ACCOUNT_ID)\n", + "saved_workflow = Workflow.create(saved_workflow_response)\n", + "print(f\"Workflow ID: {saved_workflow.id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "## 5. Create the compute configuration\n", + "### 5.1. Get list of clusters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "clusters = client.clusters.list()\n", + "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "### 5.2. Create compute configuration for the job\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.ide.compute import Compute\n", + "\n", + "# Select cluster: use specified name if provided, otherwise use first available\n", + "if CLUSTER_NAME:\n", + " cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), None)\n", + "else:\n", + " cluster = clusters[0]\n", + "\n", + "compute = Compute(\n", + " cluster=cluster,\n", + " queue=QUEUE_NAME,\n", + " ppn=PPN\n", + ")\n", + "print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}\")" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "## 6. Create the job with material and workflow configuration\n", + "### 6.1. Create job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.job import create_job\n", + "from mat3ra.notebooks_utils.ui import display_JSON\n", + "\n", + "print(f\"Material: {saved_material.id}\")\n", + "print(f\"Workflow: {saved_workflow.id}\")\n", + "print(f\"Project: {project_id}\")\n", + "\n", + "job_name = 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(\"✅ Job created successfully!\")\n", + "print(f\"Job ID: {job_id}\")\n", + "display_JSON(job_response)" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "## 7. Submit the job and monitor the status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "client.jobs.submit(job_id)\n", + "print(f\"✅ Job {job_id} submitted successfully!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "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": "42", + "metadata": {}, + "source": [ + "## 8. Retrieve results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.prode import PropertyName\n", + "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n", + "\n", + "properties_data = client.properties.get_for_job(job_id)\n", + "visualize_properties(properties_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "property_data = client.properties.get_for_job(job_id, property_name=PropertyName.scalar.zero_point_energy.value)\n", + "visualize_properties(property_data, title=\"Zero Point Energy\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/py/mat3ra/notebooks_utils/core/entity/property/analysis.py b/src/py/mat3ra/notebooks_utils/core/entity/property/analysis.py new file mode 100644 index 000000000..e1fd9762a --- /dev/null +++ b/src/py/mat3ra/notebooks_utils/core/entity/property/analysis.py @@ -0,0 +1,68 @@ +"""Phase stability (convex hull) analysis using pymatgen. + +Pure computation — no API calls, no display. +""" + +from typing import Dict, List, Union + +import pandas as pd +from mat3ra.esse.models.properties_directory.non_scalar.phase_stability_entries import PhaseStabilityEntrySchema +from pymatgen.analysis.phase_diagram import PhaseDiagram +from pymatgen.core import Composition +from pymatgen.entries.computed_entries import ComputedEntry + +# Accept both Pydantic models and plain dicts for convenience +PhaseStabilityEntry = Union[PhaseStabilityEntrySchema, Dict] + + +def build_convex_hull(entries_data: List[PhaseStabilityEntry]) -> PhaseDiagram: + """Build a pymatgen PhaseDiagram from phase stability entries. + + Args: + entries_data: List of PhaseStabilityEntry (Pydantic model or dict). + + Returns: + pymatgen PhaseDiagram object. + """ + entries = [] + for data in entries_data: + composition = Composition(data["composition"]) + entry = ComputedEntry( + composition, + data["total_energy"], + entry_id=data.get("material_id", ""), + ) + entries.append(entry) + + return PhaseDiagram(entries) + + +def get_results_table(phase_diagram: PhaseDiagram, entries_data: List[PhaseStabilityEntry]) -> pd.DataFrame: + """Build a results DataFrame from phase diagram analysis. + + Args: + phase_diagram: pymatgen PhaseDiagram object. + entries_data: List of PhaseStabilityEntry (same order as build_convex_hull input). + + Returns: + DataFrame with formula, material ID, energies, stability, and decomposition. + """ + results = [] + for i, entry in enumerate(phase_diagram.all_entries): + energy_above_hull = phase_diagram.get_e_above_hull(entry) + decomposition = phase_diagram.get_decomposition(entry.composition) + decomposition_str = " + ".join([e.composition.reduced_formula for e in decomposition]) + data = entries_data[i] + results.append( + { + "Formula": entry.composition.reduced_formula, + "Material ID": data.get("material_id", ""), + "E/atom (eV)": round(entry.energy_per_atom, 4), + "Eform/atom (eV)": round(phase_diagram.get_form_energy_per_atom(entry), 4), + "Above hull (eV)": round(energy_above_hull, 4), + "Stable": "✅" if energy_above_hull < 1e-6 else "❌", + "Decomposes to": decomposition_str if energy_above_hull > 1e-6 else "—", + } + ) + + return pd.DataFrame(results).sort_values("Above hull (eV)") diff --git a/src/py/mat3ra/notebooks_utils/ipython/entity/property/plot.py b/src/py/mat3ra/notebooks_utils/ipython/entity/property/plot.py new file mode 100644 index 000000000..1fa72a6fb --- /dev/null +++ b/src/py/mat3ra/notebooks_utils/ipython/entity/property/plot.py @@ -0,0 +1,75 @@ +"""Domain-specific charts for property visualization in notebooks.""" + +import plotly.graph_objects as go +from pymatgen.analysis.phase_diagram import PDPlotter, PhaseDiagram + +TEXT_SHADOW = "2px 2px 4px rgba(0,0,0,0.8), " "-2px -2px 4px rgba(0,0,0,0.8), " "0px 0px 8px rgba(0,0,0,0.9)" + + +def plot_convex_hull(phase_diagram: PhaseDiagram, show_unstable: float = 0.2) -> go.Figure: + """Plot an interactive phase diagram with clean, readable labels. + + Uses plotly via pymatgen's PDPlotter. Labels show formula + energy as text, + full info (including material ID) on hover. Uses an explicit dark background + so the plot looks consistent in both light and dark IDE themes. + + Args: + phase_diagram: pymatgen PhaseDiagram object. + show_unstable: Energy threshold (eV/atom) for showing unstable entries. + + Returns: + plotly Figure object. + """ + fig = PDPlotter(phase_diagram, show_unstable=show_unstable).get_plot() + + for trace in fig.data: + if not trace.hovertext: + continue + labels = [] + for hover_text in trace.hovertext: + parts = hover_text.split("
") + formula = parts[0].split("(")[0].strip() + energy_status = parts[1].strip() if len(parts) > 1 else "" + labels.append(f"{formula}
{energy_status}") + trace.text = tuple(labels) + trace.mode = "markers+text" + trace.marker.size = 20 + if trace.name == "Stable": + trace.textposition = "top center" + trace.textfont = dict(size=16, color="#FFFFFF", family="Arial Black", shadow=TEXT_SHADOW) + else: + trace.textposition = "bottom center" + trace.textfont = dict(size=14, color="#FF6666", family="Arial Black", shadow=TEXT_SHADOW) + + fig.update_layout( + height=900, + width=1000, + title=dict(text="Phase Diagram (Convex Hull)", font=dict(size=22, color="white")), + margin=dict(l=80, r=80, t=80, b=80), + paper_bgcolor="#1e1e1e", + plot_bgcolor="#1e1e1e", + ternary=dict( + bgcolor="#1e1e1e", + aaxis=dict( + title=dict(font=dict(size=22, color="white")), + linecolor="#555", + gridcolor="#333", + tickfont=dict(color="#aaa"), + ), + baxis=dict( + title=dict(font=dict(size=22, color="white")), + linecolor="#555", + gridcolor="#333", + tickfont=dict(color="#aaa"), + ), + caxis=dict( + title=dict(font=dict(size=22, color="white")), + linecolor="#555", + gridcolor="#333", + tickfont=dict(color="#aaa"), + ), + ), + legend=dict(font=dict(size=14, color="white")), + ) + + return fig