From 7418af39264cb64bee89eba248a2b74b9e4df590 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 30 Jun 2026 18:46:21 -0700 Subject: [PATCH 1/9] feat: NB to analyze FE convex hull --- .../workflows/analyze_convex_hull.ipynb | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 other/materials_designer/workflows/analyze_convex_hull.ipynb 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 00000000..5158f5d9 --- /dev/null +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -0,0 +1,387 @@ +{ + "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:** Total energy calculations must already be completed for the materials.\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\n", + "5. **Analyze stability** — formation energies, energy above hull, decomposition products" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Parameters" + ] + }, + { + "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": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Materials to search for.\n", + "FORMULAS = [\"Hf\", \"Zr\", \"O2\", \"HfO2\", \"ZrO2\"]\n", + "\n", + "# Name (partial match) or ID of a material set to search within.\n", + "# Set to None to search all materials in the account.\n", + "MATERIAL_SET = None # e.g., \"hf-zr-o-study\"\n", + "\n", + "# Organization name (partial match) to search within.\n", + "ORGANIZATION_NAME = None" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2. Authenticate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "await authenticate()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "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})\")" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## 3. Find materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "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": "code", + "execution_count": null, + "id": "9", + "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 m in matches:\n", + " m[\"_search_formula\"] = formula # track which formula query found this\n", + " all_materials.extend(matches)\n", + " print(f\"{formula}: {len(matches)} material(s) found\")\n", + "\n", + "print(f\"\\nTotal: {len(all_materials)} materials\")" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "### 3.1. Preview materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "preview = []\n", + "for m in all_materials:\n", + " elements = m.get(\"basis\", {}).get(\"elements\", [])\n", + " n_atoms = len(m.get(\"basis\", {}).get(\"coordinates\", []))\n", + " lattice_type = m.get(\"lattice\", {}).get(\"type\", \"?\")\n", + " preview.append({\n", + " \"ID\": m[\"_id\"],\n", + " \"Name\": m.get(\"name\", \"?\"),\n", + " \"Formula\": m.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": "12", + "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": "13", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import Counter\n", + "from mat3ra.prode import PropertyName\n", + "\n", + "entries_data = [] # list of {material, composition, total_energy, ...}\n", + "\n", + "for m in all_materials:\n", + " mat_id = m[\"_id\"]\n", + " formula = m.get(\"formula\", \"?\")\n", + " basis = m.get(\"basis\", {})\n", + " elements = basis.get(\"elements\", [])\n", + " n_atoms = len(basis.get(\"coordinates\", []))\n", + "\n", + " # Derive composition from actual basis elements\n", + " composition = dict(Counter(el[\"value\"] for el in elements))\n", + "\n", + " # Find jobs that used this material\n", + " jobs = client.jobs.list({\n", + " \"owner._id\": ACCOUNT_ID,\n", + " \"_material._id\": mat_id,\n", + " \"status\": \"finished\",\n", + " })\n", + "\n", + " if not jobs:\n", + " print(f\"⚠️ {formula} ({mat_id}): no finished jobs found — skipping\")\n", + " continue\n", + "\n", + " # Get total energy from the most recent finished job\n", + " job = jobs[0]\n", + " props = client.properties.get_for_job(\n", + " job[\"_id\"],\n", + " property_name=PropertyName.scalar.total_energy.value\n", + " )\n", + "\n", + " if not props or props[0].get(\"value\") is None:\n", + " print(f\"⚠️ {formula} ({mat_id}): no total energy in job {job['_id']} — skipping\")\n", + " continue\n", + "\n", + " total_energy = props[0][\"value\"]\n", + " energy_per_atom = total_energy / n_atoms\n", + "\n", + " entries_data.append({\n", + " \"material_id\": mat_id,\n", + " \"job_id\": job[\"_id\"],\n", + " \"formula\": formula,\n", + " \"composition\": composition,\n", + " \"n_atoms\": n_atoms,\n", + " \"total_energy\": total_energy,\n", + " \"energy_per_atom\": energy_per_atom,\n", + " \"lattice_type\": m.get(\"lattice\", {}).get(\"type\", \"?\"),\n", + " \"name\": m.get(\"name\", \"?\"),\n", + " })\n", + " print(f\"✅ {formula} ({m.get('lattice', {}).get('type', '?')}): \"\n", + " f\"E = {total_energy:.4f} eV, E/atom = {energy_per_atom:.4f} eV\")\n", + "\n", + "print(f\"\\n{len(entries_data)} entries with total energies.\")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## 5. Build convex hull" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "from pymatgen.core import Composition\n", + "from pymatgen.entries.computed_entries import ComputedEntry\n", + "from pymatgen.analysis.phase_diagram import PhaseDiagram\n", + "\n", + "entries = []\n", + "for data in entries_data:\n", + " comp = Composition(data[\"composition\"])\n", + " entry = ComputedEntry(comp, data[\"total_energy\"])\n", + " entries.append(entry)\n", + "\n", + "pd_diagram = PhaseDiagram(entries)\n", + "print(f\"✅ Convex hull built with {len(entries)} entries.\")" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## 6. Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "results = []\n", + "for i, entry in enumerate(entries):\n", + " e_above_hull = pd_diagram.get_e_above_hull(entry)\n", + " decomp, _ = pd_diagram.get_decomp_and_e_above_hull(entry)\n", + " decomp_str = \" + \".join([e.composition.reduced_formula for e in decomp])\n", + " data = entries_data[i]\n", + " results.append({\n", + " \"Formula\": entry.composition.reduced_formula,\n", + " \"Lattice\": data[\"lattice_type\"],\n", + " \"E/atom (eV)\": round(entry.energy_per_atom, 4),\n", + " \"E_form/atom (eV)\": round(pd_diagram.get_form_energy_per_atom(entry), 4),\n", + " \"E_above_hull (eV)\": round(e_above_hull, 4),\n", + " \"Stable\": \"✅\" if e_above_hull < 1e-6 else \"❌\",\n", + " \"Decomposes to\": decomp_str if e_above_hull > 1e-6 else \"—\",\n", + " })\n", + "\n", + "df = pd.DataFrame(results).sort_values(\"E_above_hull (eV)\")\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## 7. Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "from pymatgen.analysis.phase_diagram import PDPlotter\n", + "\n", + "fig = PDPlotter(pd_diagram, show_unstable=0.2).get_plot()\n", + "for trace in fig.data:\n", + " if trace.hovertext:\n", + " trace.text = trace.hovertext\n", + " trace.mode = \"markers+text\"\n", + " trace.textposition = \"top center\"\n", + "fig.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "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 +} From 958238334f7a9aabb60f1b8e7fc76223664b192d Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 30 Jun 2026 19:03:55 -0700 Subject: [PATCH 2/9] feat: NB to create mixed stoichiometry --- .../create_mixed_stoichiometry.ipynb | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 other/materials_designer/create_mixed_stoichiometry.ipynb diff --git a/other/materials_designer/create_mixed_stoichiometry.ipynb b/other/materials_designer/create_mixed_stoichiometry.ipynb new file mode 100644 index 00000000..ea2bac05 --- /dev/null +++ b/other/materials_designer/create_mixed_stoichiometry.ipynb @@ -0,0 +1,265 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Create Mixed Stoichiometry Material\n", + "\n", + "Load a crystal structure and substitute a fraction of one element with another to create\n", + "mixed-composition materials (e.g., Hf₁₋ₓZrₓO₂).\n", + "\n", + "

Usage

\n", + "\n", + "1. Make sure to select Input Materials (in the outer runtime) before running the notebook.\n", + "1. Set mixing 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", + "## How it works\n", + "\n", + "1. Create a supercell from the input material\n", + "2. Find all sites of the element to replace (`ORIGINAL_ELEMENT`)\n", + "3. Replace a fraction (`FRACTION`) of them with `REPLACEMENT_ELEMENT`\n", + "4. Preview and export the mixed-composition material" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Prepare the Environment\n", + "### 1.1. Set mixing parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "ORIGINAL_ELEMENT = \"Hf\" # Element to partially replace\n", + "REPLACEMENT_ELEMENT = \"Zr\" # Element to substitute in\n", + "FRACTION = 0.25 # Fraction of ORIGINAL_ELEMENT sites to replace (0–1)\n", + "\n", + "# Supercell size — larger cells allow finer composition control.\n", + "SUPERCELL_MATRIX = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]\n", + "\n", + "# Seed to randomize site selection.\n", + "RANDOM_SEED = 0" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Install Packages" + ] + }, + { + "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": [ + "### 1.4. Create supercell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", + "from mat3ra.made.tools.helpers import create_supercell\n", + "\n", + "unit_cell = materials[0]\n", + "supercell = create_supercell(unit_cell, supercell_matrix=SUPERCELL_MATRIX)\n", + "\n", + "print(f\"Unit cell: {unit_cell.name}\")\n", + "print(f\"Supercell atoms: {len(supercell.basis.coordinates.values)}\")\n", + "visualize(supercell, repetitions=[1, 1, 1], rotation=\"0x\")" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## 2. Create substitutions\n", + "### 2.1. Find sites of the original element and select which to replace" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "# Find all sites of the original element\n", + "elements = supercell.basis.elements.values\n", + "coordinates = supercell.basis.coordinates.values\n", + "\n", + "original_sites = [(i, coord) for i, (el, coord) in enumerate(zip(elements, coordinates))\n", + " if el == ORIGINAL_ELEMENT]\n", + "\n", + "n_total = len(original_sites)\n", + "n_replace = round(n_total * FRACTION)\n", + "\n", + "print(f\"Found {n_total} {ORIGINAL_ELEMENT} sites in the supercell.\")\n", + "print(f\"Will replace {n_replace} with {REPLACEMENT_ELEMENT} (fraction = {FRACTION}).\")\n", + "print(f\"Result: {ORIGINAL_ELEMENT}_{{{n_total - n_replace}}}{REPLACEMENT_ELEMENT}_{{{n_replace}}}\")\n", + "\n", + "# Select sites to replace\n", + "if RANDOM_SEED is not None:\n", + " random.seed(RANDOM_SEED)\n", + "sites_to_replace = random.sample(original_sites, n_replace)\n", + "\n", + "print(f\"\\nSites selected for substitution:\")\n", + "for idx, coord in sites_to_replace:\n", + " print(f\" Site {idx}: {coord}\")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### 2.2. Apply substitutions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "import copy\n", + "\n", + "mixed_material = copy.deepcopy(supercell)\n", + "elements = mixed_material.basis.elements.values\n", + "\n", + "for idx, _ in sites_to_replace:\n", + " elements[idx] = REPLACEMENT_ELEMENT\n", + "\n", + "mixed_material.basis.elements.values = elements\n", + "\n", + "# Verify composition and actual fraction\n", + "from collections import Counter\n", + "composition = Counter(mixed_material.basis.elements.values)\n", + "actual_fraction = composition[REPLACEMENT_ELEMENT] / (composition.get(ORIGINAL_ELEMENT, 0) + composition[REPLACEMENT_ELEMENT])\n", + "\n", + "print(f\"Composition: {dict(composition)}\")\n", + "print(f\"Total atoms: {sum(composition.values())}\")\n", + "print(f\"Requested fraction: {FRACTION}\")\n", + "print(f\"Actual fraction: {actual_fraction:.4f} ({composition[REPLACEMENT_ELEMENT]}/{composition.get(ORIGINAL_ELEMENT, 0) + composition[REPLACEMENT_ELEMENT]} {ORIGINAL_ELEMENT} sites)\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "## 3. Visualize result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", + "\n", + "visualize([{\"material\": supercell, \"title\": f\"Original ({unit_cell.name})\"},\n", + " {\"material\": mixed_material, \"title\": f\"Mixed: {dict(composition)}\"}],\n", + " rotation=\"-90x\")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## 4. Pass to outside runtime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.material import set_materials\n", + "\n", + "comp_str = \"\".join(f\"{el}{n}\" for el, n in sorted(composition.items()))\n", + "mixed_material.name = f\"{unit_cell.name} mixed {comp_str}\"\n", + "set_materials([mixed_material])" + ] + } + ], + "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 +} From 10bb19c469592b9b7745d3dc504edb853c1699dd Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 30 Jun 2026 19:04:14 -0700 Subject: [PATCH 3/9] update: NB to analyze FE convex hull --- .../workflows/analyze_convex_hull.ipynb | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/other/materials_designer/workflows/analyze_convex_hull.ipynb b/other/materials_designer/workflows/analyze_convex_hull.ipynb index 5158f5d9..d299be5b 100644 --- a/other/materials_designer/workflows/analyze_convex_hull.ipynb +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -53,14 +53,19 @@ "metadata": {}, "outputs": [], "source": [ - "# Materials to search for.\n", + "# Formulas to search for materials\n", "FORMULAS = [\"Hf\", \"Zr\", \"O2\", \"HfO2\", \"ZrO2\"]\n", "\n", - "# Name (partial match) or ID of a material set to search within.\n", - "# Set to None to search all materials in the account.\n", - "MATERIAL_SET = None # e.g., \"hf-zr-o-study\"\n", + "# Material set name to find materials and properties that belong to them. None = all materials.\n", + "MATERIAL_SET = None\n", "\n", - "# Organization name (partial match) to search within.\n", + "# Method filter for total energy properties.\n", + "METHOD_FILTER = \"qe:gga\"\n", + "\n", + "# Minimum precision value. None = accept the highest precision.\n", + "MIN_PRECISION = None\n", + "\n", + "# Organization to select.\n", "ORGANIZATION_NAME = None" ] }, @@ -207,49 +212,52 @@ "metadata": {}, "outputs": [], "source": [ + "import json as _json\n", "from collections import Counter\n", - "from mat3ra.prode import PropertyName\n", "\n", - "entries_data = [] # list of {material, composition, total_energy, ...}\n", + "entries_data = []\n", "\n", "for m in all_materials:\n", " mat_id = m[\"_id\"]\n", " formula = m.get(\"formula\", \"?\")\n", + " exabyte_id = m.get(\"exabyteId\")\n", " basis = m.get(\"basis\", {})\n", " elements = basis.get(\"elements\", [])\n", " n_atoms = len(basis.get(\"coordinates\", []))\n", - "\n", - " # Derive composition from actual basis elements\n", " composition = dict(Counter(el[\"value\"] for el in elements))\n", "\n", - " # Find jobs that used this material\n", - " jobs = client.jobs.list({\n", - " \"owner._id\": ACCOUNT_ID,\n", - " \"_material._id\": mat_id,\n", - " \"status\": \"finished\",\n", - " })\n", - "\n", - " if not jobs:\n", - " print(f\"⚠️ {formula} ({mat_id}): no finished jobs found — skipping\")\n", + " if not exabyte_id:\n", + " print(f\"⚠️ {formula} ({mat_id}): no exabyteId — skipping\")\n", " continue\n", "\n", - " # Get total energy from the most recent finished job\n", - " job = jobs[0]\n", - " props = client.properties.get_for_job(\n", - " job[\"_id\"],\n", - " property_name=PropertyName.scalar.total_energy.value\n", + " # Query properties by material identity + method\n", + " query = {\n", + " \"exabyteId\": exabyte_id,\n", + " \"data.name\": \"total_energy\",\n", + " }\n", + " if METHOD_FILTER:\n", + " query[\"group\"] = {\"$regex\": METHOD_FILTER}\n", + " if MIN_PRECISION is not None:\n", + " query[\"precision.value\"] = {\"$gte\": MIN_PRECISION}\n", + "\n", + " props = client.properties.list(\n", + " query=query,\n", + " projection={\"sort\": {\"precision.value\": -1}, \"limit\": 1},\n", " )\n", "\n", - " if not props or props[0].get(\"value\") is None:\n", - " print(f\"⚠️ {formula} ({mat_id}): no total energy in job {job['_id']} — skipping\")\n", + " if not props:\n", + " print(f\"⚠️ {formula} ({mat_id}): no total energy (method=\\\"{METHOD_FILTER}\\\", \"\n", + " f\"min_precision={MIN_PRECISION}) — skipping\")\n", " continue\n", "\n", - " total_energy = props[0][\"value\"]\n", + " prop = props[0]\n", + " total_energy = prop[\"data\"][\"value\"]\n", " energy_per_atom = total_energy / n_atoms\n", + " group = prop.get(\"group\", \"?\")\n", + " precision = prop.get(\"precision\", {}).get(\"value\", \"?\")\n", "\n", " entries_data.append({\n", " \"material_id\": mat_id,\n", - " \"job_id\": job[\"_id\"],\n", " \"formula\": formula,\n", " \"composition\": composition,\n", " \"n_atoms\": n_atoms,\n", @@ -257,11 +265,14 @@ " \"energy_per_atom\": energy_per_atom,\n", " \"lattice_type\": m.get(\"lattice\", {}).get(\"type\", \"?\"),\n", " \"name\": m.get(\"name\", \"?\"),\n", + " \"group\": group,\n", + " \"precision\": precision,\n", " })\n", - " print(f\"✅ {formula} ({m.get('lattice', {}).get('type', '?')}): \"\n", + " print(f\"✅ {formula} ({m.get('lattice', {}).get('type', '?')}) \"\n", + " f\"[{group}, prec={precision}]: \"\n", " f\"E = {total_energy:.4f} eV, E/atom = {energy_per_atom:.4f} eV\")\n", "\n", - "print(f\"\\n{len(entries_data)} entries with total energies.\")" + "print(f\"\\n{len(entries_data)} entries with total energies.\")\n" ] }, { From 48cd1fbcc1ae4e332773b47d78d3d4f5d7264bee Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 30 Jun 2026 20:10:24 -0700 Subject: [PATCH 4/9] update: search for right properties --- .../workflows/analyze_convex_hull.ipynb | 217 ++++++++++-------- 1 file changed, 123 insertions(+), 94 deletions(-) diff --git a/other/materials_designer/workflows/analyze_convex_hull.ipynb b/other/materials_designer/workflows/analyze_convex_hull.ipynb index d299be5b..053d32bd 100644 --- a/other/materials_designer/workflows/analyze_convex_hull.ipynb +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -15,14 +15,17 @@ "2. Click \"Run\" > \"Run All\".\n", "3. The notebook finds materials, retrieves total energies, and builds the convex hull.\n", "\n", - "**Prerequisites:** Total energy calculations must already be completed for the materials.\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\n", + "4. **Build convex hull** using pymatgen PhaseDiagram\n", "5. **Analyze stability** — formation energies, energy above hull, decomposition products" ] }, @@ -31,7 +34,7 @@ "id": "1", "metadata": {}, "source": [ - "## 1. Parameters" + "## 1. Set up the environment and parameters" ] }, { @@ -43,13 +46,21 @@ "source": [ "from mat3ra.notebooks_utils.packages import install_packages\n", "\n", - "await install_packages(\"made|api_examples\")" + "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": "3", + "id": "4", "metadata": {}, "outputs": [], "source": [ @@ -59,28 +70,51 @@ "# Material set name to find materials and properties that belong to them. None = all materials.\n", "MATERIAL_SET = None\n", "\n", - "# Method filter for total energy properties.\n", - "METHOD_FILTER = \"qe:gga\"\n", + "# Property group — encodes application:model:subtype:functional (e.g. \"qe:dft:gga:pbe\").\n", + "GROUP = \"qe:dft:gga:pbe\"\n", "\n", - "# Minimum precision value. None = accept the highest precision.\n", - "MIN_PRECISION = None\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" + "ORGANIZATION_NAME = None\n" ] }, { "cell_type": "markdown", - "id": "4", + "id": "5", "metadata": {}, "source": [ - "## 2. Authenticate" + "## 2. Authenticate and initialize API client" ] }, { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "6", + "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\"] = \"dVlXKmUXBUxynCx2MT7CidNDf3KentlIONIha4TorHQ\"" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "### 2.2. Initialize API client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -92,7 +126,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -109,7 +143,7 @@ }, { "cell_type": "markdown", - "id": "7", + "id": "10", "metadata": {}, "source": [ "## 3. Find materials" @@ -118,7 +152,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -136,10 +170,18 @@ " print(\"ℹ️ No material set specified — searching all materials in account.\")" ] }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "### 3.2. Search materials by formula" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -153,9 +195,10 @@ " matches = client.materials.list(query)\n", " # Filter out entity sets\n", " matches = [m for m in matches if not m.get(\"isEntitySet\")]\n", - " for m in matches:\n", - " m[\"_search_formula\"] = formula # track which formula query found this\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\")" @@ -163,30 +206,29 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "14", "metadata": {}, "source": [ - "### 3.1. Preview materials" + "### 3.3. Preview materials" ] }, { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "15", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "preview = []\n", - "for m in all_materials:\n", - " elements = m.get(\"basis\", {}).get(\"elements\", [])\n", - " n_atoms = len(m.get(\"basis\", {}).get(\"coordinates\", []))\n", - " lattice_type = m.get(\"lattice\", {}).get(\"type\", \"?\")\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\": m[\"_id\"],\n", - " \"Name\": m.get(\"name\", \"?\"),\n", - " \"Formula\": m.get(\"formula\", \"?\"),\n", + " \"ID\": material[\"_id\"],\n", + " \"Name\": material.get(\"name\", \"?\"),\n", + " \"Formula\": material.get(\"formula\", \"?\"),\n", " \"Lattice\": lattice_type,\n", " \"Atoms\": n_atoms,\n", " })\n", @@ -197,7 +239,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "16", "metadata": {}, "source": [ "## 4. Retrieve total energies\n", @@ -208,76 +250,62 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "17", "metadata": {}, "outputs": [], "source": [ - "import json as _json\n", "from collections import Counter\n", "\n", - "entries_data = []\n", - "\n", - "for m in all_materials:\n", - " mat_id = m[\"_id\"]\n", - " formula = m.get(\"formula\", \"?\")\n", - " exabyte_id = m.get(\"exabyteId\")\n", - " basis = m.get(\"basis\", {})\n", - " elements = basis.get(\"elements\", [])\n", - " n_atoms = len(basis.get(\"coordinates\", []))\n", - " composition = dict(Counter(el[\"value\"] for el in elements))\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", - " print(f\"⚠️ {formula} ({mat_id}): no exabyteId — skipping\")\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", - " # Query properties by material identity + method\n", - " query = {\n", - " \"exabyteId\": exabyte_id,\n", - " \"data.name\": \"total_energy\",\n", - " }\n", - " if METHOD_FILTER:\n", - " query[\"group\"] = {\"$regex\": METHOD_FILTER}\n", - " if MIN_PRECISION is not None:\n", - " query[\"precision.value\"] = {\"$gte\": MIN_PRECISION}\n", - "\n", - " props = client.properties.list(\n", - " query=query,\n", - " projection={\"sort\": {\"precision.value\": -1}, \"limit\": 1},\n", - " )\n", - "\n", - " if not props:\n", - " print(f\"⚠️ {formula} ({mat_id}): no total energy (method=\\\"{METHOD_FILTER}\\\", \"\n", - " f\"min_precision={MIN_PRECISION}) — skipping\")\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", - " prop = props[0]\n", - " total_energy = prop[\"data\"][\"value\"]\n", - " energy_per_atom = total_energy / n_atoms\n", - " group = prop.get(\"group\", \"?\")\n", - " precision = prop.get(\"precision\", {}).get(\"value\", \"?\")\n", - "\n", - " entries_data.append({\n", - " \"material_id\": mat_id,\n", - " \"formula\": formula,\n", - " \"composition\": composition,\n", - " \"n_atoms\": n_atoms,\n", - " \"total_energy\": total_energy,\n", - " \"energy_per_atom\": energy_per_atom,\n", - " \"lattice_type\": m.get(\"lattice\", {}).get(\"type\", \"?\"),\n", - " \"name\": m.get(\"name\", \"?\"),\n", - " \"group\": group,\n", - " \"precision\": precision,\n", - " })\n", - " print(f\"✅ {formula} ({m.get('lattice', {}).get('type', '?')}) \"\n", - " f\"[{group}, prec={precision}]: \"\n", - " f\"E = {total_energy:.4f} eV, E/atom = {energy_per_atom:.4f} eV\")\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", - "print(f\"\\n{len(entries_data)} entries with total energies.\")\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": "14", + "id": "18", "metadata": {}, "source": [ "## 5. Build convex hull" @@ -286,18 +314,20 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "19", "metadata": {}, "outputs": [], "source": [ + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", "from pymatgen.core import Composition\n", "from pymatgen.entries.computed_entries import ComputedEntry\n", "from pymatgen.analysis.phase_diagram import PhaseDiagram\n", "\n", "entries = []\n", "for data in entries_data:\n", - " comp = Composition(data[\"composition\"])\n", - " entry = ComputedEntry(comp, data[\"total_energy\"])\n", + " composition = Composition(data[\"composition\"])\n", + " entry = ComputedEntry(composition, data[\"total_energy\"], entry_id=data[\"material_id\"])\n", " entries.append(entry)\n", "\n", "pd_diagram = PhaseDiagram(entries)\n", @@ -306,7 +336,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "20", "metadata": {}, "source": [ "## 6. Results" @@ -315,7 +345,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -327,7 +357,6 @@ " data = entries_data[i]\n", " results.append({\n", " \"Formula\": entry.composition.reduced_formula,\n", - " \"Lattice\": data[\"lattice_type\"],\n", " \"E/atom (eV)\": round(entry.energy_per_atom, 4),\n", " \"E_form/atom (eV)\": round(pd_diagram.get_form_energy_per_atom(entry), 4),\n", " \"E_above_hull (eV)\": round(e_above_hull, 4),\n", @@ -341,7 +370,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "22", "metadata": {}, "source": [ "## 7. Plot" @@ -350,7 +379,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -368,7 +397,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "24", "metadata": {}, "outputs": [], "source": [] From c41b4887bbd00b03cf17bd15c91759c5d79baead Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 30 Jun 2026 22:14:57 -0700 Subject: [PATCH 5/9] update: create convex hull in utils --- .../workflows/analyze_convex_hull.ipynb | 56 +++++--------- .../core/entity/property/analysis.py | 68 +++++++++++++++++ .../ipython/entity/property/plot.py | 75 +++++++++++++++++++ 3 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 src/py/mat3ra/notebooks_utils/core/entity/property/analysis.py create mode 100644 src/py/mat3ra/notebooks_utils/ipython/entity/property/plot.py diff --git a/other/materials_designer/workflows/analyze_convex_hull.ipynb b/other/materials_designer/workflows/analyze_convex_hull.ipynb index 053d32bd..ee916f03 100644 --- a/other/materials_designer/workflows/analyze_convex_hull.ipynb +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -44,6 +44,8 @@ "metadata": {}, "outputs": [], "source": [ + "from matscipy.calculators.mcfm import cluster_data\n", + "\n", "from mat3ra.notebooks_utils.packages import install_packages\n", "\n", "await install_packages(\"made|api_examples\")\n" @@ -100,7 +102,7 @@ "os.environ[\"API_PORT\"] = \"3000\"\n", "os.environ[\"API_SECURE\"] = \"false\"\n", "os.environ[\"API_HOST\"] = \"localhost\"\n", - "# os.environ[\"OIDC_ACCESS_TOKEN\"] = \"dVlXKmUXBUxynCx2MT7CidNDf3KentlIONIha4TorHQ\"" + "os.environ[\"OIDC_ACCESS_TOKEN\"] = \"YhCx-O5H0a0f-srWMKJOH9_jzPm0-akjeVmiwoBWmVc\"" ] }, { @@ -138,7 +140,8 @@ "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})\")" + "print(f\"✅ Account: {selected_account.name} ({ACCOUNT_ID})\")\n", + "client" ] }, { @@ -320,18 +323,15 @@ "source": [ "import warnings\n", "warnings.filterwarnings(\"ignore\")\n", - "from pymatgen.core import Composition\n", - "from pymatgen.entries.computed_entries import ComputedEntry\n", - "from pymatgen.analysis.phase_diagram import PhaseDiagram\n", - "\n", - "entries = []\n", - "for data in entries_data:\n", - " composition = Composition(data[\"composition\"])\n", - " entry = ComputedEntry(composition, data[\"total_energy\"], entry_id=data[\"material_id\"])\n", - " entries.append(entry)\n", - "\n", - "pd_diagram = PhaseDiagram(entries)\n", - "print(f\"✅ Convex hull built with {len(entries)} entries.\")" + "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" ] }, { @@ -349,23 +349,8 @@ "metadata": {}, "outputs": [], "source": [ - "results = []\n", - "for i, entry in enumerate(entries):\n", - " e_above_hull = pd_diagram.get_e_above_hull(entry)\n", - " decomp, _ = pd_diagram.get_decomp_and_e_above_hull(entry)\n", - " decomp_str = \" + \".join([e.composition.reduced_formula for e in decomp])\n", - " data = entries_data[i]\n", - " results.append({\n", - " \"Formula\": entry.composition.reduced_formula,\n", - " \"E/atom (eV)\": round(entry.energy_per_atom, 4),\n", - " \"E_form/atom (eV)\": round(pd_diagram.get_form_energy_per_atom(entry), 4),\n", - " \"E_above_hull (eV)\": round(e_above_hull, 4),\n", - " \"Stable\": \"✅\" if e_above_hull < 1e-6 else \"❌\",\n", - " \"Decomposes to\": decomp_str if e_above_hull > 1e-6 else \"—\",\n", - " })\n", - "\n", - "df = pd.DataFrame(results).sort_values(\"E_above_hull (eV)\")\n", - "df" + "df_results = get_results_table(phase_diagram, entries_data)\n", + "df_results\n" ] }, { @@ -383,14 +368,7 @@ "metadata": {}, "outputs": [], "source": [ - "from pymatgen.analysis.phase_diagram import PDPlotter\n", - "\n", - "fig = PDPlotter(pd_diagram, show_unstable=0.2).get_plot()\n", - "for trace in fig.data:\n", - " if trace.hovertext:\n", - " trace.text = trace.hovertext\n", - " trace.mode = \"markers+text\"\n", - " trace.textposition = \"top center\"\n", + "fig = plot_convex_hull(phase_diagram)\n", "fig.show()\n" ] }, 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 00000000..e1fd9762 --- /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 00000000..1fa72a6f --- /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 From 00d345ed2ec025714f7d74b0f9f0a95fe118c0e8 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 1 Jul 2026 09:22:25 -0700 Subject: [PATCH 6/9] chore: cleanup --- .../materials_designer/workflows/analyze_convex_hull.ipynb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/other/materials_designer/workflows/analyze_convex_hull.ipynb b/other/materials_designer/workflows/analyze_convex_hull.ipynb index ee916f03..93ee9c76 100644 --- a/other/materials_designer/workflows/analyze_convex_hull.ipynb +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -44,10 +44,7 @@ "metadata": {}, "outputs": [], "source": [ - "from matscipy.calculators.mcfm import cluster_data\n", - "\n", "from mat3ra.notebooks_utils.packages import install_packages\n", - "\n", "await install_packages(\"made|api_examples\")\n" ] }, @@ -368,8 +365,10 @@ "metadata": {}, "outputs": [], "source": [ + "from mat3ra.notebooks_utils.ipython.plot._plotly import render_figure\n", + "\n", "fig = plot_convex_hull(phase_diagram)\n", - "fig.show()\n" + "render_figure(fig)\n" ] }, { From 033114cde11286111e4357d3a070b4b31fe5852e Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 1 Jul 2026 16:21:32 -0700 Subject: [PATCH 7/9] update: solid solution NB --- .../create_mixed_stoichiometry.ipynb | 265 ------------------ .../create_solid_solution.ipynb | 201 +++++++++++++ 2 files changed, 201 insertions(+), 265 deletions(-) delete mode 100644 other/materials_designer/create_mixed_stoichiometry.ipynb create mode 100644 other/materials_designer/create_solid_solution.ipynb diff --git a/other/materials_designer/create_mixed_stoichiometry.ipynb b/other/materials_designer/create_mixed_stoichiometry.ipynb deleted file mode 100644 index ea2bac05..00000000 --- a/other/materials_designer/create_mixed_stoichiometry.ipynb +++ /dev/null @@ -1,265 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Create Mixed Stoichiometry Material\n", - "\n", - "Load a crystal structure and substitute a fraction of one element with another to create\n", - "mixed-composition materials (e.g., Hf₁₋ₓZrₓO₂).\n", - "\n", - "

Usage

\n", - "\n", - "1. Make sure to select Input Materials (in the outer runtime) before running the notebook.\n", - "1. Set mixing 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", - "## How it works\n", - "\n", - "1. Create a supercell from the input material\n", - "2. Find all sites of the element to replace (`ORIGINAL_ELEMENT`)\n", - "3. Replace a fraction (`FRACTION`) of them with `REPLACEMENT_ELEMENT`\n", - "4. Preview and export the mixed-composition material" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## 1. Prepare the Environment\n", - "### 1.1. Set mixing parameters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2", - "metadata": {}, - "outputs": [], - "source": [ - "ORIGINAL_ELEMENT = \"Hf\" # Element to partially replace\n", - "REPLACEMENT_ELEMENT = \"Zr\" # Element to substitute in\n", - "FRACTION = 0.25 # Fraction of ORIGINAL_ELEMENT sites to replace (0–1)\n", - "\n", - "# Supercell size — larger cells allow finer composition control.\n", - "SUPERCELL_MATRIX = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]\n", - "\n", - "# Seed to randomize site selection.\n", - "RANDOM_SEED = 0" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "### 1.2. Install Packages" - ] - }, - { - "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": [ - "### 1.4. Create supercell" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8", - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", - "from mat3ra.made.tools.helpers import create_supercell\n", - "\n", - "unit_cell = materials[0]\n", - "supercell = create_supercell(unit_cell, supercell_matrix=SUPERCELL_MATRIX)\n", - "\n", - "print(f\"Unit cell: {unit_cell.name}\")\n", - "print(f\"Supercell atoms: {len(supercell.basis.coordinates.values)}\")\n", - "visualize(supercell, repetitions=[1, 1, 1], rotation=\"0x\")" - ] - }, - { - "cell_type": "markdown", - "id": "9", - "metadata": {}, - "source": [ - "## 2. Create substitutions\n", - "### 2.1. Find sites of the original element and select which to replace" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10", - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "\n", - "# Find all sites of the original element\n", - "elements = supercell.basis.elements.values\n", - "coordinates = supercell.basis.coordinates.values\n", - "\n", - "original_sites = [(i, coord) for i, (el, coord) in enumerate(zip(elements, coordinates))\n", - " if el == ORIGINAL_ELEMENT]\n", - "\n", - "n_total = len(original_sites)\n", - "n_replace = round(n_total * FRACTION)\n", - "\n", - "print(f\"Found {n_total} {ORIGINAL_ELEMENT} sites in the supercell.\")\n", - "print(f\"Will replace {n_replace} with {REPLACEMENT_ELEMENT} (fraction = {FRACTION}).\")\n", - "print(f\"Result: {ORIGINAL_ELEMENT}_{{{n_total - n_replace}}}{REPLACEMENT_ELEMENT}_{{{n_replace}}}\")\n", - "\n", - "# Select sites to replace\n", - "if RANDOM_SEED is not None:\n", - " random.seed(RANDOM_SEED)\n", - "sites_to_replace = random.sample(original_sites, n_replace)\n", - "\n", - "print(f\"\\nSites selected for substitution:\")\n", - "for idx, coord in sites_to_replace:\n", - " print(f\" Site {idx}: {coord}\")" - ] - }, - { - "cell_type": "markdown", - "id": "11", - "metadata": {}, - "source": [ - "### 2.2. Apply substitutions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "12", - "metadata": {}, - "outputs": [], - "source": [ - "import copy\n", - "\n", - "mixed_material = copy.deepcopy(supercell)\n", - "elements = mixed_material.basis.elements.values\n", - "\n", - "for idx, _ in sites_to_replace:\n", - " elements[idx] = REPLACEMENT_ELEMENT\n", - "\n", - "mixed_material.basis.elements.values = elements\n", - "\n", - "# Verify composition and actual fraction\n", - "from collections import Counter\n", - "composition = Counter(mixed_material.basis.elements.values)\n", - "actual_fraction = composition[REPLACEMENT_ELEMENT] / (composition.get(ORIGINAL_ELEMENT, 0) + composition[REPLACEMENT_ELEMENT])\n", - "\n", - "print(f\"Composition: {dict(composition)}\")\n", - "print(f\"Total atoms: {sum(composition.values())}\")\n", - "print(f\"Requested fraction: {FRACTION}\")\n", - "print(f\"Actual fraction: {actual_fraction:.4f} ({composition[REPLACEMENT_ELEMENT]}/{composition.get(ORIGINAL_ELEMENT, 0) + composition[REPLACEMENT_ELEMENT]} {ORIGINAL_ELEMENT} sites)\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "13", - "metadata": {}, - "source": [ - "## 3. Visualize result" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14", - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials as visualize\n", - "\n", - "visualize([{\"material\": supercell, \"title\": f\"Original ({unit_cell.name})\"},\n", - " {\"material\": mixed_material, \"title\": f\"Mixed: {dict(composition)}\"}],\n", - " rotation=\"-90x\")" - ] - }, - { - "cell_type": "markdown", - "id": "15", - "metadata": {}, - "source": [ - "## 4. Pass to outside runtime" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16", - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.notebooks_utils.material import set_materials\n", - "\n", - "comp_str = \"\".join(f\"{el}{n}\" for el, n in sorted(composition.items()))\n", - "mixed_material.name = f\"{unit_cell.name} mixed {comp_str}\"\n", - "set_materials([mixed_material])" - ] - } - ], - "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/create_solid_solution.ipynb b/other/materials_designer/create_solid_solution.ipynb new file mode 100644 index 00000000..243f8b8d --- /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 +} From 3f798661e046bdbeea26a378183bb765a493a537 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 1 Jul 2026 16:23:44 -0700 Subject: [PATCH 8/9] chore: cleanup --- .../workflows/analyze_convex_hull.ipynb | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/other/materials_designer/workflows/analyze_convex_hull.ipynb b/other/materials_designer/workflows/analyze_convex_hull.ipynb index 93ee9c76..1f99e6ca 100644 --- a/other/materials_designer/workflows/analyze_convex_hull.ipynb +++ b/other/materials_designer/workflows/analyze_convex_hull.ipynb @@ -87,24 +87,9 @@ "## 2. Authenticate and initialize API client" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "6", - "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\"] = \"YhCx-O5H0a0f-srWMKJOH9_jzPm0-akjeVmiwoBWmVc\"" - ] - }, { "cell_type": "markdown", - "id": "7", + "id": "6", "metadata": {}, "source": [ "### 2.2. Initialize API client" @@ -113,7 +98,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -125,7 +110,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -143,7 +128,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "9", "metadata": {}, "source": [ "## 3. Find materials" @@ -152,7 +137,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -172,7 +157,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "11", "metadata": {}, "source": [ "### 3.2. Search materials by formula" @@ -181,7 +166,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -206,7 +191,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "13", "metadata": {}, "source": [ "### 3.3. Preview materials" @@ -215,7 +200,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -239,7 +224,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "15", "metadata": {}, "source": [ "## 4. Retrieve total energies\n", @@ -250,7 +235,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -305,7 +290,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "17", "metadata": {}, "source": [ "## 5. Build convex hull" @@ -314,7 +299,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -333,7 +318,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "19", "metadata": {}, "source": [ "## 6. Results" @@ -342,7 +327,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -352,7 +337,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "21", "metadata": {}, "source": [ "## 7. Plot" @@ -361,7 +346,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -374,7 +359,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "23", "metadata": {}, "outputs": [], "source": [] From eb9c95caac376b2208bc4aee82346a4e464f2a1a Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 1 Jul 2026 17:31:01 -0700 Subject: [PATCH 9/9] update: intro nb --- other/materials_designer/workflows/Introduction.ipynb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/other/materials_designer/workflows/Introduction.ipynb b/other/materials_designer/workflows/Introduction.ipynb index 2f78cebe..ef786965 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", @@ -71,8 +71,12 @@ "### 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",